wip
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
import type { AuthProviderDTO } from './AuthProviderDTO';
|
||||
|
||||
export interface AuthCallbackCommandDTO {
|
||||
provider: AuthProviderDTO;
|
||||
code: string;
|
||||
state: string;
|
||||
returnTo?: string;
|
||||
}
|
||||
1
packages/identity/application/dto/AuthProviderDTO.ts
Normal file
1
packages/identity/application/dto/AuthProviderDTO.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type AuthProviderDTO = 'IRACING_DEMO';
|
||||
8
packages/identity/application/dto/AuthSessionDTO.ts
Normal file
8
packages/identity/application/dto/AuthSessionDTO.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { AuthenticatedUserDTO } from './AuthenticatedUserDTO';
|
||||
|
||||
export interface AuthSessionDTO {
|
||||
user: AuthenticatedUserDTO;
|
||||
issuedAt: number;
|
||||
expiresAt: number;
|
||||
token: string;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface AuthenticatedUserDTO {
|
||||
id: string;
|
||||
displayName: string;
|
||||
email?: string;
|
||||
iracingCustomerId?: string;
|
||||
primaryDriverId?: string;
|
||||
avatarUrl?: string;
|
||||
}
|
||||
4
packages/identity/application/dto/IracingAuthStateDTO.ts
Normal file
4
packages/identity/application/dto/IracingAuthStateDTO.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface IracingAuthStateDTO {
|
||||
state: string;
|
||||
returnTo?: string;
|
||||
}
|
||||
6
packages/identity/application/dto/StartAuthCommandDTO.ts
Normal file
6
packages/identity/application/dto/StartAuthCommandDTO.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { AuthProviderDTO } from './AuthProviderDTO';
|
||||
|
||||
export interface StartAuthCommandDTO {
|
||||
provider: AuthProviderDTO;
|
||||
returnTo?: string;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { AuthenticatedUserDTO } from '../dto/AuthenticatedUserDTO';
|
||||
import type { AuthCallbackCommandDTO } from '../dto/AuthCallbackCommandDTO';
|
||||
import type { StartAuthCommandDTO } from '../dto/StartAuthCommandDTO';
|
||||
|
||||
export interface IdentityProviderPort {
|
||||
startAuth(command: StartAuthCommandDTO): Promise<{ redirectUrl: string; state: string }>;
|
||||
completeAuth(command: AuthCallbackCommandDTO): Promise<AuthenticatedUserDTO>;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { AuthenticatedUserDTO } from '../dto/AuthenticatedUserDTO';
|
||||
import type { AuthSessionDTO } from '../dto/AuthSessionDTO';
|
||||
|
||||
export interface IdentitySessionPort {
|
||||
getCurrentSession(): Promise<AuthSessionDTO | null>;
|
||||
createSession(user: AuthenticatedUserDTO): Promise<AuthSessionDTO>;
|
||||
clearSession(): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { AuthSessionDTO } from '../dto/AuthSessionDTO';
|
||||
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
|
||||
|
||||
export class GetCurrentUserSessionUseCase {
|
||||
private readonly sessionPort: IdentitySessionPort;
|
||||
|
||||
constructor(sessionPort: IdentitySessionPort) {
|
||||
this.sessionPort = sessionPort;
|
||||
}
|
||||
|
||||
async execute(): Promise<AuthSessionDTO | null> {
|
||||
return this.sessionPort.getCurrentSession();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { AuthCallbackCommandDTO } from '../dto/AuthCallbackCommandDTO';
|
||||
import type { AuthSessionDTO } from '../dto/AuthSessionDTO';
|
||||
import type { AuthenticatedUserDTO } from '../dto/AuthenticatedUserDTO';
|
||||
import type { IdentityProviderPort } from '../ports/IdentityProviderPort';
|
||||
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
|
||||
|
||||
export class HandleAuthCallbackUseCase {
|
||||
private readonly provider: IdentityProviderPort;
|
||||
private readonly sessionPort: IdentitySessionPort;
|
||||
|
||||
constructor(provider: IdentityProviderPort, sessionPort: IdentitySessionPort) {
|
||||
this.provider = provider;
|
||||
this.sessionPort = sessionPort;
|
||||
}
|
||||
|
||||
async execute(command: AuthCallbackCommandDTO): Promise<AuthSessionDTO> {
|
||||
const user: AuthenticatedUserDTO = await this.provider.completeAuth(command);
|
||||
const session = await this.sessionPort.createSession(user);
|
||||
return session;
|
||||
}
|
||||
}
|
||||
13
packages/identity/application/use-cases/LogoutUseCase.ts
Normal file
13
packages/identity/application/use-cases/LogoutUseCase.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
|
||||
|
||||
export class LogoutUseCase {
|
||||
private readonly sessionPort: IdentitySessionPort;
|
||||
|
||||
constructor(sessionPort: IdentitySessionPort) {
|
||||
this.sessionPort = sessionPort;
|
||||
}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
await this.sessionPort.clearSession();
|
||||
}
|
||||
}
|
||||
14
packages/identity/application/use-cases/StartAuthUseCase.ts
Normal file
14
packages/identity/application/use-cases/StartAuthUseCase.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { StartAuthCommandDTO } from '../dto/StartAuthCommandDTO';
|
||||
import type { IdentityProviderPort } from '../ports/IdentityProviderPort';
|
||||
|
||||
export class StartAuthUseCase {
|
||||
private readonly provider: IdentityProviderPort;
|
||||
|
||||
constructor(provider: IdentityProviderPort) {
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
async execute(command: StartAuthCommandDTO): Promise<{ redirectUrl: string; state: string }> {
|
||||
return this.provider.startAuth(command);
|
||||
}
|
||||
}
|
||||
73
packages/identity/domain/entities/User.ts
Normal file
73
packages/identity/domain/entities/User.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import type { EmailValidationResult } from '../value-objects/EmailAddress';
|
||||
import { validateEmail } from '../value-objects/EmailAddress';
|
||||
import { UserId } from '../value-objects/UserId';
|
||||
|
||||
export interface UserProps {
|
||||
id: UserId;
|
||||
displayName: string;
|
||||
email?: string;
|
||||
iracingCustomerId?: string;
|
||||
primaryDriverId?: string;
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
export class User {
|
||||
private readonly id: UserId;
|
||||
private displayName: string;
|
||||
private email?: string;
|
||||
private iracingCustomerId?: string;
|
||||
private primaryDriverId?: string;
|
||||
private avatarUrl?: string;
|
||||
|
||||
private constructor(props: UserProps) {
|
||||
if (!props.displayName || !props.displayName.trim()) {
|
||||
throw new Error('User displayName cannot be empty');
|
||||
}
|
||||
|
||||
this.id = props.id;
|
||||
this.displayName = props.displayName.trim();
|
||||
this.email = props.email;
|
||||
this.iracingCustomerId = props.iracingCustomerId;
|
||||
this.primaryDriverId = props.primaryDriverId;
|
||||
this.avatarUrl = props.avatarUrl;
|
||||
}
|
||||
|
||||
public static create(props: UserProps): User {
|
||||
if (props.email) {
|
||||
const result: EmailValidationResult = validateEmail(props.email);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
return new User({
|
||||
...props,
|
||||
email: result.email,
|
||||
});
|
||||
}
|
||||
|
||||
return new User(props);
|
||||
}
|
||||
|
||||
public getId(): UserId {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public getDisplayName(): string {
|
||||
return this.displayName;
|
||||
}
|
||||
|
||||
public getEmail(): string | undefined {
|
||||
return this.email;
|
||||
}
|
||||
|
||||
public getIracingCustomerId(): string | undefined {
|
||||
return this.iracingCustomerId;
|
||||
}
|
||||
|
||||
public getPrimaryDriverId(): string | undefined {
|
||||
return this.primaryDriverId;
|
||||
}
|
||||
|
||||
public getAvatarUrl(): string | undefined {
|
||||
return this.avatarUrl;
|
||||
}
|
||||
}
|
||||
22
packages/identity/domain/value-objects/UserId.ts
Normal file
22
packages/identity/domain/value-objects/UserId.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export class UserId {
|
||||
private readonly value: string;
|
||||
|
||||
private constructor(value: string) {
|
||||
if (!value || !value.trim()) {
|
||||
throw new Error('UserId cannot be empty');
|
||||
}
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static fromString(value: string): UserId {
|
||||
return new UserId(value);
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public equals(other: UserId): boolean {
|
||||
return this.value === other.value;
|
||||
}
|
||||
}
|
||||
@@ -1 +1,15 @@
|
||||
export * from './domain/value-objects/EmailAddress';
|
||||
export * from './domain/value-objects/EmailAddress';
|
||||
export * from './domain/value-objects/UserId';
|
||||
export * from './domain/entities/User';
|
||||
|
||||
export * from './application/dto/AuthenticatedUserDTO';
|
||||
export * from './application/dto/AuthSessionDTO';
|
||||
export * from './application/dto/AuthCallbackCommandDTO';
|
||||
export * from './application/dto/StartAuthCommandDTO';
|
||||
export * from './application/dto/AuthProviderDTO';
|
||||
export * from './application/dto/IracingAuthStateDTO';
|
||||
|
||||
export * from './application/use-cases/StartAuthUseCase';
|
||||
export * from './application/use-cases/HandleAuthCallbackUseCase';
|
||||
export * from './application/use-cases/GetCurrentUserSessionUseCase';
|
||||
export * from './application/use-cases/LogoutUseCase';
|
||||
@@ -0,0 +1,50 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import { createStaticRacingSeed } from '../../../testing-support';
|
||||
import type { IdentityProviderPort } from '../../application/ports/IdentityProviderPort';
|
||||
import type { StartAuthCommandDTO } from '../../application/dto/StartAuthCommandDTO';
|
||||
import type { AuthCallbackCommandDTO } from '../../application/dto/AuthCallbackCommandDTO';
|
||||
import type { AuthenticatedUserDTO } from '../../application/dto/AuthenticatedUserDTO';
|
||||
|
||||
export class IracingDemoIdentityProviderAdapter implements IdentityProviderPort {
|
||||
private readonly seedDriverId: string;
|
||||
|
||||
constructor() {
|
||||
const seed = createStaticRacingSeed(42);
|
||||
this.seedDriverId = seed.drivers[0]?.id ?? 'driver-1';
|
||||
}
|
||||
|
||||
async startAuth(command: StartAuthCommandDTO): Promise<{ redirectUrl: string; state: string }> {
|
||||
const state = randomUUID();
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.set('code', 'dummy-code');
|
||||
params.set('state', state);
|
||||
if (command.returnTo) {
|
||||
params.set('returnTo', command.returnTo);
|
||||
}
|
||||
|
||||
return {
|
||||
redirectUrl: `/auth/iracing/callback?${params.toString()}`,
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
async completeAuth(command: AuthCallbackCommandDTO): Promise<AuthenticatedUserDTO> {
|
||||
if (!command.code) {
|
||||
throw new Error('Missing auth code');
|
||||
}
|
||||
if (!command.state) {
|
||||
throw new Error('Missing auth state');
|
||||
}
|
||||
|
||||
const user: AuthenticatedUserDTO = {
|
||||
id: 'demo-user',
|
||||
displayName: 'GridPilot Demo Driver',
|
||||
iracingCustomerId: '000000',
|
||||
primaryDriverId: this.seedDriverId,
|
||||
avatarUrl: `/api/avatar/${this.seedDriverId}`,
|
||||
};
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { cookies } from 'next/headers';
|
||||
import { randomUUID } from 'crypto';
|
||||
import type { AuthenticatedUserDTO } from '../../application/dto/AuthenticatedUserDTO';
|
||||
import type { AuthSessionDTO } from '../../application/dto/AuthSessionDTO';
|
||||
import type { IdentitySessionPort } from '../../application/ports/IdentitySessionPort';
|
||||
|
||||
const SESSION_COOKIE = 'gp_demo_session';
|
||||
|
||||
function parseCookieValue(raw: string | undefined): AuthSessionDTO | null {
|
||||
if (!raw) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as AuthSessionDTO;
|
||||
if (!parsed.expiresAt || Date.now() > parsed.expiresAt) {
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function serializeSession(session: AuthSessionDTO): string {
|
||||
return JSON.stringify(session);
|
||||
}
|
||||
|
||||
export class CookieIdentitySessionAdapter implements IdentitySessionPort {
|
||||
async getCurrentSession(): Promise<AuthSessionDTO | null> {
|
||||
const store = await cookies();
|
||||
const raw = store.get(SESSION_COOKIE)?.value;
|
||||
return parseCookieValue(raw);
|
||||
}
|
||||
|
||||
async createSession(user: AuthenticatedUserDTO): Promise<AuthSessionDTO> {
|
||||
const now = Date.now();
|
||||
const expiresAt = now + 24 * 60 * 60 * 1000;
|
||||
|
||||
const session: AuthSessionDTO = {
|
||||
user,
|
||||
issuedAt: now,
|
||||
expiresAt,
|
||||
token: randomUUID(),
|
||||
};
|
||||
|
||||
const store = await cookies();
|
||||
store.set(SESSION_COOKIE, serializeSession(session), {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
});
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
async clearSession(): Promise<void> {
|
||||
const store = await cookies();
|
||||
store.delete(SESSION_COOKIE);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,8 @@
|
||||
"types": "./index.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./domain/*": "./domain/*"
|
||||
"./domain/*": "./domain/*",
|
||||
"./application/*": "./application/*"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^3.25.76"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"outDir": "dist",
|
||||
"declaration": true,
|
||||
"declarationMap": false
|
||||
|
||||
Reference in New Issue
Block a user