Compare commits
243 Commits
0461ffec7c
...
claude-arc
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e053b0936 | |||
| 19fa1c7821 | |||
| 62adaf14b3 | |||
|
|
22cca9e08f | ||
|
|
cc711c89ea | ||
| c3578e2452 | |||
| 6bfcd2b340 | |||
| b833d3604f | |||
| 528e0d1bdf | |||
| 65350024ec | |||
| 77b7bedf28 | |||
| c31d42e3a2 | |||
| 03b16eab6b | |||
| 201e4895e4 | |||
| 4cb8e3cd2c | |||
|
|
e4cadf08dc | ||
| 92ed300f97 | |||
| bcc9060aab | |||
| 456de8e11a | |||
| 8fb5c904d9 | |||
| 57b71da781 | |||
| 03a9b49bf0 | |||
| bdd2e4e2a5 | |||
| 580eec66cd | |||
| 6599fc5a21 | |||
| b1697d6e1e | |||
| 888acdbd7e | |||
| 3c67eb75c3 | |||
| 5b213d8e51 | |||
| f755a054a8 | |||
| 96eb8bd83e | |||
| 7d07b38f0c | |||
| d6b619fcfe | |||
|
|
ea57aa129d | ||
| d7269f94fb | |||
| 20043aacab | |||
| 4edbb9004d | |||
| 8bbfeafabb | |||
| 2d16912d18 | |||
| 98078e843c | |||
| f29404405a | |||
| 6c444a474c | |||
| f862c2049a | |||
| 6cb0702b0d | |||
| f94a3937be | |||
| ae29c75e28 | |||
| 3461f47bd2 | |||
| d3ab1fb060 | |||
| 9cd6f56f8c | |||
| 1c4bfb8cc5 | |||
| 145d871b42 | |||
| 7ea70458b3 | |||
| 37525c7e54 | |||
| b92c179f9b | |||
| f3a2d2f006 | |||
| 46b0e1120a | |||
| e6387c3861 | |||
| 2b10b30d44 | |||
| d5e58923bc | |||
| 4da14d5a23 | |||
| d3dd39ceeb | |||
| 68da19bafe | |||
| 8ffb7e01ae | |||
| 1f8a4d5ae3 | |||
| 9f0e1c7f8b | |||
| cd07a5b8c8 | |||
| 7fd226d229 | |||
| 5bffff1d9b | |||
| 61b1dea366 | |||
| c94cbd7f36 | |||
| 618e023996 | |||
| 0104827234 | |||
| 92a97e140f | |||
| b9d573aeac | |||
| 3c438ca093 | |||
| d6bc7128df | |||
| 4b0659bd47 | |||
| 2351ad84bc | |||
| a16b5a232e | |||
| 0ffb0f7256 | |||
| 37ae3a06de | |||
| d0ff3536d2 | |||
| 0104b45331 | |||
| efc3ad00af | |||
| 7ac32e9199 | |||
| cb54689e7c | |||
| 992db16848 | |||
| 16b7a66c01 | |||
| 1def782149 | |||
| 55c11cce90 | |||
| ed21a14f68 | |||
| 004c58b445 | |||
| 8c9ebea489 | |||
| 19cb09f05e | |||
| ae86df8732 | |||
| a2f0b40fa8 | |||
| bf60412640 | |||
| 5f057c50ed | |||
| b2e38b3cb4 | |||
| 47e1790a8b | |||
| b67b8f18a4 | |||
| c6749fe856 | |||
| 4c7ff9d6a0 | |||
| e93edb73af | |||
| 9a6e2f67e6 | |||
| 84b1eb3f5d | |||
| 1bd5a21a94 | |||
| 5c3147536c | |||
| 8656441976 | |||
| 0d52d54eed | |||
| 1b59e304b0 | |||
| fb915487dc | |||
| a3b02b694e | |||
| 9b2a42bdc9 | |||
| 98f3cc95eb | |||
| 12f626b088 | |||
| b9b3f89910 | |||
| 24594915a9 | |||
| 6f6538fa73 | |||
| 239179931c | |||
| f1793baa57 | |||
| 418390fe8d | |||
| 4fd8688685 | |||
| f410510a7f | |||
| 274125e862 | |||
| 837dffddd5 | |||
| 1ef9d21ba4 | |||
| 9307cab1bb | |||
| e462edd99b | |||
| 48d0407c79 | |||
| 3c427af6fe | |||
| 5d54ed6f80 | |||
| 685488c7d4 | |||
| ec40aa2ec1 | |||
| 498926ae5d | |||
| 046552d09a | |||
| 27c46542e8 | |||
| 0c952a2623 | |||
| 50cdd3ea1c | |||
| 7c76016fcf | |||
| 0dca06fbc8 | |||
| 4c067c14d8 | |||
| 529842a411 | |||
| 99847c3ff0 | |||
| 96d4e32672 | |||
| c3581c7ecc | |||
| 657081678f | |||
| 89e8f5cffc | |||
| 5e0004c0d8 | |||
| aa071a1fdb | |||
| 2b9688ce44 | |||
| fa398e8c86 | |||
| c7cf401b0a | |||
| 38530ea0df | |||
| c933d6e6da | |||
| e4b648e447 | |||
| e5d4b4d2e5 | |||
| 35e0d67446 | |||
| 747f71e27c | |||
| 6e5656a00b | |||
| ab004b78eb | |||
| b42bac35bb | |||
| 7b874169cb | |||
| 2f7a51d2b7 | |||
| 6097bcbe8f | |||
| f1fd3572c5 | |||
| 3d715664db | |||
| 31cb8f6db5 | |||
| abe7a7ab08 | |||
| 46fe4f5557 | |||
| 35e4f1f6b7 | |||
| 767526054e | |||
| 69062bd828 | |||
| b3259d2981 | |||
| dc463f4cf0 | |||
| 74f4ab982e | |||
| 070e358463 | |||
| 99fc5a5600 | |||
| ee6cb6c90d | |||
| b52ba03be4 | |||
| 38c4eea2f0 | |||
| 838b33d6c5 | |||
| 470517a00f | |||
| d44a3c1a3b | |||
| 865a96c2ec | |||
| 21bbeef579 | |||
| 8523c79999 | |||
| 96153fa557 | |||
| 4d1bc9dbd0 | |||
| ca826a6229 | |||
| a7691b16f0 | |||
| d87429f98d | |||
| ccd1fbc52f | |||
| b0ba737d0d | |||
| 33aa6d361e | |||
| 1eee23953c | |||
| 1c548281b0 | |||
| 38ebb88ac6 | |||
| e4fa0ba9cd | |||
| cb50b25081 | |||
| 6d8015bdc9 | |||
| 982a4bbff9 | |||
| fa571c9ccd | |||
| ef0058c93f | |||
| e2e7c44bf6 | |||
| 3aadd164f0 | |||
| d6cafc67b2 | |||
| 8b9b6e798a | |||
| b55708721c | |||
| 3e50f5a465 | |||
| e13cc2b851 | |||
| 6e1d7efa6d | |||
| ed48c37706 | |||
| 0d773fba51 | |||
| f1ba1f050d | |||
| 62ca42bb18 | |||
| bc34fad485 | |||
| 500b5f4045 | |||
| 24d28c649c | |||
| 6fd57b0ce2 | |||
| 2dae9f667e | |||
| cb7e309915 | |||
| 55ca6fe620 | |||
| 5478623d19 | |||
| 302c52c784 | |||
| 0020c539ea | |||
| 6c790f84aa | |||
| 0ab6bb934d | |||
| a5871d399b | |||
| 9c2c67fbe1 | |||
| b6a6623406 | |||
| beedccdc29 | |||
| 11aaf00d8d | |||
| 21c1fa5d9a | |||
| 509814f3a8 | |||
| 203e04101b | |||
| 2d98ca843f | |||
| 919f5904f8 | |||
| ea2a1ba94d | |||
| 776666a77a | |||
| 9bde91f047 | |||
| 03c1181186 | |||
| 022c455334 |
1
.claude-bundle/ENCODING_PROBE.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
HELLO-ARCHITECT-PROBE-1234
|
||||||
45
.github/workflows/renovate.yml
vendored
@@ -1,45 +0,0 @@
|
|||||||
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
@@ -1,23 +0,0 @@
|
|||||||
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
|
|
||||||
56
apps/apricot-blossom/config.json
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
35
apps/apricot-blossom/docker-compose.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
apps/apricot-blossom/metadata/logo.jpg
Normal file
|
After Width: | Height: | Size: 34 KiB |
3
apps/cfddns/build/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Anything Docker / build temp products created in this dir during a build.
|
||||||
|
*.tar
|
||||||
|
*.tar.gz
|
||||||
66
apps/cfddns/build/README.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# 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>
|
||||||
39
apps/cfddns/build/build.sh
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/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"
|
||||||
87
apps/cfddns/config.json
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
{
|
||||||
|
"$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"
|
||||||
|
}
|
||||||
37
apps/cfddns/docker-compose.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
26
apps/cfddns/docker-compose.yml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
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
|
||||||
39
apps/cfddns/metadata/description.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# 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
|
||||||
BIN
apps/cfddns/metadata/logo.jpg
Normal file
|
After Width: | Height: | Size: 71 KiB |
@@ -1,11 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# 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) |
|
|
||||||
2
apps/cistech-tunnel/build/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Large binary files - track tar.gz but not 7z
|
||||||
|
*.7z
|
||||||
110
apps/cistech-tunnel/build/Dockerfile
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
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
|
||||||
67
apps/cistech-tunnel/build/README.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# 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
|
||||||
22
apps/cistech-tunnel/build/build.sh
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/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 ""
|
||||||
BIN
apps/cistech-tunnel/cistech-tunnel.7z
Normal file
@@ -1,53 +1,76 @@
|
|||||||
{
|
{
|
||||||
"name": "Cistech Tunnel",
|
"name": "cistech Tunnel",
|
||||||
"id": "cistech-tunnel",
|
|
||||||
"available": true,
|
"available": true,
|
||||||
"short_desc": "Cistech VPN client container with noVNC.",
|
"port": 6092,
|
||||||
"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": "latest",
|
|
||||||
"source": "local",
|
|
||||||
"exposable": true,
|
"exposable": true,
|
||||||
"dynamic_config": true,
|
"dynamic_config": true,
|
||||||
"no_gui": false,
|
"id": "cistech-tunnel",
|
||||||
|
"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": [
|
||||||
{
|
{
|
||||||
"label": "VPN URL",
|
"type": "email",
|
||||||
"type": "text",
|
"label": "VPN Email",
|
||||||
"env_variable": "OC_URL",
|
"hint": "Email address for VPN SSO login (configured in /shared/openconnect-vpn script)",
|
||||||
"required": true,
|
"placeholder": "your-email@company.com",
|
||||||
"default": "https://vpn.cistech.net/Employees"
|
"required": false,
|
||||||
|
"env_variable": "VPN_EMAIL",
|
||||||
|
"default": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "VNC Password",
|
|
||||||
"type": "password",
|
"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",
|
||||||
|
"label": "TOTP Secret",
|
||||||
|
"hint": "Base32 TOTP secret for 2FA (configured in /shared/openconnect-vpn script)",
|
||||||
|
"placeholder": "",
|
||||||
|
"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",
|
||||||
|
"hint": "Password for noVNC web interface",
|
||||||
|
"placeholder": "cisco123",
|
||||||
|
"required": false,
|
||||||
"env_variable": "VNC_PASSWORD",
|
"env_variable": "VNC_PASSWORD",
|
||||||
"required": true,
|
"default": ""
|
||||||
"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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,23 +1,74 @@
|
|||||||
{
|
{
|
||||||
|
"schemaVersion": 2,
|
||||||
"services": [
|
"services": [
|
||||||
{
|
{
|
||||||
"name": "cistech-tunnel",
|
"name": "cistech-tunnel",
|
||||||
"image": "cistech-vpn:latest",
|
"image": "git.alexzaw.dev/alexz/openconnect-vpn:latest",
|
||||||
"isMain": true,
|
"environment": [
|
||||||
"internalPort": 6902,
|
{
|
||||||
"privileged": true,
|
"key": "VPN_EMAIL",
|
||||||
"capAdd": ["NET_ADMIN"],
|
"value": "${VPN_EMAIL}"
|
||||||
"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"
|
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"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": [
|
"volumes": [
|
||||||
{ "hostPath": "${APP_DATA_DIR}/data", "containerPath": "/root" }
|
{
|
||||||
]
|
"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,
|
||||||
|
"extraLabels": {
|
||||||
|
"runtipi.managed": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,29 @@
|
|||||||
services:
|
services:
|
||||||
cistech-tunnel:
|
cistech-tunnel:
|
||||||
image: cistech-vpn:latest
|
image: 'git.alexzaw.dev/alexz/openconnect-vpn:latest'
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
cistech-tunnel_runtipi_network:
|
|
||||||
gw_priority: 0
|
|
||||||
tipi_main_network:
|
|
||||||
gw_priority: 1
|
|
||||||
environment:
|
|
||||||
OC_URL: ${OC_URL}
|
|
||||||
OC_SERVERCERT: ${OC_SERVERCERT}
|
|
||||||
OC_USER: ${OC_USER}
|
|
||||||
VNC_PASSWORD: ${VNC_PASSWORD}
|
|
||||||
NOVNC_PORT: "6902"
|
|
||||||
ports:
|
|
||||||
- ${APP_PORT}:6902
|
|
||||||
volumes:
|
volumes:
|
||||||
- ${APP_DATA_DIR}/data:/root
|
- '${APP_DATA_DIR}/config:/config'
|
||||||
|
- '${APP_DATA_DIR}:/runtime'
|
||||||
|
- '/etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared:/shared'
|
||||||
|
- '/etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared/xstartup:/root/.vnc/xstartup:ro'
|
||||||
|
environment:
|
||||||
|
- 'VPN_EMAIL=${VPN_EMAIL}'
|
||||||
|
- 'VPN_PASSWORD=${VPN_PASSWORD}'
|
||||||
|
- 'VPN_TOTP_SECRET=${VPN_TOTP_SECRET}'
|
||||||
|
- 'VPN_HOST=${VPN_HOST}'
|
||||||
|
- 'VNC_PASSWORD=${VNC_PASSWORD}'
|
||||||
|
- 'TZ=${TZ}'
|
||||||
|
- 'TARGET_IP=${TARGET_IP}'
|
||||||
labels:
|
labels:
|
||||||
generated: true
|
'runtipi.managed': true
|
||||||
traefik.enable: true
|
privileged: true
|
||||||
traefik.docker.network: runtipi_tipi_main_network
|
cap_add:
|
||||||
traefik.http.middlewares.cistech-tunnel-runtipi-web-redirect.redirectscheme.scheme: https
|
- 'NET_ADMIN'
|
||||||
traefik.http.services.cistech-tunnel-runtipi.loadbalancer.server.port: "6902"
|
stop_grace_period: '30s'
|
||||||
traefik.http.routers.cistech-tunnel-runtipi-insecure.rule: Host(`${APP_DOMAIN}`)
|
devices:
|
||||||
traefik.http.routers.cistech-tunnel-runtipi-insecure.entrypoints: web
|
- '/dev/net/tun'
|
||||||
traefik.http.routers.cistech-tunnel-runtipi-insecure.service: cistech-tunnel-runtipi
|
x-runtipi:
|
||||||
traefik.http.routers.cistech-tunnel-runtipi-insecure.middlewares: cistech-tunnel-runtipi-web-redirect
|
internal_port: 6092
|
||||||
traefik.http.routers.cistech-tunnel-runtipi.rule: Host(`${APP_DOMAIN}`)
|
is_main: true
|
||||||
traefik.http.routers.cistech-tunnel-runtipi.entrypoints: websecure
|
x-runtipi:
|
||||||
traefik.http.routers.cistech-tunnel-runtipi.service: cistech-tunnel-runtipi
|
schema_version: 2
|
||||||
traefik.http.routers.cistech-tunnel-runtipi.tls.certresolver: myresolver
|
|
||||||
runtipi.managed: true
|
|
||||||
|
|||||||
1217
apps/cistech-tunnel/metadata/compose-schema.json
Normal file
@@ -1,20 +1,147 @@
|
|||||||
# Dockerized OpenConnect-SSO with noVNC and Cloudflared
|
# Cistech Tunnel - OpenConnect-SSO VPN
|
||||||
|
|
||||||
## Setup
|
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.
|
||||||
1) Copy `.env.example` to `.env` and fill values (URLs, servercert pins, VNC passwords, cloudflared tokens).
|
|
||||||
|
|
||||||
2) First-time SSO: leave `OC_SSO_ARGS_*=--browser-display-mode visible`.
|
## Features
|
||||||
|
|
||||||
3) Build and start:
|
- **OpenConnect-SSO** - Handles SAML/SSO authentication automatically
|
||||||
docker compose build
|
- **Playwright browser** - Headless Chromium for SSO login
|
||||||
docker compose up -d vpn_a
|
- **Web-based access** via noVNC (port 6092)
|
||||||
# Open http://localhost:6901, complete SSO.
|
- **Auto-login with TOTP** - Credentials stored in keyring
|
||||||
# After success, attach app containers or start cloudflared_a.
|
- **LAN routing** - Other machines on your network can reach VPN targets
|
||||||
|
- **Lightweight** - No systemd, no Cisco bloat
|
||||||
|
|
||||||
4) Optional: switch to headless after first login:
|
## Architecture
|
||||||
Set `OC_SSO_ARGS_*=--browser-display-mode hidden` (or `headless`) and restart the vpn service.
|
|
||||||
|
|
||||||
## Notes
|
```
|
||||||
- Each VPN runs in its own net namespace; routes from one cannot affect the other or the host.
|
LAN Devices ──► Linux Host ──► Container (172.30.0.10) ──► VPN Tunnel ──► Target
|
||||||
- DNS from the VPN applies within its container namespace and attached services only.
|
│ │
|
||||||
- Persisted state lives in the named volumes mounted at `/root` (Playwright cache, configs).
|
│ └── openconnect-sso + openconnect
|
||||||
|
│ └── 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)
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 609 KiB After Width: | Height: | Size: 6.0 KiB |
83
apps/cistech-tunnel/shared/entrypoint.sh
Executable file
@@ -0,0 +1,83 @@
|
|||||||
|
#!/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
|
||||||
123
apps/cistech-tunnel/shared/host-routing.sh
Executable file
@@ -0,0 +1,123 @@
|
|||||||
|
#!/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
|
||||||
55
apps/cistech-tunnel/shared/install-host-services.sh
Executable file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/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"
|
||||||
662
apps/cistech-tunnel/shared/openconnect-vpn
Executable file
@@ -0,0 +1,662 @@
|
|||||||
|
#!/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
|
||||||
12
apps/cistech-tunnel/shared/startup-vnc.sh
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/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
|
||||||
24
apps/cistech-tunnel/shared/uninstall-host-services.sh
Executable file
@@ -0,0 +1,24 @@
|
|||||||
|
#!/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."
|
||||||
53
apps/cistech-tunnel/shared/xstartup
Executable file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/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
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
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"]
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@@ -1,46 +1,16 @@
|
|||||||
services:
|
services:
|
||||||
cloudbeaver:
|
cloudbeaver:
|
||||||
image: dbeaver/cloudbeaver:25.2.5
|
image: 'dbeaver/cloudbeaver:25.2.5'
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
cloudbeaver_runtipi_network:
|
|
||||||
gw_priority: 0
|
|
||||||
tipi_main_network:
|
|
||||||
gw_priority: 1
|
|
||||||
extra_hosts:
|
|
||||||
- host.docker.internal:host-gateway
|
|
||||||
environment:
|
|
||||||
CB_SERVER_NAME: CloudBeaver
|
|
||||||
CB_ADMIN_NAME: admin
|
|
||||||
CB_ADMIN_PASSWORD: admin
|
|
||||||
ports:
|
|
||||||
- ${APP_PORT}:8978
|
|
||||||
volumes:
|
volumes:
|
||||||
- ${APP_DATA_DIR}/data:/opt/cloudbeaver/workspace
|
- '${APP_DATA_DIR}/data:/opt/cloudbeaver/workspace'
|
||||||
labels:
|
environment:
|
||||||
generated: true
|
- 'CB_SERVER_NAME=CloudBeaver'
|
||||||
traefik.enable: true
|
- 'CB_ADMIN_NAME=admin'
|
||||||
traefik.docker.network: runtipi_tipi_main_network
|
- 'CB_ADMIN_PASSWORD=admin'
|
||||||
traefik.http.middlewares.cloudbeaver-runtipi-web-redirect.redirectscheme.scheme: https
|
extra_hosts:
|
||||||
traefik.http.services.cloudbeaver-runtipi.loadbalancer.server.port: "8978"
|
- 'host.docker.internal:host-gateway'
|
||||||
traefik.http.routers.cloudbeaver-runtipi-insecure.rule: Host(`${APP_DOMAIN}`)
|
x-runtipi:
|
||||||
traefik.http.routers.cloudbeaver-runtipi-insecure.entrypoints: web
|
internal_port: 8978
|
||||||
traefik.http.routers.cloudbeaver-runtipi-insecure.service: cloudbeaver-runtipi
|
is_main: true
|
||||||
traefik.http.routers.cloudbeaver-runtipi-insecure.middlewares: cloudbeaver-runtipi-web-redirect
|
x-runtipi:
|
||||||
traefik.http.routers.cloudbeaver-runtipi.rule: Host(`${APP_DOMAIN}`)
|
schema_version: 2
|
||||||
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
|
|
||||||
|
|
||||||
|
|||||||
83
apps/docspace/config.json
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
4
apps/docspace/data/config/nginx/letsencrypt.conf
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
|
root /letsencrypt;
|
||||||
|
default_type "text/plain";
|
||||||
|
}
|
||||||
33
apps/docspace/data/config/nginx/nginx.conf.template
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
30
apps/docspace/data/config/nginx/onlyoffice-proxy.conf
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
32
apps/docspace/data/config/nginx/proxy.upstream.conf.template
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
725
apps/docspace/docker-compose.json
Normal file
@@ -0,0 +1,725 @@
|
|||||||
|
{
|
||||||
|
"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" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
37
apps/docspace/metadata/description.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# 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.
|
||||||
BIN
apps/docspace/metadata/logo.jpg
Normal file
|
After Width: | Height: | Size: 33 KiB |
135
apps/erp-integration/config.json
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
96
apps/erp-integration/docker-compose.json
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
33
apps/erp-integration/docker-compose.yml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
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
|
||||||
52
apps/erp-integration/metadata/description.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# 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.
|
||||||
BIN
apps/erp-integration/metadata/logo.jpg
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
35
apps/mcp-manager/build/README.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# 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`).
|
||||||
37
apps/mcp-manager/build/build.sh
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#!/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"
|
||||||
45
apps/mcp-manager/config.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"$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."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
66
apps/mcp-manager/docker-compose.json
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"$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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
38
apps/mcp-manager/metadata/description.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# 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
|
||||||
BIN
apps/mcp-manager/metadata/logo.jpg
Normal file
|
After Width: | Height: | Size: 34 KiB |
19
apps/md2html/config.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"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"]
|
||||||
|
}
|
||||||
22
apps/md2html/docker-compose.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
12
apps/md2html/metadata/description.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# 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
|
||||||
BIN
apps/md2html/metadata/logo.jpg
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
28
apps/mergedrop/build/README.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# 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.
|
||||||
37
apps/mergedrop/build/build.sh
Executable file
@@ -0,0 +1,37 @@
|
|||||||
|
#!/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"
|
||||||
43
apps/mergedrop/config.json
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"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"]
|
||||||
|
}
|
||||||
47
apps/mergedrop/docker-compose.json
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"$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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
46
apps/mergedrop/metadata/description.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# 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
|
||||||
BIN
apps/mergedrop/metadata/logo.jpg
Normal file
|
After Width: | Height: | Size: 12 KiB |
54
apps/nas-samba/config.json
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"$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"
|
||||||
|
}
|
||||||
34
apps/nas-samba/docker-compose.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
apps/nas-samba/metadata/logo.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
51
apps/nas-samba/source/Dockerfile
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
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"]
|
||||||
13
apps/nas-samba/source/app/frontend/build/asset-manifest.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
1
apps/nas-samba/source/app/frontend/build/index.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!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>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
15845
apps/nas-samba/source/app/frontend/package-lock.json
generated
Normal file
39
apps/nas-samba/source/app/frontend/package.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
6
apps/nas-samba/source/app/frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
14
apps/nas-samba/source/app/frontend/public/index.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!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>
|
||||||
1
apps/nas-samba/source/app/frontend/src/App.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/* Additional app-specific styles */
|
||||||
51
apps/nas-samba/source/app/frontend/src/App.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
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;
|
||||||
1312
apps/nas-samba/source/app/frontend/src/components/Dashboard.js
Normal file
141
apps/nas-samba/source/app/frontend/src/components/LoginPage.js
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
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;
|
||||||
182
apps/nas-samba/source/app/frontend/src/components/SharePage.js
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
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;
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
116
apps/nas-samba/source/app/frontend/src/index.css
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
@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;
|
||||||
|
}
|
||||||
11
apps/nas-samba/source/app/frontend/src/index.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
136
apps/nas-samba/source/app/frontend/src/lib/api.js
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
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;
|
||||||
33
apps/nas-samba/source/app/frontend/src/lib/utils.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
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() : '';
|
||||||
|
}
|
||||||
60
apps/nas-samba/source/app/frontend/tailwind.config.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: ["./src/**/*.{js,jsx,ts,tsx}"],
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
background: "var(--background)",
|
||||||
|
foreground: "var(--foreground)",
|
||||||
|
card: "var(--card)",
|
||||||
|
"card-foreground": "var(--card-foreground)",
|
||||||
|
popover: "var(--popover)",
|
||||||
|
"popover-foreground": "var(--popover-foreground)",
|
||||||
|
primary: "var(--primary)",
|
||||||
|
"primary-foreground": "var(--primary-foreground)",
|
||||||
|
secondary: "var(--secondary)",
|
||||||
|
"secondary-foreground": "var(--secondary-foreground)",
|
||||||
|
muted: "var(--muted)",
|
||||||
|
"muted-foreground": "var(--muted-foreground)",
|
||||||
|
accent: "var(--accent)",
|
||||||
|
"accent-foreground": "var(--accent-foreground)",
|
||||||
|
destructive: "var(--destructive)",
|
||||||
|
"destructive-foreground": "var(--destructive-foreground)",
|
||||||
|
border: "var(--border)",
|
||||||
|
input: "var(--input)",
|
||||||
|
ring: "var(--ring)",
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
primary: ['Inter', 'system-ui', 'sans-serif'],
|
||||||
|
secondary: ['Manrope', 'system-ui', 'sans-serif'],
|
||||||
|
mono: ['JetBrains Mono', 'monospace'],
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: "var(--radius)",
|
||||||
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
sm: "calc(var(--radius) - 4px)",
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
'fade-in': 'fadeIn 0.3s ease-out',
|
||||||
|
'slide-up': 'slideUp 0.3s ease-out',
|
||||||
|
'slide-down': 'slideDown 0.3s ease-out',
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
fadeIn: {
|
||||||
|
'0%': { opacity: '0' },
|
||||||
|
'100%': { opacity: '1' },
|
||||||
|
},
|
||||||
|
slideUp: {
|
||||||
|
'0%': { opacity: '0', transform: 'translateY(10px)' },
|
||||||
|
'100%': { opacity: '1', transform: 'translateY(0)' },
|
||||||
|
},
|
||||||
|
slideDown: {
|
||||||
|
'0%': { opacity: '0', transform: 'translateY(-10px)' },
|
||||||
|
'100%': { opacity: '1', transform: 'translateY(0)' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
2558
apps/nas-samba/source/app/package-lock.json
generated
Normal file
21
apps/nas-samba/source/app/package.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "cloudsync",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "CloudSync File Manager - NAS Edition",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"dev": "node --watch server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"archiver": "^7.0.1",
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
|
"better-sqlite3": "^11.7.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.4.7",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"mime-types": "^2.1.35",
|
||||||
|
"multer": "^1.4.5-lts.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
1718
apps/nas-samba/source/app/server.js
Normal file
11
apps/nas-samba/source/entrypoint.sh
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Ensure directories exist
|
||||||
|
mkdir -p /config /var/log/samba /run/samba /nas/home /var/lib/samba/private
|
||||||
|
|
||||||
|
# Ensure /nas/home is traversable by all users
|
||||||
|
chmod 0755 /nas /nas/home
|
||||||
|
|
||||||
|
echo "Starting supervisord..."
|
||||||
|
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
||||||
19
apps/nas-samba/source/smb.conf
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[global]
|
||||||
|
workgroup = WORKGROUP
|
||||||
|
server string = NAS Samba
|
||||||
|
server role = standalone server
|
||||||
|
log file = /var/log/samba/log.%m
|
||||||
|
max log size = 50
|
||||||
|
logging = file
|
||||||
|
map to guest = bad user
|
||||||
|
server min protocol = SMB2
|
||||||
|
server max protocol = SMB3
|
||||||
|
|
||||||
|
[homes]
|
||||||
|
comment = Home Directory
|
||||||
|
browseable = no
|
||||||
|
read only = no
|
||||||
|
valid users = %S
|
||||||
|
path = /nas/home/%S
|
||||||
|
create mask = 0664
|
||||||
|
directory mask = 0775
|
||||||