add logger
Deploy / deploy (push) Failing after 24s

This commit is contained in:
Rayyan
2026-08-03 23:46:19 +07:00
parent e0bf3882be
commit 6ffff9ef7b
8 changed files with 210 additions and 10 deletions
+31
View File
@@ -24,6 +24,7 @@
"bullmq": "^1.73.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"cookie-parser": "^1.4.7",
"dotenv": "^16.6.1",
"ioredis": "^5.3.2",
"jose": "^6.2.6",
@@ -43,6 +44,7 @@
"@nestjs/testing": "^10.4.22",
"@playwright/test": "^1.62.1",
"@types/bcrypt": "^5.0.2",
"@types/cookie-parser": "^1.4.10",
"@types/jest": "^29.5.14",
"@types/node": "^22.20.1",
"@types/passport-jwt": "^4.0.1",
@@ -2260,6 +2262,16 @@
"@types/node": "*"
}
},
"node_modules/@types/cookie-parser": {
"version": "1.4.10",
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz",
"integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/express": "*"
}
},
"node_modules/@types/cookiejar": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz",
@@ -3667,6 +3679,25 @@
"node": ">= 0.6"
}
},
"node_modules/cookie-parser": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
"integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
"license": "MIT",
"dependencies": {
"cookie": "0.7.2",
"cookie-signature": "1.0.6"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/cookie-parser/node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT"
},
"node_modules/cookie-signature": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+3 -1
View File
@@ -40,6 +40,7 @@
"bullmq": "^1.73.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"cookie-parser": "^1.4.7",
"dotenv": "^16.6.1",
"ioredis": "^5.3.2",
"jose": "^6.2.6",
@@ -59,6 +60,7 @@
"@nestjs/testing": "^10.4.22",
"@playwright/test": "^1.62.1",
"@types/bcrypt": "^5.0.2",
"@types/cookie-parser": "^1.4.10",
"@types/jest": "^29.5.14",
"@types/node": "^22.20.1",
"@types/passport-jwt": "^4.0.1",
@@ -78,6 +80,6 @@
"typescript": "^5.9.3"
},
"prisma": {
"seed": "node --loader ts-node/esm prisma/seed.ts"
"seed": "node --loader ts-node/esm prisma/seed.ts"
}
}
+22
View File
@@ -0,0 +1,22 @@
import { Injectable, Logger, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtGuard extends AuthGuard('jwt') {
private readonly logger = new Logger('JwtGuard');
canActivate(context: ExecutionContext) {
const req = context.switchToHttp().getRequest();
this.logger.debug(`JwtGuard invoked. cookies=${JSON.stringify(req.cookies)} authHeader=${req.headers?.authorization}`);
return super.canActivate(context);
}
handleRequest(err: any, user: any, info: any) {
if (err || !user) {
this.logger.debug(`JwtGuard handleRequest failed. err=${err} user=${!!user} info=${JSON.stringify(info)}`);
} else {
this.logger.debug(`JwtGuard handleRequest success user=${JSON.stringify(user)}`);
}
return super.handleRequest(err, user, info);
}
}
+22
View File
@@ -0,0 +1,22 @@
import { Injectable, Logger, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class RefreshGuard extends AuthGuard('refresh') {
private readonly logger = new Logger('RefreshGuard');
canActivate(context: ExecutionContext) {
const req = context.switchToHttp().getRequest();
this.logger.debug(`RefreshGuard invoked. cookies=${JSON.stringify(req.cookies)} body=${JSON.stringify(req.body)}`);
return super.canActivate(context);
}
handleRequest(err: any, user: any, info: any) {
if (err || !user) {
this.logger.debug(`RefreshGuard handleRequest failed. err=${err} user=${!!user} info=${JSON.stringify(info)}`);
} else {
this.logger.debug(`RefreshGuard handleRequest success userId=${user.id} authInfo=${JSON.stringify(info)}`);
}
return super.handleRequest(err, user, info);
}
}
+29
View File
@@ -0,0 +1,29 @@
import { Injectable, Logger } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy as JwtStrategyBase, ExtractJwt } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
function cookieExtractor(req: any): string | null {
if (!req) return null;
if (req.cookies && req.cookies.raylab_jwt) return req.cookies.raylab_jwt as string;
return null;
}
@Injectable()
export class JwtStrategy extends PassportStrategy(JwtStrategyBase, 'jwt') {
private readonly logger = new Logger('JwtStrategy');
constructor(private config: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromExtractors([cookieExtractor, ExtractJwt.fromAuthHeaderAsBearerToken()]),
secretOrKey: config.get<string>('RAYLAB_JWT_SECRET') || process.env.RAYLAB_JWT_SECRET || 'raylab-secret',
algorithms: ['HS256'],
});
this.logger.debug('JwtStrategy initialized');
}
async validate(payload: any) {
this.logger.debug(`JwtStrategy.validate payload=${JSON.stringify(payload)}`);
return payload; // attached to req.user
}
}
@@ -0,0 +1,58 @@
import { Injectable, Logger } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-strategy';
import { InMemoryRefreshStore } from '../../../modules/auth/refresh/inmemory-refresh.store';
import { IUser } from '../../../modules/identity/domain/repositories/user.interface';
import { Inject } from '@nestjs/common';
// Minimal Passport Strategy for refresh tokens (non-JWT random tokens)
class RefreshTokenStrategy extends Strategy {
name = 'refresh';
authenticate(req: any) {
// This will be overridden in PassportStrategy wrapper
this.error(new Error('Not implemented'));
}
}
@Injectable()
export class RefreshStrategy extends PassportStrategy(RefreshTokenStrategy, 'refresh') {
private readonly logger = new Logger('RefreshStrategy');
constructor(private refreshStore: InMemoryRefreshStore, @Inject(IUser) private userRepository: IUser) {
super();
this.logger.debug('RefreshStrategy initialized');
}
async authenticate(req: any, options?: any) {
const cookies = req.cookies || null;
const token = (cookies && cookies.raylab_refresh) || (req.body && req.body.refreshToken);
this.logger.debug(`RefreshStrategy.authenticate cookies=${JSON.stringify(cookies)} tokenExtracted=${!!token}`);
if (!token) {
this.logger.debug('RefreshStrategy: no refresh token provided');
return this.fail('Missing refresh token', 401);
}
try {
const data = await this.refreshStore.get(token);
if (!data) {
this.logger.debug('RefreshStrategy: refresh token not found/expired');
return this.fail('Invalid refresh token', 401);
}
const user = await this.userRepository.getById(data.userId);
if (!user) {
this.logger.debug('RefreshStrategy: user not found for refresh token');
return this.fail('User not found', 401);
}
// success - attach user + token info
const info = { refreshToken: token, userId: data.userId };
this.logger.debug(`RefreshStrategy: validated refresh token for userId=${data.userId}`);
return this.success(user, info);
} catch (e) {
this.logger.debug(`RefreshStrategy error: ${(e as Error).message}`);
return this.error(e as Error);
}
}
}
+33 -9
View File
@@ -1,8 +1,10 @@
import { Controller, Get, Post, Query, Res, Req, Body, HttpCode, HttpStatus, Logger } from '@nestjs/common';
import { Controller, Get, Post, Query, Res, Req, Body, HttpCode, HttpStatus, Logger, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation } from '@nestjs/swagger';
import { AuthService } from './auth.service';
import { Response, Request } from 'express';
import { JwtGuard } from '../../../core/auth/guards/jwt.guard';
import { RefreshGuard } from '../../../core/auth/guards/refresh.guard';
import { ConfigService } from '@nestjs/config';
@ApiTags('Auth')
@@ -56,21 +58,43 @@ export class AuthController {
}
@Post('refresh')
@UseGuards(RefreshGuard)
@ApiOperation({ summary: 'Refresh internal JWT using internal refresh token' })
async refresh(@Req() req: Request) {
const refreshToken = req.cookies?.raylab_refresh || req.body?.refreshToken;
this.logger.debug(`Refresh called. cookies present=${!!req.cookies} refreshTokenProvided=${!!refreshToken}`);
async refresh(@Req() req: Request, @Res() res: Response) {
// req.authInfo should contain { refreshToken }
const refreshToken = (req as any).authInfo?.refreshToken || (req.cookies?.raylab_refresh) || (req.body?.refreshToken);
this.logger.debug(`Refresh called (guarded). cookies=${JSON.stringify((req as any).cookies)} authInfo=${JSON.stringify((req as any).authInfo)}`);
const result = await this.authService.refresh(refreshToken);
this.logger.debug(`Refresh result for user. expiresIn=${result.expiresIn}`);
return { success: true, data: result };
// set cookies with environment-aware options
const isProd = this.config.get<string>('NODE_ENV') === 'production' || process.env.NODE_ENV === 'production';
const cookieDomain = this.config.get<string>('RAYLAB_COOKIE_DOMAIN') || (isProd ? '.raylab.site' : undefined);
const cookieOptions: any = {
httpOnly: true,
secure: isProd,
sameSite: isProd ? 'none' : 'lax',
path: '/',
};
if (cookieDomain) cookieOptions.domain = cookieDomain;
// set new cookies (rotate)
res.cookie('raylab_jwt', result.accessToken, { ...cookieOptions, maxAge: result.expiresIn * 1000 });
res.cookie('raylab_refresh', result.refreshToken, { ...cookieOptions, maxAge: result.refreshTtl * 1000 });
this.logger.debug(`Refresh complete. set new cookies for user`);
return res.json({ success: true, data: result });
}
@Get('me')
@UseGuards(JwtGuard)
@ApiOperation({ summary: 'Get current user from internal JWT (cookie or Authorization header)' })
async me(@Req() req: Request) {
const token = (req.cookies?.raylab_jwt) || (req.headers.authorization && (req.headers.authorization as string).replace(/^Bearer\s+/i, ''));
this.logger.debug(`Me called. cookies present=${!!req.cookies} tokenProvided=${!!token}`);
const user = await this.authService.me(token);
// With JwtGuard, req.user should be payload from token
this.logger.debug(`Me called (guarded). cookies=${JSON.stringify((req as any).cookies)} user=${JSON.stringify((req as any).user)}`);
const payload = (req as any).user;
const user = await this.authService.me((req.cookies?.raylab_jwt) || (req.headers.authorization && (req.headers.authorization as string).replace(/^Bearer\s+/i, '')));
this.logger.debug(`Me returning user id=${user?.id}`);
return { success: true, data: user };
}
+12
View File
@@ -1,4 +1,9 @@
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { JwtStrategy } from '../../core/auth/strategies/jwt.strategy';
import { RefreshStrategy } from '../../core/auth/strategies/refresh.strategy';
import { JwtGuard } from '../../core/auth/guards/jwt.guard';
import { RefreshGuard } from '../../core/auth/guards/refresh.guard';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { ConfigModule, ConfigService } from '@nestjs/config';
@@ -27,6 +32,7 @@ import { AuthorizationModule } from '../authorization/authorization.module';
imports: [
ConfigModule,
AuthorizationModule,
PassportModule.register({ defaultStrategy: 'jwt', session: false }),
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (config: ConfigService) => ({
@@ -59,6 +65,12 @@ import { AuthorizationModule } from '../authorization/authorization.module';
EventBus,
AuditService,
RedisService,
// Strategies & Guards
JwtStrategy,
RefreshStrategy,
JwtGuard,
RefreshGuard,
],
exports: [AuthService, OidcService, RoleSyncService, AuthenticationService, EventBus],
})