Initial commit: Complete NodeJS-native setup
- Migrated from Python pre-commit to NodeJS-native solution - Reorganized documentation structure - Set up Husky + lint-staged for efficient pre-commit hooks - Fixed Dockerfile healthcheck issue - Added comprehensive documentation index
This commit is contained in:
244
services/auth/auth.service.ts
Normal file
244
services/auth/auth.service.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { dbService } from '../../services/couchdb.factory';
|
||||
import { AccountStatus } from './auth.constants';
|
||||
import { User } from '../../types';
|
||||
import { AuthenticatedUser } from './auth.types';
|
||||
import { EmailVerificationService } from './emailVerification.service';
|
||||
|
||||
const emailVerificationService = new EmailVerificationService();
|
||||
|
||||
const authService = {
|
||||
async register(email: string, password: string, username?: string) {
|
||||
try {
|
||||
// Create user with password
|
||||
const user = await dbService.createUserWithPassword(
|
||||
email,
|
||||
password,
|
||||
username
|
||||
);
|
||||
|
||||
// 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('An account with this email already exists');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async login(input: { email: string; password: string }) {
|
||||
console.log('🔐 Login attempt for:', input.email);
|
||||
|
||||
// Find user by email
|
||||
const user = await dbService.findUserByEmail(input.email);
|
||||
|
||||
if (!user) {
|
||||
console.log('❌ User not found for email:', input.email);
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
console.log('👤 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) {
|
||||
console.log('❌ No password found - OAuth account');
|
||||
throw new Error(
|
||||
'This account was created with OAuth. Please use Google or GitHub to sign in.'
|
||||
);
|
||||
}
|
||||
|
||||
// Simple password verification (in production, use bcrypt)
|
||||
console.log('🔍 Comparing passwords:', {
|
||||
inputPassword: input.password,
|
||||
storedPassword: user.password,
|
||||
match: user.password === input.password,
|
||||
});
|
||||
|
||||
if (user.password !== input.password) {
|
||||
console.log('❌ Password mismatch');
|
||||
throw new Error('Invalid password');
|
||||
}
|
||||
|
||||
console.log('✅ 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 dbService.findUserByEmail(userData.email);
|
||||
|
||||
if (!user) {
|
||||
// Create new user from OAuth data
|
||||
user = await dbService.createUserFromOAuth(userData);
|
||||
}
|
||||
|
||||
// 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 dbService.getUserById(userId);
|
||||
|
||||
if (!user) {
|
||||
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 password
|
||||
const updatedUser = await dbService.changeUserPassword(userId, newPassword);
|
||||
|
||||
return {
|
||||
user: updatedUser,
|
||||
message: 'Password changed successfully',
|
||||
};
|
||||
},
|
||||
|
||||
async requestPasswordReset(email: string) {
|
||||
const user = await dbService.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
|
||||
|
||||
// Store reset token (in production, save to database)
|
||||
const resetTokens = JSON.parse(
|
||||
localStorage.getItem('password_reset_tokens') || '[]'
|
||||
);
|
||||
resetTokens.push({
|
||||
userId: user._id,
|
||||
email: user.email,
|
||||
token: resetToken,
|
||||
expiresAt,
|
||||
});
|
||||
localStorage.setItem('password_reset_tokens', JSON.stringify(resetTokens));
|
||||
|
||||
// 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) {
|
||||
// Get reset tokens
|
||||
const resetTokens = JSON.parse(
|
||||
localStorage.getItem('password_reset_tokens') || '[]'
|
||||
);
|
||||
const resetToken = resetTokens.find((t: any) => t.token === 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');
|
||||
}
|
||||
|
||||
// Update password
|
||||
const updatedUser = await dbService.changeUserPassword(
|
||||
resetToken.userId,
|
||||
newPassword
|
||||
);
|
||||
|
||||
// Remove used token
|
||||
const filteredTokens = resetTokens.filter((t: any) => t.token !== token);
|
||||
localStorage.setItem(
|
||||
'password_reset_tokens',
|
||||
JSON.stringify(filteredTokens)
|
||||
);
|
||||
|
||||
return {
|
||||
user: updatedUser,
|
||||
message: 'Password reset successfully',
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export { authService };
|
||||
export default authService;
|
||||
Reference in New Issue
Block a user