From 9a3fc97a3422101ebd850f68059f402ff6f948fc Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Tue, 13 Jan 2026 14:14:30 -0700 Subject: [PATCH] Phase 1 done --- .env.example | 34 ++++++ .gitignore | 15 +++ IMPLEMENTATION_PLAN.md | 66 ++++++------ authentik/certs/.gitkeep | 0 authentik/custom-templates/.gitkeep | 0 authentik/media/.gitkeep | 0 docker-compose.yml | 129 ++++++++++++++++++++++ docs/AUTHENTIK_SETUP.md | 159 ++++++++++++++++++++++++++++ features.md | 4 + scripts/setup.sh | 48 +++++++++ server/Dockerfile | 15 +++ server/package.json | 28 +++++ server/src/index.ts | 16 +++ server/tsconfig.json | 15 +++ 14 files changed, 496 insertions(+), 33 deletions(-) create mode 100644 .env.example create mode 100644 authentik/certs/.gitkeep create mode 100644 authentik/custom-templates/.gitkeep create mode 100644 authentik/media/.gitkeep create mode 100644 docker-compose.yml create mode 100644 docs/AUTHENTIK_SETUP.md create mode 100644 features.md create mode 100755 scripts/setup.sh create mode 100644 server/Dockerfile create mode 100644 server/package.json create mode 100644 server/src/index.ts create mode 100644 server/tsconfig.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5357fdf --- /dev/null +++ b/.env.example @@ -0,0 +1,34 @@ +# ============================================================================== +# REQUIRED - Generate with: openssl rand -base64 36 | tr -d '\n' +# ============================================================================== +PG_PASS= +AUTHENTIK_SECRET_KEY= + +# ============================================================================== +# OPTIONAL - Authentik Database +# ============================================================================== +PG_USER=authentik +PG_DB=authentik + +# ============================================================================== +# OPTIONAL - Ports +# ============================================================================== +AUTHENTIK_PORT_HTTP=9000 +AUTHENTIK_PORT_HTTPS=9443 +KABOOT_BACKEND_PORT=3001 + +# ============================================================================== +# OPTIONAL - Authentik Settings +# ============================================================================== +AUTHENTIK_ERROR_REPORTING=false + +# ============================================================================== +# OPTIONAL - OIDC (Override if using custom domain) +# ============================================================================== +OIDC_ISSUER=http://localhost:9000/application/o/kaboot/ +OIDC_JWKS_URI=http://localhost:9000/application/o/kaboot/jwks/ + +# ============================================================================== +# OPTIONAL - CORS (Frontend origin for backend API) +# ============================================================================== +CORS_ORIGIN=http://localhost:5173 diff --git a/.gitignore b/.gitignore index a547bf3..b88f839 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,18 @@ dist-ssr *.njsproj *.sln *.sw? + +# Environment secrets +.env +.env.local +.env.*.local + +# Authentik volumes (keep structure, ignore data) +authentik/media/* +!authentik/media/.gitkeep +authentik/certs/* +!authentik/certs/.gitkeep + +# Backend data +server/data/ +*.db diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index db75bb2..5e78081 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -31,40 +31,40 @@ Add user accounts via Authentik (OIDC) and persist quizzes to SQLite database. U ## Phase 1: Infrastructure Setup ### 1.1 Docker Compose Configuration -- [ ] Create `docker-compose.yml` with all services: - - [ ] PostgreSQL (Authentik database) - - [ ] Redis (Authentik cache) - - [ ] Authentik server - - [ ] Authentik worker - - [ ] Kaboot backend service -- [ ] Create `.env.example` with required variables: - - [ ] `PG_PASS` - PostgreSQL password - - [ ] `AUTHENTIK_SECRET_KEY` - Authentik secret - - [ ] `PG_USER`, `PG_DB` - Optional customization -- [ ] Create setup script to generate secrets (`scripts/setup.sh`) -- [ ] Add `authentik/` directory structure for volumes: - - [ ] `authentik/media/` - - [ ] `authentik/certs/` - - [ ] `authentik/custom-templates/` -- [ ] Update `.gitignore` for new files: - - [ ] `.env` - - [ ] `authentik/media/*` - - [ ] `authentik/certs/*` - - [ ] `server/data/` +- [x] Create `docker-compose.yml` with all services: + - [x] PostgreSQL (Authentik database) + - [x] Redis (Authentik cache) + - [x] Authentik server + - [x] Authentik worker + - [x] Kaboot backend service +- [x] Create `.env.example` with required variables: + - [x] `PG_PASS` - PostgreSQL password + - [x] `AUTHENTIK_SECRET_KEY` - Authentik secret + - [x] `PG_USER`, `PG_DB` - Optional customization +- [x] Create setup script to generate secrets (`scripts/setup.sh`) +- [x] Add `authentik/` directory structure for volumes: + - [x] `authentik/media/` + - [x] `authentik/certs/` + - [x] `authentik/custom-templates/` +- [x] Update `.gitignore` for new files: + - [x] `.env` + - [x] `authentik/media/*` + - [x] `authentik/certs/*` + - [x] `server/data/` ### 1.2 Authentik Configuration Documentation -- [ ] Document initial setup steps in `docs/AUTHENTIK_SETUP.md`: - - [ ] Navigate to `http://localhost:9000/if/flow/initial-setup/` - - [ ] Create admin account - - [ ] Create OAuth2/OIDC Application + Provider: - - [ ] Application name: `Kaboot` - - [ ] Application slug: `kaboot` - - [ ] Provider type: `OAuth2/OIDC` - - [ ] Client type: `Public` (SPA with PKCE) - - [ ] Client ID: `kaboot-spa` - - [ ] Redirect URIs: `http://localhost:5173/callback`, `http://localhost:5173/silent-renew.html` - - [ ] Scopes: `openid`, `profile`, `email`, `offline_access` - - [ ] Note down OIDC endpoints (issuer, JWKS URI, etc.) +- [x] Document initial setup steps in `docs/AUTHENTIK_SETUP.md`: + - [x] Navigate to `http://localhost:9000/if/flow/initial-setup/` + - [x] Create admin account + - [x] Create OAuth2/OIDC Application + Provider: + - [x] Application name: `Kaboot` + - [x] Application slug: `kaboot` + - [x] Provider type: `OAuth2/OIDC` + - [x] Client type: `Public` (SPA with PKCE) + - [x] Client ID: `kaboot-spa` + - [x] Redirect URIs: `http://localhost:5173/callback`, `http://localhost:5173/silent-renew.html` + - [x] Scopes: `openid`, `profile`, `email`, `offline_access` + - [x] Note down OIDC endpoints (issuer, JWKS URI, etc.) --- @@ -400,7 +400,7 @@ kaboot/ | Phase | Status | Notes | |-------|--------|-------| -| Phase 1 | Not Started | | +| Phase 1 | **COMPLETE** | Docker Compose, .env, setup script, Authentik docs | | Phase 2 | Not Started | | | Phase 3 | Not Started | | | Phase 4 | Not Started | | diff --git a/authentik/certs/.gitkeep b/authentik/certs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/authentik/custom-templates/.gitkeep b/authentik/custom-templates/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/authentik/media/.gitkeep b/authentik/media/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4007394 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,129 @@ +services: + # ═══════════════════════════════════════════════════════════════════════════ + # AUTHENTIK - Identity Provider + # ═══════════════════════════════════════════════════════════════════════════ + + postgresql: + image: docker.io/library/postgres:16-alpine + container_name: kaboot-postgresql + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"] + start_period: 20s + interval: 30s + retries: 5 + timeout: 5s + volumes: + - postgresql-data:/var/lib/postgresql/data + environment: + POSTGRES_PASSWORD: ${PG_PASS:?database password required} + POSTGRES_USER: ${PG_USER:-authentik} + POSTGRES_DB: ${PG_DB:-authentik} + networks: + - kaboot-network + + redis: + image: docker.io/library/redis:alpine + container_name: kaboot-redis + command: --save 60 1 --loglevel warning + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "redis-cli ping | grep PONG"] + start_period: 20s + interval: 30s + retries: 5 + timeout: 3s + volumes: + - redis-data:/data + networks: + - kaboot-network + + authentik-server: + image: ghcr.io/goauthentik/server:2025.2 + container_name: kaboot-authentik-server + restart: unless-stopped + command: server + environment: + AUTHENTIK_REDIS__HOST: redis + AUTHENTIK_POSTGRESQL__HOST: postgresql + AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik} + AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik} + AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS} + AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?authentik secret key required} + AUTHENTIK_ERROR_REPORTING__ENABLED: ${AUTHENTIK_ERROR_REPORTING:-false} + volumes: + - ./authentik/media:/media + - ./authentik/custom-templates:/templates + ports: + - "${AUTHENTIK_PORT_HTTP:-9000}:9000" + - "${AUTHENTIK_PORT_HTTPS:-9443}:9443" + depends_on: + postgresql: + condition: service_healthy + redis: + condition: service_healthy + networks: + - kaboot-network + + authentik-worker: + image: ghcr.io/goauthentik/server:2025.2 + container_name: kaboot-authentik-worker + restart: unless-stopped + command: worker + environment: + AUTHENTIK_REDIS__HOST: redis + AUTHENTIK_POSTGRESQL__HOST: postgresql + AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik} + AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik} + AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS} + AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY} + user: root + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./authentik/media:/media + - ./authentik/certs:/certs + - ./authentik/custom-templates:/templates + depends_on: + postgresql: + condition: service_healthy + redis: + condition: service_healthy + networks: + - kaboot-network + + # ═══════════════════════════════════════════════════════════════════════════ + # KABOOT - Application Backend + # ═══════════════════════════════════════════════════════════════════════════ + + kaboot-backend: + build: + context: ./server + dockerfile: Dockerfile + container_name: kaboot-backend + restart: unless-stopped + environment: + NODE_ENV: production + PORT: 3001 + DATABASE_PATH: /data/kaboot.db + # OIDC Configuration - points to Authentik + OIDC_ISSUER: ${OIDC_ISSUER:-http://authentik-server:9000/application/o/kaboot/} + OIDC_JWKS_URI: ${OIDC_JWKS_URI:-http://authentik-server:9000/application/o/kaboot/jwks/} + # CORS - allow frontend origin + CORS_ORIGIN: ${CORS_ORIGIN:-http://localhost:5173} + volumes: + - kaboot-data:/data + ports: + - "${KABOOT_BACKEND_PORT:-3001}:3001" + depends_on: + - authentik-server + networks: + - kaboot-network + +volumes: + postgresql-data: + redis-data: + kaboot-data: + +networks: + kaboot-network: + driver: bridge diff --git a/docs/AUTHENTIK_SETUP.md b/docs/AUTHENTIK_SETUP.md new file mode 100644 index 0000000..60a6694 --- /dev/null +++ b/docs/AUTHENTIK_SETUP.md @@ -0,0 +1,159 @@ +# Authentik Setup Guide for Kaboot + +This guide walks through configuring Authentik as the OAuth2/OIDC identity provider for Kaboot. + +## Prerequisites + +- Docker and Docker Compose installed +- Kaboot stack running (`docker compose up -d`) +- Access to `http://localhost:9000` + +## Step 1: Initial Authentik Setup + +1. Navigate to `http://localhost:9000/if/flow/initial-setup/` + - **Important**: Include the trailing slash `/` + +2. Create the admin account: + - Email: Your email address + - Password: Choose a strong password + +3. Log in with the credentials you just created + +## Step 2: Create the Kaboot Application + +1. In the Authentik admin interface, go to **Applications** > **Applications** + +2. Click **Create with provider** + +3. **Application Settings**: + | Field | Value | + |-------|-------| + | Name | `Kaboot` | + | Slug | `kaboot` | + | Launch URL | `http://localhost:5173` | + +4. Click **Next** + +## Step 3: Configure OAuth2/OIDC Provider + +1. Select **OAuth2/OIDC** as the Provider Type + +2. Click **Next** + +3. **Provider Configuration**: + | Field | Value | + |-------|-------| + | Name | `Kaboot OAuth2` | + | Authorization flow | `default-provider-authorization-implicit-consent` | + | Client type | `Public` | + | Client ID | `kaboot-spa` | + +4. **Redirect URIs** (one per line): + ``` + http://localhost:5173/callback + http://localhost:5173/silent-renew.html + http://localhost:5173 + ``` + +5. **Advanced Settings**: + | Field | Value | + |-------|-------| + | Subject mode | `Based on the User's hashed ID` | + | Include claims in id_token | `Yes` | + | Issuer mode | `Each provider has a different issuer` | + +6. **Scopes** - Ensure these are selected: + - `openid` + - `profile` + - `email` + - `offline_access` (for refresh tokens) + +7. Click **Submit** + +## Step 4: Verify OIDC Endpoints + +After creation, go to **Applications** > **Providers** > **Kaboot OAuth2** + +Note these endpoints (you'll need them for frontend configuration): + +| Endpoint | URL | +|----------|-----| +| Issuer | `http://localhost:9000/application/o/kaboot/` | +| Authorization | `http://localhost:9000/application/o/authorize/` | +| Token | `http://localhost:9000/application/o/token/` | +| UserInfo | `http://localhost:9000/application/o/userinfo/` | +| JWKS | `http://localhost:9000/application/o/kaboot/jwks/` | + +## Step 5: Test the Configuration + +1. Open the OpenID Configuration URL in your browser: + ``` + http://localhost:9000/application/o/kaboot/.well-known/openid-configuration + ``` + +2. You should see a JSON response with all OIDC endpoints + +## Step 6: Create a Test User (Optional) + +1. Go to **Directory** > **Users** + +2. Click **Create** + +3. Fill in user details: + - Username: `testuser` + - Name: `Test User` + - Email: `test@example.com` + +4. After creation, click on the user and go to the **Credentials** tab + +5. Click **Set password** to create a password + +## Environment Variables + +Ensure your `.env` file has the correct OIDC configuration: + +```bash +OIDC_ISSUER=http://localhost:9000/application/o/kaboot/ +OIDC_JWKS_URI=http://localhost:9000/application/o/kaboot/jwks/ +``` + +For the frontend OIDC config (`src/config/oidc.ts`): + +```typescript +export const oidcConfig = { + authority: 'http://localhost:9000/application/o/kaboot/', + client_id: 'kaboot-spa', + redirect_uri: `${window.location.origin}/callback`, + // ... rest of config +}; +``` + +## Troubleshooting + +### "Invalid redirect URI" error +- Ensure all redirect URIs are added exactly as configured in the provider +- Check for trailing slashes - they must match exactly + +### "Client not found" error +- Verify the Client ID matches `kaboot-spa` +- Ensure the application is enabled (not archived) + +### CORS errors +- Authentik handles CORS automatically for configured redirect URIs +- Ensure your frontend origin (`http://localhost:5173`) is in the redirect URIs + +### Token validation fails on backend +- Verify `OIDC_ISSUER` and `OIDC_JWKS_URI` are correct +- The backend must be able to reach Authentik at `http://authentik-server:9000` (Docker network) + +## Production Notes + +For production deployment: + +1. Use HTTPS everywhere +2. Update all URLs from `localhost` to your domain +3. Update redirect URIs in Authentik +4. Update frontend OIDC config with production URLs +5. Update `.env` with production OIDC endpoints +6. Consider enabling Authentik error reporting +7. Configure email settings in Authentik for password recovery diff --git a/features.md b/features.md new file mode 100644 index 0000000..fa248b3 --- /dev/null +++ b/features.md @@ -0,0 +1,4 @@ +- [ ] All data stored in sqlite db. +- [ ] AI generated content based on document upload +- [ ] Moderation (kick player, lock game, filter names) +- [ ] Persistent game urls while game is active diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100755 index 0000000..6331971 --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,48 @@ +#!/bin/bash +set -e + +ENV_FILE=".env" +ENV_EXAMPLE=".env.example" + +echo "Kaboot Setup Script" +echo "===================" +echo "" + +if [ -f "$ENV_FILE" ]; then + read -p ".env file already exists. Overwrite? (y/N): " -n 1 -r + echo "" + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Aborting. Existing .env file preserved." + exit 0 + fi +fi + +if [ ! -f "$ENV_EXAMPLE" ]; then + echo "Error: .env.example not found. Run this script from the project root." + exit 1 +fi + +echo "Generating secrets..." + +PG_PASS=$(openssl rand -base64 36 | tr -d '\n') +AUTHENTIK_SECRET_KEY=$(openssl rand -base64 60 | tr -d '\n') + +cp "$ENV_EXAMPLE" "$ENV_FILE" + +if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i '' "s|^PG_PASS=.*|PG_PASS=${PG_PASS}|" "$ENV_FILE" + sed -i '' "s|^AUTHENTIK_SECRET_KEY=.*|AUTHENTIK_SECRET_KEY=${AUTHENTIK_SECRET_KEY}|" "$ENV_FILE" +else + sed -i "s|^PG_PASS=.*|PG_PASS=${PG_PASS}|" "$ENV_FILE" + sed -i "s|^AUTHENTIK_SECRET_KEY=.*|AUTHENTIK_SECRET_KEY=${AUTHENTIK_SECRET_KEY}|" "$ENV_FILE" +fi + +echo "" +echo "Created .env file with generated secrets." +echo "" +echo "Next steps:" +echo " 1. Review .env and adjust settings if needed" +echo " 2. Run: docker compose up -d" +echo " 3. Open: http://localhost:9000/if/flow/initial-setup/" +echo " 4. Follow docs/AUTHENTIK_SETUP.md to configure the OAuth2 provider" +echo "" diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..b714965 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,15 @@ +FROM node:22-alpine + +WORKDIR /app + +RUN apk add --no-cache python3 make g++ + +COPY package*.json ./ +RUN npm install + +COPY . . +RUN npm run build + +EXPOSE 3001 + +CMD ["npm", "start"] diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..40e2412 --- /dev/null +++ b/server/package.json @@ -0,0 +1,28 @@ +{ + "name": "kaboot-backend", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js" + }, + "dependencies": { + "better-sqlite3": "^11.7.0", + "cors": "^2.8.5", + "express": "^4.21.2", + "jsonwebtoken": "^9.0.2", + "jwks-rsa": "^3.1.0", + "uuid": "^11.0.5" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.12", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/jsonwebtoken": "^9.0.7", + "@types/node": "^22.10.7", + "@types/uuid": "^10.0.0", + "tsx": "^4.19.2", + "typescript": "^5.7.3" + } +} diff --git a/server/src/index.ts b/server/src/index.ts new file mode 100644 index 0000000..f8f34b8 --- /dev/null +++ b/server/src/index.ts @@ -0,0 +1,16 @@ +import express from 'express'; +import cors from 'cors'; + +const app = express(); +const PORT = process.env.PORT || 3001; + +app.use(cors({ origin: process.env.CORS_ORIGIN || 'http://localhost:5173' })); +app.use(express.json()); + +app.get('/health', (_req, res) => { + res.json({ status: 'ok', timestamp: new Date().toISOString() }); +}); + +app.listen(PORT, () => { + console.log(`Kaboot backend running on port ${PORT}`); +}); diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 0000000..bbd4ab1 --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "node", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "outDir": "dist", + "rootDir": "src", + "declaration": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}