make debugger
Deploy / deploy (push) Failing after 14s

This commit is contained in:
Rayyan
2026-08-03 23:33:40 +07:00
parent a5db9d69b3
commit e0bf3882be
4 changed files with 63 additions and 15 deletions
+24 -8
View File
@@ -3,6 +3,7 @@ import {
CanActivate, CanActivate,
ExecutionContext, ExecutionContext,
UnauthorizedException, UnauthorizedException,
Logger,
} from '@nestjs/common'; } from '@nestjs/common';
import { Request } from 'express'; import { Request } from 'express';
import { createRemoteJWKSet, jwtVerify } from 'jose'; import { createRemoteJWKSet, jwtVerify } from 'jose';
@@ -16,27 +17,37 @@ import * as jwt from 'jsonwebtoken';
@Injectable() @Injectable()
export class JwtAuthGuard implements CanActivate { export class JwtAuthGuard implements CanActivate {
private jwks: ReturnType<typeof createRemoteJWKSet> | null = null; private jwks: ReturnType<typeof createRemoteJWKSet> | null = null;
private readonly logger = new Logger('JwtAuthGuard');
constructor() {} constructor() {}
async canActivate(context: ExecutionContext): Promise<boolean> { async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request & { identity?: IdentityData }>(); const request = context.switchToHttp().getRequest<Request & { identity?: IdentityData }>();
const authHeader = request.headers.authorization; let token: string | undefined;
if (!authHeader) { const authHeader = request.headers.authorization;
throw new UnauthorizedException('Authorization header is missing.'); if (authHeader) {
const [type, t] = authHeader.split(' ');
if (type === 'Bearer' && t) token = t;
} }
const [type, token] = authHeader.split(' '); // fallback to cookie if no Authorization header
if (!token) {
token = (request as any).cookies?.raylab_jwt;
this.logger.debug(`No Authorization header. Trying cookie. cookiePresent=${!!(request as any).cookies} tokenFromCookie=${!!token}`);
} else {
this.logger.debug('Authorization header found. Using Bearer token.');
}
if (type !== 'Bearer' || !token) { if (!token) {
throw new UnauthorizedException('Invalid authorization header.'); this.logger.debug('No token found in Authorization header or cookie.');
throw new UnauthorizedException('Authorization token is missing.');
} }
const jwksUri = process.env.AUTHENTIK_JWKS_URI; const jwksUri = process.env.AUTHENTIK_JWKS_URI;
// First try verifying with external JWKS (Authentik) // First try verifying with external JWKS (Authentik)
if (jwksUri) { if (jwksUri) {
try { try {
if (!this.jwks) this.jwks = createRemoteJWKSet(new URL(jwksUri)); if (!this.jwks) this.jwks = createRemoteJWKSet(new URL(jwksUri));
@@ -54,15 +65,18 @@ export class JwtAuthGuard implements CanActivate {
); );
request.identity = identity; request.identity = identity;
this.logger.debug(`Verified token using external JWKS. sub=${payload.sub}`);
return true; return true;
} catch (err) { } catch (err) {
this.logger.debug(`External JWKS verification failed: ${(err as Error).message}`);
// ignore and try internal verification // ignore and try internal verification
} }
} }
// Fallback: verify with internal symmetric secret // Fallback: verify with internal symmetric secret
const secret = process.env.RAYLAB_JWT_SECRET; const secret = process.env.RAYLAB_JWT_SECRET;
if (!secret) { if (!secret) {
this.logger.error('RAYLAB_JWT_SECRET is not configured.');
throw new UnauthorizedException('Invalid or expired token.'); throw new UnauthorizedException('Invalid or expired token.');
} }
@@ -77,8 +91,10 @@ export class JwtAuthGuard implements CanActivate {
); );
request.identity = identity; request.identity = identity;
this.logger.debug(`Verified token using internal secret. sub=${payload.sub}`);
return true; return true;
} catch (err: any) { } catch (err: any) {
this.logger.debug(`Internal token verification failed: ${(err as Error).message}`);
throw new UnauthorizedException('Invalid or expired token.'); throw new UnauthorizedException('Invalid or expired token.');
} }
} }
+11 -3
View File
@@ -1,4 +1,5 @@
import { ValidationPipe } from '@nestjs/common'; import { ValidationPipe, Logger } from '@nestjs/common';
import cookieParser from 'cookie-parser';
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
@@ -9,6 +10,10 @@ async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
const config = app.get(ConfigService); const config = app.get(ConfigService);
const logger = new Logger('Bootstrap');
// enable cookie parser so req.cookies is populated
app.use(cookieParser());
app.setGlobalPrefix(''); app.setGlobalPrefix('');
@@ -55,11 +60,14 @@ async function bootstrap() {
SwaggerModule.setup('ApiList', app, document); SwaggerModule.setup('ApiList', app, document);
} }
const port = config.get<number>('PORT') || 3000; const port = config.get<number>('PORT') || 3000;
logger.log(`Allowed CORS origins: ${JSON.stringify(allowedOrigins)}`);
logger.log(`Server listening on port ${port}`);
await app.listen(port); await app.listen(port);
console.log(`Server running on http://localhost:${port}`); logger.log(`Server running on http://localhost:${port}`);
} }
bootstrap(); bootstrap();
+8 -1
View File
@@ -1,5 +1,5 @@
import { Controller, Get, Post, Query, Res, Req, Body, HttpCode, HttpStatus } from '@nestjs/common'; import { Controller, Get, Post, Query, Res, Req, Body, HttpCode, HttpStatus, Logger } from '@nestjs/common';
import { ApiTags, ApiOperation } from '@nestjs/swagger'; import { ApiTags, ApiOperation } from '@nestjs/swagger';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { Response, Request } from 'express'; import { Response, Request } from 'express';
@@ -8,6 +8,7 @@ import { ConfigService } from '@nestjs/config';
@ApiTags('Auth') @ApiTags('Auth')
@Controller('auth') @Controller('auth')
export class AuthController { export class AuthController {
private readonly logger = new Logger('AuthController');
constructor(private readonly authService: AuthService, private readonly config: ConfigService) {} constructor(private readonly authService: AuthService, private readonly config: ConfigService) {}
@Get('login') @Get('login')
@@ -35,11 +36,13 @@ export class AuthController {
if (cookieDomain) cookieOptions.domain = cookieDomain; if (cookieDomain) cookieOptions.domain = cookieDomain;
// access token cookie (internal JWT) // access token cookie (internal JWT)
this.logger.log(`Setting access & refresh cookies. cookieOptions=${JSON.stringify(cookieOptions)}`);
res.cookie('raylab_jwt', result.accessToken, { ...cookieOptions, maxAge: result.expiresIn * 1000 }); res.cookie('raylab_jwt', result.accessToken, { ...cookieOptions, maxAge: result.expiresIn * 1000 });
// refresh token cookie // refresh token cookie
res.cookie('raylab_refresh', result.refreshToken, { ...cookieOptions, maxAge: result.refreshTtl * 1000 }); res.cookie('raylab_refresh', result.refreshToken, { ...cookieOptions, maxAge: result.refreshTtl * 1000 });
this.logger.debug(`Callback complete. returnTo=${result.returnTo} user=${JSON.stringify(result.user)}`);
return res.redirect(302, result.returnTo || '/'); return res.redirect(302, result.returnTo || '/');
} }
@@ -56,7 +59,9 @@ export class AuthController {
@ApiOperation({ summary: 'Refresh internal JWT using internal refresh token' }) @ApiOperation({ summary: 'Refresh internal JWT using internal refresh token' })
async refresh(@Req() req: Request) { async refresh(@Req() req: Request) {
const refreshToken = req.cookies?.raylab_refresh || req.body?.refreshToken; const refreshToken = req.cookies?.raylab_refresh || req.body?.refreshToken;
this.logger.debug(`Refresh called. cookies present=${!!req.cookies} refreshTokenProvided=${!!refreshToken}`);
const result = await this.authService.refresh(refreshToken); const result = await this.authService.refresh(refreshToken);
this.logger.debug(`Refresh result for user. expiresIn=${result.expiresIn}`);
return { success: true, data: result }; return { success: true, data: result };
} }
@@ -64,7 +69,9 @@ export class AuthController {
@ApiOperation({ summary: 'Get current user from internal JWT (cookie or Authorization header)' }) @ApiOperation({ summary: 'Get current user from internal JWT (cookie or Authorization header)' })
async me(@Req() req: Request) { async me(@Req() req: Request) {
const token = (req.cookies?.raylab_jwt) || (req.headers.authorization && (req.headers.authorization as string).replace(/^Bearer\s+/i, '')); 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); const user = await this.authService.me(token);
this.logger.debug(`Me returning user id=${user?.id}`);
return { success: true, data: user }; return { success: true, data: user };
} }
} }
+20 -3
View File
@@ -140,11 +140,14 @@ export class AuthService {
const expiresIn = Number(this.config.get('RAYLAB_JWT_EXPIRES_IN') || 3600); const expiresIn = Number(this.config.get('RAYLAB_JWT_EXPIRES_IN') || 3600);
const access = this.jwtService.sign(jwtPayload, { expiresIn }); const access = this.jwtService.sign(jwtPayload, { expiresIn });
logger.debug(`Created internal access token for user=${domainUser.id} expiresIn=${expiresIn}`);
// create internal refresh token // create internal refresh token
const refreshToken = crypto.randomUUID(); const refreshToken = crypto.randomUUID();
const refreshTtl = Number(this.config.get('RAYLAB_REFRESH_EXPIRES_IN') || 30 * 24 * 3600); // default 30 days const refreshTtl = Number(this.config.get('RAYLAB_REFRESH_EXPIRES_IN') || 30 * 24 * 3600); // default 30 days
await this.refreshStore.set(refreshToken, { userId: domainUser.id }, refreshTtl); await this.refreshStore.set(refreshToken, { userId: domainUser.id }, refreshTtl);
logger.debug(`Stored refresh token for user=${domainUser.id} ttl=${refreshTtl}`);
return { return {
accessToken: access, accessToken: access,
@@ -162,9 +165,16 @@ export class AuthService {
} }
async refresh(refreshToken?: string) { async refresh(refreshToken?: string) {
if (!refreshToken) throw new UnauthorizedException('Missing refresh token'); if (!refreshToken) {
logger.debug('Refresh called without refresh token');
throw new UnauthorizedException('Missing refresh token');
}
const data = await this.refreshStore.get(refreshToken); const data = await this.refreshStore.get(refreshToken);
if (!data) throw new UnauthorizedException('Invalid refresh token'); if (!data) {
logger.debug(`Refresh token not found or expired: ${refreshToken}`);
throw new UnauthorizedException('Invalid refresh token');
}
logger.debug(`Refresh token validated for userId=${data.userId}`);
const userId = data.userId; const userId = data.userId;
// load user // load user
@@ -177,9 +187,11 @@ export class AuthService {
const newRefresh = crypto.randomUUID(); const newRefresh = crypto.randomUUID();
const refreshTtl = Number(this.config.get('RAYLAB_REFRESH_EXPIRES_IN') || 30 * 24 * 3600); const refreshTtl = Number(this.config.get('RAYLAB_REFRESH_EXPIRES_IN') || 30 * 24 * 3600);
await this.refreshStore.set(newRefresh, { userId }, refreshTtl); await this.refreshStore.set(newRefresh, { userId }, refreshTtl);
logger.debug(`Rotated refresh token for userId=${userId} newRefresh=${newRefresh} ttl=${refreshTtl}`);
const expiresIn = Number(this.config.get('RAYLAB_JWT_EXPIRES_IN') || 3600); const expiresIn = Number(this.config.get('RAYLAB_JWT_EXPIRES_IN') || 3600);
const access = this.jwtService.sign({ sub: domainUser.id, preferred_username: domainUser.username, email: domainUser.email }, { expiresIn }); const access = this.jwtService.sign({ sub: domainUser.id, preferred_username: domainUser.username, email: domainUser.email }, { expiresIn });
logger.debug(`Issued new access token for user=${userId} expiresIn=${expiresIn}`);
return { accessToken: access, refreshToken: newRefresh, expiresIn, refreshTtl }; return { accessToken: access, refreshToken: newRefresh, expiresIn, refreshTtl };
} }
@@ -204,13 +216,18 @@ export class AuthService {
} }
async me(token?: string) { async me(token?: string) {
if (!token) throw new UnauthorizedException('Missing token'); if (!token) {
logger.debug('Me called without token');
throw new UnauthorizedException('Missing token');
}
try { try {
const payload: any = this.jwtService.verify(token); const payload: any = this.jwtService.verify(token);
logger.debug(`Token verified successfully. payload.sub=${payload.sub}`);
const user = await this.userRepository.getById(payload.sub); const user = await this.userRepository.getById(payload.sub);
if (!user) throw new UnauthorizedException('User not found'); if (!user) throw new UnauthorizedException('User not found');
return { id: user.id, username: user.username, email: user.email, roles: user.roles || [] }; return { id: user.id, username: user.username, email: user.email, roles: user.roles || [] };
} catch (e) { } catch (e) {
logger.debug(`Token verification failed: ${(e as Error).message}`);
throw new UnauthorizedException('Invalid token'); throw new UnauthorizedException('Invalid token');
} }
} }