a control panel for my server
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