Skip to content
This repository was archived by the owner on Feb 11, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ import { AuthController } from './presentation/controllers/auth.controller';
import { UserController } from './presentation/controllers/user.controller';
import { UserRepository } from './infrastructure/database/repositories/user.repository';
import { SessionActivityMiddleware } from './presentation/middleware/session-activity.middleware';
import { LoggerService } from './common/logger.service';
import { RequestLoggingMiddleware } from './presentation/middleware/request-logging.middleware';

@Module({
imports: [],
providers: [
LoggerService,
PrismaService,
{
provide: 'SERVICE.AUTH',
Expand All @@ -28,6 +31,8 @@ import { SessionActivityMiddleware } from './presentation/middleware/session-act
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(SessionActivityMiddleware).forRoutes('*');
consumer
.apply(RequestLoggingMiddleware, SessionActivityMiddleware)
.forRoutes('*');
}
}
23 changes: 20 additions & 3 deletions src/application/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { type IUserRepository } from '@/domain/user/user-repository.interface';
import { IAuthService } from '@/application/ports/auth.port';
import { verify, type Options } from '@node-rs/argon2';
import { Inject, Injectable } from '@nestjs/common';
import { LoggerService } from '@/common/logger.service';
import { fail, succeed } from '@/result';

@Injectable()
Expand All @@ -14,18 +15,34 @@ export class AuthService implements IAuthService {
timeCost: 2,
};

constructor(@Inject('REPO.USER') _userRepository: IUserRepository) {
constructor(
@Inject('REPO.USER') _userRepository: IUserRepository,
private readonly logger?: LoggerService,

Copilot AI Dec 16, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logger parameter is marked as optional, but the code uses optional chaining throughout. Consider making it required to ensure consistent logging behavior across all authentication operations.

Copilot uses AI. Check for mistakes.
) {
this.userRepository = _userRepository;
}

async login(email: string, password: string) {
const result = await this.userRepository.findByEmail(email);
if (result._tag === 'Failure') return fail(result.error);
if (result._tag === 'Failure') {
// Don't log repository-specific "User not found" to avoid exposing user existence.
const repoMsg = result.error?.message ?? 'Unknown error';
const logMsg =
repoMsg === 'User not found' ? 'Credentials not correct' : repoMsg;
this.logger?.warn(
`Failed login attempt for ${email}: ${logMsg}`,

Copilot AI Dec 16, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logging user email addresses in failed login attempts may expose sensitive information in logs. Consider hashing or redacting email addresses, or logging only a session/request identifier for correlation while keeping PII out of logs.

Copilot uses AI. Check for mistakes.
'AuthService',
);
return fail(result.error);
}

const user = result.data;

const isMatch = await verify(user.hashedPassword, password);
if (!isMatch) return fail(new Error('Invalid credentials'));
if (!isMatch) {
this.logger?.warn(`Invalid credentials for ${email}`, 'AuthService');

Copilot AI Dec 16, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logging user email addresses in failed login attempts may expose sensitive information in logs. Consider hashing or redacting email addresses, or logging only a session/request identifier for correlation while keeping PII out of logs.

Suggested change
this.logger?.warn(`Invalid credentials for ${email}`, 'AuthService');
this.logger?.warn('Invalid credentials', 'AuthService');

Copilot uses AI. Check for mistakes.
return fail(new Error('Invalid credentials'));
}

return succeed({ user });
}
Expand Down
53 changes: 53 additions & 0 deletions src/common/logger.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Injectable, LoggerService as NestLoggerService } from '@nestjs/common';

type LogLevel = 'log' | 'error' | 'warn' | 'debug' | 'verbose';

@Injectable()
export class LoggerService implements NestLoggerService {
private name = 'App';

setContext(name: string) {
this.name = name;
}

private format(level: LogLevel, message: any, context?: string, meta?: any) {
const payload: Record<string, any> = {
timestamp: new Date().toISOString(),
level,
context: context ?? this.name,
Comment on lines +7 to +17

Copilot AI Dec 16, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default context name 'App' is generic and may not be helpful for distinguishing log sources. Consider using a more descriptive default or requiring context to be set explicitly during initialization.

Suggested change
private name = 'App';
setContext(name: string) {
this.name = name;
}
private format(level: LogLevel, message: any, context?: string, meta?: any) {
const payload: Record<string, any> = {
timestamp: new Date().toISOString(),
level,
context: context ?? this.name,
private name?: string;
setContext(name: string) {
this.name = name;
}
private format(level: LogLevel, message: any, context?: string, meta?: any) {
const resolvedContext = context ?? this.name;
if (!resolvedContext) {
throw new Error('LoggerService context is not set. Please set a context using setContext() or provide one when logging.');
}
const payload: Record<string, any> = {
timestamp: new Date().toISOString(),
level,
context: resolvedContext,

Copilot uses AI. Check for mistakes.
message: typeof message === 'string' ? message : JSON.stringify(message),
};

if (meta) payload.meta = meta;
return JSON.stringify(payload);
}

log(message: any, context?: string) {
// Info
// eslint-disable-next-line no-console
console.info(this.format('log', message, context));
}

error(message: any, trace?: string, context?: string) {
const meta = trace ? { trace } : undefined;
// eslint-disable-next-line no-console
console.error(this.format('error', message, context, meta));
}

warn(message: any, context?: string) {
// eslint-disable-next-line no-console
console.warn(this.format('warn', message, context));
}

debug?(message: any, context?: string) {
// eslint-disable-next-line no-console
console.debug(this.format('debug', message, context));
}

verbose?(message: any, context?: string) {
// eslint-disable-next-line no-console
console.debug(this.format('verbose', message, context));
}
}

export default LoggerService;
76 changes: 72 additions & 4 deletions src/infrastructure/database/prisma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,91 @@ import 'dotenv/config';
import { PrismaMariaDb } from '@prisma/adapter-mariadb';
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@/infrastructure/database/generated/prisma/client';
import { LoggerService } from '@/common/logger.service';

@Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
constructor() {
const adapter = new PrismaMariaDb(process.env.DATABASE_URL!);
constructor(private readonly logger: LoggerService) {
// Parse DATABASE_URL and create mariadb pool config
const dbUrl = new URL(process.env.DATABASE_URL!);
const adapter = new PrismaMariaDb({
host: dbUrl.hostname,
port: parseInt(dbUrl.port || '3306', 10),
user: dbUrl.username,
password: dbUrl.password,
database: dbUrl.pathname.slice(1), // remove leading /
connectionLimit: 20,
connectTimeout: 30000,
});
super({ adapter });

// Forward Prisma client events to our logger
try {
// Prisma client's $on typing may be strict depending on generated client.
// Cast to `any` to attach listeners at runtime while keeping TypeScript happy.
(this as any).$on('info', (e: any) => {
this.logger.log(`${e.message}`, 'Prisma');
});

(this as any).$on('warn', (e: any) => {

Copilot AI Dec 16, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple type assertions to any reduce type safety. Consider defining an interface for the event object structure or using a type guard to safely access the Prisma event properties.

Copilot uses AI. Check for mistakes.
this.logger.warn(`${e.message}`, 'Prisma');
});

(this as any).$on('error', (e: any) => {

Copilot AI Dec 16, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple type assertions to any reduce type safety. Consider defining an interface for the event object structure or using a type guard to safely access the Prisma event properties.

Copilot uses AI. Check for mistakes.
// e is an object with message and perhaps stack
this.logger.error(
`${e.message}`,
(e as any).stack ?? undefined,
Comment on lines +28 to +42

Copilot AI Dec 16, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple type assertions to any reduce type safety. Consider defining an interface for the event object structure or using a type guard to safely access the Prisma event properties.

Suggested change
// Prisma client's $on typing may be strict depending on generated client.
// Cast to `any` to attach listeners at runtime while keeping TypeScript happy.
(this as any).$on('info', (e: any) => {
this.logger.log(`${e.message}`, 'Prisma');
});
(this as any).$on('warn', (e: any) => {
this.logger.warn(`${e.message}`, 'Prisma');
});
(this as any).$on('error', (e: any) => {
// e is an object with message and perhaps stack
this.logger.error(
`${e.message}`,
(e as any).stack ?? undefined,
// Define event interfaces for Prisma events
interface PrismaLogEvent {
message: string;
target?: string;
timestamp?: Date;
}
interface PrismaErrorEvent extends PrismaLogEvent {
stack?: string;
}
// Cast only `this` to any if needed, but type event parameter
(this as any).$on('info', (e: PrismaLogEvent) => {
this.logger.log(`${e.message}`, 'Prisma');
});
(this as any).$on('warn', (e: PrismaLogEvent) => {
this.logger.warn(`${e.message}`, 'Prisma');
});
(this as any).$on('error', (e: PrismaErrorEvent) => {
// e is an object with message and perhaps stack
this.logger.error(
`${e.message}`,
e.stack ?? undefined,

Copilot uses AI. Check for mistakes.
'Prisma',
);
});
} catch (e) {
// Listening may fail in some runtime builds; safely ignore

Copilot AI Dec 16, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silently catching and ignoring exceptions without logging makes debugging difficult if event registration actually fails. Consider logging this failure at debug level to aid troubleshooting.

Suggested change
// Listening may fail in some runtime builds; safely ignore
// Listening may fail in some runtime builds; log at debug level for troubleshooting
this.logger.debug(
`Failed to register Prisma event listeners: ${e instanceof Error ? e.message : String(e)}`,
(e instanceof Error && e.stack) ? e.stack : undefined,
'PrismaService',
);

Copilot uses AI. Check for mistakes.
}
}

async onModuleInit() {
await this.$connect();
const maxRetries = 5;
const delayMs = 2000;
let attempt = 0;
while (attempt < maxRetries) {
try {
await this.$connect();
this.logger.log('Connected to database', 'PrismaService');
return;
} catch (err) {
attempt += 1;
const msg = `Database connect attempt ${attempt} failed: ${
err instanceof Error ? err.message : String(err)
}`;
this.logger.warn(msg, 'PrismaService');
if (attempt >= maxRetries) {
this.logger.error(
'Failed to connect to database after retries',
err instanceof Error ? err.stack : String(err),
'PrismaService',
);
throw err;
}
// wait before retrying
await new Promise((r) => setTimeout(r, delayMs));
}
}
}

async onModuleDestroy() {
await this.$disconnect();
try {
await this.$disconnect();
this.logger.log('Disconnected from database', 'PrismaService');
} catch (e) {
this.logger.warn(
`Error during disconnect: ${e instanceof Error ? e.message : String(e)}`,
'PrismaService',
);
}
}
}

Expand Down
9 changes: 6 additions & 3 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Logger, ValidationPipe, VersioningType } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { LoggerService } from './common/logger.service';
import cookieParser from 'cookie-parser';
import session from 'express-session';
import helmet from 'helmet';
Expand All @@ -10,8 +11,10 @@ import { configDotenv } from 'dotenv';
configDotenv();

async function bootstrap() {
const logger = new Logger('Bootstrap');
const app = await NestFactory.create(AppModule);
const logger = app.get(LoggerService);
// Let Nest use our LoggerService for framework logs and exceptions
app.useLogger(logger);
const isProduction = process.env.NODE_ENV === 'production';

// Global API prefix - must be set before middleware
Expand Down Expand Up @@ -80,15 +83,15 @@ async function bootstrap() {

const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, document);
logger.log('Swagger UI initialized at /docs');
logger.log('Swagger UI initialized at /docs', 'Bootstrap');
}

// Graceful Shutdown
app.enableShutdownHooks();

const port = process.env.PUBLIC_PORT ?? 4000;
await app.listen(port);
logger.log(`Application is running on: ${await app.getUrl()}`);
logger.log(`Application is running on: ${await app.getUrl()}`, 'Bootstrap');
}

bootstrap().catch((err) => {
Expand Down
23 changes: 23 additions & 0 deletions src/presentation/middleware/request-logging.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { LoggerService } from '../../common/logger.service';

@Injectable()
export class RequestLoggingMiddleware implements NestMiddleware {
constructor(private readonly logger: LoggerService) {}

use(req: Request, res: Response, next: NextFunction) {
const start = Date.now();
const { method, originalUrl } = req;

res.on('finish', () => {
const duration = Date.now() - start;
const message = `${method} ${originalUrl} ${res.statusCode} ${duration}ms`;
this.logger.log(message, 'HTTP');
});

next();
}
}

export default RequestLoggingMiddleware;