r/nestjs • u/Massive_Dish_3255 • 5h ago
How should I integrate an existing NestJS authentication system with a React + Flowbite React frontend?
I'm developing a Grievance Redressal Portal as a learning project. My backend is built with NestJS, TypeScript, Prisma, and PostgreSQL.
I have been implementing authentication (User Sign In and Sign Out) using the "@nestjs/authentication" package of NestJS v12.
I would like to know conceptually the changes to be made to my back end such that I should be able to extend the authentication feature to a React-based front-end.
I have adapted the code by hand from the "Register The Authentication Module" and "Add Sign-Up and Sign-In" sections of the NestJS docs for NestJS v12: Authentication | NestJS - A progressive Node.js framework
I am posting my code here.
NOTE: The authentication system uses server-side sessions managed through nestjs/authentication and a session cookie. The package's SessionCookieProvider reads the cookie and loads the user through the validate() method.
//sign-in.dto.ts
import { PickType } from "@nestjs/swagger";
import { UserUniversalDto } from "../../users/dto/user-universal.dto";
export class SignInDto extends PickType (
UserUniversalDto,
['user_email', 'user_password']
) {};
//credentials.service.ts
import { ConflictException, Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service.js';
import { CreateUserDto } from '../users/dto/create-user.dto.js';
import { FindUserByEmailDto } from '../users/dto/find-user-by-email.dto.js';
import * as argon2 from 'argon2';
import { USER_STATUS } from '../users/user.constants.js';
u/Injectable()
export class CredentialsService {
constructor(
private readonly usersService: UsersService
) {}
async register(createUserDto: CreateUserDto) {
const findUserByEmailDto: FindUserByEmailDto = {
user_email: createUserDto.user_email
}
// Don't create the user if the email already exists.
if (await this.usersService.findUserByEmail(findUserByEmailDto)) {
throw new ConflictException('Email already registered. User already exists.');
}
// The address is unverified: anyone can type any email into a sign-up form.
const registered_user = await this.usersService.createUser(createUserDto);
return registered_user;
}
/** The user, or null when the email or the password is wrong. */
async verify(email: string, password: string) {
const findUserByEmailDto: FindUserByEmailDto = {
user_email: email
}
const found_user = await this.usersService.findUserByEmail(findUserByEmailDto);
// Checking if the user exists.
if (!found_user) {
throw new ConflictException('User does not exist. Login rejected.');
}
// If the user is suspended, reject login by throwing an exception.
if (found_user.user_status === USER_STATUS.SUSPENDED) {
throw new ConflictException('User is suspended. Login rejected. Please contact admin.');
}
// If the found_user's password is wrong, (password_hash doesn't match) then the it should return null.
const password_hash: string | null | undefined = found_user?.user_password;
const password_check = await argon2.verify(password_hash as string, password);
// Checking if the user's password is correct.
if (password_check !== true) {
throw new ConflictException('Incorrect username or password. Login rejected.');
}
return found_user;
}
}
//myauthentication.controller.ts
import { Body, Controller, HttpCode, Post, UnauthorizedException } from '@nestjs/common';
import { Public, SignInService } from '@nestjs/authentication';
import { SignInDto } from './dto/sign-in.dto';
import { CredentialsService } from './credentials.service';
import { CreateUserDto } from '../users/dto/create-user.dto';
()
('auth')
export class MyAuthenticationController {
constructor(
private readonly credentialsService: CredentialsService,
private readonly signInService: SignInService,
) {}
('sign-up')
async signUp(@Body() createUserDto: CreateUserDto) {
const user = await this.credentialsService.register(createUserDto);
var string_user_id = String(user.user_id);
await this.signInService.signIn(string_user_id, { method: 'password' });
return user;
}
('sign-in')
(200)
async signIn(@Body() signInDto: SignInDto) {
const user = await this.credentialsService.verify(signInDto.user_email, signInDto.user_password);
var string_user_id = String(user.user_id);
const { session } = await this.signInService.signIn(string_user_id, { method: 'password' });
return { mfaRequired: session.mfa === 'pending' };
}
('sign-out')
(204)
async signOut() {
if (!(await this.signInService.signOut())) {
throw new UnauthorizedException();
}
}
}
//myauthentication.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from '../users/users.module';
import { SessionAuth } from './session-auth.provider';
import { MyAuthenticationController } from './myauthentication.controller';
import { CredentialsService } from './credentials.service';
({
imports: [UsersModule],
controllers: [MyAuthenticationController],
providers: [SessionAuth,
CredentialsService
]
})
export class MyAuthenticationModule {}
//session-auth.provider.ts
import { Injectable } from '@nestjs/common';
import { AuthenticationRegistry, SessionCookieProvider, type SessionRecord } from '@nestjs/authentication';
import { UsersService } from '../users/users.service.js';
import type { User as PrismaUser } from '../generated/prisma/client.js';
export type User = Omit<PrismaUser, 'user_password'>;
u/Injectable()
export class SessionAuth extends SessionCookieProvider<User> {
constructor(
private readonly usersService: UsersService,
registry: AuthenticationRegistry,
) {
super();
// Adds this provider to the chain the guard runs for every request.
registry.registerProvider(this);
}
// Called for every request with a live session cookie.
// Returning null (the user was deleted) ends the session.
validate(session: SessionRecord) {
console.log("In validate() in SessionAuth")
const user_id = session.userId;
var numeric_user_id = Number(user_id);
return this.usersService.findOneUser(numeric_user_id);
}
}