Compare commits
195 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9364b4af81 | |||
| 6dedc6d432 | |||
| 4a9d5f7ede | |||
| 517c8849ec | |||
| e1a4566cc3 | |||
| f4fe31c561 | |||
| 5cff118d0c | |||
| 44326c542d | |||
| 48cda41d69 | |||
| 6cf084b931 | |||
| 359a0dccb6 | |||
| ae9b0b7a81 | |||
| aa1be0d4cd | |||
| b32180c268 | |||
| d82b1a9a6b | |||
| 03ae910b3f | |||
| 1849c65c68 | |||
| a21e63c4a2 | |||
| 0d7defff4e | |||
| 14e60151fd | |||
| 62b42635b2 | |||
| 9f6c85ce60 | |||
| 7303c94a3c | |||
| f0c6aa1a2e | |||
| 080341f8a8 | |||
| 034e6688de | |||
| 9ba62e4c8b | |||
| 0dbb576d1b | |||
| 336574571f | |||
| fc93201313 | |||
| fc1a98cbfe | |||
| 3df375e047 | |||
| b060e23a1d | |||
| e92382266f | |||
| 3ca19403fa | |||
| d41d1443a8 | |||
| 89ea56e255 | |||
| 1b24799026 | |||
| 2999f5da47 | |||
| d23b6c2909 | |||
| 6e95c088e4 | |||
| 6113f62ad2 | |||
| 81e5505f2a | |||
| 07288f4e19 | |||
| 7b797f0785 | |||
| 2a255b55ec | |||
| 20cc74ae50 | |||
| b7c65d37cd | |||
| a4b92feed9 | |||
| 54b2baf36c | |||
| 960ee5bc7c | |||
| cd4c48b2f5 | |||
| 10d8d8bfc6 | |||
| 87320e43e1 | |||
| 9083119aca | |||
| 634cab3464 | |||
| 30b0b88660 | |||
| 5e3c75d564 | |||
| b24bc4246b | |||
| 12b2a787ab | |||
| b555f153be | |||
| 414ead35d0 | |||
| 8770476d61 | |||
| 8d2ffb503a | |||
| 11bcd78a13 | |||
| 8561f59beb | |||
| 7e12e8d831 | |||
| 956b9f2968 | |||
| 079ca4bedb | |||
| 2ce600bb6d | |||
| 55e1cbece2 | |||
| 66eb350564 | |||
| e39d1b9ebd | |||
| 31bb7771cd | |||
| bb00b5cfb7 | |||
| 73bc5fa528 | |||
| b30ddacef0 | |||
| 5374473a59 | |||
| e34e71bf6a | |||
| 19817691b2 | |||
| d9f480be95 | |||
| 2e21aa8bea | |||
| 1e37c33603 | |||
| e7566e66c7 | |||
| ee29b28156 | |||
| a1d4368a0a | |||
| 2026510572 | |||
| a38e9fa7f3 | |||
| 5bff56dcda | |||
| 1ca2b028c8 | |||
| 6b00f081c0 | |||
| 7868322af3 | |||
| fc066da921 | |||
| af85ea31e7 | |||
| 60669b47a3 | |||
| bde90a8186 | |||
| 45f2509fe4 | |||
| 3f3e929374 | |||
| 207f7532be | |||
| f6a18dd8e2 | |||
| af8dc42dd6 | |||
| bad04bf390 | |||
| 9790d9338a | |||
| 857ce833cf | |||
| e1e8ad0f08 | |||
| 3e04d8f1c0 | |||
| 707eb9ec81 | |||
| ee9e533485 | |||
| d699dd34c4 | |||
| f5744a65d1 | |||
| fcacc28f55 | |||
| 3332bc3d59 | |||
| 22958d09e7 | |||
| f45132962c | |||
| 993514d92e | |||
| 594683dbf8 | |||
| 7927addcf7 | |||
| 80a5579537 | |||
| a56a485f42 | |||
| f3d0259bd4 | |||
| 9b71a13197 | |||
| f8468270af | |||
| 8f25bc5d63 | |||
| 11e893eb02 | |||
| ab8e46b3a4 | |||
| 07aff0ba28 | |||
| c252fb17ed | |||
| a287a67fd1 | |||
| e52b7e6915 | |||
| 94ae185d47 | |||
| 61b1587cd6 | |||
| 09a74425b3 | |||
| b948399d1d | |||
| d420f179a2 | |||
| aa29d363ce | |||
| 6ea3164d72 | |||
| abeae6cbbf | |||
| 5d30facbcc | |||
| dea385a71c | |||
| 7a44f112a5 | |||
| 48241364aa | |||
| f44041f0d5 | |||
| b6938d3800 | |||
| d6e0387b1c | |||
| b0325267db | |||
| b684242eb9 | |||
| ea82a51307 | |||
| efaa917013 | |||
| c426b711df | |||
| a3b31ed2ab | |||
| 1705d1e32d | |||
| 1298589afa | |||
| ec739975ec | |||
| 8b2301f3a4 | |||
| 93f853bd40 | |||
| 47d66f476d | |||
| 585d4e6ec8 | |||
| e03cb76017 | |||
| fa9537bcb5 | |||
| c42807d076 | |||
| 90c8c57fbe | |||
| b1004e4514 | |||
| fd1fa7dbeb | |||
| 710781bc47 | |||
| b2e3086e10 | |||
| 13c1bd2d13 | |||
| 5f46d3c2b8 | |||
| 6d87718b43 | |||
| c7d25c0107 | |||
| 881b94099c | |||
| 1eb470fa12 | |||
| d144fcdc28 | |||
| fbbf1732b3 | |||
| 5aca6025c5 | |||
| 5f168d21f1 | |||
| 02bab22a5d | |||
| 533b6f4137 | |||
| 434b23aee2 | |||
| 63099703a1 | |||
| 360112c7b7 | |||
| 2e7a0e3645 | |||
| b0a3cd3206 | |||
| 25c0e7b9b7 | |||
| 2398bdce83 | |||
| d73b67614e | |||
| 038d923a11 | |||
| 4e50bf15dd | |||
| ddef538795 | |||
| 7b3942658d | |||
| 79bf23618d | |||
| d0f9854084 | |||
| ccd362a964 | |||
| acd9671359 | |||
| eabbcdaa2d | |||
| 22471e479b |
29 changed files with 3025 additions and 743 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -1,5 +1,5 @@
|
|||
# Binaries
|
||||
nextworkspace
|
||||
/nextworkspace
|
||||
app/core
|
||||
app/core.exe
|
||||
app/data/*.db
|
||||
|
|
@ -18,6 +18,9 @@ Thumbs.db
|
|||
*.swp
|
||||
*.swo
|
||||
|
||||
# AI / Agent config (stored at project root ~/development/)
|
||||
AGENT.md
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
|
|
@ -25,3 +28,5 @@ Thumbs.db
|
|||
# Temp
|
||||
tmp/
|
||||
*.tmp
|
||||
tools/nextwks-tool/nextwks-tool
|
||||
tools/register-certs/register-certs
|
||||
|
|
|
|||
160
CHANGELOG.md
160
CHANGELOG.md
|
|
@ -1,5 +1,165 @@
|
|||
# Changelog
|
||||
|
||||
## 0.1.0.0048 — 2026-07-15
|
||||
|
||||
### Fixed
|
||||
- CSV import: loading spinner with "Importing..." message during upload
|
||||
- CSV import: better error display and proper modal close after completion
|
||||
- Admin panel: Import modal shows results and allows closing on success/failure
|
||||
|
||||
## 0.1.0.0046 — 2026-07-11
|
||||
|
||||
### Added
|
||||
- CSV bulk user import in Access tab — download template, fill data, upload
|
||||
- `/api/templates/users.csv` — sample CSV template download
|
||||
- `/api/users/import` — CSV import handler that parses and creates users via authelia-api
|
||||
|
||||
## 0.1.0.0045 — 2026-07-11
|
||||
|
||||
### Changed
|
||||
- MFA enforcement: after saving email in settings, if TOTP is not enabled, a blocking overlay forces the user to set up two-factor on the Authelia portal before proceeding
|
||||
|
||||
## 0.1.0.0044 — 2026-07-11
|
||||
|
||||
### Added
|
||||
- MFA/TOTP check on user settings page — shows setup prompt if no authenticator is configured
|
||||
- `/api/user/mfa-status` endpoint — checks Authelia for TOTP enrollment status
|
||||
|
||||
## 0.1.0.0043 — 2026-07-11
|
||||
|
||||
### Added
|
||||
- Edit user button in Access tab — admin can change email and groups (delete + recreate approach)
|
||||
- Edit user modal with email, groups fields, and new password display
|
||||
|
||||
## 0.1.0.0039 — 2026-07-11
|
||||
|
||||
### Changed
|
||||
- Modernized Authelia config format (fixes all deprecation warnings):
|
||||
- `server.address: tcp://0.0.0.0:9091` (replaces `host` + `port`)
|
||||
- `identity_validation.reset_password.jwt_secret` (replaces `jwt_secret`)
|
||||
- `notifier.smtp.address: submission://...` (replaces `host` + `port`)
|
||||
- `authentication_backend.file.watch: true` (auto-reload on user changes)
|
||||
- `session.remember_me` (replaces `remember_me_duration`)
|
||||
|
||||
## 0.1.0.0038 — 2026-07-11
|
||||
|
||||
### Fixed
|
||||
- Authelia `authentication_backend.file.watch: true` — YAML changes now auto-reload, so new users can log in immediately after creation
|
||||
|
||||
### Investigation: User Onboarding Emails
|
||||
- SMTP config is correct (`notifier.smtp` → `smtp.openxchange.eu:587`)
|
||||
- SMTP connection test passed (TLS handshake successful)
|
||||
- authelia-api does NOT send onboarding emails — returns `placeholder_password` in API response instead
|
||||
- This is an API feature gap, not a configuration issue
|
||||
|
||||
## 0.1.0.0037 — 2026-07-11
|
||||
|
||||
### Changed
|
||||
- Simplified groups model: per-app groups (`drive`, `office`, `chat`, etc.) replaced with `users` + `admins` only
|
||||
- `config/authelia/configuration.yml` — access_control rules reduced from 12 rules to 4
|
||||
- `config/authelia/users_database.yml` — master user groups simplified to `admins`, `users`
|
||||
- `config/nextworkspace/apps.yaml` — all user-facing apps use `groups: ["users"]`
|
||||
- Admin panel user creation form — 9 checkboxes replaced with 2 (User + Admin)
|
||||
|
||||
## 0.1.0.0036 — 2026-07-11
|
||||
|
||||
### Fixed
|
||||
- Admin panel user management: `apiProxyHandler` was stripping `/api` prefix before forwarding to authelia-api, causing 404 on all `/api/users` calls. Removed the `TrimPrefix` — authelia-api expects the full `/api/...` path.
|
||||
|
||||
## 0.1.0.0035 — 2026-07-11
|
||||
|
||||
### Added
|
||||
- `AUTHELIA_API_LISTEN=0.0.0.0:8080` explicitly set in compose (default already correct)
|
||||
|
||||
## 0.1.0.0034 — 2026-07-11
|
||||
|
||||
### Added
|
||||
- Fixed subnet `172.18.0.0/24` for `nextwks-net`
|
||||
- Static IPv4 addresses for all containers (Caddy `.10`, Authelia `.11`, Launcher `.12`)
|
||||
|
||||
### Changed
|
||||
- `compose/stack.yaml`: network config uses `ipv4_address` instead of flat list
|
||||
- `tools/nextwks.sh`: network creation now uses `--subnet 172.18.0.0/24`
|
||||
|
||||
## 0.1.0.0033 — 2026-07-11
|
||||
|
||||
### Added
|
||||
- `tools/firewall-routing.sh` — iptables redirects + VM firewall
|
||||
- `storage.encryption_key` to Authelia config (required by v4.38+)
|
||||
- Auto-detection of existing install in `--install` mode
|
||||
|
||||
### Changed
|
||||
- **Rootless Podman**: all container commands run without sudo
|
||||
- **Ports**: Caddy binds to 8080/8443, iptables redirects 80/443
|
||||
- `.gitignore`: `/nextworkspace` (root-scoped) to track `config/nextworkspace/`
|
||||
- Configs regenerated on every mode (install/update/destroy)
|
||||
- `.env` values single-quoted, written via `tee -a` to preserve `$` in bcrypt hashes
|
||||
- Admin password: now 24 mixed-case alphanumeric chars (base64)
|
||||
- Containers stopped before binary copy to avoid "Text file busy"
|
||||
- Firewall rules persisted via `netfilter-persistent save`
|
||||
- Docs: AGENT.md, README.md fully updated
|
||||
|
||||
### Fixed
|
||||
- `SSL_ERROR_INTERNAL_ERROR_ALERT` — Authelia now starts with proper config
|
||||
- Password hash corruption — `$2a$...` no longer mangled by `bash -c`
|
||||
- "Text file busy" during `--update` — containers stopped before copy
|
||||
- `--update` skipped config regeneration (now always regenerates)
|
||||
|
||||
## 0.1.0.0032 — 2026-07-11
|
||||
|
||||
### Added
|
||||
- `tools/nextwks.sh` — unified install/update/destroy script
|
||||
- `AGENT.md` — workflow instructions for agents
|
||||
|
||||
### Changed
|
||||
- Replaced `deploy.sh` and `install.sh` with single `tools/nextwks.sh`
|
||||
- Build moved from `/opt/NextWks` (persistent git repo) to `/tmp/nextwks-build` (ephemeral clone)
|
||||
- README.md updated for unified script workflow
|
||||
- `SESSION_SECRET` persisted in `/opt/backup/.env` for idempotent `--destroy`
|
||||
|
||||
### Removed
|
||||
- `deploy.sh` (replaced by `tools/nextwks.sh --update / --destroy`)
|
||||
- `install.sh` (replaced by `tools/nextwks.sh --install`)
|
||||
|
||||
## 0.1.0.0007 — 2026-07-08
|
||||
|
||||
### Added
|
||||
- Caddy reverse proxy (auto LE TLS, subdomain routing, forward auth)
|
||||
- Authelia OIDC provider (2FA, identity store, user management)
|
||||
- `compose/caddy.yaml` and `compose/authelia.yaml`
|
||||
- `config/caddy/Caddyfile` with `{DOMAIN}` template
|
||||
- `config/authelia/configuration.yml` with secret injection
|
||||
- `tools/hash-password/` for bcrypt password hashing
|
||||
- Certificate backup to `/opt/backup/certificates/` across destroys
|
||||
- README.md with architecture overview
|
||||
|
||||
### Changed
|
||||
- Replaced Zoraxy entirely with Caddy + Authelia
|
||||
- Binary trusts `Remote-User` header from Caddy forward auth
|
||||
- deploy.sh rewritten for Caddy/Authelia deployment
|
||||
- install.sh creates Caddy/Authelia directories
|
||||
|
||||
### Removed
|
||||
- Zoraxy compose, configs, proxy rules
|
||||
- `tools/nextwks-tool` (no longer needed)
|
||||
- `tools/register-certs` (no longer needed)
|
||||
- BoltDB logic, `chattr +i`, CSRF handling
|
||||
- All Zoraxy-specific deployment code
|
||||
|
||||
## 0.1.0.0001 — 2026-07-07
|
||||
|
||||
### Fixed
|
||||
- Deploy workflow: start Zoraxy → upload certs via API → stop → write proxy configs → restart
|
||||
- Lego runs per-domain instead of SAN cert
|
||||
- Removed CSRF token issues by separating config phases
|
||||
- `.env` quoting for special chars, email validation in install.sh
|
||||
- Configurable subdomains (hub/noc/www/auth)
|
||||
|
||||
### Changed
|
||||
- LE certs backed up to `/opt/backup/certificates/`, persistent across `--destroy`
|
||||
- Helper tool `nextwks-tool` for LE (lego) + BoltDB operations
|
||||
- Binary simplified: no login form, trusts `X-Forwarded-User` from Zoraxy
|
||||
|
||||
## 0.1.0 — 2026-07-06
|
||||
|
||||
### Added
|
||||
|
|
|
|||
85
README.md
Normal file
85
README.md
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# NextWorkspace
|
||||
|
||||
A self-hosted productivity suite for startups. One binary + Caddy + Authelia.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Internet :443 ──iptables──> :8443 ──> Caddy container :443
|
||||
Internet :80 ──iptables──> :8080 ──> Caddy container :80
|
||||
|
||||
Caddy (rootless podman, nextwks-net)
|
||||
├── auth.{DOMAIN} ──> Authelia :9091 (internal)
|
||||
├── app.{DOMAIN} ──> Launcher :9000 (forward auth via Authelia)
|
||||
└── www.{DOMAIN} ──> static files
|
||||
|
||||
Authelia :9091 ──> api :8080 (internal)
|
||||
Launcher :9000 ──> /config, /people, /settings, /health
|
||||
```
|
||||
|
||||
- **Caddy**: TLS termination (ZeroSSL/LE), subdomain routing, forward auth to Authelia
|
||||
- **Authelia**: OIDC provider, 2FA, identity store, user management API
|
||||
- **Launcher**: Go binary — app dashboard, people directory, admin panel, settings
|
||||
- **iptables**: Redirects 80→8080 and 443→8443 so Caddy can run rootless
|
||||
|
||||
## Quick Start (Bare VM)
|
||||
|
||||
```bash
|
||||
# Download the script to your home folder
|
||||
curl -o ~/nextwks.sh https://git.lohmar.co.uk/lexton-it/NextWks/raw/branch/main/tools/nextwks.sh
|
||||
chmod +x ~/nextwks.sh
|
||||
|
||||
# Run the installer (no sudo — it'll ask only where needed)
|
||||
./nextwks.sh --install
|
||||
```
|
||||
|
||||
Prompts for domain, TLS email, and admin credentials. Installs deps (Go, Podman, git),
|
||||
clones repo to `/tmp/nextwks-build/`, builds binary, generates configs, deploys stack.
|
||||
The script stays in `~/nextwks.sh` for future updates.
|
||||
|
||||
## Directory Layout
|
||||
|
||||
```
|
||||
/opt/nextworkspace/ # Runtime (freshly populated on every deploy)
|
||||
├── config/
|
||||
│ ├── caddy/Caddyfile
|
||||
│ ├── authelia/configuration.yml
|
||||
│ ├── authelia/users_database.yml
|
||||
│ └── nextworkspace/{config,apps}.yaml
|
||||
├── data/
|
||||
│ ├── caddy/ (certs + runtime)
|
||||
│ └── authelia/ (database)
|
||||
├── compose/stack.yaml
|
||||
├── www/ (landing page)
|
||||
├── lng/ (translations)
|
||||
└── nextworkspace (static Go binary)
|
||||
|
||||
/opt/backup/ # Secrets vault (survives --destroy)
|
||||
├── .env
|
||||
└── certificates/
|
||||
|
||||
/tmp/nextwks-build/ # Ephemeral build dir (git clone --depth 1)
|
||||
```
|
||||
|
||||
## Operations
|
||||
|
||||
```bash
|
||||
# Smart update (pull, build, copy, restart)
|
||||
./nextwks.sh --update
|
||||
|
||||
# Full redeploy (tear down, rebuild from scratch with saved secrets)
|
||||
./nextwks.sh --destroy
|
||||
```
|
||||
|
||||
## Workflow (Development)
|
||||
|
||||
1. Edit code in your clone.
|
||||
2. Bump `VERSION`, update `CHANGELOG.md`.
|
||||
3. `git commit -m "message" && git tag v$(cat VERSION) && git push origin main --tags`
|
||||
4. On the server: `./nextwks.sh --update`
|
||||
|
||||
The script clones fresh from git every time — no stale repos, no permissions issues.
|
||||
|
||||
## Version
|
||||
|
||||
Current: 0.1.0.0032 — see [CHANGELOG.md](CHANGELOG.md)
|
||||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.1.0
|
||||
0.1.0.0048
|
||||
|
|
|
|||
75
compose/stack.yaml
Normal file
75
compose/stack.yaml
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
services:
|
||||
caddy:
|
||||
image: caddy:latest
|
||||
container_name: caddy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:80"
|
||||
- "8443:443"
|
||||
volumes:
|
||||
- /opt/nextworkspace/config/caddy/:/etc/caddy/
|
||||
- /opt/nextworkspace/data/caddy/:/data/:Z
|
||||
- /opt/nextworkspace/logs/caddy/:/var/log/caddy/
|
||||
- /opt/nextworkspace/www/:/opt/nextworkspace/www/
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:80/"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
networks:
|
||||
nextwks-net:
|
||||
ipv4_address: 172.18.0.10
|
||||
|
||||
authelia:
|
||||
image: git24hcom/authelia:latest
|
||||
container_name: authelia
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- "9091"
|
||||
- "8080"
|
||||
environment:
|
||||
- TZ=UTC
|
||||
- AUTHELIA_API_LISTEN=0.0.0.0:8080
|
||||
volumes:
|
||||
- /opt/nextworkspace/config/authelia/:/config/
|
||||
- /opt/nextworkspace/data/authelia/:/data/
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:9091/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
networks:
|
||||
nextwks-net:
|
||||
ipv4_address: 172.18.0.11
|
||||
|
||||
launcher:
|
||||
image: alpine:latest
|
||||
container_name: launcher
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- "9000"
|
||||
volumes:
|
||||
- /opt/nextworkspace/:/opt/nextworkspace/
|
||||
working_dir: /opt/nextworkspace
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- "apk add --no-cache curl sqlite >/dev/null 2>&1 && exec /opt/nextworkspace/nextworkspace"
|
||||
environment:
|
||||
- CONFIG_DIR=/opt/nextworkspace/config/nextworkspace
|
||||
- AUTHELIA_SECRET={AUTHELIA_SECRET}
|
||||
- DOMAIN={DOMAIN}
|
||||
- TLS_EMAIL={TLS_EMAIL}
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-sf", "http://127.0.0.1:9000/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 5s
|
||||
networks:
|
||||
nextwks-net:
|
||||
ipv4_address: 172.18.0.12
|
||||
|
||||
networks:
|
||||
nextwks-net:
|
||||
external: true
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
services:
|
||||
zoraxy:
|
||||
image: zoraxydocker/zoraxy:latest
|
||||
container_name: zoraxy
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
volumes:
|
||||
- /opt/nextworkspace/config/zoraxy/:/opt/zoraxy/config/
|
||||
- /opt/nextworkspace/data/zoraxy/:/opt/zoraxy/data/
|
||||
- /opt/nextworkspace/logs/zoraxy/:/opt/zoraxy/log/
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
- PORT=8000
|
||||
- NOAUTH=false
|
||||
- DOCKER=true
|
||||
- TZ=UTC
|
||||
83
config/authelia/configuration.yml
Normal file
83
config/authelia/configuration.yml
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
###############################################################
|
||||
# Authelia configuration #
|
||||
###############################################################
|
||||
server:
|
||||
address: tcp://0.0.0.0:9091
|
||||
|
||||
log:
|
||||
level: info
|
||||
|
||||
theme: dark
|
||||
|
||||
identity_validation:
|
||||
reset_password:
|
||||
jwt_secret: {JWT_SECRET}
|
||||
|
||||
default_redirection_url: https://app.{DOMAIN}/
|
||||
|
||||
totp:
|
||||
issuer: nextworkspace
|
||||
|
||||
access_control:
|
||||
default_policy: deny
|
||||
rules:
|
||||
# Auth and public pages — no auth required
|
||||
- domain: "auth.{DOMAIN}"
|
||||
policy: bypass
|
||||
- domain: "www.{DOMAIN}"
|
||||
policy: bypass
|
||||
|
||||
# Admin panel — admins only
|
||||
- domain: "app.{DOMAIN}"
|
||||
resources:
|
||||
- "^/config(/.*)?$"
|
||||
subject:
|
||||
- "group:admins"
|
||||
policy: one_factor
|
||||
|
||||
# Users with TFA enforcement — two-factor required
|
||||
- domain: "app.{DOMAIN}"
|
||||
subject:
|
||||
- "group:tfa_required"
|
||||
policy: two_factor
|
||||
|
||||
# Everything else — any authenticated user
|
||||
- domain: "app.{DOMAIN}"
|
||||
policy: one_factor
|
||||
|
||||
authentication_backend:
|
||||
file:
|
||||
path: /config/users_database.yml
|
||||
watch: true
|
||||
|
||||
session:
|
||||
name: nextworkspace_session
|
||||
secret: {SESSION_SECRET}
|
||||
domain: "{DOMAIN}"
|
||||
same_site: lax
|
||||
expiration: 1h
|
||||
inactivity: 5m
|
||||
remember_me: 1M
|
||||
|
||||
regulation:
|
||||
max_retries: 5
|
||||
find_time: 2m
|
||||
ban_time: 5m
|
||||
|
||||
storage:
|
||||
encryption_key: {STORAGE_ENCRYPTION_KEY}
|
||||
local:
|
||||
path: /data/db.sqlite
|
||||
|
||||
notifier:
|
||||
smtp:
|
||||
host: "{SMTP_HOST}"
|
||||
port: {SMTP_PORT}
|
||||
username: "{SMTP_USER}"
|
||||
password: "{SMTP_PASS}"
|
||||
sender: "{SMTP_USER}"
|
||||
subject: "NextWorkspace - {DOMAIN}"
|
||||
disable_require_tls: false
|
||||
disable_starttls: false
|
||||
tls:
|
||||
skip_verify: false
|
||||
9
config/authelia/users_database.yml
Normal file
9
config/authelia/users_database.yml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
users:
|
||||
master:
|
||||
disabled: false
|
||||
displayname: "Master Admin"
|
||||
password: "{ADMIN_PASSWORD_HASH}"
|
||||
email: "{TLS_EMAIL}"
|
||||
groups:
|
||||
- admins
|
||||
- users
|
||||
25
config/caddy/Caddyfile
Normal file
25
config/caddy/Caddyfile
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Global options
|
||||
{
|
||||
email {TLS_EMAIL}
|
||||
admin off
|
||||
}
|
||||
|
||||
# Authelia OIDC provider
|
||||
auth.{DOMAIN} {
|
||||
reverse_proxy authelia:9091
|
||||
}
|
||||
|
||||
# Main workspace (forward auth with Authelia)
|
||||
app.{DOMAIN} {
|
||||
forward_auth authelia:9091 {
|
||||
uri /api/verify?rd=https://auth.{DOMAIN}/
|
||||
copy_headers Remote-User Remote-Name Remote-Email Remote-Groups
|
||||
}
|
||||
reverse_proxy launcher:9000
|
||||
}
|
||||
|
||||
# Public landing page
|
||||
www.{DOMAIN} {
|
||||
root * /opt/nextworkspace/www
|
||||
file_server
|
||||
}
|
||||
59
config/nextworkspace/apps.yaml
Normal file
59
config/nextworkspace/apps.yaml
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
apps:
|
||||
- name: "NextWks Core"
|
||||
subtitle: "Launcher & Workspace Hub"
|
||||
path: "/home"
|
||||
icon: "home"
|
||||
groups: ["users"]
|
||||
- name: "OpenCloud"
|
||||
subtitle: "File Storage"
|
||||
path: "/drive"
|
||||
upstream: "http://127.0.0.1:9100"
|
||||
icon: "cloud"
|
||||
groups: ["users"]
|
||||
- name: "Euro Office"
|
||||
subtitle: "Collaborative Suite"
|
||||
path: "/office"
|
||||
upstream: "http://127.0.0.1:9200"
|
||||
icon: "office"
|
||||
groups: ["users"]
|
||||
- name: "ERPNext"
|
||||
subtitle: "Enterprise ERP"
|
||||
path: "/enterprise"
|
||||
upstream: "http://127.0.0.1:9300"
|
||||
icon: "erp"
|
||||
groups: ["users"]
|
||||
- name: "Matrix Chat"
|
||||
subtitle: "Team Communication"
|
||||
path: "/chat"
|
||||
upstream: "http://127.0.0.1:9400"
|
||||
icon: "chat"
|
||||
groups: ["users"]
|
||||
- name: "Jitsi"
|
||||
subtitle: "Video Conferencing"
|
||||
path: "/meet"
|
||||
upstream: "http://127.0.0.1:9500"
|
||||
icon: "meet"
|
||||
groups: ["users"]
|
||||
- name: "Webmail"
|
||||
subtitle: "Email Client"
|
||||
path: "/connect"
|
||||
upstream: "http://127.0.0.1:9600"
|
||||
icon: "mail"
|
||||
groups: ["users"]
|
||||
- name: "AI Chat"
|
||||
subtitle: "Open WebUI"
|
||||
path: "/aida"
|
||||
upstream: "http://127.0.0.1:9700"
|
||||
icon: "ai"
|
||||
groups: ["users"]
|
||||
- name: "Portainer"
|
||||
subtitle: "Container Management"
|
||||
path: "/admin"
|
||||
upstream: "http://127.0.0.1:9800"
|
||||
icon: "admin"
|
||||
groups: ["admins"]
|
||||
- name: "Admin Panel"
|
||||
subtitle: "Workspace Configuration"
|
||||
path: "/config"
|
||||
icon: "settings"
|
||||
groups: ["admins"]
|
||||
7
config/nextworkspace/config.yaml
Normal file
7
config/nextworkspace/config.yaml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
server:
|
||||
port: 9000
|
||||
host: "0.0.0.0"
|
||||
|
||||
app:
|
||||
name: "NextWorkspace"
|
||||
description: "Your Self-Hosted Workspace"
|
||||
14
config/nextworkspace/settings.yaml
Normal file
14
config/nextworkspace/settings.yaml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
company:
|
||||
name: "NextWorkspace"
|
||||
subtitle: "Your Self-Hosted Workspace for Startups"
|
||||
logo: ""
|
||||
language: "en"
|
||||
timezone: "UTC"
|
||||
smtp:
|
||||
host: ""
|
||||
port: 587
|
||||
user: ""
|
||||
sender: ""
|
||||
imap:
|
||||
host: ""
|
||||
port: 993
|
||||
44
config/www/index.html
Normal file
44
config/www/index.html
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>NextWorkspace</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #1a1a2e; color: #fff; display: flex; align-items: center;
|
||||
justify-content: center; min-height: 100vh; margin: 0; }
|
||||
.hero { text-align: center; max-width: 600px; padding: 2rem; }
|
||||
.hero h1 { font-size: 2.5rem; margin-bottom: 0.5rem; }
|
||||
.hero p { color: #a0aec0; font-size: 1.2rem; margin-bottom: 2rem; }
|
||||
.hero img { max-height: 80px; margin-bottom: 1rem; }
|
||||
.btn { display: inline-block; padding: 0.75rem 2rem; background: #63b3ed;
|
||||
color: #fff; text-decoration: none; border-radius: 8px; font-weight: 500; }
|
||||
.links { margin-top: 3rem; display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; }
|
||||
.links a { color: #a0aec0; text-decoration: none; font-size: 0.9rem; }
|
||||
.links a:hover { color: #63b3ed; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="hero">
|
||||
<img id="company-logo" style="display:none" alt="Logo">
|
||||
<h1 id="company-name">NextWorkspace</h1>
|
||||
<p id="company-subtitle">Your Self-Hosted Workspace for Startups</p>
|
||||
<a class="btn" href="https://app.nextwks.eu/">Launch Workspace</a>
|
||||
<div class="links">
|
||||
<a href="https://auth.nextwks.eu/">Admin Login</a>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
fetch('/api/settings/public')
|
||||
.then(r => r.json())
|
||||
.then(s => {
|
||||
if (s.logo) { const img = document.getElementById('company-logo');
|
||||
img.src = s.logo; img.style.display = 'block'; }
|
||||
document.getElementById('company-name').textContent = s.name;
|
||||
document.getElementById('company-subtitle').textContent = s.subtitle;
|
||||
document.title = s.name;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"ProxyType": 1,
|
||||
"RootOrMatchingDomain": "app.nextwks.eu",
|
||||
"ActiveOrigins": [{
|
||||
"OriginIpOrDomain": "127.0.0.1:9000",
|
||||
"RequireTLS": false,
|
||||
"Weight": 1,
|
||||
"MaxConn": 0
|
||||
}],
|
||||
"Disabled": false,
|
||||
"AuthenticationProvider": {"AuthMethod": 0}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"ProxyType": 1,
|
||||
"RootOrMatchingDomain": "dns.nextwks.eu",
|
||||
"ActiveOrigins": [{
|
||||
"OriginIpOrDomain": "127.0.0.1:8000",
|
||||
"RequireTLS": false,
|
||||
"Weight": 1,
|
||||
"MaxConn": 0
|
||||
}],
|
||||
"Disabled": false,
|
||||
"AuthenticationProvider": {"AuthMethod": 0}
|
||||
}
|
||||
391
deploy.sh
391
deploy.sh
|
|
@ -1,391 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_DIR="/opt/NextWks"
|
||||
TARGET_DIR="/opt/nextworkspace"
|
||||
SERVICE_NAME="nextworkspace"
|
||||
BINARY_NAME="nextworkspace"
|
||||
HEALTH_CHECK_RETRIES=10
|
||||
HEALTH_CHECK_INTERVAL=2
|
||||
|
||||
# --- Load .env from runtime root ---
|
||||
ENV_FILE="$TARGET_DIR/.env"
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
set -a
|
||||
source "$ENV_FILE"
|
||||
set +a
|
||||
fi
|
||||
|
||||
# Default domain if .env wasn't loaded
|
||||
DOMAIN="${DOMAIN:-nextwks.eu}"
|
||||
|
||||
# --- Mode detection ---
|
||||
GREENFIELD=false
|
||||
if [ "${1:-}" = "--destroy" ]; then
|
||||
GREENFIELD=true
|
||||
echo "[MODE] Greenfield deploy (--destroy)"
|
||||
elif [ ! -d "$TARGET_DIR" ]; then
|
||||
GREENFIELD=true
|
||||
echo "[MODE] Greenfield deploy (target missing)"
|
||||
else
|
||||
echo "[MODE] Smart update (target exists)"
|
||||
fi
|
||||
|
||||
# --- Common: pull + build ---
|
||||
cd "$REPO_DIR"
|
||||
echo "[1/6] Pulling latest code..."
|
||||
git pull
|
||||
|
||||
echo "[2/6] Building binary..."
|
||||
export PATH=$PATH:/usr/local/go/bin
|
||||
go build -o "$BINARY_NAME" .
|
||||
|
||||
# --- Greenfield path ---
|
||||
if [ "$GREENFIELD" = true ]; then
|
||||
echo "[3/6] Removing old deployment..."
|
||||
|
||||
# Preserve .env across greenfield destroy
|
||||
if [ -f "$TARGET_DIR/.env" ]; then
|
||||
cp "$TARGET_DIR/.env" /tmp/nextworkspace.env.bak
|
||||
echo "[INFO] Preserved .env"
|
||||
fi
|
||||
|
||||
rm -rf "$TARGET_DIR"
|
||||
|
||||
echo "[4/6] Creating target directories..."
|
||||
mkdir -p "$TARGET_DIR/config/nextworkspace"
|
||||
mkdir -p "$TARGET_DIR/config/zoraxy/conf/proxy"
|
||||
mkdir -p "$TARGET_DIR/data/zoraxy"
|
||||
mkdir -p "$TARGET_DIR/compose"
|
||||
mkdir -p "$TARGET_DIR/logs"
|
||||
|
||||
# Restore preserved .env
|
||||
if [ -f /tmp/nextworkspace.env.bak ]; then
|
||||
mv /tmp/nextworkspace.env.bak "$TARGET_DIR/.env"
|
||||
chmod 600 "$TARGET_DIR/.env"
|
||||
echo "[INFO] Restored .env"
|
||||
fi
|
||||
|
||||
echo "[5/6] Copying binary..."
|
||||
cp "$BINARY_NAME" "$TARGET_DIR/$BINARY_NAME"
|
||||
|
||||
if [ -f "$REPO_DIR/VERSION" ]; then
|
||||
cp "$REPO_DIR/VERSION" "$TARGET_DIR/VERSION"
|
||||
echo "[INFO] Version: $(cat $TARGET_DIR/VERSION)"
|
||||
fi
|
||||
|
||||
echo "[6/6] Deploying Zoraxy..."
|
||||
cp compose/zoraxy.yaml "$TARGET_DIR/compose/zoraxy.yaml"
|
||||
podman-compose -f "$TARGET_DIR/compose/zoraxy.yaml" up -d 2>&1 || echo "[WARN] Zoraxy deploy had issues (see above)"
|
||||
|
||||
# Generate Zoraxy proxy configs with full schema (prevents Zoraxy from clearing OriginIpOrDomain on expand)
|
||||
echo "[*] Generating Zoraxy proxy configs..."
|
||||
cat > "$TARGET_DIR/config/zoraxy/conf/proxy/app.$DOMAIN.config" <<ZORAXY_APP
|
||||
{
|
||||
"ProxyType": 1,
|
||||
"RootOrMatchingDomain": "app.$DOMAIN",
|
||||
"MatchingDomainAlias": [],
|
||||
"ActiveOrigins": [
|
||||
{
|
||||
"OriginIpOrDomain": "127.0.0.1:9000",
|
||||
"RequireTLS": false,
|
||||
"SkipCertValidations": false,
|
||||
"SkipWebSocketOriginCheck": false,
|
||||
"Weight": 1,
|
||||
"MaxConn": 0,
|
||||
"RespTimeout": 0
|
||||
}
|
||||
],
|
||||
"InactiveOrigins": [],
|
||||
"UseStickySession": false,
|
||||
"UseActiveLoadBalance": false,
|
||||
"Disabled": false,
|
||||
"BypassGlobalTLS": false,
|
||||
"VirtualDirectories": [],
|
||||
"HeaderRewriteRules": {
|
||||
"UserDefinedHeaders": null,
|
||||
"RequestHostOverwrite": "",
|
||||
"HSTSMaxAge": 0,
|
||||
"EnablePermissionPolicyHeader": false,
|
||||
"PermissionPolicy": null,
|
||||
"DisableHopByHopHeaderRemoval": false
|
||||
},
|
||||
"EnableWebsocketCustomHeaders": false,
|
||||
"AuthenticationProvider": {
|
||||
"AuthMethod": 0,
|
||||
"BasicAuthCredentials": null,
|
||||
"BasicAuthExceptionRules": null,
|
||||
"BasicAuthGroupIDs": [],
|
||||
"ForwardAuthURL": "",
|
||||
"ForwardAuthResponseHeaders": [],
|
||||
"ForwardAuthResponseClientHeaders": [],
|
||||
"ForwardAuthRequestHeaders": [],
|
||||
"ForwardAuthRequestExcludedCookies": []
|
||||
},
|
||||
"RequireRateLimit": false,
|
||||
"RateLimit": 0,
|
||||
"DisableUptimeMonitor": false,
|
||||
"AccessFilterUUID": "",
|
||||
"DefaultSiteOption": 0,
|
||||
"DefaultSiteValue": "",
|
||||
"Tags": []
|
||||
}
|
||||
ZORAXY_APP
|
||||
|
||||
cat > "$TARGET_DIR/config/zoraxy/conf/proxy/dns.$DOMAIN.config" <<ZORAXY_DNS
|
||||
{
|
||||
"ProxyType": 1,
|
||||
"RootOrMatchingDomain": "dns.$DOMAIN",
|
||||
"MatchingDomainAlias": [],
|
||||
"ActiveOrigins": [
|
||||
{
|
||||
"OriginIpOrDomain": "127.0.0.1:8000",
|
||||
"RequireTLS": false,
|
||||
"SkipCertValidations": false,
|
||||
"SkipWebSocketOriginCheck": false,
|
||||
"Weight": 1,
|
||||
"MaxConn": 0,
|
||||
"RespTimeout": 0
|
||||
}
|
||||
],
|
||||
"InactiveOrigins": [],
|
||||
"UseStickySession": false,
|
||||
"UseActiveLoadBalance": false,
|
||||
"Disabled": false,
|
||||
"BypassGlobalTLS": false,
|
||||
"VirtualDirectories": [],
|
||||
"HeaderRewriteRules": {
|
||||
"UserDefinedHeaders": null,
|
||||
"RequestHostOverwrite": "",
|
||||
"HSTSMaxAge": 0,
|
||||
"EnablePermissionPolicyHeader": false,
|
||||
"PermissionPolicy": null,
|
||||
"DisableHopByHopHeaderRemoval": false
|
||||
},
|
||||
"EnableWebsocketCustomHeaders": false,
|
||||
"AuthenticationProvider": {
|
||||
"AuthMethod": 0,
|
||||
"BasicAuthCredentials": null,
|
||||
"BasicAuthExceptionRules": null,
|
||||
"BasicAuthGroupIDs": [],
|
||||
"ForwardAuthURL": "",
|
||||
"ForwardAuthResponseHeaders": [],
|
||||
"ForwardAuthResponseClientHeaders": [],
|
||||
"ForwardAuthRequestHeaders": [],
|
||||
"ForwardAuthRequestExcludedCookies": []
|
||||
},
|
||||
"RequireRateLimit": false,
|
||||
"RateLimit": 0,
|
||||
"DisableUptimeMonitor": false,
|
||||
"AccessFilterUUID": "",
|
||||
"DefaultSiteOption": 0,
|
||||
"DefaultSiteValue": "",
|
||||
"Tags": []
|
||||
}
|
||||
ZORAXY_DNS
|
||||
|
||||
# Lock proxy configs so Zoraxy cannot rewrite them (clearing OriginIpOrDomain)
|
||||
echo "[*] Locking proxy configs (chattr +i)..."
|
||||
chattr -R +i "$TARGET_DIR/config/zoraxy/conf/proxy/" 2>/dev/null || true
|
||||
|
||||
# Copy launcher config files
|
||||
cp -r config/nextworkspace/* "$TARGET_DIR/config/nextworkspace/"
|
||||
|
||||
# Generate launcher apps.yaml with path-based URLs
|
||||
echo "[*] Generating apps.yaml..."
|
||||
cat > "$TARGET_DIR/config/nextworkspace/apps.yaml" <<EOF
|
||||
apps:
|
||||
- name: "OpenCloud"
|
||||
subtitle: "File Storage"
|
||||
path: "/cloud"
|
||||
upstream: "http://127.0.0.1:9100"
|
||||
icon: "cloud"
|
||||
- name: "Euro Office"
|
||||
subtitle: "Collaborative Suite"
|
||||
path: "/office"
|
||||
upstream: "http://127.0.0.1:9200"
|
||||
icon: "office"
|
||||
- name: "ERPNext"
|
||||
subtitle: "Enterprise ERP"
|
||||
path: "/erp"
|
||||
upstream: "http://127.0.0.1:9300"
|
||||
icon: "erp"
|
||||
- name: "Matrix Chat"
|
||||
subtitle: "Team Communication"
|
||||
path: "/chat"
|
||||
upstream: "http://127.0.0.1:9400"
|
||||
icon: "chat"
|
||||
- name: "Jitsi"
|
||||
subtitle: "Video Conferencing"
|
||||
path: "/meet"
|
||||
upstream: "http://127.0.0.1:9500"
|
||||
icon: "video"
|
||||
- name: "Webmail"
|
||||
subtitle: "Email Client"
|
||||
path: "/mail"
|
||||
upstream: "http://127.0.0.1:9600"
|
||||
icon: "mail"
|
||||
- name: "AI Chat"
|
||||
subtitle: "Open WebUI"
|
||||
path: "/ai"
|
||||
upstream: "http://127.0.0.1:9700"
|
||||
icon: "bot"
|
||||
- name: "Portainer"
|
||||
subtitle: "Container Management"
|
||||
path: "/admin"
|
||||
upstream: "http://127.0.0.1:9800"
|
||||
icon: "admin"
|
||||
EOF
|
||||
|
||||
# --- Configure Zoraxy admin + LE via API ---
|
||||
echo "[*] Configuring Zoraxy..."
|
||||
|
||||
# Wait for Zoraxy to be ready
|
||||
for i in $(seq 1 15); do
|
||||
if curl -sf "http://127.0.0.1:8000/api/auth/userCount" > /dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
echo " Waiting for Zoraxy... ($i/15)"
|
||||
sleep 2
|
||||
done
|
||||
sleep 1
|
||||
|
||||
# Helper: fetch CSRF token + cookie jar from Zoraxy login page
|
||||
fetch_csrf() {
|
||||
COOKIE_JAR="/tmp/zoraxy_cookies.txt"
|
||||
rm -f "$COOKIE_JAR"
|
||||
local page
|
||||
page=$(curl -sL -c "$COOKIE_JAR" http://127.0.0.1:8000/ 2>&1)
|
||||
local token
|
||||
token=$(echo "$page" | grep -oP '(?<=<meta name="zoraxy.csrf.Token" content=")[^"]+' || echo "")
|
||||
echo "$token"
|
||||
}
|
||||
|
||||
# Step 1: Create admin account
|
||||
echo " Creating admin account..."
|
||||
CSRF=$(fetch_csrf)
|
||||
if [ -z "$CSRF" ]; then
|
||||
echo " [WARN] Could not fetch CSRF token — trying registration without it"
|
||||
ADMIN_RESULT=$(curl -s -X POST "http://127.0.0.1:8000/api/auth/register" \
|
||||
-d "username=${ADMIN_USERNAME:-master}" \
|
||||
-d "password=${ADMIN_PASSWORD:-9Aku7MfklZU9ldnZ}" 2>&1)
|
||||
else
|
||||
ADMIN_RESULT=$(curl -s -b "$COOKIE_JAR" -X POST "http://127.0.0.1:8000/api/auth/register" \
|
||||
-H "X-CSRF-Token: $CSRF" \
|
||||
-d "username=${ADMIN_USERNAME:-master}" \
|
||||
-d "password=${ADMIN_PASSWORD:-9Aku7MfklZU9ldnZ}" 2>&1)
|
||||
fi
|
||||
|
||||
if echo "$ADMIN_RESULT" | grep -qi '"success"\|"ok"\|"registered'; then
|
||||
echo " [OK] Admin account created: $ADMIN_USERNAME"
|
||||
elif echo "$ADMIN_RESULT" | grep -qi 'already\|exist'; then
|
||||
echo " [OK] Admin account already exists"
|
||||
else
|
||||
echo " [INFO] Admin registration response: $ADMIN_RESULT"
|
||||
fi
|
||||
|
||||
# Step 2: Login with Zoraxy admin for LE configuration
|
||||
echo " Logging in for LE configuration..."
|
||||
CSRF=$(fetch_csrf)
|
||||
if [ -z "$CSRF" ]; then
|
||||
echo " [WARN] No CSRF token — skipping LE automation"
|
||||
else
|
||||
LOGIN_RESULT=$(curl -s -b "$COOKIE_JAR" -X POST "http://127.0.0.1:8000/api/auth/login" \
|
||||
-H "X-CSRF-Token: $CSRF" \
|
||||
-d "username=${ADMIN_USERNAME:-master}" \
|
||||
-d "password=${ADMIN_PASSWORD:-9Aku7MfklZU9ldnZ}" 2>&1)
|
||||
|
||||
if echo "$LOGIN_RESULT" | grep -qi '"success"\|"ok"'; then
|
||||
echo " [OK] Logged in as $ADMIN_USERNAME"
|
||||
|
||||
# Set LE email
|
||||
echo " Setting Let's Encrypt email: ${TLS_EMAIL}"
|
||||
curl -s -b "$COOKIE_JAR" -X POST "http://127.0.0.1:8000/api/acme/autoRenew/email" \
|
||||
-d "set=${TLS_EMAIL}" > /dev/null
|
||||
|
||||
# Obtain certificate
|
||||
echo " Requesting LE certificate for app.${DOMAIN}..."
|
||||
CERT_RESULT=$(curl -s -b "$COOKIE_JAR" -X GET "http://127.0.0.1:8000/api/acme/obtainCert" \
|
||||
-G -d "domains=app.${DOMAIN}" -d "filename=app.${DOMAIN}" \
|
||||
-d "email=${TLS_EMAIL}" -d "ca=Let's Encrypt" -d "dns=false" 2>&1)
|
||||
if echo "$CERT_RESULT" | grep -qi '"success"\|"ok"\|"installed'; then
|
||||
echo " [OK] Certificate obtained for app.${DOMAIN}"
|
||||
else
|
||||
echo " [INFO] Certificate result: $CERT_RESULT (expected if DNS doesn't resolve yet)"
|
||||
fi
|
||||
|
||||
# Enable auto-renew
|
||||
echo " Enabling auto-renew..."
|
||||
curl -s -b "$COOKIE_JAR" -X POST "http://127.0.0.1:8000/api/acme/autoRenew/enable" \
|
||||
-d "enable=true" > /dev/null
|
||||
echo " [OK] LE auto-renew enabled"
|
||||
else
|
||||
echo " [WARN] Login failed: $LOGIN_RESULT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Restart Zoraxy to pick up configs
|
||||
echo "[*] Restarting Zoraxy to apply configs..."
|
||||
podman-compose -f "$TARGET_DIR/compose/zoraxy.yaml" restart 2>&1 || true
|
||||
sleep 2
|
||||
|
||||
# Write systemd service with EnvironmentFile for .env vars
|
||||
echo "[*] Writing systemd service..."
|
||||
cat > /etc/systemd/system/$SERVICE_NAME.service <<UNIT
|
||||
[Unit]
|
||||
Description=NextWorkspace Launcher + Auth Proxy
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Environment=CONFIG_DIR=$TARGET_DIR/config/nextworkspace
|
||||
EnvironmentFile=$TARGET_DIR/.env
|
||||
ExecStart=$TARGET_DIR/$BINARY_NAME
|
||||
WorkingDirectory=$TARGET_DIR
|
||||
Restart=always
|
||||
User=root
|
||||
Group=root
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNIT
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now $SERVICE_NAME
|
||||
|
||||
# --- Smart update path ---
|
||||
else
|
||||
echo "[3/6] Stopping launcher..."
|
||||
systemctl stop $SERVICE_NAME 2>/dev/null || true
|
||||
|
||||
echo "[4/6] Swapping binary..."
|
||||
cp "$BINARY_NAME" "$TARGET_DIR/$BINARY_NAME"
|
||||
|
||||
echo "[5/6] Refreshing configs..."
|
||||
if [ -f config/nextworkspace/apps.yaml ]; then
|
||||
cp config/nextworkspace/apps.yaml "$TARGET_DIR/config/nextworkspace/apps.yaml"
|
||||
fi
|
||||
if [ -d config/zoraxy/conf/proxy ]; then
|
||||
# Unlock, copy, re-lock to prevent Zoraxy from clearing OriginIpOrDomain
|
||||
chattr -R -i "$TARGET_DIR/config/zoraxy/conf/proxy/" 2>/dev/null || true
|
||||
cp config/zoraxy/conf/proxy/* "$TARGET_DIR/config/zoraxy/conf/proxy/" 2>/dev/null || true
|
||||
chattr -R +i "$TARGET_DIR/config/zoraxy/conf/proxy/" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "[6/6] Restarting Zoraxy and launcher..."
|
||||
podman-compose -f "$TARGET_DIR/compose/zoraxy.yaml" restart 2>&1 || true
|
||||
systemctl restart $SERVICE_NAME
|
||||
fi
|
||||
|
||||
# --- Health check ---
|
||||
echo "[*] Running health check..."
|
||||
for i in $(seq 1 $HEALTH_CHECK_RETRIES); do
|
||||
if curl -sf http://127.0.0.1:9000/health > /dev/null 2>&1; then
|
||||
echo "[OK] NextWorkspace launcher is healthy on http://127.0.0.1:9000/"
|
||||
exit 0
|
||||
fi
|
||||
echo " Attempt $i/$HEALTH_CHECK_RETRIES — not ready yet..."
|
||||
sleep $HEALTH_CHECK_INTERVAL
|
||||
done
|
||||
|
||||
echo "[FAIL] Health check failed — launcher did not respond on port 9000"
|
||||
exit 1
|
||||
10
go.mod
10
go.mod
|
|
@ -2,9 +2,13 @@ module nextworkspace
|
|||
|
||||
go 1.25.0
|
||||
|
||||
require gopkg.in/yaml.v3 v3.0.1
|
||||
require (
|
||||
go.etcd.io/bbolt v1.5.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
go.etcd.io/bbolt v1.5.0 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
golang.org/x/crypto v0.53.0 // indirect
|
||||
golang.org/x/sync v0.21.0 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
)
|
||||
|
|
|
|||
14
go.sum
14
go.sum
|
|
@ -1,7 +1,17 @@
|
|||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU=
|
||||
go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
|
|
|||
88
install.sh
88
install.sh
|
|
@ -1,88 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# NextWorkspace Installer — bootstraps a bare Linux VM
|
||||
# Idempotent: safe to run multiple times.
|
||||
|
||||
TARGET_DIR="/opt/nextworkspace"
|
||||
|
||||
# --- Create runtime directory structure ---
|
||||
echo "=== NextWorkspace Setup ==="
|
||||
mkdir -p "$TARGET_DIR/config/nextworkspace"
|
||||
mkdir -p "$TARGET_DIR/config/zoraxy/conf/proxy"
|
||||
mkdir -p "$TARGET_DIR/data/zoraxy"
|
||||
mkdir -p "$TARGET_DIR/compose"
|
||||
mkdir -p "$TARGET_DIR/logs"
|
||||
|
||||
# --- Interactive prompts ---
|
||||
read -p "Domain [nextwks.eu]: " DOMAIN
|
||||
DOMAIN="${DOMAIN:-nextwks.eu}"
|
||||
|
||||
read -p "TLS email (Let's Encrypt) [den@2-4-h.net]: " TLS_EMAIL
|
||||
TLS_EMAIL="${TLS_EMAIL:-den@2-4-h.net}"
|
||||
|
||||
read -p "Admin username [master]: " ADMIN_USERNAME
|
||||
ADMIN_USERNAME="${ADMIN_USERNAME:-master}"
|
||||
|
||||
read -p "Admin password [auto-generated]: " ADMIN_PASSWORD
|
||||
if [ -z "$ADMIN_PASSWORD" ]; then
|
||||
ADMIN_PASSWORD="9Aku7MfklZU9ldnZ"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " Admin username: $ADMIN_USERNAME"
|
||||
echo " Admin password: $ADMIN_PASSWORD"
|
||||
echo " Save this password — it won't be shown again!"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# Write .env file at runtime root
|
||||
ENV_FILE="$TARGET_DIR/.env"
|
||||
cat > "$ENV_FILE" <<EOF
|
||||
# NextWorkspace Configuration
|
||||
# This file is auto-generated by install.sh — do not edit manually
|
||||
DOMAIN=$DOMAIN
|
||||
TLS_EMAIL=$TLS_EMAIL
|
||||
ADMIN_USERNAME=$ADMIN_USERNAME
|
||||
ADMIN_PASSWORD=$ADMIN_PASSWORD
|
||||
EOF
|
||||
|
||||
chmod 600 "$ENV_FILE"
|
||||
|
||||
# ---- Go ----
|
||||
if command -v go &>/dev/null; then
|
||||
echo "[SKIP] Go already installed: $(go version)"
|
||||
else
|
||||
echo "[INSTALL] Installing Go..."
|
||||
GO_URL="https://go.dev/dl/$(curl -sL https://go.dev/VERSION?m=text | head -1).linux-amd64.tar.gz"
|
||||
curl -sL "$GO_URL" -o /tmp/go.tar.gz
|
||||
rm -rf /usr/local/go
|
||||
tar -C /usr/local -xzf /tmp/go.tar.gz
|
||||
rm /tmp/go.tar.gz
|
||||
echo 'export PATH=$PATH:/usr/local/go/bin' > /etc/profile.d/go.sh
|
||||
chmod +x /etc/profile.d/go.sh
|
||||
export PATH=$PATH:/usr/local/go/bin
|
||||
echo "[OK] Go installed: $(go version)"
|
||||
fi
|
||||
|
||||
# ---- System deps ----
|
||||
echo "[INSTALL] git, build-essential, podman, podman-compose..."
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq git build-essential curl podman podman-compose
|
||||
|
||||
# ---- Clone / pull repo ----
|
||||
REPO_DIR="/opt/NextWks"
|
||||
REPO_URL="https://git.lohmar.co.uk/lexton-it/NextWks.git"
|
||||
|
||||
if [ -d "$REPO_DIR/.git" ]; then
|
||||
echo "[UPDATE] Repository exists — pulling latest..."
|
||||
cd "$REPO_DIR"
|
||||
git pull
|
||||
else
|
||||
echo "[CLONE] Cloning repository..."
|
||||
git clone "$REPO_URL" "$REPO_DIR"
|
||||
fi
|
||||
|
||||
echo "[DONE] Bootstrapping complete. Running first deploy..."
|
||||
"$REPO_DIR/deploy.sh" --destroy
|
||||
32
lng/de/admin.yaml
Normal file
32
lng/de/admin.yaml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
page_title: "Admin Bereich"
|
||||
nav_global: "Global"
|
||||
nav_access: "Zugriff"
|
||||
nav_security: "Sicherheit"
|
||||
nav_domain: "Domain"
|
||||
nav_mail: "E-Mail"
|
||||
nav_docs: "Dokumente"
|
||||
nav_calendar: "Kalender"
|
||||
save: "Einstellungen speichern"
|
||||
saved: "✓ Gespeichert"
|
||||
cancel: "Abbrechen"
|
||||
company_section: "Unternehmen"
|
||||
company_name: "Unternehmensname"
|
||||
company_subtitle: "Untertitel"
|
||||
company_logo: "Logo URL"
|
||||
language: "Standardsprache"
|
||||
timezone: "Zeitzone"
|
||||
smtp_section: "SMTP"
|
||||
smtp_host: "SMTP Host"
|
||||
smtp_port: "SMTP Port"
|
||||
smtp_user: "SMTP Benutzer"
|
||||
smtp_password: "SMTP Passwort"
|
||||
smtp_sender: "Absender E-Mail"
|
||||
system_section: "System (schreibgeschützt)"
|
||||
domain_label: "Domain"
|
||||
admin_email: "Admin E-Mail"
|
||||
authelia_status: "Authelia"
|
||||
version_label: "Version"
|
||||
global_title: "Globale Einstellungen"
|
||||
access_title: "Zugriffsverwaltung"
|
||||
users_label: "Benutzer"
|
||||
create_user: "Benutzer anlegen"
|
||||
8
lng/de/launcher.yaml
Normal file
8
lng/de/launcher.yaml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
app_title: "NextWorkspace"
|
||||
welcome: "Willkommen, {user}"
|
||||
login: "Anmelden"
|
||||
logout: "Abmelden"
|
||||
launcher_title: "Ihr Arbeitsbereich"
|
||||
no_apps: "Keine Anwendungen verfügbar"
|
||||
admin_panel: "Admin Bereich"
|
||||
settings: "Einstellungen"
|
||||
32
lng/en/admin.yaml
Normal file
32
lng/en/admin.yaml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
page_title: "Admin Panel"
|
||||
nav_global: "Global"
|
||||
nav_access: "Access"
|
||||
nav_security: "Security"
|
||||
nav_domain: "Domain"
|
||||
nav_mail: "Mail"
|
||||
nav_docs: "Docs"
|
||||
nav_calendar: "Calendar"
|
||||
save: "Save Settings"
|
||||
saved: "✓ Saved"
|
||||
cancel: "Cancel"
|
||||
company_section: "Company"
|
||||
company_name: "Company Name"
|
||||
company_subtitle: "Subtitle"
|
||||
company_logo: "Logo URL"
|
||||
language: "Default Language"
|
||||
timezone: "Timezone"
|
||||
smtp_section: "SMTP"
|
||||
smtp_host: "SMTP Host"
|
||||
smtp_port: "SMTP Port"
|
||||
smtp_user: "SMTP User"
|
||||
smtp_password: "SMTP Password"
|
||||
smtp_sender: "Sender Email"
|
||||
system_section: "System (read-only)"
|
||||
domain_label: "Domain"
|
||||
admin_email: "Admin Email"
|
||||
authelia_status: "Authelia"
|
||||
version_label: "Version"
|
||||
global_title: "Global Settings"
|
||||
access_title: "Access Management"
|
||||
users_label: "Users"
|
||||
create_user: "Create User"
|
||||
8
lng/en/launcher.yaml
Normal file
8
lng/en/launcher.yaml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
app_title: "NextWorkspace"
|
||||
welcome: "Welcome, {user}"
|
||||
login: "Sign In"
|
||||
logout: "Logout"
|
||||
launcher_title: "Your Workspace"
|
||||
no_apps: "No applications available"
|
||||
admin_panel: "Admin Panel"
|
||||
settings: "Settings"
|
||||
51
tools/firewall-routing.sh
Executable file
51
tools/firewall-routing.sh
Executable file
|
|
@ -0,0 +1,51 @@
|
|||
#!/bin/bash
|
||||
# =====================================================================
|
||||
# VM LOCAL FIREWALL & PORT REDIRECTION SCRIPT
|
||||
# VM IP: 172.16.9.10 | Internal Interface: eth0 (or similar)
|
||||
# Redirects inbound 80/443 to non-root Caddy on 8080/8443
|
||||
# =====================================================================
|
||||
|
||||
# 1. CLEAN SLATE
|
||||
# Flush all rules and delete custom chains across filter and NAT tables
|
||||
iptables -P INPUT ACCEPT
|
||||
iptables -P FORWARD ACCEPT
|
||||
iptables -P OUTPUT ACCEPT
|
||||
iptables -t nat -F
|
||||
iptables -F
|
||||
iptables -X
|
||||
iptables -t nat -X
|
||||
|
||||
# 2. LOCAL PORT REDIRECTION (Caddy Non-Root Helper)
|
||||
# ---------------------------------------------------------------------
|
||||
# A. Inbound traffic coming from outside the VM (e.g., forwarded from Proxmox)
|
||||
iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-ports 8080
|
||||
iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-ports 8443
|
||||
|
||||
# B. Local traffic generated inside the VM aimed strictly at localhost/127.0.0.1
|
||||
# Note: By specifying '-o lo', you leave your outbound internet (GitHub, Google) untouched!
|
||||
iptables -t nat -A OUTPUT -o lo -p tcp --dport 80 -j REDIRECT --to-ports 8080
|
||||
iptables -t nat -A OUTPUT -o lo -p tcp --dport 443 -j REDIRECT --to-ports 8443
|
||||
|
||||
# 3. VM INPUT FIREWALL RULES
|
||||
# ---------------------------------------------------------------------
|
||||
# Allow everything on loopback
|
||||
iptables -A INPUT -i lo -j ACCEPT
|
||||
|
||||
# Allow established connections (allows responses to your outbound traffic like curl)
|
||||
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
||||
|
||||
# Allow SSH (Port 22) - Important for your Proxmox port forward (22910 -> 22)
|
||||
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
|
||||
|
||||
# Allow the actual redirected Caddy ports from outside (just in case)
|
||||
iptables -A INPUT -p tcp --dport 8080 -j ACCEPT
|
||||
iptables -A INPUT -p tcp --dport 8443 -j ACCEPT
|
||||
|
||||
# Allow alternative app ports (like the 8000 you have forwarded in Proxmox)
|
||||
iptables -A INPUT -p tcp --dport 8000 -j ACCEPT
|
||||
|
||||
# 4. GLOBAL SECURITY DROP RULE
|
||||
# Drop all other unsolicited inbound traffic targeting this VM
|
||||
iptables -A INPUT -j DROP
|
||||
|
||||
echo "VM Firewall and Caddy Redirection Applied Successfully."
|
||||
BIN
tools/hash-password/hash-password
Executable file
BIN
tools/hash-password/hash-password
Executable file
Binary file not shown.
21
tools/hash-password/main.go
Normal file
21
tools/hash-password/main.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Fprintln(os.Stderr, "Usage: hash-password <password>")
|
||||
os.Exit(1)
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(os.Args[1]), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Print(string(hash))
|
||||
}
|
||||
42
tools/manage-users.sh
Normal file
42
tools/manage-users.sh
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#!/usr/bin/env bash
|
||||
# Manage users via authelia-api
|
||||
# Usage: ./manage-users.sh list
|
||||
# ./manage-users.sh create username displayname email groups...
|
||||
# ./manage-users.sh delete username
|
||||
|
||||
API_BASE="http://127.0.0.1:8080"
|
||||
TOKEN=$(grep -oP 'session_secret: \K.*' /opt/nextworkspace/config/authelia/configuration.yml)
|
||||
|
||||
case "${1:-}" in
|
||||
list)
|
||||
curl -s -H "Authorization: Bearer $TOKEN" "$API_BASE/api/users" | jq . 2>/dev/null || \
|
||||
curl -s -H "Authorization: Bearer $TOKEN" "$API_BASE/api/users"
|
||||
;;
|
||||
create)
|
||||
shift
|
||||
if [ $# -lt 3 ]; then
|
||||
echo "Usage: $0 create username displayname email [groups...]" >&2
|
||||
exit 1
|
||||
fi
|
||||
USERNAME="$1"; DISPLAY="$2"; EMAIL="$3"; shift 3
|
||||
GROUPS='["users"'
|
||||
for g in "$@"; do GROUPS="$GROUPS,\"$g\""; done
|
||||
GROUPS="$GROUPS]"
|
||||
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"users\":[{\"username\":\"$USERNAME\",\"display_name\":\"$DISPLAY\",\"email\":\"$EMAIL\",\"groups\":$GROUPS}]}" \
|
||||
"$API_BASE/api/users/bulk"
|
||||
;;
|
||||
delete)
|
||||
if [ -z "${2:-}" ]; then
|
||||
echo "Usage: $0 delete username" >&2
|
||||
exit 1
|
||||
fi
|
||||
curl -s -X DELETE -H "Authorization: Bearer $TOKEN" \
|
||||
"$API_BASE/api/users/$2"
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {list|create|delete} ..." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
356
tools/nextwks.sh
Executable file
356
tools/nextwks.sh
Executable file
|
|
@ -0,0 +1,356 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_URL="https://git.lohmar.co.uk/lexton-it/NextWks.git"
|
||||
BUILD_DIR="/tmp/nextwks-build"
|
||||
TARGET_DIR="/opt/nextworkspace"
|
||||
BACKUP_DIR="/opt/backup"
|
||||
NETWORK_NAME="nextwks-net"
|
||||
HEALTH_CHECK_RETRIES=15
|
||||
HEALTH_CHECK_INTERVAL=3
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 [--install|--update|--destroy]"
|
||||
echo " --install First-time setup on a bare VM (prompts for config)"
|
||||
echo " --update Smart update: pull, build, copy, bounce containers"
|
||||
echo " --destroy Full greenfield redeploy (uses saved secrets)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
[ $# -eq 0 ] && usage
|
||||
MODE="${1#--}"
|
||||
case "$MODE" in install|update|destroy) ;; *) usage ;; esac
|
||||
|
||||
# MUST NOT run as root — podman must be rootless
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
echo "ERROR: Do NOT run this script with sudo or as root."
|
||||
echo " Run it as your normal user: ./nextwks.sh --$MODE"
|
||||
echo " The script will prompt for sudo only where needed (apt, /opt/, iptables)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Helper: run with sudo for operations that need root
|
||||
maybe_sudo() {
|
||||
sudo "$@"
|
||||
}
|
||||
|
||||
# --- Load existing env (if any), skip if unreadable ---
|
||||
# Temporarily disable -u because .env may contain $ signs (bcrypt hashes)
|
||||
set +u
|
||||
if [ -r "$BACKUP_DIR/.env" ]; then
|
||||
set -a; source "$BACKUP_DIR/.env"; set +a
|
||||
elif [ -r "$TARGET_DIR/.env" ]; then
|
||||
set -a; source "$TARGET_DIR/.env"; set +a
|
||||
fi
|
||||
set -u
|
||||
if [ -z "${DOMAIN:-}" ]; then
|
||||
echo "ERROR: DOMAIN is not set. Configure it in /opt/backup/.env or run --install to set it up."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== NextWorkspace ${MODE} ==="
|
||||
|
||||
# ============================================================
|
||||
# 1. INSTALL MODE — first-time setup (only on bare VM)
|
||||
# ============================================================
|
||||
if [ "$MODE" = "install" ]; then
|
||||
if [ -f "$TARGET_DIR/nextworkspace" ]; then
|
||||
echo "================================================================="
|
||||
echo " NextWorkspace is already installed at $TARGET_DIR"
|
||||
echo ""
|
||||
echo " Use --update to rebuild and restart:"
|
||||
echo " ./nextwks.sh --update"
|
||||
echo ""
|
||||
echo " Use --destroy for a full greenfield redeploy:"
|
||||
echo " ./nextwks.sh --destroy"
|
||||
echo "================================================================="
|
||||
exit 0
|
||||
fi
|
||||
echo "[*] Installing system dependencies..."
|
||||
maybe_sudo apt-get update -qq
|
||||
maybe_sudo apt-get install -y -qq git build-essential curl podman podman-compose iptables-persistent
|
||||
|
||||
if ! command -v go &>/dev/null; then
|
||||
echo "[*] Installing Go..."
|
||||
GO_VERSION=$(curl -sL https://go.dev/VERSION?m=text)
|
||||
GO_URL="https://go.dev/dl/${GO_VERSION}.linux-amd64.tar.gz"
|
||||
curl -sL "$GO_URL" -o /tmp/go.tar.gz
|
||||
maybe_sudo rm -rf /usr/local/go
|
||||
maybe_sudo tar -C /usr/local -xzf /tmp/go.tar.gz
|
||||
rm /tmp/go.tar.gz
|
||||
maybe_sudo sh -c 'echo "export PATH=\$PATH:/usr/local/go/bin" > /etc/profile.d/go.sh'
|
||||
maybe_sudo chmod +x /etc/profile.d/go.sh
|
||||
export PATH=$PATH:/usr/local/go/bin
|
||||
fi
|
||||
|
||||
# Enable user lingering — containers stay alive after logout
|
||||
maybe_sudo loginctl enable-linger "$USER" 2>/dev/null || true
|
||||
|
||||
# Clean up any old rootful containers from a previous deploy
|
||||
echo "[*] Cleaning up old rootful containers (if any)..."
|
||||
maybe_sudo podman stop caddy authelia launcher 2>/dev/null || true
|
||||
maybe_sudo podman rm caddy authelia launcher 2>/dev/null || true
|
||||
maybe_sudo podman network rm "$NETWORK_NAME" 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
echo "--- NextWorkspace Configuration ---"
|
||||
read -p "Domain [nextwks.eu]: " input; DOMAIN="${input:-$DOMAIN}"
|
||||
read -p "TLS email (Let's Encrypt): " TLS_EMAIL
|
||||
while [ -z "$TLS_EMAIL" ]; do read -p "TLS email (required): " TLS_EMAIL; done
|
||||
while echo "$TLS_EMAIL" | grep -qv '@'; do read -p "Invalid email: " TLS_EMAIL; done
|
||||
|
||||
# Validate required configs
|
||||
if [ -z "$TLS_EMAIL" ] || [ -z "$DOMAIN" ]; then
|
||||
echo "ERROR: TLS_EMAIL and DOMAIN are required."
|
||||
exit 1
|
||||
fi
|
||||
read -p "Admin username: " ADMIN_USERNAME
|
||||
while [ -z "$ADMIN_USERNAME" ]; do read -p "Admin username (required): " ADMIN_USERNAME; done
|
||||
# 24 chars, mixed case + numbers, no special chars (safe for .env)
|
||||
ADMIN_PASSWORD=$(openssl rand -base64 30 | tr -dc 'A-Za-z0-9')
|
||||
ADMIN_PASSWORD="${ADMIN_PASSWORD:0:24}"
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " Domain: $DOMAIN"
|
||||
echo " TLS email: $TLS_EMAIL"
|
||||
echo " Admin username: $ADMIN_USERNAME"
|
||||
echo " Admin password: $ADMIN_PASSWORD"
|
||||
echo " Save this password — it won't be shown again!"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
read -p "SMTP host [smtp.openxchange.eu]: " SMTP_HOST; SMTP_HOST="${SMTP_HOST:-smtp.openxchange.eu}"
|
||||
read -p "SMTP port [587]: " SMTP_PORT; SMTP_PORT="${SMTP_PORT:-587}"
|
||||
read -p "SMTP user [post@nextwks.eu]: " SMTP_USER; SMTP_USER="${SMTP_USER:-post@nextwks.eu}"
|
||||
read -sp "SMTP password: " SMTP_PASS; echo ""
|
||||
[ -z "$SMTP_PASS" ] && echo "ERROR: SMTP password required" && exit 1
|
||||
|
||||
read -p "IMAP host [imap.openxchange.eu]: " IMAP_HOST; IMAP_HOST="${IMAP_HOST:-imap.openxchange.eu}"
|
||||
read -p "IMAP port [993]: " IMAP_PORT; IMAP_PORT="${IMAP_PORT:-993}"
|
||||
|
||||
# Persist config to backup vault (single-quote values to protect $ signs)
|
||||
maybe_sudo mkdir -p "$BACKUP_DIR"
|
||||
maybe_sudo sh -c "cat > '$BACKUP_DIR/.env' <<'ENVEOF'
|
||||
# NextWorkspace Configuration — auto-generated by nextwks.sh --install
|
||||
DOMAIN='$DOMAIN'
|
||||
TLS_EMAIL='$TLS_EMAIL'
|
||||
ADMIN_USERNAME='$ADMIN_USERNAME'
|
||||
ADMIN_PASSWORD='$ADMIN_PASSWORD'
|
||||
SMTP_HOST='$SMTP_HOST'
|
||||
SMTP_PORT='$SMTP_PORT'
|
||||
SMTP_USER='$SMTP_USER'
|
||||
SMTP_PASS='$SMTP_PASS'
|
||||
IMAP_HOST='$IMAP_HOST'
|
||||
IMAP_PORT='$IMAP_PORT'
|
||||
ENVEOF"
|
||||
maybe_sudo chmod 600 "$BACKUP_DIR/.env"
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# 2. CLONE fresh (every mode — ensures latest code)
|
||||
# ============================================================
|
||||
echo "[*] Cloning repository..."
|
||||
rm -rf "$BUILD_DIR"
|
||||
git clone --depth 1 "$REPO_URL" "$BUILD_DIR"
|
||||
cd "$BUILD_DIR"
|
||||
|
||||
# Save script to user's home for easy future access (--install only)
|
||||
if [ "$MODE" = "install" ]; then
|
||||
cp "$BUILD_DIR/tools/nextwks.sh" "$HOME/nextwks.sh"
|
||||
chmod +x "$HOME/nextwks.sh"
|
||||
echo "[*] Saved to $HOME/nextwks.sh — use it for future updates"
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# 3. FIREWALL SETUP (all modes)
|
||||
# ============================================================
|
||||
if [ "$MODE" = "install" ]; then
|
||||
echo "[*] Applying firewall and port redirects (80→8080, 443→8443)..."
|
||||
maybe_sudo bash "$BUILD_DIR/tools/firewall-routing.sh"
|
||||
elif [ "$MODE" = "update" ] || [ "$MODE" = "destroy" ]; then
|
||||
# Lightweight: ensure redirects exist without flushing existing rules
|
||||
echo "[*] Ensuring port redirects (80→8080, 443→8443)..."
|
||||
maybe_sudo iptables -t nat -C PREROUTING -p tcp --dport 80 -j REDIRECT --to-ports 8080 2>/dev/null || \
|
||||
maybe_sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-ports 8080
|
||||
maybe_sudo iptables -t nat -C PREROUTING -p tcp --dport 443 -j REDIRECT --to-ports 8443 2>/dev/null || \
|
||||
maybe_sudo iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-ports 8443
|
||||
maybe_sudo iptables -t nat -C OUTPUT -o lo -p tcp --dport 80 -j REDIRECT --to-ports 8080 2>/dev/null || \
|
||||
maybe_sudo iptables -t nat -A OUTPUT -o lo -p tcp --dport 80 -j REDIRECT --to-ports 8080
|
||||
maybe_sudo iptables -t nat -C OUTPUT -o lo -p tcp --dport 443 -j REDIRECT --to-ports 8443 2>/dev/null || \
|
||||
maybe_sudo iptables -t nat -A OUTPUT -o lo -p tcp --dport 443 -j REDIRECT --to-ports 8443
|
||||
fi
|
||||
# Persist across reboots (always)
|
||||
if command -v netfilter-persistent &>/dev/null; then
|
||||
maybe_sudo netfilter-persistent save 2>/dev/null || true
|
||||
else
|
||||
maybe_sudo mkdir -p /etc/iptables
|
||||
maybe_sudo sh -c 'iptables-save > /etc/iptables/rules.v4'
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# 4. BUILD static binary
|
||||
# ============================================================
|
||||
echo "[*] Building static binary..."
|
||||
export PATH=$PATH:/usr/local/go/bin
|
||||
CGO_ENABLED=0 go build -o nextworkspace .
|
||||
|
||||
# ============================================================
|
||||
# 5. STOP containers (all modes — binary is mounted, must stop before copy)
|
||||
# ============================================================
|
||||
echo "[*] Stopping containers..."
|
||||
podman stop caddy authelia launcher 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
# ============================================================
|
||||
# 6. CREATE target & backup directories (as root)
|
||||
# ============================================================
|
||||
maybe_sudo mkdir -p "$TARGET_DIR/config/caddy" "$TARGET_DIR/config/authelia" \
|
||||
"$TARGET_DIR/data/caddy" "$TARGET_DIR/data/authelia" \
|
||||
"$TARGET_DIR/compose" "$TARGET_DIR/www" \
|
||||
"$TARGET_DIR/config/nextworkspace" "$TARGET_DIR/logs" \
|
||||
"$BACKUP_DIR"
|
||||
|
||||
# ============================================================
|
||||
# 7. TEARDOWN (destroy mode only — wipes target dir)
|
||||
# ============================================================
|
||||
if [ "$MODE" = "destroy" ]; then
|
||||
echo "[*] Full teardown..."
|
||||
# Stop rootless containers
|
||||
podman stop caddy authelia launcher 2>/dev/null || true
|
||||
podman rm caddy authelia launcher 2>/dev/null || true
|
||||
podman network rm -f "$NETWORK_NAME" 2>/dev/null || true
|
||||
# Wipe target
|
||||
maybe_sudo rm -rf "$TARGET_DIR"
|
||||
maybe_sudo mkdir -p "$TARGET_DIR/config/caddy" "$TARGET_DIR/config/authelia" \
|
||||
"$TARGET_DIR/data/caddy" "$TARGET_DIR/data/authelia" \
|
||||
"$TARGET_DIR/compose" "$TARGET_DIR/www" \
|
||||
"$TARGET_DIR/config/nextworkspace" "$TARGET_DIR/logs"
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# 8. COPY artifacts to target (as root)
|
||||
# ============================================================
|
||||
echo "[*] Copying artifacts..."
|
||||
maybe_sudo cp nextworkspace "$TARGET_DIR/nextworkspace"
|
||||
maybe_sudo cp "$BUILD_DIR/VERSION" "$TARGET_DIR/VERSION"
|
||||
if [ -d "$BUILD_DIR/config/www" ]; then
|
||||
maybe_sudo cp -r "$BUILD_DIR/config/www"/* "$TARGET_DIR/www/"
|
||||
fi
|
||||
if [ -d "$BUILD_DIR/lng" ]; then
|
||||
maybe_sudo rm -rf "$TARGET_DIR/lng"
|
||||
maybe_sudo cp -r "$BUILD_DIR/lng" "$TARGET_DIR/lng"
|
||||
fi
|
||||
if [ -d "$BUILD_DIR/config/nextworkspace" ]; then
|
||||
maybe_sudo cp -r "$BUILD_DIR/config/nextworkspace"/* "$TARGET_DIR/config/nextworkspace/"
|
||||
fi
|
||||
# Restore .env from backup
|
||||
if [ -f "$BACKUP_DIR/.env" ]; then
|
||||
maybe_sudo cp "$BACKUP_DIR/.env" "$TARGET_DIR/.env"
|
||||
maybe_sudo chmod 644 "$TARGET_DIR/.env"
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# 9. GENERATE config files with placeholder substitution
|
||||
# Write to /tmp first, then sudo cp to target
|
||||
# ============================================================
|
||||
GEN_DIR=$(mktemp -d)
|
||||
trap "rm -rf '$GEN_DIR'" EXIT
|
||||
|
||||
echo "[*] Generating config files..."
|
||||
|
||||
# Caddyfile
|
||||
sed -e "s|{DOMAIN}|$DOMAIN|g" -e "s|{TLS_EMAIL}|$TLS_EMAIL|g" \
|
||||
"$BUILD_DIR/config/caddy/Caddyfile" > "$GEN_DIR/Caddyfile"
|
||||
|
||||
# Authelia config — preserve existing secrets if present
|
||||
JWT_SECRET="${JWT_SECRET:-$(openssl rand -hex 32)}"
|
||||
SESSION_SECRET="${SESSION_SECRET:-$(openssl rand -hex 32)}"
|
||||
STORAGE_ENCRYPTION_KEY="${STORAGE_ENCRYPTION_KEY:-$(openssl rand -hex 32)}"
|
||||
sed -e "s|{DOMAIN}|$DOMAIN|g" -e "s|{JWT_SECRET}|$JWT_SECRET|g" \
|
||||
-e "s|{SESSION_SECRET}|$SESSION_SECRET|g" \
|
||||
-e "s|{STORAGE_ENCRYPTION_KEY}|$STORAGE_ENCRYPTION_KEY|g" \
|
||||
-e "s|{SMTP_HOST}|${SMTP_HOST:-smtp.openxchange.eu}|g" \
|
||||
-e "s|{SMTP_PORT}|${SMTP_PORT:-587}|g" \
|
||||
-e "s|{SMTP_USER}|${SMTP_USER:-post@nextwks.eu}|g" \
|
||||
-e "s|{SMTP_PASS}|$SMTP_PASS|g" \
|
||||
"$BUILD_DIR/config/authelia/configuration.yml" > "$GEN_DIR/configuration.yml"
|
||||
|
||||
# Users database — regenerate hash if ADMIN_PASSWORD is available
|
||||
if [ -n "${ADMIN_PASSWORD:-}" ]; then
|
||||
ADMIN_PASSWORD_HASH=$(cd "$BUILD_DIR" && go run ./tools/hash-password/ "$ADMIN_PASSWORD" 2>/dev/null || echo "$ADMIN_PASSWORD_HASH")
|
||||
fi
|
||||
sed -e "s|{ADMIN_PASSWORD_HASH}|$ADMIN_PASSWORD_HASH|g" \
|
||||
-e "s|{TLS_EMAIL}|$TLS_EMAIL|g" \
|
||||
"$BUILD_DIR/config/authelia/users_database.yml" > "$GEN_DIR/users_database.yml"
|
||||
|
||||
# Copy generated configs to target
|
||||
maybe_sudo cp "$GEN_DIR/Caddyfile" "$TARGET_DIR/config/caddy/Caddyfile"
|
||||
maybe_sudo cp "$GEN_DIR/configuration.yml" "$TARGET_DIR/config/authelia/configuration.yml"
|
||||
maybe_sudo cp "$GEN_DIR/users_database.yml" "$TARGET_DIR/config/authelia/users_database.yml"
|
||||
|
||||
# Persist generated secrets so --destroy is idempotent
|
||||
if [ -f "$BACKUP_DIR/.env" ]; then
|
||||
maybe_sudo sed -i "/^JWT_SECRET=/d; /^SESSION_SECRET=/d; /^STORAGE_ENCRYPTION_KEY=/d; /^ADMIN_PASSWORD_HASH=/d" "$BACKUP_DIR/.env" 2>/dev/null || true
|
||||
fi
|
||||
# Use pipe to avoid bash -c re-expanding $ signs (bcrypt hashes contain $2a$10$...)
|
||||
echo "JWT_SECRET='$JWT_SECRET'" | maybe_sudo tee -a "$BACKUP_DIR/.env" >/dev/null
|
||||
echo "SESSION_SECRET='$SESSION_SECRET'" | maybe_sudo tee -a "$BACKUP_DIR/.env" >/dev/null
|
||||
echo "STORAGE_ENCRYPTION_KEY='$STORAGE_ENCRYPTION_KEY'" | maybe_sudo tee -a "$BACKUP_DIR/.env" >/dev/null
|
||||
[ -n "$ADMIN_PASSWORD_HASH" ] && echo "ADMIN_PASSWORD_HASH='$ADMIN_PASSWORD_HASH'" | maybe_sudo tee -a "$BACKUP_DIR/.env" >/dev/null
|
||||
maybe_sudo chmod 600 "$BACKUP_DIR/.env"
|
||||
|
||||
# ============================================================
|
||||
# 10. FIX OWNERSHIP — all files in TARGET_DIR/BACKUP_DIR to user
|
||||
# ============================================================
|
||||
RUN_USER="${SUDO_USER:-${USER}}"
|
||||
echo "[*] Setting file ownership to $RUN_USER..."
|
||||
maybe_sudo chown -R "$RUN_USER:" "$TARGET_DIR" 2>/dev/null || true
|
||||
maybe_sudo chown -R "$RUN_USER:" "$BACKUP_DIR" 2>/dev/null || true
|
||||
|
||||
# ============================================================
|
||||
# 11. DEPLOY stack (rootless podman — no sudo!)
|
||||
# ============================================================
|
||||
echo "[*] Deploying containers on $NETWORK_NAME..."
|
||||
|
||||
podman network rm -f "$NETWORK_NAME" 2>/dev/null || true
|
||||
podman network create --subnet 172.18.0.0/24 "$NETWORK_NAME"
|
||||
|
||||
# AUTHELIA_SECRET is SESSION_SECRET (Authelia session.secret)
|
||||
AUTHELIA_SECRET="${SESSION_SECRET:-}"
|
||||
if [ -z "$AUTHELIA_SECRET" ]; then
|
||||
AUTHELIA_SECRET=$(sed -n '/^session:/,/^[a-z]/p' "$TARGET_DIR/config/authelia/configuration.yml" \
|
||||
| grep 'secret:' | awk '{print $2}' 2>/dev/null || echo "")
|
||||
fi
|
||||
|
||||
# Generate compose file with substituted secret
|
||||
sed -e "s|{AUTHELIA_SECRET}|$AUTHELIA_SECRET|g" \
|
||||
"$BUILD_DIR/compose/stack.yaml" > "$GEN_DIR/stack.yaml"
|
||||
cp "$GEN_DIR/stack.yaml" "$TARGET_DIR/compose/stack.yaml"
|
||||
|
||||
podman-compose -f "$TARGET_DIR/compose/stack.yaml" down 2>/dev/null || true
|
||||
sleep 1
|
||||
podman-compose -f "$TARGET_DIR/compose/stack.yaml" up -d 2>&1 || echo "[WARN] Stack deploy had issues"
|
||||
|
||||
# ============================================================
|
||||
# 12. HEALTH CHECK
|
||||
# ============================================================
|
||||
echo "[*] Running health check..."
|
||||
for i in $(seq 1 $HEALTH_CHECK_RETRIES); do
|
||||
HEALTH=$(podman exec launcher curl -sf http://127.0.0.1:9000/health 2>/dev/null || echo "")
|
||||
if [ "$HEALTH" = "OK" ]; then
|
||||
echo "[OK] NextWorkspace launcher is healthy"
|
||||
echo "[OK] https://$DOMAIN/"
|
||||
exit 0
|
||||
fi
|
||||
sleep $HEALTH_CHECK_INTERVAL
|
||||
done
|
||||
|
||||
echo "[FAIL] Health check failed — launcher did not respond"
|
||||
echo ""
|
||||
echo "--- Container status ---"
|
||||
podman ps -a --filter "name=caddy|authelia|launcher" 2>/dev/null || true
|
||||
echo ""
|
||||
echo "--- Launcher logs (last 20 lines) ---"
|
||||
podman logs launcher --tail 20 2>/dev/null || echo " (no logs)"
|
||||
exit 1
|
||||
Loading…
Reference in a new issue