Files
gridpilot.gg/plans/auth-finalization-plan.md
2025-12-31 19:55:43 +01:00

560 lines
14 KiB
Markdown

# Auth Solution Finalization Plan
## Overview
This plan outlines the comprehensive enhancement of the GridPilot authentication system to meet production requirements while maintaining clean architecture principles and supporting both in-memory and TypeORM implementations.
## Current State Analysis
### ✅ What's Working
- Clean Architecture with proper separation of concerns
- Email/password signup and login
- iRacing OAuth flow (placeholder)
- Session management with cookies
- Basic route protection (mode-based)
- Dev tools overlay with demo login
- In-memory and TypeORM persistence adapters
### ❌ What's Missing/Needs Enhancement
1. **Real Name Validation**: Current system allows any displayName, but we need to enforce real names
2. **Modern Auth Features**: No password reset, magic links, or modern recovery flows
3. **Production-Ready Demo Login**: Current demo uses cookies but needs proper integration
4. **Proper Route Protection**: Website middleware only checks app mode, not authentication status
5. **Enhanced Error Handling**: Need better validation and user-friendly error messages
6. **Security Hardening**: Need to ensure all endpoints are properly protected
## Enhanced Architecture Design
### 1. Domain Layer Changes
#### User Entity Updates
```typescript
// Enhanced validation for real names
export class User {
// ... existing properties
private validateDisplayName(displayName: string): void {
const trimmed = displayName.trim();
// Must be a real name (no nicknames)
if (trimmed.length < 2) {
throw new Error('Name must be at least 2 characters');
}
// No special characters except basic punctuation
if (!/^[A-Za-z\s\-']{2,50}$/.test(trimmed)) {
throw new Error('Name can only contain letters, spaces, hyphens, and apostrophes');
}
// No common nickname patterns
const nicknamePatterns = [/^user/i, /^test/i, /^[a-z0-9_]+$/i];
if (nicknamePatterns.some(pattern => pattern.test(trimmed))) {
throw new Error('Please use your real name, not a nickname');
}
// Capitalize first letter of each word
this.displayName = trimmed.replace(/\b\w/g, l => l.toUpperCase());
}
}
```
#### New Value Objects
- `MagicLinkToken`: Secure token for password reset
- `EmailVerificationToken`: For email verification (future)
- `PasswordResetRequest`: Entity for tracking reset requests
#### New Repositories
- `IMagicLinkRepository`: Store and validate magic links
- `IPasswordResetRepository`: Track password reset requests
### 2. Application Layer Changes
#### New Use Cases
```typescript
// Forgot Password Use Case
export class ForgotPasswordUseCase {
async execute(email: string): Promise<Result<void, ApplicationError>> {
// 1. Validate email exists
// 2. Generate secure token
// 3. Store token with expiration
// 4. Send magic link email (or return link for dev)
// 5. Rate limiting
}
}
// Reset Password Use Case
export class ResetPasswordUseCase {
async execute(token: string, newPassword: string): Promise<Result<void, ApplicationError>> {
// 1. Validate token
// 2. Check expiration
// 3. Update password
// 4. Invalidate token
// 5. Clear other sessions
}
}
// Demo Login Use Case (Dev Only)
export class DemoLoginUseCase {
async execute(role: 'driver' | 'sponsor'): Promise<Result<AuthSession, ApplicationError>> {
// 1. Check environment (dev only)
// 2. Create demo user if doesn't exist
// 3. Generate session
// 4. Return session
}
}
```
#### Enhanced Signup Use Case
```typescript
export class SignupUseCase {
// Add real name validation
// Add email format validation
// Add password strength requirements
// Optional: Email verification flow
}
```
### 3. API Layer Changes
#### New Auth Endpoints
```typescript
@Public()
@Controller('auth')
export class AuthController {
// Existing:
// POST /auth/signup
// POST /auth/login
// GET /auth/session
// POST /auth/logout
// GET /auth/iracing/start
// GET /auth/iracing/callback
// New:
// POST /auth/forgot-password
// POST /auth/reset-password
// POST /auth/demo-login (dev only)
// POST /auth/verify-email (future)
}
```
#### Enhanced DTOs
```typescript
export class SignupParamsDTO {
@ApiProperty()
@IsEmail()
email: string;
@ApiProperty()
@IsString()
@MinLength(8)
@Matches(/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, {
message: 'Password must contain uppercase, lowercase, and number'
})
password: string;
@ApiProperty()
@IsString()
@Matches(/^[A-Za-z\s\-']{2,50}$/, {
message: 'Please use your real name (letters, spaces, hyphens only)'
})
displayName: string;
}
export class ForgotPasswordDTO {
@ApiProperty()
@IsEmail()
email: string;
}
export class ResetPasswordDTO {
@ApiProperty()
@IsString()
token: string;
@ApiProperty()
@IsString()
@MinLength(8)
newPassword: string;
}
export class DemoLoginDTO {
@ApiProperty({ enum: ['driver', 'sponsor'] })
role: 'driver' | 'sponsor';
}
```
#### Enhanced Guards
```typescript
@Injectable()
export class AuthenticationGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
// Check session
const session = await this.sessionPort.getCurrentSession();
if (!session?.user?.id) {
throw new UnauthorizedException('Authentication required');
}
// Attach user to request
request.user = { userId: session.user.id };
return true;
}
}
@Injectable()
export class ProductionGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
// Block demo login in production
if (process.env.NODE_ENV === 'production') {
const request = context.switchToHttp().getRequest();
if (request.path === '/auth/demo-login') {
throw new ForbiddenException('Demo login not available in production');
}
}
return true;
}
}
```
### 4. Website Layer Changes
#### Enhanced Middleware
```typescript
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Public routes (always accessible)
const publicRoutes = [
'/',
'/auth/login',
'/auth/signup',
'/auth/forgot-password',
'/auth/reset-password',
'/auth/iracing',
'/auth/iracing/start',
'/auth/iracing/callback',
'/api/auth/signup',
'/api/auth/login',
'/api/auth/forgot-password',
'/api/auth/reset-password',
'/api/auth/demo-login', // dev only
'/api/auth/session',
'/api/auth/logout'
];
// Protected routes (require authentication)
const protectedRoutes = [
'/dashboard',
'/profile',
'/leagues',
'/races',
'/teams',
'/sponsor',
'/onboarding'
];
// Check if route is public
if (publicRoutes.includes(pathname)) {
return NextResponse.next();
}
// Check if route is protected
if (protectedRoutes.some(route => pathname.startsWith(route))) {
// Verify authentication by calling API
const response = NextResponse.next();
// Add a header that can be checked by client components
// This is a simple approach - in production, consider server-side session validation
return response;
}
// Allow other routes
return NextResponse.next();
}
```
#### Client-Side Route Protection
```typescript
// Higher-order component for route protection
export function withAuth<P extends object>(Component: React.ComponentType<P>) {
return function ProtectedComponent(props: P) {
const { session, loading } = useAuth();
const router = useRouter();
useEffect(() => {
if (!loading && !session) {
router.push(`/auth/login?returnTo=${encodeURIComponent(window.location.pathname)}`);
}
}, [session, loading, router]);
if (loading) {
return <LoadingScreen />;
}
if (!session) {
return null; // or redirecting indicator
}
return <Component {...props} />;
};
}
// Hook for protected data fetching
export function useProtectedData<T>(fetcher: () => Promise<T>) {
const { session, loading } = useAuth();
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
if (!loading && !session) {
setError(new Error('Authentication required'));
return;
}
if (session) {
fetcher()
.then(setData)
.catch(setError);
}
}, [session, loading, fetcher]);
return { data, error, loading };
}
```
#### Enhanced Auth Pages
- **Login**: Add "Forgot Password" link
- **Signup**: Add real name validation with helpful hints
- **New**: Forgot Password page
- **New**: Reset Password page
- **New**: Magic Link landing page
#### Enhanced Dev Tools
```typescript
// Add proper demo login flow
const handleDemoLogin = async (role: 'driver' | 'sponsor') => {
try {
const response = await fetch('/api/auth/demo-login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ role })
});
if (!response.ok) throw new Error('Demo login failed');
// Refresh session
await refreshSession();
// Redirect based on role
if (role === 'sponsor') {
router.push('/sponsor/dashboard');
} else {
router.push('/dashboard');
}
} catch (error) {
console.error('Demo login failed:', error);
}
};
```
### 5. Persistence Layer Changes
#### Enhanced Repositories
Both InMemory and TypeORM implementations need to support:
- Storing magic link tokens with expiration
- Password reset request tracking
- Rate limiting (failed login attempts)
#### Database Schema Updates (TypeORM)
```typescript
@Entity()
export class MagicLinkToken {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
userId: string;
@Column()
token: string;
@Column()
expiresAt: Date;
@Column({ default: false })
used: boolean;
@CreateDateColumn()
createdAt: Date;
}
@Entity()
export class PasswordResetRequest {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
email: string;
@Column()
token: string;
@Column()
expiresAt: Date;
@Column({ default: false })
used: boolean;
@Column({ default: 0 })
attemptCount: number;
@CreateDateColumn()
createdAt: Date;
}
```
### 6. Security & Validation
#### Rate Limiting
- Implement rate limiting on auth endpoints
- Track failed login attempts
- Lock accounts after too many failures
#### Input Validation
- Email format validation
- Password strength requirements
- Real name validation
- Token format validation
#### Environment Detection
```typescript
export function isDevelopment(): boolean {
return process.env.NODE_ENV === 'development';
}
export function isProduction(): boolean {
return process.env.NODE_ENV === 'production';
}
export function allowDemoLogin(): boolean {
return isDevelopment() || process.env.ALLOW_DEMO_LOGIN === 'true';
}
```
### 7. Integration Points
#### API Routes (Next.js)
```typescript
// app/api/auth/forgot-password/route.ts
export async function POST(request: Request) {
// Validate input
// Call ForgotPasswordUseCase
// Return appropriate response
}
// app/api/auth/reset-password/route.ts
export async function POST(request: Request) {
// Validate token
// Call ResetPasswordUseCase
// Return success/error
}
// app/api/auth/demo-login/route.ts (dev only)
export async function POST(request: Request) {
if (!allowDemoLogin()) {
return NextResponse.json({ error: 'Not available' }, { status: 403 });
}
// Call DemoLoginUseCase
}
```
#### Website Components
```typescript
// ProtectedPageWrapper.tsx
export function ProtectedPageWrapper({ children }: { children: React.ReactNode }) {
const { session, loading } = useAuth();
const router = useRouter();
if (loading) return <LoadingScreen />;
if (!session) {
router.push(`/auth/login?returnTo=${encodeURIComponent(window.location.pathname)}`);
return null;
}
return <>{children}</>;
}
// AuthForm.tsx - Reusable form with validation
// MagicLinkNotification.tsx - Show success message
// PasswordStrengthMeter.tsx - Visual feedback
```
## Implementation Phases
### Phase 1: Core Domain & Use Cases
- [ ] Update User entity with real name validation
- [ ] Create new use cases (ForgotPassword, ResetPassword, DemoLogin)
- [ ] Create new repositories/interfaces
- [ ] Add new value objects
### Phase 2: API Layer
- [ ] Add new auth endpoints
- [ ] Create new DTOs with validation
- [ ] Update existing endpoints with enhanced validation
- [ ] Add guards and middleware
### Phase 3: Persistence
- [ ] Update InMemory repositories
- [ ] Update TypeORM repositories
- [ ] Add database migrations (if needed)
- [ ] Implement rate limiting storage
### Phase 4: Website Integration
- [ ] Update middleware for proper route protection
- [ ] Create new auth pages (forgot password, reset)
- [ ] Enhance existing pages with validation
- [ ] Update dev tools overlay
- [ ] Add client-side route protection HOCs
### Phase 5: Testing & Documentation
- [ ] Write unit tests for new use cases
- [ ] Write integration tests for new endpoints
- [ ] Test both in-memory and TypeORM implementations
- [ ] Update API documentation
- [ ] Update architecture docs
## Key Requirements Checklist
### ✅ Must Work With
- [ ] InMemory implementation
- [ ] TypeORM implementation
- [ ] Dev tools overlay
- [ ] Existing session management
### ✅ Must Provide
- [ ] Demo login for dev (not production)
- [ ] Forgot password solution (modern approach)
- [ ] Real name validation (no nicknames)
- [ ] Proper website route protection
### ✅ Must Not Break
- [ ] Existing signup/login flow
- [ ] iRacing OAuth flow
- [ ] Existing tests
- [ ] Clean architecture principles
## Success Metrics
1. **Security**: All protected routes require authentication
2. **User Experience**: Clear validation messages, helpful error states
3. **Developer Experience**: Easy demo login, clear separation of concerns
4. **Maintainability**: Clean architecture, well-tested, documented
5. **Scalability**: Works with both in-memory and database persistence
## Notes
- The demo login should be clearly marked as development-only
- Magic links should have short expiration times (15-30 minutes)
- Consider adding email verification as a future enhancement
- Rate limiting should be configurable per environment
- All new features should follow the existing clean architecture patterns