feat(auth): add user management endpoint and update guards

Introduce new "me" endpoints for fetching, updating, and deleting current user data. Update guards to properly inject dependent services using @Inject decorator for better DI handling.
This commit is contained in:
Mathis H (Avnyr) 2024-07-24 20:25:46 +02:00
parent 5d24f36133
commit 27f79d4e46
Signed by: Mathis
GPG Key ID: DD9E0666A747D126
5 changed files with 124 additions and 48 deletions

View File

@ -1,11 +1,21 @@
import { Body, Controller, HttpCode, HttpStatus, Post } from "@nestjs/common"; import {
import { SignInDto, SignUpDto } from "src/auth/auth.dto"; Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus, Patch,
Post,
UnauthorizedException,
UseGuards
} from "@nestjs/common";
import { SignInDto, SignUpDto, UpdateUserDto } from "src/auth/auth.dto";
import { AuthService } from "src/auth/auth.service"; import { AuthService } from "src/auth/auth.service";
import * as console from "node:console"; import { UserGuard } from "./auth.guard";
@Controller("auth") @Controller("auth")
export class AuthController { export class AuthController {
constructor(private readonly authService: AuthService) {} constructor(private readonly authService: AuthService) { }
//POST signup //POST signup
@HttpCode(HttpStatus.CREATED) @HttpCode(HttpStatus.CREATED)
@ -23,6 +33,42 @@ export class AuthController {
return this.authService.doLogin(dto); return this.authService.doLogin(dto);
} }
//GET me -- Get current user data via jwt //GET me -- Get current user data via jwt
@HttpCode(HttpStatus.OK)
@Get("me")
@UseGuards(UserGuard)
async getMe(@Body() body: object) {
// @ts-ignore
const targetId = body.sourceUserId
const userData = await this.authService.fetchUserById(targetId)
if (!userData) {
throw new UnauthorizedException();
}
return userData;
}
//DELETE me //DELETE me
@HttpCode(HttpStatus.FOUND)
@Delete("me")
@UseGuards(UserGuard)
async deleteMe(@Body() body: object) {
// @ts-ignore
const targetId = body.sourceUserId
try {
await this.authService.deleteUser(targetId)
} catch (err) {
throw new UnauthorizedException();
}
}
//PATCH me //PATCH me
@HttpCode(HttpStatus.OK)
@Patch("me")
@UseGuards(UserGuard)
async patchMe(@Body() body: UpdateUserDto) {
console.log(body);
// @ts-ignore
const targetId = body.sourceUserId;
await this.authService.updateUser(targetId, body);
return await this.authService.fetchUserById(targetId)
}
} }

View File

@ -48,3 +48,17 @@ export class SignInDto {
}) })
password: string; password: string;
} }
export class UpdateUserDto {
@MinLength(1)
@MaxLength(24)
@IsNotEmpty()
@IsString()
firstName: string;
@MinLength(1)
@MaxLength(24)
@IsNotEmpty()
@IsString()
lastName: string;
}

View File

@ -1,5 +1,5 @@
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; import { Injectable, CanActivate, ExecutionContext, UnauthorizedException, Inject } from "@nestjs/common";
import { Request } from 'express'; import { Request } from "express";
import { CredentialsService } from "src/credentials/credentials.service"; import { CredentialsService } from "src/credentials/credentials.service";
import { DrizzleService } from "src/drizzle/drizzle.service"; import { DrizzleService } from "src/drizzle/drizzle.service";
import { UsersTable } from "src/schema"; import { UsersTable } from "src/schema";
@ -8,37 +8,35 @@ import { Reflector } from "@nestjs/core";
@Injectable() @Injectable()
export class UserGuard implements CanActivate { export class UserGuard implements CanActivate {
private readonly credentialService: CredentialsService;
private readonly databaseService: DrizzleService;
constructor() { constructor(
const reflector = new Reflector(); @Inject(CredentialsService) private readonly credentialService: CredentialsService,
this.credentialService = reflector.get<CredentialsService>('CredentialsService', UserGuard); @Inject(DrizzleService) private readonly databaseService: DrizzleService,
this.databaseService = reflector.get<DrizzleService>('DrizzleService', UserGuard); ) {
} }
async canActivate( async canActivate(
context: ExecutionContext, context: ExecutionContext
): Promise<boolean> { ): Promise<boolean> {
const request: Request = context.switchToHttp().getRequest(); const request: Request = context.switchToHttp().getRequest();
const authHeader = request.headers.authorization; const authHeader = request.headers.authorization;
if (!authHeader) if (!authHeader)
throw new UnauthorizedException('No authorization header found.'); throw new UnauthorizedException("No authorization header found.");
const token = authHeader.split(' ')[1]; const token = authHeader.split(" ")[1];
const vToken = await this.credentialService.verifyAuthToken(token) const vToken = await this.credentialService.verifyAuthToken(token);
const user = await this.databaseService.use() const user = await this.databaseService.use()
.select() .select()
.from(UsersTable) .from(UsersTable)
.where(eq(UsersTable.uuid, vToken.payload.sub)) .where(eq(UsersTable.uuid, vToken.payload.sub));
if (user.length !== 1) if (user.length !== 1)
throw new UnauthorizedException('No such user found.'); throw new UnauthorizedException("No such user found.");
if (user[0].emailCode) if (user[0].emailCode)
throw new UnauthorizedException('Email not verified.'); throw new UnauthorizedException("Email not verified.");
// Inject user ID into request body // Inject user ID into request body
request.body.sourceUserId = vToken.payload.sub; request.body.sourceUserId = vToken.payload.sub;
@ -49,33 +47,34 @@ export class UserGuard implements CanActivate {
@Injectable() @Injectable()
export class AdminGuard implements CanActivate { export class AdminGuard implements CanActivate {
constructor(
private readonly credentialService: CredentialsService,
private readonly databaseService: DrizzleService
) {}
constructor(
@Inject(CredentialsService) private readonly credentialService: CredentialsService,
@Inject(DrizzleService) private readonly databaseService: DrizzleService,
) {}
async canActivate( async canActivate(
context: ExecutionContext, context: ExecutionContext
): Promise<boolean> { ): Promise<boolean> {
const request: Request = context.switchToHttp().getRequest(); const request: Request = context.switchToHttp().getRequest();
const authHeader = request.headers.authorization; const authHeader = request.headers.authorization;
if (!authHeader) {
throw new UnauthorizedException("No authorization header found.");
if (!authHeader) }
throw new UnauthorizedException('No authorization header found.'); const token = authHeader.split(" ")[1];
const vToken = await this.credentialService.verifyAuthToken(token);
const token = authHeader.split(' ')[1];
const vToken = await this.credentialService.verifyAuthToken(token)
const user = await this.databaseService.use() const user = await this.databaseService.use()
.select() .select()
.from(UsersTable) .from(UsersTable)
.where(eq(UsersTable.uuid, vToken.payload.sub)) .where(eq(UsersTable.uuid, vToken.payload.sub));
if (user.length !== 1) if (user.length !== 1)
throw new UnauthorizedException('No such user found.'); throw new UnauthorizedException("No such user found.");
if (!user[0].isAdmin) { if (!user[0].isAdmin) {
throw new UnauthorizedException('Administrator only..'); throw new UnauthorizedException("Administrator only..");
} }
// Inject user ID into request body // Inject user ID into request body

View File

@ -3,11 +3,12 @@ import { CredentialsModule } from "src/credentials/credentials.module";
import { DrizzleModule } from "src/drizzle/drizzle.module"; import { DrizzleModule } from "src/drizzle/drizzle.module";
import { AuthController } from "./auth.controller"; import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service"; import { AuthService } from "./auth.service";
import { AdminGuard } from "src/auth/auth.guard"; import { AdminGuard, UserGuard } from "src/auth/auth.guard";
import { CredentialsService } from "src/credentials/credentials.service";
@Module({ @Module({
imports: [DrizzleModule, CredentialsModule], imports: [DrizzleModule, CredentialsModule],
providers: [AuthService, AdminGuard], providers: [AuthService, CredentialsService, AdminGuard, UserGuard],
controllers: [AuthController], controllers: [AuthController],
}) })
export class AuthModule {} export class AuthModule {}

View File

@ -75,7 +75,23 @@ export class AuthService implements OnModuleInit {
}; };
} }
async fetchUser() { async fetchUserById(userId: string) {
const user = await this.db
.use()
.select()
.from(UsersTable)
.where(eq(UsersTable.uuid, userId))
.prepare("userById")
.execute();
if (user.length !== 1) {
throw new UnauthorizedException("User not found");
}
delete user[0].hash;
delete user[0].emailCode;
return user[0];
}
async fetchUsers() {
//TODO Pagination //TODO Pagination
const usersInDb = await this.db.use().select().from(UsersTable); const usersInDb = await this.db.use().select().from(UsersTable);
const result = { const result = {
@ -134,7 +150,7 @@ export class AuthService implements OnModuleInit {
async onModuleInit() { async onModuleInit() {
setTimeout(() => { setTimeout(() => {
this.fetchUser(); this.fetchUsers();
}, 2000); }, 2000);
} }
} }