wip
This commit is contained in:
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user