Re_Backend/src/controllers/auth.controller.ts

317 lines
10 KiB
TypeScript

import { Request, Response } from 'express';
import { AuthService } from '../services/auth.service';
import { validateSSOCallback, validateRefreshToken, validateTokenExchange } from '../validators/auth.validator';
import { ResponseHandler } from '../utils/responseHandler';
import type { AuthenticatedRequest } from '../types/express';
import logger from '../utils/logger';
export class AuthController {
private authService: AuthService;
constructor() {
this.authService = new AuthService();
}
/**
* Handle SSO callback from frontend
* POST /api/v1/auth/sso-callback
*/
async handleSSOCallback(req: Request, res: Response): Promise<void> {
try {
// Validate request body
const validatedData = validateSSOCallback(req.body);
const result = await this.authService.handleSSOCallback(validatedData as any);
ResponseHandler.success(res, {
user: result.user,
accessToken: result.accessToken,
refreshToken: result.refreshToken
}, 'Authentication successful');
} catch (error) {
logger.error('SSO callback failed:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
ResponseHandler.error(res, 'Authentication failed', 400, errorMessage);
}
}
/**
* Get current user profile
* GET /api/v1/auth/me
*/
async getCurrentUser(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
const user = await this.authService.getUserProfile(req.user.userId);
if (!user) {
ResponseHandler.notFound(res, 'User not found');
return;
}
ResponseHandler.success(res, {
userId: user.userId,
employeeId: user.employeeId,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
displayName: user.displayName,
department: user.department,
designation: user.designation,
phone: user.phone,
location: user.location,
isAdmin: user.isAdmin,
isActive: user.isActive,
lastLogin: user.lastLogin,
createdAt: user.createdAt,
updatedAt: user.updatedAt
}, 'User profile retrieved successfully');
} catch (error) {
logger.error('Failed to get current user:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
ResponseHandler.error(res, 'Failed to get user profile', 500, errorMessage);
}
}
/**
* Refresh access token
* POST /api/v1/auth/refresh
* Supports both request body and cookie-based refresh tokens
*/
async refreshToken(req: Request, res: Response): Promise<void> {
try {
// Try to get refresh token from request body first, then from cookies
let refreshToken: string;
if (req.body?.refreshToken) {
const validated = validateRefreshToken(req.body);
refreshToken = validated.refreshToken;
} else if ((req as any).cookies?.refreshToken) {
// Fallback to cookie if available (requires cookie-parser middleware)
refreshToken = (req as any).cookies.refreshToken;
} else {
throw new Error('Refresh token is required');
}
const newAccessToken = await this.authService.refreshAccessToken(refreshToken);
// Set new access token in cookie if using cookie-based auth
const isProduction = process.env.NODE_ENV === 'production';
const cookieOptions = {
httpOnly: true,
secure: isProduction,
sameSite: 'lax' as const,
maxAge: 24 * 60 * 60 * 1000, // 24 hours
};
res.cookie('accessToken', newAccessToken, cookieOptions);
ResponseHandler.success(res, {
accessToken: newAccessToken
}, 'Token refreshed successfully');
} catch (error) {
logger.error('Token refresh failed:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
ResponseHandler.error(res, 'Token refresh failed', 401, errorMessage);
}
}
/**
* Logout user
* POST /api/v1/auth/logout
* Clears all authentication cookies and tokens
* IMPORTANT: Must use EXACT same cookie options as when setting cookies
*/
async logout(req: Request, res: Response): Promise<void> {
const isProduction = process.env.NODE_ENV === 'production';
// Helper function to clear cookies with all possible option combinations
const clearCookiesCompletely = () => {
const cookieNames = ['accessToken', 'refreshToken'];
// Get the EXACT options used when setting cookies (from exchangeToken)
// These MUST match exactly: httpOnly, secure, sameSite, path
const cookieOptions = {
httpOnly: true,
secure: isProduction,
sameSite: 'lax' as const,
path: '/',
};
logger.info('Attempting to clear cookies with options:', {
httpOnly: cookieOptions.httpOnly,
secure: cookieOptions.secure,
sameSite: cookieOptions.sameSite,
path: cookieOptions.path,
isProduction,
});
// Method 1: Set expired cookie with exact same options
// This is the most reliable method - sets cookie to expire immediately
const expiredDate = new Date(0); // Jan 1, 1970
cookieNames.forEach(name => {
res.cookie(name, '', {
httpOnly: cookieOptions.httpOnly,
secure: cookieOptions.secure,
sameSite: cookieOptions.sameSite,
path: cookieOptions.path,
expires: expiredDate,
maxAge: 0,
});
logger.info(`Set expired cookie: ${name}`);
});
// Method 2: Use clearCookie with exact same options
// clearCookie requires same options that were used to set the cookie
cookieNames.forEach(name => {
res.clearCookie(name, cookieOptions);
logger.info(`Called clearCookie for: ${name}`);
});
// Method 3: Try without secure flag (for localhost/development)
if (!isProduction) {
cookieNames.forEach(name => {
res.clearCookie(name, {
httpOnly: true,
secure: false,
sameSite: 'lax',
path: '/',
});
});
}
// Method 4: Try with all possible path variations
const paths = ['/', '/api', '/api/v1'];
paths.forEach(path => {
cookieNames.forEach(name => {
res.clearCookie(name, {
httpOnly: true,
secure: isProduction,
sameSite: 'lax',
path: path,
});
});
});
logger.info('Cookies clearing attempted with all methods', {
cookieNames,
isProduction,
paths: ['/', '/api', '/api/v1'],
});
};
try {
// Logout should work even without authentication (to clear cookies)
// User might be null if token was invalid/expired
const userId = req.user?.userId || 'unknown';
const email = req.user?.email || 'unknown';
logger.info('User logout initiated', {
userId,
email,
hasUser: !!req.user,
hasCookies: !!req.cookies?.accessToken || !!req.cookies?.refreshToken,
hasHeaderToken: !!req.headers.authorization,
});
// Clear all cookies using multiple methods
clearCookiesCompletely();
logger.info('User logout successful - cookies cleared', {
userId: req.user?.userId || 'unknown',
email: req.user?.email || 'unknown',
});
// Return success response
ResponseHandler.success(res, { message: 'Logout successful, cookies cleared' }, 'Logout successful');
} catch (error) {
logger.error('Logout failed:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
// Even on error, try to clear cookies as last resort
try {
clearCookiesCompletely();
} catch (cookieError) {
logger.error('Error clearing cookies in catch block:', cookieError);
}
ResponseHandler.error(res, 'Logout failed', 500, errorMessage);
}
}
/**
* Validate token endpoint
* GET /api/v1/auth/validate
*/
async validateToken(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
ResponseHandler.success(res, {
valid: true,
user: req.user
}, 'Token is valid');
} catch (error) {
logger.error('Token validation failed:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
ResponseHandler.error(res, 'Token validation failed', 401, errorMessage);
}
}
/**
* Exchange authorization code for tokens
* POST /api/v1/auth/token-exchange
*/
async exchangeToken(req: Request, res: Response): Promise<void> {
try {
logger.info('Token exchange request received', {
body: {
code: req.body?.code ? `${req.body.code.substring(0, 10)}...` : 'MISSING',
redirectUri: req.body?.redirectUri,
},
headers: req.headers,
});
const { code, redirectUri } = validateTokenExchange(req.body);
logger.info('Token exchange validation passed', { redirectUri });
const result = await this.authService.exchangeCodeForTokens(code, redirectUri);
// Set cookies with httpOnly flag for security
const isProduction = process.env.NODE_ENV === 'production';
const cookieOptions = {
httpOnly: true,
secure: isProduction,
sameSite: 'lax' as const,
maxAge: 24 * 60 * 60 * 1000, // 24 hours for access token
};
res.cookie('accessToken', result.accessToken, cookieOptions);
const refreshCookieOptions = {
...cookieOptions,
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days for refresh token
};
res.cookie('refreshToken', result.refreshToken, refreshCookieOptions);
// Ensure Content-Type is set to JSON
res.setHeader('Content-Type', 'application/json');
logger.info('Sending token exchange response', {
hasUser: !!result.user,
hasAccessToken: !!result.accessToken,
hasRefreshToken: !!result.refreshToken,
});
ResponseHandler.success(res, {
user: result.user,
accessToken: result.accessToken,
refreshToken: result.refreshToken,
idToken: result.oktaIdToken // Include id_token for frontend logout
}, 'Token exchange successful');
} catch (error) {
logger.error('Token exchange failed:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
ResponseHandler.error(res, 'Token exchange failed', 400, errorMessage);
}
}
}