feat: onboarding pre-work
This commit is contained in:
10
Makefile
10
Makefile
@@ -1,4 +1,4 @@
|
||||
.PHONY: help setup start stop clean logs shell-backend shell-frontend migrate rebuild traefik-dashboard traefik-logs service-discovery network-inspect health-check-all mobile-setup db-shell-app install type-check lint build-local
|
||||
.PHONY: help setup start stop clean logs shell-backend shell-frontend migrate create-admin rebuild traefik-dashboard traefik-logs service-discovery network-inspect health-check-all mobile-setup db-shell-app install type-check lint build-local
|
||||
|
||||
help:
|
||||
@echo "MotoVaultPro - Simplified 5-Container Architecture"
|
||||
@@ -14,6 +14,7 @@ help:
|
||||
@echo " make shell-backend - Open shell in backend container"
|
||||
@echo " make shell-frontend - Open shell in frontend container"
|
||||
@echo " make migrate - Run database migrations"
|
||||
@echo " make create-admin - Create initial admin user (fresh deployments only)"
|
||||
@echo ""
|
||||
@echo "K8s-Ready Architecture Commands:"
|
||||
@echo " make traefik-dashboard - Access Traefik service discovery dashboard"
|
||||
@@ -94,6 +95,13 @@ migrate:
|
||||
@docker compose exec mvp-backend node dist/_system/migrations/run-all.js
|
||||
@echo "Migrations completed."
|
||||
|
||||
create-admin:
|
||||
@echo ""
|
||||
@echo "Creating initial admin user..."
|
||||
@echo "This command is only for fresh deployments with no existing admins."
|
||||
@echo ""
|
||||
@docker compose exec -it mvp-backend node dist/_system/cli/create-admin.js
|
||||
|
||||
rebuild:
|
||||
@echo "Rebuilding containers with latest code changes..."
|
||||
@docker compose up -d --build --remove-orphans
|
||||
|
||||
193
ONBOARDING-FIX.md
Normal file
193
ONBOARDING-FIX.md
Normal file
@@ -0,0 +1,193 @@
|
||||
# Fix New User Signup Wizard Flow
|
||||
|
||||
## Problem Summary
|
||||
The new user signup wizard is broken:
|
||||
1. After Auth0 callback, users go to `/garage` instead of `/verify-email`
|
||||
2. Users can access `/garage/*` without verified email
|
||||
3. Onboarding flow is bypassed entirely
|
||||
4. **New Requirement**: Block login completely at Auth0 for unverified users
|
||||
|
||||
## Root Causes
|
||||
1. **Auth0Provider.tsx:29** - `onRedirectCallback` defaults to `/garage` without checking verification
|
||||
2. **App.tsx:472-481** - Callback route just shows "Processing login..." with no routing logic
|
||||
3. **App.tsx:549+** - Protected routes have no email verification check
|
||||
4. **Auth0** - No rule/action blocking unverified users from logging in
|
||||
|
||||
---
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Auth0 Configuration (Manual Step)
|
||||
|
||||
**Auth0 Dashboard -> Actions -> Flows -> Login**
|
||||
|
||||
Create a Post Login Action to block unverified users:
|
||||
|
||||
```javascript
|
||||
exports.onExecutePostLogin = async (event, api) => {
|
||||
if (!event.user.email_verified) {
|
||||
api.access.deny('Please verify your email address before logging in. Check your inbox for a verification link.');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
This ensures:
|
||||
- Unverified users cannot get a JWT
|
||||
- They see a clear error on the Auth0 login screen
|
||||
- They must click the verification link before logging in
|
||||
|
||||
### Phase 2: Update Signup Flow
|
||||
|
||||
**After signup, redirect to a "Check Your Email" page (not /verify-email)**
|
||||
|
||||
The new flow:
|
||||
1. User submits signup form
|
||||
2. Backend creates Auth0 user (unverified)
|
||||
3. Auth0 automatically sends verification email
|
||||
4. Frontend shows "Check Your Email" page with:
|
||||
- Message: "We've sent a verification link to your email"
|
||||
- Resend button (calls public resend endpoint)
|
||||
- "Back to Login" button
|
||||
5. User clicks email link -> Auth0 marks as verified
|
||||
6. User can now login -> goes to /onboarding
|
||||
|
||||
### Phase 3: Backend Changes
|
||||
|
||||
**File: `backend/src/features/auth/api/auth.routes.ts`**
|
||||
- Add `POST /api/auth/resend-verification-public` (no JWT required)
|
||||
- Takes email address, looks up user, resends verification
|
||||
|
||||
**File: `backend/src/features/auth/api/auth.controller.ts`**
|
||||
- Add `resendVerificationPublic` handler
|
||||
|
||||
**File: `backend/src/features/auth/domain/auth.service.ts`**
|
||||
- Add `resendVerificationByEmail` method
|
||||
|
||||
**File: `backend/src/features/auth/api/auth.routes.ts`**
|
||||
- Add `GET /api/auth/user-status` (JWT required)
|
||||
- Returns: `{ emailVerified, onboardingCompleted, email }`
|
||||
|
||||
**File: `backend/src/core/plugins/auth.plugin.ts`**
|
||||
- Add `/api/auth/user-status` to `VERIFICATION_EXEMPT_ROUTES`
|
||||
|
||||
### Phase 4: Create Callback Handler
|
||||
|
||||
**File: `frontend/src/features/auth/pages/CallbackPage.tsx`** (NEW)
|
||||
- Fetches user status after Auth0 callback
|
||||
- Routes based on status:
|
||||
- Not onboarded -> `/onboarding`
|
||||
- Onboarded -> `/garage` (or returnTo)
|
||||
- Note: Unverified users never reach this (blocked by Auth0)
|
||||
|
||||
**File: `frontend/src/features/auth/mobile/CallbackMobileScreen.tsx`** (NEW)
|
||||
- Mobile version
|
||||
|
||||
### Phase 5: Update Auth0Provider
|
||||
|
||||
**File: `frontend/src/core/auth/Auth0Provider.tsx`**
|
||||
|
||||
Update `onRedirectCallback` (line 27-31):
|
||||
|
||||
```typescript
|
||||
const onRedirectCallback = (appState?: { returnTo?: string }) => {
|
||||
navigate('/callback', {
|
||||
replace: true,
|
||||
state: { returnTo: appState?.returnTo || '/garage' }
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### Phase 6: Rename/Update Verify Email Page
|
||||
|
||||
**File: `frontend/src/features/auth/pages/VerifyEmailPage.tsx`**
|
||||
- Rename concept to "Check Your Email" page
|
||||
- Remove polling (user can't be authenticated)
|
||||
- Show static message + resend button (calls public endpoint)
|
||||
- Add "Back to Login" button
|
||||
|
||||
**File: `frontend/src/features/auth/mobile/VerifyEmailMobileScreen.tsx`**
|
||||
- Same changes for mobile
|
||||
|
||||
### Phase 7: Update App.tsx Routing
|
||||
|
||||
**File: `frontend/src/App.tsx`**
|
||||
|
||||
1. Replace callback handling (lines 472-481) with CallbackPage
|
||||
2. Add onboarding guard after authentication check
|
||||
3. Remove email verification check from frontend (Auth0 handles it)
|
||||
|
||||
```typescript
|
||||
// After isAuthenticated check:
|
||||
// Fetch onboarding status
|
||||
// If not onboarded -> /onboarding
|
||||
// Otherwise -> proceed to /garage
|
||||
```
|
||||
|
||||
### Phase 8: Create Supporting Files
|
||||
|
||||
**File: `frontend/src/core/auth/useUserStatus.ts`** (NEW)
|
||||
- Hook for fetching user status
|
||||
|
||||
**File: `frontend/src/features/auth/api/auth.api.ts`**
|
||||
- Add `getUserStatus()`
|
||||
- Add `resendVerificationPublic(email)` (no auth)
|
||||
|
||||
---
|
||||
|
||||
## Files to Modify
|
||||
|
||||
### Auth0 (Manual Configuration)
|
||||
- Create Post Login Action to block unverified users
|
||||
|
||||
### Backend
|
||||
- `backend/src/features/auth/api/auth.routes.ts` - Add endpoints
|
||||
- `backend/src/features/auth/api/auth.controller.ts` - Add handlers
|
||||
- `backend/src/features/auth/domain/auth.service.ts` - Add methods
|
||||
- `backend/src/core/plugins/auth.plugin.ts` - Update exempt routes
|
||||
|
||||
### Frontend
|
||||
- `frontend/src/core/auth/Auth0Provider.tsx` - Fix onRedirectCallback
|
||||
- `frontend/src/App.tsx` - Add route guards and callback handler
|
||||
- `frontend/src/features/auth/pages/CallbackPage.tsx` - NEW
|
||||
- `frontend/src/features/auth/mobile/CallbackMobileScreen.tsx` - NEW
|
||||
- `frontend/src/features/auth/pages/VerifyEmailPage.tsx` - Update to static page
|
||||
- `frontend/src/features/auth/mobile/VerifyEmailMobileScreen.tsx` - Update
|
||||
- `frontend/src/core/auth/useUserStatus.ts` - NEW
|
||||
- `frontend/src/features/auth/api/auth.api.ts` - Add functions
|
||||
|
||||
---
|
||||
|
||||
## New User Flow (After Fix)
|
||||
|
||||
```
|
||||
1. Signup form submission
|
||||
2. -> POST /api/auth/signup (creates unverified Auth0 user)
|
||||
3. -> Navigate to /verify-email (static "Check Your Email" page)
|
||||
4. User clicks verification link in email
|
||||
5. -> Auth0 marks user as verified
|
||||
6. User clicks "Login" on /verify-email page
|
||||
7. -> Auth0 login succeeds (user is now verified)
|
||||
8. -> /callback page fetches status
|
||||
9. -> Not onboarded? -> /onboarding
|
||||
10. -> Complete onboarding -> /garage
|
||||
```
|
||||
|
||||
## Returning User Flow
|
||||
|
||||
```
|
||||
1. Login attempt (unverified) -> Auth0 blocks with error message
|
||||
2. Login attempt (verified, not onboarded) -> /callback -> /onboarding
|
||||
3. Login attempt (verified, onboarded) -> /callback -> /garage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
- [ ] Auth0 Action blocks unverified login with clear error
|
||||
- [ ] Signup -> check-email page -> verify via email -> login works
|
||||
- [ ] Resend verification from check-email page works
|
||||
- [ ] Verified user (no onboarding) -> onboarding wizard
|
||||
- [ ] Verified + onboarded user -> direct to garage
|
||||
- [ ] Direct URL access to /garage -> requires login
|
||||
- [ ] All flows work on mobile
|
||||
- [ ] All flows work on desktop
|
||||
315
backend/src/_system/cli/create-admin.ts
Normal file
315
backend/src/_system/cli/create-admin.ts
Normal file
@@ -0,0 +1,315 @@
|
||||
/**
|
||||
* @ai-summary CLI script for creating initial admin user on fresh deployments
|
||||
* @ai-context Runs inside backend container with access to Auth0 and PostgreSQL
|
||||
*
|
||||
* Usage: node dist/_system/cli/create-admin.js
|
||||
*
|
||||
* Interactive prompts for email and password, then:
|
||||
* 1. Checks if any admin exists (errors if yes)
|
||||
* 2. Creates user in Auth0 via Management API
|
||||
* 3. Creates user_profile record
|
||||
* 4. Creates admin_users record with audit log
|
||||
*/
|
||||
|
||||
import * as readline from 'readline';
|
||||
import { Pool } from 'pg';
|
||||
import { appConfig } from '../../core/config/config-loader';
|
||||
import { auth0ManagementClient } from '../../core/auth/auth0-management.client';
|
||||
import { UserProfileRepository } from '../../features/user-profile/data/user-profile.repository';
|
||||
import { AdminRepository } from '../../features/admin/data/admin.repository';
|
||||
import { AdminService } from '../../features/admin/domain/admin.service';
|
||||
|
||||
// Terminal color codes
|
||||
const colors = {
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
cyan: '\x1b[36m',
|
||||
reset: '\x1b[0m',
|
||||
bold: '\x1b[1m',
|
||||
};
|
||||
|
||||
function printInfo(msg: string): void {
|
||||
console.log(`${colors.green}[INFO]${colors.reset} ${msg}`);
|
||||
}
|
||||
|
||||
function printError(msg: string): void {
|
||||
console.log(`${colors.red}[ERROR]${colors.reset} ${msg}`);
|
||||
}
|
||||
|
||||
function printWarn(msg: string): void {
|
||||
console.log(`${colors.yellow}[WARN]${colors.reset} ${msg}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt for text input
|
||||
*/
|
||||
function prompt(rl: readline.Interface, question: string): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer) => {
|
||||
resolve(answer.trim());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt for password input (masked - characters not echoed)
|
||||
*/
|
||||
function promptPassword(question: string): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
process.stdout.write(question);
|
||||
|
||||
const stdin = process.stdin;
|
||||
const wasRaw = stdin.isRaw;
|
||||
|
||||
stdin.setRawMode?.(true);
|
||||
stdin.resume();
|
||||
|
||||
let password = '';
|
||||
|
||||
const onData = (char: Buffer): void => {
|
||||
const c = char.toString();
|
||||
|
||||
if (c === '\n' || c === '\r') {
|
||||
stdin.removeListener('data', onData);
|
||||
stdin.setRawMode?.(wasRaw);
|
||||
stdin.pause();
|
||||
console.log(); // newline after password
|
||||
resolve(password);
|
||||
} else if (c === '\u0003') {
|
||||
// Ctrl+C - exit gracefully
|
||||
console.log('\n\nAborted by user.');
|
||||
process.exit(1);
|
||||
} else if (c === '\u007f' || c === '\b') {
|
||||
// Backspace
|
||||
password = password.slice(0, -1);
|
||||
// Optionally show backspace effect
|
||||
if (password.length >= 0) {
|
||||
process.stdout.write('\b \b');
|
||||
}
|
||||
} else if (c.charCodeAt(0) >= 32) {
|
||||
// Printable character
|
||||
password += c;
|
||||
process.stdout.write('*'); // Show asterisk for each character
|
||||
}
|
||||
};
|
||||
|
||||
stdin.on('data', onData);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate email format
|
||||
*/
|
||||
function validateEmail(email: string): boolean {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate password strength
|
||||
* Returns error message or null if valid
|
||||
*/
|
||||
function validatePassword(password: string): string | null {
|
||||
if (password.length < 8) {
|
||||
return 'Password must be at least 8 characters long';
|
||||
}
|
||||
if (!/[A-Z]/.test(password)) {
|
||||
return 'Password must contain at least one uppercase letter';
|
||||
}
|
||||
if (!/[a-z]/.test(password)) {
|
||||
return 'Password must contain at least one lowercase letter';
|
||||
}
|
||||
if (!/[0-9]/.test(password)) {
|
||||
return 'Password must contain at least one number';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for database to be available
|
||||
*/
|
||||
async function waitForDatabase(pool: Pool, timeoutMs = 30000): Promise<void> {
|
||||
const start = Date.now();
|
||||
while (true) {
|
||||
try {
|
||||
await pool.query('SELECT 1');
|
||||
return;
|
||||
} catch (error) {
|
||||
if (Date.now() - start > timeoutMs) {
|
||||
throw new Error('Database connection timeout');
|
||||
}
|
||||
await new Promise((res) => setTimeout(res, 1000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
console.log('');
|
||||
console.log(`${colors.bold}${colors.cyan}========================================${colors.reset}`);
|
||||
console.log(`${colors.bold}${colors.cyan} MotoVaultPro - Create Admin User${colors.reset}`);
|
||||
console.log(`${colors.bold}${colors.cyan}========================================${colors.reset}`);
|
||||
console.log('');
|
||||
|
||||
// Initialize database pool
|
||||
const pool = new Pool({
|
||||
connectionString: appConfig.getDatabaseUrl(),
|
||||
});
|
||||
|
||||
try {
|
||||
// Wait for database
|
||||
printInfo('Connecting to database...');
|
||||
await waitForDatabase(pool);
|
||||
printInfo('Database connected.');
|
||||
|
||||
// Initialize repositories and services
|
||||
const userProfileRepo = new UserProfileRepository(pool);
|
||||
const adminRepo = new AdminRepository(pool);
|
||||
const adminService = new AdminService(adminRepo);
|
||||
|
||||
// Step 1: Check if any admin already exists
|
||||
printInfo('Checking for existing admin users...');
|
||||
const existingAdmins = await adminService.getActiveAdmins();
|
||||
|
||||
if (existingAdmins.length > 0) {
|
||||
console.log('');
|
||||
printError('An admin user already exists!');
|
||||
console.log('');
|
||||
console.log(` Found ${existingAdmins.length} existing admin(s):`);
|
||||
for (const admin of existingAdmins) {
|
||||
console.log(` - ${admin.email} (${admin.role})`);
|
||||
}
|
||||
console.log('');
|
||||
console.log(' This command is only for initial admin setup on fresh deployments.');
|
||||
console.log(' To create additional admins, use the admin panel.');
|
||||
console.log('');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
printInfo('No existing admins found. Proceeding with setup.');
|
||||
console.log('');
|
||||
|
||||
// Step 2: Create readline interface for interactive prompts
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
// Prompt for email
|
||||
let email: string;
|
||||
while (true) {
|
||||
email = await prompt(rl, 'Enter admin email: ');
|
||||
if (!email) {
|
||||
printError('Email is required');
|
||||
continue;
|
||||
}
|
||||
if (!validateEmail(email)) {
|
||||
printError('Please enter a valid email address');
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Close readline before password prompts (we handle raw input directly)
|
||||
rl.close();
|
||||
|
||||
// Prompt for password
|
||||
let password: string;
|
||||
while (true) {
|
||||
password = await promptPassword('Enter password: ');
|
||||
const passwordError = validatePassword(password);
|
||||
if (passwordError) {
|
||||
printError(passwordError);
|
||||
continue;
|
||||
}
|
||||
|
||||
const confirmPassword = await promptPassword('Confirm password: ');
|
||||
if (password !== confirmPassword) {
|
||||
printError('Passwords do not match');
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
console.log('');
|
||||
printInfo('Creating admin user...');
|
||||
|
||||
// Step 3: Create user in Auth0 (with email pre-verified for trusted CLI-created admins)
|
||||
printInfo('Creating user in Auth0...');
|
||||
let auth0Sub: string;
|
||||
try {
|
||||
auth0Sub = await auth0ManagementClient.createUser({ email, password, emailVerified: true });
|
||||
printInfo(`Auth0 user created: ${auth0Sub}`);
|
||||
} catch (error) {
|
||||
printError(
|
||||
`Failed to create Auth0 user: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Step 4: Create user_profile record (with email verified for trusted CLI-created admins)
|
||||
printInfo('Creating user profile...');
|
||||
try {
|
||||
const displayName = email.split('@')[0]; // Use email prefix as display name
|
||||
await userProfileRepo.create(auth0Sub, email, displayName);
|
||||
// Mark email as verified in database (trusted admin created via CLI)
|
||||
await userProfileRepo.updateEmailVerified(auth0Sub, true);
|
||||
printInfo('User profile created (email verified).');
|
||||
} catch (error) {
|
||||
printError(
|
||||
`Failed to create user profile: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
// Attempt to clean up Auth0 user
|
||||
printWarn('Attempting to clean up Auth0 user...');
|
||||
try {
|
||||
await auth0ManagementClient.deleteUser(auth0Sub);
|
||||
printInfo('Auth0 user cleaned up.');
|
||||
} catch (cleanupError) {
|
||||
printError('Failed to clean up Auth0 user - manual cleanup may be required');
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Step 5: Create admin_users record
|
||||
printInfo('Creating admin record...');
|
||||
try {
|
||||
const admin = await adminService.createAdmin(
|
||||
email,
|
||||
'admin',
|
||||
auth0Sub,
|
||||
'system-cli' // createdBy indicator for CLI-created admins
|
||||
);
|
||||
printInfo(`Admin record created: ${admin.email} (${admin.role})`);
|
||||
} catch (error) {
|
||||
printError(
|
||||
`Failed to create admin record: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
printWarn('User was created but admin privileges were not assigned.');
|
||||
printWarn('You may need to manually grant admin access via the database.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Success
|
||||
console.log('');
|
||||
console.log(`${colors.bold}${colors.green}========================================${colors.reset}`);
|
||||
console.log(`${colors.bold}${colors.green} Admin User Created Successfully!${colors.reset}`);
|
||||
console.log(`${colors.bold}${colors.green}========================================${colors.reset}`);
|
||||
console.log('');
|
||||
console.log(` Email: ${email}`);
|
||||
console.log(` Role: admin`);
|
||||
console.log(` Auth0 ID: ${auth0Sub}`);
|
||||
console.log('');
|
||||
console.log(' You can now log in at https://motovaultpro.com');
|
||||
console.log('');
|
||||
} catch (error) {
|
||||
printError(`Unexpected error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
// Only run if executed directly
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { logger } from '../logging/logger';
|
||||
interface CreateUserParams {
|
||||
email: string;
|
||||
password: string;
|
||||
emailVerified?: boolean; // Optional: set true for trusted users (e.g., CLI-created admins)
|
||||
}
|
||||
|
||||
interface UserDetails {
|
||||
@@ -46,7 +47,7 @@ class Auth0ManagementClientSingleton {
|
||||
* @param password User's password
|
||||
* @returns Auth0 user ID
|
||||
*/
|
||||
async createUser({ email, password }: CreateUserParams): Promise<string> {
|
||||
async createUser({ email, password, emailVerified = false }: CreateUserParams): Promise<string> {
|
||||
try {
|
||||
const client = this.getClient();
|
||||
|
||||
@@ -54,7 +55,7 @@ class Auth0ManagementClientSingleton {
|
||||
connection: this.CONNECTION_NAME,
|
||||
email,
|
||||
password,
|
||||
email_verified: false,
|
||||
email_verified: emailVerified,
|
||||
});
|
||||
|
||||
const user = response.data;
|
||||
|
||||
@@ -18,10 +18,8 @@ CREATE INDEX IF NOT EXISTS idx_admin_users_created_at ON admin_users(created_at)
|
||||
-- Create index on revoked_at for active admin queries
|
||||
CREATE INDEX IF NOT EXISTS idx_admin_users_revoked_at ON admin_users(revoked_at);
|
||||
|
||||
-- Seed initial admin user (idempotent)
|
||||
INSERT INTO admin_users (auth0_sub, email, role, created_by)
|
||||
VALUES ('system|bootstrap', 'admin@motovaultpro.com', 'admin', 'system')
|
||||
ON CONFLICT (auth0_sub) DO NOTHING;
|
||||
-- Note: Initial admin user is created via `make create-admin` command
|
||||
-- This allows for dynamic email/password configuration on fresh deployments
|
||||
|
||||
-- Create update trigger function (if not exists)
|
||||
DO $$
|
||||
|
||||
@@ -157,12 +157,11 @@ export class UserProfileController {
|
||||
});
|
||||
}
|
||||
|
||||
const { password, confirmationText } = validation.data;
|
||||
const { confirmationText } = validation.data;
|
||||
|
||||
// Request deletion
|
||||
// Request deletion (user is already authenticated via JWT)
|
||||
const profile = await this.userProfileService.requestDeletion(
|
||||
auth0Sub,
|
||||
password,
|
||||
confirmationText
|
||||
);
|
||||
|
||||
@@ -178,13 +177,6 @@ export class UserProfileController {
|
||||
userId: request.userContext?.userId,
|
||||
});
|
||||
|
||||
if (error.message.includes('Invalid password')) {
|
||||
return reply.code(401).send({
|
||||
error: 'Unauthorized',
|
||||
message: 'Invalid password',
|
||||
});
|
||||
}
|
||||
|
||||
if (error.message.includes('Invalid confirmation')) {
|
||||
return reply.code(400).send({
|
||||
error: 'Bad Request',
|
||||
|
||||
@@ -18,7 +18,6 @@ export const updateProfileSchema = z.object({
|
||||
export type UpdateProfileInput = z.infer<typeof updateProfileSchema>;
|
||||
|
||||
export const requestDeletionSchema = z.object({
|
||||
password: z.string().min(1, 'Password is required'),
|
||||
confirmationText: z.string().refine((val) => val === 'DELETE', {
|
||||
message: 'Confirmation text must be exactly "DELETE"',
|
||||
}),
|
||||
|
||||
@@ -328,12 +328,12 @@ export class UserProfileService {
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Request account deletion with password verification
|
||||
* Request account deletion
|
||||
* Sets 30-day grace period before permanent deletion
|
||||
* Note: User is already authenticated via JWT, confirmation text is sufficient
|
||||
*/
|
||||
async requestDeletion(
|
||||
auth0Sub: string,
|
||||
password: string,
|
||||
confirmationText: string
|
||||
): Promise<UserProfile> {
|
||||
try {
|
||||
@@ -353,12 +353,6 @@ export class UserProfileService {
|
||||
throw new Error('Deletion already requested');
|
||||
}
|
||||
|
||||
// Verify password with Auth0
|
||||
const passwordValid = await auth0ManagementClient.verifyPassword(profile.email, password);
|
||||
if (!passwordValid) {
|
||||
throw new Error('Invalid password');
|
||||
}
|
||||
|
||||
// Request deletion
|
||||
const updatedProfile = await this.repository.requestDeletion(auth0Sub);
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ comprehensive spec.md - containing requirements, architecture decisions, data mo
|
||||
You are a senior software engineer specializsing in NodeJS, Typescript, front end and back end development. You will be delegating tasks to the platform-agent, feature-agent, first-frontend-agent and quality-agent when appropriate.
|
||||
|
||||
*** ACTION ***
|
||||
- You will be implementing improvements to the User Management.
|
||||
- You will be fixing a workflow logic in the new user sign up wizard.
|
||||
- Make no assumptions.
|
||||
- Ask clarifying questions.
|
||||
- Ultrathink
|
||||
@@ -27,11 +27,10 @@ You are a senior software engineer specializsing in NodeJS, Typescript, front en
|
||||
*** CONTEXT ***
|
||||
- This is a modern web app for managing a vehicle fleet. It has both a desktop and mobile versions of the site that both need to maintain feature parity. It's currently deployed via docker compose but in the future will be deployed via k8s.
|
||||
- Read README.md CLAUDE.md and AI-INDEX.md and follow relevant instructions to understand this code repository in the context of this change.
|
||||
- There is no delete option for users
|
||||
- GPDR requires that users are able to fully delete their information
|
||||
- There is a Delete button in the user settings. This needs to be implemented
|
||||
- The same functionality should be enabled admin settings for user management.
|
||||
|
||||
- When a new user signs up, they are immediately redirected to https://motovaultpro.com/verify-email which is supposed to send them through a new user wizard.
|
||||
- The user is also allowed to login before the email is confirmed. There are API errors but the login is allowed.
|
||||
- It should not even let people login without a verified email.
|
||||
- The new user wizard exists. It worked in the past. Recent user changes must have broken the workflow.
|
||||
|
||||
*** CHANGES TO IMPLEMENT ***
|
||||
- Research this code base and ask iterative questions to compile a complete plan.
|
||||
|
||||
@@ -23,28 +23,26 @@ interface DeleteAccountDialogProps {
|
||||
}
|
||||
|
||||
export const DeleteAccountDialog: React.FC<DeleteAccountDialogProps> = ({ open, onClose }) => {
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmationText, setConfirmationText] = useState('');
|
||||
const requestDeletionMutation = useRequestDeletion();
|
||||
|
||||
// Clear form when dialog closes
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setPassword('');
|
||||
setConfirmationText('');
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!password || confirmationText !== 'DELETE') {
|
||||
if (confirmationText !== 'DELETE') {
|
||||
return;
|
||||
}
|
||||
|
||||
await requestDeletionMutation.mutateAsync({ password, confirmationText });
|
||||
await requestDeletionMutation.mutateAsync({ confirmationText });
|
||||
onClose();
|
||||
};
|
||||
|
||||
const isValid = password.length > 0 && confirmationText === 'DELETE';
|
||||
const isValid = confirmationText === 'DELETE';
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
@@ -61,17 +59,6 @@ export const DeleteAccountDialog: React.FC<DeleteAccountDialogProps> = ({ open,
|
||||
</Alert>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<TextField
|
||||
label="Password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
fullWidth
|
||||
required
|
||||
autoComplete="current-password"
|
||||
helperText="Enter your password to confirm"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Type DELETE to confirm"
|
||||
value={confirmationText}
|
||||
|
||||
@@ -11,14 +11,12 @@ interface DeleteAccountModalProps {
|
||||
}
|
||||
|
||||
export const DeleteAccountModal: React.FC<DeleteAccountModalProps> = ({ isOpen, onClose }) => {
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmationText, setConfirmationText] = useState('');
|
||||
const requestDeletionMutation = useRequestDeletion();
|
||||
|
||||
// Clear form when modal closes
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setPassword('');
|
||||
setConfirmationText('');
|
||||
}
|
||||
}, [isOpen]);
|
||||
@@ -26,15 +24,15 @@ export const DeleteAccountModal: React.FC<DeleteAccountModalProps> = ({ isOpen,
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!password || confirmationText !== 'DELETE') {
|
||||
if (confirmationText !== 'DELETE') {
|
||||
return;
|
||||
}
|
||||
|
||||
await requestDeletionMutation.mutateAsync({ password, confirmationText });
|
||||
await requestDeletionMutation.mutateAsync({ confirmationText });
|
||||
onClose();
|
||||
};
|
||||
|
||||
const isValid = password.length > 0 && confirmationText === 'DELETE';
|
||||
const isValid = confirmationText === 'DELETE';
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
@@ -50,23 +48,6 @@ export const DeleteAccountModal: React.FC<DeleteAccountModalProps> = ({ isOpen,
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Password Input */}
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Enter your password"
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-red-500"
|
||||
style={{ fontSize: '16px', minHeight: '44px' }}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
<p className="text-xs text-slate-500 mt-1">Enter your password to confirm</p>
|
||||
</div>
|
||||
|
||||
{/* Confirmation Input */}
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||
|
||||
@@ -25,7 +25,6 @@ export interface DeletionStatus {
|
||||
}
|
||||
|
||||
export interface RequestDeletionRequest {
|
||||
password: string;
|
||||
confirmationText: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user