181 lines
7.5 KiB
Markdown
181 lines
7.5 KiB
Markdown
# Clean Architecture Compliant Auth Layer Design
|
|
|
|
## Current State Analysis (Diagnosis)
|
|
- Existing [`core/identity/index.ts](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](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](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.
|
|
|
|
```mermaid
|
|
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
|
|
```ts
|
|
// 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
|
|
```ts
|
|
// 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
|
|
```ts
|
|
// 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)
|
|
```ts
|
|
// apps/website/lib/auth/AuthService.ts
|
|
import { injectable, inject } from 'di'; // assume
|
|
import { LoginUseCase } from '@core/identity';
|
|
import type { LoginDto } from '@core/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
|
|
```ts
|
|
// 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
|
|
```tsx
|
|
// 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.
|