Skip to content
Open
43 changes: 29 additions & 14 deletions src/adapters/postgres/cohort-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import { FieldValueConverter } from 'src/utils/field-value-converter';
export class PostgresCohortService {
// Cache for repository column names (static data)
private cachedCohortColumnNames: string[] | null = null;

// Cache for custom fields metadata (with TTL)
private customFieldsCache: {
data: any[];
Expand Down Expand Up @@ -333,8 +333,10 @@ export class PostgresCohortService {
return new Map();
}

// Optimized query: Filter FieldValues first, then apply DISTINCT ON
// This avoids scanning the entire FieldValues table
const query = `
SELECT DISTINCT
SELECT
fv."itemId",
f."fieldId",
f."label",
Expand All @@ -354,17 +356,29 @@ export class PostgresCohortService {
f."type",
f."fieldParams",
f."sourceDetails"
FROM public."Cohort" c
LEFT JOIN (
SELECT DISTINCT ON (fv."fieldId", fv."itemId") fv.*
FROM (
SELECT DISTINCT ON (fv."fieldId", fv."itemId")
fv."fieldId",
fv."itemId",
fv."textValue",
fv."numberValue",
fv."calendarValue",
fv."dropdownValue",
fv."radioValue",
fv."checkboxValue",
fv."textareaValue",
fv."fileValue",
fv."value"
FROM public."FieldValues" fv
) fv ON fv."itemId" = c."cohortId"
WHERE fv."itemId" = ANY($1)
ORDER BY fv."fieldId", fv."itemId", fv."createdAt" DESC, fv."fieldValuesId" DESC
) fv
INNER JOIN public."Fields" f ON fv."fieldId" = f."fieldId"
WHERE c."cohortId" = ANY($1);
ORDER BY fv."itemId", f."fieldId";
`;

let results = await this.cohortMembersRepository.query(query, [cohortIds]);

// Process results for dynamic options
results = await Promise.all(
results.map(async (data) => {
Expand Down Expand Up @@ -1471,12 +1485,13 @@ export class PostgresCohortService {
whereClause['cohortId'] = In(cohortIds);
}

const [cohortData, totalCount] = await this.cohortRepository.findAndCount({
where: whereClause,
order,
skip: offset,
take: limit,
});
const [cohortData, totalCount] =
await this.cohortRepository.findAndCount({
where: whereClause,
order,
skip: offset,
take: limit,
});

count = totalCount;

Expand Down
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
153 changes: 150 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,47 @@ 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'
);

// Keep original API response behavior - always return INTERNAL_SERVER_ERROR
return APIResponse.error(
response,
apiId,
Expand Down
Loading