Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#2790 - Update API Access Logs #4334

Merged
merged 15 commits into from
Feb 11, 2025
Merged
4 changes: 2 additions & 2 deletions devops/openshift/api-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ objects:
failureThreshold: 3
successThreshold: 1
httpGet:
path: /api/
path: /health/
port: "${{PORT}}"
initialDelaySeconds: 40
periodSeconds: 30
Expand All @@ -228,7 +228,7 @@ objects:
failureThreshold: 3
successThreshold: 1
httpGet:
path: /api/
path: /health/
port: "${{PORT}}"
scheme: HTTP
initialDelaySeconds: 10
Expand Down
13 changes: 0 additions & 13 deletions sources/packages/backend/apps/api/src/app.controller.ts

This file was deleted.

13 changes: 9 additions & 4 deletions sources/packages/backend/apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common";
import { AppService } from "./app.service";
import { RouterModule } from "@nestjs/core";
import {
Expand All @@ -13,6 +12,7 @@ import {
AuditController,
ConfigController,
DynamicFormController,
HealthController,
} from "./route-controllers";
import { AuthModule } from "./auth/auth.module";
import { AppAESTModule } from "./app.aest.module";
Expand All @@ -31,6 +31,7 @@ import { DatabaseModule } from "@sims/sims-db";
import { NotificationsModule } from "@sims/services/notifications";
import { QueueModule } from "@sims/services/queue";
import { AppExternalModule } from "./app.external.module";
import { AccessLoggerMiddleware } from "./middlewares";

@Module({
imports: [
Expand Down Expand Up @@ -72,7 +73,7 @@ import { AppExternalModule } from "./app.external.module";
]),
],
controllers: [
AppController,
HealthController,
ConfigController,
DynamicFormController,
AuditController,
Expand All @@ -86,4 +87,8 @@ import { AppExternalModule } from "./app.external.module";
ProgramYearService,
],
})
export class AppModule {}
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(AccessLoggerMiddleware).exclude("health").forRoutes("*");
}
}
13 changes: 1 addition & 12 deletions sources/packages/backend/apps/api/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { AppAllExceptionsFilter } from "./app.exception.filter";
import { exit } from "process";
import { setGlobalPipes } from "./utilities/auth-utils";
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
import { Request, Response } from "express";
import { KeycloakConfig } from "@sims/auth/config";
import helmet from "helmet";
import { SystemUsersService } from "@sims/services";
Expand All @@ -29,7 +28,7 @@ async function bootstrap() {
await systemUsersService.loadSystemUser();

// Setting global prefix
app.setGlobalPrefix("api");
app.setGlobalPrefix("api", { exclude: ["health"] });

// Using helmet.
app.use(helmet());
Expand Down Expand Up @@ -58,16 +57,6 @@ async function bootstrap() {
origin: allowAnyOrigin,
});

// Log every request.
app.use((req: Request, _res: Response, next: () => void) => {
logger.log(
`Request - ${req.method} ${req.path} From ${
req.headers.origin ?? "unknown"
} | ${req.headers["user-agent"] ?? "unknown"}`,
);
next();
});

// pipes
setGlobalPipes(app);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Injectable, LoggerService, NestMiddleware } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { InjectLogger } from "@sims/utilities/logger";
import { IUserToken } from "../auth";
import { getClientIPFromRequest } from "../utilities";
import { Request, Response, NextFunction } from "express";

@Injectable()
export class AccessLoggerMiddleware implements NestMiddleware {
constructor(private readonly jwtService: JwtService) {}
/**
* Logs access information of every request.
* @param request http request.
* @param _response http response.
* @param next next function.
*/
use(request: Request, _response: Response, next: NextFunction) {
const { headers, originalUrl, method } = request;
const clientIP = getClientIPFromRequest(request);
const user = this.getUserFromBearerToken(request.headers.authorization);
const userGUID = user ? user.userName : "User GUID not found";
const userAgent = headers["user-agent"] ?? "User agent not found";
const userAccessLog = `Request - ${method} ${originalUrl} From ${clientIP} | User GUID: ${userGUID} | User Agent: ${userAgent}`;
this.logger.log(userAccessLog);
next();
}

/**
* Get decoded user information from bearer token.
* @param bearerToken token to be decoded.
* @returns decoded user information.
*/
private getUserFromBearerToken(bearerToken: string): IUserToken | null {
if (!bearerToken) {
return null;
}
const user = this.jwtService.decode<IUserToken>(
bearerToken.replace("Bearer ", ""),
);
return user;
}

@InjectLogger()
logger: LoggerService;
}
1 change: 1 addition & 0 deletions sources/packages/backend/apps/api/src/middlewares/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./access-logger.middleware";
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import BaseController from "../BaseController";
import { AuditService } from "../../services";
import { Request } from "express";
import { AuditAPIInDTO } from "./models/audit.dto";
import { getClientIPFromRequest } from "../../utilities";

@AllowAuthorizedParty(
AuthorizedParties.institution,
Expand Down Expand Up @@ -37,9 +38,7 @@ export class AuditController extends BaseController {
@UserToken() userToken: IUserToken,
@Req() request: Request,
): void {
const clientIP =
(request.headers["x-forwarded-for"] as string) ||
request.socket.remoteAddress;
const clientIP = getClientIPFromRequest(request);
this.auditService.audit(
clientIP,
userToken.userName,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
import { Test, TestingModule } from "@nestjs/testing";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { HealthController } from "./health.controller";
import { DatabaseModule } from "@sims/sims-db";

// TODO: must mock DB dependencies.
describe.skip("AppController", () => {
let appController: AppController;
describe.skip("HealthController", () => {
let healthController: HealthController;

beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
imports: [DatabaseModule],
controllers: [AppController],
providers: [AppService],
controllers: [HealthController],
}).compile();

appController = app.get<AppController>(AppController);
healthController = app.get<HealthController>(HealthController);
});

describe("root", () => {
it("should return Hello world string with db connection status and version", () => {
const expected = `Hello World! The database dataSource is true and version: ${
process.env.VERSION ?? "-1"
}`;
expect(appController.getHello()).toBe(expected);
expect(healthController.getHello()).toBe(expected);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Controller, Get } from "@nestjs/common";
import { Public } from "../../auth/decorators";
import { DataSource } from "typeorm";

@Controller("health")
export class HealthController {
constructor(private readonly dataSource: DataSource) {}

/**
* Returns a health check status.
* @returns health check status.
*/
@Get()
@Public()
getHello(): string {
return this.getHeathCheckStatus();
}

/**
* Get health check status.
* @returns health check status.
*/
private getHeathCheckStatus(): string {
try {
return `Hello World! The database dataSource is ${
this.dataSource.isInitialized
} and version: ${process.env.VERSION ?? "-1"}`;
} catch (error: unknown) {
return `Hello world! Fail with error: ${error}`;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,4 @@ export * from "./student/student.external.controller";
export * from "./cas-invoice-batch/cas-invoice-batch.aest.controller";
export * from "./models/primary.identifier.dto";
export * from "./models/common.dto";
export * from "./health-check/health.controller";
18 changes: 18 additions & 0 deletions sources/packages/backend/apps/api/src/utilities/http-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Request } from "express";

/**
* Header name for the client IP address.
*/
const CLIENT_IP_HEADER_NAME = "x-forwarded-for";

/**
* Gets the client IP address from the http request.
* @param request http request.
* @returns client IP address.
*/
export function getClientIPFromRequest(request: Request): string {
return (
(request.headers[CLIENT_IP_HEADER_NAME] as string) ??
request.socket.remoteAddress
);
}
1 change: 1 addition & 0 deletions sources/packages/backend/apps/api/src/utilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export * from "./datatable-database-mapping-utils";
export * from "./datatable-config";
export * from "./pagination-utils";
export * from "./address-utils";
export * from "./http-utils";
Loading