diff --git a/README.md b/README.md index da85fd4..1f72bff 100644 --- a/README.md +++ b/README.md @@ -73,10 +73,10 @@ Todas usan la contraseña **`demo1234`**. | Dueño | `owner@agendamax.demo` | `demo1234` | Daniela Reyes | | Empleado | `valentina.cruz@lumiere.mx` | `demo1234` | Valentina Cruz | | Empleado | `mateo.herrera@lumiere.mx` | `demo1234` | Mateo Herrera | -| Empleado | `sofia.ramirez@lumiere.mx` | `demo1234` | Sofía Ramírez | +| Empleado | `sofía.ramírez@lumiere.mx` | `demo1234` | Sofía Ramírez | | Empleado | `diego.castillo@lumiere.mx` | `demo1234` | Diego Castillo | | Empleado | `isabela.torres@lumiere.mx` | `demo1234` | Isabela Torres | -| Empleado | `carolina.mendez@lumiere.mx` | `demo1234` | Carolina Méndez | +| Empleado | `carolina.méndez@lumiere.mx` | `demo1234` | Carolina Méndez | > En la pantalla de login aparecen botones de acceso rápido a las cuentas demo, > y desde la barra lateral puedes cambiar de cuenta con **“Ver como…”**. diff --git a/docs/superpowers/plans/2026-07-27-demo-email-domain.md b/docs/superpowers/plans/2026-07-27-demo-email-domain.md index 1e86e1f..11e53f5 100644 --- a/docs/superpowers/plans/2026-07-27-demo-email-domain.md +++ b/docs/superpowers/plans/2026-07-27-demo-email-domain.md @@ -4,14 +4,14 @@ **Goal:** Standardize demo accounts on `@agendamax.demo`, make every demo login explicit in the README, and migrate local and Coolify data without deleting records. -**Architecture:** Keep the current simple demo authentication and replace only the two platform-owned demo email constants plus their test fixtures. Apply one targeted, idempotent SQLite `UPDATE` to existing local and production users; appointments, businesses, and related records remain unchanged. Deploy the source through the existing GitHub mirror and verify the live app through health and login endpoints. +**Architecture:** Keep the current simple demo authentication and replace only the two platform-owned demo email constants plus their test fixtures. Apply one targeted, idempotent SQLite `UPDATE` to the exact existing admin and owner rows; appointments, businesses, and related records remain unchanged. Deploy the source through the existing GitHub mirror and verify the live app through health and login endpoints. **Tech Stack:** React/Vite, Express, TypeScript, Node 22 `node:sqlite`, SQLite, PowerShell/OpenSSH, Coolify v4.1.2. ## Global Constraints - Keep the demo password exactly `demo1234`. -- Replace active `@agendapro.demo` references with `@agendamax.demo`; retain the old domain only in migration documentation or historical records. +- Replace active `@agendapro.demo` references with `@agendamax.demo`; retain the old domain only in explicit negative-login assertions or migration-history documentation. - Do not reset or delete local or production business, appointment, client, employee, ticket, or review data. - Never commit `.env.local.ps1` or print token values. - Production deployment must finish before the production data migration is run. @@ -27,7 +27,7 @@ - Modify: `server/scripts/seed.ts:315-334` — use the new admin and owner emails for fresh databases. - Modify: `src/pages/LoginPage.tsx:8-15` — use the new default development login. - Modify: `admin-test.mjs`, `e2e-test.mjs`, `e2e-full.mjs`, `server/scripts/booking-e2e.mjs`, `visual-audit.mjs`, and any other active file returned by `rg -l 'agendapro\.demo'` — keep automated login fixtures aligned. -- Modify: active plan/report documents containing executable login examples; leave the new migration specification as the historical explanation of the old domain. +- Modify: active plan/report documents containing executable login examples; keep old-domain examples only as migration-history documentation or explicit negative-login assertions. **Interfaces:** - Fresh seed continues to create users with the existing schema and password. @@ -41,7 +41,7 @@ Run: rg -n --glob '!node_modules' --glob '!data/**' 'agendapro\.demo|demo1234' . ``` -Expected: references are limited to the known seed, login defaults, tests, README, and historical documentation; no secret token values are printed. +Expected: references are limited to the known seed, login defaults, tests, README, explicit negative-login assertions, and migration-history documentation; no secret token values are printed. - [ ] **Step 2: Update the README credentials table first** @@ -81,7 +81,7 @@ Run: rg -n --glob '!node_modules' --glob '!data/**' --glob '!docs/superpowers/specs/**' 'agendapro\.demo' . ``` -Expected: no output, except explicitly retained historical migration notes if they are included by the chosen glob. +Expected: no output from application code or positive fixtures; the intentional old-domain negative-login assertions and migration-history documents are the only allowed matches. ### Task 2: Migrate the Local SQLite Demo Users @@ -90,7 +90,7 @@ Expected: no output, except explicitly retained historical migration notes if th **Interfaces:** - Uses the existing `server/db.ts` connection and `users` table. -- Produces updated admin/owner email rows while preserving all row counts outside `users.email`. +- Produces updated exact admin/owner email rows while preserving all row counts outside `users.email`. - [ ] **Step 1: Confirm the local database and take a count snapshot** @@ -103,22 +103,30 @@ node --import tsx --input-type=module -e "import { db } from './server/db.ts'; c Expected: the local database exists or the command reports that a fresh seed is required; existing appointment/client counts are recorded before mutation. +Before updating, inspect the exact old/new admin and owner rows from the `users` +snapshot. For either role, if its target email already exists while its matching +old-domain row also exists, stop and resolve that unique-email collision instead +of running the update. If no old-domain rows exist and the new rows are present, +record a safe no-op and skip the update. + - [ ] **Step 2: Apply the idempotent local email update** Run: ```powershell -node --import tsx --input-type=module -e "import { db } from './server/db.ts'; const r = db.prepare(\"UPDATE users SET email = REPLACE(email, '@agendapro.demo', '@agendamax.demo') WHERE email LIKE '%@agendapro.demo'\").run(); console.log(JSON.stringify(r));" +node --% --import tsx --input-type=module -e "import { db } from './server/db.ts'; const r = db.prepare(\"UPDATE users SET email = CASE email WHEN 'admin@agendapro.demo' THEN 'admin@agendamax.demo' WHEN 'owner@agendapro.demo' THEN 'owner@agendamax.demo' END WHERE (email = 'admin@agendapro.demo' AND role = 'admin') OR (email = 'owner@agendapro.demo' AND role = 'owner')\").run(); console.log(JSON.stringify(r));" ``` -Expected: only the old-domain demo user rows are changed; running the same command again reports zero additional changes. +Expected: only the exact email/role pairs are changed; an old-domain employee or +other role is untouched. Running the same command again reports zero additional +changes. - [ ] **Step 3: Verify local credentials and preservation** Run: ```powershell -node --import tsx --input-type=module -e "import { db } from './server/db.ts'; console.log(JSON.stringify({demo: db.prepare(\"SELECT email,role,password FROM users WHERE email LIKE '%@agendamax.demo' ORDER BY role\").all(), appointments: db.prepare('SELECT COUNT(*) AS c FROM appointments').get(), clients: db.prepare('SELECT COUNT(*) AS c FROM clients').get()}));" +node --% --import tsx --input-type=module -e "import { db } from './server/db.ts'; console.log(JSON.stringify({demo: db.prepare(\"SELECT email,role,password FROM users WHERE email IN ('admin@agendamax.demo','owner@agendamax.demo') ORDER BY role\").all(), appointments: db.prepare('SELECT COUNT(*) AS c FROM appointments').get(), clients: db.prepare('SELECT COUNT(*) AS c FROM clients').get()}));" ``` Expected: `admin@agendamax.demo` and `owner@agendamax.demo` use `demo1234`; appointment and client counts match the snapshot. @@ -148,7 +156,12 @@ npm run test:admin npm run test:e2e ``` -Expected: all existing checks pass with the new admin/owner emails. If a test requires a running dev server, start the repository's documented server first without resetting the database. +Expected: typecheck, build, unit, admin, and non-slot authentication checks pass. On the +already-migrated local database, `businesses.working_hours` and employee +`working_hours` are null, so the slot-dependent e2e/booking assertions may fail +before booking (including their existing downstream dereference); record this +pre-existing limitation as unrelated to the email change and do not change +scheduling data for this task. - [ ] **Step 3: Inspect the final diff** @@ -216,28 +229,38 @@ Use the existing Proxmox SSH chain to run inside LXC 102: ```sh container=$(docker ps --format '{{.ID}} {{.Names}}' | awk '$2 ~ /agendamax|s30f7/ {print $1; exit}') test -n "$container" || { echo 'AgendaMax container not found' >&2; exit 1; } -docker exec "$container" node -e "const {DatabaseSync}=require('node:sqlite'); const db=new DatabaseSync('/app/data/agendapro.db'); console.log(db.prepare(\"SELECT id,email,role FROM users WHERE email LIKE '%@agendapro.demo' OR email LIKE '%@agendamax.demo' ORDER BY id\").all())" +docker exec "$container" node -e "const {DatabaseSync}=require('node:sqlite'); const db=new DatabaseSync('/app/data/agendapro.db'); console.log(db.prepare(\"SELECT id,email,role FROM users WHERE email IN ('admin@agendapro.demo','owner@agendapro.demo','admin@agendamax.demo','owner@agendamax.demo') ORDER BY id\").all())" ``` -Expected: the app is running the new image and the current admin/owner rows are visible before mutation. +Expected: the app is running the new image and the current exact admin/owner +rows are visible before mutation. If an old row and its matching new row both +exist, stop and resolve the unique-email collision before updating. - [ ] **Step 2: Apply the production email update** ```sh -docker exec "$container" node -e "const {DatabaseSync}=require('node:sqlite'); const db=new DatabaseSync('/app/data/agendapro.db'); const r=db.prepare(\"UPDATE users SET email=REPLACE(email, '@agendapro.demo', '@agendamax.demo') WHERE email LIKE '%@agendapro.demo'\").run(); console.log(r)" +docker exec "$container" node -e "const {DatabaseSync}=require('node:sqlite'); const db=new DatabaseSync('/app/data/agendapro.db'); const r=db.prepare(\"UPDATE users SET email=CASE email WHEN 'admin@agendapro.demo' THEN 'admin@agendamax.demo' WHEN 'owner@agendapro.demo' THEN 'owner@agendamax.demo' END WHERE (email='admin@agendapro.demo' AND role='admin') OR (email='owner@agendapro.demo' AND role='owner')\").run(); console.log(r)" ``` -Expected: the admin and owner rows move to `@agendamax.demo`; no business or appointment rows are deleted. +Expected: only the matching admin and owner rows move to `@agendamax.demo`; no +old-domain employee or other role is rewritten, and no business or appointment +rows are deleted. + +If the preflight query shows an old row alongside its matching new row, stop and +resolve the unique-email collision. If it shows no old-domain rows because the +deployment seeded a fresh database with the new domain, record the update as a +safe no-op and skip it. - [ ] **Step 3: Verify live health and credentials** ```powershell -Invoke-RestMethod https://agendamax.urieljareth.org/api/health -Invoke-RestMethod https://agendamax.urieljareth.org/api/auth/login -Method Post -ContentType 'application/json' -Body '{"email":"admin@agendamax.demo","password":"demo1234"}' -Invoke-RestMethod https://agendamax.urieljareth.org/api/auth/login -Method Post -ContentType 'application/json' -Body '{"email":"owner@agendamax.demo","password":"demo1234"}' +$health = Invoke-RestMethod https://agendamax.urieljareth.org/api/health +$admin = Invoke-RestMethod https://agendamax.urieljareth.org/api/auth/login -Method Post -ContentType 'application/json' -Body '{"email":"admin@agendamax.demo","password":"demo1234"}' +$owner = Invoke-RestMethod https://agendamax.urieljareth.org/api/auth/login -Method Post -ContentType 'application/json' -Body '{"email":"owner@agendamax.demo","password":"demo1234"}' +Write-Host "health_ok=$($health.ok) admin_role=$($admin.user.role) admin_token_present=$([bool]$admin.token) owner_role=$($owner.user.role) owner_token_present=$([bool]$owner.token)" ``` -Expected: health returns `ok: true`; both logins return tokens and the correct roles. +Expected: health returns `ok: true`; both logins return the correct roles. Only boolean token-presence values are printed, never token contents. - [ ] **Step 4: Verify the deployed page and repository state** diff --git a/docs/superpowers/specs/2026-07-27-demo-email-domain-design.md b/docs/superpowers/specs/2026-07-27-demo-email-domain-design.md index a0ca07f..6b1d250 100644 --- a/docs/superpowers/specs/2026-07-27-demo-email-domain-design.md +++ b/docs/superpowers/specs/2026-07-27-demo-email-domain-design.md @@ -8,22 +8,25 @@ data, and the deployed Coolify instance. ## Scope -- Replace active `@agendapro.demo` references in application code, tests, - README, and operational documentation with `@agendamax.demo`; retain the old - domain only where this specification documents the migration itself. +- Replace active positive `@agendapro.demo` references in application code, + tests, README, and operational documentation with `@agendamax.demo`; retain + the old domain only in migration-history documentation or explicit + negative-login assertions. - Keep the existing demo password `demo1234` and expose it clearly beside each demo account in `README.md`. - Update the local SQLite demo users without deleting businesses, appointments, or other seeded records. - Deploy the code to the existing AgendaMax Coolify application. -- Update the persistent production SQLite users for the admin and owner accounts - with a targeted SQL change, preserving all other data. +- Update the persistent production SQLite users for the exact admin and owner + accounts with a targeted SQL change, preserving all other data. ## Data Flow Fresh databases receive the new admin and owner emails from `server/scripts/seed.ts` and `server/index.ts`. Existing local and production databases are migrated by -updating only `users.email` values whose domain is `agendapro.demo`. +updating only these two exact rows: `admin@agendapro.demo` and +`owner@agendapro.demo`. If the active database is fresh and already contains the +new emails, the SQL update is a no-op and is skipped. The employee accounts already use their business domains and are not changed. Login verification uses `POST /api/auth/login` with the documented password and @@ -34,8 +37,11 @@ does not expose password values through the API. 1. Run the local checks and build. 2. Push the change to the AgendaMax source mirror used by Coolify. 3. Trigger and monitor a Coolify deployment. -4. Run a targeted SQLite update inside the persistent production app volume: - `UPDATE users SET email = REPLACE(email, '@agendapro.demo', '@agendamax.demo') WHERE email LIKE '%@agendapro.demo';` +4. Inspect the exact old/new admin and owner rows. If an old row and its matching + new row both exist, stop and resolve the unique-email collision. If no old + rows exist and the new rows are present, record a safe no-op. Otherwise run a + targeted SQLite update inside the persistent production app volume: + `UPDATE users SET email = CASE email WHEN 'admin@agendapro.demo' THEN 'admin@agendamax.demo' WHEN 'owner@agendapro.demo' THEN 'owner@agendamax.demo' END WHERE (email = 'admin@agendapro.demo' AND role = 'admin') OR (email = 'owner@agendapro.demo' AND role = 'owner');` 5. Verify health, login for admin and owner, and the production page. If deployment fails, do not run the production SQL migration; inspect the @@ -43,11 +49,25 @@ deployment first and use the existing Coolify rollback procedure. ## Acceptance Criteria -- `rg "agendapro\\.demo"` returns no active application, test, or README - references (historical notes may be explicitly excluded if any are retained). +- `rg "agendapro\\.demo"` returns no active positive application, test, or + README references; only explicit negative-login assertions and + migration-history documentation may retain the old domain. - README lists every demo account with email and `demo1234`. - Fresh seed creates `admin@agendamax.demo` and `owner@agendamax.demo`. - Existing local data contains the new emails and retains its records. - Coolify deployment finishes successfully. - Production `/api/health` returns `ok: true`. - Production admin and owner logins succeed with `demo1234`. +- Production admin and owner logins using the old domain return `401`. +- README employee emails exactly match the accent-preserving emails generated by the seed. +- The targeted SQL pairs each old email with its matching role and never rewrites + another old-domain user. + +## Known Verification Limitation + +The already-migrated local database has null `businesses.working_hours` and +employee `working_hours` values. As a result, the slot-dependent `test:e2e` and +`test:booking` assertions can fail before booking, including their existing +downstream dereference. This is a pre-existing scheduling-data limitation, +unrelated to the email-domain change; do not change working-hours data for this +task. diff --git a/e2e-full.mjs b/e2e-full.mjs index a834623..2db09ae 100644 --- a/e2e-full.mjs +++ b/e2e-full.mjs @@ -27,6 +27,18 @@ const me = await req("/api/auth/demo-users"); check("demo-users >= 8", me.json.u const owner = await login("owner@agendamax.demo"); check("owner login", !!owner.token && owner.user.role === "owner"); const empLogin = await login("valentina.cruz@lumiere.mx"); check("employee login", empLogin.user.role === "employee" && empLogin.user.employee_id); const adminLogin = await login("admin@agendamax.demo"); check("admin login", adminLogin.user.role === "admin"); +const legacyAdmin = await req("/api/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email: "admin@agendapro.demo", password: "demo1234" }), +}); +check("admin@agendapro.demo legacy login rejected", legacyAdmin.status === 401, `status=${legacyAdmin.status}`); +const legacyOwner = await req("/api/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email: "owner@agendapro.demo", password: "demo1234" }), +}); +check("owner@agendapro.demo legacy login rejected", legacyOwner.status === 401, `status=${legacyOwner.status}`); const t = owner.token, eT = empLogin.token, aT = adminLogin.token; // /me fix