refactor to adapters

This commit is contained in:
2025-12-15 18:34:20 +01:00
parent fc671482c8
commit c817d76092
145 changed files with 906 additions and 361 deletions

View File

@@ -0,0 +1,14 @@
import { User } from '../../domain/entities/User';
import { IUserRepository } from '../../domain/repositories/IUserRepository';
export class GetUserUseCase {
constructor(private userRepo: IUserRepository) {}
async execute(userId: string): Promise<User> {
const stored = await this.userRepo.findById(userId);
if (!stored) {
throw new Error('User not found');
}
return User.fromStored(stored);
}
}

View File

@@ -0,0 +1,29 @@
import { EmailAddress } from '../../domain/value-objects/EmailAddress';
import { User } from '../../domain/entities/User';
import { IAuthRepository } from '../../domain/repositories/IAuthRepository';
import { IPasswordHashingService } from '../../domain/services/PasswordHashingService';
/**
* Application Use Case: LoginUseCase
*
* Handles user login by verifying credentials.
*/
export class LoginUseCase {
constructor(
private authRepo: IAuthRepository,
private passwordService: IPasswordHashingService
) {}
async execute(email: string, password: string): Promise<User> {
const emailVO = EmailAddress.create(email);
const user = await this.authRepo.findByEmail(emailVO);
if (!user || !user.getPasswordHash()) {
throw new Error('Invalid credentials');
}
const isValid = await this.passwordService.verify(password, user.getPasswordHash()!.value);
if (!isValid) {
throw new Error('Invalid credentials');
}
return user;
}
}

View File

@@ -0,0 +1,41 @@
import { EmailAddress } from '../../domain/value-objects/EmailAddress';
import { UserId } from '../../domain/value-objects/UserId';
import { User } from '../../domain/entities/User';
import { IAuthRepository } from '../../domain/repositories/IAuthRepository';
import { IPasswordHashingService } from '../../domain/services/PasswordHashingService';
/**
* Application Use Case: SignupUseCase
*
* Handles user registration.
*/
export class SignupUseCase {
constructor(
private authRepo: IAuthRepository,
private passwordService: IPasswordHashingService
) {}
async execute(email: string, password: string, displayName: string): Promise<User> {
const emailVO = EmailAddress.create(email);
// Check if user already exists
const existingUser = await this.authRepo.findByEmail(emailVO);
if (existingUser) {
throw new Error('User already exists');
}
const hashedPassword = await this.passwordService.hash(password);
const passwordHash = await import('../../domain/value-objects/PasswordHash').then(m => m.PasswordHash.fromHash(hashedPassword));
const userId = UserId.create();
const user = User.create({
id: userId,
displayName,
email: emailVO.value,
passwordHash,
});
await this.authRepo.save(user);
return user;
}
}

View File

@@ -1,11 +1,14 @@
import type { EmailValidationResult } from '../types/EmailAddress';
import { validateEmail } from '../types/EmailAddress';
import { UserId } from '../value-objects/UserId';
import { PasswordHash } from '../value-objects/PasswordHash';
import { StoredUser } from '../repositories/IUserRepository';
export interface UserProps {
id: UserId;
displayName: string;
email?: string;
passwordHash?: PasswordHash;
iracingCustomerId?: string;
primaryDriverId?: string;
avatarUrl?: string;
@@ -15,6 +18,7 @@ export class User {
private readonly id: UserId;
private displayName: string;
private email: string | undefined;
private passwordHash: PasswordHash | undefined;
private iracingCustomerId: string | undefined;
private primaryDriverId: string | undefined;
private avatarUrl: string | undefined;
@@ -47,6 +51,22 @@ export class User {
return new User(props);
}
public static fromStored(stored: StoredUser): User {
const passwordHash = stored.passwordHash ? PasswordHash.fromHash(stored.passwordHash) : undefined;
const userProps: any = {
id: UserId.fromString(stored.id),
displayName: stored.displayName,
email: stored.email,
};
if (passwordHash) {
userProps.passwordHash = passwordHash;
}
if (stored.primaryDriverId) {
userProps.primaryDriverId = stored.primaryDriverId;
}
return new User(userProps);
}
public getId(): UserId {
return this.id;
}
@@ -59,6 +79,10 @@ export class User {
return this.email;
}
public getPasswordHash(): PasswordHash | undefined {
return this.passwordHash;
}
public getIracingCustomerId(): string | undefined {
return this.iracingCustomerId;
}

View File

@@ -0,0 +1,19 @@
import { EmailAddress } from '../value-objects/EmailAddress';
import { User } from '../entities/User';
/**
* Domain Repository: IAuthRepository
*
* Repository interface for authentication operations.
*/
export interface IAuthRepository {
/**
* Find user by email
*/
findByEmail(email: EmailAddress): Promise<User | null>;
/**
* Save a user
*/
save(user: User): Promise<void>;
}

View File

@@ -0,0 +1,26 @@
import { PasswordHash } from '../value-objects/PasswordHash';
/**
* Domain Service: PasswordHashingService
*
* Service for password hashing and verification.
*/
export interface IPasswordHashingService {
hash(plain: string): Promise<string>;
verify(plain: string, hash: string): Promise<boolean>;
}
/**
* Implementation using bcrypt via PasswordHash VO.
*/
export class PasswordHashingService implements IPasswordHashingService {
async hash(plain: string): Promise<string> {
const passwordHash = await PasswordHash.create(plain);
return passwordHash.value;
}
async verify(plain: string, hash: string): Promise<boolean> {
const passwordHash = PasswordHash.fromHash(hash);
return passwordHash.verify(plain);
}
}

View File

@@ -0,0 +1,41 @@
import bcrypt from 'bcrypt';
import type { IValueObject } from '@gridpilot/shared/domain';
export interface PasswordHashProps {
value: string;
}
/**
* Value Object: PasswordHash
*
* Wraps a bcrypt-hashed password string and provides verification.
*/
export class PasswordHash implements IValueObject<PasswordHashProps> {
public readonly props: PasswordHashProps;
private constructor(value: string) {
this.props = { value };
}
static async create(plain: string): Promise<PasswordHash> {
const saltRounds = 12;
const hash = await bcrypt.hash(plain, saltRounds);
return new PasswordHash(hash);
}
static fromHash(hash: string): PasswordHash {
return new PasswordHash(hash);
}
get value(): string {
return this.props.value;
}
async verify(plain: string): Promise<boolean> {
return bcrypt.compare(plain, this.props.value);
}
equals(other: IValueObject<PasswordHashProps>): boolean {
return this.props.value === other.props.value;
}
}

View File

@@ -1,3 +1,4 @@
import { v4 as uuidv4 } from 'uuid';
import type { IValueObject } from '@gridpilot/shared/domain';
export interface UserIdProps {
@@ -14,6 +15,10 @@ export class UserId implements IValueObject<UserIdProps> {
this.props = { value };
}
public static create(): UserId {
return new UserId(uuidv4());
}
public static fromString(value: string): UserId {
return new UserId(value);
}

View File

@@ -1,50 +0,0 @@
import { randomUUID } from 'crypto';
import { createStaticRacingSeed } from '@gridpilot/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;
}
}

View File

@@ -1,291 +0,0 @@
/**
* Infrastructure Adapter: InMemoryAchievementRepository
*
* In-memory implementation of IAchievementRepository
*/
import {
Achievement,
AchievementCategory,
DRIVER_ACHIEVEMENTS,
STEWARD_ACHIEVEMENTS,
ADMIN_ACHIEVEMENTS,
COMMUNITY_ACHIEVEMENTS,
} from '../../domain/entities/Achievement';
import { UserAchievement } from '../../domain/entities/UserAchievement';
import type { IAchievementRepository } from '../../domain/repositories/IAchievementRepository';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemoryAchievementRepository implements IAchievementRepository {
private achievements: Map<string, Achievement> = new Map();
private userAchievements: Map<string, UserAchievement> = new Map();
private readonly logger: ILogger;
constructor(logger: ILogger) {
this.logger = logger;
this.logger.info('InMemoryAchievementRepository initialized.');
// Seed with predefined achievements
this.seedAchievements();
}
private seedAchievements(): void {
this.logger.debug('Seeding predefined achievements.');
const allAchievements = [
...DRIVER_ACHIEVEMENTS,
...STEWARD_ACHIEVEMENTS,
...ADMIN_ACHIEVEMENTS,
...COMMUNITY_ACHIEVEMENTS,
];
for (const props of allAchievements) {
const achievement = Achievement.create(props);
this.achievements.set(achievement.id, achievement);
this.logger.debug(`Seeded achievement: ${achievement.id} (${achievement.name}).`);
}
this.logger.info(`Seeded ${allAchievements.length} predefined achievements.`);
}
// Achievement operations
async findAchievementById(id: string): Promise<Achievement | null> {
this.logger.debug(`Finding achievement by id: ${id}`);
try {
const achievement = this.achievements.get(id) ?? null;
if (achievement) {
this.logger.info(`Found achievement: ${id}.`);
} else {
this.logger.warn(`Achievement with id ${id} not found.`);
}
return achievement;
} catch (error) {
this.logger.error(`Error finding achievement by id ${id}:`, error);
throw error;
}
}
async findAllAchievements(): Promise<Achievement[]> {
this.logger.debug('Finding all achievements.');
try {
const achievements = Array.from(this.achievements.values());
this.logger.info(`Found ${achievements.length} achievements.`);
return achievements;
} catch (error) {
this.logger.error('Error finding all achievements:', error);
throw error;
}
}
async findAchievementsByCategory(category: AchievementCategory): Promise<Achievement[]> {
this.logger.debug(`Finding achievements by category: ${category}`);
try {
const achievements = Array.from(this.achievements.values())
.filter(a => a.category === category);
this.logger.info(`Found ${achievements.length} achievements for category: ${category}.`);
return achievements;
} catch (error) {
this.logger.error(`Error finding achievements by category ${category}:`, error);
throw error;
}
}
async createAchievement(achievement: Achievement): Promise<Achievement> {
this.logger.debug(`Creating achievement: ${achievement.id}`);
try {
if (this.achievements.has(achievement.id)) {
this.logger.warn(`Achievement with ID ${achievement.id} already exists.`);
throw new Error('Achievement with this ID already exists');
}
this.achievements.set(achievement.id, achievement);
this.logger.info(`Achievement ${achievement.id} created successfully.`);
return achievement;
} catch (error) {
this.logger.error(`Error creating achievement ${achievement.id}:`, error);
throw error;
}
}
// UserAchievement operations
async findUserAchievementById(id: string): Promise<UserAchievement | null> {
this.logger.debug(`Finding user achievement by id: ${id}`);
try {
const userAchievement = this.userAchievements.get(id) ?? null;
if (userAchievement) {
this.logger.info(`Found user achievement: ${id}.`);
} else {
this.logger.warn(`User achievement with id ${id} not found.`);
}
return userAchievement;
} catch (error) {
this.logger.error(`Error finding user achievement by id ${id}:`, error);
throw error;
}
}
async findUserAchievementsByUserId(userId: string): Promise<UserAchievement[]> {
this.logger.debug(`Finding user achievements by user id: ${userId}`);
try {
const userAchievements = Array.from(this.userAchievements.values())
.filter(ua => ua.userId === userId);
this.logger.info(`Found ${userAchievements.length} user achievements for user id: ${userId}.`);
return userAchievements;
} catch (error) {
this.logger.error(`Error finding user achievements by user id ${userId}:`, error);
throw error;
}
}
async findUserAchievementByUserAndAchievement(
userId: string,
achievementId: string
): Promise<UserAchievement | null> {
this.logger.debug(`Finding user achievement for user: ${userId}, achievement: ${achievementId}`);
try {
for (const ua of this.userAchievements.values()) {
if (ua.userId === userId && ua.achievementId === achievementId) {
this.logger.info(`Found user achievement for user: ${userId}, achievement: ${achievementId}.`);
return ua;
}
}
this.logger.warn(`User achievement for user ${userId}, achievement ${achievementId} not found.`);
return null;
} catch (error) {
this.logger.error(`Error finding user achievement for user ${userId}, achievement ${achievementId}:`, error);
throw error;
}
}
async hasUserEarnedAchievement(userId: string, achievementId: string): Promise<boolean> {
this.logger.debug(`Checking if user ${userId} earned achievement ${achievementId}`);
try {
const ua = await this.findUserAchievementByUserAndAchievement(userId, achievementId);
const hasEarned = ua !== null && ua.isComplete();
this.logger.debug(`User ${userId} earned achievement ${achievementId}: ${hasEarned}.`);
return hasEarned;
} catch (error) {
this.logger.error(`Error checking if user ${userId} earned achievement ${achievementId}:`, error);
throw error;
}
}
async createUserAchievement(userAchievement: UserAchievement): Promise<UserAchievement> {
this.logger.debug(`Creating user achievement: ${userAchievement.id}`);
try {
if (this.userAchievements.has(userAchievement.id)) {
this.logger.warn(`UserAchievement with ID ${userAchievement.id} already exists.`);
throw new Error('UserAchievement with this ID already exists');
}
this.userAchievements.set(userAchievement.id, userAchievement);
this.logger.info(`UserAchievement ${userAchievement.id} created successfully.`);
return userAchievement;
} catch (error) {
this.logger.error(`Error creating user achievement ${userAchievement.id}:`, error);
throw error;
}
}
async updateUserAchievement(userAchievement: UserAchievement): Promise<UserAchievement> {
this.logger.debug(`Updating user achievement: ${userAchievement.id}`);
try {
if (!this.userAchievements.has(userAchievement.id)) {
this.logger.warn(`UserAchievement with ID ${userAchievement.id} not found for update.`);
throw new Error('UserAchievement not found');
}
this.userAchievements.set(userAchievement.id, userAchievement);
this.logger.info(`UserAchievement ${userAchievement.id} updated successfully.`);
return userAchievement;
} catch (error) {
this.logger.error(`Error updating user achievement ${userAchievement.id}:`, error);
throw error;
}
}
// Stats
async getAchievementLeaderboard(limit: number): Promise<{ userId: string; points: number; count: number }[]> {
this.logger.debug(`Getting achievement leaderboard with limit: ${limit}`);
try {
const userStats = new Map<string, { points: number; count: number }>();
for (const ua of this.userAchievements.values()) {
if (!ua.isComplete()) continue;
const achievement = this.achievements.get(ua.achievementId);
if (!achievement) {
this.logger.warn(`Achievement ${ua.achievementId} not found while building leaderboard.`);
continue;
}
const existing = userStats.get(ua.userId) ?? { points: 0, count: 0 };
userStats.set(ua.userId, {
points: existing.points + achievement.points,
count: existing.count + 1,
});
}
const leaderboard = Array.from(userStats.entries())
.map(([userId, stats]) => ({ userId, ...stats }))
.sort((a, b) => b.points - a.points)
.slice(0, limit);
this.logger.info(`Generated achievement leaderboard with ${leaderboard.length} entries.`);
return leaderboard;
} catch (error) {
this.logger.error(`Error getting achievement leaderboard:`, error);
throw error;
}
}
async getUserAchievementStats(userId: string): Promise<{
total: number;
points: number;
byCategory: Record<AchievementCategory, number>
}> {
this.logger.debug(`Getting achievement stats for user: ${userId}`);
try {
const userAchievements = await this.findUserAchievementsByUserId(userId);
const completedAchievements = userAchievements.filter(ua => ua.isComplete());
this.logger.debug(`Found ${completedAchievements.length} completed achievements for user ${userId}.`);
const byCategory: Record<AchievementCategory, number> = {
driver: 0,
steward: 0,
admin: 0,
community: 0,
};
let points = 0;
for (const ua of completedAchievements) {
const achievement = this.achievements.get(ua.achievementId);
if (achievement) {
points += achievement.points;
byCategory[achievement.category]++;
} else {
this.logger.warn(`Achievement ${ua.achievementId} not found while calculating user stats for user ${userId}.`);
}
}
const stats = {
total: completedAchievements.length,
points,
byCategory,
};
this.logger.info(`Generated achievement stats for user ${userId}:`, stats);
return stats;
} catch (error) {
this.logger.error(`Error getting user achievement stats for user ${userId}:`, error);
throw error;
}
}
// Test helpers
clearUserAchievements(): void {
this.logger.debug('Clearing all user achievements.');
this.userAchievements.clear();
this.logger.info('All user achievements cleared.');
}
clear(): void {
this.logger.debug('Clearing all achievement data.');
this.achievements.clear();
this.userAchievements.clear();
this.logger.info('All achievement data cleared.');
}
}

View File

@@ -1,123 +0,0 @@
/**
* Infrastructure: InMemorySponsorAccountRepository
*
* In-memory implementation of ISponsorAccountRepository for development/testing.
*/
import type { ISponsorAccountRepository } from '../../domain/repositories/ISponsorAccountRepository';
import type { SponsorAccount } from '../../domain/entities/SponsorAccount';
import type { UserId } from '../../domain/value-objects/UserId';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemorySponsorAccountRepository implements ISponsorAccountRepository {
private accounts: Map<string, SponsorAccount> = new Map();
private readonly logger: ILogger;
constructor(logger: ILogger, seedData?: SponsorAccount[]) {
this.logger = logger;
this.logger.info('InMemorySponsorAccountRepository initialized.');
if (seedData) {
this.seed(seedData);
}
}
async save(account: SponsorAccount): Promise<void> {
this.logger.debug(`Saving sponsor account: ${account.getId().value}`);
try {
this.accounts.set(account.getId().value, account);
this.logger.info(`Sponsor account ${account.getId().value} saved successfully.`);
} catch (error) {
this.logger.error(`Error saving sponsor account ${account.getId().value}:`, error);
throw error;
}
}
async findById(id: UserId): Promise<SponsorAccount | null> {
this.logger.debug(`Finding sponsor account by id: ${id.value}`);
try {
const account = this.accounts.get(id.value) ?? null;
if (account) {
this.logger.info(`Found sponsor account: ${id.value}.`);
} else {
this.logger.warn(`Sponsor account with id ${id.value} not found.`);
}
return account;
} catch (error) {
this.logger.error(`Error finding sponsor account by id ${id.value}:`, error);
throw error;
}
}
async findBySponsorId(sponsorId: string): Promise<SponsorAccount | null> {
this.logger.debug(`Finding sponsor account by sponsor id: ${sponsorId}`);
try {
const account = Array.from(this.accounts.values()).find(
a => a.getSponsorId() === sponsorId
) ?? null;
if (account) {
this.logger.info(`Found sponsor account for sponsor id: ${sponsorId}.`);
} else {
this.logger.warn(`Sponsor account for sponsor id ${sponsorId} not found.`);
}
return account;
} catch (error) {
this.logger.error(`Error finding sponsor account by sponsor id ${sponsorId}:`, error);
throw error;
}
}
async findByEmail(email: string): Promise<SponsorAccount | null> {
this.logger.debug(`Finding sponsor account by email: ${email}`);
try {
const normalizedEmail = email.toLowerCase().trim();
const account = Array.from(this.accounts.values()).find(
a => a.getEmail().toLowerCase() === normalizedEmail
) ?? null;
if (account) {
this.logger.info(`Found sponsor account by email: ${email}.`);
} else {
this.logger.warn(`Sponsor account with email ${email} not found.`);
}
return account;
} catch (error) {
this.logger.error(`Error finding sponsor account by email ${email}:`, error);
throw error;
}
}
async delete(id: UserId): Promise<void> {
this.logger.debug(`Deleting sponsor account: ${id.value}`);
try {
if (this.accounts.delete(id.value)) {
this.logger.info(`Sponsor account ${id.value} deleted successfully.`);
} else {
this.logger.warn(`Sponsor account with id ${id.value} not found for deletion.`);
}
} catch (error) {
this.logger.error(`Error deleting sponsor account ${id.value}:`, error);
throw error;
}
}
// Helper for testing
clear(): void {
this.logger.debug('Clearing all sponsor accounts.');
this.accounts.clear();
this.logger.info('All sponsor accounts cleared.');
}
// Helper for seeding demo data
seed(accounts: SponsorAccount[]): void {
this.logger.debug(`Seeding ${accounts.length} sponsor accounts.`);
try {
accounts.forEach(a => {
this.accounts.set(a.getId().value, a);
this.logger.debug(`Seeded sponsor account: ${a.getId().value}.`);
});
this.logger.info(`Successfully seeded ${accounts.length} sponsor accounts.`);
} catch (error) {
this.logger.error(`Error seeding sponsor accounts:`, error);
throw error;
}
}
}

View File

@@ -1,153 +0,0 @@
/**
* Infrastructure Adapter: InMemoryUserRatingRepository
*
* In-memory implementation of IUserRatingRepository
*/
import { UserRating } from '../../domain/value-objects/UserRating';
import type { IUserRatingRepository } from '../../domain/repositories/IUserRatingRepository';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemoryUserRatingRepository implements IUserRatingRepository {
private ratings: Map<string, UserRating> = new Map();
private readonly logger: ILogger;
constructor(logger: ILogger, seedData?: UserRating[]) {
this.logger = logger;
this.logger.info('InMemoryUserRatingRepository initialized.');
if (seedData) {
seedData.forEach(rating => this.ratings.set(rating.userId, rating));
this.logger.debug(`Seeded ${seedData.length} user ratings.`);
}
}
async findByUserId(userId: string): Promise<UserRating | null> {
this.logger.debug(`Finding user rating for user id: ${userId}`);
try {
const rating = this.ratings.get(userId) ?? null;
if (rating) {
this.logger.info(`Found user rating for user id: ${userId}.`);
} else {
this.logger.warn(`User rating for user id ${userId} not found.`);
}
return rating;
} catch (error) {
this.logger.error(`Error finding user rating for user id ${userId}:`, error);
throw error;
}
}
async findByUserIds(userIds: string[]): Promise<UserRating[]> {
this.logger.debug(`Finding user ratings for user ids: ${userIds.join(', ')}`);
try {
const results: UserRating[] = [];
for (const userId of userIds) {
const rating = this.ratings.get(userId);
if (rating) {
results.push(rating);
} else {
this.logger.warn(`User rating for user id ${userId} not found.`);
}
}
this.logger.info(`Found ${results.length} user ratings for ${userIds.length} requested users.`);
return results;
} catch (error) {
this.logger.error(`Error finding user ratings for user ids ${userIds.join(', ')}:`, error);
throw error;
}
}
async save(rating: UserRating): Promise<UserRating> {
this.logger.debug(`Saving user rating for user id: ${rating.userId}`);
try {
if (this.ratings.has(rating.userId)) {
this.logger.debug(`Updating existing user rating for user id: ${rating.userId}.`);
} else {
this.logger.debug(`Creating new user rating for user id: ${rating.userId}.`);
}
this.ratings.set(rating.userId, rating);
this.logger.info(`User rating for user id ${rating.userId} saved successfully.`);
return rating;
} catch (error) {
this.logger.error(`Error saving user rating for user id ${rating.userId}:`, error);
throw error;
}
}
async getTopDrivers(limit: number): Promise<UserRating[]> {
this.logger.debug(`Getting top ${limit} drivers.`);
try {
const topDrivers = Array.from(this.ratings.values())
.filter(r => r.driver.sampleSize > 0)
.sort((a, b) => b.driver.value - a.driver.value)
.slice(0, limit);
this.logger.info(`Retrieved ${topDrivers.length} top drivers.`);
return topDrivers;
} catch (error) {
this.logger.error(`Error getting top drivers:`, error);
throw error;
}
}
async getTopTrusted(limit: number): Promise<UserRating[]> {
this.logger.debug(`Getting top ${limit} trusted users.`);
try {
const topTrusted = Array.from(this.ratings.values())
.filter(r => r.trust.sampleSize > 0)
.sort((a, b) => b.trust.value - a.trust.value)
.slice(0, limit);
this.logger.info(`Retrieved ${topTrusted.length} top trusted users.`);
return topTrusted;
} catch (error) {
this.logger.error(`Error getting top trusted users:`, error);
throw error;
}
}
async getEligibleStewards(): Promise<UserRating[]> {
this.logger.debug('Getting eligible stewards.');
try {
const eligibleStewards = Array.from(this.ratings.values())
.filter(r => r.canBeSteward());
this.logger.info(`Found ${eligibleStewards.length} eligible stewards.`);
return eligibleStewards;
} catch (error) {
this.logger.error(`Error getting eligible stewards:`, error);
throw error;
}
}
async findByDriverTier(tier: 'rookie' | 'amateur' | 'semi-pro' | 'pro' | 'elite'): Promise<UserRating[]> {
this.logger.debug(`Finding user ratings by driver tier: ${tier}`);
try {
const ratingsByTier = Array.from(this.ratings.values())
.filter(r => r.getDriverTier() === tier);
this.logger.info(`Found ${ratingsByTier.length} user ratings for driver tier: ${tier}.`);
return ratingsByTier;
} catch (error) {
this.logger.error(`Error finding user ratings by driver tier ${tier}:`, error);
throw error;
}
}
async delete(userId: string): Promise<void> {
this.logger.debug(`Deleting user rating for user id: ${userId}`);
try {
if (this.ratings.delete(userId)) {
this.logger.info(`User rating for user id ${userId} deleted successfully.`);
} else {
this.logger.warn(`User rating for user id ${userId} not found for deletion.`);
}
} catch (error) {
this.logger.error(`Error deleting user rating for user id ${userId}:`, error);
throw error;
}
}
// Test helper
clear(): void {
this.logger.debug('Clearing all user ratings.');
this.ratings.clear();
this.logger.info('All user ratings cleared.');
}
}

View File

@@ -1,117 +0,0 @@
/**
* In-Memory User Repository
*
* Stores users in memory for demo/development purposes.
*/
import type { IUserRepository, StoredUser } from '../../domain/repositories/IUserRepository';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export class InMemoryUserRepository implements IUserRepository {
private users: Map<string, StoredUser> = new Map();
private emailIndex: Map<string, string> = new Map(); // email -> userId
private readonly logger: ILogger;
constructor(logger: ILogger, initialUsers: StoredUser[] = []) {
this.logger = logger;
this.logger.info('InMemoryUserRepository initialized.');
for (const user of initialUsers) {
this.users.set(user.id, user);
this.emailIndex.set(user.email.toLowerCase(), user.id);
this.logger.debug(`Seeded user: ${user.id} (${user.email}).`);
}
}
async findByEmail(email: string): Promise<StoredUser | null> {
this.logger.debug(`Finding user by email: ${email}`);
try {
const userId = this.emailIndex.get(email.toLowerCase());
if (!userId) {
this.logger.warn(`User with email ${email} not found.`);
return null;
}
const user = this.users.get(userId) ?? null;
if (user) {
this.logger.info(`Found user by email: ${email}.`);
} else {
this.logger.warn(`User with ID ${userId} (from email index) not found.`);
}
return user;
} catch (error) {
this.logger.error(`Error finding user by email ${email}:`, error);
throw error;
}
}
async findById(id: string): Promise<StoredUser | null> {
this.logger.debug(`Finding user by id: ${id}`);
try {
const user = this.users.get(id) ?? null;
if (user) {
this.logger.info(`Found user: ${id}.`);
} else {
this.logger.warn(`User with id ${id} not found.`);
}
return user;
} catch (error) {
this.logger.error(`Error finding user by id ${id}:`, error);
throw error;
}
}
async create(user: StoredUser): Promise<StoredUser> {
this.logger.debug(`Creating user: ${user.id} with email: ${user.email}`);
try {
if (this.emailIndex.has(user.email.toLowerCase())) {
this.logger.warn(`Email ${user.email} already exists.`);
throw new Error('Email already exists');
}
this.users.set(user.id, user);
this.emailIndex.set(user.email.toLowerCase(), user.id);
this.logger.info(`User ${user.id} (${user.email}) created successfully.`);
return user;
} catch (error) {
this.logger.error(`Error creating user ${user.id} (${user.email}):`, error);
throw error;
}
}
async update(user: StoredUser): Promise<StoredUser> {
this.logger.debug(`Updating user: ${user.id} with email: ${user.email}`);
try {
const existing = this.users.get(user.id);
if (!existing) {
this.logger.warn(`User with ID ${user.id} not found for update.`);
throw new Error('User not found');
}
// If email changed, update index
if (existing.email.toLowerCase() !== user.email.toLowerCase()) {
if (this.emailIndex.has(user.email.toLowerCase()) && this.emailIndex.get(user.email.toLowerCase()) !== user.id) {
this.logger.warn(`Cannot update user ${user.id} to email ${user.email} as it's already taken.`);
throw new Error('Email already exists for another user');
}
this.logger.debug(`Updating email index from ${existing.email} to ${user.email}.`);
this.emailIndex.delete(existing.email.toLowerCase());
this.emailIndex.set(user.email.toLowerCase(), user.id);
}
this.users.set(user.id, user);
this.logger.info(`User ${user.id} (${user.email}) updated successfully.`);
return user;
} catch (error) {
this.logger.error(`Error updating user ${user.id} (${user.email}):`, error);
throw error;
}
}
async emailExists(email: string): Promise<boolean> {
this.logger.debug(`Checking existence of email: ${email}`);
try {
const exists = this.emailIndex.has(email.toLowerCase());
this.logger.debug(`Email ${email} exists: ${exists}.`);
return exists;
} catch (error) {
this.logger.error(`Error checking existence of email ${email}:`, error);
throw error;
}
}
}

View File

@@ -1,59 +0,0 @@
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);
}
}