diff --git a/authentik/blueprints/kaboot-setup-production.yaml.example b/authentik/blueprints/kaboot-setup-production.yaml.example index 517206c..561bf5b 100644 --- a/authentik/blueprints/kaboot-setup-production.yaml.example +++ b/authentik/blueprints/kaboot-setup-production.yaml.example @@ -74,115 +74,6 @@ entries: evaluate_on_plan: true re_evaluate_policies: false - # ═══════════════════════════════════════════════════════════════════════════════ - # BRANDING - # ═══════════════════════════════════════════════════════════════════════════════ - - - id: kaboot-brand - model: authentik_brands.brand - identifiers: - domain: !Context auth_domain - attrs: - domain: !Context auth_domain - default: false - branding_title: Kaboot - branding_logo: /media/branding/logo.svg - branding_favicon: /media/branding/logo.svg - flow_authentication: !Find [authentik_flows.flow, [slug, default-authentication-flow]] - flow_invalidation: !KeyOf kaboot-invalidation-flow - flow_user_settings: !Find [authentik_flows.flow, [slug, default-user-settings-flow]] - default_application: !Find [authentik_core.application, [slug, kaboot]] - attributes: - settings: - theme: - base: light - css: | - :root { - --ak-accent: #2563eb; - --ak-primary: #2563eb; - --ak-primary-dark: #1e40af; - --ak-primary-light: #60a5fa; - --ak-error: #ef4444; - --ak-success: #22c55e; - --pf-global--FontFamily--sans-serif: "Inter", system-ui, -apple-system, sans-serif; - } - body { - background-color: #2563eb !important; - } - .pf-c-login__main { - background-color: #ffffff !important; - border-radius: 2rem !important; - box-shadow: 0 10px 0 rgba(0,0,0,0.1) !important; - padding: 3rem !important; - max-width: 500px !important; - margin: 0 auto; - } - .pf-c-form-control { - border: 2px solid #e5e7eb !important; - border-radius: 1rem !important; - padding: 0.75rem 1rem !important; - font-weight: 600 !important; - color: #333 !important; - font-size: 1rem !important; - box-shadow: none !important; - transition: all 0.2s ease !important; - } - .pf-c-form-control:focus { - border-color: #2563eb !important; - outline: none !important; - } - .pf-c-button.pf-m-primary { - background-color: #333333 !important; - color: #ffffff !important; - border: none !important; - border-radius: 1rem !important; - padding: 0.75rem 1.5rem !important; - font-weight: 800 !important; - font-size: 1.1rem !important; - box-shadow: 0 6px 0 #000000 !important; - transform: translateY(0) !important; - transition: all 0.1s ease !important; - text-transform: uppercase !important; - letter-spacing: 0.05em !important; - } - .pf-c-button.pf-m-primary:hover { - background-color: #1a1a1a !important; - } - .pf-c-button.pf-m-primary:active { - transform: translateY(6px) !important; - box-shadow: none !important; - } - .pf-c-button.pf-m-secondary, .pf-c-button.pf-m-link { - color: #2563eb !important; - font-weight: 600 !important; - } - .pf-c-title { - font-weight: 900 !important; - color: #111827 !important; - text-align: center !important; - margin-bottom: 1.5rem !important; - } - .pf-c-brand { - filter: drop-shadow(0 4px 6px rgba(0,0,0,0.1)); - } - .pf-c-alert { - border-radius: 1rem !important; - border: 2px solid transparent !important; - box-shadow: 0 4px 0 rgba(0,0,0,0.05) !important; - } - .pf-c-alert.pf-m-danger { - background-color: #fef2f2 !important; - border-color: #fca5a5 !important; - color: #991b1b !important; - } - .pf-c-login__main-footer-links-item-link { - color: #6b7280 !important; - font-weight: 500 !important; - } - .pf-c-background-image { - display: none !important; - } - # ═══════════════════════════════════════════════════════════════════════════════ # FLOW BACKGROUNDS # ═══════════════════════════════════════════════════════════════════════════════ @@ -299,6 +190,115 @@ entries: negate: false timeout: 30 + # ═══════════════════════════════════════════════════════════════════════════════ + # BRANDING (must be after application is created) + # ═══════════════════════════════════════════════════════════════════════════════ + + - id: kaboot-brand + model: authentik_brands.brand + identifiers: + domain: !Context auth_domain + attrs: + domain: !Context auth_domain + default: false + branding_title: Kaboot + branding_logo: /media/branding/logo.svg + branding_favicon: /media/branding/logo.svg + flow_authentication: !Find [authentik_flows.flow, [slug, default-authentication-flow]] + flow_invalidation: !KeyOf kaboot-invalidation-flow + flow_user_settings: !Find [authentik_flows.flow, [slug, default-user-settings-flow]] + default_application: !KeyOf kaboot-application + attributes: + settings: + theme: + base: light + css: | + :root { + --ak-accent: #2563eb; + --ak-primary: #2563eb; + --ak-primary-dark: #1e40af; + --ak-primary-light: #60a5fa; + --ak-error: #ef4444; + --ak-success: #22c55e; + --pf-global--FontFamily--sans-serif: "Inter", system-ui, -apple-system, sans-serif; + } + body { + background-color: #2563eb !important; + } + .pf-c-login__main { + background-color: #ffffff !important; + border-radius: 2rem !important; + box-shadow: 0 10px 0 rgba(0,0,0,0.1) !important; + padding: 3rem !important; + max-width: 500px !important; + margin: 0 auto; + } + .pf-c-form-control { + border: 2px solid #e5e7eb !important; + border-radius: 1rem !important; + padding: 0.75rem 1rem !important; + font-weight: 600 !important; + color: #333 !important; + font-size: 1rem !important; + box-shadow: none !important; + transition: all 0.2s ease !important; + } + .pf-c-form-control:focus { + border-color: #2563eb !important; + outline: none !important; + } + .pf-c-button.pf-m-primary { + background-color: #333333 !important; + color: #ffffff !important; + border: none !important; + border-radius: 1rem !important; + padding: 0.75rem 1.5rem !important; + font-weight: 800 !important; + font-size: 1.1rem !important; + box-shadow: 0 6px 0 #000000 !important; + transform: translateY(0) !important; + transition: all 0.1s ease !important; + text-transform: uppercase !important; + letter-spacing: 0.05em !important; + } + .pf-c-button.pf-m-primary:hover { + background-color: #1a1a1a !important; + } + .pf-c-button.pf-m-primary:active { + transform: translateY(6px) !important; + box-shadow: none !important; + } + .pf-c-button.pf-m-secondary, .pf-c-button.pf-m-link { + color: #2563eb !important; + font-weight: 600 !important; + } + .pf-c-title { + font-weight: 900 !important; + color: #111827 !important; + text-align: center !important; + margin-bottom: 1.5rem !important; + } + .pf-c-brand { + filter: drop-shadow(0 4px 6px rgba(0,0,0,0.1)); + } + .pf-c-alert { + border-radius: 1rem !important; + border: 2px solid transparent !important; + box-shadow: 0 4px 0 rgba(0,0,0,0.05) !important; + } + .pf-c-alert.pf-m-danger { + background-color: #fef2f2 !important; + border-color: #fca5a5 !important; + color: #991b1b !important; + } + .pf-c-login__main-footer-links-item-link { + color: #6b7280 !important; + font-weight: 500 !important; + } + .pf-c-background-image { + display: none !important; + } + # ═══════════════════════════════════════════════════════════════════════════════ # PASSWORD POLICY # ═══════════════════════════════════════════════════════════════════════════════ diff --git a/authentik/blueprints/kaboot-setup.yaml b/authentik/blueprints/kaboot-setup.yaml index 6dc4cd9..91e6ef8 100644 --- a/authentik/blueprints/kaboot-setup.yaml +++ b/authentik/blueprints/kaboot-setup.yaml @@ -26,15 +26,109 @@ metadata: entries: # ═══════════════════════════════════════════════════════════════════════════════ - # BRANDING + # GROUPS + # ═══════════════════════════════════════════════════════════════════════════════ + + - id: kaboot-users-group + model: authentik_core.group + identifiers: + name: kaboot-users + attrs: + name: kaboot-users + + - id: kaboot-ai-access-group + model: authentik_core.group + identifiers: + name: kaboot-ai-access + attrs: + name: kaboot-ai-access + + # ═══════════════════════════════════════════════════════════════════════════════ + # GROUPS SCOPE MAPPING + # ═══════════════════════════════════════════════════════════════════════════════ + + - id: groups-scope-mapping + model: authentik_providers_oauth2.scopemapping + identifiers: + managed: goauthentik.io/providers/oauth2/scope-kaboot-groups + attrs: + name: "Kaboot Groups Scope" + scope_name: groups + description: "Include user groups in the token" + expression: | + return [group.name for group in user.ak_groups.all()] + + # ═══════════════════════════════════════════════════════════════════════════════ + # OAUTH2/OIDC PROVIDER + # ═══════════════════════════════════════════════════════════════════════════════ + + - id: kaboot-oauth2-provider + model: authentik_providers_oauth2.oauth2provider + identifiers: + name: Kaboot OAuth2 + attrs: + name: Kaboot OAuth2 + authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Internal JWT Certificate]] + client_type: public + client_id: kaboot-spa + redirect_uris: + - url: http://localhost:5173/callback + matching_mode: strict + - url: http://localhost:5173/silent-renew.html + matching_mode: strict + - url: http://localhost:5173 + matching_mode: strict + access_code_validity: minutes=1 + access_token_validity: minutes=30 + refresh_token_validity: days=30 + sub_mode: hashed_user_id + include_claims_in_id_token: true + issuer_mode: per_provider + property_mappings: + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, offline_access]] + - !KeyOf groups-scope-mapping + + # ═══════════════════════════════════════════════════════════════════════════════ + # APPLICATION + # ═══════════════════════════════════════════════════════════════════════════════ + + - id: kaboot-application + model: authentik_core.application + identifiers: + slug: kaboot + attrs: + name: Kaboot + slug: kaboot + provider: !KeyOf kaboot-oauth2-provider + policy_engine_mode: any + meta_launch_url: http://localhost:5173 + + - id: kaboot-group-policy-binding + model: authentik_policies.policybinding + identifiers: + target: !KeyOf kaboot-application + group: !KeyOf kaboot-users-group + attrs: + order: 0 + enabled: true + negate: false + timeout: 30 + + # ═══════════════════════════════════════════════════════════════════════════════ + # BRANDING (must be after application is created) # ═══════════════════════════════════════════════════════════════════════════════ - id: kaboot-brand model: authentik_brands.brand identifiers: - domain: localhost + domain: authentik-default attrs: - domain: localhost + domain: authentik-default default: true branding_title: Kaboot branding_logo: /media/branding/logo.svg @@ -42,7 +136,7 @@ entries: flow_authentication: !Find [authentik_flows.flow, [slug, default-authentication-flow]] flow_invalidation: !Find [authentik_flows.flow, [slug, default-invalidation-flow]] flow_user_settings: !Find [authentik_flows.flow, [slug, default-user-settings-flow]] - default_application: !Find [authentik_core.application, [slug, kaboot]] + default_application: !KeyOf kaboot-application attributes: settings: theme: @@ -186,100 +280,6 @@ entries: attrs: background: /media/branding/background.svg - # ═══════════════════════════════════════════════════════════════════════════════ - # GROUPS - # ═══════════════════════════════════════════════════════════════════════════════ - - - id: kaboot-users-group - model: authentik_core.group - identifiers: - name: kaboot-users - attrs: - name: kaboot-users - - - id: kaboot-ai-access-group - model: authentik_core.group - identifiers: - name: kaboot-ai-access - attrs: - name: kaboot-ai-access - - # ═══════════════════════════════════════════════════════════════════════════════ - # GROUPS SCOPE MAPPING - # ═══════════════════════════════════════════════════════════════════════════════ - - - id: groups-scope-mapping - model: authentik_providers_oauth2.scopemapping - identifiers: - managed: goauthentik.io/providers/oauth2/scope-kaboot-groups - attrs: - name: "Kaboot Groups Scope" - scope_name: groups - description: "Include user groups in the token" - expression: | - return [group.name for group in user.ak_groups.all()] - - # ═══════════════════════════════════════════════════════════════════════════════ - # OAUTH2/OIDC PROVIDER - # ═══════════════════════════════════════════════════════════════════════════════ - - - id: kaboot-oauth2-provider - model: authentik_providers_oauth2.oauth2provider - identifiers: - name: Kaboot OAuth2 - attrs: - name: Kaboot OAuth2 - authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] - invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] - signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Internal JWT Certificate]] - client_type: public - client_id: kaboot-spa - redirect_uris: - - url: http://localhost:5173/callback - matching_mode: strict - - url: http://localhost:5173/silent-renew.html - matching_mode: strict - - url: http://localhost:5173 - matching_mode: strict - access_code_validity: minutes=1 - access_token_validity: minutes=30 - refresh_token_validity: days=30 - sub_mode: hashed_user_id - include_claims_in_id_token: true - issuer_mode: per_provider - property_mappings: - - !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]] - - !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]] - - !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]] - - !Find [authentik_providers_oauth2.scopemapping, [scope_name, offline_access]] - - !KeyOf groups-scope-mapping - - # ═══════════════════════════════════════════════════════════════════════════════ - # APPLICATION - # ═══════════════════════════════════════════════════════════════════════════════ - - - id: kaboot-application - model: authentik_core.application - identifiers: - slug: kaboot - attrs: - name: Kaboot - slug: kaboot - provider: !KeyOf kaboot-oauth2-provider - policy_engine_mode: any - meta_launch_url: http://localhost:5173 - - - id: kaboot-group-policy-binding - model: authentik_policies.policybinding - identifiers: - target: !KeyOf kaboot-application - group: !KeyOf kaboot-users-group - attrs: - order: 0 - enabled: true - negate: false - timeout: 30 - # ═══════════════════════════════════════════════════════════════════════════════ # PASSWORD POLICY # ═══════════════════════════════════════════════════════════════════════════════ diff --git a/docker-compose.yml b/docker-compose.yml index ad49eee..59a2dee 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,8 +11,8 @@ services: test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"] start_period: 20s interval: 30s - retries: 5 timeout: 5s + retries: 5 volumes: - postgresql-data:/var/lib/postgresql/data environment: @@ -31,15 +31,15 @@ services: test: ["CMD-SHELL", "redis-cli ping | grep PONG"] start_period: 20s interval: 30s - retries: 5 timeout: 3s + retries: 5 volumes: - redis-data:/data networks: - kaboot-network authentik-server: - image: ghcr.io/goauthentik/server:2025.2 + image: ghcr.io/goauthentik/server:2025.10.3 container_name: kaboot-authentik-server restart: unless-stopped command: server @@ -69,7 +69,7 @@ services: - kaboot-network authentik-worker: - image: ghcr.io/goauthentik/server:2025.2 + image: ghcr.io/goauthentik/server:2025.10.3 container_name: kaboot-authentik-worker restart: unless-stopped command: worker @@ -80,12 +80,15 @@ services: AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik} AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS} AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY} + AUTHENTIK_BOOTSTRAP_PASSWORD: ${AUTHENTIK_BOOTSTRAP_PASSWORD:-} + AUTHENTIK_BOOTSTRAP_TOKEN: ${AUTHENTIK_BOOTSTRAP_TOKEN:-} user: root volumes: - /var/run/docker.sock:/var/run/docker.sock - ./authentik/media:/media - ./authentik/certs:/certs - ./authentik/custom-templates:/templates + - ./authentik/blueprints:/blueprints/custom depends_on: postgresql: condition: service_healthy @@ -113,6 +116,8 @@ services: OIDC_JWKS_URI: http://${KABOOT_HOST:-localhost}:${AUTHENTIK_PORT_HTTP:-9000}/application/o/kaboot/jwks/ OIDC_INTERNAL_JWKS_URI: http://authentik-server:9000/application/o/kaboot/jwks/ CORS_ORIGIN: http://localhost:${KABOOT_FRONTEND_PORT:-5173},http://${KABOOT_HOST:-localhost}:${KABOOT_FRONTEND_PORT:-5173} + LOG_REQUESTS: ${LOG_REQUESTS:-true} + GEMINI_API_KEY: ${GEMINI_API_KEY:-} volumes: - ./data:/data ports: diff --git a/hooks/useGame.ts b/hooks/useGame.ts index 36d29fa..f6c23c0 100644 --- a/hooks/useGame.ts +++ b/hooks/useGame.ts @@ -854,6 +854,7 @@ export const useGame = () => { if (p.id !== playerId) return p; return { ...p, score: newScore, previousScore: p.score, streak: newStreak, lastAnswerCorrect: isCorrect, selectedShape, pointsBreakdown: breakdown }; }); + playersRef.current = updatedPlayers; setPlayers(updatedPlayers); conn.send({ type: 'RESULT', payload: { isCorrect, scoreAdded: breakdown.total, newScore, breakdown } }); @@ -1379,6 +1380,7 @@ export const useGame = () => { if (p.id !== 'host') return p; return { ...p, score: newScore, previousScore: p.score, streak: newStreak, lastAnswerCorrect: isCorrect, selectedShape: option.shape, pointsBreakdown: breakdown }; }); + playersRef.current = updatedPlayers; setPlayers(updatedPlayers); const allAnswered = updatedPlayers.every(p => p.lastAnswerCorrect !== null); diff --git a/scripts/setup.sh b/scripts/setup.sh index 1fdca31..ca3eebd 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -5,8 +5,8 @@ ENV_FILE=".env" ENV_EXAMPLE=".env.example" ENV_LOCAL=".env.local" -echo "Kaboot Setup Script" -echo "===================" +echo "Kaboot Development Setup" +echo "========================" echo "" if [ -f "$ENV_FILE" ]; then @@ -39,14 +39,22 @@ echo "" read -p "Enter host IP/domain for network access [$DETECTED_IP]: " KABOOT_HOST KABOOT_HOST=${KABOOT_HOST:-$DETECTED_IP} +echo "" +echo "System AI Configuration (Optional)" +echo "-----------------------------------" +echo "You can provide a Gemini API key to enable AI quiz generation" +echo "for all users without requiring them to set up their own key." +echo "" +read -p "Enter Gemini API key (or press Enter to skip): " GEMINI_API_KEY + echo "" echo "Generating secrets..." -PG_PASS=$(openssl rand -base64 36 | tr -d '\n') -AUTHENTIK_SECRET_KEY=$(openssl rand -base64 60 | tr -d '\n') -AUTHENTIK_BOOTSTRAP_PASSWORD=$(openssl rand -base64 24 | tr -d '\n') -AUTHENTIK_BOOTSTRAP_TOKEN=$(openssl rand -base64 36 | tr -d '\n') -ENCRYPTION_KEY=$(openssl rand -base64 36 | tr -d '\n') +PG_PASS=$(openssl rand -base64 36 | tr -d '\n' | tr -d '/') +AUTHENTIK_SECRET_KEY=$(openssl rand -base64 60 | tr -d '\n' | tr -d '/') +AUTHENTIK_BOOTSTRAP_PASSWORD=$(openssl rand -base64 24 | tr -d '\n' | tr -d '/') +AUTHENTIK_BOOTSTRAP_TOKEN=$(openssl rand -hex 32) +ENCRYPTION_KEY=$(openssl rand -base64 36 | tr -d '\n' | tr -d '/') cp "$ENV_EXAMPLE" "$ENV_FILE" @@ -57,6 +65,8 @@ if [[ "$OSTYPE" == "darwin"* ]]; then sed -i '' "s|^AUTHENTIK_BOOTSTRAP_TOKEN=.*|AUTHENTIK_BOOTSTRAP_TOKEN=${AUTHENTIK_BOOTSTRAP_TOKEN}|" "$ENV_FILE" sed -i '' "s|^ENCRYPTION_KEY=.*|ENCRYPTION_KEY=${ENCRYPTION_KEY}|" "$ENV_FILE" sed -i '' "s|^KABOOT_HOST=.*|KABOOT_HOST=${KABOOT_HOST}|" "$ENV_FILE" + sed -i '' "s|^LOG_REQUESTS=.*|LOG_REQUESTS=true|" "$ENV_FILE" + sed -i '' "s|^GEMINI_API_KEY=.*|GEMINI_API_KEY=${GEMINI_API_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" @@ -64,32 +74,51 @@ else sed -i "s|^AUTHENTIK_BOOTSTRAP_TOKEN=.*|AUTHENTIK_BOOTSTRAP_TOKEN=${AUTHENTIK_BOOTSTRAP_TOKEN}|" "$ENV_FILE" sed -i "s|^ENCRYPTION_KEY=.*|ENCRYPTION_KEY=${ENCRYPTION_KEY}|" "$ENV_FILE" sed -i "s|^KABOOT_HOST=.*|KABOOT_HOST=${KABOOT_HOST}|" "$ENV_FILE" + sed -i "s|^LOG_REQUESTS=.*|LOG_REQUESTS=true|" "$ENV_FILE" + sed -i "s|^GEMINI_API_KEY=.*|GEMINI_API_KEY=${GEMINI_API_KEY}|" "$ENV_FILE" fi # Create .env.local for Vite frontend cat > "$ENV_LOCAL" << EOF # Auto-generated by setup.sh - Frontend environment variables +VITE_API_URL=http://${KABOOT_HOST}:3001 VITE_BACKEND_URL=http://${KABOOT_HOST}:3001 VITE_AUTHENTIK_URL=http://${KABOOT_HOST}:9000 +VITE_OIDC_CLIENT_ID=kaboot-spa +VITE_OIDC_APP_SLUG=kaboot EOF echo "" -echo "Created .env and .env.local files." +echo "Setup Complete!" +echo "===============" echo "" -echo "Host configuration:" +echo "Configuration Summary" +echo "---------------------" echo " KABOOT_HOST: ${KABOOT_HOST}" echo " Frontend: http://${KABOOT_HOST}:5173" echo " Backend: http://${KABOOT_HOST}:3001" echo " Authentik: http://${KABOOT_HOST}:9000" +if [ -n "$GEMINI_API_KEY" ]; then + echo " System AI: Enabled" +else + echo " System AI: Disabled (users need own API key)" +fi echo "" -echo "Authentik admin credentials (save these):" +echo "Authentik Admin Credentials (SAVE THESE)" +echo "-----------------------------------------" echo " Username: akadmin" echo " Password: ${AUTHENTIK_BOOTSTRAP_PASSWORD}" echo "" -echo "Next steps:" +echo "Files Created" +echo "-------------" +echo " .env - Docker Compose environment variables" +echo " .env.local - Vite frontend environment variables" +echo "" +echo "Next Steps" +echo "----------" echo " 1. Run: docker compose up -d" echo " 2. Wait for Authentik to start (~30 seconds)" -echo " 3. For development: npm run dev -- --host" +echo " 3. For development: npm install && npm run dev -- --host" echo " 4. Access from other devices via http://${KABOOT_HOST}:5173" echo "" echo "Note: Update Authentik redirect URIs to include http://${KABOOT_HOST}:5173/callback"