This commit is contained in:
2025-12-11 13:50:38 +01:00
parent e4c1be628d
commit c7e5de40d6
212 changed files with 2965 additions and 763 deletions

View File

@@ -1,64 +1,48 @@
import { z } from 'zod';
import type { IValueObject } from '@gridpilot/shared/domain';
import type { EmailValidationResult } from '../types/EmailAddress';
import { validateEmail, isDisposableEmail } from '../types/EmailAddress';
/**
* Core email validation schema
*/
export const emailSchema = z
.string()
.trim()
.toLowerCase()
.min(6, 'Email too short')
.max(254, 'Email too long')
.email('Invalid email format');
export type EmailValidationSuccess = {
success: true;
email: string;
error?: undefined;
};
export type EmailValidationFailure = {
success: false;
email?: undefined;
error: string;
};
export type EmailValidationResult = EmailValidationSuccess | EmailValidationFailure;
/**
* Validate and normalize an email address.
* Mirrors the previous apps/website/lib/email-validation.ts behavior.
*/
export function validateEmail(email: string): EmailValidationResult {
const result = emailSchema.safeParse(email);
if (result.success) {
return {
success: true,
email: result.data,
};
}
return {
success: false,
error: result.error.errors[0]?.message || 'Invalid email',
};
export interface EmailAddressProps {
value: string;
}
/**
* Basic disposable email detection.
* This list matches the previous website-local implementation and
* can be extended in the future without changing the public API.
* Value Object: EmailAddress
*
* Wraps a validated, normalized email string and provides equality semantics.
* Validation and helper utilities live in domain/types/EmailAddress.
*/
export const DISPOSABLE_DOMAINS = new Set<string>([
'tempmail.com',
'throwaway.email',
'guerrillamail.com',
'mailinator.com',
'10minutemail.com',
]);
export class EmailAddress implements IValueObject<EmailAddressProps> {
public readonly props: EmailAddressProps;
export function isDisposableEmail(email: string): boolean {
const domain = email.split('@')[1]?.toLowerCase();
return domain ? DISPOSABLE_DOMAINS.has(domain) : false;
}
private constructor(value: string) {
this.props = { value };
}
static create(raw: string): EmailAddress {
const result: EmailValidationResult = validateEmail(raw);
if (!result.success) {
throw new Error(result.error);
}
return new EmailAddress(result.email);
}
static fromValidated(value: string): EmailAddress {
return new EmailAddress(value);
}
get value(): string {
return this.props.value;
}
equals(other: IValueObject<EmailAddressProps>): boolean {
return this.props.value === other.props.value;
}
isDisposable(): boolean {
return isDisposableEmail(this.props.value);
}
}
export type { EmailValidationResult } from '../types/EmailAddress';
export { validateEmail, isDisposableEmail } from '../types/EmailAddress';

View File

@@ -1,22 +1,32 @@
export class UserId {
private readonly value: string;
import type { IValueObject } from '@gridpilot/shared/domain';
export interface UserIdProps {
value: string;
}
export class UserId implements IValueObject<UserIdProps> {
public readonly props: UserIdProps;
private constructor(value: string) {
if (!value || !value.trim()) {
throw new Error('UserId cannot be empty');
}
this.value = value;
this.props = { value };
}
public static fromString(value: string): UserId {
return new UserId(value);
}
public toString(): string {
return this.value;
get value(): string {
return this.props.value;
}
public equals(other: UserId): boolean {
return this.value === other.value;
public toString(): string {
return this.props.value;
}
public equals(other: IValueObject<UserIdProps>): boolean {
return this.props.value === other.props.value;
}
}

View File

@@ -1,6 +1,8 @@
import type { IValueObject } from '@gridpilot/shared/domain';
/**
* Value Object: UserRating
*
*
* Multi-dimensional rating system for users covering:
* - Driver skill: racing ability, lap times, consistency
* - Admin competence: league management, event organization
@@ -37,27 +39,47 @@ const DEFAULT_DIMENSION: RatingDimension = {
lastUpdated: new Date(),
};
export class UserRating {
readonly userId: string;
readonly driver: RatingDimension;
readonly admin: RatingDimension;
readonly steward: RatingDimension;
readonly trust: RatingDimension;
readonly fairness: RatingDimension;
readonly overallReputation: number;
readonly createdAt: Date;
readonly updatedAt: Date;
export class UserRating implements IValueObject<UserRatingProps> {
readonly props: UserRatingProps;
private constructor(props: UserRatingProps) {
this.userId = props.userId;
this.driver = props.driver;
this.admin = props.admin;
this.steward = props.steward;
this.trust = props.trust;
this.fairness = props.fairness;
this.overallReputation = props.overallReputation;
this.createdAt = props.createdAt;
this.updatedAt = props.updatedAt;
this.props = props;
}
get userId(): string {
return this.props.userId;
}
get driver(): RatingDimension {
return this.props.driver;
}
get admin(): RatingDimension {
return this.props.admin;
}
get steward(): RatingDimension {
return this.props.steward;
}
get trust(): RatingDimension {
return this.props.trust;
}
get fairness(): RatingDimension {
return this.props.fairness;
}
get overallReputation(): number {
return this.props.overallReputation;
}
get createdAt(): Date {
return this.props.createdAt;
}
get updatedAt(): Date {
return this.props.updatedAt;
}
static create(userId: string): UserRating {
@@ -83,6 +105,10 @@ export class UserRating {
return new UserRating(props);
}
equals(other: IValueObject<UserRatingProps>): boolean {
return this.props.userId === other.props.userId;
}
/**
* Update driver rating based on race performance
*/
@@ -241,14 +267,14 @@ export class UserRating {
private withUpdates(updates: Partial<UserRatingProps>): UserRating {
const newRating = new UserRating({
...this,
...this.props,
...updates,
updatedAt: new Date(),
});
// Recalculate overall reputation
return new UserRating({
...newRating,
...newRating.props,
overallReputation: newRating.calculateOverallReputation(),
});
}