feat(auth): add optional authentication guard and extend AuthModule providers
Some checks failed
Backend Tests / test (push) Failing after 5m0s
Lint / lint (push) Failing after 5m2s

Introduce `OptionalAuthGuard` to allow conditional authentication for routes. Update `AuthModule` to include `AuthGuard`, `OptionalAuthGuard`, and `RolesGuard` in providers and exports for broader reuse.

feat(app): integrate `AdminModule` into app module

Add `AdminModule` to the app's main module to enable administration functionalities.

feat(users): enhance user profiles with bio and avatar fields

Extend `UpdateUserDto` to include optional `bio` and `avatarUrl` fields for better user customization.

feat(categories): add functionality to count all categories

Implement `countAll` method in `CategoriesRepository` to fetch the total number of categories using raw SQL counting.
This commit is contained in:
Mathis HERRIOT
2026-01-14 21:45:32 +01:00
parent 0e9edd4bfc
commit 65f8860cc0
5 changed files with 81 additions and 3 deletions

View File

@@ -5,6 +5,7 @@ import { ScheduleModule } from "@nestjs/schedule";
import { ThrottlerModule } from "@nestjs/throttler"; import { ThrottlerModule } from "@nestjs/throttler";
import { redisStore } from "cache-manager-redis-yet"; import { redisStore } from "cache-manager-redis-yet";
import { ApiKeysModule } from "./api-keys/api-keys.module"; import { ApiKeysModule } from "./api-keys/api-keys.module";
import { AdminModule } from "./admin/admin.module";
import { AppController } from "./app.controller"; import { AppController } from "./app.controller";
import { AppService } from "./app.service"; import { AppService } from "./app.service";
import { AuthModule } from "./auth/auth.module"; import { AuthModule } from "./auth/auth.module";
@@ -42,6 +43,7 @@ import { UsersModule } from "./users/users.module";
SessionsModule, SessionsModule,
ReportsModule, ReportsModule,
ApiKeysModule, ApiKeysModule,
AdminModule,
ScheduleModule.forRoot(), ScheduleModule.forRoot(),
ThrottlerModule.forRootAsync({ ThrottlerModule.forRootAsync({
imports: [ConfigModule], imports: [ConfigModule],

View File

@@ -5,6 +5,9 @@ import { SessionsModule } from "../sessions/sessions.module";
import { UsersModule } from "../users/users.module"; import { UsersModule } from "../users/users.module";
import { AuthController } from "./auth.controller"; import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service"; import { AuthService } from "./auth.service";
import { AuthGuard } from "./guards/auth.guard";
import { OptionalAuthGuard } from "./guards/optional-auth.guard";
import { RolesGuard } from "./guards/roles.guard";
import { RbacService } from "./rbac.service"; import { RbacService } from "./rbac.service";
import { RbacRepository } from "./repositories/rbac.repository"; import { RbacRepository } from "./repositories/rbac.repository";
@@ -16,7 +19,21 @@ import { RbacRepository } from "./repositories/rbac.repository";
DatabaseModule, DatabaseModule,
], ],
controllers: [AuthController], controllers: [AuthController],
providers: [AuthService, RbacService, RbacRepository], providers: [
exports: [AuthService, RbacService, RbacRepository], AuthService,
RbacService,
RbacRepository,
AuthGuard,
OptionalAuthGuard,
RolesGuard,
],
exports: [
AuthService,
RbacService,
RbacRepository,
AuthGuard,
OptionalAuthGuard,
RolesGuard,
],
}) })
export class AuthModule {} export class AuthModule {}

View File

@@ -0,0 +1,43 @@
import {
CanActivate,
ExecutionContext,
Injectable,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { getIronSession } from "iron-session";
import { JwtService } from "../../crypto/services/jwt.service";
import { getSessionOptions, SessionData } from "../session.config";
@Injectable()
export class OptionalAuthGuard implements CanActivate {
constructor(
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
const session = await getIronSession<SessionData>(
request,
response,
getSessionOptions(this.configService.get("SESSION_PASSWORD") as string),
);
const token = session.accessToken;
if (!token) {
return true;
}
try {
const payload = await this.jwtService.verifyJwt(token);
request.user = payload;
} catch {
// Ignore invalid tokens for optional auth
}
return true;
}
}

View File

@@ -1,5 +1,5 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { eq } from "drizzle-orm"; import { eq, sql } from "drizzle-orm";
import { DatabaseService } from "../../database/database.service"; import { DatabaseService } from "../../database/database.service";
import { categories } from "../../database/schemas"; import { categories } from "../../database/schemas";
import type { CreateCategoryDto } from "../dto/create-category.dto"; import type { CreateCategoryDto } from "../dto/create-category.dto";
@@ -16,6 +16,13 @@ export class CategoriesRepository {
.orderBy(categories.name); .orderBy(categories.name);
} }
async countAll() {
const result = await this.databaseService.db
.select({ count: sql<number>`count(*)` })
.from(categories);
return Number(result[0].count);
}
async findOne(id: string) { async findOne(id: string) {
const result = await this.databaseService.db const result = await this.databaseService.db
.select() .select()

View File

@@ -5,4 +5,13 @@ export class UpdateUserDto {
@IsString() @IsString()
@MaxLength(32) @MaxLength(32)
displayName?: string; displayName?: string;
@IsOptional()
@IsString()
@MaxLength(255)
bio?: string;
@IsOptional()
@IsString()
avatarUrl?: string;
} }