einbau von rollen, neue Admin Ansicht

This commit is contained in:
2025-03-08 11:18:31 +01:00
parent dded8b8ca9
commit 5a56b3554d
29 changed files with 788 additions and 426 deletions

View File

@@ -19,12 +19,13 @@ import { ConfigModule } from '@nestjs/config';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { ClsMiddleware, ClsModule } from 'nestjs-cls';
import path from 'path';
import { AuthService } from './auth/auth.service';
import { FirebaseAdminModule } from './firebase-admin/firebase-admin.module';
import { LoggingInterceptor } from './interceptors/logging.interceptor';
import { UserInterceptor } from './interceptors/user.interceptor';
import { RequestDurationMiddleware } from './request-duration/request-duration.middleware';
import { SelectOptionsModule } from './select-options/select-options.module';
import { UserModule } from './user/user.module';
import { FirebaseAdminModule } from './firebase-admin/firebase-admin.module';
//loadEnvFiles();
console.log('Loaded environment variables:');
@@ -82,6 +83,7 @@ console.log('Loaded environment variables:');
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor, // Registriere den LoggingInterceptor global
},
AuthService,
],
})
export class AppModule implements NestModule {

View File

@@ -1,11 +1,17 @@
import { Body, Controller, HttpException, HttpStatus, Inject, Post } from '@nestjs/common';
import { Body, Controller, Get, HttpException, HttpStatus, Inject, Param, Post, Query, Req, UseGuards } from '@nestjs/common';
import * as admin from 'firebase-admin';
import { AdminGuard } from 'src/jwt-auth/admin-auth.guard';
import { AuthGuard } from 'src/jwt-auth/auth.guard';
import { LocalhostGuard } from 'src/jwt-auth/localhost.guard';
import { UserRole, UsersResponse } from 'src/models/main.model';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(
@Inject('FIREBASE_ADMIN')
private readonly firebaseAdmin: typeof admin,
private readonly authService: AuthService,
) {}
@Post('verify-email')
async verifyEmail(@Body('oobCode') oobCode: string, @Body('email') email: string) {
@@ -33,5 +39,91 @@ export class AuthController {
throw new HttpException(error.message || 'Failed to verify email', HttpStatus.BAD_REQUEST);
}
}
@Post(':uid/role')
@UseGuards(AuthGuard, AdminGuard) // Only admins can change roles
async setUserRole(@Param('uid') uid: string, @Body('role') role: UserRole): Promise<{ success: boolean }> {
await this.authService.setUserRole(uid, role);
return { success: true };
}
@Get('me/role')
@UseGuards(AuthGuard)
async getMyRole(@Req() req: any): Promise<{ role: UserRole | null }> {
console.log('->', req.user);
console.log('-->', req.user.uid);
const uid = req.user.uid; // From FirebaseAuthGuard
const role = await this.authService.getUserRole(uid);
return { role };
}
@Get(':uid/role')
@UseGuards(AuthGuard)
async getUserRole(@Param('uid') uid: string): Promise<{ role: UserRole | null }> {
const role = await this.authService.getUserRole(uid);
return { role };
}
@Get('role/:role')
@UseGuards(AuthGuard, AdminGuard) // Only admins can list users by role
async getUsersByRole(@Param('role') role: UserRole): Promise<{ users: any[] }> {
const users = await this.authService.getUsersByRole(role);
// Map to simpler objects to avoid circular references
const simplifiedUsers = users.map(user => ({
uid: user.uid,
email: user.email,
displayName: user.displayName,
}));
return { users: simplifiedUsers };
}
/**
* Ruft alle Firebase-Benutzer mit ihren Rollen ab
* @param maxResults Maximale Anzahl an zurückzugebenden Benutzern (optional, Standard: 1000)
* @param pageToken Token für die Paginierung (optional)
* @returns Eine Liste von Benutzern mit ihren Rollen und Metadaten
*/
@Get()
@UseGuards(AuthGuard, AdminGuard) // Only admins can list all users
async getAllUsers(@Query('maxResults') maxResults?: number, @Query('pageToken') pageToken?: string): Promise<UsersResponse> {
const result = await this.authService.getAllUsers(maxResults ? parseInt(maxResults.toString(), 10) : undefined, pageToken);
return {
users: result.users,
totalCount: result.users.length,
...(result.pageToken && { pageToken: result.pageToken }),
};
}
/**
* Endpoint zum direkten Einstellen einer Rolle für Debug-Zwecke
* WARNUNG: Dieser Endpoint sollte in der Produktion entfernt oder stark gesichert werden
*/
@Post('set-role')
@UseGuards(AuthGuard, LocalhostGuard)
async setUserRoleOnLocalhost(@Req() req: any, @Body('role') role: UserRole): Promise<{ success: boolean; message: string }> {
try {
const uid = req.user.uid;
// Aktuelle Rolle protokollieren
const currentUser = await this.authService.getUserRole(uid);
console.log(`Changing role for user ${uid} from ${currentUser} to ${role}`);
// Neue Rolle setzen
await this.authService.setUserRole(uid, role);
// Rolle erneut prüfen, um zu bestätigen
const newRole = await this.authService.getUserRole(uid);
return {
success: true,
message: `Rolle für Benutzer ${uid} von ${currentUser} zu ${newRole} geändert`,
};
} catch (error) {
console.error('Fehler beim Setzen der Rolle:', error);
return {
success: false,
message: `Fehler: ${error.message}`,
};
}
}
}

View File

@@ -2,11 +2,13 @@ import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { FirebaseAdminModule } from 'src/firebase-admin/firebase-admin.module';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
@Module({
imports: [ConfigModule.forRoot({ envFilePath: '.env' }),FirebaseAdminModule],
controllers: [AuthController],
providers: [AuthService],
exports: [],
})
export class AuthModule {}

View File

@@ -0,0 +1,113 @@
import { Inject, Injectable } from '@nestjs/common';
import * as admin from 'firebase-admin';
import { FirebaseUserInfo, UserRole } from 'src/models/main.model';
@Injectable()
export class AuthService {
constructor(@Inject('FIREBASE_ADMIN') private firebaseAdmin: admin.app.App) {}
/**
* Set a user's role via Firebase custom claims
*/
async setUserRole(uid: string, role: UserRole): Promise<void> {
try {
// Get the current custom claims
const user = await this.firebaseAdmin.auth().getUser(uid);
const currentClaims = user.customClaims || {};
// Set the new role
await this.firebaseAdmin.auth().setCustomUserClaims(uid, {
...currentClaims,
role: role,
});
} catch (error) {
console.error('Error setting user role:', error);
throw error;
}
}
/**
* Get a user's current role
*/
async getUserRole(uid: string): Promise<UserRole | null> {
try {
const user = await this.firebaseAdmin.auth().getUser(uid);
const claims = user.customClaims || {};
return (claims.role as UserRole) || null;
} catch (error) {
console.error('Error getting user role:', error);
throw error;
}
}
/**
* Get all users with a specific role
*/
async getUsersByRole(role: UserRole): Promise<admin.auth.UserRecord[]> {
// Note: Firebase Admin doesn't provide a direct way to query users by custom claims
// For a production app, you might want to store role information in Firestore as well
// This is a simple implementation that lists all users and filters them
try {
const listUsersResult = await this.firebaseAdmin.auth().listUsers();
return listUsersResult.users.filter(user => user.customClaims && user.customClaims.role === role);
} catch (error) {
console.error('Error getting users by role:', error);
throw error;
}
}
/**
* Get all Firebase users with their roles
* @param maxResults Maximum number of users to return (optional, default 1000)
* @param pageToken Token for pagination (optional)
*/
async getAllUsers(maxResults: number = 1000, pageToken?: string): Promise<{ users: FirebaseUserInfo[]; pageToken?: string }> {
try {
const listUsersResult = await this.firebaseAdmin.auth().listUsers(maxResults, pageToken);
const users = listUsersResult.users.map(user => this.mapUserRecord(user));
return {
users,
pageToken: listUsersResult.pageToken,
};
} catch (error) {
console.error('Error getting all users:', error);
throw error;
}
}
/**
* Maps a Firebase UserRecord to our FirebaseUserInfo interface
*/
private mapUserRecord(user: admin.auth.UserRecord): FirebaseUserInfo {
return {
uid: user.uid,
email: user.email || null,
displayName: user.displayName || null,
photoURL: user.photoURL || null,
phoneNumber: user.phoneNumber || null,
disabled: user.disabled,
emailVerified: user.emailVerified,
role: user.customClaims?.role || null,
creationTime: user.metadata.creationTime,
lastSignInTime: user.metadata.lastSignInTime,
// Optionally include other customClaims if needed
customClaims: user.customClaims,
};
}
/**
* Set default role for a new user
*/
async setDefaultRole(uid: string): Promise<void> {
return this.setUserRole(uid, 'guest');
}
/**
* Verify if a user has a specific role
*/
async hasRole(uid: string, role: UserRole): Promise<boolean> {
const userRole = await this.getUserRole(uid);
return userRole === role;
}
}

View File

@@ -0,0 +1,20 @@
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
@Injectable()
export class AdminGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
// The FirebaseAuthGuard should run before this guard
// and populate the request.user object
if (!request.user) {
throw new ForbiddenException('User not authenticated');
}
if (request.user.role !== 'admin') {
throw new ForbiddenException('Requires admin privileges');
}
return true;
}
}

View File

@@ -4,53 +4,39 @@ import * as admin from 'firebase-admin';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
@Inject('FIREBASE_ADMIN')
private readonly firebaseAdmin: typeof admin,
@Inject('FIREBASE_ADMIN') private firebaseAdmin: admin.app.App,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const token = this.extractTokenFromHeader(request);
const request = context.switchToHttp().getRequest();
const authHeader = request.headers.authorization;
if (!token) {
throw new UnauthorizedException('No token provided');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new UnauthorizedException('Missing or invalid authorization token');
}
const token = authHeader.split('Bearer ')[1];
try {
const decodedToken = await this.firebaseAdmin.auth().verifyIdToken(token);
request['user'] = decodedToken;
// Check if email is verified (optional but recommended)
if (!decodedToken.email_verified) {
throw new UnauthorizedException('Email not verified');
}
// Add the user to the request
request.user = {
uid: decodedToken.uid,
email: decodedToken.email,
role: decodedToken.role || null,
// Add other user info as needed
};
return true;
} catch (error) {
throw new UnauthorizedException('Invalid token');
}
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers['authorization']?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
// @Injectable()
// export class AuthGuard implements CanActivate {
// async canActivate(context: ExecutionContext): Promise<boolean> {
// const request = context.switchToHttp().getRequest<Request>();
// const token = this.extractTokenFromHeader(request);
// if (!token) {
// throw new UnauthorizedException('No token provided');
// }
// try {
// const decodedToken = await admin.auth().verifyIdToken(token);
// request['user'] = decodedToken; // Fügen Sie die Benutzerdaten dem Request-Objekt hinzu
// return true;
// } catch (error) {
// throw new UnauthorizedException('Invalid token');
// }
// }
// private extractTokenFromHeader(request: Request): string | undefined {
// const [type, token] = request.headers['authorization']?.split(' ') ?? [];
// return type === 'Bearer' ? token : undefined;
// }
// }

View File

@@ -1,16 +0,0 @@
// import * as admin from 'firebase-admin';
// import { ServiceAccount } from 'firebase-admin';
// console.log('--> '+process.env['FIREBASE_PROJECT_ID'])
// const serviceAccount: ServiceAccount = {
// projectId: process.env['FIREBASE_PROJECT_ID'],
// clientEmail: process.env['FIREBASE_CLIENT_EMAIL'],
// privateKey: process.env['FIREBASE_PRIVATE_KEY']?.replace(/\\n/g, '\n'), // Ersetzen Sie escaped newlines
// };
// if (!admin.apps.length) {
// admin.initializeApp({
// credential: admin.credential.cert(serviceAccount),
// });
// }
// export default admin;

View File

@@ -0,0 +1,21 @@
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
import { Request } from 'express';
import { Observable } from 'rxjs';
@Injectable()
export class LocalhostGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const ip = request.ip;
// Liste der erlaubten IPs
const allowedIPs = ['127.0.0.1', '::1', 'localhost', '::ffff:127.0.0.1'];
if (!allowedIPs.includes(ip)) {
console.warn(`Versuchter Zugriff von unerlaubter IP: ${ip}`);
throw new ForbiddenException('Dieser Endpunkt kann nur lokal aufgerufen werden');
}
return true;
}
}

View File

@@ -3,54 +3,70 @@ import * as admin from 'firebase-admin';
@Injectable()
export class OptionalAuthGuard implements CanActivate {
constructor(
@Inject('FIREBASE_ADMIN')
private readonly firebaseAdmin: typeof admin,
) {}
constructor(@Inject('FIREBASE_ADMIN') private firebaseAdmin: admin.app.App) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const token = this.extractTokenFromHeader(request);
const request = context.switchToHttp().getRequest();
const authHeader = request.headers.authorization;
if (!token) {
if (!authHeader || !authHeader.startsWith('Bearer ')) {
//throw new UnauthorizedException('Missing or invalid authorization token');
return true;
}
const token = authHeader.split('Bearer ')[1];
try {
const decodedToken = await this.firebaseAdmin.auth().verifyIdToken(token);
request['user'] = decodedToken;
// Check if email is verified (optional but recommended)
if (!decodedToken.email_verified) {
//throw new UnauthorizedException('Email not verified');
return true;
}
// Add the user to the request
request.user = {
uid: decodedToken.uid,
email: decodedToken.email,
role: decodedToken.role || null,
// Add other user info as needed
};
return true;
} catch (error) {
//throw new UnauthorizedException('Invalid token');
request['user'] = null;
return true;
}
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers['authorization']?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
// import { CanActivate, ExecutionContext, Inject, Injectable } from '@nestjs/common';
// import * as admin from 'firebase-admin';
// @Injectable()
// export class OptionalAuthGuard implements CanActivate {
// constructor(
// @Inject('FIREBASE_ADMIN')
// private readonly firebaseAdmin: typeof admin,
// ) {}
// async canActivate(context: ExecutionContext): Promise<boolean> {
// const request = context.switchToHttp().getRequest<Request>();
// const token = this.extractTokenFromHeader(request);
// if (!token) {
// return true; // Kein Token vorhanden, aber Zugriff erlaubt
// return true;
// }
// try {
// const decodedToken = await admin.auth().verifyIdToken(token);
// request['user'] = decodedToken; // Benutzerdaten zum Request hinzufügen, wenn Token gültig
// const decodedToken = await this.firebaseAdmin.auth().verifyIdToken(token);
// request['user'] = decodedToken;
// return true;
// } catch (error) {
// // Bei ungültigem Token wird kein Fehler geworfen, sondern einfach kein User gesetzt
// //throw new UnauthorizedException('Invalid token');
// request['user'] = null;
// return true;
// }
// return true; // Zugriff wird immer erlaubt
// }
// private extractTokenFromHeader(request: Request): string | undefined {

View File

@@ -278,6 +278,26 @@ export interface Checkout {
email: string;
name: string;
}
export type UserRole = 'admin' | 'pro' | 'guest' | null;
export interface FirebaseUserInfo {
uid: string;
email: string | null;
displayName: string | null;
photoURL: string | null;
phoneNumber: string | null;
disabled: boolean;
emailVerified: boolean;
role: UserRole;
creationTime?: string;
lastSignInTime?: string;
customClaims?: Record<string, any>;
}
export interface UsersResponse {
users: FirebaseUserInfo[];
totalCount: number;
pageToken?: string;
}
export function isEmpty(value: any): boolean {
// Check for undefined or null
if (value === undefined || value === null) {

View File

@@ -0,0 +1,27 @@
import { Injectable } from '@nestjs/common';
import { Command, CommandRunner } from 'nest-commander';
import { AuthService } from './auth/auth.service';
@Injectable()
@Command({ name: 'setup-admin', description: 'Set up the first admin user' })
export class SetupAdminCommand extends CommandRunner {
constructor(private readonly authService: AuthService) {
super();
}
async run(passedParams: string[]): Promise<void> {
if (passedParams.length < 1) {
console.error('Please provide a user UID');
return;
}
const uid = passedParams[0];
try {
await this.authService.setUserRole(uid, 'admin');
console.log(`User ${uid} has been set as admin`);
} catch (error) {
console.error('Error setting admin role:', error);
}
}
}

View File

@@ -4,6 +4,7 @@ import { Logger } from 'winston';
import { ZodError } from 'zod';
import { FileService } from '../file/file.service';
import { AdminGuard } from 'src/jwt-auth/admin-auth.guard';
import { AuthGuard } from 'src/jwt-auth/auth.guard';
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard';
import { User } from '../models/db.model';
@@ -31,11 +32,11 @@ export class UserController {
const user = await this.userService.getUserById(id);
return user;
}
// @UseGuards(AdminAuthGuard)
// @Get('user/all')
// async getAllUser(): Promise<User[]> {
// return await this.userService.getAllUser();
// }
@UseGuards(AdminGuard)
@Get('user/all')
async getAllUser(): Promise<User[]> {
return await this.userService.getAllUser();
}
@UseGuards(OptionalAuthGuard)
@Post()