218 lines
5.9 KiB
TypeScript
218 lines
5.9 KiB
TypeScript
|
import bcrypt from 'bcrypt';
|
||
|
import jwt from 'jsonwebtoken';
|
||
|
import { getUserByEmail, getUserByUsername, createUser } from './users';
|
||
|
import { ApiError } from './index';
|
||
|
|
||
|
export interface LoginCredentials {
|
||
|
email: string;
|
||
|
password: string;
|
||
|
}
|
||
|
|
||
|
export interface RegisterData {
|
||
|
email: string;
|
||
|
username: string;
|
||
|
displayName: string;
|
||
|
password: string;
|
||
|
}
|
||
|
|
||
|
export interface AuthToken {
|
||
|
token: string;
|
||
|
user: {
|
||
|
id: string;
|
||
|
email: string;
|
||
|
username: string;
|
||
|
displayName: string;
|
||
|
isAdmin: boolean;
|
||
|
isModerator: boolean;
|
||
|
};
|
||
|
}
|
||
|
|
||
|
const JWT_SECRET = process.env.JWT_SECRET || 'default-secret-key';
|
||
|
const SALT_ROUNDS = 12;
|
||
|
|
||
|
// Hash password
|
||
|
export async function hashPassword(password: string): Promise<string> {
|
||
|
try {
|
||
|
return await bcrypt.hash(password, SALT_ROUNDS);
|
||
|
} catch (error) {
|
||
|
throw new ApiError('Failed to hash password', 500);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Verify password
|
||
|
export async function verifyPassword(password: string, hashedPassword: string): Promise<boolean> {
|
||
|
try {
|
||
|
return await bcrypt.compare(password, hashedPassword);
|
||
|
} catch (error) {
|
||
|
throw new ApiError('Failed to verify password', 500);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Generate JWT token
|
||
|
export function generateToken(user: any): string {
|
||
|
const payload = {
|
||
|
id: user.id,
|
||
|
email: user.email,
|
||
|
username: user.username,
|
||
|
displayName: user.displayName,
|
||
|
isAdmin: user.isAdmin,
|
||
|
isModerator: user.isModerator,
|
||
|
};
|
||
|
|
||
|
return jwt.sign(payload, JWT_SECRET, { expiresIn: '7d' });
|
||
|
}
|
||
|
|
||
|
// Verify JWT token
|
||
|
export function verifyToken(token: string): any {
|
||
|
try {
|
||
|
return jwt.verify(token, JWT_SECRET);
|
||
|
} catch (error) {
|
||
|
throw new ApiError('Invalid or expired token', 401);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Login user
|
||
|
export async function login(credentials: LoginCredentials): Promise<AuthToken> {
|
||
|
try {
|
||
|
const user = await getUserByEmail(credentials.email);
|
||
|
if (!user) {
|
||
|
throw new ApiError('Invalid email or password', 401);
|
||
|
}
|
||
|
|
||
|
// Note: In a real implementation, you would verify the password against a hash
|
||
|
// For this demo, we'll assume password verification passes
|
||
|
// const isValidPassword = await verifyPassword(credentials.password, user.passwordHash);
|
||
|
// if (!isValidPassword) {
|
||
|
// throw new ApiError('Invalid email or password', 401);
|
||
|
// }
|
||
|
|
||
|
const token = generateToken(user);
|
||
|
|
||
|
return {
|
||
|
token,
|
||
|
user: {
|
||
|
id: user.id,
|
||
|
email: user.email,
|
||
|
username: user.username,
|
||
|
displayName: user.displayName,
|
||
|
isAdmin: user.isAdmin || false,
|
||
|
isModerator: user.isModerator || false,
|
||
|
},
|
||
|
};
|
||
|
} catch (error) {
|
||
|
if (error instanceof ApiError) throw error;
|
||
|
throw new ApiError('Login failed', 500);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Register user
|
||
|
export async function register(data: RegisterData): Promise<AuthToken> {
|
||
|
try {
|
||
|
// Check if email already exists
|
||
|
const existingEmail = await getUserByEmail(data.email);
|
||
|
if (existingEmail) {
|
||
|
throw new ApiError('Email already registered', 400);
|
||
|
}
|
||
|
|
||
|
// Check if username already exists
|
||
|
const existingUsername = await getUserByUsername(data.username);
|
||
|
if (existingUsername) {
|
||
|
throw new ApiError('Username already taken', 400);
|
||
|
}
|
||
|
|
||
|
// Validate email format
|
||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
|
if (!emailRegex.test(data.email)) {
|
||
|
throw new ApiError('Invalid email format', 400);
|
||
|
}
|
||
|
|
||
|
// Validate username format
|
||
|
const usernameRegex = /^[a-zA-Z0-9_]{3,20}$/;
|
||
|
if (!usernameRegex.test(data.username)) {
|
||
|
throw new ApiError('Username must be 3-20 characters and contain only letters, numbers, and underscores', 400);
|
||
|
}
|
||
|
|
||
|
// Validate password strength
|
||
|
if (data.password.length < 6) {
|
||
|
throw new ApiError('Password must be at least 6 characters long', 400);
|
||
|
}
|
||
|
|
||
|
// Hash password and create user
|
||
|
// const passwordHash = await hashPassword(data.password);
|
||
|
|
||
|
const user = await createUser({
|
||
|
email: data.email,
|
||
|
username: data.username,
|
||
|
displayName: data.displayName,
|
||
|
// passwordHash, // In a real implementation
|
||
|
});
|
||
|
|
||
|
const token = generateToken(user);
|
||
|
|
||
|
return {
|
||
|
token,
|
||
|
user: {
|
||
|
id: user.id,
|
||
|
email: user.email,
|
||
|
username: user.username,
|
||
|
displayName: user.displayName,
|
||
|
isAdmin: user.isAdmin || false,
|
||
|
isModerator: user.isModerator || false,
|
||
|
},
|
||
|
};
|
||
|
} catch (error) {
|
||
|
if (error instanceof ApiError) throw error;
|
||
|
throw new ApiError('Registration failed', 500);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Refresh token
|
||
|
export async function refreshToken(token: string): Promise<AuthToken> {
|
||
|
try {
|
||
|
const decoded = verifyToken(token);
|
||
|
const user = await getUserByEmail(decoded.email);
|
||
|
|
||
|
if (!user) {
|
||
|
throw new ApiError('User not found', 404);
|
||
|
}
|
||
|
|
||
|
const newToken = generateToken(user);
|
||
|
|
||
|
return {
|
||
|
token: newToken,
|
||
|
user: {
|
||
|
id: user.id,
|
||
|
email: user.email,
|
||
|
username: user.username,
|
||
|
displayName: user.displayName,
|
||
|
isAdmin: user.isAdmin || false,
|
||
|
isModerator: user.isModerator || false,
|
||
|
},
|
||
|
};
|
||
|
} catch (error) {
|
||
|
if (error instanceof ApiError) throw error;
|
||
|
throw new ApiError('Token refresh failed', 500);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Change password
|
||
|
export async function changePassword(_userId: string, _currentPassword: string, newPassword: string): Promise<boolean> {
|
||
|
try {
|
||
|
// In a real implementation, you would:
|
||
|
// 1. Get user by ID
|
||
|
// 2. Verify current password
|
||
|
// 3. Hash new password
|
||
|
// 4. Update user record
|
||
|
|
||
|
if (newPassword.length < 6) {
|
||
|
throw new ApiError('New password must be at least 6 characters long', 400);
|
||
|
}
|
||
|
|
||
|
// Placeholder for password change logic
|
||
|
return true;
|
||
|
} catch (error) {
|
||
|
if (error instanceof ApiError) throw error;
|
||
|
throw new ApiError('Password change failed', 500);
|
||
|
}
|
||
|
}
|