Compare commits

..

9 Commits

Author SHA1 Message Date
55a17bb287 Add Cisco AnyConnect installer to build
Some checks failed
Test / test (push) Has been cancelled
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 08:47:49 +00:00
5ed8f84419 Update scripts to use dynamic username instead of hardcoded alexz
Some checks failed
Test / test (push) Has been cancelled
- setup-autologin-sshd.ps1: Uses $env:USERNAME, prompts for password
- setup-ssh-keys.ps1: Uses $env:USERNAME, prompts for public key
- install-nodejs.ps1: Uses $env:USERNAME in instructions
- vpn-login.js: Uses process.env.USERPROFILE for paths

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 08:44:33 +00:00
069ad3880f Fix cistech-tunnel version to semantic format
Some checks failed
Test / test (push) Has been cancelled
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 08:37:43 +00:00
dca864e77f Fix version to semantic format
Some checks failed
Test / test (push) Has been cancelled
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 08:37:09 +00:00
fffa65178f Use pre-built rego-windows image instead of build directive
Some checks failed
Test / test (push) Has been cancelled
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 08:35:53 +00:00
68225e8100 Add full dockurr/windows source with rego customizations
Some checks failed
Test / test (push) Has been cancelled
- Includes complete dockurr/windows source (not just FROM image)
- Added openssh-client and sshpass to Dockerfile
- Added SSH key for Windows VM access
- Added VPN automation scripts (vpn-login.js, socks5.js, vpn.bat)
- Added Windows setup scripts (install-nodejs.ps1, setup-autologin-sshd.ps1, setup-ssh-keys.ps1)
- Added rego-startup.sh for script deployment and network setup
- Scripts auto-copy to shared folder on container start

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 08:34:48 +00:00
a79a5b11f6 Add build directive to docker-compose for rego-tunnel
Some checks failed
Test / test (push) Has been cancelled
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 08:30:16 +00:00
89ea16a43f Add build files with SSH client, VPN scripts, and auto-setup
Some checks failed
Test / test (push) Has been cancelled
Includes:
- Dockerfile extending dockurr/windows with openssh-client
- SSH key for Windows VM access
- Startup script for network setup and script deployment
- VPN automation scripts (vpn-login.js, socks5.js, vpn.bat)
- Windows setup scripts (install-nodejs.ps1, setup-autologin-sshd.ps1, setup-ssh-keys.ps1)
- Technical README

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 08:29:09 +00:00
d38fada941 Use custom rego-windows image with SSH client
Some checks failed
Test / test (push) Has been cancelled
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 08:26:26 +00:00
260 changed files with 21419 additions and 36098 deletions

45
.github/workflows/renovate.yml vendored Executable file
View File

@@ -0,0 +1,45 @@
name: Renovate
on:
workflow_dispatch:
inputs:
log_level:
type: choice
description: Log level
default: INFO
options:
- DEBUG
- INFO
- WARN
- ERROR
- FATAL
schedule:
- cron: 0 2 * * *
jobs:
renovate:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install node
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Install bun
uses: oven-sh/setup-bun@v2
- name: Cache Bun global packages
uses: actions/cache@v4
with:
path: ~/.bun/install/global
key: ${{ runner.os }}-bun-global-renovate-40
restore-keys: |
${{ runner.os }}-bun-global-
- name: Install Renovate
run: bun install -g renovate@40
- name: Run renovate
run: LOG_LEVEL=${{ github.event.inputs.log_level || 'INFO' }} renovate --token ${{ secrets.GITHUB_TOKEN }} ${{ github.repository }}

23
.github/workflows/test.yml vendored Executable file
View File

@@ -0,0 +1,23 @@
name: Test
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install
- name: Run tests
run: bun test

3
.gitignore vendored
View File

@@ -1,4 +1 @@
node_modules/ node_modules/
# Never commit SSH bundles / secrets
**/ssh.zip

View File

@@ -1,42 +0,0 @@
# Runtipi Development Guidelines
## App-Specific Context Files
## Deployment Workflow
**IMPORTANT:** Always follow this workflow when deploying changes to runtipi apps:
1. Push changes to the repo:
```bash
cd /etc/runtipi/repos/runtipi
git add . && git commit -m "message" && git push
```
2. Update the appstore to pull latest changes:
```bash
sudo runtipi-cli appstore update
```
3. Stop the app:
```bash
sudo runtipi-cli app stop <APP_NAME>:runtipi
```
4. Start the app:
```bash
sudo runtipi-cli app start <APP_NAME>:runtipi
```
**NEVER** use `docker compose up/down` or `docker run` directly for runtipi apps unless debugging.
## App Naming Convention
Apps require the `:runtipi` namespace suffix when using runtipi-cli commands:
- `rego-tunnel-linux:runtipi`
- `cistech-tunnel:runtipi`
## Directory Structure
- `/etc/runtipi/repos/runtipi/apps/` - App definitions (config.json, docker-compose.yml, docker-compose.json, source/)
- `/etc/runtipi/app-data/` - Runtime app data (managed by runtipi)
- `/etc/runtipi/user-config/` - User configuration overrides

View File

@@ -1,56 +0,0 @@
{
"name": "Apricot Blossom",
"id": "apricot-blossom",
"available": true,
"port": 3000,
"exposable": true,
"dynamic_config": true,
"short_desc": "AI avatar platform for preserving memories and voices of loved ones",
"author": "alexz",
"source": "https://gits.alexzaw.dev/alexz/runtipi",
"website": "",
"tipi_version": 1,
"version": "1.0.0",
"categories": ["ai", "utilities"],
"supported_architectures": ["arm64", "amd64"],
"min_tipi_version": "4.0.0",
"created_at": 1745107200000,
"updated_at": 1745107200000,
"form_fields": [
{
"type": "password",
"label": "Anthropic API Key",
"hint": "Your Anthropic API key (starts with sk-ant-)",
"required": true,
"env_variable": "ANTHROPIC_API_KEY"
},
{
"type": "password",
"label": "ElevenLabs API Key",
"hint": "Your ElevenLabs API key for voice synthesis",
"required": true,
"env_variable": "ELEVENLABS_API_KEY"
},
{
"type": "text",
"label": "ElevenLabs Voice ID",
"hint": "The voice ID to use for speech synthesis",
"required": true,
"env_variable": "ELEVENLABS_VOICE_ID"
},
{
"type": "text",
"label": "Supabase URL",
"hint": "Your Supabase project URL (e.g. https://xxxx.supabase.co)",
"required": true,
"env_variable": "NEXT_PUBLIC_SUPABASE_URL"
},
{
"type": "password",
"label": "Supabase Anon Key",
"hint": "Your Supabase anonymous/public key",
"required": true,
"env_variable": "NEXT_PUBLIC_SUPABASE_ANON_KEY"
}
]
}

View File

@@ -1,35 +0,0 @@
{
"schemaVersion": 2,
"$schema": "https://schemas.runtipi.io/dynamic-compose.json",
"services": [
{
"name": "apricot-blossom",
"image": "git.alexzaw.dev/alexz/apricot-blossom:latest",
"isMain": true,
"internalPort": 3000,
"environment": [
{ "key": "NODE_ENV", "value": "production" },
{ "key": "NEXT_TELEMETRY_DISABLED", "value": "1" },
{ "key": "TZ", "value": "${TZ}" },
{ "key": "ANTHROPIC_API_KEY", "value": "${ANTHROPIC_API_KEY}" },
{ "key": "ELEVENLABS_API_KEY", "value": "${ELEVENLABS_API_KEY}" },
{ "key": "ELEVENLABS_VOICE_ID", "value": "${ELEVENLABS_VOICE_ID}" },
{ "key": "NEXT_PUBLIC_SUPABASE_URL", "value": "${NEXT_PUBLIC_SUPABASE_URL}" },
{ "key": "NEXT_PUBLIC_SUPABASE_ANON_KEY", "value": "${NEXT_PUBLIC_SUPABASE_ANON_KEY}" }
],
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data",
"containerPath": "/app/data",
"readOnly": false
}
],
"healthCheck": {
"test": "wget --no-verbose --tries=1 --spider http://127.0.0.1:3000/ || exit 1",
"interval": "30s",
"timeout": "10s",
"retries": 3
}
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -1,34 +0,0 @@
# be9700-api — Build Notes
## Image
`git.alexzaw.dev/alexz/be9700-api:<tag>`
## Source
`/home/alexz/be9700-api` (also mirrored to `https://git.alexzaw.dev/alexz/be9700-api`)
The Dockerfile lives in the root of the source directory alongside `server.js`.
## Architecture
- **amd64 only** — the app is pure Node.js (no native addons) but the image is
only built and tested on the amd64 host. Add `--platform linux/arm64` to the
build command and push a separate tag to add arm64 support.
## Security
`.dockerignore` excludes `.env`, `.env.*`, `.git`, `scripts/`, `*.log`, `*.curl`,
`network-captures.js`. The image contains no router credentials.
## Build
```bash
# From this directory:
bash build.sh
# Or with overrides:
IMAGE_TAG=1.0.1 bash build.sh --no-cache
```
Requires the Docker daemon to be authenticated to `git.alexzaw.dev`.

View File

@@ -1,26 +0,0 @@
#!/bin/bash
# Build and push the be9700-api Docker image from the source in /home/alexz/be9700-api.
# The Dockerfile lives alongside the source (not in this build/ dir).
set -euo pipefail
IMAGE_NAME="${IMAGE_NAME:-git.alexzaw.dev/alexz/be9700-api}"
IMAGE_TAG="${IMAGE_TAG:-1.0.0}"
SOURCE_DIR="${SOURCE_DIR:-/home/alexz/be9700-api}"
echo "==> Building ${IMAGE_NAME}:${IMAGE_TAG}"
docker build "$@" \
-t "${IMAGE_NAME}:${IMAGE_TAG}" \
-t "${IMAGE_NAME}:latest" \
"${SOURCE_DIR}"
echo "==> Pushing ${IMAGE_NAME}:${IMAGE_TAG}"
docker push "${IMAGE_NAME}:${IMAGE_TAG}"
echo "==> Pushing ${IMAGE_NAME}:latest"
docker push "${IMAGE_NAME}:latest"
echo ""
echo "Done. To update on Runtipi:"
echo " cd /etc/runtipi/repos/runtipi && sudo git add . && sudo git commit -m 'be9700-api: bump' && sudo git push origin main"
echo " sudo /etc/runtipi/runtipi-cli appstore update"
echo " sudo /etc/runtipi/runtipi-cli app update be9700-api:runtipi"

View File

@@ -1,21 +0,0 @@
{
"$schema": "https://schemas.runtipi.io/app-info.json",
"name": "BE9700 Router API",
"id": "be9700-api",
"available": true,
"short_desc": "Stateless proxy + Scalar API docs for the TP-Link BE9700 WiFi 7 router",
"author": "alexz",
"port": 8347,
"categories": ["network", "utilities"],
"description": "A stateless Node.js proxy server that exposes the TP-Link BE9700 WiFi 7 router's internal API as a clean REST surface with interactive Scalar docs at /docs. Credentials are never stored — callers pass X-Router-Host and X-Router-Password (or X-Router-Session) per request. Covers 203 endpoints: wireless bands (2.4 GHz / 5 GHz / 6 GHz), LAN/DHCP, NAT, UPnP, time, and diagnostics.",
"tipi_version": 1,
"version": "1.1.0",
"source": "https://git.alexzaw.dev/alexz/be9700-api",
"website": "https://git.alexzaw.dev/alexz/be9700-api",
"exposable": true,
"dynamic_config": true,
"no_gui": false,
"form_fields": [],
"supported_architectures": ["amd64"],
"min_tipi_version": "4.0.0"
}

View File

@@ -1,24 +0,0 @@
{
"$schema": "https://schemas.runtipi.io/dynamic-compose.json",
"schemaVersion": 2,
"services": [
{
"name": "be9700-api",
"image": "git.alexzaw.dev/alexz/be9700-api:1.1.0",
"isMain": true,
"internalPort": 3000,
"environment": [
{ "key": "PORT", "value": "3000" },
{ "key": "TZ", "value": "${TZ}" },
{ "key": "NODE_ENV", "value": "production" }
],
"healthCheck": {
"test": "wget -qO- http://127.0.0.1:3000/ >/dev/null 2>&1 || exit 1",
"interval": "30s",
"timeout": "5s",
"retries": 3,
"startPeriod": "15s"
}
}
]
}

View File

@@ -1,42 +0,0 @@
# BE9700 Router API
A stateless Node.js proxy and interactive API documentation server for the
**TP-Link BE9700 WiFi 7** router.
## What it does
- Exposes the router's internal API as a clean, resource-oriented REST surface
(`/api/*`) covering **203 endpoints** across wireless bands (2.4 GHz / 5 GHz /
6 GHz), LAN/DHCP, NAT, UPnP, system time, and diagnostics.
- Serves **interactive Scalar API docs** at `/docs` — explore and try every
endpoint directly in the browser.
- Returns the OpenAPI 3.1 specification at `/openapi.json` for use with any
compatible tooling.
- Health/status JSON at `/`.
## Stateless header-auth model
No router credentials are stored in the container or in Runtipi form fields.
Every request to `/api/*` must carry:
| Header | Purpose |
|--------|---------|
| `X-Router-Host` | Router IP or hostname (e.g. `192.168.0.1`) |
| `X-Router-Password` | Router admin password (for fresh sessions) |
| `X-Router-Session` | Reuse a session token returned by a previous call (JSON) |
The container holds no stored secrets, but the `/api/*` surface controls your
router and CORS is open — anyone who can reach the service and supply a valid
router password can change router settings. Keep this app LAN-only unless you
place an auth middleware (e.g. Traefik forward-auth / Authentik) in front of it
before exposing it to the internet.
## Quick start
Once installed, open the **Scalar docs** at `http://<your-server>:8347/docs`
and authenticate each request with the `X-Router-Host` and
`X-Router-Password` headers.
## Source
`https://git.alexzaw.dev/alexz/be9700-api`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -1,3 +0,0 @@
# Anything Docker / build temp products created in this dir during a build.
*.tar
*.tar.gz

View File

@@ -1,66 +0,0 @@
# cfddns — Build Files
The Docker image for this app is **not built from this directory**. The
Dockerfile and Go source live in a separate Gitea-hosted fork at
[`git.alexzaw.dev/alexz/cloudflare-ddns`](https://git.alexzaw.dev/alexz/cloudflare-ddns)
(hard fork of [`favonia/cloudflare-ddns`](https://github.com/favonia/cloudflare-ddns)
with an added `cmd/manager` binary that wraps the original `ddns` updater
with a web UI for managing the domain list).
The published image is `git.alexzaw.dev/alexz/cloudflare-ddns:latest`.
## What the image contains
- `/bin/ddns` — the unmodified upstream Cloudflare DDNS updater.
- `/bin/manager` — the wrapper: web UI (HTTP basic auth) on port 8080, YAML
config at `/config/cfddns.yaml`, supervises `ddns` as a subprocess and
restarts it cleanly on config changes.
- Base: `alpine:3.23` (needed because the Runtipi healthcheck shells out to
`wget --spider``scratch` has no userland).
- Multi-arch: currently `linux/amd64` only. Add `linux/arm64` by extending
the buildx build with `--platform`.
## Building
```bash
cd /etc/runtipi/repos/runtipi/apps/cfddns/build
./build.sh
```
This clones (or updates) the fork into `/tmp/cfddns-build`, runs `docker
build`, and pushes the resulting image to
`git.alexzaw.dev/alexz/cloudflare-ddns:latest`. Docker push credentials must
already be present in `/root/.docker/config.json` (the script runs with
`sudo`).
To target a different tag or registry:
```bash
IMAGE_TAG=v1-ui2 ./build.sh
IMAGE_NAME=git.alexzaw.dev/alexz/cfddns IMAGE_TAG=test ./build.sh
```
To pass extra flags to `docker build` (e.g. `--no-cache`):
```bash
./build.sh --no-cache
```
## After building
Follow the standard custom-app deploy chain:
```bash
cd /etc/runtipi/repos/runtipi
sudo git push origin main # only if app definition changed
cd /etc/runtipi
sudo ./runtipi-cli appstore update
sudo ./runtipi-cli app update cfddns:runtipi
```
The new image is pulled automatically by `app update`.
## Source
- Fork: <https://git.alexzaw.dev/alexz/cloudflare-ddns>
- Upstream: <https://github.com/favonia/cloudflare-ddns>

View File

@@ -1,39 +0,0 @@
#!/bin/bash
# Build and push the cfddns Docker image.
# The actual Dockerfile + Go source lives in a separate Gitea repo:
# https://git.alexzaw.dev/alexz/cloudflare-ddns
# This script clones (or updates) that repo into /tmp/cfddns-build and runs
# `docker build` + `docker push`. Any positional args are forwarded to
# `docker build` (e.g. `./build.sh --no-cache`).
set -euo pipefail
IMAGE_NAME="${IMAGE_NAME:-git.alexzaw.dev/alexz/cloudflare-ddns}"
IMAGE_TAG="${IMAGE_TAG:-latest}"
SOURCE_REPO="${SOURCE_REPO:-ssh://git@git.alexzaw.dev:222/alexz/cloudflare-ddns.git}"
WORKDIR="${WORKDIR:-/tmp/cfddns-build}"
echo "Building ${IMAGE_NAME}:${IMAGE_TAG} from ${SOURCE_REPO}..."
if [ -d "${WORKDIR}/.git" ]; then
echo "Refreshing existing clone at ${WORKDIR}..."
git -C "${WORKDIR}" fetch --all --tags
git -C "${WORKDIR}" reset --hard origin/main
else
echo "Cloning ${SOURCE_REPO} -> ${WORKDIR}..."
rm -rf "${WORKDIR}"
git clone "${SOURCE_REPO}" "${WORKDIR}"
fi
cd "${WORKDIR}"
sudo docker build "$@" -t "${IMAGE_NAME}:${IMAGE_TAG}" .
sudo docker push "${IMAGE_NAME}:${IMAGE_TAG}"
echo ""
echo "Built and pushed ${IMAGE_NAME}:${IMAGE_TAG}"
echo ""
echo "Next: deploy via runtipi-cli"
echo " cd /etc/runtipi"
echo " sudo ./runtipi-cli appstore update"
echo " sudo ./runtipi-cli app update cfddns:runtipi"

View File

@@ -1,87 +0,0 @@
{
"$schema": "../app-info-schema.json",
"name": "Cloudflare DDNS",
"id": "cfddns",
"available": true,
"port": 8533,
"exposable": true,
"dynamic_config": true,
"no_gui": false,
"tipi_version": 8,
"version": "v1-ui1",
"categories": [
"network",
"utilities"
],
"description": "Automatically updates Cloudflare DNS records with your current public IP. Manage domains, proxied state, schedule, API token, and view live status & logs from a built-in web UI. Fork of favonia/cloudflare-ddns with an added management binary.",
"short_desc": "Dynamic DNS for Cloudflare, with web UI",
"author": "alexz (fork of favonia/cloudflare-ddns)",
"source": "https://git.alexzaw.dev/alexz/cloudflare-ddns",
"website": "https://github.com/favonia/cloudflare-ddns",
"form_fields": [
{
"type": "text",
"label": "Web UI Username",
"env_variable": "CFDDNS_WEB_USER",
"required": false,
"default": "admin",
"min": 3,
"max": 50,
"hint": "Username for HTTP basic auth on the management UI. Leave blank to disable auth (the manager will log a WARN)."
},
{
"type": "random",
"label": "Web UI Password",
"env_variable": "CFDDNS_WEB_PASSWORD",
"required": false,
"min": 24,
"encoding": "hex",
"hint": "Auto-generated on first install. View in the app's Settings tab. Leave blank to disable auth (manager logs a WARN)."
},
{
"type": "password",
"label": "Cloudflare API Token",
"env_variable": "CLOUDFLARE_API_TOKEN",
"required": true,
"hint": "API token with Zone:DNS:Edit permissions. After first start the UI manages this."
},
{
"type": "text",
"label": "Initial Domains (comma-separated)",
"env_variable": "DOMAINS",
"required": false,
"hint": "First-run seed only — managed by the web UI afterward. e.g. home.example.com,vpn.example.com"
},
{
"type": "boolean",
"label": "Initial Proxied (Orange Cloud)",
"env_variable": "PROXIED",
"required": false,
"default": false,
"hint": "First-run seed only — proxy state is managed per-domain by the UI afterward"
},
{
"type": "text",
"label": "Initial Update Schedule",
"env_variable": "UPDATE_CRON",
"required": false,
"default": "@every 5m",
"hint": "First-run seed only — managed by the UI afterward. Cron expression or @every Xm."
},
{
"type": "text",
"label": "Initial Timezone",
"env_variable": "TZ",
"required": false,
"default": "UTC",
"hint": "First-run seed only — managed by the UI afterward"
}
],
"supported_architectures": [
"amd64"
],
"created_at": 1736974800000,
"updated_at": 1747555200000,
"deprecated": false,
"min_tipi_version": "4.5.0"
}

View File

@@ -1,37 +0,0 @@
{
"schemaVersion": 2,
"$schema": "https://schemas.runtipi.io/dynamic-compose.json",
"services": [
{
"name": "cfddns",
"image": "git.alexzaw.dev/alexz/cloudflare-ddns:latest",
"isMain": true,
"internalPort": 8080,
"environment": [
{ "key": "CFDDNS_LISTEN", "value": ":8080" },
{ "key": "CFDDNS_CONFIG_FILE", "value": "/config/cfddns.yaml" },
{ "key": "CFDDNS_WEB_USER", "value": "${CFDDNS_WEB_USER}" },
{ "key": "CFDDNS_WEB_PASSWORD", "value": "${CFDDNS_WEB_PASSWORD}" },
{ "key": "CLOUDFLARE_API_TOKEN", "value": "${CLOUDFLARE_API_TOKEN}" },
{ "key": "DOMAINS", "value": "${DOMAINS:-}" },
{ "key": "PROXIED", "value": "${PROXIED:-false}" },
{ "key": "UPDATE_CRON", "value": "${UPDATE_CRON:-@every 5m}" },
{ "key": "TZ", "value": "${TZ:-UTC}" }
],
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data",
"containerPath": "/config",
"readOnly": false
}
],
"healthCheck": {
"test": "wget --spider -q http://127.0.0.1:8080/healthz || exit 1",
"interval": "30s",
"timeout": "5s",
"retries": 3,
"startPeriod": "10s"
}
}
]
}

View File

@@ -1,26 +0,0 @@
services:
cfddns:
image: 'git.alexzaw.dev/alexz/cloudflare-ddns:latest'
environment:
- 'CFDDNS_LISTEN=:8080'
- 'CFDDNS_CONFIG_FILE=/config/cfddns.yaml'
- 'CFDDNS_WEB_USER=${CFDDNS_WEB_USER}'
- 'CFDDNS_WEB_PASSWORD=${CFDDNS_WEB_PASSWORD}'
- 'CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}'
- 'DOMAINS=${DOMAINS:-}'
- 'PROXIED=${PROXIED:-false}'
- 'UPDATE_CRON=${UPDATE_CRON:-@every 5m}'
- 'TZ=${TZ:-UTC}'
volumes:
- '${APP_DATA_DIR}/data:/config'
healthcheck:
test: ['CMD-SHELL', 'wget --spider -q http://127.0.0.1:8080/healthz || exit 1']
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
x-runtipi:
is_main: true
internal_port: 8080
x-runtipi:
schema_version: 2

View File

@@ -1,39 +0,0 @@
# Cloudflare DDNS
Keeps your Cloudflare DNS A/AAAA records in sync with the public IP address of this server, with a web management UI for managing the domain list and settings.
This is a private fork of [favonia/cloudflare-ddns](https://github.com/favonia/cloudflare-ddns) with an added `manager` binary that wraps the upstream updater. The manager:
- Loads configuration from `cfddns.yaml` in the app data dir.
- Exposes a web UI (HTTP basic auth) for adding, editing, and removing domain entries, toggling proxy state per domain, editing the Cloudflare API token, and viewing live status and logs.
- Supervises the upstream `ddns` binary as a child process and restarts it cleanly when the configuration changes.
## First install
On first start the manager seeds `cfddns.yaml` from the `DOMAINS`, `PROXIED`, `UPDATE_CRON`, `TZ`, and `CLOUDFLARE_API_TOKEN` env vars (set via the install form). After that the YAML file is the source of truth and the env vars are ignored.
If both `DOMAINS` and `CLOUDFLARE_API_TOKEN` are empty at first start, the manager will not launch the updater — it logs a warning and waits for you to configure domains via the web UI.
## Web UI
Open the app from the Runtipi dashboard. Log in with the username/password you set during install (`CFDDNS_WEB_USER` / `CFDDNS_WEB_PASSWORD`).
The UI shows:
- Current public IPv4/IPv6 (scraped from updater output)
- Last update time and status
- Updater process state and PID
- Domain table with add / edit / delete actions
- Per-domain proxied checkbox
- Cron schedule, timezone, and API token editor
- Live tail of the updater log
- "Refresh now" button that triggers an immediate update
## Storage
Configuration: `${APP_DATA_DIR}/data/cfddns.yaml` (mode 0600 — contains your API token).
## Source
- Fork: https://git.alexzaw.dev/alexz/cloudflare-ddns
- Upstream: https://github.com/favonia/cloudflare-ddns

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

View File

@@ -0,0 +1,11 @@
# Required
OC_URL=https://vpn.cistech.net/Employees
OC_SERVERCERT=pin-sha256:HyHob3LiVmIp8ch9AzHJ9jMYqI43tO5N13oWeBLiZ/0=
# Optional
OC_AUTHGROUP=
OC_SSO_ARGS=--browser-display-mode shown
VNC_PASSWORD=vpnSSO12
NOVNC_PORT=6901
PUBLISH_ADDR=0.0.0.0
SSH_KEY_PATH=/home/alexz/.ssh/id_ed25519-lenovo

42
apps/cistech-tunnel/README.md Executable file
View File

@@ -0,0 +1,42 @@
# Cistech Tunnel
OpenConnect-SSO VPN client running in a container with noVNC for browser-based access.
## Features
- **OpenConnect-SSO**: Cisco AnyConnect VPN with SSO/SAML authentication
- **TOTP Support**: Automatic 2FA via keyring integration
- **Auto-reconnect**: Automatically reconnects on disconnection
- **noVNC**: Browser-based VNC access on port 6902
- **NAT/Masquerade**: Routes traffic through VPN tunnel
- **Cloudflared**: Optional Cloudflare tunnel support
- **SSH Tunnels**: Optional SSH port forwarding
## Runtipi Installation
1. Install from the app store or custom repo
2. Configure the required environment variables
3. Start the app via Runtipi dashboard
## First-time SSO Login
1. Open noVNC at `http://<host>:6902`
2. Enter VNC password
3. Complete SSO login in the browser window
4. VPN will connect and auto-reconnect on disconnect
## Source Files
- `source/Dockerfile`: Container build file
- `source/entrypoint.sh`: Container entrypoint with auto-reconnect
## Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| OC_URL | Yes | VPN server URL |
| OC_SERVERCERT | Yes | Server certificate pin |
| OC_USER | No | Username (enables hidden browser mode) |
| VNC_PASSWORD | Yes | noVNC access password |
| OC_TOTP_SECRET | No | TOTP secret for auto 2FA |
| NOVNC_PORT | No | noVNC port (default: 6901) |

View File

@@ -1,2 +0,0 @@
# Large binary files - track tar.gz but not 7z
*.7z

View File

@@ -1,110 +0,0 @@
FROM ubuntu:22.04
LABEL maintainer="alexz"
LABEL description="OpenConnect-SSO VPN in Docker with noVNC"
LABEL version="1.0.0"
ENV DEBIAN_FRONTEND=noninteractive
ENV container=docker
# VNC/noVNC settings
ENV DISPLAY=:1
ENV VNC_PORT=5901
ENV NOVNC_PORT=6092
# Python/Playwright settings
ENV VIRTUAL_ENV=/opt/venv
ENV PATH=/opt/venv/bin:$PATH
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
# Install system dependencies
RUN apt-get update && apt-get install -y \
# Core tools
openconnect \
vpnc-scripts \
iptables \
iproute2 \
iputils-ping \
net-tools \
procps \
curl \
nano \
ca-certificates \
# Python
python3 \
python3-pip \
python3-venv \
# VNC/noVNC (novnc installed from GitHub below)
tigervnc-standalone-server \
tigervnc-common \
websockify \
x11vnc \
xvfb \
# Window manager & terminal
openbox \
fluxbox \
xterm \
# Automation tools
xdotool \
xclip \
oathtool \
# X11/GUI dependencies for browser
dbus \
dbus-x11 \
libgtk-3-0 \
libglib2.0-0 \
libnss3 \
libatk1.0-0 \
libatk-bridge2.0-0 \
libx11-6 \
libx11-xcb1 \
libxcomposite1 \
libxrandr2 \
libgbm1 \
libxdamage1 \
libpango-1.0-0 \
libxkbcommon0 \
libxkbcommon-x11-0 \
fonts-liberation \
# EGL/GL for PyQt6 WebEngine + software rendering
libegl1 \
libgl1 \
libopengl0 \
libdbus-1-3 \
mesa-utils \
libgl1-mesa-dri \
# XCB libraries for Qt6 (complete set)
libxcb1 \
libxcb-cursor0 \
libxcb-icccm4 \
libxcb-image0 \
libxcb-keysyms1 \
libxcb-render0 \
libxcb-render-util0 \
libxcb-shm0 \
libxcb-xfixes0 \
libxcb-xinerama0 \
libxcb-randr0 \
libxcb-glx0 \
libxcb-shape0 \
# sudo needed for openconnect-sso
sudo \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Install noVNC from GitHub (v1.4.0 - stable release with ES6 modules)
RUN curl -fsSL https://github.com/novnc/noVNC/archive/refs/tags/v1.4.0.tar.gz | tar -xz -C /usr/share/ \
&& mv /usr/share/noVNC-1.4.0 /usr/share/novnc \
&& ln -sf /usr/share/novnc/vnc.html /usr/share/novnc/index.html
# Create Python venv and install openconnect-sso with all dependencies
RUN python3 -m venv "$VIRTUAL_ENV" && \
pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir \
'openconnect-sso[full]' \
playwright \
keyring \
keyrings.alt
# Install Playwright browser (Chromium)
RUN python -m playwright install --with-deps chromium

View File

@@ -1,67 +0,0 @@
# Cistech Tunnel - Build Files
This directory contains the Dockerfile and scripts to build the OpenConnect-SSO VPN Docker image.
## Files
- `Dockerfile` - Docker image definition (Ubuntu 22.04 + openconnect-sso + noVNC)
- `build.sh` - Build and push script
- `scripts/entrypoint.sh` - Container entrypoint
## Building
```bash
cd /etc/runtipi/repos/runtipi/apps/cistech-tunnel/build
./build.sh
```
This builds and pushes to `git.alexzaw.dev/alexz/openconnect-vpn:latest`
To build without pushing:
```bash
docker build -t git.alexzaw.dev/alexz/openconnect-vpn:latest .
```
## What's in the image
The Dockerfile creates an image with:
- Ubuntu 22.04
- openconnect + openconnect-sso[full] (Python)
- Playwright Chromium browser (for SSO authentication)
- TigerVNC server + noVNC (web-based VNC)
- Tools: oathtool (TOTP), openbox, xterm
### Scripts (baked in)
- `/opt/scripts/startup-vnc.sh` - Starts VNC server and noVNC
- `/opt/scripts/entrypoint.sh` - Container entrypoint (DNS fix, IP forwarding, config generation)
## Runtime mounts (from shared/)
When running as cistech-tunnel app, these are mounted from `shared/`:
- `/shared/openconnect-vpn` - Main VPN connection script
- `/shared/xstartup` -> `/root/.vnc/xstartup` - VNC session startup
## Environment Variables
| Variable | Description |
|----------|-------------|
| `VPN_EMAIL` | Email/username for SSO login |
| `VPN_PASSWORD` | Password for SSO login |
| `VPN_TOTP_SECRET` | TOTP secret for 2FA (base32) |
| `VPN_HOST` | VPN server URL (e.g., `https://vpn.example.com/Group`) |
| `TARGET_IP` | Target IP for connectivity testing |
| `VNC_PASSWORD` | VNC access password |
## Ports
- `5901` - VNC server
- `6092` - noVNC web interface
## How it works
1. Container starts, generates openconnect-sso config from env vars
2. VNC server starts with noVNC web interface
3. xterm launches with the `openconnect-vpn` script
4. Script sets up keyring with credentials (password + TOTP)
5. openconnect-sso handles SSO authentication via hidden browser
6. VPN connects and IP forwarding/NAT is configured

View File

@@ -1,22 +0,0 @@
#!/bin/bash
# Build and push the OpenConnect-SSO VPN Docker image
# Run this from the build directory
set -euo pipefail
IMAGE_NAME="${IMAGE_NAME:-git.alexzaw.dev/alexz/openconnect-vpn}"
IMAGE_TAG="${IMAGE_TAG:-latest}"
echo "Building ${IMAGE_NAME}:${IMAGE_TAG}..."
docker build "$@" -t "${IMAGE_NAME}:${IMAGE_TAG}" .
docker push "${IMAGE_NAME}:${IMAGE_TAG}"
echo ""
echo "Build complete!"
echo ""
echo "To test locally:"
echo " docker run -d --cap-add=NET_ADMIN --device=/dev/net/tun -p 5901:5901 -p 6092:6092 -e VNC_PASSWORD=changeme -e VPN_HOST=https://vpn.example.com -e VPN_EMAIL=user@example.com ${IMAGE_NAME}:${IMAGE_TAG}"
echo ""
echo "Then connect via VNC to localhost:5901 or open noVNC at http://localhost:6092/vnc.html"
echo ""

View File

@@ -1,76 +1,53 @@
{ {
"name": "cistech Tunnel", "name": "Cistech Tunnel",
"id": "cistech-tunnel",
"available": true, "available": true,
"port": 6092, "short_desc": "Cistech VPN client container with noVNC.",
"author": "alexz",
"port": 6902,
"categories": [
"utilities",
"network"
],
"description": "OpenConnect-SSO VPN running in an isolated namespace with noVNC for first-time SSO reconnects.",
"tipi_version": 1,
"version": "1.0.0",
"source": "local",
"exposable": true, "exposable": true,
"dynamic_config": true, "dynamic_config": true,
"id": "cistech-tunnel", "no_gui": false,
"description": "openconnect-sso in Docker with noVNC web UI for accessing cistech environments. Native Docker - no VM overhead.",
"tipi_version": 7,
"version": "5.1.14.145",
"categories": [
"utilities"
],
"short_desc": "openconnect-sso VPN tunnel to cistech environments (native Docker)",
"author": "alexz",
"source": "https://git.alexzaw.dev/alexz/runtipi",
"form_fields": [ "form_fields": [
{ {
"type": "email", "label": "VPN URL",
"label": "VPN Email",
"hint": "Email address for VPN SSO login (configured in /shared/openconnect-vpn script)",
"placeholder": "your-email@company.com",
"required": false,
"env_variable": "VPN_EMAIL",
"default": ""
},
{
"type": "password",
"label": "VPN Password",
"hint": "Password for VPN SSO login (configured in /shared/openconnect-vpn script)",
"placeholder": "",
"required": false,
"env_variable": "VPN_PASSWORD",
"default": ""
},
{
"type": "text", "type": "text",
"label": "TOTP Secret", "env_variable": "OC_URL",
"hint": "Base32 TOTP secret for 2FA (configured in /shared/openconnect-vpn script)", "required": true,
"placeholder": "", "default": "https://vpn.cistech.net/Employees"
"required": false,
"env_variable": "VPN_TOTP_SECRET",
"default": ""
}, },
{ {
"type": "text",
"label": "VPN Host",
"hint": "VPN server hostname",
"placeholder": "vpn.company.com",
"required": false,
"env_variable": "VPN_HOST",
"default": "vpn.cistech.net/Employees"
},
{
"type": "text",
"label": "Target IP",
"hint": "IP address to route through VPN (e.g., IBM i server)",
"placeholder": "10.3.1.201",
"required": false,
"env_variable": "TARGET_IP",
"default": "10.3.1.201"
},
{
"type": "password",
"label": "VNC Password", "label": "VNC Password",
"hint": "Password for noVNC web interface", "type": "password",
"placeholder": "cisco123",
"required": false,
"env_variable": "VNC_PASSWORD", "env_variable": "VNC_PASSWORD",
"default": "" "required": true,
"default": "Az@83278327$$@@"
},
{
"label": "Server Certificate",
"type": "text",
"env_variable": "OC_SERVERCERT",
"required": true,
"default": "pin-sha256:HyHob3LiVmIp8ch9AzHJ9jMYqI43tO5N13oWeBLiZ/0="
},
{
"label": "Username",
"type": "text",
"env_variable": "OC_USER",
"required": true,
"default": "alex.zaw@cistech.net"
} }
], ],
"supported_architectures": [ "supported_architectures": [
"arm64",
"amd64" "amd64"
] ]
} }

View File

@@ -1,74 +1,23 @@
{ {
"schemaVersion": 2,
"services": [ "services": [
{ {
"name": "cistech-tunnel", "name": "cistech-tunnel",
"image": "git.alexzaw.dev/alexz/openconnect-vpn:latest", "image": "cistech-vpn:latest",
"environment": [
{
"key": "VPN_EMAIL",
"value": "${VPN_EMAIL}"
},
{
"key": "VPN_PASSWORD",
"value": "${VPN_PASSWORD}"
},
{
"key": "VPN_TOTP_SECRET",
"value": "${VPN_TOTP_SECRET}"
},
{
"key": "VPN_HOST",
"value": "${VPN_HOST}"
},
{
"key": "VNC_PASSWORD",
"value": "${VNC_PASSWORD}"
},
{
"key": "TZ",
"value": "${TZ}"
},
{
"key": "TARGET_IP",
"value": "${TARGET_IP}"
}
],
"internalPort": 6092,
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/config",
"containerPath": "/config",
"readOnly": false
},
{
"hostPath": "${APP_DATA_DIR}",
"containerPath": "/runtime",
"readOnly": false
},
{
"hostPath": "/etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared",
"containerPath": "/shared",
"readOnly": false
},
{
"hostPath": "/etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared/xstartup",
"containerPath": "/root/.vnc/xstartup",
"readOnly": true
}
],
"stopGracePeriod": "30s",
"devices": [
"/dev/net/tun"
],
"privileged": true,
"capAdd": [
"NET_ADMIN"
],
"isMain": true, "isMain": true,
"extraLabels": { "internalPort": 6902,
"runtipi.managed": true "privileged": true,
} "capAdd": ["NET_ADMIN"],
"devices": ["/dev/net/tun:/dev/net/tun"],
"environment": {
"OC_URL": "${OC_URL}",
"OC_SERVERCERT": "${OC_SERVERCERT}",
"OC_USER": "${OC_USER}",
"VNC_PASSWORD": "${VNC_PASSWORD}",
"NOVNC_PORT": "6902"
},
"volumes": [
{ "hostPath": "${APP_DATA_DIR}/data", "containerPath": "/root" }
]
} }
] ]
} }

View File

@@ -1,29 +1,34 @@
services: services:
cistech-tunnel: cistech-tunnel:
image: 'git.alexzaw.dev/alexz/openconnect-vpn:latest' image: cistech-vpn:latest
volumes: restart: unless-stopped
- '${APP_DATA_DIR}/config:/config' networks:
- '${APP_DATA_DIR}:/runtime' cistech-tunnel_runtipi_network:
- '/etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared:/shared' gw_priority: 0
- '/etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared/xstartup:/root/.vnc/xstartup:ro' tipi_main_network:
gw_priority: 1
environment: environment:
- 'VPN_EMAIL=${VPN_EMAIL}' OC_URL: ${OC_URL}
- 'VPN_PASSWORD=${VPN_PASSWORD}' OC_SERVERCERT: ${OC_SERVERCERT}
- 'VPN_TOTP_SECRET=${VPN_TOTP_SECRET}' OC_USER: ${OC_USER}
- 'VPN_HOST=${VPN_HOST}' VNC_PASSWORD: ${VNC_PASSWORD}
- 'VNC_PASSWORD=${VNC_PASSWORD}' NOVNC_PORT: "6902"
- 'TZ=${TZ}' ports:
- 'TARGET_IP=${TARGET_IP}' - ${APP_PORT}:6902
volumes:
- ${APP_DATA_DIR}/data:/root
labels: labels:
'runtipi.managed': true generated: true
privileged: true traefik.enable: true
cap_add: traefik.docker.network: runtipi_tipi_main_network
- 'NET_ADMIN' traefik.http.middlewares.cistech-tunnel-runtipi-web-redirect.redirectscheme.scheme: https
stop_grace_period: '30s' traefik.http.services.cistech-tunnel-runtipi.loadbalancer.server.port: "6902"
devices: traefik.http.routers.cistech-tunnel-runtipi-insecure.rule: Host(`${APP_DOMAIN}`)
- '/dev/net/tun' traefik.http.routers.cistech-tunnel-runtipi-insecure.entrypoints: web
x-runtipi: traefik.http.routers.cistech-tunnel-runtipi-insecure.service: cistech-tunnel-runtipi
internal_port: 6092 traefik.http.routers.cistech-tunnel-runtipi-insecure.middlewares: cistech-tunnel-runtipi-web-redirect
is_main: true traefik.http.routers.cistech-tunnel-runtipi.rule: Host(`${APP_DOMAIN}`)
x-runtipi: traefik.http.routers.cistech-tunnel-runtipi.entrypoints: websecure
schema_version: 2 traefik.http.routers.cistech-tunnel-runtipi.service: cistech-tunnel-runtipi
traefik.http.routers.cistech-tunnel-runtipi.tls.certresolver: myresolver
runtipi.managed: true

File diff suppressed because it is too large Load Diff

View File

@@ -1,147 +1,20 @@
# Cistech Tunnel - OpenConnect-SSO VPN # Dockerized OpenConnect-SSO with noVNC and Cloudflared
Docker container running OpenConnect-SSO for Cisco AnyConnect VPN with SSO/SAML authentication support via noVNC. Provides transparent VPN access to protected resources from your LAN. ## Setup
1) Copy `.env.example` to `.env` and fill values (URLs, servercert pins, VNC passwords, cloudflared tokens).
## Features 2) First-time SSO: leave `OC_SSO_ARGS_*=--browser-display-mode visible`.
- **OpenConnect-SSO** - Handles SAML/SSO authentication automatically 3) Build and start:
- **Playwright browser** - Headless Chromium for SSO login docker compose build
- **Web-based access** via noVNC (port 6092) docker compose up -d vpn_a
- **Auto-login with TOTP** - Credentials stored in keyring # Open http://localhost:6901, complete SSO.
- **LAN routing** - Other machines on your network can reach VPN targets # After success, attach app containers or start cloudflared_a.
- **Lightweight** - No systemd, no Cisco bloat
## Architecture 4) Optional: switch to headless after first login:
Set `OC_SSO_ARGS_*=--browser-display-mode hidden` (or `headless`) and restart the vpn service.
``` ## Notes
LAN Devices ──► Linux Host ──► Container (172.30.0.10) ──► VPN Tunnel ──► Target - Each VPN runs in its own net namespace; routes from one cannot affect the other or the host.
│ │ - DNS from the VPN applies within its container namespace and attached services only.
│ └── openconnect-sso + openconnect - Persisted state lives in the named volumes mounted at `/root` (Playwright cache, configs).
│ └── noVNC web UI (port 6092)
└── Host routing service
(routes VPN traffic through container)
```
## Installation
### 1. Install the app through Runtipi
Configure your VPN credentials in app settings:
- VPN Email
- VPN Password
- TOTP Secret (base32)
- VPN Host (e.g., `https://vpn.cistech.net/Employees`)
- Target IP (for connectivity testing)
### 2. Install host routing service (required for LAN access)
**Run this ONCE on the host after app install:**
```bash
/etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared/install-host-services.sh
```
This creates systemd services that route VPN traffic through the container.
### 3. Access the VPN GUI
Open `http://<your-server>:6092/vnc.html`
The VPN will auto-connect using your configured credentials.
## Usage
### Access noVNC
Navigate to port 6092 on your server. The openconnect-vpn script runs automatically and provides a menu:
```
1 - Connect VPN
2 - Disconnect VPN
3 - Show VPN status
4 - Setup IP forwarding
5 - Test connection
6 - Show network status
7 - Show routing table
8 - Setup keyring
q - Quit
```
### Command line options
```bash
# Inside container
openconnect-vpn -c # Connect and exit
openconnect-vpn -d # Disconnect and exit
openconnect-vpn -s # Show status
openconnect-vpn --help # Show all options
```
### View logs
```bash
# Inside container
cat /var/log/openconnect-vpn/$(date +%Y-%m-%d).log
# On host
cat /var/log/cistech-routing.log
```
## LAN Access
After the host routing service is installed, any device on your LAN can reach the VPN target:
1. **From the host:** Works automatically
2. **From other LAN devices:** Add a static route pointing to your host
Example (Windows client):
```cmd
route add 10.3.1.0 mask 255.255.255.0 192.168.0.150 -p
```
Where `192.168.0.150` is your Linux host IP.
## Uninstall
Before removing the app from Runtipi:
```bash
/etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared/uninstall-host-services.sh
```
## Troubleshooting
### VPN not connecting
```bash
# Check logs
docker exec cistech-tunnel cat /var/log/openconnect-vpn/$(date +%Y-%m-%d).log
# Try manual connect
docker exec -it cistech-tunnel /shared/openconnect-vpn -c
```
### VPN connects but can't reach target
```bash
# Check routes inside container
docker exec cistech-tunnel ip route
# Check host routing
ip route | grep <target-ip>
```
### Host routing not working
```bash
# Check watcher service
systemctl status cistech-routing-watcher.path
# Manually trigger routing
touch /etc/runtipi/app-data/runtipi/cistech-tunnel/restart-routing
```
## Technical Details
- **Container IP:** 172.30.0.10 (on br-cistech-vpn bridge)
- **Ports:** 6092 (noVNC), 5901 (VNC)
- **Capabilities:** `NET_ADMIN`, `/dev/net/tun`
- **Log retention:** 7 days (auto-cleanup)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 609 KiB

View File

@@ -1,83 +0,0 @@
#!/bin/bash
# Entrypoint: VNC password setup + DNS fix + start VNC
set -euo pipefail
# Force software rendering (no GPU/OpenGL)
export QT_QUICK_BACKEND=software
export LIBGL_ALWAYS_SOFTWARE=1
export GALLIUM_DRIVER=llvmpipe
export MESA_GL_VERSION_OVERRIDE=3.3
# Qt/Chromium flags for running as root
export QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu --use-gl=swiftshader"
export QTWEBENGINE_DISABLE_SANDBOX=1
# Ensure all shared scripts are executable (permissions may reset after git pull/appstore update)
chmod +x /shared/*.sh /shared/openconnect-vpn /root/.vnc/xstartup 2>/dev/null || true
# Setup TigerVNC password file from env var (passed by runtipi)
if [ -n "${VNC_PASSWORD:-}" ]; then
mkdir -p /root/.vnc
printf '%s\n%s\n' "$VNC_PASSWORD" "$VNC_PASSWORD" | vncpasswd -f > /root/.vnc/passwd
chmod 600 /root/.vnc/passwd
fi
# DNS fix - unmount Docker's read-only mounts
cp /etc/resolv.conf /tmp/resolv.conf.bak 2>/dev/null || true
cp /etc/hosts /tmp/hosts.bak 2>/dev/null || true
umount /etc/resolv.conf 2>/dev/null || true
umount /etc/hosts 2>/dev/null || true
cat /tmp/resolv.conf.bak > /etc/resolv.conf 2>/dev/null || echo "nameserver 8.8.8.8" > /etc/resolv.conf
cat /tmp/hosts.bak > /etc/hosts 2>/dev/null || echo "127.0.0.1 localhost" > /etc/hosts
# Enable IP forwarding
echo 1 > /proc/sys/net/ipv4/ip_forward
echo "[entrypoint] IP forwarding enabled"
# Generate openconnect-sso config from environment variables
mkdir -p /root/.config/openconnect-sso
cat > /root/.config/openconnect-sso/config.toml << EOF
on_disconnect = ""
[default_profile]
address = "${VPN_HOST:-}"
user_group = ""
name = ""
[credentials]
username = "${VPN_EMAIL:-}"
[auto_fill_rules]
[[auto_fill_rules."https://*"]]
selector = "div[id=passwordError]"
action = "stop"
[[auto_fill_rules."https://*"]]
selector = "input[type=email]"
fill = "username"
[[auto_fill_rules."https://*"]]
selector = "input[name=passwd]"
fill = "password"
[[auto_fill_rules."https://*"]]
selector = "input[data-report-event=Signin_Submit]"
action = "click"
[[auto_fill_rules."https://*"]]
selector = "div[data-value=PhoneAppOTP]"
action = "click"
[[auto_fill_rules."https://*"]]
selector = "a[id=signInAnotherWay]"
action = "click"
[[auto_fill_rules."https://*"]]
selector = "input[id=idTxtBx_SAOTCC_OTC]"
fill = "totp"
EOF
echo "[entrypoint] openconnect-sso config generated"
# Start VNC server
exec /shared/startup-vnc.sh

View File

@@ -1,123 +0,0 @@
#!/usr/bin/env bash
#
# Host routing script for cistech-tunnel
# Routes TARGET_IP through the VPN container
#
set -euo pipefail
ACTION="${1:-start}"
# Fixed configuration (we assigned these)
CONTAINER_IP="172.30.0.10"
BRIDGE_NAME="br-cistech-vpn"
TARGET_IP="${TARGET_IP:-10.3.1.0}"
TARGET_SUBNET="$(echo "$TARGET_IP" | cut -d. -f1-3).0/24"
LAN_SUBNET="192.168.0.0/23"
LAN_INTERFACES="eth0 eth1 wlan0"
LOG_FILE="/var/log/cistech-routing.log"
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [cistech-routing] $*"
echo "$msg" | tee -a "$LOG_FILE" >&2
}
get_lan_interface() {
ip route show default | awk '/default/ {for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}' | head -1
}
remove_routes() {
log "Removing stale routes for $TARGET_SUBNET..."
# Remove any existing route to TARGET_SUBNET
ip route del "$TARGET_SUBNET" 2>/dev/null || true
log "Stale routes removed"
}
apply_routes() {
local lan_if
lan_if="$(get_lan_interface)"
log "Applying host routing rules..."
log " Container IP: $CONTAINER_IP"
log " Bridge: $BRIDGE_NAME"
log " Target Subnet: $TARGET_SUBNET"
log " LAN interface: ${lan_if:-unknown}"
# Enable IP forwarding
echo 1 > /proc/sys/net/ipv4/ip_forward
log "IP forwarding enabled"
# Add route to TARGET_SUBNET via container
ip route replace "$TARGET_SUBNET" via "$CONTAINER_IP" dev "$BRIDGE_NAME"
log "Route added: $TARGET_SUBNET via $CONTAINER_IP dev $BRIDGE_NAME"
# Allow forwarding in DOCKER-USER chain for all LAN interfaces
for lan_if in $LAN_INTERFACES; do
# Check if interface exists
if ip link show "$lan_if" &>/dev/null; then
# Allow traffic from LAN to container for TARGET_SUBNET
iptables -C DOCKER-USER -i "$lan_if" -o "$BRIDGE_NAME" -d "$TARGET_SUBNET" -j ACCEPT 2>/dev/null || \
iptables -I DOCKER-USER 1 -i "$lan_if" -o "$BRIDGE_NAME" -d "$TARGET_SUBNET" -j ACCEPT
# Allow return traffic
iptables -C DOCKER-USER -i "$BRIDGE_NAME" -o "$lan_if" -s "$TARGET_SUBNET" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \
iptables -I DOCKER-USER 1 -i "$BRIDGE_NAME" -o "$lan_if" -s "$TARGET_SUBNET" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
log "DOCKER-USER iptables rules added for $lan_if <-> $BRIDGE_NAME"
fi
done
# Masquerade traffic from LAN subnet to VPN bridge (so return traffic routes correctly)
# Use nft since iptables-nft backend doesn't support iptables -t nat commands
if ! nft list chain ip nat POSTROUTING 2>/dev/null | grep -q "saddr $LAN_SUBNET.*oifname.*$BRIDGE_NAME.*masquerade"; then
nft add rule ip nat POSTROUTING ip saddr "$LAN_SUBNET" oifname "$BRIDGE_NAME" counter masquerade
log "NAT masquerade rule added for $LAN_SUBNET -> $BRIDGE_NAME"
else
log "NAT masquerade rule already exists for $LAN_SUBNET -> $BRIDGE_NAME"
fi
log "OK: Host routing applied - $TARGET_SUBNET via $CONTAINER_IP ($BRIDGE_NAME)"
}
remove_all() {
log "Removing all routing rules..."
# Remove route
ip route del "$TARGET_SUBNET" via "$CONTAINER_IP" dev "$BRIDGE_NAME" 2>/dev/null || true
# Remove iptables rules for all LAN interfaces
for lan_if in $LAN_INTERFACES; do
iptables -D DOCKER-USER -i "$lan_if" -o "$BRIDGE_NAME" -d "$TARGET_SUBNET" -j ACCEPT 2>/dev/null || true
iptables -D DOCKER-USER -i "$BRIDGE_NAME" -o "$lan_if" -s "$TARGET_SUBNET" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true
done
# Remove masquerade rule (using nft)
local handle=""
handle=$(nft -a list chain ip nat POSTROUTING 2>/dev/null | grep "saddr $LAN_SUBNET.*oifname.*$BRIDGE_NAME.*masquerade" | grep -oP 'handle \K\d+' | head -1 || true)
if [ -n "$handle" ]; then
nft delete rule ip nat POSTROUTING handle "$handle" 2>/dev/null || true
fi
log "All routing rules removed"
}
case "$ACTION" in
start)
remove_routes
apply_routes
;;
stop)
remove_all
;;
restart)
remove_all
sleep 1
remove_routes
apply_routes
;;
*)
echo "Usage: $0 {start|stop|restart}" >&2
exit 2
;;
esac

View File

@@ -1,55 +0,0 @@
#!/usr/bin/env bash
#
# Install host-side systemd services for cistech-tunnel
# Run this ONCE on the host after installing the app in Runtipi
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APP_DATA_DIR="/etc/runtipi/app-data/runtipi/cistech-tunnel"
echo "Installing cistech-tunnel host services..."
# Create the path watcher unit
cat << 'EOF' | sudo tee /etc/systemd/system/cistech-routing-watcher.path
[Unit]
Description=Watch for cistech-tunnel routing trigger
[Path]
PathExists=/etc/runtipi/app-data/runtipi/cistech-tunnel/restart-routing
Unit=cistech-routing-watcher.service
[Install]
WantedBy=multi-user.target
EOF
# Create the service unit
cat << EOF | sudo tee /etc/systemd/system/cistech-routing-watcher.service
[Unit]
Description=Apply cistech-tunnel routing rules
After=docker.service
[Service]
Type=oneshot
ExecStart=/bin/bash ${SCRIPT_DIR}/host-routing.sh restart
ExecStartPost=/bin/rm -f ${APP_DATA_DIR}/restart-routing
ExecStartPost=/bin/bash -c 'echo "trigger cleared at \$(date)" >> ${APP_DATA_DIR}/watcher.log'
EOF
# Make host-routing.sh executable
sudo chmod +x "${SCRIPT_DIR}/host-routing.sh"
# Reload systemd and enable the watcher
sudo systemctl daemon-reload
sudo systemctl enable --now cistech-routing-watcher.path
echo ""
echo "Done! Services installed:"
echo " - cistech-routing-watcher.path (watches for trigger file)"
echo " - cistech-routing-watcher.service (applies routing rules)"
echo ""
echo "To check status:"
echo " systemctl status cistech-routing-watcher.path"
echo ""
echo "To manually trigger routing:"
echo " touch ${APP_DATA_DIR}/restart-routing"

View File

@@ -1,662 +0,0 @@
#!/bin/bash
# Cistech VPN Connection Script with OpenConnect-SSO
# Usage: ./openconnect-vpn [-c|--connect] [-d|--disconnect] [-m|--menu] [-r|--routes] [-s|--status] [--help]
#
# Options:
# -c, --connect Connect to VPN and exit
# -d, --disconnect Disconnect VPN and exit
# -m, --menu Skip auto-connect, show menu directly
# -r, --routes Show routing table and exit
# -s, --status Show VPN status and exit
# --help Show this help message
# Credentials from environment variables (set by runtipi)
VPN_EMAIL="${VPN_EMAIL:-}"
VPN_PASSWORD="${VPN_PASSWORD:-}"
VPN_TOTP_SECRET="${VPN_TOTP_SECRET:-}"
VPN_HOST="${VPN_HOST:-}"
TARGET_IP="${TARGET_IP:-10.3.1.201}"
TARGET_SUBNET="$(echo "$TARGET_IP" | cut -d. -f1-3).0/24"
VPN_INTERFACE="${VPN_INTERFACE:-tun0}"
CONTAINER_NETWORK="172.30.0.0/24"
# Log directory
LOG_DIR="/var/log/openconnect-vpn"
LOG_RETENTION_DAYS=7
mkdir -p "$LOG_DIR" 2>/dev/null
get_log_file() {
echo "$LOG_DIR/$(date '+%Y-%m-%d').log"
}
cleanup_old_logs() {
find "$LOG_DIR" -name "*.log" -type f -mtime +$LOG_RETENTION_DAYS -delete 2>/dev/null
}
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
GRAY='\033[0;90m'
NC='\033[0m'
print_banner() {
echo -e "${CYAN}========================================${NC}"
echo -e "${CYAN} Cistech VPN Connection Script ${NC}"
echo -e "${CYAN}========================================${NC}"
echo ""
}
# Flags
SKIP_AUTO_CONNECT=false
DO_CONNECT=false
DO_DISCONNECT=false
# Logging
log() {
local level="$1"
local msg="$2"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
local timestamp_short=$(date '+%H:%M:%S')
local log_file=$(get_log_file)
local msg_plain=$(echo -e "$msg" | sed 's/\x1b\[[0-9;]*m//g')
echo "[$timestamp] [$level] $msg_plain" >> "$log_file"
case $level in
INFO) echo -e "${GRAY}[$timestamp_short]${NC} ${GREEN}[INFO]${NC} $msg" ;;
WARN) echo -e "${GRAY}[$timestamp_short]${NC} ${YELLOW}[WARN]${NC} $msg" ;;
ERROR) echo -e "${GRAY}[$timestamp_short]${NC} ${RED}[ERROR]${NC} $msg" ;;
DEBUG) echo -e "${GRAY}[$timestamp_short]${NC} ${CYAN}[DEBUG]${NC} $msg" ;;
CMD) echo -e "${GRAY}[$timestamp_short]${NC} ${GRAY}[CMD]${NC} $msg" ;;
*) echo -e "${GRAY}[$timestamp_short]${NC} $msg" ;;
esac
}
run_cmd() {
local desc="$1"
shift
log CMD "$desc: $*"
output=$("$@" 2>&1)
local rc=$?
if [ -n "$output" ]; then
echo "$output" | while IFS= read -r line; do
echo -e " ${GRAY}│${NC} $line"
done
fi
return $rc
}
show_help() {
echo -e "${CYAN}Cistech VPN Connection Script${NC}"
echo ""
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " -c, --connect Connect to VPN and exit"
echo " -d, --disconnect Disconnect VPN and exit"
echo " -m, --menu Skip auto-connect, show menu directly"
echo " -r, --routes Show routing table and exit"
echo " -s, --status Show VPN status and exit"
echo " --help Show this help message"
echo ""
echo "Menu Options:"
echo " 1 - Connect VPN"
echo " 2 - Disconnect VPN"
echo " 3 - Show VPN status"
echo " 4 - Setup IP forwarding"
echo " 5 - Test connection"
echo " 6 - Show network status"
echo " 7 - Show routing table"
echo " 8 - Show live TOTP"
echo " q - Quit"
}
# Fetch server certificate fingerprint
get_server_cert() {
local host="$1"
local server=$(echo "$host" | sed -E 's|^https?://||; s|/.*$||')
[[ "$server" != *:* ]] && server="${server}:443"
log DEBUG "Fetching server certificate from $server..."
local cert_pin=$(echo | openssl s_client -connect "$server" 2>/dev/null \
| openssl x509 -pubkey -noout 2>/dev/null \
| openssl pkey -pubin -outform der 2>/dev/null \
| openssl dgst -sha256 -binary \
| openssl enc -base64)
if [[ -n "$cert_pin" ]]; then
echo "pin-sha256:${cert_pin}"
else
log WARN "Could not fetch server certificate"
echo ""
fi
}
# Setup keyring with credentials
setup_keyring() {
log INFO "Setting up keyring credentials..."
if [[ -z "$VPN_EMAIL" ]]; then
log ERROR "VPN_EMAIL is not set"
return 1
fi
python3 << PYTHON
import keyring
from keyrings.alt.file import PlaintextKeyring
keyring.set_keyring(PlaintextKeyring())
email = "$VPN_EMAIL"
password = "$VPN_PASSWORD"
totp_secret = "$VPN_TOTP_SECRET"
if password:
keyring.set_password('openconnect-sso', email, password)
print(f"Password stored in keyring for {email}")
if totp_secret:
keyring.set_password('openconnect-sso', f'totp/{email}', totp_secret.upper())
print(f"TOTP secret stored in keyring for {email}")
print("Keyring setup complete")
PYTHON
if [ $? -eq 0 ]; then
log INFO "Keyring credentials configured"
else
log ERROR "Failed to setup keyring"
return 1
fi
}
get_totp() {
oathtool --totp -b "$VPN_TOTP_SECRET"
}
# Test connection to TARGET_IP
test_connection() {
if [[ -z "$TARGET_IP" ]]; then
log WARN "TARGET_IP not set"
return 1
fi
log INFO "Testing connection to $TARGET_IP..."
if ping -c 3 -W 3 "$TARGET_IP" &>/dev/null; then
log INFO "Connection test: ${GREEN}SUCCESS${NC}"
return 0
else
log WARN "Connection test: ${RED}FAILED${NC} (may need manual route on host)"
return 1
fi
}
show_totp() {
log INFO "Starting live TOTP display (Ctrl+C to stop)"
echo ""
while true; do
TOTP=$(get_totp)
SECONDS_LEFT=$((30 - ($(date +%s) % 30)))
echo -ne "\r ${CYAN}Current TOTP:${NC} ${GREEN}$TOTP${NC} (expires in ${YELLOW}${SECONDS_LEFT}s${NC}) "
sleep 1
done
}
get_vpn_interface() {
local iface=$(ip link show | grep -oP 'tun\d+(?=:.*UP)' | head -1)
if [ -z "$iface" ]; then
iface=$(ip link show | grep -oP 'tun\d+' | head -1)
fi
echo "$iface"
}
get_container_ip() {
ip addr show eth0 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1
}
get_vpn_ip() {
local iface=$(get_vpn_interface)
if [ -n "$iface" ]; then
ip addr show "$iface" 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1
fi
}
check_vpn_status() {
local vpn_iface=$(get_vpn_interface)
if [ -n "$vpn_iface" ]; then
local vpn_ip=$(get_vpn_ip)
log INFO "VPN is ${GREEN}CONNECTED${NC}"
log DEBUG " Interface: $vpn_iface"
log DEBUG " VPN IP: $vpn_ip"
return 0
else
log WARN "VPN is ${RED}NOT CONNECTED${NC}"
return 1
fi
}
show_routes() {
echo -e "${CYAN}========================================${NC}"
echo -e "${CYAN} Current Routing Table ${NC}"
echo -e "${CYAN}========================================${NC}"
echo ""
echo -e "${GREEN}IPv4 Routes:${NC}"
ip -4 route show | while IFS= read -r line; do
if echo "$line" | grep -qE "tun[0-9]"; then
echo -e " ${YELLOW}$line${NC}"
elif echo "$line" | grep -qE "10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\."; then
echo -e " ${CYAN}$line${NC}"
else
echo " $line"
fi
done
echo ""
echo -e "${GREEN}VPN Interface:${NC}"
local vpn_iface=$(get_vpn_interface)
if [ -n "$vpn_iface" ]; then
ip addr show "$vpn_iface" 2>/dev/null | grep -E "inet|link" | while IFS= read -r line; do
echo " $line"
done
else
echo -e " ${RED}No VPN interface found${NC}"
fi
}
show_network_status() {
log INFO "Current network status:"
echo ""
log DEBUG "Container Network Interfaces:"
ip -4 addr show | grep -E "inet |^[0-9]+:" | while IFS= read -r line; do
echo -e " ${GRAY}│${NC} $line"
done
echo ""
local vpn_iface=$(get_vpn_interface)
if [ -n "$vpn_iface" ]; then
local vpn_ip=$(get_vpn_ip)
log INFO "VPN Status: ${GREEN}CONNECTED${NC}"
log DEBUG " Interface: $vpn_iface"
log DEBUG " VPN IP: $vpn_ip"
else
log WARN "VPN Status: ${RED}NOT CONNECTED${NC}"
fi
local container_ip=$(get_container_ip)
if [ -n "$container_ip" ]; then
log DEBUG "Container IP: $container_ip"
fi
echo ""
log DEBUG "Default gateway:"
ip route show default | while IFS= read -r line; do
echo -e " ${GRAY}│${NC} $line"
done
echo ""
}
kill_vpn_processes() {
log INFO "Killing VPN processes..."
local killed=0
for pid in $(pgrep -x "openconnect" 2>/dev/null); do
log DEBUG "Killing openconnect (PID $pid)"
kill -9 "$pid" 2>/dev/null && ((killed++))
done
for pid in $(pgrep -f "openconnect-sso" 2>/dev/null); do
log DEBUG "Killing openconnect-sso (PID $pid)"
kill -9 "$pid" 2>/dev/null && ((killed++))
done
if [ $killed -eq 0 ]; then
log INFO "No VPN processes were running"
else
log INFO "Killed $killed process(es)"
sleep 2
fi
}
disconnect_vpn() {
log INFO "Disconnecting VPN..."
kill_vpn_processes
if check_vpn_status; then
log WARN "VPN still appears connected"
return 1
fi
log INFO "VPN disconnected"
return 0
}
setup_forwarding() {
log INFO "Setting up IP forwarding rules for $TARGET_SUBNET..."
local vpn_iface=$(get_vpn_interface)
if [ -z "$vpn_iface" ]; then
log ERROR "No VPN interface found! Is VPN connected?"
return 1
fi
local vpn_ip=$(get_vpn_ip)
local container_ip=$(get_container_ip)
log DEBUG "VPN interface: $vpn_iface"
log DEBUG "VPN IP: $vpn_ip"
log DEBUG "Container IP: $container_ip"
# Enable IP forwarding
run_cmd "Enabling IP forwarding" sysctl -w net.ipv4.ip_forward=1
# NAT masquerade for container network going through VPN
if ! iptables -t nat -C POSTROUTING -s "$CONTAINER_NETWORK" -o "$vpn_iface" -j MASQUERADE 2>/dev/null; then
run_cmd "Adding NAT masquerade for container network -> VPN" iptables -t nat -A POSTROUTING -s "$CONTAINER_NETWORK" -o "$vpn_iface" -j MASQUERADE
else
log DEBUG "NAT masquerade for container network already exists"
fi
# Forward rules at position 1
iptables -D FORWARD -d "$TARGET_SUBNET" -j ACCEPT 2>/dev/null || true
iptables -D FORWARD -s "$TARGET_SUBNET" -j ACCEPT 2>/dev/null || true
iptables -D FORWARD -s "$CONTAINER_NETWORK" -j ACCEPT 2>/dev/null || true
iptables -D FORWARD -d "$CONTAINER_NETWORK" -j ACCEPT 2>/dev/null || true
run_cmd "Inserting forward rule (to container network)" iptables -I FORWARD 1 -d "$CONTAINER_NETWORK" -j ACCEPT
run_cmd "Inserting forward rule (from container network)" iptables -I FORWARD 1 -s "$CONTAINER_NETWORK" -j ACCEPT
run_cmd "Inserting forward rule (from target)" iptables -I FORWARD 1 -s "$TARGET_SUBNET" -j ACCEPT
run_cmd "Inserting forward rule (to target)" iptables -I FORWARD 1 -d "$TARGET_SUBNET" -j ACCEPT
log INFO "Forwarding rules configured"
echo ""
# Trigger host routing service restart
log INFO "Triggering host routing service restart..."
touch /runtime/restart-routing 2>/dev/null || true
sleep 2
if [ ! -f /runtime/restart-routing ]; then
log INFO "Host routing service restarted"
else
log WARN "Host watcher may not be running (trigger file still exists)"
fi
log INFO "Routing configured for $TARGET_SUBNET through VPN tunnel"
echo ""
}
connect_vpn() {
log INFO "=== Starting OpenConnect-SSO VPN ==="
echo ""
# Kill any existing VPN processes
kill_vpn_processes
# Reset DNS to public servers (VPN script may have overwritten it)
log DEBUG "Resetting DNS to public servers..."
echo "nameserver 8.8.8.8" > /etc/resolv.conf
echo "nameserver 1.1.1.1" >> /etc/resolv.conf
# Clean up stale tun interface
ip link delete tun0 2>/dev/null || true
# Validate required variables
if [[ -z "$VPN_HOST" ]]; then
log ERROR "VPN_HOST is not set"
return 1
fi
# Setup keyring credentials
setup_keyring
# Fetch server certificate
log INFO "Fetching server certificate..."
local servercert=$(get_server_cert "$VPN_HOST")
log INFO "Connecting to: $VPN_HOST"
log DEBUG "Interface: $VPN_INTERFACE"
# Build openconnect-sso command
local sso_args=()
sso_args+=("-s" "$VPN_HOST")
if [[ -n "$VPN_EMAIL" ]]; then
sso_args+=("-u" "$VPN_EMAIL")
fi
# Use hidden browser for automated login
sso_args+=("--browser-display-mode" "hidden")
# Build openconnect args
local oc_args=()
oc_args+=("--protocol=anyconnect")
oc_args+=("--interface" "$VPN_INTERFACE")
oc_args+=("--script" "/usr/share/vpnc-scripts/vpnc-script")
if [[ -n "$servercert" ]]; then
oc_args+=("--servercert" "$servercert")
log DEBUG "Server cert: $servercert"
fi
# Launch openconnect-sso
log INFO "Launching openconnect-sso..."
openconnect-sso "${sso_args[@]}" -- /usr/sbin/openconnect "${oc_args[@]}" &
OC_PID=$!
disown $OC_PID
log DEBUG "openconnect-sso started with PID $OC_PID"
# Wait for VPN to connect
log INFO "Waiting for VPN connection..."
local wait_count=0
local max_wait=300
while [ -z "$(get_vpn_interface)" ]; do
sleep 2
((wait_count+=2))
if [ $((wait_count % 10)) -eq 0 ]; then
log DEBUG "Still waiting for VPN... (${wait_count}s)"
fi
if [ $wait_count -ge $max_wait ]; then
log ERROR "Timeout waiting for VPN connection after ${max_wait}s"
return 1
fi
done
log INFO "VPN connected!"
local vpn_iface=$(get_vpn_interface)
local vpn_ip=$(get_vpn_ip)
log DEBUG " Interface: $vpn_iface"
log DEBUG " VPN IP: $vpn_ip"
# Wait for routes to stabilize
log DEBUG "Waiting for routes to stabilize..."
sleep 3
# Setup forwarding
setup_forwarding
# Test connection to IBMI host
test_connection
# Disable screen blanking
xset s off 2>/dev/null || true
xset -dpms 2>/dev/null || true
xset s noblank 2>/dev/null || true
log DEBUG "Screen blanking disabled"
log INFO "VPN setup complete"
# Start watchdog in background
start_watchdog &
WATCHDOG_PID=$!
log DEBUG "Watchdog started with PID $WATCHDOG_PID"
return 0
}
# Watchdog - monitors VPN and reconnects if dropped
start_watchdog() {
log INFO "Starting VPN watchdog (check every 60s, keepalive ping every 5min)..."
local check_interval=60
local keepalive_interval=300
local last_keepalive=0
local reconnect_attempts=0
local max_reconnect_attempts=3
while true; do
sleep $check_interval
local now=$(date +%s)
local vpn_iface=$(get_vpn_interface)
if [ -n "$vpn_iface" ]; then
# VPN is up
reconnect_attempts=0
# Keepalive ping every 5 minutes
if [ $((now - last_keepalive)) -ge $keepalive_interval ]; then
test_connection || log WARN "VPN may be degraded"
last_keepalive=$now
fi
else
# VPN is down
((reconnect_attempts++))
log WARN "VPN disconnected! Reconnect attempt $reconnect_attempts/$max_reconnect_attempts..."
if [ $reconnect_attempts -le $max_reconnect_attempts ]; then
connect_vpn
else
log ERROR "Max reconnect attempts reached. Manual intervention required."
sleep 300
reconnect_attempts=0
fi
fi
done
}
# Main menu
main_menu() {
echo -e "${GREEN}Options:${NC}"
echo -e " ${CYAN}1${NC} - Connect VPN"
echo -e " ${CYAN}2${NC} - Disconnect VPN"
echo -e " ${CYAN}3${NC} - Show VPN status"
echo -e " ${CYAN}4${NC} - Setup IP forwarding only"
echo -e " ${CYAN}5${NC} - Test connection to $TARGET_IP"
echo -e " ${CYAN}6${NC} - Show network status"
echo -e " ${CYAN}7${NC} - Show routing table"
echo -e " ${CYAN}8${NC} - Show live TOTP"
echo -e " ${CYAN}q${NC} - Quit"
echo ""
}
parse_args() {
while [[ $# -gt 0 ]]; do
case $1 in
-c|--connect)
DO_CONNECT=true
shift
;;
-d|--disconnect)
DO_DISCONNECT=true
shift
;;
-m|--menu)
SKIP_AUTO_CONNECT=true
shift
;;
-s|--status)
print_banner
check_vpn_status
echo ""
show_network_status
exit 0
;;
-r|--routes)
show_routes
exit 0
;;
--help)
show_help
exit 0
;;
*)
echo "Unknown option: $1"
echo "Use --help for usage information"
exit 1
;;
esac
done
if [ "$DO_CONNECT" = "true" ] && [ "$DO_DISCONNECT" = "true" ]; then
echo "Error: --connect and --disconnect are mutually exclusive"
exit 1
fi
}
# Main
parse_args "$@"
cleanup_old_logs
echo "" >> "$(get_log_file)"
echo "========================================" >> "$(get_log_file)"
log INFO "=== Starting OpenConnect-SSO VPN ==="
echo ""
log DEBUG "VPN_EMAIL=$VPN_EMAIL"
log DEBUG "VPN_HOST=$VPN_HOST"
log DEBUG "TARGET_IP=$TARGET_IP"
log DEBUG "TARGET_SUBNET=$TARGET_SUBNET"
log DEBUG "VPN_TOTP_SECRET is $([ -n "$VPN_TOTP_SECRET" ] && echo 'set' || echo 'NOT SET')"
print_banner
if [ "$DO_DISCONNECT" = "true" ]; then
disconnect_vpn
exit $?
fi
if [ "$DO_CONNECT" = "true" ]; then
connect_vpn
exit $?
fi
# Auto-connect logic (unless -m flag)
if [ "$SKIP_AUTO_CONNECT" = "true" ]; then
log INFO "Menu mode - skipping auto-connect"
elif check_vpn_status; then
echo ""
log INFO "VPN already connected. Setting up forwarding..."
setup_forwarding
else
echo ""
log INFO "Auto-starting VPN connection..."
echo ""
connect_vpn
fi
# Interactive menu loop
while true; do
echo ""
main_menu
echo -ne "${CYAN}Choice: ${NC}"
read -r choice
echo ""
[[ -z "${choice// }" ]] && continue
case $choice in
1) connect_vpn ;;
2) disconnect_vpn ;;
3) check_vpn_status ;;
4) setup_forwarding ;;
5) test_connection ;;
6) show_network_status ;;
7) show_routes ;;
8) show_totp ;;
q|Q) log INFO "Goodbye!"; exit 0 ;;
*) ;;
esac
done

View File

@@ -1,12 +0,0 @@
#!/bin/bash
set -e
export HOME='/root'
export USER='root'
rm -f /tmp/.P1-lock /tmp/.X11-unix/X1 2>/dev/null || true
rm -rf /tmp/.X*-lock /tmp/.X14-unix/* 2>/dev/null || true
echo "Starting TigerVNC server on display :1..."
vncserver :1 -geometry 1280x800 -depth 24 -SecurityTypes VncAuth -localhost no
sleep 2
echo "Starting noVNC on port ${NOVNC_PORT:-6092}..."
websockify --web=/usr/share/novnc/ ${NOVNC_PORT:-6092} localhost:${VNC_PORT:-5901} &
tail -f /root/.vnc/*.log

View File

@@ -1,24 +0,0 @@
#!/usr/bin/env bash
#
# Uninstall host-side systemd services for cistech-tunnel
#
set -euo pipefail
echo "Removing cistech-tunnel host services..."
# Stop and disable the watcher
sudo systemctl stop cistech-routing-watcher.path 2>/dev/null || true
sudo systemctl disable cistech-routing-watcher.path 2>/dev/null || true
# Remove routing rules
/etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared/host-routing.sh stop 2>/dev/null || true
# Remove systemd units
sudo rm -f /etc/systemd/system/cistech-routing-watcher.path
sudo rm -f /etc/systemd/system/cistech-routing-watcher.service
# Reload systemd
sudo systemctl daemon-reload
echo ""
echo "Done! Host services removed."

View File

@@ -1,53 +0,0 @@
#!/bin/bash
# VNC xstartup - launches terminal with cisco-vpn script
unset SESSION_MANAGER
unset DBUS_SESSION_BUS_ADDRESS
# Import environment variables from container (PID 1)
# Systemd services don't inherit Docker env vars, so we source them here
while IFS= read -r -d '' line; do
export "$line"
done < /proc/1/environ
export XDG_RUNTIME_DIR=/tmp/runtime-root
mkdir -p $XDG_RUNTIME_DIR
chmod 700 $XDG_RUNTIME_DIR
# Force software rendering (no GPU/OpenGL)
export QT_QUICK_BACKEND=software
export LIBGL_ALWAYS_SOFTWARE=1
export GALLIUM_DRIVER=llvmpipe
export MESA_GL_VERSION_OVERRIDE=3.3
# GPU/WebKit workarounds for Cisco UI
export GDK_BACKEND=x11
export WEBKIT_DISABLE_DMABUF_RENDERER=1
# Qt/Chromium flags for running as root (no sandbox)
export QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu"
export QTWEBENGINE_DISABLE_SANDBOX=1
# Start system dbus daemon (needed for Chromium)
mkdir -p /run/dbus
dbus-daemon --system --fork 2>/dev/null || true
# Start dbus session
[ -x /usr/bin/dbus-launch ] && eval $(dbus-launch --sh-syntax --exit-with-session)
# Start window manager
openbox &
sleep 2
# Disable screen blanking and power saving
xset s off 2>/dev/null || true
xset -dpms 2>/dev/null || true
xset s noblank 2>/dev/null || true
# Make script executable and launch in terminal
chmod +x /shared/openconnect-vpn 2>/dev/null || true
xterm -fn fixed -bg black -fg white -geometry 130x45+10+10 \
-title "Cistech VPN Terminal" \
-e "bash -c '/shared/openconnect-vpn; exec bash'" &
wait

View File

@@ -0,0 +1,46 @@
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive \
PLAYWRIGHT_BROWSERS_PATH=/ms-playwright \
VIRTUAL_ENV=/opt/venv \
PATH=/opt/venv/bin:$PATH \
QTWEBENGINE_DISABLE_SANDBOX=1 \
QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu" \
OC_URL="https://vpn.cistech.net/Employees" \
OC_SERVERCERT="pin-sha256:HyHob3LiVmIp8ch9AzHJ9jMYqI43tO5N13oWeBLiZ/0=" \
OC_USER="alex.zaw@cistech.net" \
OC_TOTP_SECRET="t6ypnjqvyx2yvw2l" \
VNC_PASSWORD="Az@83278327\$\$@@"
RUN apt-get update && apt-get install -y \
openconnect iproute2 iptables ca-certificates \
python3 python3-pip python3-venv \
vpnc-scripts curl wget openssh-client \
x11vnc xvfb fluxbox novnc websockify xterm nano oathtool \
xauth libnss3 libatk1.0-0 libatk-bridge2.0-0 \
libx11-6 libx11-xcb1 libxcomposite1 libxrandr2 libgbm1 libxdamage1 \
libpango-1.0-0 fonts-liberation \
libegl1 libgl1 libopengl0 libdbus-1-3 libglib2.0-0 \
libxkbcommon0 libxkbcommon-x11-0 \
libxcb1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-render0 libxcb-render-util0 libxcb-shm0 libxcb-xfixes0 libxcb-xinerama0 libxcb-randr0 libxcb-glx0 \
sudo && rm -rf /var/lib/apt/lists/*
RUN apt-get update && (apt-get install -y libasound2t64 || apt-get install -y libasound2) && rm -rf /var/lib/apt/lists/*
# Python venv + Playwright + openconnect-sso
RUN python3 -m venv "$VIRTUAL_ENV"
RUN pip install --no-cache-dir openconnect-sso playwright keyring keyrings.alt && \
python -m playwright install --with-deps chromium
# Cloudflared (amd64)
RUN arch=$(dpkg --print-architecture) && \
if [ "$arch" = "amd64" ]; then \
curl -fsSL https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -o /tmp/cloudflared.deb && \
apt-get update && apt-get install -y /tmp/cloudflared.deb && rm -f /tmp/cloudflared.deb ; \
else \
echo "Install cloudflared manually for arch=$arch" && exit 1 ; \
fi
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 6901
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -0,0 +1,182 @@
#!/usr/bin/env bash
set -euo pipefail
: "${OC_URL:?OC_URL required}"
: "${OC_SERVERCERT:?OC_SERVERCERT required}"
NOVNC_PORT="${NOVNC_PORT:-6901}"
VNC_PASSWORD="${VNC_PASSWORD:-changeme}"
DISPLAY_ADDR="${DISPLAY:-:1}"
OC_INTERFACE="${OC_INTERFACE:-tun0}"
OC_USER="${OC_USER:-}"
OC_TOTP_SECRET="${OC_TOTP_SECRET:-}"
# Default to hidden browser if OC_USER is set
if [[ -n "$OC_USER" ]]; then
OC_SSO_ARGS_DEFAULT="--browser-display-mode hidden -u $OC_USER"
else
OC_SSO_ARGS_DEFAULT="--browser-display-mode shown"
fi
CLOUDFLARED_MODE="${CLOUDFLARED_MODE:-off}" # off|token|config
CLOUDFLARED_TOKEN="${CLOUDFLARED_TOKEN:-}"
SSH_TUNNEL_ENABLE="${SSH_TUNNEL_ENABLE:-0}"
SSH_DEST="${SSH_DEST:-zawa@10.3.1.201}"
SSH_FORWARDS="${SSH_FORWARDS:-0.0.0.0:8090:localhost:8090}"
pids=()
# Setup keyring with TOTP secret if provided
setup_keyring() {
if [[ -n "$OC_TOTP_SECRET" && -n "$OC_USER" ]]; then
python3 -c "
import keyring
keyring.set_password('openconnect-sso', 'totp/$OC_USER', '$OC_TOTP_SECRET'.upper())
print('TOTP secret stored in keyring for $OC_USER')
"
fi
}
# Create vpn_connect command in PATH and save environment
create_vpn_command() {
# Save environment variables to a file
cat > /etc/vpn.env << ENVFILE
export OC_URL="$OC_URL"
export OC_SERVERCERT="$OC_SERVERCERT"
export OC_INTERFACE="$OC_INTERFACE"
export OC_USER="$OC_USER"
export OC_SSO_ARGS_DEFAULT="$OC_SSO_ARGS_DEFAULT"
export OC_SSO_ARGS="${OC_SSO_ARGS:-$OC_SSO_ARGS_DEFAULT}"
export OC_AUTHGROUP="${OC_AUTHGROUP:-}"
export OC_USERAGENT="${OC_USERAGENT:-}"
export OC_EXTRA_ARGS="${OC_EXTRA_ARGS:-}"
export OC_TOTP_SECRET="$OC_TOTP_SECRET"
export DISPLAY=":1"
ENVFILE
# Build openconnect command
OPENCONNECT_CMD="/usr/sbin/openconnect --protocol=anyconnect --servercert $OC_SERVERCERT --interface $OC_INTERFACE --script /usr/share/vpnc-scripts/vpnc-script"
[[ -n "${OC_AUTHGROUP:-}" ]] && OPENCONNECT_CMD+=" --authgroup $OC_AUTHGROUP"
[[ -n "${OC_USERAGENT:-}" ]] && OPENCONNECT_CMD+=" --useragent $OC_USERAGENT"
[[ -n "${OC_EXTRA_ARGS:-}" ]] && OPENCONNECT_CMD+=" ${OC_EXTRA_ARGS}"
echo "export OPENCONNECT_CMD=\"$OPENCONNECT_CMD\"" >> /etc/vpn.env
cat > /usr/local/bin/vpn_connect << 'VPNCMD'
#!/usr/bin/env bash
source /etc/vpn.env
echo "[$(date)] Starting VPN connection..."
# openconnect-sso reads TOTP from keyring automatically
if [[ -n "$OC_USER" ]]; then
echo "" | openconnect-sso -s "$OC_URL" ${OC_SSO_ARGS:-$OC_SSO_ARGS_DEFAULT} -- $OPENCONNECT_CMD
else
openconnect-sso -s "$OC_URL" ${OC_SSO_ARGS:-$OC_SSO_ARGS_DEFAULT} -- $OPENCONNECT_CMD
fi
VPNCMD
chmod +x /usr/local/bin/vpn_connect
}
# Create VPN runner script that keeps shell open
create_vpn_script() {
cat > /tmp/vpn-runner.sh << 'VPNSCRIPT'
#!/usr/bin/env bash
cd /root
echo "============================================"
echo " Cistech VPN Container"
echo "============================================"
echo ""
echo "Commands:"
echo " vpn_connect - Start/restart VPN connection"
echo " Ctrl+C - Stop auto-reconnect and drop to shell"
echo ""
echo "Starting VPN with auto-reconnect..."
echo ""
while true; do
vpn_connect
echo ""
echo "[$(date)] VPN disconnected. Reconnecting in 10 seconds..."
echo "(Press Ctrl+C to stop auto-reconnect)"
sleep 10
done
VPNSCRIPT
chmod +x /tmp/vpn-runner.sh
}
start_gui() {
mkdir -p /root/.vnc
x11vnc -storepasswd "$VNC_PASSWORD" /root/.vnc/pass >/dev/null 2>&1 || true
rm -f /tmp/.X1-lock /tmp/.X11-unix/X1 2>/dev/null || true
Xvfb "$DISPLAY_ADDR" -screen 0 ${XVFB_WxHxD:-1280x800x24} +extension RANDR &
pids+=($!)
sleep 0.5
export DISPLAY="$DISPLAY_ADDR"
fluxbox >/tmp/fluxbox.log 2>&1 &
pids+=($!)
x11vnc -display "$DISPLAY_ADDR" -rfbauth /root/.vnc/pass -forever -shared -rfbport 5900 -quiet &
pids+=($!)
websockify --web=/usr/share/novnc/ 0.0.0.0:"$NOVNC_PORT" localhost:5900 >/tmp/websockify.log 2>&1 &
pids+=($!)
}
start_vpn_terminal() {
# Start xterm with VPN script
sleep 1
xterm -fa 'Monospace' -fs 11 -bg black -fg white -geometry 120x35+50+50 \
-T "Cistech VPN" -e /tmp/vpn-runner.sh &
pids+=($!)
}
start_cloudflared() {
case "$CLOUDFLARED_MODE" in
token)
[ -n "$CLOUDFLARED_TOKEN" ] && cloudflared tunnel run --token "$CLOUDFLARED_TOKEN" >/tmp/cloudflared.log 2>&1 &
pids+=($!)
;;
config)
cloudflared --no-autoupdate --config /etc/cloudflared/config.yml tunnel run >/tmp/cloudflared.log 2>&1 &
pids+=($!)
;;
off|*)
;;
esac
}
start_ssh_tunnel() {
[ "$SSH_TUNNEL_ENABLE" = "1" ] || return 0
IFS=',' read -ra LINES <<< "$SSH_FORWARDS"
args=(-N -o StrictHostKeyChecking=no -o ServerAliveInterval=60)
for m in "${LINES[@]}"; do args+=(-L "$m"); done
ssh "${args[@]}" "$SSH_DEST" &
pids+=($!)
}
setup_nat() {
(
for i in {1..60}; do
if ip link show "$OC_INTERFACE" >/dev/null 2>&1; then
sysctl -w net.ipv4.ip_forward=1 >/dev/null
iptables -t nat -C POSTROUTING -o "$OC_INTERFACE" -j MASQUERADE 2>/dev/null || \
iptables -t nat -A POSTROUTING -o "$OC_INTERFACE" -j MASQUERADE
echo "NAT enabled on $OC_INTERFACE"
break
fi
sleep 2
done
) &
}
trap 'kill 0' INT TERM
# Always start GUI now
setup_keyring
create_vpn_command
create_vpn_script
start_gui
start_vpn_terminal
setup_nat
start_cloudflared
start_ssh_tunnel
wait

View File

@@ -1,16 +1,46 @@
services: services:
cloudbeaver: cloudbeaver:
image: 'dbeaver/cloudbeaver:25.2.5' image: dbeaver/cloudbeaver:25.2.5
volumes: restart: unless-stopped
- '${APP_DATA_DIR}/data:/opt/cloudbeaver/workspace' networks:
environment: cloudbeaver_runtipi_network:
- 'CB_SERVER_NAME=CloudBeaver' gw_priority: 0
- 'CB_ADMIN_NAME=admin' tipi_main_network:
- 'CB_ADMIN_PASSWORD=admin' gw_priority: 1
extra_hosts: extra_hosts:
- 'host.docker.internal:host-gateway' - host.docker.internal:host-gateway
x-runtipi: environment:
internal_port: 8978 CB_SERVER_NAME: CloudBeaver
is_main: true CB_ADMIN_NAME: admin
x-runtipi: CB_ADMIN_PASSWORD: admin
schema_version: 2 ports:
- ${APP_PORT}:8978
volumes:
- ${APP_DATA_DIR}/data:/opt/cloudbeaver/workspace
labels:
generated: true
traefik.enable: true
traefik.docker.network: runtipi_tipi_main_network
traefik.http.middlewares.cloudbeaver-runtipi-web-redirect.redirectscheme.scheme: https
traefik.http.services.cloudbeaver-runtipi.loadbalancer.server.port: "8978"
traefik.http.routers.cloudbeaver-runtipi-insecure.rule: Host(`${APP_DOMAIN}`)
traefik.http.routers.cloudbeaver-runtipi-insecure.entrypoints: web
traefik.http.routers.cloudbeaver-runtipi-insecure.service: cloudbeaver-runtipi
traefik.http.routers.cloudbeaver-runtipi-insecure.middlewares: cloudbeaver-runtipi-web-redirect
traefik.http.routers.cloudbeaver-runtipi.rule: Host(`${APP_DOMAIN}`)
traefik.http.routers.cloudbeaver-runtipi.entrypoints: websecure
traefik.http.routers.cloudbeaver-runtipi.service: cloudbeaver-runtipi
traefik.http.routers.cloudbeaver-runtipi.tls.certresolver: myresolver
runtipi.managed: true
runtipi.appurn: cloudbeaver:runtipi
networks:
tipi_main_network:
name: runtipi_tipi_main_network
external: true
cloudbeaver_runtipi_network:
name: cloudbeaver_runtipi_network
external: false
ipam:
config:
- subnet: 10.128.18.0/24

View File

@@ -1,83 +0,0 @@
{
"name": "ONLYOFFICE DocSpace",
"id": "docspace",
"available": true,
"short_desc": "Collaborative room-based document management platform with built-in editors",
"author": "ONLYOFFICE",
"port": 8843,
"categories": ["utilities", "data"],
"tipi_version": 1,
"version": "latest",
"source": "https://github.com/ONLYOFFICE/DocSpace",
"website": "https://www.onlyoffice.com/docspace.aspx",
"exposable": true,
"dynamic_config": true,
"supported_architectures": ["amd64"],
"min_tipi_version": "4.0.0",
"form_fields": [
{
"type": "random",
"label": "JWT Secret",
"hint": "Secret key for Document Server JWT authentication",
"env_variable": "DOCSPACE_JWT_SECRET",
"min": 32,
"encoding": "hex"
},
{
"type": "random",
"label": "MySQL Root Password",
"hint": "Root password for the MySQL database",
"env_variable": "DOCSPACE_MYSQL_ROOT_PASSWORD",
"min": 24,
"encoding": "hex"
},
{
"type": "random",
"label": "MySQL User Password",
"hint": "Password for the docspace MySQL user",
"env_variable": "DOCSPACE_MYSQL_PASSWORD",
"min": 24,
"encoding": "hex"
},
{
"type": "random",
"label": "App Machine Key",
"hint": "Core machine key used for encryption and signing",
"env_variable": "DOCSPACE_MACHINEKEY",
"min": 32,
"encoding": "hex"
},
{
"type": "random",
"label": "Identity Encryption Secret",
"hint": "Secret key for identity service encryption",
"env_variable": "DOCSPACE_IDENTITY_SECRET",
"min": 32,
"encoding": "hex"
},
{
"type": "random",
"label": "RabbitMQ Password",
"hint": "Password for the RabbitMQ message broker",
"env_variable": "DOCSPACE_RABBITMQ_PASSWORD",
"min": 24,
"encoding": "hex"
},
{
"type": "email",
"label": "Admin Email",
"hint": "Email address for the DocSpace administrator account",
"required": true,
"env_variable": "DOCSPACE_ADMIN_EMAIL"
},
{
"type": "fqdn",
"label": "DocSpace Domain",
"hint": "Domain name for accessing DocSpace (e.g., docspace.example.com)",
"required": false,
"env_variable": "DOCSPACE_DOMAIN"
}
],
"created_at": 1712793600000,
"updated_at": 1712793600000
}

View File

@@ -1,4 +0,0 @@
location ^~ /.well-known/acme-challenge/ {
root /letsencrypt;
default_type "text/plain";
}

View File

@@ -1,33 +0,0 @@
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /tmp/nginx.pid;
events {
worker_connections 1024;
}
http {
client_body_temp_path /tmp/client_temp;
proxy_temp_path /tmp/proxy_temp_path;
fastcgi_temp_path /tmp/fastcgi_temp;
uwsgi_temp_path /tmp/uwsgi_temp;
scgi_temp_path /tmp/scgi_temp;
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
include /etc/nginx/conf.d/*.conf;
}

View File

@@ -1,30 +0,0 @@
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $proxy_connection;
proxy_set_header Host $this_host;
proxy_set_header X-Forwarded-Host $proxy_x_forwarded_host;
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_hide_header 'Server';
proxy_hide_header 'X-Powered-By';
proxy_buffering off;
proxy_http_version 1.1;
proxy_buffer_size 128k;
proxy_buffers 8 128k;
proxy_busy_buffers_size 256k;
access_log /var/log/nginx/access-proxy.log;
error_log /var/log/nginx/error-proxy.log;
server {
listen 0.0.0.0:80;
listen [::]:80 default_server;
client_max_body_size 4G;
location / {
proxy_pass http://$router_host:8092;
}
include includes/letsencrypt.conf;
}

View File

@@ -1,32 +0,0 @@
resolver 127.0.0.11 valid=30s;
map $http_host $this_host {
"" $host;
default $http_host;
}
map $http_x_forwarded_proto $proxy_x_forwarded_proto {
default $http_x_forwarded_proto;
"" $scheme;
}
map $http_x_forwarded_host $proxy_x_forwarded_host {
default $http_x_forwarded_host;
"" $this_host;
}
map $http_x_forwarded_port $proxy_x_forwarded_port {
default $http_x_forwarded_port;
'' $server_port;
}
map $http_upgrade $proxy_connection {
default upgrade;
"" close;
}
map $ROUTER_HOST $router_host {
volatile;
default onlyoffice-router;
~^(.*)$ $1;
}

View File

@@ -1,725 +0,0 @@
{
"schemaVersion": 2,
"$schema": "https://schemas.runtipi.io/dynamic-compose.json",
"services": [
{
"name": "docspace-mysql",
"image": "mysql:8.3.0",
"capAdd": ["SYS_NICE"],
"user": "mysql",
"environment": [
{ "key": "MYSQL_ROOT_PASSWORD", "value": "${DOCSPACE_MYSQL_ROOT_PASSWORD}" },
{ "key": "MYSQL_DATABASE", "value": "docspace" },
{ "key": "MYSQL_USER", "value": "onlyoffice_user" },
{ "key": "MYSQL_PASSWORD", "value": "${DOCSPACE_MYSQL_PASSWORD}" }
],
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data/mysql",
"containerPath": "/var/lib/mysql"
}
],
"healthCheck": {
"test": "mysql -u onlyoffice_user --password=${DOCSPACE_MYSQL_PASSWORD} -e ';'",
"interval": "10s",
"timeout": "5s",
"retries": 5
}
},
{
"name": "docspace-redis",
"image": "redis:7",
"healthCheck": {
"test": "redis-cli ping",
"interval": "10s",
"timeout": "10s",
"retries": 3
}
},
{
"name": "docspace-rabbitmq",
"image": "rabbitmq:3",
"environment": [
{ "key": "RABBITMQ_DEFAULT_USER", "value": "guest" },
{ "key": "RABBITMQ_DEFAULT_PASS", "value": "${DOCSPACE_RABBITMQ_PASSWORD}" }
],
"healthCheck": {
"test": "rabbitmq-diagnostics status",
"interval": "10s",
"timeout": "10s",
"retries": 5
}
},
{
"name": "docspace-opensearch",
"image": "onlyoffice/opensearch:2.18.0",
"environment": [
{ "key": "discovery.type", "value": "single-node" },
{ "key": "bootstrap.memory_lock", "value": "true" },
{ "key": "OPENSEARCH_JAVA_OPTS", "value": "-Xms1g -Xmx1g -Dlog4j2.formatMsgNoLookups=true" },
{ "key": "indices.fielddata.cache.size", "value": "30%" },
{ "key": "indices.memory.index_buffer_size", "value": "30%" },
{ "key": "DISABLE_INSTALL_DEMO_CONFIG", "value": "true" },
{ "key": "DISABLE_SECURITY_PLUGIN", "value": "true" }
],
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data/opensearch",
"containerPath": "/usr/share/opensearch/data"
}
],
"ulimits": {
"memlock": { "soft": -1, "hard": -1 },
"nofile": { "soft": 65536, "hard": 65536 }
},
"healthCheck": {
"test": "curl -f http://localhost:9200/_cluster/health?pretty",
"interval": "30s",
"timeout": "10s",
"retries": 5
}
},
{
"name": "docspace-document-server",
"image": "onlyoffice/documentserver:latest",
"environment": [
{ "key": "JWT_ENABLED", "value": "true" },
{ "key": "JWT_SECRET", "value": "${DOCSPACE_JWT_SECRET}" },
{ "key": "JWT_HEADER", "value": "AuthorizationJwt" },
{ "key": "JWT_IN_BODY", "value": "true" },
{ "key": "AMQP_URI", "value": "amqp://guest:${DOCSPACE_RABBITMQ_PASSWORD}@docspace-rabbitmq:5672/" },
{ "key": "REDIS_SERVER_HOST", "value": "docspace-redis" },
{ "key": "REDIS_SERVER_PORT", "value": "6379" },
{ "key": "REDIS_SERVER_USER", "value": "" },
{ "key": "REDIS_SERVER_PASS", "value": "" },
{ "key": "REDIS_SERVER_DB", "value": "0" }
],
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data/ds-app",
"containerPath": "/var/www/onlyoffice/Data"
},
{
"hostPath": "${APP_DATA_DIR}/data/ds-log",
"containerPath": "/var/log/onlyoffice"
},
{
"hostPath": "${APP_DATA_DIR}/data/ds-fonts",
"containerPath": "/usr/share/fonts"
}
],
"healthCheck": {
"test": "curl -f http://localhost:8000/info/info.json",
"interval": "30s",
"timeout": "10s",
"retries": 5
},
"dependsOn": {
"docspace-redis": { "condition": "service_healthy" },
"docspace-rabbitmq": { "condition": "service_healthy" }
}
},
{
"name": "docspace-migration-runner",
"image": "onlyoffice/docspace-migration-runner:latest",
"environment": [
{ "key": "MYSQL_CONTAINER_NAME", "value": "docspace-mysql" },
{ "key": "MYSQL_HOST", "value": "" },
{ "key": "MYSQL_PORT", "value": "3306" },
{ "key": "MYSQL_DATABASE", "value": "docspace" },
{ "key": "MYSQL_USER", "value": "onlyoffice_user" },
{ "key": "MYSQL_PASSWORD", "value": "${DOCSPACE_MYSQL_PASSWORD}" }
],
"dependsOn": {
"docspace-mysql": { "condition": "service_healthy" }
}
},
{
"name": "docspace-dotnet",
"image": "onlyoffice/docspace-dotnet:latest",
"user": "root:root",
"environment": [
{ "key": "APP_CORE_BASE_DOMAIN", "value": "${DOCSPACE_DOMAIN:-localhost}" },
{ "key": "APP_CORE_MACHINEKEY", "value": "${DOCSPACE_MACHINEKEY}" },
{ "key": "APP_CORE_SERVER_ROOT", "value": "" },
{ "key": "APP_KNOWN_NETWORKS", "value": "" },
{ "key": "APP_KNOWN_PROXIES", "value": "" },
{ "key": "APP_URL_PORTAL", "value": "http://docspace-router:8092" },
{ "key": "CERTIFICATE_PATH", "value": "" },
{ "key": "DEBUG_INFO", "value": "false" },
{ "key": "DOCUMENT_CONTAINER_NAME", "value": "docspace-document-server" },
{ "key": "DOCUMENT_SERVER_JWT_HEADER", "value": "AuthorizationJwt" },
{ "key": "DOCUMENT_SERVER_JWT_SECRET", "value": "${DOCSPACE_JWT_SECRET}" },
{ "key": "DOCUMENT_SERVER_URL_EXTERNAL", "value": "" },
{ "key": "DOCUMENT_SERVER_URL_PUBLIC", "value": "/ds-vpath/" },
{ "key": "ELK_HOST", "value": "docspace-opensearch" },
{ "key": "ELK_PORT", "value": "9200" },
{ "key": "ELK_SCHEME", "value": "http" },
{ "key": "IDENTITY_API_SERVER_PORT", "value": "9090" },
{ "key": "IDENTITY_AUTHORIZATION_SERVER_PORT", "value": "8080" },
{ "key": "INSTALLATION_TYPE", "value": "COMMUNITY" },
{ "key": "JDBC_DATABASE", "value": "docspace" },
{ "key": "JDBC_PASSWORD", "value": "${DOCSPACE_MYSQL_PASSWORD}" },
{ "key": "JDBC_URL", "value": "docspace-mysql" },
{ "key": "JDBC_USER_NAME", "value": "onlyoffice_user" },
{ "key": "LOG_LEVEL", "value": "Warning" },
{ "key": "MYSQL_CONTAINER_NAME", "value": "docspace-mysql" },
{ "key": "MYSQL_DATABASE", "value": "docspace" },
{ "key": "MYSQL_HOST", "value": "" },
{ "key": "MYSQL_PASSWORD", "value": "${DOCSPACE_MYSQL_PASSWORD}" },
{ "key": "MYSQL_PORT", "value": "3306" },
{ "key": "MYSQL_ROOT_PASSWORD", "value": "${DOCSPACE_MYSQL_ROOT_PASSWORD}" },
{ "key": "MYSQL_USER", "value": "onlyoffice_user" },
{ "key": "NODE_CONTAINER_NAME", "value": "docspace-node" },
{ "key": "NODE_OPTIONS", "value": "--max_old_space_size=4096" },
{ "key": "OAUTH_REDIRECT_URL", "value": "https://service.onlyoffice.com/oauth2.aspx" },
{ "key": "RABBIT_HOST", "value": "docspace-rabbitmq" },
{ "key": "RABBIT_PASSWORD", "value": "${DOCSPACE_RABBITMQ_PASSWORD}" },
{ "key": "RABBIT_PORT", "value": "5672" },
{ "key": "RABBIT_PROTOCOL", "value": "amqp" },
{ "key": "RABBIT_USER_NAME", "value": "guest" },
{ "key": "RABBIT_VIRTUAL_HOST", "value": "/" },
{ "key": "REDIS_DB", "value": "0" },
{ "key": "REDIS_HOST", "value": "docspace-redis" },
{ "key": "REDIS_PASSWORD", "value": "" },
{ "key": "REDIS_PORT", "value": "6379" },
{ "key": "REDIS_USER_NAME", "value": "" },
{ "key": "ROUTER_HOST", "value": "docspace-router" },
{ "key": "SERVICE_AI_PORT", "value": "5157" },
{ "key": "SERVICE_AI_SERVICE_PORT", "value": "5124" },
{ "key": "SERVICE_API_PORT", "value": "5000" },
{ "key": "SERVICE_API_SYSTEM_PORT", "value": "5010" },
{ "key": "SERVICE_BACKUP_BACKGRUOND_TASKS_PORT", "value": "5032" },
{ "key": "SERVICE_BACKUP_PORT", "value": "5012" },
{ "key": "SERVICE_CLEAR_EVENTS_PORT", "value": "5027" },
{ "key": "SERVICE_DOCEDITOR_PORT", "value": "5013" },
{ "key": "SERVICE_FILES_PORT", "value": "5007" },
{ "key": "SERVICE_FILES_SERVICES_PORT", "value": "5009" },
{ "key": "SERVICE_HEALTHCHECKS_PORT", "value": "5033" },
{ "key": "SERVICE_LOGIN_PORT", "value": "5011" },
{ "key": "SERVICE_MANAGEMENT_PORT", "value": "5015" },
{ "key": "SERVICE_NOTIFY_PORT", "value": "5005" },
{ "key": "SERVICE_PEOPLE_SERVER_PORT", "value": "5004" },
{ "key": "SERVICE_SDK_PORT", "value": "5099" },
{ "key": "SERVICE_SOCKET_PORT", "value": "9899" },
{ "key": "SERVICE_SSOAUTH_PORT", "value": "9834" },
{ "key": "SERVICE_STUDIO_NOTIFY_PORT", "value": "5006" },
{ "key": "SERVICE_STUDIO_PORT", "value": "5003" },
{ "key": "SERVICE_TELEGRAM_PORT", "value": "5075" },
{ "key": "SPRING_APPLICATION_ENCRYPTION_SECRET", "value": "${DOCSPACE_IDENTITY_SECRET}" },
{ "key": "SPRING_APPLICATION_SIGNATURE_SECRET", "value": "${DOCSPACE_MACHINEKEY}" },
{ "key": "SPRING_PROFILES_ACTIVE", "value": "dev,server" }
],
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data/app",
"containerPath": "/app/onlyoffice/data"
},
{
"hostPath": "${APP_DATA_DIR}/data/log",
"containerPath": "/var/log/onlyoffice"
}
],
"healthCheck": {
"test": "supervisorctl status | grep -v 'ASC.Migration.Runner' | grep -qv RUNNING && exit 1 || exit 0",
"interval": "60s",
"timeout": "10s",
"retries": 5
},
"dependsOn": {
"docspace-migration-runner": { "condition": "service_completed_successfully" },
"docspace-mysql": { "condition": "service_healthy" },
"docspace-redis": { "condition": "service_healthy" },
"docspace-rabbitmq": { "condition": "service_healthy" },
"docspace-opensearch": { "condition": "service_healthy" }
}
},
{
"name": "docspace-java",
"image": "onlyoffice/docspace-java:latest",
"user": "root:root",
"environment": [
{ "key": "APP_CORE_BASE_DOMAIN", "value": "${DOCSPACE_DOMAIN:-localhost}" },
{ "key": "APP_CORE_MACHINEKEY", "value": "${DOCSPACE_MACHINEKEY}" },
{ "key": "APP_CORE_SERVER_ROOT", "value": "" },
{ "key": "APP_KNOWN_NETWORKS", "value": "" },
{ "key": "APP_KNOWN_PROXIES", "value": "" },
{ "key": "APP_URL_PORTAL", "value": "http://docspace-router:8092" },
{ "key": "CERTIFICATE_PATH", "value": "" },
{ "key": "DEBUG_INFO", "value": "false" },
{ "key": "DOCUMENT_CONTAINER_NAME", "value": "docspace-document-server" },
{ "key": "DOCUMENT_SERVER_JWT_HEADER", "value": "AuthorizationJwt" },
{ "key": "DOCUMENT_SERVER_JWT_SECRET", "value": "${DOCSPACE_JWT_SECRET}" },
{ "key": "DOCUMENT_SERVER_URL_EXTERNAL", "value": "" },
{ "key": "DOCUMENT_SERVER_URL_PUBLIC", "value": "/ds-vpath/" },
{ "key": "ELK_HOST", "value": "docspace-opensearch" },
{ "key": "ELK_PORT", "value": "9200" },
{ "key": "ELK_SCHEME", "value": "http" },
{ "key": "IDENTITY_API_SERVER_PORT", "value": "9090" },
{ "key": "IDENTITY_AUTHORIZATION_SERVER_PORT", "value": "8080" },
{ "key": "INSTALLATION_TYPE", "value": "COMMUNITY" },
{ "key": "JDBC_DATABASE", "value": "docspace" },
{ "key": "JDBC_PASSWORD", "value": "${DOCSPACE_MYSQL_PASSWORD}" },
{ "key": "JDBC_URL", "value": "docspace-mysql" },
{ "key": "JDBC_USER_NAME", "value": "onlyoffice_user" },
{ "key": "LOG_LEVEL", "value": "Warning" },
{ "key": "MYSQL_CONTAINER_NAME", "value": "docspace-mysql" },
{ "key": "MYSQL_DATABASE", "value": "docspace" },
{ "key": "MYSQL_HOST", "value": "" },
{ "key": "MYSQL_PASSWORD", "value": "${DOCSPACE_MYSQL_PASSWORD}" },
{ "key": "MYSQL_PORT", "value": "3306" },
{ "key": "MYSQL_ROOT_PASSWORD", "value": "${DOCSPACE_MYSQL_ROOT_PASSWORD}" },
{ "key": "MYSQL_USER", "value": "onlyoffice_user" },
{ "key": "NODE_CONTAINER_NAME", "value": "docspace-node" },
{ "key": "NODE_OPTIONS", "value": "--max_old_space_size=4096" },
{ "key": "OAUTH_REDIRECT_URL", "value": "https://service.onlyoffice.com/oauth2.aspx" },
{ "key": "RABBIT_HOST", "value": "docspace-rabbitmq" },
{ "key": "RABBIT_PASSWORD", "value": "${DOCSPACE_RABBITMQ_PASSWORD}" },
{ "key": "RABBIT_PORT", "value": "5672" },
{ "key": "RABBIT_PROTOCOL", "value": "amqp" },
{ "key": "RABBIT_USER_NAME", "value": "guest" },
{ "key": "RABBIT_VIRTUAL_HOST", "value": "/" },
{ "key": "REDIS_DB", "value": "0" },
{ "key": "REDIS_HOST", "value": "docspace-redis" },
{ "key": "REDIS_PASSWORD", "value": "" },
{ "key": "REDIS_PORT", "value": "6379" },
{ "key": "REDIS_USER_NAME", "value": "" },
{ "key": "ROUTER_HOST", "value": "docspace-router" },
{ "key": "SERVICE_AI_PORT", "value": "5157" },
{ "key": "SERVICE_AI_SERVICE_PORT", "value": "5124" },
{ "key": "SERVICE_API_PORT", "value": "5000" },
{ "key": "SERVICE_API_SYSTEM_PORT", "value": "5010" },
{ "key": "SERVICE_BACKUP_BACKGRUOND_TASKS_PORT", "value": "5032" },
{ "key": "SERVICE_BACKUP_PORT", "value": "5012" },
{ "key": "SERVICE_CLEAR_EVENTS_PORT", "value": "5027" },
{ "key": "SERVICE_DOCEDITOR_PORT", "value": "5013" },
{ "key": "SERVICE_FILES_PORT", "value": "5007" },
{ "key": "SERVICE_FILES_SERVICES_PORT", "value": "5009" },
{ "key": "SERVICE_HEALTHCHECKS_PORT", "value": "5033" },
{ "key": "SERVICE_LOGIN_PORT", "value": "5011" },
{ "key": "SERVICE_MANAGEMENT_PORT", "value": "5015" },
{ "key": "SERVICE_NOTIFY_PORT", "value": "5005" },
{ "key": "SERVICE_PEOPLE_SERVER_PORT", "value": "5004" },
{ "key": "SERVICE_SDK_PORT", "value": "5099" },
{ "key": "SERVICE_SOCKET_PORT", "value": "9899" },
{ "key": "SERVICE_SSOAUTH_PORT", "value": "9834" },
{ "key": "SERVICE_STUDIO_NOTIFY_PORT", "value": "5006" },
{ "key": "SERVICE_STUDIO_PORT", "value": "5003" },
{ "key": "SERVICE_TELEGRAM_PORT", "value": "5075" },
{ "key": "SPRING_APPLICATION_ENCRYPTION_SECRET", "value": "${DOCSPACE_IDENTITY_SECRET}" },
{ "key": "SPRING_APPLICATION_SIGNATURE_SECRET", "value": "${DOCSPACE_MACHINEKEY}" },
{ "key": "SPRING_PROFILES_ACTIVE", "value": "dev,server" }
],
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data/app",
"containerPath": "/app/onlyoffice/data"
},
{
"hostPath": "${APP_DATA_DIR}/data/log",
"containerPath": "/var/log/onlyoffice"
}
],
"healthCheck": {
"test": "supervisorctl status | grep -v 'ASC.Migration.Runner' | grep -qv RUNNING && exit 1 || exit 0",
"interval": "60s",
"timeout": "10s",
"retries": 5
},
"dependsOn": {
"docspace-migration-runner": { "condition": "service_completed_successfully" },
"docspace-mysql": { "condition": "service_healthy" },
"docspace-redis": { "condition": "service_healthy" },
"docspace-rabbitmq": { "condition": "service_healthy" },
"docspace-opensearch": { "condition": "service_healthy" }
}
},
{
"name": "docspace-node",
"image": "onlyoffice/docspace-node:latest",
"user": "root:root",
"environment": [
{ "key": "APP_CORE_BASE_DOMAIN", "value": "${DOCSPACE_DOMAIN:-localhost}" },
{ "key": "APP_CORE_MACHINEKEY", "value": "${DOCSPACE_MACHINEKEY}" },
{ "key": "APP_CORE_SERVER_ROOT", "value": "" },
{ "key": "APP_KNOWN_NETWORKS", "value": "" },
{ "key": "APP_KNOWN_PROXIES", "value": "" },
{ "key": "APP_URL_PORTAL", "value": "http://docspace-router:8092" },
{ "key": "CERTIFICATE_PATH", "value": "" },
{ "key": "DEBUG_INFO", "value": "false" },
{ "key": "DOCUMENT_CONTAINER_NAME", "value": "docspace-document-server" },
{ "key": "DOCUMENT_SERVER_JWT_HEADER", "value": "AuthorizationJwt" },
{ "key": "DOCUMENT_SERVER_JWT_SECRET", "value": "${DOCSPACE_JWT_SECRET}" },
{ "key": "DOCUMENT_SERVER_URL_EXTERNAL", "value": "" },
{ "key": "DOCUMENT_SERVER_URL_PUBLIC", "value": "/ds-vpath/" },
{ "key": "ELK_HOST", "value": "docspace-opensearch" },
{ "key": "ELK_PORT", "value": "9200" },
{ "key": "ELK_SCHEME", "value": "http" },
{ "key": "IDENTITY_API_SERVER_PORT", "value": "9090" },
{ "key": "IDENTITY_AUTHORIZATION_SERVER_PORT", "value": "8080" },
{ "key": "INSTALLATION_TYPE", "value": "COMMUNITY" },
{ "key": "JDBC_DATABASE", "value": "docspace" },
{ "key": "JDBC_PASSWORD", "value": "${DOCSPACE_MYSQL_PASSWORD}" },
{ "key": "JDBC_URL", "value": "docspace-mysql" },
{ "key": "JDBC_USER_NAME", "value": "onlyoffice_user" },
{ "key": "LOG_LEVEL", "value": "Warning" },
{ "key": "MYSQL_CONTAINER_NAME", "value": "docspace-mysql" },
{ "key": "MYSQL_DATABASE", "value": "docspace" },
{ "key": "MYSQL_HOST", "value": "" },
{ "key": "MYSQL_PASSWORD", "value": "${DOCSPACE_MYSQL_PASSWORD}" },
{ "key": "MYSQL_PORT", "value": "3306" },
{ "key": "MYSQL_ROOT_PASSWORD", "value": "${DOCSPACE_MYSQL_ROOT_PASSWORD}" },
{ "key": "MYSQL_USER", "value": "onlyoffice_user" },
{ "key": "NODE_CONTAINER_NAME", "value": "docspace-node" },
{ "key": "NODE_OPTIONS", "value": "--max_old_space_size=4096" },
{ "key": "OAUTH_REDIRECT_URL", "value": "https://service.onlyoffice.com/oauth2.aspx" },
{ "key": "RABBIT_HOST", "value": "docspace-rabbitmq" },
{ "key": "RABBIT_PASSWORD", "value": "${DOCSPACE_RABBITMQ_PASSWORD}" },
{ "key": "RABBIT_PORT", "value": "5672" },
{ "key": "RABBIT_PROTOCOL", "value": "amqp" },
{ "key": "RABBIT_USER_NAME", "value": "guest" },
{ "key": "RABBIT_VIRTUAL_HOST", "value": "/" },
{ "key": "REDIS_DB", "value": "0" },
{ "key": "REDIS_HOST", "value": "docspace-redis" },
{ "key": "REDIS_PASSWORD", "value": "" },
{ "key": "REDIS_PORT", "value": "6379" },
{ "key": "REDIS_USER_NAME", "value": "" },
{ "key": "ROUTER_HOST", "value": "docspace-router" },
{ "key": "SERVICE_AI_PORT", "value": "5157" },
{ "key": "SERVICE_AI_SERVICE_PORT", "value": "5124" },
{ "key": "SERVICE_API_PORT", "value": "5000" },
{ "key": "SERVICE_API_SYSTEM_PORT", "value": "5010" },
{ "key": "SERVICE_BACKUP_BACKGRUOND_TASKS_PORT", "value": "5032" },
{ "key": "SERVICE_BACKUP_PORT", "value": "5012" },
{ "key": "SERVICE_CLEAR_EVENTS_PORT", "value": "5027" },
{ "key": "SERVICE_DOCEDITOR_PORT", "value": "5013" },
{ "key": "SERVICE_FILES_PORT", "value": "5007" },
{ "key": "SERVICE_FILES_SERVICES_PORT", "value": "5009" },
{ "key": "SERVICE_HEALTHCHECKS_PORT", "value": "5033" },
{ "key": "SERVICE_LOGIN_PORT", "value": "5011" },
{ "key": "SERVICE_MANAGEMENT_PORT", "value": "5015" },
{ "key": "SERVICE_NOTIFY_PORT", "value": "5005" },
{ "key": "SERVICE_PEOPLE_SERVER_PORT", "value": "5004" },
{ "key": "SERVICE_SDK_PORT", "value": "5099" },
{ "key": "SERVICE_SOCKET_PORT", "value": "9899" },
{ "key": "SERVICE_SSOAUTH_PORT", "value": "9834" },
{ "key": "SERVICE_STUDIO_NOTIFY_PORT", "value": "5006" },
{ "key": "SERVICE_STUDIO_PORT", "value": "5003" },
{ "key": "SERVICE_TELEGRAM_PORT", "value": "5075" },
{ "key": "SPRING_APPLICATION_ENCRYPTION_SECRET", "value": "${DOCSPACE_IDENTITY_SECRET}" },
{ "key": "SPRING_APPLICATION_SIGNATURE_SECRET", "value": "${DOCSPACE_MACHINEKEY}" },
{ "key": "SPRING_PROFILES_ACTIVE", "value": "dev,server" }
],
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data/app",
"containerPath": "/app/onlyoffice/data"
},
{
"hostPath": "${APP_DATA_DIR}/data/log",
"containerPath": "/var/log/onlyoffice"
}
],
"healthCheck": {
"test": "supervisorctl status | grep -v 'ASC.Migration.Runner' | grep -qv RUNNING && exit 1 || exit 0",
"interval": "60s",
"timeout": "10s",
"retries": 5
},
"dependsOn": {
"docspace-migration-runner": { "condition": "service_completed_successfully" },
"docspace-mysql": { "condition": "service_healthy" },
"docspace-redis": { "condition": "service_healthy" },
"docspace-rabbitmq": { "condition": "service_healthy" },
"docspace-opensearch": { "condition": "service_healthy" }
}
},
{
"name": "docspace-router",
"image": "onlyoffice/docspace-router:latest",
"user": "root:root",
"environment": [
{ "key": "DASHBOARDS_CONTAINER_NAME", "value": "" },
{ "key": "DASHBOARDS_PASSWORD", "value": "" },
{ "key": "DASHBOARDS_USERNAME", "value": "" },
{ "key": "DOCUMENT_CONTAINER_NAME", "value": "docspace-document-server" },
{ "key": "DOCUMENT_SERVER_URL_EXTERNAL", "value": "" },
{ "key": "REDIS_CONTAINER_NAME", "value": "docspace-redis" },
{ "key": "REDIS_DB", "value": "0" },
{ "key": "REDIS_HOST", "value": "" },
{ "key": "REDIS_PASSWORD", "value": "" },
{ "key": "REDIS_PORT", "value": "6379" },
{ "key": "SERVICE_AI", "value": "docspace-dotnet:5157" },
{ "key": "SERVICE_AI_SERVICE", "value": "docspace-dotnet:5124" },
{ "key": "SERVICE_API", "value": "docspace-dotnet:5000" },
{ "key": "SERVICE_API_SYSTEM", "value": "docspace-dotnet:5010" },
{ "key": "SERVICE_BACKUP", "value": "docspace-dotnet:5012" },
{ "key": "SERVICE_CLEAR_EVENTS", "value": "docspace-dotnet:5027" },
{ "key": "SERVICE_DOCEDITOR", "value": "docspace-node:5013" },
{ "key": "SERVICE_FILES", "value": "docspace-dotnet:5007" },
{ "key": "SERVICE_FILES_SERVICES", "value": "docspace-dotnet:5009" },
{ "key": "SERVICE_HEALTHCHECKS", "value": "docspace-dotnet:5033" },
{ "key": "SERVICE_IDENTITY", "value": "docspace-java:8080" },
{ "key": "SERVICE_IDENTITY_API", "value": "docspace-java:9090" },
{ "key": "SERVICE_LOGIN", "value": "docspace-node:5011" },
{ "key": "SERVICE_MANAGEMENT", "value": "docspace-node:5015" },
{ "key": "SERVICE_NOTIFY", "value": "docspace-dotnet:5005" },
{ "key": "SERVICE_PORT", "value": "5050" },
{ "key": "SERVICE_PEOPLE_SERVER", "value": "docspace-dotnet:5004" },
{ "key": "SERVICE_SDK", "value": "docspace-node:5099" },
{ "key": "SERVICE_SOCKET", "value": "docspace-node:9899" },
{ "key": "SERVICE_SSOAUTH", "value": "docspace-node:9834" },
{ "key": "SERVICE_STUDIO", "value": "docspace-dotnet:5003" },
{ "key": "SERVICE_STUDIO_NOTIFY", "value": "docspace-dotnet:5006" },
{ "key": "SERVICE_TELEGRAM", "value": "docspace-dotnet:5075" },
{ "key": "WRONG_PORTAL_NAME_URL", "value": "" }
],
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data/log",
"containerPath": "/var/log/onlyoffice"
}
],
"healthCheck": {
"test": "nginx -t || exit 1",
"interval": "60s",
"timeout": "10s",
"retries": 5
},
"dependsOn": {
"docspace-dotnet": { "condition": "service_healthy" },
"docspace-java": { "condition": "service_healthy" },
"docspace-node": { "condition": "service_healthy" }
}
},
{
"name": "docspace-identity-authorization",
"image": "onlyoffice/docspace-identity-authorization:latest",
"user": "root:root",
"environment": [
{ "key": "SPRING_PROFILES_ACTIVE", "value": "dev,server" },
{ "key": "SPRING_APPLICATION_SIGNATURE_SECRET", "value": "${DOCSPACE_MACHINEKEY}" },
{ "key": "SPRING_APPLICATION_ENCRYPTION_SECRET", "value": "${DOCSPACE_IDENTITY_SECRET}" },
{ "key": "MYSQL_CONTAINER_NAME", "value": "docspace-mysql" },
{ "key": "MYSQL_HOST", "value": "" },
{ "key": "MYSQL_PORT", "value": "3306" },
{ "key": "MYSQL_DATABASE", "value": "docspace" },
{ "key": "MYSQL_USER", "value": "onlyoffice_user" },
{ "key": "MYSQL_PASSWORD", "value": "${DOCSPACE_MYSQL_PASSWORD}" },
{ "key": "RABBIT_CONTAINER_NAME", "value": "docspace-rabbitmq" },
{ "key": "RABBIT_PROTOCOL", "value": "amqp" },
{ "key": "RABBIT_HOST", "value": "" },
{ "key": "RABBIT_USER_NAME", "value": "guest" },
{ "key": "RABBIT_PASSWORD", "value": "${DOCSPACE_RABBITMQ_PASSWORD}" },
{ "key": "RABBIT_VIRTUAL_HOST", "value": "/" },
{ "key": "REDIS_HOST", "value": "" },
{ "key": "REDIS_PORT", "value": "6379" },
{ "key": "REDIS_USER_NAME", "value": "" },
{ "key": "REDIS_PASSWORD", "value": "" },
{ "key": "REDIS_CONTAINER_NAME", "value": "docspace-redis" },
{ "key": "SERVER_PORT", "value": "8080" },
{ "key": "GRPC_CLIENT_REGISTRATION_ADDRESS", "value": "static://docspace-identity-api:8888" }
],
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data/log",
"containerPath": "/var/log/onlyoffice"
}
],
"healthCheck": {
"test": "curl --fail http://docspace-identity-authorization:8080/health/ || exit 1",
"interval": "60s",
"timeout": "10s",
"retries": 5
},
"dependsOn": {
"docspace-migration-runner": { "condition": "service_completed_successfully" },
"docspace-mysql": { "condition": "service_healthy" },
"docspace-redis": { "condition": "service_healthy" },
"docspace-rabbitmq": { "condition": "service_healthy" }
}
},
{
"name": "docspace-identity-api",
"image": "onlyoffice/docspace-identity-api:latest",
"user": "root:root",
"environment": [
{ "key": "SPRING_PROFILES_ACTIVE", "value": "dev,server" },
{ "key": "SPRING_APPLICATION_SIGNATURE_SECRET", "value": "${DOCSPACE_MACHINEKEY}" },
{ "key": "SPRING_APPLICATION_ENCRYPTION_SECRET", "value": "${DOCSPACE_IDENTITY_SECRET}" },
{ "key": "MYSQL_CONTAINER_NAME", "value": "docspace-mysql" },
{ "key": "MYSQL_HOST", "value": "" },
{ "key": "MYSQL_PORT", "value": "3306" },
{ "key": "MYSQL_DATABASE", "value": "docspace" },
{ "key": "MYSQL_USER", "value": "onlyoffice_user" },
{ "key": "MYSQL_PASSWORD", "value": "${DOCSPACE_MYSQL_PASSWORD}" },
{ "key": "RABBIT_CONTAINER_NAME", "value": "docspace-rabbitmq" },
{ "key": "RABBIT_PROTOCOL", "value": "amqp" },
{ "key": "RABBIT_HOST", "value": "" },
{ "key": "RABBIT_USER_NAME", "value": "guest" },
{ "key": "RABBIT_PASSWORD", "value": "${DOCSPACE_RABBITMQ_PASSWORD}" },
{ "key": "RABBIT_VIRTUAL_HOST", "value": "/" },
{ "key": "REDIS_HOST", "value": "" },
{ "key": "REDIS_PORT", "value": "6379" },
{ "key": "REDIS_USER_NAME", "value": "" },
{ "key": "REDIS_PASSWORD", "value": "" },
{ "key": "REDIS_CONTAINER_NAME", "value": "docspace-redis" },
{ "key": "SERVER_PORT", "value": "9090" },
{ "key": "GRPC_CLIENT_AUTHORIZATION_ADDRESS", "value": "static://docspace-identity-authorization:9999" }
],
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data/log",
"containerPath": "/var/log/onlyoffice"
}
],
"healthCheck": {
"test": "curl --fail http://docspace-identity-api:9090/health/ || exit 1",
"interval": "60s",
"timeout": "10s",
"retries": 5
},
"dependsOn": {
"docspace-migration-runner": { "condition": "service_completed_successfully" },
"docspace-mysql": { "condition": "service_healthy" },
"docspace-redis": { "condition": "service_healthy" },
"docspace-rabbitmq": { "condition": "service_healthy" }
}
},
{
"name": "docspace-notify",
"image": "onlyoffice/docspace-notify:latest",
"environment": [
{ "key": "MYSQL_CONTAINER_NAME", "value": "docspace-mysql" },
{ "key": "MYSQL_HOST", "value": "" },
{ "key": "MYSQL_PORT", "value": "3306" },
{ "key": "MYSQL_ROOT_PASSWORD", "value": "${DOCSPACE_MYSQL_ROOT_PASSWORD}" },
{ "key": "MYSQL_DATABASE", "value": "docspace" },
{ "key": "MYSQL_USER", "value": "onlyoffice_user" },
{ "key": "MYSQL_PASSWORD", "value": "${DOCSPACE_MYSQL_PASSWORD}" },
{ "key": "DATABASE_MIGRATION", "value": "false" },
{ "key": "APP_DOTNET_ENV", "value": "" },
{ "key": "APP_KNOWN_NETWORKS", "value": "" },
{ "key": "APP_KNOWN_PROXIES", "value": "" },
{ "key": "APP_CORE_SERVER_ROOT", "value": "" },
{ "key": "APP_CORE_BASE_DOMAIN", "value": "${DOCSPACE_DOMAIN:-localhost}" },
{ "key": "APP_CORE_MACHINEKEY", "value": "${DOCSPACE_MACHINEKEY}" },
{ "key": "APP_URL_PORTAL", "value": "http://docspace-router:8092" },
{ "key": "INSTALLATION_TYPE", "value": "COMMUNITY" },
{ "key": "OAUTH_REDIRECT_URL", "value": "https://service.onlyoffice.com/oauth2.aspx" },
{ "key": "DOCUMENT_SERVER_JWT_SECRET", "value": "${DOCSPACE_JWT_SECRET}" },
{ "key": "DOCUMENT_SERVER_JWT_HEADER", "value": "AuthorizationJwt" },
{ "key": "DOCUMENT_SERVER_URL_PUBLIC", "value": "/ds-vpath/" },
{ "key": "DOCUMENT_CONTAINER_NAME", "value": "docspace-document-server" },
{ "key": "DOCUMENT_SERVER_URL_EXTERNAL", "value": "" },
{ "key": "ELK_CONTAINER_NAME", "value": "docspace-opensearch" },
{ "key": "ELK_SCHEME", "value": "http" },
{ "key": "ELK_HOST", "value": "" },
{ "key": "ELK_PORT", "value": "9200" },
{ "key": "REDIS_CONTAINER_NAME", "value": "docspace-redis" },
{ "key": "REDIS_HOST", "value": "" },
{ "key": "REDIS_PORT", "value": "6379" },
{ "key": "REDIS_USER_NAME", "value": "" },
{ "key": "REDIS_PASSWORD", "value": "" },
{ "key": "RABBIT_CONTAINER_NAME", "value": "docspace-rabbitmq" },
{ "key": "RABBIT_PROTOCOL", "value": "amqp" },
{ "key": "RABBIT_HOST", "value": "" },
{ "key": "RABBIT_PORT", "value": "5672" },
{ "key": "RABBIT_VIRTUAL_HOST", "value": "/" },
{ "key": "RABBIT_USER_NAME", "value": "guest" },
{ "key": "RABBIT_PASSWORD", "value": "${DOCSPACE_RABBITMQ_PASSWORD}" },
{ "key": "ROUTER_HOST", "value": "docspace-router" },
{ "key": "LOG_LEVEL", "value": "Warning" },
{ "key": "DEBUG_INFO", "value": "false" }
],
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data/app",
"containerPath": "/app/onlyoffice/data"
},
{
"hostPath": "${APP_DATA_DIR}/data/log",
"containerPath": "/var/log/onlyoffice"
}
],
"healthCheck": {
"test": "curl --fail http://docspace-notify:5005/health/ || exit 1",
"interval": "60s",
"timeout": "10s",
"retries": 5
},
"dependsOn": {
"docspace-migration-runner": { "condition": "service_completed_successfully" },
"docspace-mysql": { "condition": "service_healthy" },
"docspace-redis": { "condition": "service_healthy" },
"docspace-rabbitmq": { "condition": "service_healthy" }
}
},
{
"name": "docspace-healthchecks",
"image": "onlyoffice/docspace-healthchecks:latest",
"environment": [
{ "key": "LOG_LEVEL", "value": "Warning" }
],
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data/app",
"containerPath": "/app/onlyoffice/data"
}
],
"healthCheck": {
"test": "curl --fail http://docspace-healthchecks:5033/liveness/ || exit 1",
"interval": "60s",
"timeout": "10s",
"retries": 5
},
"dependsOn": {
"docspace-dotnet": { "condition": "service_healthy" }
}
},
{
"name": "docspace",
"image": "nginx:latest",
"isMain": true,
"internalPort": 80,
"environment": [
{ "key": "ROUTER_HOST", "value": "docspace-router" }
],
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data/nginx-log",
"containerPath": "/var/log/nginx"
},
{
"hostPath": "${APP_DATA_DIR}/config/nginx/nginx.conf.template",
"containerPath": "/etc/nginx/nginx.conf",
"readOnly": true
},
{
"hostPath": "${APP_DATA_DIR}/config/nginx/letsencrypt.conf",
"containerPath": "/etc/nginx/includes/letsencrypt.conf",
"readOnly": true
},
{
"hostPath": "${APP_DATA_DIR}/config/nginx/proxy.upstream.conf.template",
"containerPath": "/etc/nginx/templates/proxy.upstream.conf.template",
"readOnly": true
},
{
"hostPath": "${APP_DATA_DIR}/config/nginx/onlyoffice-proxy.conf",
"containerPath": "/etc/nginx/conf.d/default.conf",
"readOnly": true
}
],
"healthCheck": {
"test": "nginx -t || exit 1",
"interval": "60s",
"timeout": "10s",
"retries": 5
},
"dependsOn": {
"docspace-router": { "condition": "service_healthy" }
}
}
]
}

View File

@@ -1,37 +0,0 @@
# ONLYOFFICE DocSpace
ONLYOFFICE DocSpace is a collaborative room-based document management platform that enables teams to create, edit, and manage documents, spreadsheets, and presentations in real-time.
## Key Features
- **Room-based collaboration** — Organize work into dedicated rooms with granular access controls for teams, departments, or external partners
- **Built-in document editors** — Full-featured editors for documents, spreadsheets, presentations, and fillable forms powered by ONLYOFFICE Docs
- **File management** — Upload, organize, and share files with version history and audit trails
- **Third-party integrations** — Connect external storage providers (Google Drive, Dropbox, OneDrive, Nextcloud, etc.)
- **Guest access** — Invite external users with limited permissions for secure document sharing
- **Customizable branding** — White-label the interface with your organization's logo and colors
## Architecture
This deployment includes the full DocSpace stack:
- **Nginx proxy** — Reverse proxy and entry point
- **DocSpace router** — Internal request routing
- **.NET services** — Core backend APIs (files, people, backup, studio, etc.)
- **Java services** — Identity and authorization (OAuth2/OpenID Connect)
- **Node.js services** — Frontend apps (login, editor, SDK, management)
- **ONLYOFFICE Document Server** — Real-time document editing engine
- **MySQL 8.3** — Primary database
- **Redis 7** — Caching and session storage
- **RabbitMQ 3** — Message queue for async operations
- **OpenSearch 2.18** — Full-text search indexing
## System Requirements
- **RAM:** Minimum 8GB recommended (OpenSearch alone needs 1GB+)
- **Disk:** 10GB+ for application data
- **Architecture:** amd64 only
## First Launch
After installation, navigate to the app URL and complete the initial setup wizard to create your administrator account.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -1,135 +0,0 @@
{
"name": "SAP Migration Dashboard",
"id": "erp-integration",
"available": true,
"short_desc": "SAP S/4HANA migration planning and tracking dashboard with AI assistance",
"author": "alexz",
"port": 8001,
"categories": [
"utilities",
"development"
],
"tipi_version": 1,
"version": "1.0.0",
"source": "https://git.alexzaw.dev/alexz/ERP-Integration",
"website": "https://git.alexzaw.dev/alexz/ERP-Integration",
"exposable": true,
"dynamic_config": true,
"supported_architectures": [
"arm64",
"amd64"
],
"min_tipi_version": "4.0.0",
"created_at": 1742342400000,
"updated_at": 1743292800000,
"form_fields": [
{
"type": "text",
"label": "Chat Agent Provider",
"hint": "Which AI provider to use: anthropic, ollama, or gemini",
"required": true,
"default": "anthropic",
"env_variable": "AGENT_PROVIDER"
},
{
"type": "boolean",
"label": "Enable AI Chat",
"hint": "Enable the AI-powered agent chat feature",
"required": false,
"default": true,
"env_variable": "AGENT_CHAT_ENABLED"
},
{
"type": "password",
"label": "Anthropic API Key",
"hint": "Your Anthropic API key for AI chat features",
"required": false,
"env_variable": "ANTHROPIC_API_KEY",
"min": 0,
"max": 200
},
{
"type": "text",
"label": "Anthropic Base URL",
"hint": "Anthropic API base URL",
"required": false,
"default": "https://api.anthropic.com",
"env_variable": "ANTHROPIC_BASE_URL"
},
{
"type": "text",
"label": "Anthropic Model",
"hint": "Claude model to use (e.g. claude-haiku-4-5, claude-sonnet-4-20250514)",
"required": false,
"default": "claude-haiku-4-5",
"env_variable": "ANTHROPIC_MODEL"
},
{
"type": "text",
"label": "Ollama Base URL",
"hint": "Local Ollama API endpoint (e.g. http://192.168.1.150:11434). Required if AGENT_PROVIDER=ollama.",
"required": false,
"default": "http://192.168.1.150:11434",
"env_variable": "OLLAMA_BASE_URL"
},
{
"type": "text",
"label": "Ollama Model",
"hint": "Ollama model name (e.g. gpt-oss:20b)",
"required": false,
"default": "gpt-oss:20b",
"env_variable": "OLLAMA_MODEL"
},
{
"type": "password",
"label": "Ollama API Key",
"hint": "API key for authenticated Ollama proxy access (X-Api-Key header). Leave empty if not required.",
"required": false,
"env_variable": "OLLAMA_API_KEY",
"min": 0,
"max": 200
},
{
"type": "password",
"label": "Gemini API Key",
"hint": "Google Gemini API key. Required if AGENT_PROVIDER=gemini.",
"required": false,
"env_variable": "GEMINI_API_KEY",
"min": 0,
"max": 200
},
{
"type": "text",
"label": "Gemini Model",
"hint": "Gemini model to use (e.g. gemini-2.0-flash)",
"required": false,
"default": "gemini-2.0-flash",
"env_variable": "GEMINI_MODEL"
},
{
"type": "text",
"label": "Context Root",
"hint": "Optional URL prefix for all routes (e.g. /everest). Leave empty for no prefix.",
"required": false,
"default": "/everest",
"env_variable": "CONTEXT_ROOT"
},
{
"type": "text",
"label": "CORS Origin",
"hint": "Allowed CORS origins (* for all)",
"required": false,
"default": "*",
"env_variable": "CORS_ORIGIN"
},
{
"type": "password",
"label": "Proxy Auth Secret",
"hint": "Shared secret for trusted reverse proxy authentication (X-Proxy-Secret header). Leave empty to allow unauthenticated access.",
"required": false,
"env_variable": "PROXY_SECRET",
"min": 0,
"max": 200
}
]
}

View File

@@ -1,96 +0,0 @@
{
"schemaVersion": 2,
"$schema": "https://schemas.runtipi.io/dynamic-compose.json",
"services": [
{
"name": "erp-integration",
"image": "git.alexzaw.dev/alexz/erp-integration:latest",
"isMain": true,
"internalPort": 8001,
"environment": [
{
"key": "TZ",
"value": "${TZ}"
},
{
"key": "NODE_ENV",
"value": "production"
},
{
"key": "PORT",
"value": "8001"
},
{
"key": "CONTEXT_ROOT",
"value": "${CONTEXT_ROOT}"
},
{
"key": "CORS_ORIGIN",
"value": "${CORS_ORIGIN}"
},
{
"key": "AGENT_PROVIDER",
"value": "${AGENT_PROVIDER}"
},
{
"key": "AGENT_CHAT_ENABLED",
"value": "${AGENT_CHAT_ENABLED}"
},
{
"key": "ANTHROPIC_API_KEY",
"value": "${ANTHROPIC_API_KEY}"
},
{
"key": "ANTHROPIC_BASE_URL",
"value": "${ANTHROPIC_BASE_URL}"
},
{
"key": "ANTHROPIC_MODEL",
"value": "${ANTHROPIC_MODEL}"
},
{
"key": "OLLAMA_BASE_URL",
"value": "${OLLAMA_BASE_URL}"
},
{
"key": "OLLAMA_MODEL",
"value": "${OLLAMA_MODEL}"
},
{
"key": "OLLAMA_API_KEY",
"value": "${OLLAMA_API_KEY}"
},
{
"key": "GEMINI_API_KEY",
"value": "${GEMINI_API_KEY}"
},
{
"key": "GEMINI_MODEL",
"value": "${GEMINI_MODEL}"
},
{
"key": "PROXY_SECRET",
"value": "${PROXY_SECRET}"
}
],
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data",
"containerPath": "/app/backend/data",
"readOnly": false
},
{
"hostPath": "${APP_DATA_DIR}/uploads",
"containerPath": "/app/backend/uploads",
"readOnly": false
}
],
"healthCheck": {
"test": "curl --fail http://localhost:8001${CONTEXT_ROOT}/api/health || exit 1",
"interval": "30s",
"timeout": "10s",
"retries": 3
}
}
]
}

View File

@@ -1,33 +0,0 @@
x-runtipi:
schema_version: 2
services:
erp-integration:
image: git.alexzaw.dev/alexz/erp-integration:latest
ports:
- 8001:8001
volumes:
- ${APP_DATA_DIR}/data:/app/backend/data
- ${APP_DATA_DIR}/uploads:/app/backend/uploads
environment:
- TZ=${TZ}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
- ANTHROPIC_MODEL=${ANTHROPIC_MODEL}
- AGENT_CHAT_ENABLED=${AGENT_CHAT_ENABLED}
- CORS_ORIGIN=${CORS_ORIGIN}
- NODE_ENV=production
- PORT=8001
- MCP_API_BASE_URL=${MCP_API_BASE_URL}
- SHAREPOINT_BASE_URL=${SHAREPOINT_BASE_URL}
- OLLAMA_BASE_URL=${OLLAMA_BASE_URL}
- OLLAMA_MODEL=${OLLAMA_MODEL}
- OLLAMA_API_KEY=${OLLAMA_API_KEY}
- CONTEXT_ROOT=${CONTEXT_ROOT}
- PROXY_SECRET=${PROXY_SECRET}
healthcheck:
test: curl --fail http://localhost:8001$${CONTEXT_ROOT}/api/health || exit 1
interval: 30s
timeout: 10s
retries: 3
x-runtipi:
internal_port: 8001
is_main: true

View File

@@ -1,52 +0,0 @@
# SAP Migration Dashboard
A comprehensive enterprise data migration management platform designed for planning, tracking, and executing SAP S/4HANA system integrations. Built for migration teams that need structured workflows, capacity planning, and optional AI-powered assistance.
## Features
### Migration Package Management
Create and manage migration packages with full lifecycle tracking. Each package moves through clearly defined stages from initial planning through go-live, giving your team complete visibility into migration progress.
### 7-Phase Implementation Workflow
Follow a structured implementation methodology with seven distinct phases:
1. **Discover** - Assess current landscape and define scope
2. **Prepare** - Set up project governance and infrastructure
3. **Explore** - Analyze business processes and data
4. **Realize** - Configure, develop, and test
5. **Deploy** - Execute cutover and migration
6. **Run** - Stabilize and support production
7. **Optimize** - Continuous improvement post go-live
### Team Capacity Planning
Manage team members, roles, and availability. Track resource allocation across packages and phases to prevent overcommitment and identify bottlenecks before they impact timelines.
### Task Assignment and Gantt Visualization
Assign tasks to team members with deadlines and dependencies. Visualize project timelines with interactive Gantt charts to keep the entire migration on track.
### Process Mapping with Mermaid Diagrams
Document and visualize business processes using Mermaid diagram syntax. Map current-state and future-state processes directly within the platform for easy reference during migration execution.
### Excel Export
Export migration data, task lists, and reports to Excel format for stakeholder reporting, offline analysis, and integration with existing project management workflows.
### AI Agent Chat (Optional)
Enable the built-in AI assistant powered by Anthropic's Claude to get help with migration planning, process analysis, and troubleshooting. Requires an Anthropic API key and can be toggled on or off via configuration.
### File Management
Upload and organize migration-related documents, configuration files, and reference materials. Keep all project artifacts centralized and accessible to the team.
## Configuration
| Variable | Description | Required |
|----------|-------------|----------|
| `ANTHROPIC_API_KEY` | Anthropic API key for AI chat features | Yes |
| `ANTHROPIC_MODEL` | Claude model to use (default: `claude-opus-4-6`) | No |
| `AGENT_CHAT_ENABLED` | Enable AI chat feature (default: `false`) | No |
| `CORS_ORIGIN` | Allowed CORS origins (default: `*`) | No |
## Data Persistence
- **SQLite database** stored in `/app/backend/data`
- **Uploaded files** stored in `/app/backend/uploads`
Both directories are persisted across container restarts through Runtipi volume mounts.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -1,35 +0,0 @@
# MCP Manager image build
The Dockerfile and full source for the **MCP Manager** image live in a separate Gitea repo:
- Source: <https://git.alexzaw.dev/alexz/mcp-manager>
- Published image: `git.alexzaw.dev/alexz/mcp-manager:<tag>`
This folder holds only a thin helper script (`build.sh`) that clones the source
repo and pushes the resulting image to the in-house Docker registry.
## Rebuild
```bash
cd build/
./build.sh # builds + pushes :latest and the version tag
IMAGE_TAG=1.0.1 ./build.sh # override the tag
```
Requires `docker login git.alexzaw.dev` to already be done (or `~/.docker/config.json`
to have the registry credentials, which is the case on this host).
## Notes
- Two-stage build: `node:20-bookworm-slim` builds the React frontend, then a
`python:3.11-slim` runtime stage serves the API + static UI on port `8001`.
- The runtime image bundles **Node 20** (copied from the bookworm node stage, so
it's glibc-compatible — no NodeSource/apt) and **uv / uvx** (`pip install uv`),
so the npx- and uvx-based MCP servers the app manages can actually run.
- All package caches/installs are pinned under `/data` via env vars
(`NPM_CONFIG_CACHE`, `npm_config_prefix`, `UV_CACHE_DIR`, `UV_TOOL_DIR`, pip cache).
Runtipi mounts `/data` to app-data, so MCP servers download once and persist.
- `amd64` only. Add `--platform linux/arm64` to `build.sh` args to cross-build
(Node is copied from a multi-arch base, so this works).
- Upstream ships no `frontend/yarn.lock`; the Dockerfile resolves dependencies
fresh at build time (no `--frozen-lockfile`).

View File

@@ -1,37 +0,0 @@
#!/bin/bash
# Build and push the MCP Manager Docker image.
# Clones the source repo (or reuses an existing checkout) and tags/pushes
# both :latest and the version tag.
set -euo pipefail
IMAGE_NAME="${IMAGE_NAME:-git.alexzaw.dev/alexz/mcp-manager}"
IMAGE_TAG="${IMAGE_TAG:-1.5.0}"
SOURCE_REPO="${SOURCE_REPO:-https://git.alexzaw.dev/alexz/mcp-manager.git}"
WORKDIR="${WORKDIR:-/tmp/mcp-manager-build}"
echo "==> Preparing source at ${WORKDIR}"
if [[ -d "${WORKDIR}/.git" ]]; then
git -C "${WORKDIR}" fetch --quiet origin
git -C "${WORKDIR}" reset --hard --quiet origin/HEAD
else
rm -rf "${WORKDIR}"
git clone --quiet "${SOURCE_REPO}" "${WORKDIR}"
fi
echo "==> Building ${IMAGE_NAME}:${IMAGE_TAG} (and :latest)"
cd "${WORKDIR}"
docker build "$@" \
-t "${IMAGE_NAME}:${IMAGE_TAG}" \
-t "${IMAGE_NAME}:latest" \
.
echo "==> Pushing ${IMAGE_NAME}:${IMAGE_TAG}"
docker push "${IMAGE_NAME}:${IMAGE_TAG}"
echo "==> Pushing ${IMAGE_NAME}:latest"
docker push "${IMAGE_NAME}:latest"
echo ""
echo "Done. To install on Runtipi:"
echo " sudo /etc/runtipi/runtipi-cli appstore update"
echo " sudo /etc/runtipi/runtipi-cli app install mcp-manager:runtipi"

View File

@@ -1,45 +0,0 @@
{
"$schema": "https://schemas.runtipi.io/app-info.json",
"name": "MCP Manager",
"id": "mcp-manager",
"available": true,
"short_desc": "Manage MCP servers behind a login and expose them to Claude over HTTPS/SSE.",
"author": "alexz",
"port": 8501,
"categories": ["ai", "utilities"],
"description": "A control panel for your Model Context Protocol (MCP) servers. Add, edit, run and monitor stdio / SSE / HTTP servers, then export a ready-to-paste Claude configuration. stdio servers (npx- and uvx-based) are launched inside the container and exposed over HTTPS through the built-in SSE bridge. Node 24 and uv/uvx ship in the image, and every package cache is persisted to app-data so servers download once and survive restarts. The built-in chat agent can install MCP servers from a git+https:// repo or https:// archive (allow-listed hosts) for packages not in a public registry.",
"tipi_version": 5,
"version": "1.5.2",
"source": "https://git.alexzaw.dev/alexz/mcp-manager",
"website": "https://git.alexzaw.dev/alexz/mcp-manager",
"exposable": true,
"dynamic_config": true,
"no_gui": false,
"min_tipi_version": "4.0.0",
"supported_architectures": ["amd64"],
"form_fields": [
{
"type": "text",
"label": "Admin email",
"env_variable": "ADMIN_EMAIL",
"required": true,
"hint": "Email used to log in to the dashboard."
},
{
"type": "password",
"label": "Admin password",
"env_variable": "ADMIN_PASSWORD",
"required": true,
"min": 8,
"hint": "Password for the admin account."
},
{
"type": "random",
"label": "JWT secret",
"env_variable": "JWT_SECRET",
"min": 64,
"encoding": "hex",
"hint": "Auto-generated signing secret for login sessions."
}
]
}

View File

@@ -1,66 +0,0 @@
{
"$schema": "https://schemas.runtipi.io/dynamic-compose.json",
"schemaVersion": 2,
"services": [
{
"name": "mcp-manager",
"image": "git.alexzaw.dev/alexz/mcp-manager:1.5.2",
"isMain": true,
"internalPort": 8001,
"dependsOn": {
"mcp-manager-db": { "condition": "service_healthy" }
},
"environment": [
{ "key": "TZ", "value": "${TZ}" },
{ "key": "MONGO_URL", "value": "mongodb://mcp-manager-db:27017" },
{ "key": "DB_NAME", "value": "mcp_manager" },
{ "key": "DATA_DIR", "value": "/data" },
{ "key": "STATIC_DIR", "value": "/app/static" },
{ "key": "JWT_SECRET", "value": "${JWT_SECRET}" },
{ "key": "ADMIN_EMAIL", "value": "${ADMIN_EMAIL}" },
{ "key": "ADMIN_PASSWORD", "value": "${ADMIN_PASSWORD}" },
{ "key": "FRONTEND_URL", "value": "${APP_PROTOCOL}://${APP_DOMAIN}" },
{ "key": "AGENT_PROVIDER", "value": "ollama" },
{ "key": "OLLAMA_BASE_URL", "value": "http://192.168.0.32:11434/v1" },
{ "key": "OLLAMA_MODEL", "value": "gemma4:26b-tuned" },
{ "key": "OLLAMA_API_KEY", "value": "ollama" },
{ "key": "AGENT_CHAT_ENABLED", "value": "true" }
],
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data/app",
"containerPath": "/data",
"readOnly": false
}
],
"healthCheck": {
"test": "curl -fsS http://127.0.0.1:8001/api/ || exit 1",
"interval": "30s",
"timeout": "5s",
"retries": 3,
"startPeriod": "30s"
}
},
{
"name": "mcp-manager-db",
"image": "mongo:7",
"isMain": false,
"command": ["--quiet"],
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data/db",
"containerPath": "/data/db",
"readOnly": false
}
],
"healthCheck": {
"test": "mongosh --quiet --eval 'db.runCommand({ ping: 1 }).ok' || exit 1",
"interval": "300s",
"timeout": "10s",
"retries": 5,
"startPeriod": "30s",
"startInterval": "5s"
}
}
]
}

View File

@@ -1,38 +0,0 @@
# MCP Manager
A self-hosted control panel for your **Model Context Protocol (MCP) servers**. Add, edit, run and monitor servers from one dashboard, then export a ready-to-paste Claude configuration.
## Why this exists
MCP servers are scattered across `npx`, `uvx`, and one-off HTTP/SSE endpoints, and wiring them into Claude by hand is tedious. MCP Manager keeps them in one place behind a login and turns any local **stdio** server into an HTTPS-reachable **SSE** endpoint you can drop straight into a Claude config.
## Features
- Add **stdio**, **SSE**, and **HTTP** MCP servers with custom command, args, and env
- Launch and stop stdio servers as managed processes, with live status
- Built-in **SSE bridge** — exposes each stdio server over HTTPS:
- `GET https://<domain>/api/mcp/<name>/sse?token=<token>`
- `POST https://<domain>/api/mcp/<name>/messages?session_id=...&token=<token>`
- Per-server auto-generated `access_token`; unauthenticated requests get `401`
- **Claude Config** tab emits copy-paste-ready stdio and HTTPS/SSE blocks
- Admin login (JWT cookie); config snapshot + running state persisted to disk
- Single container + MongoDB; the React UI and the `/api` are served on one port
## Runtime
- **Node 20** and **uv / uvx** ship inside the image, so the common npx- and uvx-based MCP servers run out of the box.
- All package caches and installs (`npm`, `npx`, `uv` tools, global npm prefix, pip) live under `/data`, which is mounted to Runtipi **app-data** — so servers download **once** and survive container restarts and updates.
## First run
1. Install and set **Admin email** + **Admin password** (the JWT secret is generated).
2. Toggle **Expose app** and assign a domain — Traefik issues TLS automatically.
3. Add your MCP servers, start the stdio ones, and copy the generated Claude config.
## Backup
Everything needed to rebuild lives under app-data:
- `servers.json` — full server configuration snapshot (re-seeded if the DB is empty on boot)
- `running_state.json` — which stdio servers were running (auto-restarted on boot)
- the MongoDB volume — accounts and live server records

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -1,19 +0,0 @@
{
"name": "Markdown Converter",
"id": "md2html",
"available": true,
"short_desc": "Convert Markdown to styled HTML or PDF with TOC and Mermaid diagrams",
"author": "alexz",
"port": 8340,
"categories": ["utilities", "development"],
"description": "A web-based Markdown converter that produces beautifully styled HTML documents with sidebar table of contents, Mermaid diagram rendering, and PDF export via headless Chromium. Supports drag-and-drop file upload, live preview, and downloadable output.",
"tipi_version": 1,
"version": "1.0.2",
"source": "https://git.alexzaw.dev/alexz/md2html",
"website": "https://git.alexzaw.dev/alexz/md2html",
"exposable": true,
"dynamic_config": true,
"no_gui": false,
"form_fields": [],
"supported_architectures": ["arm64", "amd64"]
}

View File

@@ -1,22 +0,0 @@
{
"schemaVersion": 2,
"$schema": "https://schemas.runtipi.io/dynamic-compose.json",
"services": [
{
"name": "md2html",
"image": "git.alexzaw.dev/alexz/md2html:1.0.2",
"isMain": true,
"internalPort": 3000,
"environment": [
{
"key": "PORT",
"value": "3000"
},
{
"key": "CHROME_PATH",
"value": "/usr/bin/chromium"
}
]
}
]
}

View File

@@ -1,12 +0,0 @@
# Markdown Converter
Convert Markdown files to beautifully styled HTML or PDF documents.
## Features
- **Sidebar TOC** — Auto-generated table of contents with scroll-tracking highlights
- **Mermaid Diagrams** — Renders flowcharts, sequence diagrams, and more
- **PDF Export** — Server-side PDF generation via headless Chromium
- **Drag & Drop** — Upload .md files or paste markdown directly
- **Live Preview** — Preview the styled HTML in a new tab before downloading
- **Responsive** — Mobile-friendly layout with collapsible sidebar

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

View File

@@ -1,28 +0,0 @@
# MergeDrop image build
The Dockerfile and full source for the **MergeDrop** image live in a separate Gitea repo:
- Source: <https://git.alexzaw.dev/alexz/video-merger>
- Published image: `git.alexzaw.dev/alexz/mergedrop:<tag>`
This folder holds only a thin helper script (`build.sh`) that clones the source
repo and pushes the resulting image to the in-house Docker registry.
## Rebuild
```bash
cd build/
./build.sh # builds + pushes :latest and the version tag
IMAGE_TAG=1.0.1 ./build.sh # override the tag
```
Requires `docker login git.alexzaw.dev` to already be done (or `~/.docker/config.json`
to have the registry credentials, which is the case on this host).
## Notes
- The image is a two-stage build (Node 20 → Python 3.11-slim) and produces an
`amd64` image only. Add `--platform linux/arm64` to `build.sh` args to cross-build.
- The application is a single FastAPI process that serves the static React build
and a `/api/*` JSON API on the same port (`8000` inside the container).
- `ffmpeg` is installed in the runtime image; no host-level ffmpeg required.

View File

@@ -1,37 +0,0 @@
#!/bin/bash
# Build and push the MergeDrop Docker image.
# Clones the source repo (or reuses an existing checkout) and tags/pushes
# both :latest and the version tag.
set -euo pipefail
IMAGE_NAME="${IMAGE_NAME:-git.alexzaw.dev/alexz/mergedrop}"
IMAGE_TAG="${IMAGE_TAG:-1.0.0}"
SOURCE_REPO="${SOURCE_REPO:-https://git.alexzaw.dev/alexz/video-merger.git}"
WORKDIR="${WORKDIR:-/tmp/mergedrop-build}"
echo "==> Preparing source at ${WORKDIR}"
if [[ -d "${WORKDIR}/.git" ]]; then
git -C "${WORKDIR}" fetch --quiet origin
git -C "${WORKDIR}" reset --hard --quiet origin/HEAD
else
rm -rf "${WORKDIR}"
git clone --quiet "${SOURCE_REPO}" "${WORKDIR}"
fi
echo "==> Building ${IMAGE_NAME}:${IMAGE_TAG} (and :latest)"
cd "${WORKDIR}"
docker build "$@" \
-t "${IMAGE_NAME}:${IMAGE_TAG}" \
-t "${IMAGE_NAME}:latest" \
.
echo "==> Pushing ${IMAGE_NAME}:${IMAGE_TAG}"
docker push "${IMAGE_NAME}:${IMAGE_TAG}"
echo "==> Pushing ${IMAGE_NAME}:latest"
docker push "${IMAGE_NAME}:latest"
echo ""
echo "Done. To install on Runtipi:"
echo " sudo /etc/runtipi/runtipi-cli appstore update"
echo " sudo /etc/runtipi/runtipi-cli app install mergedrop:runtipi"

View File

@@ -1,43 +0,0 @@
{
"name": "MergeDrop",
"id": "mergedrop",
"available": true,
"short_desc": "Drag-and-drop video merger — drop clips, reorder, build one MP4.",
"author": "alexz",
"port": 8765,
"categories": ["media", "utilities"],
"description": "Self-hosted, drag-and-drop video merger. Upload multiple clips, reorder them, and produce a single normalized MP4 (h.264 + AAC) via ffmpeg. Handles mixed codecs, resolutions and framerates. Live progress streaming, in-browser preview, and downloadable result. No accounts, no telemetry, single container.",
"tipi_version": 1,
"version": "1.0.0",
"source": "https://git.alexzaw.dev/alexz/video-merger",
"website": "https://git.alexzaw.dev/alexz/video-merger",
"exposable": true,
"dynamic_config": true,
"no_gui": false,
"form_fields": [
{
"type": "text",
"label": "Max files per merge",
"default": "20",
"required": false,
"env_variable": "MERGEDROP_MAX_FILES"
},
{
"type": "text",
"label": "Max per-file size (bytes)",
"default": "2147483648",
"required": false,
"env_variable": "MERGEDROP_MAX_FILE_SIZE",
"hint": "Default is 2 GiB (2147483648). Raise for larger source clips."
},
{
"type": "text",
"label": "Output retention (hours)",
"default": "24",
"required": false,
"env_variable": "MERGEDROP_OUTPUT_MAX_AGE_HOURS",
"hint": "How long merged MP4s stay on disk before the janitor deletes them."
}
],
"supported_architectures": ["amd64"]
}

View File

@@ -1,47 +0,0 @@
{
"$schema": "https://schemas.runtipi.io/dynamic-compose.json",
"schemaVersion": 2,
"services": [
{
"name": "mergedrop",
"image": "git.alexzaw.dev/alexz/mergedrop:1.0.0",
"isMain": true,
"internalPort": 8000,
"environment": [
{ "key": "PORT", "value": "8000" },
{ "key": "TZ", "value": "${TZ}" },
{ "key": "MERGEDROP_UPLOAD_DIR", "value": "/data/uploads" },
{ "key": "MERGEDROP_OUTPUT_DIR", "value": "/data/outputs" },
{ "key": "MERGEDROP_THUMB_DIR", "value": "/data/thumbs" },
{ "key": "MERGEDROP_MAX_FILES", "value": "${MERGEDROP_MAX_FILES}" },
{ "key": "MERGEDROP_MAX_FILE_SIZE", "value": "${MERGEDROP_MAX_FILE_SIZE}" },
{ "key": "MERGEDROP_OUTPUT_MAX_AGE_HOURS", "value": "${MERGEDROP_OUTPUT_MAX_AGE_HOURS}" },
{ "key": "CORS_ORIGINS", "value": "*" }
],
"volumes": [
{
"hostPath": "${APP_DATA_DIR}/data/uploads",
"containerPath": "/data/uploads",
"readOnly": false
},
{
"hostPath": "${APP_DATA_DIR}/data/outputs",
"containerPath": "/data/outputs",
"readOnly": false
},
{
"hostPath": "${APP_DATA_DIR}/data/thumbs",
"containerPath": "/data/thumbs",
"readOnly": false
}
],
"healthCheck": {
"test": "curl -fsS http://127.0.0.1:8000/api/ || exit 1",
"interval": "30s",
"timeout": "5s",
"retries": 3,
"startPeriod": "20s"
}
}
]
}

View File

@@ -1,46 +0,0 @@
# MergeDrop
A self-hosted, drag-and-drop **video merger**. Drop clips → reorder → click **Merge & Build MP4** → download a single ready-to-share file.
## Why this exists
Most "merge video" tools are bloated desktop apps, paid SaaS, or sketchy converter sites that re-upload your footage to who-knows-where. MergeDrop is the opposite:
- 100% self-hosted, runs in a single container on your Runtipi
- No accounts, no telemetry, no third-party upload caps
- Mixed formats / resolutions / framerates → normalized to a clean **1080p30 h.264 + AAC** MP4
## Features
- Drag-and-drop upload, drag-to-reorder, drag-to-remove
- Live ffmpeg progress (percentage + tail of stderr) per merge job
- In-browser preview of the merged result, then one-click download
- Built-in janitor — old outputs are auto-deleted after a configurable retention window
## Supported inputs
`mp4`, `mov`, `mkv`, `avi`, `webm`, `m4v`, `flv`, `wmv`, `mpg/mpeg` (mixed codecs and dimensions are normalized via two-pass ffmpeg pipeline).
## Output presets
`1080p` (default), `720p`, `4K`, `vertical 9:16`, `square 1:1` — selectable per job from the UI.
## Configuration
| Setting | Default | Description |
| --- | --- | --- |
| Max files per merge | `20` | Hard cap per job |
| Max per-file size | `2 GiB` | Per-upload limit (bytes) |
| Output retention | `24h` | Janitor sweeps merged MP4s older than this |
## Volumes (persistent)
- `${APP_DATA_DIR}/data/uploads` — staged source clips
- `${APP_DATA_DIR}/data/outputs` — merged MP4 results
- `${APP_DATA_DIR}/data/thumbs` — generated upload thumbnails
## Tech stack
- Backend: FastAPI + `ffmpeg` subprocess (async, streamed progress)
- Frontend: React 19 + Tailwind + `@dnd-kit` + `react-dropzone`
- Single container, single port, no database

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,54 +0,0 @@
{
"$schema": "../app-info-schema.json",
"name": "NAS Samba + CloudNAS",
"id": "nas-samba",
"available": true,
"port": 8080,
"exposable": true,
"dynamic_config": true,
"no_gui": false,
"tipi_version": 2,
"version": "2.0.0",
"categories": ["network", "utilities"],
"description": "All-in-one NAS container with Samba SMB file sharing and CloudNAS web file manager. Browse files via web UI or connect via SMB at \\\\192.168.0.15\\data with username alexz. No database required — uses filesystem + SQLite.",
"short_desc": "NAS with SMB + web file manager",
"author": "alexz",
"source": "https://gits.alexzaw.dev/alexz/runtipi",
"form_fields": [
{
"type": "password",
"label": "SMB Password",
"env_variable": "SMB_PASSWORD",
"required": true,
"hint": "Password for SMB user alexz"
},
{
"type": "random",
"label": "JWT Secret",
"env_variable": "JWT_SECRET",
"min": 32,
"encoding": "hex"
},
{
"type": "text",
"label": "NAS Host Path",
"env_variable": "NAS_PATH",
"required": true,
"default": "/nas",
"hint": "Host path to share via SMB and web UI"
},
{
"type": "text",
"label": "Bind IP",
"env_variable": "BIND_IP",
"required": true,
"default": "192.168.0.15",
"hint": "IP address to bind SMB ports to"
}
],
"supported_architectures": ["arm64", "amd64"],
"created_at": 1741536000000,
"updated_at": 1742169600000,
"deprecated": false,
"min_tipi_version": "4.5.0"
}

View File

@@ -1,34 +0,0 @@
{
"schemaVersion": 2,
"$schema": "https://schemas.runtipi.io/dynamic-compose.json",
"services": [
{
"name": "nas-samba",
"image": "git.alexzaw.dev/alexz/nas-samba:2.0.0",
"isMain": true,
"internalPort": 8080,
"hostname": "nas.alexzaw.dev",
"addPorts": [
{ "hostPort": 445, "containerPort": 445, "interface": "${BIND_IP}" },
{ "hostPort": 139, "containerPort": 139, "interface": "${BIND_IP}" }
],
"volumes": [
{ "hostPath": "${NAS_PATH:-/nas}", "containerPath": "/nas" },
{ "hostPath": "${APP_DATA_DIR}/data/config", "containerPath": "/config" },
{ "hostPath": "${APP_DATA_DIR}/data/samba", "containerPath": "/var/lib/samba" }
],
"environment": [
{ "key": "SMB_PASSWORD", "value": "${SMB_PASSWORD}" },
{ "key": "JWT_SECRET", "value": "${JWT_SECRET}" },
{ "key": "TZ", "value": "${TZ}" }
],
"healthCheck": {
"test": "curl --fail http://127.0.0.1:8080/api/health || exit 1",
"interval": "30s",
"timeout": "10s",
"retries": 3,
"startPeriod": "15s"
}
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -1,51 +0,0 @@
FROM ubuntu:24.04
LABEL maintainer="alexz"
LABEL description="NAS Samba + CloudNAS file manager"
ENV DEBIAN_FRONTEND=noninteractive
# Install runtime: Samba, supervisor, tini, Node.js 20.x, build tools for native modules
RUN apt-get update && \
apt-get install -y --no-install-recommends \
samba \
samba-common-bin \
supervisor \
curl \
ca-certificates \
tini \
python3 \
make \
g++ \
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/*
# Copy app source
COPY app/ /app/
WORKDIR /app
# Install production dependencies (compiles better-sqlite3 native module)
RUN npm ci --omit=dev && npm cache clean --force
# Clean up
RUN rm -rf /var/lib/apt/lists/* /usr/share/doc /usr/share/man
# Create directories
RUN mkdir -p /nas /config /var/log/supervisor /var/log/samba /run/samba
# Samba configuration
COPY smb.conf /etc/samba/smb.conf
# Supervisord configuration
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Entrypoint
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 8080 139 445
ENTRYPOINT ["tini", "--"]
CMD ["/entrypoint.sh"]

View File

@@ -1,13 +0,0 @@
{
"files": {
"main.css": "/static/css/main.6f136b27.css",
"main.js": "/static/js/main.f1998df4.js",
"index.html": "/index.html",
"main.6f136b27.css.map": "/static/css/main.6f136b27.css.map",
"main.f1998df4.js.map": "/static/js/main.f1998df4.js.map"
},
"entrypoints": [
"static/css/main.6f136b27.css",
"static/js/main.f1998df4.js"
]
}

View File

@@ -1 +0,0 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#0F172A"/><meta name="description" content="CloudSync - Enterprise File Manager"/><title>CloudSync - Enterprise File Manager</title><script defer="defer" src="/static/js/main.f1998df4.js"></script><link href="/static/css/main.6f136b27.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,46 +0,0 @@
/**
* @license React
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license lucide-react v0.312.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -1,39 +0,0 @@
{
"name": "cloudsync-frontend",
"version": "2.0.0",
"private": true,
"dependencies": {
"axios": "^1.6.5",
"clsx": "^2.1.1",
"date-fns": "^3.3.0",
"lucide-react": "^0.312.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View File

@@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@@ -1,14 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#0F172A" />
<meta name="description" content="CloudSync - Enterprise File Manager" />
<title>CloudSync - Enterprise File Manager</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

View File

@@ -1 +0,0 @@
/* Additional app-specific styles */

View File

@@ -1,51 +0,0 @@
import React, { useState, useCallback, useRef, useEffect } from 'react';
import { Toaster, toast } from 'sonner';
import { AuthProvider, useAuth } from './context/AuthContext';
import { ThemeProvider, useTheme } from './context/ThemeContext';
import LoginPage from './components/LoginPage';
import Dashboard from './components/Dashboard';
import SharePage from './components/SharePage';
import LoadingScreen from './components/ui/LoadingScreen';
function AppContent() {
const { user, loading } = useAuth();
// Handle /share/:token routes without React Router
const path = window.location.pathname;
const shareMatch = path.match(/^\/share\/([a-f0-9]+)$/);
if (shareMatch) {
return <SharePage token={shareMatch[1]} />;
}
if (loading) {
return <LoadingScreen />;
}
if (!user) {
return <LoginPage />;
}
return <Dashboard />;
}
function App() {
return (
<ThemeProvider>
<AuthProvider>
<Toaster
position="top-right"
richColors
closeButton
toastOptions={{
style: {
fontFamily: 'Inter, system-ui, sans-serif',
},
}}
/>
<AppContent />
</AuthProvider>
</ThemeProvider>
);
}
export default App;

File diff suppressed because it is too large Load Diff

View File

@@ -1,141 +0,0 @@
import React, { useState } from 'react';
import { useAuth } from '../context/AuthContext';
import { toast } from 'sonner';
import { Cloud, Eye, EyeOff, Loader2 } from 'lucide-react';
function LoginPage() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [loading, setLoading] = useState(false);
const { login } = useAuth();
const handleSubmit = async (e) => {
e.preventDefault();
if (!username.trim() || !password.trim()) {
toast.error('Please enter username and password');
return;
}
setLoading(true);
try {
const user = await login(username, password);
toast.success(`Welcome back, ${user.name}!`);
} catch (error) {
const message = error.response?.data?.detail || 'Invalid credentials';
toast.error(message);
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen flex">
{/* Left side - Background image */}
<div
className="hidden lg:flex lg:w-1/2 xl:w-3/5 relative bg-cover bg-center"
style={{
backgroundImage: 'url(https://images.pexels.com/photos/28428586/pexels-photo-28428586.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940)'
}}
>
<div className="absolute inset-0 bg-black/40"></div>
<div className="relative z-10 flex flex-col justify-end p-12 text-white">
<h1 className="font-secondary text-4xl font-bold mb-4">CloudSync</h1>
<p className="text-lg text-white/80 max-w-md">
Enterprise-grade file management with secure sharing, storage quotas, and comprehensive activity tracking.
</p>
</div>
</div>
{/* Right side - Login form */}
<div className="flex-1 flex items-center justify-center p-8 bg-background">
<div className="w-full max-w-md">
{/* Logo for mobile */}
<div className="lg:hidden flex items-center gap-3 mb-8">
<div className="w-10 h-10 bg-primary rounded-lg flex items-center justify-center">
<Cloud className="w-5 h-5 text-primary-foreground" />
</div>
<span className="font-secondary text-2xl font-bold text-foreground">CloudSync</span>
</div>
<div className="mb-8">
<h2 className="font-secondary text-3xl font-bold text-foreground mb-2">Sign in</h2>
<p className="text-muted-foreground">Enter your credentials to access your files</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="username" className="block text-sm font-medium text-foreground mb-2">
Username
</label>
<input
id="username"
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Enter your username"
disabled={loading}
data-testid="login-username-input"
className="w-full h-11 px-4 rounded-lg border border-input bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring transition-all disabled:opacity-50"
autoComplete="username"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-foreground mb-2">
Password
</label>
<div className="relative">
<input
id="password"
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter your password"
disabled={loading}
data-testid="login-password-input"
className="w-full h-11 px-4 pr-12 rounded-lg border border-input bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring transition-all disabled:opacity-50"
autoComplete="current-password"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
data-testid="toggle-password-visibility"
>
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
<button
type="submit"
disabled={loading}
data-testid="login-submit-btn"
className="w-full h-11 bg-primary text-primary-foreground font-medium rounded-lg hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 transition-all disabled:opacity-50 flex items-center justify-center gap-2"
>
{loading ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Signing in...
</>
) : (
'Sign in'
)}
</button>
</form>
<div className="mt-8 p-4 bg-muted rounded-lg">
<p className="text-sm text-muted-foreground">
Use your NAS credentials to sign in. Contact your administrator if you need access.
</p>
</div>
</div>
</div>
</div>
);
}
export default LoginPage;

View File

@@ -1,182 +0,0 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Cloud, CheckCircle, Lock, FileText, AlertCircle } from 'lucide-react';
import { formatBytes } from '../lib/utils';
import * as api from '../lib/api';
export default function SharePage({ token }) {
const [fileInfo, setFileInfo] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
const [password, setPassword] = useState('');
const [downloaded, setDownloaded] = useState(false);
const [downloading, setDownloading] = useState(false);
const doDownload = useCallback(async (pwd) => {
if (!fileInfo || downloading) return;
setDownloading(true);
setError(null);
try {
const url = api.downloadSharedFile(token, pwd || null);
const res = await fetch(url, { method: 'POST' });
if (!res.ok) {
const data = await res.json();
throw new Error(data.detail || 'Download failed');
}
const blob = await res.blob();
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = fileInfo.name;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(blobUrl);
setDownloaded(true);
} catch (err) {
setError(err.message);
setDownloading(false);
}
}, [fileInfo, downloading, token]);
useEffect(() => {
async function load() {
try {
const res = await api.getSharedFileInfo(token);
setFileInfo(res.data);
} catch (err) {
const status = err.response?.status;
if (status === 410) {
setError('This share link has expired.');
} else if (status === 404) {
setError('Share link not found.');
} else {
setError('Failed to load shared file.');
}
} finally {
setLoading(false);
}
}
load();
}, [token]);
// Auto-download if no password required
useEffect(() => {
if (fileInfo && !fileInfo.requires_password && !downloaded && !downloading) {
doDownload(null);
}
}, [fileInfo, downloaded, downloading, doDownload]);
if (loading) {
return (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="animate-spin w-8 h-8 border-2 border-primary border-t-transparent rounded-full"></div>
</div>
);
}
if (error && !fileInfo) {
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<div className="bg-card border border-border rounded-lg shadow-lg p-8 max-w-md w-full text-center">
<AlertCircle className="w-12 h-12 text-destructive mx-auto mb-4" />
<h2 className="text-xl font-semibold mb-2">Unavailable</h2>
<p className="text-muted-foreground">{error}</p>
</div>
</div>
);
}
// Downloaded state
if (downloaded) {
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<div className="bg-card border border-border rounded-lg shadow-lg p-8 max-w-md w-full text-center">
<div className="flex items-center justify-center gap-2 mb-6">
<Cloud className="w-6 h-6 text-primary" />
<span className="text-lg font-semibold">CloudSync</span>
</div>
<CheckCircle className="w-16 h-16 text-green-500 mx-auto mb-4" />
<h2 className="text-xl font-semibold mb-2">File Downloaded</h2>
<div className="flex items-center gap-3 justify-center p-3 bg-accent/50 rounded-lg mt-4">
<FileText className="w-6 h-6 text-muted-foreground flex-shrink-0" />
<div className="text-left min-w-0">
<p className="font-medium text-sm truncate">{fileInfo.name}</p>
<p className="text-xs text-muted-foreground">{formatBytes(fileInfo.size)}</p>
</div>
</div>
<p className="text-sm text-muted-foreground mt-4">This link has been used and is no longer active.</p>
</div>
</div>
);
}
// Password required state
if (fileInfo.requires_password && !downloaded) {
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<div className="bg-card border border-border rounded-lg shadow-lg p-8 max-w-md w-full">
<div className="flex items-center gap-2 mb-6">
<Cloud className="w-6 h-6 text-primary" />
<span className="text-lg font-semibold">CloudSync</span>
</div>
<div className="flex items-center gap-4 mb-6 p-4 bg-accent/50 rounded-lg">
<FileText className="w-10 h-10 text-muted-foreground flex-shrink-0" />
<div className="min-w-0">
<p className="font-medium truncate">{fileInfo.name}</p>
<p className="text-sm text-muted-foreground">{formatBytes(fileInfo.size)}</p>
</div>
</div>
{error && (
<div className="mb-4 p-3 bg-destructive/10 text-destructive text-sm rounded-lg">
{error}
</div>
)}
<div className="mb-4">
<label className="block text-sm font-medium mb-2">
<Lock className="w-4 h-4 inline mr-1" />
This file is password protected
</label>
<input
type="password"
value={password}
onChange={(e) => { setPassword(e.target.value); setError(null); }}
onKeyDown={(e) => { if (e.key === 'Enter' && password) doDownload(password); }}
placeholder="Enter password"
className="w-full h-10 px-4 bg-background border border-input rounded-lg focus:outline-none focus:ring-2 focus:ring-ring"
autoFocus
/>
</div>
<button
onClick={() => doDownload(password)}
disabled={downloading || !password}
className="w-full h-10 bg-primary text-primary-foreground rounded-lg hover:opacity-90 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
{downloading ? (
<div className="animate-spin w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full"></div>
) : (
'Download'
)}
</button>
</div>
</div>
);
}
// Downloading state (auto-download in progress, no password)
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<div className="bg-card border border-border rounded-lg shadow-lg p-8 max-w-md w-full text-center">
<div className="flex items-center justify-center gap-2 mb-6">
<Cloud className="w-6 h-6 text-primary" />
<span className="text-lg font-semibold">CloudSync</span>
</div>
<div className="animate-spin w-8 h-8 border-2 border-primary border-t-transparent rounded-full mx-auto mb-4"></div>
<p className="text-muted-foreground">Downloading {fileInfo.name}...</p>
</div>
</div>
);
}

View File

@@ -1,18 +0,0 @@
import React from 'react';
import { cn } from '../../lib/utils';
export function LoadingScreen() {
return (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="flex flex-col items-center gap-4">
<div className="relative w-12 h-12">
<div className="absolute inset-0 rounded-full border-2 border-muted"></div>
<div className="absolute inset-0 rounded-full border-2 border-primary border-t-transparent animate-spin"></div>
</div>
<p className="text-muted-foreground font-medium">Loading CloudSync...</p>
</div>
</div>
);
}
export default LoadingScreen;

View File

@@ -1,69 +0,0 @@
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
import { login as apiLogin, getMe } from '../lib/api';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [token, setToken] = useState(localStorage.getItem('token'));
const checkAuth = useCallback(async () => {
const storedToken = localStorage.getItem('token');
if (!storedToken) {
setLoading(false);
return;
}
try {
const response = await getMe();
setUser(response.data);
setToken(storedToken);
} catch (error) {
localStorage.removeItem('token');
setUser(null);
setToken(null);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
checkAuth();
}, [checkAuth]);
const login = async (username, password) => {
const response = await apiLogin(username, password);
const { token: newToken, user: userData } = response.data;
localStorage.setItem('token', newToken);
setToken(newToken);
setUser(userData);
return userData;
};
const logout = () => {
localStorage.removeItem('token');
setToken(null);
setUser(null);
};
const updateUser = useCallback((userData) => {
setUser(prev => ({ ...prev, ...userData }));
}, []);
return (
<AuthContext.Provider value={{ user, token, loading, login, logout, updateUser, checkAuth }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}

View File

@@ -1,36 +0,0 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState(() => {
const stored = localStorage.getItem('theme');
if (stored) return stored;
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
});
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove('light', 'dark');
root.classList.add(theme);
localStorage.setItem('theme', theme);
}, [theme]);
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}

View File

@@ -1,116 +0,0 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Manrope:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--background: #FFFFFF;
--foreground: #0A0A0A;
--card: #FFFFFF;
--card-foreground: #0A0A0A;
--popover: #FFFFFF;
--popover-foreground: #0A0A0A;
--primary: #0F172A;
--primary-foreground: #F8FAFC;
--secondary: #F1F5F9;
--secondary-foreground: #0F172A;
--muted: #F1F5F9;
--muted-foreground: #64748B;
--accent: #F1F5F9;
--accent-foreground: #0F172A;
--destructive: #EF4444;
--destructive-foreground: #F8FAFC;
--border: #E2E8F0;
--input: #E2E8F0;
--ring: #0F172A;
--radius: 0.5rem;
}
.dark {
--background: #020617;
--foreground: #F8FAFC;
--card: #0F172A;
--card-foreground: #F8FAFC;
--popover: #0F172A;
--popover-foreground: #F8FAFC;
--primary: #F8FAFC;
--primary-foreground: #0F172A;
--secondary: #1E293B;
--secondary-foreground: #F8FAFC;
--muted: #1E293B;
--muted-foreground: #94A3B8;
--accent: #1E293B;
--accent-foreground: #F8FAFC;
--destructive: #7F1D1D;
--destructive-foreground: #F8FAFC;
--border: #1E293B;
--input: #1E293B;
--ring: #D1D5DB;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body, #root {
height: 100%;
}
body {
font-family: 'Inter', system-ui, sans-serif;
background-color: var(--background);
color: var(--foreground);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--muted);
}
::-webkit-scrollbar-thumb {
background: var(--muted-foreground);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--foreground);
}
/* Skeleton loader animation */
@keyframes skeleton-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.skeleton {
animation: skeleton-pulse 1.5s ease-in-out infinite;
background: linear-gradient(90deg, var(--muted) 25%, var(--border) 50%, var(--muted) 75%);
background-size: 200% 100%;
}
/* File drop zone styles */
.drop-zone-active {
border-color: var(--ring) !important;
background-color: var(--accent) !important;
}
/* Focus visible styles */
.focus-visible:focus {
outline: 2px solid var(--ring);
outline-offset: 2px;
}
/* Transition utilities */
.transition-default {
transition: all 200ms ease-in-out;
}

View File

@@ -1,11 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@@ -1,136 +0,0 @@
import axios from 'axios';
const API_URL = process.env.REACT_APP_BACKEND_URL || '';
const api = axios.create({
baseURL: API_URL,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor to add auth token
api.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Response interceptor to handle auth errors
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.reload();
}
return Promise.reject(error);
}
);
// Auth
export const login = (username, password) =>
api.post('/api/auth/login', { username, password });
export const getMe = () =>
api.get('/api/auth/me');
// Users
export const getUsers = () =>
api.get('/api/users');
export const createUser = (userData) =>
api.post('/api/users', userData);
export const updateUser = (userId, userData) =>
api.put(`/api/users/${userId}`, userData);
export const deleteUser = (userId) =>
api.delete(`/api/users/${userId}`);
// Files
export const getFiles = (parentId = null, search = null) => {
const params = new URLSearchParams();
if (parentId) params.append('parent_id', parentId);
if (search) params.append('search', search);
return api.get(`/api/files?${params.toString()}`);
};
export const getFile = (fileId) =>
api.get(`/api/files/${fileId}`);
export const createFolder = (name, parentId = null) =>
api.post('/api/files/folder', { name, parent_id: parentId, is_folder: true });
export const uploadFile = (file, parentId, onProgress) => {
const formData = new FormData();
formData.append('file', file);
if (parentId) {
formData.append('parent_id', parentId);
}
return api.post('/api/files/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: (progressEvent) => {
if (onProgress) {
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
onProgress(progress);
}
},
});
};
export const renameFile = (fileId, newName) =>
api.put(`/api/files/${fileId}/rename`, { new_name: newName });
export const moveFile = (fileId, destinationId) =>
api.put(`/api/files/${fileId}/move`, { destination_id: destinationId });
export const copyFile = (fileId, destinationId) =>
api.post(`/api/files/${fileId}/copy`, { destination_id: destinationId });
export const deleteFile = (fileId) =>
api.delete(`/api/files/${fileId}`);
export const bulkDeleteFiles = (fileIds) =>
api.post('/api/files/bulk-delete', fileIds);
export const getDownloadUrl = (fileId) =>
`${API_URL}/api/files/${fileId}/download`;
export const getPreviewUrl = (fileId) =>
`${API_URL}/api/files/${fileId}/preview`;
export const getBreadcrumb = (fileId) =>
api.get(`/api/files/${fileId}/breadcrumb`);
// Shares
export const getShares = () =>
api.get('/api/shares');
export const createShare = (fileId, expiresInHours = 24, password = null) =>
api.post('/api/shares', { file_id: fileId, expires_in_hours: expiresInHours, password });
export const deleteShare = (shareId) =>
api.delete(`/api/shares/${shareId}`);
export const getSharedFileInfo = (token) =>
api.get(`/api/share/${token}`);
export const downloadSharedFile = (token, password = null) => {
const params = password ? `?password=${encodeURIComponent(password)}` : '';
return `${API_URL}/api/share/${token}/download${params}`;
};
// Activity
export const getActivity = (limit = 50) =>
api.get(`/api/activity?limit=${limit}`);
// Stats
export const getStats = () =>
api.get('/api/stats');
export default api;

View File

@@ -1,33 +0,0 @@
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs) {
return twMerge(clsx(inputs));
}
export function formatBytes(bytes, decimals = 1) {
if (bytes === 0) return '0 B';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
export function formatDate(dateString) {
if (!dateString) return '-';
const date = new Date(dateString);
const now = new Date();
const diff = Math.floor((now - date) / 1000);
if (diff < 60) return 'Just now';
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`;
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
}
export function getFileExtension(filename) {
return filename.includes('.') ? filename.split('.').pop().toLowerCase() : '';
}

Some files were not shown because too many files have changed in this diff Show More