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:
parent
46f8a61c9e
commit
c0a61cde3b
76
apps/backend/src/app/auth/auth.controller.ts
Normal file
76
apps/backend/src/app/auth/auth.controller.ts
Normal 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)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
68
apps/backend/src/app/auth/auth.dto.ts
Normal file
68
apps/backend/src/app/auth/auth.dto.ts
Normal 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;
|
||||||
|
}
|
||||||
|
**/
|
87
apps/backend/src/app/auth/auth.guard.ts
Normal file
87
apps/backend/src/app/auth/auth.guard.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
14
apps/backend/src/app/auth/auth.module.ts
Normal file
14
apps/backend/src/app/auth/auth.module.ts
Normal 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 {}
|
159
apps/backend/src/app/auth/auth.service.ts
Normal file
159
apps/backend/src/app/auth/auth.service.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user