prisma with psotgresql

This commit is contained in:
2025-11-23 23:53:13 +08:00
parent d88c2057c0
commit 978158353c
25 changed files with 1718 additions and 407 deletions

View File

@@ -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

View File

@@ -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),
}),
);

View File

@@ -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;
}
/**

View File

@@ -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;

View File

@@ -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'}`,
);
}

View File

@@ -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);
}