Files
gridpilot.gg/plans/auth-clean-arch.md
2025-12-15 18:34:20 +01:00

7.5 KiB

Clean Architecture Compliant Auth Layer Design

Current State Analysis (Diagnosis)

  • Existing `core/identity/index.ts exports User entity, IUserRepository port, but lacks credential-based auth ports (IAuthRepository), value objects (PasswordHash), and use cases (LoginUseCase, SignupUseCase).
  • iRacing-specific auth use cases present (StartAuthUseCase, HandleAuthCallbackUseCase), but no traditional email/password flows.
  • No domain services for password hashing/validation.
  • In-memory impls limited to ratings/achievements; missing User/Auth repo impls.
  • `apps/website/lib/auth/InMemoryAuthService.ts (visible) embeds business logic, violating dependency inversion.
  • App-layer AuthService exists but thick; no thin delivery via DI-injected use cases.
  • No AuthContext integration with clean AuthService.
  • DI setup in `apps/website/lib/di-tokens.ts etc. needs auth bindings.

Architectural Plan

  1. Add PasswordHash value object in core/identity/domain/value-objects/PasswordHash.ts.
  2. Create IAuthRepository port in core/identity/domain/repositories/IAuthRepository.ts with findByCredentials(email: EmailAddress, passwordHash: PasswordHash): Promise<User | null>, save(user: User): Promise<void>.
  3. Extend IUserRepository if needed for non-auth user ops.
  4. Implement InMemoryUserRepository in adapters/identity/inmem/InMemoryUserRepository.ts satisfying IUserRepository & IAuthRepository.
  5. Add domain service PasswordHashingService in core/identity/domain/services/PasswordHashingService.ts (interface + dummy impl).
  6. Create LoginUseCase in core/identity/application/use-cases/LoginUseCase.ts orchestrating repo find + hashing service.
  7. Create SignupUseCase in core/identity/application/use-cases/SignupUseCase.ts validating, hashing, save via repos.
  8. Create GetUserUseCase in core/identity/application/use-cases/GetUserUseCase.ts via IUserRepository.
  9. Refactor apps/website/lib/auth/AuthService.ts as thin adapter: DTO -> use case calls via injected deps.
  10. Update apps/website/lib/auth/AuthContext.tsx to provide/use AuthService via React Context.
  11. Add DI tokens in apps/website/lib/di-tokens.ts: AUTH_REPOSITORY_TOKEN, USER_REPOSITORY_TOKEN, LOGIN_USE_CASE_TOKEN, etc.
  12. Bind in apps/website/lib/di-config.ts / di-container.ts: ports -> inmem impls, use cases -> deps, AuthService -> use cases.

Summary

Layer auth into domain entities/VOs/services, application use cases/ports, infrastructure adapters (inmem), thin app delivery (AuthService) wired via DI. Coexists with existing iRacing provider auth.

Design Overview

Follows strict Clean Architecture:

  • Entities: User with EmailAddress, PasswordHash VOs.
  • Use Cases: Pure orchestrators, depend on ports/services.
  • Ports: IAuthRepository (credential ops), IUserRepository (user data).
  • Adapters: Inmem impls.
  • Delivery: AuthService maps HTTP/JS DTOs to use cases.
  • DI: Inversion via tokens/container.
graph TB
    DTO[DTOs<br/>apps/website/lib/auth] --> AuthService[AuthService<br/>apps/website/lib/auth]
    AuthService --> LoginUC[LoginUseCase<br/>core/identity/application]
    AuthService --> SignupUC[SignupUseCase]
    LoginUC --> IAuthRepo[IAuthRepository<br/>core/identity/domain]
    SignupUC --> PasswordSvc[PasswordHashingService<br/>core/identity/domain]
    IAuthRepo --> InMemRepo[InMemoryUserRepository<br/>adapters/identity/inmem]
    AuthService -.-> DI[DI Container<br/>apps/website/lib/di-*]
    AuthContext[AuthContext.tsx] --> AuthService

Files Structure

core/identity/
├── domain/
│   ├── value-objects/PasswordHash.ts (new)
│   ├── entities/User.ts (extend if needed)
│   ├── repositories/IAuthRepository.ts (new)
│   └── services/PasswordHashingService.ts (new)
├── application/
│   └── use-cases/LoginUseCase.ts (new)
│             SignupUseCase.ts (new)
│             GetUserUseCase.ts (new)
adapters/identity/inmem/
├── InMemoryUserRepository.ts (new)
apps/website/lib/auth/
├── AuthService.ts (refactor)
└── AuthContext.tsx (update)
apps/website/lib/
├── di-tokens.ts (update)
├── di-config.ts (update)
└── di-container.ts (update)

Code Snippets

PasswordHash VO

// core/identity/domain/value-objects/PasswordHash.ts
import { ValueObject } from '../../../shared/domain/ValueObject'; // assume shared

export class PasswordHash extends ValueObject<string> {
  static create(plain: string): PasswordHash {
    // dummy bcrypt hash
    return new PasswordHash(btoa(plain)); // prod: use bcrypt
  }

  verify(plain: string): boolean {
    return btoa(plain) === this.value;
  }
}

IAuthRepository Port

// core/identity/domain/repositories/IAuthRepository.ts
import { UserId, EmailAddress } from '../value-objects';
import { User } from '../entities/User';
import { PasswordHash } from '../value-objects/PasswordHash';

export interface IAuthRepository {
  findByCredentials(email: EmailAddress, passwordHash: PasswordHash): Promise<User | null>;
  save(user: User): Promise<void>;
}

LoginUseCase

// core/identity/application/use-cases/LoginUseCase.ts
import { Injectable } from 'di'; // assume
import { IAuthRepository } from '../../domain/repositories/IAuthRepository';
import { PasswordHash } from '../../domain/value-objects/PasswordHash';
import { EmailAddress } from '../../domain/value-objects/EmailAddress';
import { User } from '../../domain/entities/User';

export class LoginUseCase {
  constructor(
    private authRepo: IAuthRepository
  ) {}

  async execute(email: string, password: string): Promise<User> {
    const emailVO = EmailAddress.create(email);
    const passwordHash = PasswordHash.create(password);
    const user = await this.authRepo.findByCredentials(emailVO, passwordHash);
    if (!user) throw new Error('Invalid credentials');
    return user;
  }
}

AuthService (Thin Adapter)

// apps/website/lib/auth/AuthService.ts
import { injectable, inject } from 'di'; // assume
import { LoginUseCase } from '@gridpilot/identity';
import type { LoginDto } from '@gridpilot/identity/application/dto'; // define DTO

@injectable()
export class AuthService {
  constructor(
    @inject(LoginUseCase_TOKEN) private loginUC: LoginUseCase
  ) {}

  async login(credentials: {email: string, password: string}) {
    return await this.loginUC.execute(credentials.email, credentials.password);
  }
  // similar for signup, getUser
}

DI Tokens Update

// apps/website/lib/di-tokens.ts
export const LOGIN_USE_CASE_TOKEN = Symbol('LoginUseCase');
export const AUTH_REPOSITORY_TOKEN = Symbol('IAuthRepository');
// etc.

AuthContext Usage Example

// apps/website/lib/auth/AuthContext.tsx
import { createContext, useContext } from 'react';
import { AuthService } from './AuthService';
import { diContainer } from '../di-container';

const AuthContext = createContext<AuthService>(null!);

export function AuthProvider({ children }) {
  const authService = diContainer.resolve(AuthService);
  return <AuthContext.Provider value={authService}>{children}</AuthContext.Provider>;
}

export const useAuth = () => useContext(AuthContext);

Implementation Notes

  • Update core/identity/index.ts exports for new modules.
  • Update core/identity/package.json exports if needed.
  • Use dummy hashing for dev; prod adapter swaps repo impl.
  • No business logic in app/website: all in core use cases.
  • Coexists with iRacing auth: separate use cases/services.