a control panel for my server
at main 309 lines 9.4 kB view raw view rendered
1# Control Panel Spec 2 3A simple admin dashboard for managing Caddy toggles, protected by Indiko OAuth. 4 5## Overview 6 7- **Domain**: `control.dunkirk.sh` 8- **Auth**: Indiko OAuth 2.0 with PKCE 9- **Runtime**: Bun + Hono 10- **Purpose**: Toggle feature flags that control Caddy behavior via marker files 11 12## Architecture 13 14``` 15┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ 16│ control.d.sh │────▶│ Indiko OAuth │────▶│ Passkey Auth │ 17└────────┬────────┘ └─────────────────┘ └─────────────────┘ 18 19 20┌─────────────────┐ ┌─────────────────┐ 21│ Toggle API │────▶│ Marker Files │ 22│ POST /flags/* │ │ /var/lib/... │ 23└─────────────────┘ └─────────────────┘ 24 25 26┌─────────────────┐ 27│ Caddy checks │ 28│ file existence │ 29└─────────────────┘ 30``` 31 32## Authentication Flow 33 341. User visits `control.dunkirk.sh` 352. If no session, redirect to Indiko OAuth authorize endpoint 363. User authenticates with passkey via Indiko 374. Indiko redirects back with auth code 385. Control exchanges code for access token 396. Store session in cookie (signed, httpOnly) 407. Optionally: check user role for admin access 41 42### OAuth Configuration 43 44``` 45Client ID: https://control.dunkirk.sh/ 46Redirect URI: https://control.dunkirk.sh/auth/callback 47Scopes: profile email 48Auth endpoint: https://indiko.dunkirk.sh/auth/authorize 49Token endpoint: https://indiko.dunkirk.sh/auth/token 50``` 51 52For role-based access control, pre-register the client with Indiko admin to get a client secret and define allowed roles (e.g., `admin`). 53 54## Environment Variables 55 56```bash 57# OAuth 58INDIKO_URL=https://indiko.dunkirk.sh 59CLIENT_ID=https://control.dunkirk.sh/ 60CLIENT_SECRET=<from-indiko-admin> # if pre-registered 61REDIRECT_URI=https://control.dunkirk.sh/auth/callback 62 63# Session 64SESSION_SECRET=<random-32-bytes> 65 66# Flags directory (where marker files live) 67FLAGS_DIR=/var/lib/caddy/flags 68 69# Optional: restrict to specific role 70REQUIRED_ROLE=admin 71``` 72 73## API Endpoints 74 75### Auth 76 77| Endpoint | Method | Description | 78|----------|--------|-------------| 79| `/` | GET | Dashboard UI (requires auth) | 80| `/auth/login` | GET | Redirect to Indiko OAuth | 81| `/auth/callback` | GET | OAuth callback, exchange code for token | 82| `/auth/logout` | POST | Clear session | 83 84### Flags 85 86| Endpoint | Method | Description | 87|----------|--------|-------------| 88| `/api/flags` | GET | List all flags and their status | 89| `/api/flags/:name` | GET | Get single flag status | 90| `/api/flags/:name` | PUT | Set flag (body: `{ enabled: true/false }`) | 91| `/api/flags/:name` | DELETE | Remove flag (same as enabled: false) | 92 93## Flag Definitions 94 95Flags are defined in a config file or hardcoded. Each flag maps to a marker file path. 96 97```typescript 98const FLAGS = { 99 "block-map-sse": { 100 name: "Block Map SSE", 101 description: "Disable Server-Sent Events on map.dunkirk.sh", 102 file: "block-map-sse", 103 service: "map.dunkirk.sh" 104 }, 105 // Add more flags as needed 106} as const; 107``` 108 109When enabled, the app touches the file: `${FLAGS_DIR}/${flag.file}` 110When disabled, the app removes the file. 111 112## UI 113 114Simple, minimal dashboard matching your style (Space Grotesk, dark theme). 115 116### Dashboard View 117 118``` 119┌──────────────────────────────────────────────────────────┐ 120│ CONTROL PANEL [user] logout │ 121├──────────────────────────────────────────────────────────┤ 122│ │ 123│ map.dunkirk.sh │ 124│ ┌────────────────────────────────────────────────────┐ │ 125│ │ Block SSE Endpoint [ OFF ] │ │ 126│ │ Disable /sse Server-Sent Events │ │ 127│ └────────────────────────────────────────────────────┘ │ 128│ │ 129│ (more services/flags can be added here) │ 130│ │ 131└──────────────────────────────────────────────────────────┘ 132``` 133 134Toggle switches use a simple on/off design. Changes take effect immediately (Caddy checks file existence on each request). 135 136## File Structure 137 138``` 139control/ 140├── src/ 141│ ├── index.ts # Hono app entrypoint 142│ ├── auth.ts # OAuth flow + session management 143│ ├── flags.ts # Flag toggle logic 144│ ├── middleware.ts # Auth middleware, role check 145│ └── ui.ts # HTML templates 146├── package.json 147├── tsconfig.json 148└── README.md 149``` 150 151## Implementation Notes 152 153### PKCE Flow 154 155```typescript 156import { createHash, randomBytes } from "crypto"; 157 158function generatePKCE() { 159 const verifier = randomBytes(32).toString("base64url"); 160 const challenge = createHash("sha256") 161 .update(verifier) 162 .digest("base64url"); 163 return { verifier, challenge }; 164} 165``` 166 167### Session Cookie 168 169Use signed cookies with `hono/cookie`: 170 171```typescript 172import { setCookie, getCookie } from "hono/cookie"; 173import { sign, verify } from "hono/jwt"; 174 175// After successful OAuth: 176const session = await sign({ 177 sub: user.me, 178 name: user.profile.name, 179 role: user.role, 180 exp: Math.floor(Date.now() / 1000) + 86400 * 7 // 7 days 181}, SESSION_SECRET); 182 183setCookie(c, "session", session, { 184 httpOnly: true, 185 secure: true, 186 sameSite: "Lax", 187 path: "/", 188 maxAge: 86400 * 7 189}); 190``` 191 192### Flag Toggle 193 194```typescript 195import { exists, unlink } from "fs/promises"; 196import { join } from "path"; 197 198const FLAGS_DIR = process.env.FLAGS_DIR || "/var/lib/caddy/flags"; 199 200async function setFlag(name: string, enabled: boolean) { 201 const path = join(FLAGS_DIR, name); 202 if (enabled) { 203 await Bun.write(path, ""); // touch file 204 } else { 205 if (await exists(path)) { 206 await unlink(path); 207 } 208 } 209} 210 211async function getFlag(name: string): Promise<boolean> { 212 return exists(join(FLAGS_DIR, name)); 213} 214``` 215 216## Caddy Integration 217 218### map.dunkirk.sh config update 219 220```nix 221virtualHosts."map.dunkirk.sh" = { 222 extraConfig = '' 223 tls { 224 dns cloudflare {env.CLOUDFLARE_API_TOKEN} 225 } 226 227 # Check for SSE block flag 228 @sse_blocked { 229 path /sse 230 file /var/lib/caddy/flags/block-map-sse 231 } 232 respond @sse_blocked "SSE temporarily disabled" 503 233 234 header { 235 Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" 236 } 237 reverse_proxy localhost:8084 { 238 header_up X-Forwarded-Proto {scheme} 239 header_up X-Forwarded-For {remote} 240 } 241 ''; 242}; 243``` 244 245### Flags directory setup 246 247```nix 248systemd.tmpfiles.rules = [ 249 "d /var/lib/caddy/flags 0755 caddy caddy -" 250]; 251``` 252 253### Control service needs write access 254 255The control service user needs to write to the flags directory: 256 257```nix 258users.users.control.extraGroups = [ "caddy" ]; 259# Or use ACLs / tmpfiles with proper permissions 260``` 261 262## NixOS Service Module 263 264```nix 265# modules/nixos/services/control.nix 266let 267 mkService = import ../../lib/mkService.nix; 268in 269mkService { 270 name = "control"; 271 description = "Control Panel - Admin dashboard for Caddy toggles"; 272 defaultPort = 3010; 273 runtime = "bun"; 274 entryPoint = "src/index.ts"; 275 276 extraConfig = cfg: { 277 atelier.services.control.environment = { 278 INDIKO_URL = "https://indiko.dunkirk.sh"; 279 CLIENT_ID = "https://${cfg.domain}/"; 280 REDIRECT_URI = "https://${cfg.domain}/auth/callback"; 281 FLAGS_DIR = "/var/lib/caddy/flags"; 282 }; 283 284 # Ensure flags directory exists with correct permissions 285 systemd.tmpfiles.rules = [ 286 "d /var/lib/caddy/flags 0775 caddy caddy -" 287 ]; 288 289 # Give control user access to flags directory 290 users.users.control.extraGroups = [ "caddy" ]; 291 }; 292} 293``` 294 295## Security Considerations 296 2971. **OAuth PKCE**: Always use PKCE, never store tokens in localStorage 2982. **Session cookies**: httpOnly, secure, sameSite=Lax 2993. **Role checking**: If `REQUIRED_ROLE` is set, verify user has that role 3004. **CSRF**: Use sameSite cookies, optionally add CSRF tokens for mutations 3015. **Flag validation**: Only allow toggling pre-defined flags, never arbitrary file paths 302 303## Future Extensions 304 305- Add more flag types (rate limit thresholds, maintenance mode) 306- Service status dashboard (systemd status via D-Bus) 307- Caddy config viewer (read-only) 308- Audit log of flag changes 309- Webhook notifications on toggle