86211923db
Repository Interface Optimization: - Created IBaseRepository.ts and IPaginatedRepository.ts - Refactored all 7 repository interfaces to extend base interfaces - Eliminated ~200 lines of redundant code (70% reduction) - Improved type safety and maintainability Dependency Injection Improvements: - Added EmailService and GameTokenService to DIContainer - Updated CreateUserCommandHandler constructor for DI - Updated RequestPasswordResetCommandHandler constructor for DI - Enhanced testability and service consistency Environment Configuration: - Created comprehensive .env.example with 40+ variables - Organized into 12 logical sections (Database, Security, Email, etc.) - Added security guidelines and best practices - Documented all backend environment requirements Documentation: - Added comprehensive codebase review - Created refactoring summary report - Added frontend implementation guide Impact: Improved code quality, reduced maintenance overhead, enhanced developer experience
230 lines
7.3 KiB
TypeScript
230 lines
7.3 KiB
TypeScript
import * as crypto from 'crypto';
|
|
import { logError } from './Logger';
|
|
|
|
export interface VerificationToken {
|
|
token: string;
|
|
expiresAt: Date;
|
|
createdAt: Date;
|
|
}
|
|
|
|
export interface PasswordResetToken {
|
|
token: string;
|
|
expiresAt: Date;
|
|
createdAt: Date;
|
|
}
|
|
|
|
export class TokenService {
|
|
private static readonly VERIFICATION_TOKEN_EXPIRES_HOURS = 24;
|
|
private static readonly PASSWORD_RESET_TOKEN_EXPIRES_HOURS = 1;
|
|
private static readonly TOKEN_LENGTH = 32;
|
|
|
|
/**
|
|
* Generate a secure random token
|
|
* @param length - Length of the token in bytes (default: 32)
|
|
* @returns Hexadecimal string token
|
|
*/
|
|
static generateSecureToken(length: number = TokenService.TOKEN_LENGTH): string {
|
|
try {
|
|
return crypto.randomBytes(length).toString('hex');
|
|
} catch (error) {
|
|
logError('TokenService.generateSecureToken error', error instanceof Error ? error : new Error(String(error)));
|
|
throw new Error('Failed to generate secure token');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate email verification token with expiration
|
|
* @returns VerificationToken object with token and expiration info
|
|
*/
|
|
static generateVerificationToken(): VerificationToken {
|
|
try {
|
|
const token = this.generateSecureToken();
|
|
const createdAt = new Date();
|
|
const expiresAt = new Date(createdAt.getTime() + (this.VERIFICATION_TOKEN_EXPIRES_HOURS * 60 * 60 * 1000));
|
|
|
|
return {
|
|
token,
|
|
createdAt,
|
|
expiresAt
|
|
};
|
|
} catch (error) {
|
|
logError('TokenService.generateVerificationToken error', error instanceof Error ? error : new Error(String(error)));
|
|
throw new Error('Failed to generate verification token');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate password reset token with expiration
|
|
* @returns PasswordResetToken object with token and expiration info
|
|
*/
|
|
static generatePasswordResetToken(): PasswordResetToken {
|
|
try {
|
|
const token = this.generateSecureToken();
|
|
const createdAt = new Date();
|
|
const expiresAt = new Date(createdAt.getTime() + (this.PASSWORD_RESET_TOKEN_EXPIRES_HOURS * 60 * 60 * 1000));
|
|
|
|
return {
|
|
token,
|
|
createdAt,
|
|
expiresAt
|
|
};
|
|
} catch (error) {
|
|
logError('TokenService.generatePasswordResetToken error', error instanceof Error ? error : new Error(String(error)));
|
|
throw new Error('Failed to generate password reset token');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a token has expired
|
|
* @param expiresAt - Expiration date of the token
|
|
* @returns True if token has expired, false otherwise
|
|
*/
|
|
static isTokenExpired(expiresAt: Date): boolean {
|
|
try {
|
|
return new Date() > expiresAt;
|
|
} catch (error) {
|
|
logError('TokenService.isTokenExpired error', error instanceof Error ? error : new Error(String(error)));
|
|
return true; // Assume expired on error for security
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate token format (basic validation)
|
|
* @param token - Token to validate
|
|
* @returns True if token format is valid, false otherwise
|
|
*/
|
|
static isValidTokenFormat(token: string): boolean {
|
|
try {
|
|
if (!token || typeof token !== 'string') {
|
|
return false;
|
|
}
|
|
|
|
// Check if token is hexadecimal and has expected length
|
|
const hexRegex = /^[a-f0-9]+$/i;
|
|
const expectedLength = this.TOKEN_LENGTH * 2; // Each byte becomes 2 hex characters
|
|
|
|
return hexRegex.test(token) && token.length === expectedLength;
|
|
} catch (error) {
|
|
logError('TokenService.isValidTokenFormat error', error instanceof Error ? error : new Error(String(error)));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate a verification URL with token
|
|
* @param baseUrl - Base URL of the application
|
|
* @param token - Verification token
|
|
* @returns Complete verification URL
|
|
*/
|
|
static generateVerificationUrl(baseUrl: string, token: string): string {
|
|
try {
|
|
// Remove trailing slash from baseUrl if present
|
|
const cleanBaseUrl = baseUrl.replace(/\/$/, '');
|
|
return `${cleanBaseUrl}/api/auth/verify-email?token=${encodeURIComponent(token)}`;
|
|
} catch (error) {
|
|
logError('TokenService.generateVerificationUrl error', error instanceof Error ? error : new Error(String(error)));
|
|
throw new Error('Failed to generate verification URL');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate a password reset URL with token
|
|
* @param baseUrl - Base URL of the application
|
|
* @param token - Password reset token
|
|
* @returns Complete password reset URL
|
|
*/
|
|
static generatePasswordResetUrl(baseUrl: string, token: string): string {
|
|
try {
|
|
// Remove trailing slash from baseUrl if present
|
|
const cleanBaseUrl = baseUrl.replace(/\/$/, '');
|
|
return `${cleanBaseUrl}/api/auth/reset-password?token=${encodeURIComponent(token)}`;
|
|
} catch (error) {
|
|
logError('TokenService.generatePasswordResetUrl error', error instanceof Error ? error : new Error(String(error)));
|
|
throw new Error('Failed to generate password reset URL');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hash a token for secure storage in database
|
|
* @param token - Plain text token to hash
|
|
* @returns Hashed token
|
|
*/
|
|
static async hashToken(token: string): Promise<string> {
|
|
try {
|
|
if (!token || typeof token !== 'string') {
|
|
throw new Error('Token must be a non-empty string');
|
|
}
|
|
|
|
return crypto.createHash('sha256').update(token).digest('hex');
|
|
} catch (error) {
|
|
logError('TokenService.hashToken error', error instanceof Error ? error : new Error(String(error)));
|
|
throw new Error('Failed to hash token');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify a plain text token against a hashed token
|
|
* @param plainToken - Plain text token to verify
|
|
* @param hashedToken - Hashed token to compare against
|
|
* @returns True if tokens match, false otherwise
|
|
*/
|
|
static async verifyToken(plainToken: string, hashedToken: string): Promise<boolean> {
|
|
try {
|
|
if (!plainToken || !hashedToken) {
|
|
return false;
|
|
}
|
|
|
|
const hashedPlainToken = await this.hashToken(plainToken);
|
|
return hashedPlainToken === hashedToken;
|
|
} catch (error) {
|
|
logError('TokenService.verifyToken error', error instanceof Error ? error : new Error(String(error)));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get token expiration info in human-readable format
|
|
* @param expiresAt - Expiration date
|
|
* @returns Human-readable expiration info
|
|
*/
|
|
static getExpirationInfo(expiresAt: Date): { expired: boolean; timeLeft: string } {
|
|
try {
|
|
const now = new Date();
|
|
const expired = now > expiresAt;
|
|
|
|
if (expired) {
|
|
const timeAgo = Math.floor((now.getTime() - expiresAt.getTime()) / (1000 * 60));
|
|
return {
|
|
expired: true,
|
|
timeLeft: `Expired ${timeAgo} minute(s) ago`
|
|
};
|
|
}
|
|
|
|
const timeLeft = Math.floor((expiresAt.getTime() - now.getTime()) / (1000 * 60));
|
|
const hours = Math.floor(timeLeft / 60);
|
|
const minutes = timeLeft % 60;
|
|
|
|
let timeString = '';
|
|
if (hours > 0) {
|
|
timeString = `${hours} hour(s)`;
|
|
if (minutes > 0) {
|
|
timeString += ` and ${minutes} minute(s)`;
|
|
}
|
|
} else {
|
|
timeString = `${minutes} minute(s)`;
|
|
}
|
|
|
|
return {
|
|
expired: false,
|
|
timeLeft: `Expires in ${timeString}`
|
|
};
|
|
} catch (error) {
|
|
logError('TokenService.getExpirationInfo error', error instanceof Error ? error : new Error(String(error)));
|
|
return {
|
|
expired: true,
|
|
timeLeft: 'Unable to determine expiration'
|
|
};
|
|
}
|
|
}
|
|
}
|