Skip to content
Open
10 changes: 7 additions & 3 deletions src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { AuthService } from "./auth.service";
import { JwtAuthGuard } from "src/common/guards/keycloak.guard";
import { APIID } from "src/common/utils/api-id.config";
import { AllExceptionsFilter } from "src/common/filters/exception.filter";
import { Response } from "express";
import { Response, Request } from "express";

@ApiTags("Auth")
@Controller("auth")
Expand All @@ -43,8 +43,12 @@ export class AuthController {
@UsePipes(ValidationPipe)
@HttpCode(HttpStatus.OK)
@ApiForbiddenResponse({ description: "Forbidden" })
public async login(@Body() authDto: AuthDto, @Res() response: Response) {
return this.authService.login(authDto, response);
public async login(
@Body() authDto: AuthDto,
@Req() request: Request,
@Res() response: Response
) {
return this.authService.login(authDto, request, response);
}

@UseFilters(new AllExceptionsFilter(APIID.USER_AUTH))
Expand Down
154 changes: 151 additions & 3 deletions src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import jwt_decode from 'jwt-decode';
import APIResponse from 'src/common/responses/response';
import { KeycloakService } from 'src/common/utils/keycloak.service';
import { APIID } from 'src/common/utils/api-id.config';
import { Response } from 'express';
import { Response, Request } from 'express';
import { LoggerUtil } from 'src/common/logger/LoggerUtil';
import { AuthDto } from './dto/auth-dto';

type LoginResponse = {
access_token: string;
Expand All @@ -25,9 +27,21 @@ export class AuthService {
private readonly keycloakService: KeycloakService
) {}

async login(authDto, response: Response) {
async login(authDto: AuthDto, request: Request, response: Response) {
const apiId = APIID.LOGIN;
const { username, password } = authDto;

// Extract request information for logging
const userAgent = request.headers['user-agent'] || 'Unknown';

// Log login attempt start (username and IP excluded for legal compliance)
LoggerUtil.log(
`Login attempt initiated - User-Agent: ${userAgent}`,
'AuthService',
undefined,
'info'
);

try {
// Fetch user details by username
const userData = await this.useradapter
Expand All @@ -40,6 +54,15 @@ export class AuthService {
? 'User details not found for user'
: 'User is inactive, please verify your email';

const failureReason = userData ? 'USER_INACTIVE' : 'USER_NOT_FOUND';

// Log failed login attempt with reason and status code (username and IP excluded for legal compliance)
LoggerUtil.error(
`Login failed - StatusCode: ${HttpStatus.BAD_REQUEST}, Reason: ${failureReason}, Message: ${errorMessage}, IssueType: CLIENT_ERROR`,
errorMessage,
'AuthService'
);

return APIResponse.error(
response,
apiId,
Expand All @@ -66,6 +89,14 @@ export class AuthService {
token_type,
};

// Log successful login with status code (username and IP excluded for legal compliance)
LoggerUtil.log(
`Login successful - User-Agent: ${userAgent}, StatusCode: ${HttpStatus.OK}`,
'AuthService',
undefined,
'info'
);

return APIResponse.success(
response,
apiId,
Expand All @@ -75,9 +106,52 @@ export class AuthService {
);
} catch (error) {
if (error.response && error.response.status === 401) {
// Log invalid credentials with status code (username and IP excluded for legal compliance)
LoggerUtil.error(
`Login failed - StatusCode: ${HttpStatus.UNAUTHORIZED}, Reason: INVALID_CREDENTIALS, Message: Invalid username or password, IssueType: CLIENT_ERROR`,
'Invalid username or password',
'AuthService'
);
throw new NotFoundException('Invalid username or password');
Comment thread
Tusharmahajan12 marked this conversation as resolved.
} else {
const errorMessage = error?.message || 'Something went wrong';
const errorStack = error?.stack || 'No stack trace available';
const httpStatus =
error?.response?.status || HttpStatus.INTERNAL_SERVER_ERROR;
const issueType = httpStatus >= 500 ? 'SERVER_ERROR' : 'CLIENT_ERROR';

// Determine failure reason based on httpStatus
let failureReason = 'INTERNAL_SERVER_ERROR';
if (httpStatus >= 400 && httpStatus < 500) {
if (httpStatus === 400) {
failureReason = 'BAD_REQUEST';
} else if (httpStatus === 403) {
failureReason = 'FORBIDDEN';
} else if (httpStatus === 404) {
failureReason = 'NOT_FOUND';
} else if (httpStatus === 429) {
failureReason = 'RATE_LIMIT_EXCEEDED';
} else {
failureReason = 'CLIENT_ERROR';
}
} else if (httpStatus >= 500) {
if (httpStatus === 502) {
failureReason = 'BAD_GATEWAY';
} else if (httpStatus === 503) {
failureReason = 'SERVICE_UNAVAILABLE';
} else if (httpStatus === 504) {
failureReason = 'GATEWAY_TIMEOUT';
}
// failureReason already defaults to 'INTERNAL_SERVER_ERROR' for other 5xx errors
}

// Log error with status code and issue type (username and IP excluded for legal compliance)
LoggerUtil.error(
`Login failed - StatusCode: ${httpStatus}, Reason: ${failureReason}, Message: ${errorMessage}, IssueType: ${issueType}`,
errorStack,
'AuthService'
);
Comment thread
Tusharmahajan12 marked this conversation as resolved.

return APIResponse.error(
response,
apiId,
Expand All @@ -91,13 +165,45 @@ export class AuthService {

public async getUserByAuth(request: any, tenantId, response: Response) {
const apiId = APIID.USER_AUTH;

// Extract request information for logging
const userAgent = request.headers['user-agent'] || 'Unknown';

try {
// Log API call attempt (username and IP excluded for legal compliance)
LoggerUtil.log(
`GetUserByAuth attempt - User-Agent: ${userAgent}, TenantId: ${
tenantId || 'Not provided'
}`,
'AuthService',
undefined,
'info'
);

// Decode JWT token to get username
const decoded: any = jwt_decode(request.headers.authorization);
const username = decoded.preferred_username;
const username = decoded.preferred_username || 'Unknown';

// Log with username after decoding (username, userId, and IP excluded for legal compliance)
LoggerUtil.log(
`GetUserByAuth processing - TenantId: ${tenantId || 'Not provided'}`,
'AuthService',
undefined,
'info'
);

const data = await this.useradapter
.buildUserAdapter()
.findUserDetails(null, username, tenantId);

// Log successful response (username, userId, and IP excluded for legal compliance)
LoggerUtil.log(
`GetUserByAuth successful - StatusCode: ${HttpStatus.OK}`,
'AuthService',
undefined,
'info'
);

return APIResponse.success(
response,
apiId,
Expand All @@ -107,6 +213,48 @@ export class AuthService {
);
} catch (e) {
const errorMessage = e?.message || 'Something went wrong';
const errorStack = e?.stack || 'No stack trace available';

// Determine error type for logging purposes (but keep API response consistent)
let detectedStatus = HttpStatus.INTERNAL_SERVER_ERROR;
let failureReason = 'INTERNAL_SERVER_ERROR';
let issueType = 'SERVER_ERROR';

if (
e.name === 'JsonWebTokenError' ||
e.message?.includes('token') ||
e.message?.includes('jwt')
) {
detectedStatus = HttpStatus.UNAUTHORIZED;
failureReason = 'INVALID_TOKEN';
issueType = 'CLIENT_ERROR';
} else if (
e.message?.includes('not found') ||
e.message?.includes('does not exist')
) {
detectedStatus = HttpStatus.NOT_FOUND;
failureReason = 'USER_NOT_FOUND';
issueType = 'CLIENT_ERROR';
} else if (
e.message?.includes('unauthorized') ||
e.message?.includes('forbidden')
) {
detectedStatus = HttpStatus.FORBIDDEN;
failureReason = 'UNAUTHORIZED';
issueType = 'CLIENT_ERROR';
}

// Log failed attempt with comprehensive details (username, userId, and IP excluded for legal compliance)
LoggerUtil.error(
`GetUserByAuth failed - DetectedStatusCode: ${detectedStatus}, Reason: ${failureReason}, Message: ${errorMessage}, IssueType: ${issueType}, TenantId: ${
tenantId || 'Not provided'
}`,
errorStack,
'AuthService',
undefined
);

// Keep original API response behavior - always return INTERNAL_SERVER_ERROR
return APIResponse.error(
response,
apiId,
Expand Down
112 changes: 89 additions & 23 deletions src/common/logger/LoggerUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,57 +22,123 @@ export class LoggerUtil {
level: 'info',
format: winston.format.combine(winston.format.timestamp(), customFormat),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
new winston.transports.Console({
// Console logging is fast and non-blocking
handleExceptions: false,
handleRejections: false,
}),
new winston.transports.File({
filename: 'error.log',
level: 'error',
// Log rotation configuration for performance and disk space management
// When log file reaches 5MB, it will be rotated (renamed) and a new file created
maxsize: 5242880, // 5MB - maximum size before rotation
maxFiles: 5, // Keep maximum 5 rotated log files (error.log, error.log.1, error.log.2, etc.)
tailable: true, // Oldest logs are deleted when maxFiles is reached
}),
new winston.transports.File({
filename: 'combined.log',
// Log rotation configuration for performance and disk space management
// When log file reaches 5MB, it will be rotated (renamed) and a new file created
maxsize: 5242880, // 5MB - maximum size before rotation
maxFiles: 5, // Keep maximum 5 rotated log files (combined.log, combined.log.1, combined.log.2, etc.)
tailable: true, // Oldest logs are deleted when maxFiles is reached
}),
],
// Prevent Winston from exiting the process when logging errors occur
// This ensures logging failures don't crash the application
exitOnError: false,
});
}
return this.logger;
}

/**
* Non-blocking log method - uses process.nextTick to offload logging
* This ensures API responses are not delayed by logging operations
*/
static log(
message: string,
context?: string,
user?: string,
level: string = 'info',
) {
this.getLogger().log({
level: level,
message: message,
context: context,
user: user,
timestamp: new Date().toISOString(),
// Use process.nextTick to make logging non-blocking
// This ensures the API response is sent before logging completes
process.nextTick(() => {
try {
this.getLogger().log({
level: level,
message: message,
context: context,
user: user,
timestamp: new Date().toISOString(),
});
} catch (err) {
// Silently fail - don't let logging errors affect API responses
// Only log to console as last resort
console.error('Logger error:', err);
}
});
}

/**
* Non-blocking error log method
*/
static error(
message: string,
error?: string,
context?: string,
user?: string,
) {
this.getLogger().error({
message: message,
error: error,
context: context,
user: user,
timestamp: new Date().toISOString(),
// Use process.nextTick to make logging non-blocking
process.nextTick(() => {
try {
this.getLogger().error({
message: message,
error: error,
context: context,
user: user,
timestamp: new Date().toISOString(),
});
} catch (err) {
// Silently fail - don't let logging errors affect API responses
console.error('Logger error:', err);
}
});
}

/**
* Non-blocking warn log method
*/
static warn(message: string, context?: string) {
this.getLogger().warn({
message: message,
context: context,
timestamp: new Date().toISOString(),
process.nextTick(() => {
try {
this.getLogger().warn({
message: message,
context: context,
timestamp: new Date().toISOString(),
});
} catch (err) {
console.error('Logger error:', err);
}
});
}

/**
* Non-blocking debug log method
*/
static debug(message: string, context?: string) {
this.getLogger().debug({
message: message,
context: context,
timestamp: new Date().toISOString(),
process.nextTick(() => {
try {
this.getLogger().debug({
message: message,
context: context,
timestamp: new Date().toISOString(),
});
} catch (err) {
console.error('Logger error:', err);
}
});
}
}
Loading