docs: refactor GEMINI.md -> AGENTS.md, update user_creation docs and fix code quality issues
This commit is contained in:
parent
08c7ef4660
commit
a16ebd641e
|
|
@ -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`.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue