chore: refacto NestJS in modules (#308)

* chore: wip refacto in modules

* fix: rollback port

* fix: jwt guard in wrong folder

* chore: rename folder exception-filter in filters

* fix: tests are running

* fix: excessive stack depth comparing types

* fix: auth issue

* chore: move createUser in UserService

* fix: test

* fix: guards

* fix: jwt guard don't handle falsy user
This commit is contained in:
Jérémy M
2023-06-16 10:38:11 +02:00
committed by GitHub
parent 5921c7f11d
commit 2cd081234f
1084 changed files with 2251 additions and 758 deletions

View File

@ -0,0 +1,31 @@
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
import { AuthService } from './services/auth.service';
import { GoogleAuthController } from './google.auth.controller';
import { GoogleStrategy } from './strategies/google.auth.strategy';
import { TokenController } from './token.controller';
import { PrismaService } from 'src/database/prisma.service';
import { UserModule } from '../user/user.module';
const jwtModule = JwtModule.registerAsync({
useFactory: async (configService: ConfigService) => {
return {
secret: configService.get<string>('JWT_SECRET'),
signOptions: {
expiresIn: configService.get<string>('JWT_EXPIRES_IN') + 's',
},
};
},
imports: [ConfigModule.forRoot({})],
inject: [ConfigService],
});
@Module({
imports: [jwtModule, ConfigModule.forRoot({}), UserModule],
controllers: [GoogleAuthController, TokenController],
providers: [AuthService, JwtAuthStrategy, GoogleStrategy, PrismaService],
exports: [jwtModule],
})
export class AuthModule {}

View File

@ -0,0 +1,46 @@
import {
Controller,
Get,
HttpException,
HttpStatus,
Req,
Res,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Response } from 'express';
import { AuthService } from './services/auth.service';
import { GoogleRequest } from './strategies/google.auth.strategy';
import { UserService } from '../user/user.service';
@Controller('auth/google')
export class GoogleAuthController {
constructor(
private readonly authService: AuthService,
private readonly userService: UserService,
) {}
@Get()
@UseGuards(AuthGuard('google'))
async googleAuth() {
// As this method is protected by Google Auth guard, it will trigger Google SSO flow
return;
}
@Get('redirect')
@UseGuards(AuthGuard('google'))
async googleAuthRedirect(@Req() req: GoogleRequest, @Res() res: Response) {
const user = await this.userService.createUser(req.user);
if (!user) {
throw new HttpException(
{ reason: 'User email domain does not match an existing workspace' },
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
const refreshToken = await this.authService.registerRefreshToken(user);
return res.redirect(
this.authService.computeRedirectURI(refreshToken.refreshToken),
);
}
}

View File

@ -0,0 +1,36 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';
import { PrismaService } from 'src/database/prisma.service';
import { prismaMock } from 'src/prisma-mock/jest-prisma-singleton';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
describe('AuthService', () => {
let service: AuthService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AuthService,
{
provide: JwtService,
useValue: {},
},
{
provide: ConfigService,
useValue: {},
},
{
provide: PrismaService,
useValue: prismaMock,
},
],
}).compile();
service = module.get<AuthService>(AuthService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,85 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { JwtPayload } from '../strategies/jwt.auth.strategy';
import { ConfigService } from '@nestjs/config';
import { v4 } from 'uuid';
import { RefreshToken, User } from '@prisma/client';
import { PrismaService } from 'src/database/prisma.service';
export type UserPayload = {
firstName: string;
lastName: string;
email: string;
};
@Injectable()
export class AuthService {
constructor(
private jwtService: JwtService,
private configService: ConfigService,
private prismaService: PrismaService,
) {}
async generateAccessToken(refreshToken: string): Promise<string | undefined> {
const refreshTokenObject = await this.prismaService.refreshToken.findFirst({
where: { refreshToken: refreshToken },
});
if (!refreshTokenObject) {
throw new HttpException(
{ reason: 'Invalid Refresh token' },
HttpStatus.FORBIDDEN,
);
}
const user = await this.prismaService.user.findUnique({
where: { id: refreshTokenObject.userId },
});
if (!user) {
throw new HttpException(
{ reason: 'Refresh token is not associated to a valid user' },
HttpStatus.FORBIDDEN,
);
}
const workspace = await this.prismaService.workspace.findFirst({
where: { workspaceMember: { some: { userId: user.id } } },
});
if (!workspace) {
throw new HttpException(
{ reason: 'Refresh token is not associated to a valid workspace' },
HttpStatus.FORBIDDEN,
);
}
const payload: JwtPayload = {
userId: user.id,
workspaceId: workspace.id,
};
return this.jwtService.sign(payload);
}
async registerRefreshToken(user: User): Promise<RefreshToken> {
const refreshToken = await this.prismaService.refreshToken.upsert({
where: {
id: user.id,
},
create: {
id: v4(),
userId: user.id,
refreshToken: v4(),
},
update: {},
});
return refreshToken;
}
computeRedirectURI(refreshToken: string): string {
return `${this.configService.get<string>(
'FRONT_AUTH_CALLBACK_URL',
)}?refreshToken=${refreshToken}`;
}
}

View File

@ -0,0 +1,40 @@
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, VerifyCallback } from 'passport-google-oauth20';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Request } from 'express';
export type GoogleRequest = Request & {
user: { firstName: string; lastName: string; email: string };
};
@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
constructor(configService: ConfigService) {
super({
clientID: configService.get<string>('AUTH_GOOGLE_CLIENT_ID'),
clientSecret: configService.get<string>('AUTH_GOOGLE_CLIENT_SECRET'),
callbackURL: configService.get<string>('AUTH_GOOGLE_CALLBACK_URL'),
scope: ['email', 'profile'],
});
}
async validate(
accessToken: string,
refreshToken: string,
profile: any,
done: VerifyCallback,
): Promise<any> {
const { name, emails, photos } = profile;
const user = {
email: emails[0].value,
firstName: name.givenName,
lastName: name.familyName,
picture: photos[0].value,
refreshToken,
accessToken,
};
done(null, user);
}
}

View File

@ -0,0 +1,43 @@
import { Strategy, ExtractJwt } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PrismaService } from 'src/database/prisma.service';
import { User, Workspace } from '@prisma/client';
export type JwtPayload = { userId: string; workspaceId: string };
export type PassportUser = { user: User; workspace: Workspace };
@Injectable()
export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(
private readonly configService: ConfigService,
private readonly prismaService: PrismaService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET'),
});
}
async validate(payload: JwtPayload): Promise<PassportUser> {
const user = await this.prismaService.user.findUniqueOrThrow({
where: { id: payload.userId },
});
if (!user) {
throw new UnauthorizedException();
}
const workspace = await this.prismaService.workspace.findUniqueOrThrow({
where: { id: payload.workspaceId },
});
if (!workspace) {
throw new UnauthorizedException();
}
return { user, workspace };
}
}

View File

@ -0,0 +1,20 @@
import { Controller, Post, Req, Res } from '@nestjs/common';
import { Request, Response } from 'express';
import { AuthService } from './services/auth.service';
@Controller('auth/token')
export class TokenController {
constructor(private authService: AuthService) {}
@Post()
async generateAccessToken(@Req() req: Request, @Res() res: Response) {
const refreshToken = req.body.refreshToken;
if (!refreshToken) {
return res.status(400).send('Refresh token not found');
}
const token = await this.authService.generateAccessToken(refreshToken);
return res.send({ accessToken: token });
}
}