prisma with psotgresql
This commit is contained in:
@@ -15,9 +15,6 @@ import { UsersModule } from '../users/users.module';
|
||||
* - Integration with UsersModule for user synchronization
|
||||
*
|
||||
* The module requires the following environment variables:
|
||||
* - KEYCLOAK_AUTH_SERVER_URL: Base URL of Keycloak server
|
||||
* - KEYCLOAK_REALM: Keycloak realm name
|
||||
* - KEYCLOAK_CLIENT_ID: Client ID registered in Keycloak
|
||||
* - JWT_ISSUER: Expected JWT issuer
|
||||
* - JWT_AUDIENCE: Expected JWT audience
|
||||
* - JWKS_URI: URI for fetching Keycloak's public keys
|
||||
|
||||
@@ -98,7 +98,7 @@ describe('AuthService', () => {
|
||||
username: 'testuser',
|
||||
picture: 'https://example.com/avatar.jpg',
|
||||
roles: ['user', 'premium'],
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
|
||||
lastLoginAt: expect.any(Date),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -28,12 +28,12 @@ export class AuthService {
|
||||
authenticatedUser;
|
||||
|
||||
// Try to find existing user by Keycloak subject
|
||||
let user = this.usersService.findByKeycloakSub(keycloakSub);
|
||||
let user = await this.usersService.findByKeycloakSub(keycloakSub);
|
||||
|
||||
if (user) {
|
||||
// User exists - update last login and sync profile data
|
||||
this.logger.debug(`Syncing existing user: ${keycloakSub}`);
|
||||
user = this.usersService.updateFromToken(keycloakSub, {
|
||||
user = await this.usersService.updateFromToken(keycloakSub, {
|
||||
email,
|
||||
name,
|
||||
username,
|
||||
@@ -44,7 +44,7 @@ export class AuthService {
|
||||
} else {
|
||||
// New user - create from token data
|
||||
this.logger.log(`Creating new user from token: ${keycloakSub}`);
|
||||
user = this.usersService.createFromToken({
|
||||
user = await this.usersService.createFromToken({
|
||||
keycloakSub,
|
||||
email: email || '',
|
||||
name: name || username || 'Unknown User',
|
||||
@@ -54,7 +54,7 @@ export class AuthService {
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -40,10 +40,10 @@ export interface AuthenticatedUser {
|
||||
*/
|
||||
export const CurrentUser = createParamDecorator(
|
||||
(data: keyof AuthenticatedUser | undefined, ctx: ExecutionContext) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const user = request.user as AuthenticatedUser;
|
||||
const request = ctx
|
||||
.switchToHttp()
|
||||
.getRequest<{ user?: AuthenticatedUser }>();
|
||||
const user = request.user;
|
||||
|
||||
// If a specific property is requested, return only that property
|
||||
return data ? user?.[data] : user;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ExecutionContext, Injectable, Logger } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Observable } from 'rxjs';
|
||||
import type { Request } from 'express';
|
||||
import { User } from 'src/users/users.entity';
|
||||
|
||||
/**
|
||||
* JWT Authentication Guard
|
||||
@@ -29,13 +31,18 @@ export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||
context: ExecutionContext,
|
||||
): boolean | Promise<boolean> | Observable<boolean> {
|
||||
// Log the authentication attempt
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const request = context.switchToHttp().getRequest();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const authHeader = request.headers.authorization;
|
||||
|
||||
if (!authHeader) {
|
||||
this.logger.warn('Authentication attempt without Authorization header');
|
||||
this.logger.warn(
|
||||
'❌ Authentication attempt without Authorization header',
|
||||
);
|
||||
} else {
|
||||
const tokenPreview = String(authHeader).substring(0, 20);
|
||||
this.logger.debug(
|
||||
`🔐 Authentication attempt with token: ${tokenPreview}...`,
|
||||
);
|
||||
}
|
||||
|
||||
return super.canActivate(context);
|
||||
@@ -50,24 +57,32 @@ export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||
|
||||
handleRequest(
|
||||
err: any,
|
||||
user: any,
|
||||
user: User,
|
||||
info: any,
|
||||
context: ExecutionContext,
|
||||
status?: any,
|
||||
): any {
|
||||
const hasMessage = (value: unknown): value is { message?: unknown } =>
|
||||
typeof value === 'object' && value !== null && 'message' in value;
|
||||
|
||||
if (err || !user) {
|
||||
const infoMessage =
|
||||
info && typeof info === 'object' && 'message' in info
|
||||
? // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
String(info.message)
|
||||
: '';
|
||||
const errMessage =
|
||||
err && typeof err === 'object' && 'message' in err
|
||||
? // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
String(err.message)
|
||||
: '';
|
||||
this.logger.warn(
|
||||
`Authentication failed: ${infoMessage || errMessage || 'Unknown error'}`,
|
||||
const infoMessage = hasMessage(info) ? String(info.message) : '';
|
||||
const errMessage = hasMessage(err) ? String(err.message) : '';
|
||||
|
||||
this.logger.error(`❌ JWT Authentication failed`);
|
||||
this.logger.error(` Error: ${errMessage || 'none'}`);
|
||||
this.logger.error(` Info: ${infoMessage || 'none'}`);
|
||||
|
||||
if (info && typeof info === 'object') {
|
||||
this.logger.error(` Info details: ${JSON.stringify(info)}`);
|
||||
}
|
||||
|
||||
if (err && typeof err === 'object') {
|
||||
this.logger.error(` Error details: ${JSON.stringify(err)}`);
|
||||
}
|
||||
} else {
|
||||
this.logger.debug(
|
||||
`✅ JWT Authentication successful for user: ${user.keycloakSub || 'unknown'}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Strategy, ExtractJwt } from 'passport-jwt';
|
||||
|
||||
import { passportJwtSecret } from 'jwks-rsa';
|
||||
|
||||
/**
|
||||
@@ -74,7 +75,10 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
algorithms: ['RS256'],
|
||||
});
|
||||
|
||||
this.logger.log(`JWT Strategy initialized with issuer: ${issuer}`);
|
||||
this.logger.log(`JWT Strategy initialized`);
|
||||
this.logger.log(` JWKS URI: ${jwksUri}`);
|
||||
this.logger.log(` Issuer: ${issuer}`);
|
||||
this.logger.log(` Audience: ${audience || 'NOT SET'}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,6 +97,16 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
picture?: string;
|
||||
roles?: string[];
|
||||
}> {
|
||||
this.logger.debug(`Validating JWT token payload`);
|
||||
this.logger.debug(` Issuer: ${payload.iss}`);
|
||||
this.logger.debug(
|
||||
` Audience: ${Array.isArray(payload.aud) ? payload.aud.join(',') : payload.aud}`,
|
||||
);
|
||||
this.logger.debug(` Subject: ${payload.sub}`);
|
||||
this.logger.debug(
|
||||
` Expires: ${new Date(payload.exp * 1000).toISOString()}`,
|
||||
);
|
||||
|
||||
if (!payload.sub) {
|
||||
this.logger.warn('JWT token missing required "sub" claim');
|
||||
throw new UnauthorizedException('Invalid token: missing subject');
|
||||
@@ -120,7 +134,9 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
roles: roles.length > 0 ? roles : undefined,
|
||||
};
|
||||
|
||||
this.logger.debug(`Validated token for user: ${payload.sub}`);
|
||||
this.logger.log(
|
||||
`✅ Successfully validated token for user: ${payload.sub} (${payload.email ?? payload.preferred_username ?? 'no email'})`,
|
||||
);
|
||||
|
||||
return Promise.resolve(user);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user