auth implemented

This commit is contained in:
dimitar 2024-12-09 04:10:04 +01:00
parent ce501e6450
commit 96e8628dad
35 changed files with 1496 additions and 25 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
/home/echo/dev/nestjs_boilerplate/server/node_modules
/server/node_modules
/server/dist

3
server/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
# Keep environment variables out of version control
.env

922
server/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -21,8 +21,19 @@
},
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.3.0",
"@nestjs/core": "^10.0.0",
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@prisma/client": "^6.0.1",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"pg": "^8.13.1",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1"
},
@ -30,9 +41,12 @@
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/bcrypt": "^5.0.2",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
@ -41,6 +55,7 @@
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.5.0",
"prettier": "^3.0.0",
"prisma": "^6.0.1",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.1.0",

View File

@ -0,0 +1,19 @@
-- CreateEnum
CREATE TYPE "Role" AS ENUM ('USER', 'ADMIN');
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"password" TEXT NOT NULL,
"firstName" TEXT,
"lastName" TEXT,
"role" "Role" NOT NULL DEFAULT 'USER',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");

View File

@ -0,0 +1,9 @@
/*
Warnings:
- You are about to drop the column `firstName` on the `users` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "users" DROP COLUMN "firstName",
ADD COLUMN "name" TEXT;

View File

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

View File

@ -0,0 +1,32 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid())
email String @unique
password String
name String?
lastName String?
role Role @default(USER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("users")
}
enum Role {
USER
ADMIN
}

View File

@ -1,10 +1,24 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import { jwtConfig } from './config/configuration';
import { AuthModule } from './modules/auth/auth.module';
import { PrismaService } from './prisma/prisma.service';
import { PrismaModule } from './prisma/prisma.module';
import { UsersModule } from './modules/users/users.module';
@Module({
imports: [],
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [jwtConfig],
}),
AuthModule,
PrismaModule,
UsersModule,
],
controllers: [AppController],
providers: [AppService],
providers: [AppService, PrismaService],
})
export class AppModule {}

View File

@ -3,6 +3,6 @@ import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
return 'up and running ';
}
}

View File

@ -0,0 +1,4 @@
export enum Role {
USER = 'USER',
ADMIN = 'ADMIN',
}

View File

@ -0,0 +1,8 @@
import { registerAs } from '@nestjs/config';
export const jwtConfig = registerAs('jwt', () => ({
secret: process.env.JWT_SECRET || 'secret',
expiresIn: process.env.JWT_EXPIRES_IN || '1d',
}));
export const PORT = process.env.PORT || 3000;

View File

@ -0,0 +1,33 @@
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
} from '@nestjs/common';
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message =
exception instanceof HttpException
? exception.getResponse()
: 'Internal server error';
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message,
});
}
}

View File

@ -1,8 +1,22 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ConfigService } from '@nestjs/config';
import { GlobalExceptionFilter } from './core/filters/http-exception.filters';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT ?? 3000);
app.setGlobalPrefix('api');
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
}),
);
app.useGlobalFilters(new GlobalExceptionFilter());
app.enableCors();
const configService = app.get(ConfigService);
await app.listen(configService.get('PORT') || 3000);
}
bootstrap();

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthController } from './auth.controller';
describe('AuthController', () => {
let controller: AuthController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
}).compile();
controller = module.get<AuthController>(AuthController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,19 @@
import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('register')
async register(@Body() registerDto: RegisterDto) {
return this.authService.register(registerDto);
}
@Post('login')
async login(@Body() loginDto: LoginDto) {
return this.authService.login(loginDto);
}
}

View File

@ -0,0 +1,27 @@
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
// import { LocalStrategy } from './local.strategy';
import { JwtStrategy } from './strategies/jwt.strategy';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
UsersModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get('jwt.secret'),
signOptions: { expiresIn: configService.get('jwt.expiresIn') },
}),
inject: [ConfigService],
}),
],
providers: [AuthService, JwtStrategy],
controllers: [AuthController],
})
export class AuthModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';
describe('AuthService', () => {
let service: AuthService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AuthService],
}).compile();
service = module.get<AuthService>(AuthService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,59 @@
import {
Injectable,
UnauthorizedException,
ConflictException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
import * as bcrypt from 'bcrypt';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
import { CreateUserDto } from '../users/dtos/create-user.dto';
@Injectable()
export class AuthService {
constructor(
private readonly jwtService: JwtService,
private readonly usersService: UsersService,
) {}
async register(createUserDto: CreateUserDto) {
// Check if user already exists
const existingUser = await this.usersService.findByEmail(
createUserDto.email,
);
if (existingUser) {
throw new ConflictException('User already exists');
}
// Create new user
const user = await this.usersService.createUser(createUserDto);
// Generate JWT token
const payload = { email: user.email, sub: user.id, role: user.role };
return {
user,
access_token: this.jwtService.sign(payload),
};
}
async validateUser(email: string, password: string) {
const user = await this.usersService.findByEmail(email);
if (!user) {
throw new UnauthorizedException('Invalid email or password');
}
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
throw new UnauthorizedException('Invalid email or password');
}
return user;
}
async login(loginDto: LoginDto) {
const payload = { email: loginDto.email, password: loginDto.password };
return {
access_token: this.jwtService.sign(payload),
};
}
}

View File

@ -0,0 +1,5 @@
import { SetMetadata } from '@nestjs/common';
import { Role } from '../../../common/enums/role.enum';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);

View File

@ -0,0 +1,11 @@
import { IsString, IsNotEmpty } from 'class-validator';
export class LoginDto {
@IsString()
@IsNotEmpty()
email: string;
@IsString()
@IsNotEmpty()
password: string;
}

View File

@ -0,0 +1,18 @@
import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator';
export class RegisterDto {
@IsEmail()
email: string;
@IsString()
@MinLength(6)
password: string;
@IsString()
@IsOptional()
name?: string;
@IsString()
@IsOptional()
lastName?: string;
}

View File

@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

View File

@ -0,0 +1,20 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Role } from '../../../common/enums/role.enum';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>('roles', [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.role === role);
}
}

View File

@ -0,0 +1,19 @@
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get('JWT_SECRET'),
});
}
async validate(payload: any) {
return { id: payload.sub, email: payload.email, role: payload.role };
}
}

View File

@ -0,0 +1,30 @@
import {
IsEmail,
IsString,
MinLength,
IsOptional,
IsEnum,
} from 'class-validator';
import { Role } from 'src/common/enums/role.enum';
export class CreateUserDto {
@IsString()
@IsEmail()
email: string;
@IsString()
@MinLength(6)
password: string;
@IsString()
@IsOptional()
name?: string;
@IsString()
@IsOptional()
lastName?: string;
@IsEnum(Role)
@IsOptional()
role?: Role;
}

View File

@ -0,0 +1 @@
export const UpdateUserDto = class UpdateUserDto {};

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
describe('UsersController', () => {
let controller: UsersController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
}).compile();
controller = module.get<UsersController>(UsersController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,56 @@
import {
Controller,
Get,
Post,
Body,
Param,
UseGuards,
Delete,
Patch,
} from '@nestjs/common';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator';
import { Role } from '../../common/enums/role.enum';
import { UsersService } from './users.service';
import { CreateUserDto } from './dtos/create-user.dto';
// import { UpdateUserDto } from './dtos/update-user.dto';
@Controller('users')
// @UseGuards(JwtAuthGuard, RolesGuard)
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
@Roles(Role.ADMIN) // Only admin can create users through this endpoint
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.createUser(createUserDto);
}
@Get()
// @Roles(Role.ADMIN)
findAll() {
return this.usersService.findAll();
}
@Get()
findByEmail(email: string) {
return this.usersService.findByEmail(email);
}
// @Get(':id')
// @Roles(Role.ADMIN)
// findOne(@Param('id') id: string) {
// return this.usersService.findById(id);
// }
// @Patch(':id')
// @Roles(Role.ADMIN)
// update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
// return this.usersService.update(id, updateUserDto);
// }
// @Delete(':id')
// @Roles(Role.ADMIN)
// remove(@Param('id') id: string) {
// return this.usersService.remove(id);
// }
}

View File

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
@Module({
providers: [UsersService],
exports: [UsersService],
controllers: [UsersController],
})
export class UsersModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
describe('UsersService', () => {
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UsersService],
}).compile();
service = module.get<UsersService>(UsersService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,41 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
// import { User, Prisma } from '@prisma/client';
import * as bcrypt from 'bcrypt';
import { CreateUserDto } from './dtos/create-user.dto';
import { Role } from 'src/common/enums/role.enum';
@Injectable()
export class UsersService {
constructor(private prisma: PrismaService) {}
async createUser(createUserDto: CreateUserDto) {
const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
console.log(hashedPassword);
const user = await this.prisma.user.create({
data: {
...createUserDto,
password: hashedPassword,
},
select: {
id: true,
email: true,
name: true,
lastName: true,
role: true,
},
});
console.log(user);
return user;
}
async findByEmail(email: string) {
return this.prisma.user.findUnique({
where: { email },
});
}
async findAll() {
return this.prisma.user.findMany();
}
}

View File

@ -0,0 +1,9 @@
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PrismaService } from './prisma.service';
describe('PrismaService', () => {
let service: PrismaService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [PrismaService],
}).compile();
service = module.get<PrismaService>(PrismaService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,16 @@
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
}