Compare commits

...

195 commits
v0.1.0 ... main

Author SHA1 Message Date
9364b4af81 fix: CSV import loading spinner, error display, modal close 2026-07-15 23:03:03 +01:00
6dedc6d432 fix: save original user data before edit, restore on recreate failure 2026-07-15 21:18:05 +01:00
4a9d5f7ede chore: update TOTP enforcement message, policy API integration ready 2026-07-15 20:51:10 +01:00
517c8849ec fix: Alpine package is 'sqlite' not 'sqlite3' 2026-07-15 19:15:23 +01:00
e1a4566cc3 feat: enforce TOTP enrollment via SQLite when email is saved 2026-07-15 14:11:39 +01:00
f4fe31c561 fix: retry recreate on edit user to avoid loss on SQLITE_BUSY 2026-07-14 06:26:29 +01:00
5cff118d0c feat: MFA enforcement - blocking overlay after email save until TFA is set up 2026-07-12 07:55:25 +01:00
44326c542d feat: MFA prompt in user settings page 2026-07-12 07:29:36 +01:00
48cda41d69 fix: add 1.5s delay between delete and recreate to avoid SQLITE_BUSY 2026-07-11 23:04:11 +01:00
6cf084b931 feat: edit user groups + email via delete+recreate 2026-07-11 22:55:08 +01:00
359a0dccb6 remove domain fallback defaults - domain must be explicitly configured 2026-07-11 22:13:47 +01:00
ae9b0b7a81 revert sender to SMTP_USER (post@nextwks.eu) - SMTP requires sender=login user 2026-07-11 22:10:36 +01:00
aa1be0d4cd fix: sender should be TLS_EMAIL (dns@nextwks.eu) not SMTP_USER 2026-07-11 22:07:26 +01:00
b32180c268 modernize Authelia config to v4.39 format, add watch:true 2026-07-11 22:01:08 +01:00
d82b1a9a6b fix: add watch:true to Authelia config so new users auto-reload 2026-07-11 21:55:10 +01:00
03ae910b3f refactor: groups simplified to users + admins only 2026-07-11 21:39:45 +01:00
1849c65c68 fix apiProxyHandler: stop stripping /api prefix, authelia-api needs it 2026-07-11 17:59:23 +01:00
a21e63c4a2 explicit AUTHELIA_API_LISTEN=0.0.0.0:8080 in compose 2026-07-11 17:51:11 +01:00
0d7defff4e fix network creation: use -f for rm, show errors on create 2026-07-11 10:14:10 +01:00
14e60151fd fixed IPs: nextwks-net on 172.18.0.0/24 with static addresses 2026-07-11 10:08:52 +01:00
62b42635b2 move AGENT.md out of repo to ~/development/, add to .gitignore 2026-07-11 09:24:47 +01:00
9f6c85ce60 v0.1.0.0033: update AGENT.md/README.md/CHANGELOG with final architecture docs 2026-07-11 09:20:29 +01:00
7303c94a3c fix bcrypt hash corruption: avoid bash -c re-expanding $ signs, auto-regenerate hash on update 2026-07-11 09:16:14 +01:00
f0c6aa1a2e always regenerate configs on --update too (picks up template changes like storage.encryption_key) 2026-07-11 09:13:22 +01:00
080341f8a8 stop containers before copying binary to avoid Text file busy 2026-07-11 09:11:27 +01:00
034e6688de firewall: apply redirects in all modes, persist across reboots 2026-07-11 09:09:48 +01:00
9ba62e4c8b add storage.encryption_key to Authelia config, fix certs directory 2026-07-11 09:08:31 +01:00
0dbb576d1b admin password: 24 chars mixed case + numbers via base64 2026-07-11 09:00:56 +01:00
336574571f admin password: 24 hex chars instead of 12 2026-07-11 09:00:08 +01:00
fc93201313 install mode: detect existing install and deflect to --update or --destroy 2026-07-11 08:57:36 +01:00
fc1a98cbfe fix .gitignore: scope nextworkspace to root to not ignore config/nextworkspace/ 2026-07-11 08:54:56 +01:00
3df375e047 refuse to run under sudo, update README/AGENT.md to match 2026-07-11 08:47:54 +01:00
b060e23a1d add tools/firewall-routing.sh, fix .env Permission denied, apply firewall from cloned repo 2026-07-11 08:42:19 +01:00
e92382266f rootless podman: ports 8080/8443, iptables redirect 80→8080 443→8443, no sudo for podman 2026-07-11 00:38:24 +01:00
3ca19403fa nextwks.sh: replace full root escalation with per-command sudo, files owned by user 2026-07-11 00:34:21 +01:00
d41d1443a8 fix launcher health check: install curl in alpine, update script + applySettings path 2026-07-11 00:28:44 +01:00
89ea56e255 nextwks.sh: chown target dirs to non-root user after deploy 2026-07-11 00:23:52 +01:00
1b24799026 nextwks.sh: add IMAP prompts, stop old containers in all modes, port conflict check, health check diagnostics 2026-07-11 00:21:21 +01:00
2999f5da47 nextwks.sh: fix SIGPIPE crash on password gen and Go version fetch under pipefail 2026-07-11 00:17:07 +01:00
d23b6c2909 nextwks.sh: auto-escalate to root via sudo instead of erroring out 2026-07-11 00:14:55 +01:00
6e95c088e4 nextwks.sh: add root check with clear error message 2026-07-11 00:13:01 +01:00
6113f62ad2 README: add explicit curl download command before install 2026-07-11 00:07:56 +01:00
81e5505f2a Unified install/update/destroy script with ephemeral build dir
- Replace deploy.sh and install.sh with tools/nextwks.sh
- Build in /tmp/nextwks-build (fresh clone every time), no more /opt/NextWks
- Script saves itself to ~/nextwks.sh on --install for easy future access
- Add AGENT.md with workflow rules for the new approach
- Secrets persisted in /opt/backup/.env (JWT, SESSION, password hash)
- Update README and CHANGELOG
2026-07-11 00:02:55 +01:00
07288f4e19 chore: bump to 0.1.0.0031 2026-07-10 23:34:19 +01:00
7b797f0785 fix: version display in admin header 2026-07-10 23:34:10 +01:00
2a255b55ec chore: bump to 0.1.0.0030 2026-07-10 23:31:06 +01:00
20cc74ae50 fix: translation loader path to /opt/nextworkspace/lng 2026-07-10 23:30:57 +01:00
b7c65d37cd chore: bump to 0.1.0.0029 2026-07-10 23:24:43 +01:00
a4b92feed9 fix: show git pull errors 2026-07-10 23:24:24 +01:00
54b2baf36c chore: bump to 0.1.0.0028 2026-07-10 23:21:42 +01:00
960ee5bc7c fix: smart update stops and removes old containers before redeploy 2026-07-10 23:21:33 +01:00
cd4c48b2f5 chore: bump to 0.1.0.0027 2026-07-10 23:03:31 +01:00
10d8d8bfc6 fix: inject AUTHELIA_SECRET into launcher compose env 2026-07-10 22:48:05 +01:00
87320e43e1 fix: auto-assign subnet, no static IPs (avoids conflicts) 2026-07-10 22:10:43 +01:00
9083119aca fix: use 10.200.0.0/24 subnet (172.16.0.0/16 is host network) 2026-07-10 22:09:46 +01:00
634cab3464 fix: static IPs 172.16.0.0/24 in stack.yaml 2026-07-10 22:05:30 +01:00
30b0b88660 fix: add :Z to caddy data volume for permission handling 2026-07-10 21:58:43 +01:00
5e3c75d564 fix: kill old Caddy processes, fix .env permissions 2026-07-10 16:47:06 +01:00
b24bc4246b fix: show podman network create errors (no silent failure) 2026-07-10 16:43:49 +01:00
12b2a787ab fix: single stack.yaml for all containers (avoids pod conflicts) 2026-07-10 16:40:39 +01:00
b555f153be fix: podman-compose down before up -d for clean state 2026-07-10 16:36:49 +01:00
414ead35d0 fix: pod cleanup loop, healthcheck in all compose files 2026-07-10 16:32:32 +01:00
8770476d61 fix: force clean all old pods before fresh deploy 2026-07-10 16:28:20 +01:00
8d2ffb503a fix: all 3 containers on internal network, NO host networking 2026-07-10 16:13:57 +01:00
11bcd78a13 fix: static binary build, debian base image, env_file 2026-07-10 15:35:35 +01:00
8561f59beb fix: use container names instead of static IPs 2026-07-10 15:26:46 +01:00
7e12e8d831 fix: simplify podman network creation (no subnet, non-silent) 2026-07-10 15:26:02 +01:00
956b9f2968 fix: add missing BINARY_NAME variable 2026-07-10 14:30:52 +01:00
079ca4bedb fix: proper teardown and health check for podman network 2026-07-10 13:58:58 +01:00
2ce600bb6d fix: podman network with fixed IPs, remove host networking 2026-07-10 13:54:33 +01:00
55e1cbece2 chore: bump to 0.1.0.0026 2026-07-10 13:20:18 +01:00
66eb350564 fix: inject Bearer token in API proxy 2026-07-10 13:19:45 +01:00
e39d1b9ebd chore: bump to 0.1.0.0025 2026-07-10 13:16:30 +01:00
31bb7771cd feat: user management in Access tab with modal 2026-07-10 13:15:48 +01:00
bb00b5cfb7 chore: bump to 0.1.0.0024 2026-07-10 13:10:44 +01:00
73bc5fa528 fix: populate Manager and TopUser dropdowns 2026-07-10 13:10:12 +01:00
b30ddacef0 chore: bump to 0.1.0.0023 2026-07-10 12:48:16 +01:00
5374473a59 fix: rename Department to Office Location 2026-07-10 12:44:59 +01:00
e34e71bf6a chore: bump to 0.1.0.0022 2026-07-10 12:37:12 +01:00
19817691b2 feat: people directory with rich profiles 2026-07-10 12:36:40 +01:00
d9f480be95 chore: bump to 0.1.0.0021 2026-07-10 12:27:28 +01:00
2e21aa8bea fix: register FuncMap for settings template 2026-07-10 12:27:19 +01:00
1e37c33603 feat: user settings page with profile, mail, preferences 2026-07-10 12:25:18 +01:00
e7566e66c7 chore: bump to 0.1.0.0020 2026-07-10 12:04:09 +01:00
ee29b28156 fix: add time import for net.DialTimeout 2026-07-10 12:03:37 +01:00
a1d4368a0a fix: chown data/caddy to master in install.sh 2026-07-10 11:59:15 +01:00
2026510572 fix: chown data/caddy to master after directory creation 2026-07-10 11:57:43 +01:00
a38e9fa7f3 chore: bump to 0.1.0.0019 2026-07-10 11:55:23 +01:00
5bff56dcda chore: bump to 0.1.0.0018 2026-07-10 11:45:06 +01:00
1ca2b028c8 feat: IMAP host/port in global settings 2026-07-10 11:44:31 +01:00
6b00f081c0 chore: bump to 0.1.0.0017 2026-07-10 11:30:32 +01:00
7868322af3 fix: run all containers as master user (rootless) 2026-07-10 11:18:02 +01:00
fc066da921 chore: bump to 0.1.0.0016 2026-07-10 10:42:00 +01:00
af85ea31e7 feat: service status indicators (Caddy, Authelia, Launcher) 2026-07-10 10:41:33 +01:00
60669b47a3 fix: add .Lang to all template translation calls 2026-07-10 10:37:40 +01:00
bde90a8186 feat: multi-language with FuncMap + lng/ files 2026-07-10 08:15:14 +01:00
45f2509fe4 chore: bump to 0.1.0.0015 2026-07-08 15:55:32 +01:00
3f3e929374 fix: use FuncMap for translations in admin template 2026-07-08 15:54:57 +01:00
207f7532be fix: roll back launcher template to simple version, no translations in templates 2026-07-08 15:53:45 +01:00
f6a18dd8e2 chore: bump to 0.1.0.0014 2026-07-08 15:51:20 +01:00
af8dc42dd6 fix: deploy lng directory, absolute translation path 2026-07-08 15:50:49 +01:00
bad04bf390 chore: bump to 0.1.0.0013 2026-07-08 15:32:25 +01:00
9790d9338a feat: global settings, multi-language (en/de), company branding on launcher + www 2026-07-08 15:32:17 +01:00
857ce833cf fix: global settings form inside admin panel sidebar 2026-07-08 15:20:41 +01:00
e1e8ad0f08 chore: bump to 0.1.0.0012 2026-07-08 15:11:07 +01:00
3e04d8f1c0 feat: SMTP config, install prompts, API proxy, global settings page 2026-07-08 15:07:59 +01:00
707eb9ec81 feat: admin panel dashboard layout with sidebar navigation 2026-07-08 14:35:50 +01:00
ee9e533485 fix: admin panel with vertical split layout (sidebar + content) 2026-07-08 14:26:29 +01:00
d699dd34c4 fix: AUTHELIA_SECRET added to .env in both greenfield and smart update 2026-07-08 14:03:52 +01:00
f5744a65d1 fix: add Remote-Groups to Caddy copy_headers 2026-07-08 14:00:39 +01:00
fcacc28f55 chore: bump to 0.1.0.0011 2026-07-08 13:52:53 +01:00
3332bc3d59 feat: settings page with admin panel link for admins 2026-07-08 13:52:45 +01:00
22958d09e7 chore: bump to 0.1.0.0010 2026-07-08 13:49:34 +01:00
f45132962c fix: admin panel — passwordless create, show generated password, user table 2026-07-08 13:49:26 +01:00
993514d92e chore: bump to 0.1.0.0009 2026-07-08 13:46:22 +01:00
594683dbf8 feat: RBAC with Authelia groups + filtered launcher + admin panel 2026-07-08 13:46:02 +01:00
7927addcf7 chore: bump to 0.1.0.0008 2026-07-08 13:34:16 +01:00
80a5579537 docs: readme + changelog for Caddy/Authelia milestone 2026-07-08 13:33:57 +01:00
a56a485f42 fix: podman pod cleanup before fresh deploy 2026-07-08 10:31:24 +01:00
f3d0259bd4 chore: bump to 0.1.0.0007 2026-07-08 10:27:26 +01:00
9b71a13197 fix: backup/restore Caddy certs to /opt/backup/certificates/ 2026-07-08 10:27:18 +01:00
f8468270af chore: bump to 0.1.0.0006 2026-07-08 10:17:57 +01:00
8f25bc5d63 fix: use git24hcom/authelia:latest image 2026-07-08 10:09:41 +01:00
11e893eb02 chore: bump to 0.1.0.0005 2026-07-08 10:07:12 +01:00
ab8e46b3a4 fix: sed delimiter | instead of / to avoid conflicts 2026-07-08 10:07:05 +01:00
07aff0ba28 chore: bump to 0.1.0.0004 2026-07-08 09:56:58 +01:00
c252fb17ed fix: install.sh creates Caddy/Authelia dirs instead of Zoraxy 2026-07-08 09:54:53 +01:00
a287a67fd1 feat: Caddy + Authelia replace Zoraxy 2026-07-08 09:54:39 +01:00
e52b7e6915 fix: CSRF extraction looks for specific zoraxy.csrf.Token 2026-07-08 09:28:54 +01:00
94ae185d47 chore: bump to 0.1.0.0003 2026-07-08 09:13:08 +01:00
61b1587cd6 fix: only app subdomain active 2026-07-08 09:12:47 +01:00
09a74425b3 fix: only app+dns active, www+auth empty, dynamic domain lists 2026-07-08 09:11:02 +01:00
b948399d1d fix: cert upload uses multipart/form-data with cert+key+domain 2026-07-08 09:09:11 +01:00
d420f179a2 chore: remove committed binary, add to gitignore 2026-07-08 08:51:31 +01:00
aa29d363ce fix: use Go tool for cert upload, fix apps.yaml YAML format 2026-07-08 08:51:23 +01:00
6ea3164d72 feat: stateless API token workflow for Zoraxy 2026-07-08 08:42:40 +01:00
abeae6cbbf chore: bump to 0.1.0.0002 2026-07-07 19:29:29 +01:00
5d30facbcc fix: verbose cert upload + health check debug 2026-07-07 19:29:05 +01:00
dea385a71c chore: version format 0.1.0.0001 2026-07-07 19:22:20 +01:00
7a44f112a5 chore: bump to v0.1.1, update changelog, versioning rule in AGENT.md 2026-07-07 19:20:57 +01:00
48241364aa fix: proper workflow — deploy Zoraxy minimal, upload certs, stop, write configs, restart 2026-07-07 19:17:54 +01:00
f44041f0d5 debug: add CSRF token debug output 2026-07-07 18:51:01 +01:00
b6938d3800 fix: clear cookie jar before each cert upload attempt 2026-07-07 18:44:12 +01:00
d6e0387b1c fix: install.sh sets up git repo + overlays current code 2026-07-07 18:40:21 +01:00
b0325267db fix: wait for Zoraxy before cert upload, verbose retry messages 2026-07-07 18:33:39 +01:00
b684242eb9 fix: hardcode subdomains to hub/noc/www/auth 2026-07-07 18:27:15 +01:00
ea82a51307 fix: cert upload timeout, email validation, completion message 2026-07-07 18:21:44 +01:00
efaa917013 feat: LE_DRY_RUN=true skips LE, generates self-signed 2026-07-07 18:20:21 +01:00
c426b711df feat: configurable subdomains via APP_SUBDOMAIN, DNS_SUBDOMAIN, etc. 2026-07-07 18:18:14 +01:00
a3b31ed2ab fix: reduce lego timeout to 30s 2026-07-07 18:11:59 +01:00
1705d1e32d fix: 60s lego timeout, stop on first failure, fall back to self-signed 2026-07-07 18:06:14 +01:00
1298589afa fix: handle existing non-git repo dir (from install.sh copy) 2026-07-07 18:00:44 +01:00
ec739975ec fix: add 2s delay between per-domain lego runs 2026-07-07 17:54:09 +01:00
8b2301f3a4 fix: request individual certs per domain instead of SAN cert 2026-07-07 17:53:48 +01:00
93f853bd40 fix: 12-char password, cert upload API with CSRF retry 2026-07-07 17:17:00 +01:00
47d66f476d fix: alphanumeric password only (no special chars), safe for .env and systemd 2026-07-07 17:11:58 +01:00
585d4e6ec8 fix: install.sh no presets, random password, copies local code instead of cloning 2026-07-07 17:06:54 +01:00
e03cb76017 fix: write BoltDB before Zoraxy starts (db not locked) + remove duplicate 2026-07-07 16:47:27 +01:00
fa9537bcb5 fix: longer wait for sys.db, fallback to touch + userCount check 2026-07-07 16:33:49 +01:00
c42807d076 refactor: configure Zoraxy via BoltDB + config files, no API/CSRF 2026-07-07 16:19:34 +01:00
90c8c57fbe debug: show login.html response when CSRF fetch fails 2026-07-07 16:12:30 +01:00
b1004e4514 fix: BoltDB fallback for admin creation if CSRF API fails 2026-07-07 16:12:05 +01:00
fd1fa7dbeb fix: CSRF token — clear jar before first fetch, preserve session across calls 2026-07-07 16:08:20 +01:00
710781bc47 fix: surface admin registration result instead of swallowing it 2026-07-07 16:05:31 +01:00
b2e3086e10 fix: upload existing LE certs to Zoraxy via API after deploy 2026-07-07 15:51:25 +01:00
13c1bd2d13 fix: set lego path to /opt/backup to avoid double certificates/ nesting 2026-07-07 15:36:23 +01:00
5f46d3c2b8 refactor: rename backup dir to /opt/backup/certificates/ 2026-07-07 15:33:22 +01:00
6d87718b43 refactor: lego outputs to /opt/backup/certs/, one backup directory 2026-07-07 15:27:30 +01:00
c7d25c0107 fix: fallback to lego SAN cert directory if backup missing 2026-07-07 15:24:03 +01:00
881b94099c fix: handle SAN certs from lego, check both backup and lego cache 2026-07-07 15:23:40 +01:00
1eb470fa12 fix: lego v5 flags — use --http.address, -m for email, -d for domains 2026-07-07 15:14:46 +01:00
d144fcdc28 fix: lego v5 = syntax for flags 2026-07-07 15:14:13 +01:00
fbbf1732b3 fix: lego v5 arg order (flags after run command) 2026-07-07 15:12:58 +01:00
5aca6025c5 chore: remove committed binary, add to gitignore 2026-07-07 15:05:59 +01:00
5f168d21f1 feat: replace autocert with lego CLI for reliable LE cert issuance 2026-07-07 15:05:41 +01:00
02bab22a5d fix: build tool from script directory (correct go.mod context) 2026-07-07 14:57:30 +01:00
533b6f4137 fix: full service teardown (stop+disable+remove) + port 80 wait 2026-07-07 14:53:00 +01:00
434b23aee2 fix: tool detects LE vs self-signed, retries LE for fallback certs 2026-07-07 14:45:45 +01:00
63099703a1 fix: stop Zoraxy before running tool to free port 80 for ACME 2026-07-07 14:44:13 +01:00
360112c7b7 fix: remove old zoraxy container before deploying 2026-07-07 14:33:28 +01:00
2e7a0e3645 refactor: /opt/backup/ vault for .env + certs, clean destroy flow 2026-07-07 14:30:48 +01:00
b0a3cd3206 fix: tool only manages backup, preservation handles cert deployment 2026-07-07 14:14:36 +01:00
25c0e7b9b7 docs: remove --destroy blocker — tool handles cert backup/restore 2026-07-07 12:57:58 +01:00
2398bdce83 feat: nextwks-tool for LE + DB + Zoraxy Auth 2026-07-07 12:53:57 +01:00
d73b67614e fix: proxy www through binary for ACME challenge support 2026-07-07 12:22:11 +01:00
038d923a11 fix: include www in LE cert auto-generation loop 2026-07-07 12:19:17 +01:00
4e50bf15dd fix: www files go to html/ subdirectory (Zoraxy static root) 2026-07-07 12:13:06 +01:00
ddef538795 feat: www landing page with component listing 2026-07-07 12:09:25 +01:00
7b3942658d docs: add 5s safety warning before --destroy 2026-07-07 12:06:12 +01:00
79bf23618d fix: proper Zoraxy CSRF/session handling (login.html + cookie jar) 2026-07-07 12:02:59 +01:00
d0f9854084 fix: bypass global TLS for dns subdomain (HTTP without redirect) 2026-07-07 11:04:12 +01:00
ccd362a964 fix: hoist COOKIE_JAR outside function to avoid unbound variable 2026-07-07 10:56:17 +01:00
acd9671359 fix: preserve Zoraxy certs across greenfield destroy 2026-07-07 10:52:16 +01:00
eabbcdaa2d fix: request LE certs for all subdomains (app + dns) on greenfield 2026-07-07 10:47:38 +01:00
22471e479b fix: unlock immutable files before destroy, timeout LE cert request 2026-07-07 10:34:36 +01:00
29 changed files with 3025 additions and 743 deletions

7
.gitignore vendored
View file

@ -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

View file

@ -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
View 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)

View file

@ -1 +1 @@
0.1.0
0.1.0.0048

75
compose/stack.yaml Normal file
View 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

View file

@ -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

View 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

View 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
View 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
}

View 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"]

View file

@ -0,0 +1,7 @@
server:
port: 9000
host: "0.0.0.0"
app:
name: "NextWorkspace"
description: "Your Self-Hosted Workspace"

View 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
View 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>

View file

@ -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}
}

View file

@ -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
View file

@ -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
View file

@ -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
View file

@ -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=

View file

@ -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
View 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
View 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
View 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
View 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"

2105
main.go

File diff suppressed because it is too large Load diff

51
tools/firewall-routing.sh Executable file
View 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

Binary file not shown.

View 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
View 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
View 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