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,7 +1,17 @@
import { Body, Controller, HttpCode, HttpStatus, Post } from "@nestjs/common";
import { SignInDto, SignUpDto } from "src/auth/auth.dto";
import {
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 * as console from "node:console";
import { UserGuard } from "./auth.guard";
@Controller("auth")
export class AuthController {
@ -23,6 +33,42 @@ export class AuthController {
return this.authService.doLogin(dto);
}
//GET me -- Get current user data via jwt
//DELETE me
//PATCH me
@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
@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
@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;
}
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 { Request } from 'express';
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException, Inject } from "@nestjs/common";
import { Request } from "express";
import { CredentialsService } from "src/credentials/credentials.service";
import { DrizzleService } from "src/drizzle/drizzle.service";
import { UsersTable } from "src/schema";
@ -8,37 +8,35 @@ import { Reflector } from "@nestjs/core";
@Injectable()
export class UserGuard implements CanActivate {
private readonly credentialService: CredentialsService;
private readonly databaseService: DrizzleService;
constructor() {
const reflector = new Reflector();
this.credentialService = reflector.get<CredentialsService>('CredentialsService', UserGuard);
this.databaseService = reflector.get<DrizzleService>('DrizzleService', UserGuard);
constructor(
@Inject(CredentialsService) private readonly credentialService: CredentialsService,
@Inject(DrizzleService) private readonly databaseService: DrizzleService,
) {
}
async canActivate(
context: ExecutionContext,
context: ExecutionContext
): Promise<boolean> {
const request: Request = context.switchToHttp().getRequest();
const authHeader = request.headers.authorization;
if (!authHeader)
throw new UnauthorizedException('No authorization header found.');
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()
.select()
.from(UsersTable)
.where(eq(UsersTable.uuid, vToken.payload.sub))
.where(eq(UsersTable.uuid, vToken.payload.sub));
if (user.length !== 1)
throw new UnauthorizedException('No such user found.');
throw new UnauthorizedException("No such user found.");
if (user[0].emailCode)
throw new UnauthorizedException('Email not verified.');
throw new UnauthorizedException("Email not verified.");
// Inject user ID into request body
request.body.sourceUserId = vToken.payload.sub;
@ -49,33 +47,34 @@ export class UserGuard implements CanActivate {
@Injectable()
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(
context: ExecutionContext,
context: ExecutionContext
): Promise<boolean> {
const request: Request = context.switchToHttp().getRequest();
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()
.select()
.from(UsersTable)
.where(eq(UsersTable.uuid, vToken.payload.sub))
.where(eq(UsersTable.uuid, vToken.payload.sub));
if (user.length !== 1)
throw new UnauthorizedException('No such user found.');
throw new UnauthorizedException("No such user found.");
if (!user[0].isAdmin) {
throw new UnauthorizedException('Administrator only..');
throw new UnauthorizedException("Administrator only..");
}
// 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 { AuthController } from "./auth.controller";
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({
imports: [DrizzleModule, CredentialsModule],
providers: [AuthService, AdminGuard],
providers: [AuthService, CredentialsService, AdminGuard, UserGuard],
controllers: [AuthController],
})
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
const usersInDb = await this.db.use().select().from(UsersTable);
const result = {
@ -134,7 +150,7 @@ export class AuthService implements OnModuleInit {
async onModuleInit() {
setTimeout(() => {
this.fetchUser();
this.fetchUsers();
}, 2000);
}
}