docs: refactor GEMINI.md -> AGENTS.md, update user_creation docs and fix code quality issues
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user