feat(caddy): auto-import Caddyfile on first enable; seed default admin user
When Caddy is enabled for the first time (caddy routes table empty), importCaddyfileRoutes() reads /etc/caddy/Caddyfile and seeds all hostname/upstream blocks as custom routes — no manual entry needed after deploy. On first startup with an empty users table, a default admin user is created (admin@ghostgrid.local / admin) so the system is immediately usable.
This commit is contained in:
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
| Version | Date | Changes |
|
| Version | Date | Changes |
|
||||||
|---------|------|---------|
|
|---------|------|---------|
|
||||||
|
| 1.3 | Jun 8, 2026 | Auto-import Caddyfile on first Caddy enable; default admin user on first start |
|
||||||
| 1.2 | Jun 8, 2026 | Removed `caddy_prod_domain` / `caddy_dev_domain` settings; Caddy now routes only custom entries |
|
| 1.2 | Jun 8, 2026 | Removed `caddy_prod_domain` / `caddy_dev_domain` settings; Caddy now routes only custom entries |
|
||||||
| 1.1 | Jun 8, 2026 | Dropped the migration layer (fresh-install schema); renamed the `caddy_routes` table to `caddy` |
|
| 1.1 | Jun 8, 2026 | Dropped the migration layer (fresh-install schema); renamed the `caddy_routes` table to `caddy` |
|
||||||
| 1.0 | Jun 8, 2026 | Initial architecture reference generated from the codebase |
|
| 1.0 | Jun 8, 2026 | Initial architecture reference generated from the codebase |
|
||||||
@ -474,6 +475,10 @@ buildCaddyfile():
|
|||||||
{ local_certs } # global block
|
{ local_certs } # global block
|
||||||
per custom route { [encode] [tls internal] reverse_proxy <upstream> }
|
per custom route { [encode] [tls internal] reverse_proxy <upstream> }
|
||||||
|
|
||||||
|
importCaddyfileRoutes(): reads /etc/caddy/Caddyfile on first Caddy enable
|
||||||
|
parses hostname/upstream blocks → seeds caddy table as custom routes
|
||||||
|
(no-op if caddy table already has entries or file not found)
|
||||||
|
|
||||||
pushCaddyConfig(): POST <caddy_admin_url>/load (Content-Type: text/caddyfile)
|
pushCaddyConfig(): POST <caddy_admin_url>/load (Content-Type: text/caddyfile)
|
||||||
called on startup, after settings save, after route add/delete
|
called on startup, after settings save, after route add/delete
|
||||||
(failures logged as warnings, non-fatal; skipped if caddy_enabled !== 'true')
|
(failures logged as warnings, non-fatal; skipped if caddy_enabled !== 'true')
|
||||||
@ -481,6 +486,24 @@ pushCaddyConfig(): POST <caddy_admin_url>/load (Content-Type: text/caddyfile)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 6.4 First-start Initialization
|
||||||
|
|
||||||
|
Runs in `startServer()` before any routes are registered, every startup — both operations
|
||||||
|
are idempotent and only fire once on a blank database.
|
||||||
|
|
||||||
|
```
|
||||||
|
Default admin user:
|
||||||
|
if users table is empty:
|
||||||
|
INSERT user (name='Admin', role='Admin', email='admin@ghostgrid.local', password=bcrypt('admin'))
|
||||||
|
→ log "[Init] Default admin user created"
|
||||||
|
|
||||||
|
Default settings:
|
||||||
|
INSERT OR IGNORE all DEFAULT_SETTINGS keys from server-db.ts
|
||||||
|
→ existing values in the settings table are never overwritten
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 7. Frontend Architecture
|
## 7. Frontend Architecture
|
||||||
|
|
||||||
### 7.1 Application Structure
|
### 7.1 Application Structure
|
||||||
@ -560,7 +583,8 @@ Settings
|
|||||||
+-- Microsoft Entra ID (OAuth SSO, redirect-URI helper, allowed group)
|
+-- Microsoft Entra ID (OAuth SSO, redirect-URI helper, allowed group)
|
||||||
+-- CheckMK (API URL/user/secret, sync interval, "Run sync now")
|
+-- CheckMK (API URL/user/secret, sync interval, "Run sync now")
|
||||||
+-- Ansible Semaphore (API URL/token/project, "Test connection")
|
+-- Ansible Semaphore (API URL/token/project, "Test connection")
|
||||||
+-- Caddy (admin URL, custom route management)
|
+-- Caddy (admin URL, custom route management;
|
||||||
|
auto-seeded from /etc/caddy/Caddyfile on first enable)
|
||||||
+-- Secret inputs use the __SET__ sentinel (blank = keep existing)
|
+-- Secret inputs use the __SET__ sentinel (blank = keep existing)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
43
server.ts
43
server.ts
@ -89,6 +89,37 @@ function buildCaddyfile(): string {
|
|||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function importCaddyfileRoutes(): void {
|
||||||
|
if (getCaddyRoutes().length > 0) return;
|
||||||
|
const caddyfilePath = '/etc/caddy/Caddyfile';
|
||||||
|
if (!fs.existsSync(caddyfilePath)) return;
|
||||||
|
const lines = fs.readFileSync(caddyfilePath, 'utf-8').split('\n');
|
||||||
|
let i = 0;
|
||||||
|
while (i < lines.length) {
|
||||||
|
const line = lines[i].trim();
|
||||||
|
const headerMatch = line.match(/^(\S+)\s*\{$/);
|
||||||
|
if (headerMatch && headerMatch[1] !== '{') {
|
||||||
|
const hostname = headerMatch[1];
|
||||||
|
const blockLines: string[] = [];
|
||||||
|
i++;
|
||||||
|
while (i < lines.length && lines[i].trim() !== '}') {
|
||||||
|
blockLines.push(lines[i]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
const block = blockLines.join('\n');
|
||||||
|
const upstreamMatch = block.match(/reverse_proxy\s+(\S+)/);
|
||||||
|
if (upstreamMatch) {
|
||||||
|
const upstream = upstreamMatch[1];
|
||||||
|
const tls = /tls\s+internal/.test(block);
|
||||||
|
const compress = /encode/.test(block);
|
||||||
|
addCaddyRoute(hostname, upstream, tls, compress);
|
||||||
|
console.log(`[Caddy] Imported route from Caddyfile: ${hostname} → ${upstream}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function pushCaddyConfig(): Promise<void> {
|
async function pushCaddyConfig(): Promise<void> {
|
||||||
if (getSetting('caddy_enabled') !== 'true') return;
|
if (getSetting('caddy_enabled') !== 'true') return;
|
||||||
const adminUrl = getSetting('caddy_admin_url') || 'http://localhost:2019';
|
const adminUrl = getSetting('caddy_admin_url') || 'http://localhost:2019';
|
||||||
@ -108,6 +139,14 @@ async function startServer() {
|
|||||||
const app = express();
|
const app = express();
|
||||||
const PORT = Number(process.env.PORT) || 3000;
|
const PORT = Number(process.env.PORT) || 3000;
|
||||||
|
|
||||||
|
const { cnt } = db.prepare('SELECT COUNT(*) as cnt FROM users').get() as { cnt: number };
|
||||||
|
if (cnt === 0) {
|
||||||
|
const passwordHash = bcrypt.hashSync('admin', 10);
|
||||||
|
db.prepare('INSERT INTO users (id, name, role, email, password_hash) VALUES (?, ?, ?, ?, ?)')
|
||||||
|
.run(uid('u'), 'Admin', 'Admin', 'admin@ghostgrid.local', passwordHash);
|
||||||
|
console.log('[Init] Default admin user created — login: admin@ghostgrid.local / admin');
|
||||||
|
}
|
||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
@ -285,11 +324,15 @@ async function startServer() {
|
|||||||
'semaphore_enabled', 'semaphore_api_url', 'semaphore_api_token', 'semaphore_project_id',
|
'semaphore_enabled', 'semaphore_api_url', 'semaphore_api_token', 'semaphore_project_id',
|
||||||
'caddy_enabled', 'caddy_admin_url'];
|
'caddy_enabled', 'caddy_admin_url'];
|
||||||
const updates = req.body as Record<string, string>;
|
const updates = req.body as Record<string, string>;
|
||||||
|
const caddyWasEnabled = getSetting('caddy_enabled') === 'true';
|
||||||
for (const key of allowed) {
|
for (const key of allowed) {
|
||||||
if (key in updates && updates[key] !== '__SET__') {
|
if (key in updates && updates[key] !== '__SET__') {
|
||||||
setSetting(key, String(updates[key]));
|
setSetting(key, String(updates[key]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!caddyWasEnabled && updates.caddy_enabled === 'true') {
|
||||||
|
importCaddyfileRoutes();
|
||||||
|
}
|
||||||
pushCaddyConfig().catch(err => console.warn('[Caddy] Could not push config after settings save:', err.message));
|
pushCaddyConfig().catch(err => console.warn('[Caddy] Could not push config after settings save:', err.message));
|
||||||
res.json(maskSettings(getAllSettings()));
|
res.json(maskSettings(getAllSettings()));
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|||||||
Reference in New Issue
Block a user