270 lines
7.3 KiB
TypeScript
270 lines
7.3 KiB
TypeScript
import { v4 as uuidv4 } from 'uuid';
|
|
import { AuthenticatedUser } from './auth.types';
|
|
import { EmailVerificationService } from './emailVerification.service';
|
|
import { databaseService } from '../database';
|
|
import { logger } from '../logging';
|
|
import { tokenService } from './token.service';
|
|
|
|
const emailVerificationService = new EmailVerificationService();
|
|
|
|
const authService = {
|
|
async register(email: string, password: string, username?: string) {
|
|
try {
|
|
logger.auth.register(`Attempting to register user: ${email}`);
|
|
|
|
// Check if user already exists
|
|
const existingUser = await databaseService.findUserByEmail(email);
|
|
if (existingUser) {
|
|
logger.auth.error(
|
|
`Registration failed: User already exists with email ${email}`
|
|
);
|
|
throw new Error('User already exists');
|
|
}
|
|
|
|
// Create user with password
|
|
const user = await databaseService.createUserWithPassword(
|
|
email,
|
|
password,
|
|
username
|
|
);
|
|
|
|
logger.auth.register(`User registered successfully: ${user._id}`, {
|
|
userId: user._id,
|
|
email,
|
|
});
|
|
|
|
// Generate and send verification token (in production)
|
|
const verificationToken =
|
|
await emailVerificationService.generateVerificationToken(
|
|
user as AuthenticatedUser
|
|
);
|
|
|
|
return { user, verificationToken };
|
|
} catch (error) {
|
|
if (error.message.includes('already exists')) {
|
|
throw new Error('User already exists');
|
|
}
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
async login(input: { email: string; password: string }) {
|
|
logger.auth.login(`Login attempt for: ${input.email}`);
|
|
|
|
// Find user by email
|
|
const user = await databaseService.findUserByEmail(input.email);
|
|
|
|
if (!user) {
|
|
logger.auth.error(`User not found for email: ${input.email}`);
|
|
throw new Error('User not found');
|
|
}
|
|
|
|
logger.auth.login('User found', {
|
|
email: user.email,
|
|
hasPassword: !!user.password,
|
|
role: user.role,
|
|
status: user.status,
|
|
emailVerified: user.emailVerified,
|
|
});
|
|
|
|
// Check if user has a password (email-based account)
|
|
if (!user.password) {
|
|
logger.auth.error('No password found - OAuth account');
|
|
throw new Error(
|
|
'This account was created with OAuth. Please use Google or GitHub to sign in.'
|
|
);
|
|
}
|
|
|
|
// Check if email is verified
|
|
if (!user.emailVerified) {
|
|
throw new Error('Email verification required');
|
|
}
|
|
|
|
// Simple password verification (in production, use bcrypt)
|
|
logger.auth.login('Comparing passwords', {
|
|
inputPassword: input.password,
|
|
storedPassword: user.password,
|
|
match: user.password === input.password,
|
|
});
|
|
|
|
if (user.password !== input.password) {
|
|
logger.auth.error('Password mismatch');
|
|
throw new Error('Invalid credentials');
|
|
}
|
|
|
|
logger.auth.login(`Login successful for: ${user.email}`);
|
|
|
|
// Return mock tokens for frontend compatibility
|
|
return {
|
|
user,
|
|
accessToken: 'mock_access_token_' + Date.now(),
|
|
refreshToken: 'mock_refresh_token_' + Date.now(),
|
|
};
|
|
},
|
|
|
|
async loginWithOAuth(
|
|
provider: 'google' | 'github',
|
|
userData: { email: string; username: string; avatar?: string }
|
|
) {
|
|
try {
|
|
// Try to find existing user by email
|
|
let user = await databaseService.findUserByEmail(userData.email);
|
|
|
|
if (!user) {
|
|
// Create new user from OAuth data
|
|
user = await databaseService.createUserFromOAuth(
|
|
userData.email,
|
|
userData.username,
|
|
provider
|
|
);
|
|
}
|
|
|
|
// Generate access tokens
|
|
return {
|
|
user,
|
|
accessToken: `oauth_${provider}_token_` + Date.now(),
|
|
refreshToken: `oauth_${provider}_refresh_` + Date.now(),
|
|
};
|
|
} catch (error) {
|
|
throw new Error(`OAuth login failed: ${error.message}`);
|
|
}
|
|
},
|
|
|
|
async verifyEmail(token: string) {
|
|
const user =
|
|
await emailVerificationService.validateVerificationToken(token);
|
|
|
|
if (!user) {
|
|
throw new Error('Invalid or expired verification token');
|
|
}
|
|
|
|
await emailVerificationService.markEmailVerified(user);
|
|
|
|
return user;
|
|
},
|
|
|
|
async changePassword(
|
|
userId: string,
|
|
currentPassword: string,
|
|
newPassword: string
|
|
) {
|
|
// Get user by ID
|
|
const user = await databaseService.getUserById(userId);
|
|
if (!user) {
|
|
logger.auth.error(
|
|
`Update user profile failed: User not found for ID ${userId}`
|
|
);
|
|
throw new Error('User not found');
|
|
}
|
|
|
|
// Check if user has a password (not OAuth user)
|
|
if (!user.password) {
|
|
throw new Error('Cannot change password for OAuth accounts');
|
|
}
|
|
|
|
// Verify current password
|
|
if (user.password !== currentPassword) {
|
|
throw new Error('Current password is incorrect');
|
|
}
|
|
|
|
// Validate new password
|
|
if (newPassword.length < 6) {
|
|
throw new Error('New password must be at least 6 characters long');
|
|
}
|
|
|
|
// Update user with new password (this should be hashed before calling)
|
|
const updatedUser = await databaseService.updateUser({
|
|
...user,
|
|
password: newPassword,
|
|
});
|
|
|
|
return {
|
|
user: updatedUser,
|
|
message: 'Password changed successfully',
|
|
};
|
|
},
|
|
|
|
async requestPasswordReset(email: string) {
|
|
const user = await databaseService.findUserByEmail(email);
|
|
|
|
if (!user) {
|
|
// Don't reveal if email exists or not for security
|
|
return {
|
|
message:
|
|
'If an account with this email exists, a password reset link has been sent.',
|
|
};
|
|
}
|
|
|
|
if (!user.password) {
|
|
throw new Error('Cannot reset password for OAuth accounts');
|
|
}
|
|
|
|
// Generate reset token (similar to verification token)
|
|
const resetToken = uuidv4().replace(/-/g, '');
|
|
const expiresAt = new Date();
|
|
expiresAt.setHours(expiresAt.getHours() + 1); // 1 hour expiry
|
|
|
|
// Persist reset token
|
|
await tokenService.savePasswordResetToken({
|
|
userId: user._id,
|
|
email: user.email!,
|
|
token: resetToken,
|
|
expiresAt,
|
|
});
|
|
|
|
// Send reset email
|
|
const emailSent = await emailVerificationService.sendPasswordResetEmail(
|
|
user.email!,
|
|
resetToken
|
|
);
|
|
|
|
return {
|
|
message:
|
|
'If an account with this email exists, a password reset link has been sent.',
|
|
emailSent,
|
|
};
|
|
},
|
|
|
|
async resetPassword(token: string, newPassword: string) {
|
|
// Load reset token
|
|
const resetToken = await tokenService.findPasswordResetToken(token);
|
|
|
|
if (!resetToken) {
|
|
throw new Error('Invalid or expired reset token');
|
|
}
|
|
|
|
// Check if token is expired
|
|
if (new Date() > new Date(resetToken.expiresAt)) {
|
|
throw new Error('Reset token has expired');
|
|
}
|
|
|
|
// Validate new password
|
|
if (newPassword.length < 6) {
|
|
throw new Error('Password must be at least 6 characters long');
|
|
}
|
|
|
|
// Get user by ID first
|
|
const user = await databaseService.getUserById(resetToken.userId);
|
|
if (!user) {
|
|
throw new Error('User not found');
|
|
}
|
|
|
|
// Update user with new password (this should be hashed before calling)
|
|
const updatedUser = await databaseService.updateUser({
|
|
...user,
|
|
password: newPassword,
|
|
});
|
|
|
|
// Remove used token
|
|
await tokenService.deletePasswordResetToken(token);
|
|
|
|
return {
|
|
user: updatedUser,
|
|
message: 'Password reset successfully',
|
|
};
|
|
},
|
|
};
|
|
|
|
export { authService };
|
|
export default authService;
|