Este documento explica cómo funciona la autenticación en la nueva web. Hay 3 sistemas distintos coexistiendo, lo cual es contraintuitivo y se confunde con frecuencia. Aclararlo aquí evita decisiones erróneas en migración de datos.
| Sistema | Tabla BD | Quién lo usa | Estado |
|---|---|---|---|
| Login storefront (clientes) | (Shopify) | Clientes finales que compran | Sistema oficial |
| Login admin panel (empleados) | admins (PostgreSQL) |
Equipo Pampling para gestionar el negocio | Sistema oficial |
| Sign-in admin via users | users (PostgreSQL) |
Tests E2E y nada más | Legacy, candidato a eliminar |
Punto clave: los clientes finales NO se autentican contra la base de datos PostgreSQL del backoffice. Se autentican contra Shopify Customer Account API. El backoffice solo guarda metadata por usuario.
Es el sistema que usa cualquiera que entre a comprar en la web.
/login o intenta una acción que requiere sesión./api/auth/login.storefrontAuthService.createCustomerAccessToken).apps/storefront/src/app/api/auth/login/route.ts/account/login).users del backoffice (RegisterUserService), con password = argon2(crypto.randomUUID()) como placeholder. Esa fila guarda metadata: nombre, email, fecha de registro, puntos Pampling UP, etc.users.password no se usa para autenticar clientes. Es un placeholder técnico para satisfacer el constraint NOT NULL.El panel administrativo de la nueva web (AdminJS).
/admin/login./admin/api/login con email + password.admins con argon2.verify.ADMIN_COOKIE_NAME (httpOnly, sameSite:strict, 8h TTL).apps/backoffice/src/app/modules/admin-panel/AdminAuthService (admin-auth.service.ts)admin-auth.middleware.ts protege /admin/* salvo rutas públicas como /admin/login.admins| Columna | Tipo | Notas |
|---|---|---|
id |
UUID | PK |
email |
varchar | único |
password |
varchar | hash argon2id |
first_name, last_name, phone_number |
varchar nullable | |
role |
varchar(10) | base o super |
is_active |
boolean | |
last_login_at |
timestamp | |
totp_secret |
varchar | cifrado |
totp_enabled |
boolean | |
must_change_password |
boolean | flag primer login |
must_enable_2fa |
boolean | flag primer login |
created_at, updated_at |
timestamp |
Tabla auxiliar: pending_totp_tokens (sesiones intermedias durante 2FA).
nx run backoffice:seed-super-admin -- \
-e admin@pampling.com \
-p "PasswordFuerte123!" \
--must-change-password \
--must-enable-2fa
Opciones disponibles en la CLI: ver apps/backoffice/src/app/modules/cli/commands/seed-super-admin.command.ts.
base: acceso limitado al admin panel.super: acceso total, puede crear otros admins.users (LEGACY)Este sistema existe en código pero no se usa en producción. Lo conserva solo el suite de tests E2E.
Antes de que Tailor implementara el admin-panel con tabla admins separada (febrero 2026), había un sistema de roles dentro de users con valores basic | designer | admin | super-admin. Cuando se creó la separación, este código no se eliminó.
POST /v1/admin/administrators/sign-inSignInAdminUserServiceargon2.verify(user.password, dto.password) contra la tabla users.Es la única vía que lee la columna users.password. Si decidimos:
users.password al migrar usuarios (ver sección 4).users.password nullable.Decisión actual: dejarlo y poner argon2(randomUUID) en migración. Es deuda técnica a limpiar post go-live.
Los ~140k usuarios del sistema PHP legacy se migran en dos pasos:
Script apps/sync-db/src/commands/customers/import-customers.command.ts:
app_usuarios del MariaDB legacy.firstName + email, sin password).users del PostgreSQL con metadata (id, role='basic', is_active=true, email, first_name, shopify_id).password. Como password es NOT NULL, falla. Hay que parchear para añadir password = argon2.hash(crypto.randomUUID()).Script apps/sync-db/src/commands/import-legacy-passwords.command.ts:
app_usuarios.pass (hashes MD5) del MariaDB legacy.users del PostgreSQL: legacy_password_hash = <md5> para los users que ya existen (los importados en paso 1).Cuando ese usuario intenta loguearse:
handleLegacyUserFallback se activa:
getUserByEmailService → obtiene legacyPasswordHash.verifyLegacyPasswordService con la password en claro → backoffice calcula MD5 y compara.legacyUserMigrationService.migrateToShopify): activa el customer con la password en claro tecleada por el usuario.legacy_password_hash = NULL.apps/storefront/src/features/auth/LEGACY_AUTH.md (en el repo)phone_number | varchar nullable | |role | varchar(20) default 'basic' | basic, designer, admin, super-admin |is_active | boolean | |last_login_at | timestamp nullable | |created_at, updated_at | timestamp | |shopify_id | varchar nullable | GID del customer en Shopify |referral_code | varchar(8) unique | código de referido propio |referred_by_code | varchar(8) nullable | código que usó al registrarse |image_source | varchar(512) nullable | URL avatar |legacy_password_hash | varchar nullable | MD5 heredado del PHP, se limpia tras migrar |/api/auth/login con identityProvider=google y loginUrl=<callback>.app_usuarios_facebook). ┌────────────────────────────────┐
│ Cliente final │
└───────────────┬────────────────┘
│
(email+pwd) │ (Google OAuth)
▼
┌────────────────────────────────┐
│ Shopify Customer Account API │
└───────────────┬────────────────┘
│
(cookie) │ (si falla → fallback legacy)
▼ ▼
┌────────────────────┐ ┌────────────────────┐
│ Sesión activa │ │ Backoffice: │
│ Shopify │ │ verify MD5 + │
└────────────────────┘ │ migrar a Shopify │
└────────────────────┘
┌────────────────────────────────┐
│ Empleado Pampling │
└───────────────┬────────────────┘
│ (email+pwd+TOTP)
▼
┌────────────────────────────────┐
│ Backoffice tabla `admins` │
│ + AdminAuthService │
└────────────────────────────────┘