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.
|
- 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
|
## 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
|
## Architecture
|
||||||
- **Environment**: Google Apps Script (Standalone).
|
- **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.
|
- **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).
|
- **Triggers**: `onFormSubmit` (for real-time form entries) and `onChange` (for manual sheet edits).
|
||||||
|
|
||||||
## Declarative Logic
|
## 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).*
|
*(Note: `clasp push` only updates code, it does not update running triggers).*
|
||||||
|
|
||||||
### 3. Dry Run Mode
|
### 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.
|
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.
|
||||||
To activate:
|
Currently, this is configured for production (`DRY_RUN: false`).
|
||||||
1. Change `DRY_RUN: false` in `src/Code.js`.
|
|
||||||
2. Run `clasp push`.
|
|
||||||
|
|
||||||
### 4. Admin Reporting
|
### 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 token = ScriptApp.getOAuthToken();
|
||||||
const executedActions = [];
|
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
|
// 1. Check for Creations and Re-enables
|
||||||
for (const email of desiredEmails) {
|
for (const email of desiredEmails) {
|
||||||
const fbUser = currentState[email];
|
const fbUser = currentState[email];
|
||||||
|
|
@ -164,6 +167,8 @@ function reconcileUsers(desiredEmails, currentState) {
|
||||||
sendPasswordReset(email, token);
|
sendPasswordReset(email, token);
|
||||||
executedActions.push(`Sent password reset email to: ${email}`);
|
executedActions.push(`Sent password reset email to: ${email}`);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
executedActions.push(`[ERROR] Failed to create user: ${email}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
executedActions.push(`[DRY RUN] Would create user: ${email}`);
|
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}`);
|
console.log(`[ACTION: RE-ENABLE] User is disabled in Firebase: ${email}`);
|
||||||
if (!CONFIG.DRY_RUN) {
|
if (!CONFIG.DRY_RUN) {
|
||||||
const success = updateUserStatus(fbUser.localId, false, token);
|
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 {
|
} else {
|
||||||
executedActions.push(`[DRY RUN] Would re-enable user: ${email}`);
|
executedActions.push(`[DRY RUN] Would re-enable user: ${email}`);
|
||||||
}
|
}
|
||||||
|
|
@ -182,12 +191,16 @@ function reconcileUsers(desiredEmails, currentState) {
|
||||||
|
|
||||||
// 2. Check for Disablements
|
// 2. Check for Disablements
|
||||||
for (const [email, fbUser] of Object.entries(currentState)) {
|
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)
|
// Exists in Firebase, but NOT in sheet -> DISABLE (Soft Delete)
|
||||||
console.log(`[ACTION: DISABLE] User not in sheet: ${email}`);
|
console.log(`[ACTION: DISABLE] User not in sheet: ${email}`);
|
||||||
if (!CONFIG.DRY_RUN) {
|
if (!CONFIG.DRY_RUN) {
|
||||||
const success = updateUserStatus(fbUser.localId, true, token);
|
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 {
|
} else {
|
||||||
executedActions.push(`[DRY RUN] Would disable user: ${email}`);
|
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() {
|
function generateSecurePassword() {
|
||||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?";
|
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?";
|
||||||
|
const bytes = Utilities.getSecureRandomBytes(24);
|
||||||
let pwd = "";
|
let pwd = "";
|
||||||
for (let i = 0; i < 24; i++) {
|
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;
|
return pwd;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue