Files
SerpentRace/SerpentRace_Backend/src/Application/Services/TokenService.ts
T
Donat 86211923db Backend Complete: Interface Refactoring & Service Container Enhancements
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
2025-09-21 03:27:57 +02:00

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'
};
}
}
}