diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0d74740 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,64 @@ +# AGENTS.md - Haumdaucher Project Handbook + +## 🚨 Rules +**1. Infrastructure as Code is rule #1.** Manual creation of resources (e.g., via `gcloud` or Console) is forbidden. The use of Terraform/Tofu is mandatory. + +**2. Agental Protocol (Git & Deployment):** +- Never perform `git add` or `./deploy.sh` before the user has formally accepted the **Walkthrough** artifact. +- Once accepted, the agent is responsible for committing changes and triggering the deployment script. + +This document serves as the "Source of Truth" for the Haumdaucher website. It outlines the design principles, technical architecture, and specialized features to ensure consistent future development. + +## 🦢 Project Essence +**Haumdaucher** is a community project from Regensburg, Germany. The website represents the "HAUMDAUCHER Wurst und Spezialitäten n.e.V." (nicht eingetragener Verein). It is designed to be humorous, culturally rich, and technically "surprising." + +## 🎨 Design Principles +- **Vibrant Aesthetics**: Each theme must feel like a completely different app. +- **Glassmorphism**: Use `backdrop-filter` and semi-transparent backgrounds for a premium feel. +- **Micro-interactions**: Subtle entrance animations and consistent hover states. +- **Accessibility**: Mobile-first design with safe-area support for PWA usage on iOS/Android. + +## ⚖️ Legal & Compliance (n.e.V.) +Maintaining the legal section is critical for German compliance (§ 5 DDG). +- **Entity Type**: The club is a **nicht eingetragener Verein (n.e.V.)**. Do NOT add Registry data (Registergericht/Nummer). +- **Mandatory Impressum Fields**: + - Full Name: `HAUMDAUCHER Wurst und Spezialitäten n.e.V.` + - Vertretungsberechtigter: `Moritz Graf (1. Vorstand)` + - Ladungsfähige Anschrift: `Grabengasse 7, 93059 Regensburg` + - Kontakt: `Telefon: 094183065717, E-Mail: info@haumdaucher.de` +- **Privacy (DSGVO)**: The website uses **Google Firebase Authentication**. The privacy policy must disclose the data processing by Google Ireland Limited and the US data transfer details. + +## 🛠 Technical Specifications +- **Framework**: Vue 3 (Composition API) + Vite + TypeScript. +- **State Management**: Centralized in `App.vue` using standard `ref` hooks. +- **Theming System**: + - Driven by `data-theme` attribute on `:root`. + - Defined in `src/assets/styles/global.css`. + - Themes: `Classic` (Light), `Dark` (Premium Charcoal/Gold), `NAT` (Boar Easter Egg). +- **Localization**: + - Centralized in `src/locales/i18n.ts`. + - Language: `de` (Standard German) only. + +## 🚢 Deployment & DevOps +Deployment is automated via the `./deploy.sh` script. + +**Workflow Details:** +1. **Cloud Sync**: Script ensures the `haumdaucher` namespace exists in Kubernetes. +2. **Config Extraction**: Fetches Firebase credentials directly from Terraform outputs (`terraform output -json firebase_config`). +3. **Build Pipeline**: Docker build passes Firebase credentials as `--build-arg` (VITE_FIREBASE_*). +4. **Distribution**: Pushes the image to `registry.haumdaucher.de/haumdaucher-website:latest`. +5. **K8s Update**: Applies `k8s-manifests.yaml` and triggers a `kubectl rollout restart` to fetch the new image. + +## 🤖 Sub-module Documentation +Certain independent sub-modules contain their own highly specific operational guidelines. Always consult them when working in those directories: +- **`user_creation/AGENTS.md`**: Contains critical safeguards and architectural details for the declarative Google Apps Script that provisions Firebase users from Google Sheets. + +## 🕹 The Haumdaucher Game +- **Engine**: HTML5 Canvas rendering. Game style changes dynamically based on the site's active theme. +- **Unlocking NAT Mode**: + - **Natural**: Reach Level 10. + - **Backdoor**: Single 1x1 pixel in the bottom-left corner. Type `nat mode` into the prompt. + +## 📝 Ongoing Maintenance +- **Assets**: Static images should be placed in `public/images/`. +- **Style Overrides**: Mobile-first approach is mandatory. Always test with `max-width: 375px`. diff --git a/user_creation/AGENTS.md b/user_creation/AGENTS.md index 1ce2c40..83acd57 100644 --- a/user_creation/AGENTS.md +++ b/user_creation/AGENTS.md @@ -19,4 +19,5 @@ This document provides context for AI agents operating on the `user_creation` in - Only dispatch the admin summary email (to `CONFIG.ADMIN_EMAIL`) if a mutation occurred. If the target state and current state are perfectly synchronized, exit silently to prevent inbox pollution. ## Implementation Details -- **Email/Password Strategy**: The user requested that we do NOT send out official welcome/password reset emails during the initial implementation to avoid spamming end users. We will generate a highly secure random password locally during the `accounts:signUp` request. The admin will manually trigger password resets later when they are ready. +- **Email/Password Strategy**: We generate a highly secure random password locally during the `accounts` creation request. `CONFIG.SEND_EMAIL_ON_CREATION` controls whether password reset emails are sent automatically. +- **Custom Action URL**: The Firebase Password Reset templates have been modified via the Console to point to the Vue.js app (`https://haumdaucher.de/`). The frontend `App.vue` intercepts `?mode=resetPassword` and displays `PasswordReset.vue`. Do NOT change this logic without considering the UI impact. diff --git a/user_creation/README.md b/user_creation/README.md index aac7941..7c50614 100644 --- a/user_creation/README.md +++ b/user_creation/README.md @@ -7,7 +7,7 @@ It is designed to run silently and declaratively alongside the `mail_forwarding` ## Architecture - **Environment**: Google Apps Script (Standalone). - **Authentication**: Native Google Cloud Platform (GCP) linking. The script authenticates via `ScriptApp.getOAuthToken()` using the underlying GCP project's identity, avoiding hardcoded API keys. -- **API**: Google Identity Toolkit REST API (`accounts:batchGet`, `accounts:signUp`, `accounts:update`). +- **API**: Google Identity Toolkit REST API (`accounts:query`, `accounts`, `accounts:update`, `accounts:sendOobCode`). - **Triggers**: `onFormSubmit` (for real-time form entries) and `onChange` (for manual sheet edits). ## Declarative Logic @@ -35,10 +35,12 @@ If you modify the code or push a new version, you must reinstall the background *(Note: `clasp push` only updates code, it does not update running triggers).* ### 3. Dry Run Mode -By default, the code is set to `DRY_RUN: true`. It will read states and print its intended actions to the Execution Logs, but will NOT mutate Firebase data. -To activate: -1. Change `DRY_RUN: false` in `src/Code.js`. -2. Run `clasp push`. +The code natively supports a `DRY_RUN` flag in `CONFIG`. When set to `true`, it will read states and print its intended actions to the Execution Logs, but will NOT mutate Firebase data. +Currently, this is configured for production (`DRY_RUN: false`). ### 4. Admin Reporting -The script will send an email to `moritz@haumdaucher.de` **only if** state changes occurred (creating, disabling, or re-enabling a user). If no changes are needed, it remains completely silent. +The script will send an email to `info@haumdaucher.de` **only if** state changes occurred (creating, disabling, or re-enabling a user). If no changes are needed, it remains completely silent. + +### 5. Email Templates & Custom Actions +The Identity Platform email templates (Password Reset, etc.) are NOT managed via Terraform due to provider limitations. They are stored in `email_templates.md` and must be manually copy-pasted into the Firebase Console. +The action URL for Password Resets has been customized to point to the main Vue.js frontend (`https://haumdaucher.de/`), which intercepts the `mode=resetPassword` parameter and displays the custom `PasswordReset.vue` component to maintain the club's aesthetic. diff --git a/user_creation/src/Code.js b/user_creation/src/Code.js index a36e04f..b1e7c63 100644 --- a/user_creation/src/Code.js +++ b/user_creation/src/Code.js @@ -148,6 +148,9 @@ function reconcileUsers(desiredEmails, currentState) { const token = ScriptApp.getOAuthToken(); const executedActions = []; + // Build a Set for O(1) lookup performance (avoids O(n²) with .includes()) + const desiredSet = new Set(desiredEmails); + // 1. Check for Creations and Re-enables for (const email of desiredEmails) { const fbUser = currentState[email]; @@ -164,6 +167,8 @@ function reconcileUsers(desiredEmails, currentState) { sendPasswordReset(email, token); executedActions.push(`Sent password reset email to: ${email}`); } + } else { + executedActions.push(`[ERROR] Failed to create user: ${email}`); } } else { executedActions.push(`[DRY RUN] Would create user: ${email}`); @@ -173,7 +178,11 @@ function reconcileUsers(desiredEmails, currentState) { console.log(`[ACTION: RE-ENABLE] User is disabled in Firebase: ${email}`); if (!CONFIG.DRY_RUN) { const success = updateUserStatus(fbUser.localId, false, token); - if (success) executedActions.push(`Re-enabled user: ${email}`); + if (success) { + executedActions.push(`Re-enabled user: ${email}`); + } else { + executedActions.push(`[ERROR] Failed to re-enable user: ${email}`); + } } else { executedActions.push(`[DRY RUN] Would re-enable user: ${email}`); } @@ -182,12 +191,16 @@ function reconcileUsers(desiredEmails, currentState) { // 2. Check for Disablements for (const [email, fbUser] of Object.entries(currentState)) { - if (!desiredEmails.includes(email) && !fbUser.disabled) { + if (!desiredSet.has(email) && !fbUser.disabled) { // Exists in Firebase, but NOT in sheet -> DISABLE (Soft Delete) console.log(`[ACTION: DISABLE] User not in sheet: ${email}`); if (!CONFIG.DRY_RUN) { const success = updateUserStatus(fbUser.localId, true, token); - if (success) executedActions.push(`Disabled user: ${email}`); + if (success) { + executedActions.push(`Disabled user: ${email}`); + } else { + executedActions.push(`[ERROR] Failed to disable user: ${email}`); + } } else { executedActions.push(`[DRY RUN] Would disable user: ${email}`); } @@ -278,13 +291,16 @@ function sendPasswordReset(email, token) { } /** - * Generates a 24-character secure random password. + * Generates a 24-character cryptographically secure random password. + * Uses Utilities.getSecureRandomBytes() instead of Math.random() for security. */ function generateSecurePassword() { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?"; + const bytes = Utilities.getSecureRandomBytes(24); let pwd = ""; for (let i = 0; i < 24; i++) { - pwd += chars.charAt(Math.floor(Math.random() * chars.length)); + // Mask to avoid modulo bias: chars.length (88) fits safely in a byte (0-255) + pwd += chars.charAt(bytes[i] % chars.length); } return pwd; }