507 lines
12 KiB
TypeScript
507 lines
12 KiB
TypeScript
import express from 'express';
|
|
import { AuthService } from '../services/auth.service';
|
|
import { authenticateToken, blacklistToken } from '../middleware/auth';
|
|
import config from '../config';
|
|
|
|
// Temporary mock implementations
|
|
const prisma = {
|
|
user: {
|
|
findUnique: async (params: any): Promise<any> => {
|
|
return {
|
|
id: 'mock-user-id',
|
|
email: 'mock@example.com',
|
|
firstName: 'Mock',
|
|
lastName: 'User',
|
|
subscription: 'FREE',
|
|
isActive: true,
|
|
passwordHash: '$2b$12$mockhashedpassword',
|
|
createdAt: new Date(),
|
|
usageMetrics: {
|
|
pagesCloned: 0,
|
|
storageUsed: 0,
|
|
apiCalls: 0,
|
|
lastReset: new Date()
|
|
}
|
|
};
|
|
},
|
|
create: async (params: any): Promise<any> => ({
|
|
id: 'mock-user-id',
|
|
email: params.data.email,
|
|
firstName: params.data.firstName,
|
|
lastName: params.data.lastName,
|
|
subscription: 'FREE',
|
|
createdAt: new Date()
|
|
}),
|
|
update: async (params: any): Promise<any> => ({
|
|
id: 'mock-user-id',
|
|
email: 'mock@example.com',
|
|
firstName: 'Mock',
|
|
lastName: 'User',
|
|
subscription: 'FREE',
|
|
updatedAt: new Date()
|
|
})
|
|
},
|
|
userSession: {
|
|
create: async (params: any): Promise<any> => ({ id: 'mock-session' }),
|
|
deleteMany: async (params: any): Promise<any> => ({ count: 0 })
|
|
}
|
|
};
|
|
|
|
const validateUserRegistration = (data: any) => {
|
|
if (!data.email || !data.password) {
|
|
throw new Error('Email and password are required');
|
|
}
|
|
return data;
|
|
};
|
|
|
|
const validateUserLogin = (data: any) => {
|
|
if (!data.email || !data.password) {
|
|
throw new Error('Email and password are required');
|
|
}
|
|
return data;
|
|
};
|
|
|
|
const router = express.Router();
|
|
|
|
// Register new user
|
|
router.post('/register', async (req, res, next) => {
|
|
try {
|
|
// Validate input
|
|
const validatedData = validateUserRegistration(req.body);
|
|
|
|
// Check if user already exists
|
|
const existingUser = await prisma.user.findUnique({
|
|
where: { email: validatedData.email }
|
|
});
|
|
|
|
if (existingUser) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'User with this email already exists',
|
|
code: 'USER_EXISTS'
|
|
});
|
|
}
|
|
|
|
// Hash password
|
|
const passwordHash = await AuthService.hashPassword(validatedData.password);
|
|
|
|
// Create user
|
|
const user = await prisma.user.create({
|
|
data: {
|
|
email: validatedData.email,
|
|
passwordHash,
|
|
firstName: validatedData.firstName,
|
|
lastName: validatedData.lastName,
|
|
subscription: 'FREE',
|
|
usageMetrics: {
|
|
create: {
|
|
pagesCloned: 0,
|
|
storageUsed: 0,
|
|
apiCalls: 0
|
|
}
|
|
}
|
|
},
|
|
select: {
|
|
id: true,
|
|
email: true,
|
|
firstName: true,
|
|
lastName: true,
|
|
subscription: true,
|
|
createdAt: true
|
|
}
|
|
});
|
|
|
|
// Generate JWT token pair
|
|
const tokenPair = AuthService.generateTokenPair(
|
|
user.id,
|
|
user.email,
|
|
user.subscription
|
|
);
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
data: {
|
|
user,
|
|
...tokenPair
|
|
},
|
|
message: 'User registered successfully'
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Login user
|
|
router.post('/login', async (req, res, next) => {
|
|
try {
|
|
// Validate input
|
|
const validatedData = validateUserLogin(req.body);
|
|
|
|
// Find user
|
|
const user = await prisma.user.findUnique({
|
|
where: { email: validatedData.email },
|
|
select: {
|
|
id: true,
|
|
email: true,
|
|
passwordHash: true,
|
|
firstName: true,
|
|
lastName: true,
|
|
subscription: true,
|
|
isActive: true
|
|
}
|
|
});
|
|
|
|
if (!user || !user.isActive) {
|
|
return res.status(401).json({
|
|
success: false,
|
|
error: 'Invalid email or password',
|
|
code: 'INVALID_CREDENTIALS'
|
|
});
|
|
}
|
|
|
|
// Verify password
|
|
const isValidPassword = await AuthService.verifyPassword(validatedData.password, user.passwordHash);
|
|
|
|
if (!isValidPassword) {
|
|
return res.status(401).json({
|
|
success: false,
|
|
error: 'Invalid email or password',
|
|
code: 'INVALID_CREDENTIALS'
|
|
});
|
|
}
|
|
|
|
// Generate JWT token pair
|
|
const tokenPair = AuthService.generateTokenPair(
|
|
user.id,
|
|
user.email,
|
|
user.subscription
|
|
);
|
|
|
|
// Create session record
|
|
await prisma.userSession.create({
|
|
data: {
|
|
userId: user.id,
|
|
token: tokenPair.accessToken,
|
|
expiresAt: new Date(Date.now() + tokenPair.expiresIn * 1000)
|
|
}
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
user: {
|
|
id: user.id,
|
|
email: user.email,
|
|
firstName: user.firstName,
|
|
lastName: user.lastName,
|
|
subscription: user.subscription
|
|
},
|
|
...tokenPair
|
|
},
|
|
message: 'Login successful'
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Logout user
|
|
router.post('/logout', authenticateToken, async (req, res, next) => {
|
|
try {
|
|
const authHeader = req.headers.authorization;
|
|
const token = authHeader && authHeader.split(' ')[1];
|
|
|
|
if (token) {
|
|
// Blacklist the access token
|
|
blacklistToken(token);
|
|
|
|
// Remove session from database
|
|
await prisma.userSession.deleteMany({
|
|
where: {
|
|
userId: req.user!.id,
|
|
token
|
|
}
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Logout successful'
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Refresh token endpoint
|
|
router.post('/refresh', async (req, res, next) => {
|
|
try {
|
|
const { refreshToken } = req.body;
|
|
|
|
if (!refreshToken) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Refresh token is required',
|
|
code: 'VALIDATION_ERROR'
|
|
});
|
|
}
|
|
|
|
// Generate new token pair using refresh token
|
|
const newTokenPair = AuthService.refreshAccessToken(refreshToken);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: newTokenPair,
|
|
message: 'Token refreshed successfully'
|
|
});
|
|
} catch (error) {
|
|
return res.status(401).json({
|
|
success: false,
|
|
error: 'Invalid or expired refresh token',
|
|
code: 'INVALID_REFRESH_TOKEN'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Logout from all devices
|
|
router.post('/logout-all', authenticateToken, async (req, res, next) => {
|
|
try {
|
|
// Revoke all tokens for the user
|
|
AuthService.revokeAllUserTokens(req.user!.id);
|
|
|
|
// Remove all sessions from database
|
|
await prisma.userSession.deleteMany({
|
|
where: { userId: req.user!.id }
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Logged out from all devices successfully'
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Get current user profile
|
|
router.get('/me', authenticateToken, async (req, res, next) => {
|
|
try {
|
|
const user = await prisma.user.findUnique({
|
|
where: { id: req.user!.id },
|
|
select: {
|
|
id: true,
|
|
email: true,
|
|
firstName: true,
|
|
lastName: true,
|
|
avatar: true,
|
|
subscription: true,
|
|
emailVerified: true,
|
|
createdAt: true,
|
|
usageMetrics: {
|
|
select: {
|
|
pagesCloned: true,
|
|
storageUsed: true,
|
|
apiCalls: true,
|
|
lastReset: true
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
if (!user) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'User not found',
|
|
code: 'USER_NOT_FOUND'
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
data: user
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Update user profile
|
|
router.patch('/me', authenticateToken, async (req, res, next) => {
|
|
try {
|
|
const { firstName, lastName, avatar } = req.body;
|
|
|
|
const updatedUser = await prisma.user.update({
|
|
where: { id: req.user!.id },
|
|
data: {
|
|
...(firstName && { firstName }),
|
|
...(lastName && { lastName }),
|
|
...(avatar && { avatar })
|
|
},
|
|
select: {
|
|
id: true,
|
|
email: true,
|
|
firstName: true,
|
|
lastName: true,
|
|
avatar: true,
|
|
subscription: true,
|
|
updatedAt: true
|
|
}
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: updatedUser,
|
|
message: 'Profile updated successfully'
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Change password
|
|
router.post('/change-password', authenticateToken, async (req, res, next) => {
|
|
try {
|
|
const { currentPassword, newPassword } = req.body;
|
|
|
|
if (!currentPassword || !newPassword) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Current password and new password are required',
|
|
code: 'VALIDATION_ERROR'
|
|
});
|
|
}
|
|
|
|
if (newPassword.length < 8) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'New password must be at least 8 characters long',
|
|
code: 'VALIDATION_ERROR'
|
|
});
|
|
}
|
|
|
|
// Get current user with password
|
|
const user = await prisma.user.findUnique({
|
|
where: { id: req.user!.id },
|
|
select: { passwordHash: true }
|
|
});
|
|
|
|
if (!user) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'User not found',
|
|
code: 'USER_NOT_FOUND'
|
|
});
|
|
}
|
|
|
|
// Verify current password
|
|
const isValidPassword = await AuthService.verifyPassword(currentPassword, user.passwordHash);
|
|
|
|
if (!isValidPassword) {
|
|
return res.status(401).json({
|
|
success: false,
|
|
error: 'Current password is incorrect',
|
|
code: 'INVALID_CREDENTIALS'
|
|
});
|
|
}
|
|
|
|
// Hash new password
|
|
const newPasswordHash = await AuthService.hashPassword(newPassword);
|
|
|
|
// Update password
|
|
await prisma.user.update({
|
|
where: { id: req.user!.id },
|
|
data: { passwordHash: newPasswordHash }
|
|
});
|
|
|
|
// Invalidate all existing sessions
|
|
AuthService.revokeAllUserTokens(req.user!.id);
|
|
await prisma.userSession.deleteMany({
|
|
where: { userId: req.user!.id }
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Password changed successfully. Please log in again.'
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
export default router;
|
|
// Get active sessions
|
|
router.get('/sessions', authenticateToken, async (req, res, next) => {
|
|
try {
|
|
const sessions = AuthService.getUserActiveSessions(req.user!.id);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
sessions: sessions.map(session => ({
|
|
sessionId: session.sessionId,
|
|
expiresAt: session.expiresAt,
|
|
isCurrentSession: false // TODO: Identify current session
|
|
}))
|
|
}
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Revoke specific session
|
|
router.delete('/sessions/:sessionId', authenticateToken, async (req, res, next) => {
|
|
try {
|
|
const { sessionId } = req.params;
|
|
|
|
// Revoke specific session
|
|
AuthService.revokeUserSession(req.user!.id, sessionId);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Session revoked successfully'
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Validate token endpoint (for debugging/testing)
|
|
router.post('/validate', async (req, res, next) => {
|
|
try {
|
|
const { token } = req.body;
|
|
|
|
if (!token) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Token is required',
|
|
code: 'VALIDATION_ERROR'
|
|
});
|
|
}
|
|
|
|
const payload = AuthService.verifyToken(token);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
valid: true,
|
|
payload: {
|
|
userId: payload.userId,
|
|
email: payload.email,
|
|
subscription: payload.subscription,
|
|
type: payload.type,
|
|
sessionId: payload.sessionId,
|
|
expiresAt: new Date(payload.exp! * 1000)
|
|
}
|
|
}
|
|
});
|
|
} catch (error) {
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
valid: false,
|
|
error: error instanceof Error ? error.message : 'Invalid token'
|
|
}
|
|
});
|
|
}
|
|
}); |