Add authentication module with user and admin guards

Implemented the `AuthModule` with necessary controllers and services to handle user authentication and authorization. Added `UserGuard` and `AdminGuard` to secure endpoints based on user roles.
This commit is contained in:
Mathis H (Avnyr) 2024-09-02 14:57:13 +02:00
parent 46f8a61c9e
commit c0a61cde3b
No known key found for this signature in database
GPG Key ID: FF69BF8BF95CDD58
5 changed files with 404 additions and 0 deletions

View File

@ -0,0 +1,76 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus, Patch,
Post,
UnauthorizedException,
UseGuards
} from "@nestjs/common";
import { UserGuard } from "./auth.guard";
import { AuthService } from 'apps/backend/src/app/auth/auth.service';
import { SignInDto, SignUpDto } from 'apps/backend/src/app/auth/auth.dto';
@Controller("auth")
export class AuthController {
constructor(private readonly authService: AuthService) { }
//TODO Initial account validation for admin privileges
//POST signup
@HttpCode(HttpStatus.CREATED)
@Post("signup")
async signUp(@Body() dto: SignUpDto) {
console.log(dto);
return this.authService.doRegister(dto);
}
//POST signin
@HttpCode(HttpStatus.OK)
@Post("signin")
async signIn(@Body() dto: SignInDto) {
console.log(dto);
return this.authService.doLogin(dto);
}
//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
@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

@ -0,0 +1,68 @@
import {
IsEmail,
IsNotEmpty,
IsString,
IsStrongPassword,
MaxLength,
MinLength,
} from "class-validator";
export class SignUpDto {
/*
@MinLength(1)
@MaxLength(24)
@IsNotEmpty()
@IsString()
firstName: string;
@MinLength(1)
@MaxLength(24)
@IsNotEmpty()
@IsString()
lastName: string;
**/
@MaxLength(32)
@IsEmail()
@IsNotEmpty()
email: string;
@IsString()
@IsNotEmpty()
@IsStrongPassword({
minLength: 6,
minSymbols: 1,
})
password: string;
}
export class SignInDto {
@MaxLength(32)
@IsEmail()
@IsNotEmpty()
email: string;
@IsString()
@IsNotEmpty()
@IsStrongPassword({
minLength: 6,
minSymbols: 1,
})
password: string;
}
/*
export class UpdateUserDto {
@MinLength(1)
@MaxLength(24)
@IsNotEmpty()
@IsString()
firstName: string;
@MinLength(1)
@MaxLength(24)
@IsNotEmpty()
@IsString()
lastName: string;
}
**/

View File

@ -0,0 +1,87 @@
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException, Inject } from "@nestjs/common";
import type { Request } from "express";
import { eq } from "drizzle-orm";
import { Reflector } from "@nestjs/core";
import { DbService } from 'apps/backend/src/app/db/db.service';
import { UsersTable } from 'apps/backend/src/app/db/schema';
import { CredentialsService } from 'apps/backend/src/app/credentials/credentials.service';
@Injectable()
export class UserGuard implements CanActivate {
constructor(
@Inject(CredentialsService) private readonly credentialService: CredentialsService,
@Inject(DbService) private readonly databaseService: DbService,
) {
}
async canActivate(
context: ExecutionContext
): Promise<boolean> {
const request: Request = context.switchToHttp().getRequest();
const authHeader = request.headers.authorization;
if (!authHeader)
throw new UnauthorizedException("No authorization header found.");
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));
if (user.length !== 1)
throw new UnauthorizedException("No such user found.");
/*
if (user[0].emailCode)
throw new UnauthorizedException("Email not verified.");
*/
// Inject user ID into request body
request.body.sourceUserId = vToken.payload.sub;
return true;
}
}
@Injectable()
export class AdminGuard implements CanActivate {
constructor(
@Inject(CredentialsService) private readonly credentialService: CredentialsService,
@Inject(DbService) private readonly databaseService: DbService,
) {}
async canActivate(
context: ExecutionContext
): Promise<boolean> {
const request: Request = context.switchToHttp().getRequest();
const authHeader = request.headers.authorization;
if (!authHeader) {
throw new UnauthorizedException("No authorization header found.");
}
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));
if (user.length !== 1)
throw new UnauthorizedException("No such user found.");
if (!user[0].isAdmin) {
throw new UnauthorizedException("Administrator only..");
}
// Inject user ID into request body
request.body.sourceUserId = vToken.payload.sub;
return true;
}
}

View File

@ -0,0 +1,14 @@
import { Module } from "@nestjs/common";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { DbModule } from 'apps/backend/src/app/db/db.module';
import { CredentialsModule } from 'apps/backend/src/app/credentials/credentials.module';
import { CredentialsService } from 'apps/backend/src/app/credentials/credentials.service';
import { AdminGuard, UserGuard } from 'apps/backend/src/app/auth/auth.guard';
@Module({
imports: [DbModule, CredentialsModule],
providers: [AuthService, CredentialsService, AdminGuard, UserGuard],
controllers: [AuthController],
})
export class AuthModule {}

View File

@ -0,0 +1,159 @@
import {
Injectable,
OnModuleInit,
UnauthorizedException,
} from "@nestjs/common";
import { eq } from "drizzle-orm";
import { DbService } from 'apps/backend/src/app/db/db.service';
import { CredentialsService } from 'apps/backend/src/app/credentials/credentials.service';
import { UsersTable } from 'apps/backend/src/app/db/schema';
import { SignInDto, SignUpDto } from 'apps/backend/src/app/auth/auth.dto';
@Injectable()
export class AuthService implements OnModuleInit {
constructor(
private db: DbService,
private credentials: CredentialsService,
) {}
//TODO Initial account validation for admin privileges
async doRegister(data: SignUpDto) {
console.log(data);
const existingUser = await this.db
.use()
.select()
.from(UsersTable)
.where(eq(UsersTable.email, data.email))
.prepare("userByEmail")
.execute();
if (existingUser.length !== 0)
throw new UnauthorizedException("Already exist");
const query = await this.db
.use()
.insert(UsersTable)
.values({
//firstName: data.firstName,
//lastName: data.lastName,
email: data.email,
hash: await this.credentials.hash(data.password),
})
.returning()
.prepare("insertUser")
.execute()
.catch((err) => {
console.error(err);
throw new UnauthorizedException(
"Error occurred while inserting user",
err,
);
});
return {
message: "User created, check your email for validation.",
token: await this.credentials.signAuthToken({ sub: query[0].uuid }),
};
}
async doLogin(data: SignInDto) {
const user = await this.db
.use()
.select()
.from(UsersTable)
.where(eq(UsersTable.email, data.email))
.prepare("userByEmail")
.execute();
if (user.length !== 1)
throw new UnauthorizedException("Invalid credentials");
const passwordMatch = await this.credentials.check(
data.password,
user[0].hash,
);
if (!passwordMatch) throw new UnauthorizedException("Invalid credentials");
const token = await this.credentials.signAuthToken({ sub: user[0].uuid });
return {
message: "Login successful",
token: token,
};
}
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 = {
total: usersInDb.length,
users: usersInDb.map((user) => {
delete user.hash;
return {
...user,
};
}),
};
console.log(result);
return result;
}
/*
async updateUser(targetId: string, userData: IUserUpdateData) {
const validationResult = UserUpdateSchema.safeParse(userData);
if (!validationResult.success) {
throw new UnauthorizedException(validationResult.error);
}
const updateQuery = await this.db
.use()
.update(UsersTable)
.set({
...userData
})
.where(eq(UsersTable.uuid, targetId))
.prepare("updateUserById")
.execute()
.catch((err) => {
console.error(err);
throw new UnauthorizedException(
"Error occurred while updating user",
err,
);
});
return true;
}
*/
async deleteUser(targetId: string) {
await this.db
.use()
.delete(UsersTable)
.where(eq(UsersTable.uuid, targetId))
.prepare("deleteUserById")
.execute()
.catch((err) => {
console.error(err);
throw new UnauthorizedException(
"Error occurred while deleting user",
err,
);
});
return true;
}
async onModuleInit() {
setTimeout(() => {
this.fetchUsers();
}, 2000);
}
}