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
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import jwt, { SignOptions } from 'jsonwebtoken';
|
||||
import { Request, Response } from 'express';
|
||||
import { UserState } from '../../Domain/User/UserAggregate';
|
||||
|
||||
export interface TokenPayload {
|
||||
userId: string;
|
||||
authLevel: 0 | 1;
|
||||
userStatus: UserState;
|
||||
orgId: string;
|
||||
iat?: number;
|
||||
exp?: number;
|
||||
}
|
||||
|
||||
export class JWTService {
|
||||
private readonly secretKey: string;
|
||||
private readonly tokenExpiry: number;
|
||||
private readonly cookieName: string;
|
||||
|
||||
constructor() {
|
||||
this.secretKey = process.env.JWT_SECRET || 'your-secret-key';
|
||||
|
||||
let expiry = 86400;
|
||||
|
||||
if (process.env.JWT_EXPIRY) {
|
||||
expiry = parseInt(process.env.JWT_EXPIRY);
|
||||
} else if (process.env.JWT_EXPIRATION) {
|
||||
expiry = this.parseDuration(process.env.JWT_EXPIRATION);
|
||||
}
|
||||
|
||||
this.tokenExpiry = expiry;
|
||||
this.cookieName = 'auth_token';
|
||||
|
||||
if (process.env.NODE_ENV === 'production' && (!process.env.JWT_SECRET || process.env.JWT_SECRET === 'your-secret-key')) {
|
||||
throw new Error('JWT_SECRET environment variable must be set in production');
|
||||
}
|
||||
}
|
||||
|
||||
create(payload: TokenPayload, res: Response): string {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
const payloadWithTimestamps: TokenPayload = {
|
||||
...payload,
|
||||
iat: now,
|
||||
exp: now + this.tokenExpiry
|
||||
};
|
||||
|
||||
// Don't use expiresIn option since we're manually setting exp in payload
|
||||
const options: SignOptions = {};
|
||||
const token = jwt.sign(payloadWithTimestamps, this.secretKey, options);
|
||||
|
||||
res.cookie(this.cookieName, token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
maxAge: this.tokenExpiry * 1000, // Convert to milliseconds
|
||||
});
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
verify(req: Request): TokenPayload | null {
|
||||
try {
|
||||
const token = req.cookies[this.cookieName];
|
||||
if (!token) return null;
|
||||
|
||||
const decoded = jwt.verify(token, this.secretKey) as TokenPayload;
|
||||
return decoded;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if token needs refresh (within 25% of expiry time)
|
||||
shouldRefreshToken(payload: TokenPayload): boolean {
|
||||
if (!payload.exp || !payload.iat) return false;
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const tokenAge = now - payload.iat;
|
||||
const tokenLifetime = payload.exp - payload.iat;
|
||||
const refreshThreshold = tokenLifetime * 0.75; // Refresh when 75% of lifetime has passed
|
||||
|
||||
return tokenAge >= refreshThreshold;
|
||||
}
|
||||
|
||||
// Conditionally refresh token only if needed
|
||||
refreshIfNeeded(payload: TokenPayload, res: Response): boolean {
|
||||
if (this.shouldRefreshToken(payload)) {
|
||||
// Create new token with fresh timestamps, but same user data
|
||||
const freshPayload: Omit<TokenPayload, 'iat' | 'exp'> = {
|
||||
userId: payload.userId,
|
||||
authLevel: payload.authLevel,
|
||||
userStatus: payload.userStatus,
|
||||
orgId: payload.orgId
|
||||
};
|
||||
this.create(freshPayload, res);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse duration string to seconds (e.g., "24h", "7d", "30m")
|
||||
* @param duration Duration string
|
||||
* @returns Duration in seconds
|
||||
*/
|
||||
private parseDuration(duration: string): number {
|
||||
const match = duration.match(/^(\d+)([smhd])$/);
|
||||
if (!match) {
|
||||
throw new Error(`Invalid duration format: ${duration}. Use format like '24h', '7d', '30m'`);
|
||||
}
|
||||
|
||||
const [, value, unit] = match;
|
||||
const num = parseInt(value);
|
||||
|
||||
switch (unit) {
|
||||
case 's': return num; // seconds
|
||||
case 'm': return num * 60; // minutes
|
||||
case 'h': return num * 60 * 60; // hours
|
||||
case 'd': return num * 60 * 60 * 24; // days
|
||||
default:
|
||||
throw new Error(`Unsupported duration unit: ${unit}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user