14 KiB
14 KiB
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
- Real Name Validation: Current system allows any displayName, but we need to enforce real names
- Modern Auth Features: No password reset, magic links, or modern recovery flows
- Production-Ready Demo Login: Current demo uses cookies but needs proper integration
- Proper Route Protection: Website middleware only checks app mode, not authentication status
- Enhanced Error Handling: Need better validation and user-friendly error messages
- Security Hardening: Need to ensure all endpoints are properly protected
Enhanced Architecture Design
1. Domain Layer Changes
User Entity Updates
// 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 resetEmailVerificationToken: For email verification (future)PasswordResetRequest: Entity for tracking reset requests
New Repositories
IMagicLinkRepository: Store and validate magic linksIPasswordResetRepository: Track password reset requests
2. Application Layer Changes
New Use Cases
// 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
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
@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
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
@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
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
// 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
// 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)
@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
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)
// 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
// 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
- Security: All protected routes require authentication
- User Experience: Clear validation messages, helpful error states
- Developer Experience: Easy demo login, clear separation of concerns
- Maintainability: Clean architecture, well-tested, documented
- 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