Skip to content

Commit

Permalink
Feature/auth (#5)
Browse files Browse the repository at this point in the history
* feat(auth): adds auth

* fix(auth): should fix build
  • Loading branch information
OnlyNico43 authored Apr 12, 2024
1 parent 823acd5 commit 98cbca9
Show file tree
Hide file tree
Showing 15 changed files with 898 additions and 814 deletions.
6 changes: 5 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
{
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
"trailingComma": "all",
"semi": true
"printWidth": 120,
"endOfLine": "auto",
"arrowParens": "avoid"
}
1,431 changes: 727 additions & 704 deletions package-lock.json

Large diffs are not rendered by default.

19 changes: 10 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@
"prepare": "husky"
},
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/common": "10.0.0",
"@nestjs/config": "3.2.2",
"@nestjs/core": "10.0.0",
"@nestjs/jwt": "10.2.0",
"@nestjs/mapped-types": "2.0.5",
"@nestjs/platform-express": "^10.3.7",
"@nestjs/platform-express": "10.3.7",
"@prisma/client": "5.11.0",
"@types/bcrypt": "5.0.2",
"@types/express": "4.17.17",
Expand All @@ -32,22 +33,22 @@
"bcrypt": "5.1.1",
"class-transformer": "0.5.1",
"class-validator": "0.14.1",
"cookie-parser": "1.4.6",
"dotenv": "16.4.5",
"eslint": "8.42.0",
"eslint-config-prettier": "9.0.0",
"eslint-plugin-prettier": "5.0.0",
"eslint-plugin-unused-imports": "^3.1.0",
"reflect-metadata": "0.2.0",
"rxjs": "7.8.1",
"eslint-plugin-unused-imports": "3.1.0",
"helmet": "7.1.0",
"joi": "17.12.3",
"source-map-support": "0.5.21",
"ts-loader": "9.4.3",
"ts-node": "10.9.1",
"tsconfig-paths": "4.2.0",
"typescript": "5.1.3"
},
"devDependencies": {
"@nestjs/cli": "10.0.0",
"@nestjs/schematics": "10.0.0",
"@nestjs/testing": "10.0.0",
"@types/cookie-parser": "1.4.7",
"husky": "9.0.11",
"lint-staged": "15.2.2",
"prettier": "3.0.0",
Expand Down
24 changes: 22 additions & 2 deletions src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,28 @@
import { Module } from '@nestjs/common';
import { AuthModule } from './auth/auth.module';
import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';
import { AuthModule } from './common/auth/auth.module';
import { UsersModule } from './user/users.module';

@Module({
imports: [UsersModule, AuthModule],
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
validationSchema: Joi.object({
NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),
DATABASE_URL: Joi.string().required(),
JWT_SECRET: Joi.string().required(),
JWT_EXPIRATION_TIME: Joi.string().required(),
COOKIE_SECRET: Joi.string().required(),
}),
validationOptions: {
allowUnknown: true,
},
cache: true,
}),
UsersModule,
AuthModule,
],
})
export class AppModule {}
18 changes: 0 additions & 18 deletions src/auth/auth.controller.ts

This file was deleted.

22 changes: 22 additions & 0 deletions src/common/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Body, Controller, Post, Res } from '@nestjs/common';
import { Response } from 'express';
import { AuthService } from './auth.service';
import { Public } from './public.decorator';

@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}

@Public()
@Post('login')
async login(
@Res({ passthrough: true }) response: Response,
@Body() request: { email: string; password: string },
): Promise<void> {
response.cookie('access_token', await this.authService.login(request.email, request.password), {
sameSite: 'lax',
path: '/',
signed: true,
});
}
}
40 changes: 40 additions & 0 deletions src/common/auth/auth.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Reflector } from '@nestjs/core';
import { JwtService } from '@nestjs/jwt';
import { ErrorCodes } from 'enums/error-codes.enum';
import { Request } from 'express';

@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private jwtService: JwtService,
private reflector: Reflector,
private configService: ConfigService,
) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [context.getHandler(), context.getClass()]);
if (isPublic) {
return true;
}
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException({ error: ErrorCodes.NOT_ALLOWED });
}
try {
const payload = await this.jwtService.verifyAsync<{ id: string; email: string }>(token, {
secret: this.configService.get('JWT_SECRET'),
});
request['userId'] = payload.id;
} catch {
throw new UnauthorizedException({ error: ErrorCodes.NOT_ALLOWED });
}
return true;
}

private extractTokenFromHeader(request: Request): string | undefined {
return request.signedCookies.access_token;
}
}
18 changes: 13 additions & 5 deletions src/auth/auth.module.ts → src/common/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
import { Logger, Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { JWT_SECRET } from '../constants/constants';
import { UsersModule } from '../user/users.module';
import { UsersModule } from 'src/user/users.module';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { AuthGuard } from './auth.guard';
import { APP_GUARD } from '@nestjs/core';

@Module({
imports: [
UsersModule,
JwtModule.register({
global: true,
secret: JWT_SECRET,
signOptions: { expiresIn: '8h' },
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: process.env.JWT_EXPIRATION_TIME },
}),
],
controllers: [AuthController],
providers: [AuthService, Logger],
providers: [
AuthService,
Logger,
{
provide: APP_GUARD,
useClass: AuthGuard,
},
],
exports: [AuthService],
})
export class AuthModule {}
25 changes: 8 additions & 17 deletions src/auth/auth.service.ts → src/common/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,34 @@
import {
Injectable,
InternalServerErrorException,
Logger,
} from '@nestjs/common';
import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { ErrorCodes } from 'enums/error-codes.enum';
import { UsersService } from '../user/users.service';
import { UsersService } from 'src/user/users.service';

@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtservice: JwtService,
private jwtService: JwtService,
) {}

private readonly logger = new Logger(AuthService.name);

async login(email: string, pass: string): Promise<{ accessToken: string }> {
async login(email: string, pass: string): Promise<string> {
this.logger.log(`Attempting login for user with email: ${email}`);
const user = await this.usersService.findUserByEmail(email);
if (!user) {
this.logger.error(
`Login for user with email: ${email} failed, not found in database`,
);
this.logger.error(`Login for user with email: ${email} failed, not found in database`);
throw new InternalServerErrorException({
error: ErrorCodes.WRONG_CREDENTIALS,
});
}
if (!(await bcrypt.compare(pass, user.password))) {
this.logger.error(
`Login for user with email: ${email} failed, invalid password`,
);
this.logger.error(`Login for user with email: ${email} failed, invalid password`);
throw new InternalServerErrorException({
error: ErrorCodes.WRONG_CREDENTIALS,
});
}
const payload = { sub: user.id, email: user.email };
const accessToken = await this.jwtservice.signAsync(payload);
return { accessToken };
const payload = { id: user.id, email: user.email };
return this.jwtService.signAsync(payload);
}
}
13 changes: 13 additions & 0 deletions src/common/auth/current-user-id.decrator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { Request } from 'express';

/**
* Decorator to get the current user id from the request Only work on protected routes
* @param jwtService has to be explicitly passed in because it is not injectable
* @param ctx gets set by NestJS
* @returns the user id of the user that made the request
*/
export const CurrentUserId = createParamDecorator((data: string, ctx: ExecutionContext) => {
const req: Request = ctx.switchToHttp().getRequest();
return req['userId'];
});
4 changes: 4 additions & 0 deletions src/common/auth/public.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { CustomDecorator, SetMetadata } from '@nestjs/common';

export const IS_PUBLIC_KEY = 'isPublic';
export const Public = (): CustomDecorator<string> => SetMetadata(IS_PUBLIC_KEY, true);
2 changes: 0 additions & 2 deletions src/constants/constants.ts

This file was deleted.

15 changes: 13 additions & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import { ValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { NestFactory } from '@nestjs/core';
import * as cookieParser from 'cookie-parser';
import helmet from 'helmet';
import { AppModule } from './app.module';

async function bootstrap() {
async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService);

app.use(helmet());

app.use(cookieParser(configService.get('COOKIE_SECRET')));

app.enableCors({ credentials: true, origin: true });

app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
Expand All @@ -12,4 +23,4 @@ async function bootstrap() {
);
await app.listen(4000);
}
bootstrap();
void bootstrap();
32 changes: 9 additions & 23 deletions src/user/users.controller.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,24 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
} from '@nestjs/common';
import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { CurrentUserId } from 'src/common/auth/current-user-id.decrator';
import { Public } from 'src/common/auth/public.decorator';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { UsersService } from './users.service';

//TODO: change to user
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
constructor(private usersService: UsersService) {}

@Public()
@Post('signup')
create(@Body() createUserDto: CreateUserDto): Promise<
Prisma.UserGetPayload<{
select: {
id: true;
createdAt: true;
updatedAt: true;
email: true;
password: false;
};
}>
> {
create(@Body() createUserDto: CreateUserDto): Promise<void> {
return this.usersService.createUser(createUserDto);
}

@Get(':id')
findOne(@Param('id') id: string): Promise<
@Get()
findOne(@CurrentUserId() id: string): Promise<
Prisma.UserGetPayload<{
select: {
id: true;
Expand Down
Loading

0 comments on commit 98cbca9

Please sign in to comment.