mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-15 17:21:35 +10:00
🚀 release v3.0.0
This commit is contained in:
66
server/src/auth/auth.controller.ts
Normal file
66
server/src/auth/auth.controller.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, Post, UseGuards } from '@nestjs/common';
|
||||
|
||||
import { User } from '@/decorators/user.decorator';
|
||||
import { User as UserEntity } from '@/users/entities/user.entity';
|
||||
|
||||
import { AuthService } from './auth.service';
|
||||
import { ForgotPasswordDto } from './dto/forgot-password.dto';
|
||||
import { RegisterDto } from './dto/register.dto';
|
||||
import { ResetPasswordDto } from './dto/reset-password.dto';
|
||||
import { JwtAuthGuard } from './guards/jwt.guard';
|
||||
import { LocalAuthGuard } from './guards/local.guard';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get()
|
||||
authenticate(@User() user: UserEntity) {
|
||||
return user;
|
||||
}
|
||||
|
||||
@Post('google')
|
||||
async loginWithGoogle(@Body('accessToken') googleAccessToken: string) {
|
||||
const user = await this.authService.authenticateWithGoogle(googleAccessToken);
|
||||
const accessToken = this.authService.getAccessToken(user.id);
|
||||
|
||||
return { user, accessToken };
|
||||
}
|
||||
|
||||
@Post('register')
|
||||
async register(@Body() registerDto: RegisterDto) {
|
||||
const user = await this.authService.register(registerDto);
|
||||
const accessToken = this.authService.getAccessToken(user.id);
|
||||
|
||||
return { user, accessToken };
|
||||
}
|
||||
|
||||
@HttpCode(200)
|
||||
@UseGuards(LocalAuthGuard)
|
||||
@Post('login')
|
||||
async login(@User() user: UserEntity) {
|
||||
const accessToken = this.authService.getAccessToken(user.id);
|
||||
|
||||
return { user, accessToken };
|
||||
}
|
||||
|
||||
@HttpCode(200)
|
||||
@Post('forgot-password')
|
||||
forgotPassword(@Body() forgotPasswordDto: ForgotPasswordDto) {
|
||||
return this.authService.forgotPassword(forgotPasswordDto.email);
|
||||
}
|
||||
|
||||
@HttpCode(200)
|
||||
@Post('reset-password')
|
||||
resetPassword(@Body() resetPasswordDto: ResetPasswordDto) {
|
||||
return this.authService.resetPassword(resetPasswordDto);
|
||||
}
|
||||
|
||||
@HttpCode(200)
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Delete()
|
||||
async remove(@User('id') id: number) {
|
||||
await this.authService.removeUser(id);
|
||||
}
|
||||
}
|
||||
33
server/src/auth/auth.module.ts
Normal file
33
server/src/auth/auth.module.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
|
||||
import { UsersModule } from '@/users/users.module';
|
||||
|
||||
import { AuthController } from './auth.controller';
|
||||
import { AuthService } from './auth.service';
|
||||
import { JwtStrategy } from './strategy/jwt.strategy';
|
||||
import { LocalStrategy } from './strategy/local.strategy';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule,
|
||||
UsersModule,
|
||||
PassportModule,
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
secret: configService.get<string>('auth.jwtSecret'),
|
||||
signOptions: {
|
||||
expiresIn: `${configService.get<number>('auth.jwtExpiryTime')}s`,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
],
|
||||
providers: [AuthService, LocalStrategy, JwtStrategy],
|
||||
controllers: [AuthController],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
144
server/src/auth/auth.service.ts
Normal file
144
server/src/auth/auth.service.ts
Normal file
@ -0,0 +1,144 @@
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { SchedulerRegistry } from '@nestjs/schedule';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { google } from 'googleapis';
|
||||
|
||||
import { PostgresErrorCode } from '@/database/errorCodes.enum';
|
||||
import { CreateGoogleUserDto } from '@/users/dto/create-google-user.dto';
|
||||
import { User } from '@/users/entities/user.entity';
|
||||
import { UsersService } from '@/users/users.service';
|
||||
|
||||
import { RegisterDto } from './dto/register.dto';
|
||||
import { ResetPasswordDto } from './dto/reset-password.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private schedulerRegistry: SchedulerRegistry,
|
||||
private configService: ConfigService,
|
||||
private usersService: UsersService,
|
||||
private jwtService: JwtService
|
||||
) {}
|
||||
|
||||
async register(registerDto: RegisterDto) {
|
||||
const hashedPassword = await bcrypt.hash(registerDto.password, 10);
|
||||
|
||||
try {
|
||||
const createdUser = await this.usersService.create({
|
||||
...registerDto,
|
||||
password: hashedPassword,
|
||||
provider: 'email',
|
||||
});
|
||||
|
||||
return createdUser;
|
||||
} catch (error: any) {
|
||||
if (error?.code === PostgresErrorCode.UniqueViolation) {
|
||||
throw new HttpException('A user with that username and/or email already exists.', HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
throw new HttpException(
|
||||
'Something went wrong. Please try again later, or raise an issue on GitHub if the problem persists.',
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getUser(identifier: string, password: string) {
|
||||
try {
|
||||
const user = await this.usersService.findByIdentifier(identifier);
|
||||
|
||||
await this.verifyPassword(password, user.password);
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
throw new HttpException(
|
||||
'The username/email and password combination provided was incorrect.',
|
||||
HttpStatus.UNAUTHORIZED
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async verifyPassword(password: string, hashedPassword: string) {
|
||||
const isPasswordMatching = await bcrypt.compare(password, hashedPassword);
|
||||
|
||||
if (!isPasswordMatching) {
|
||||
throw new HttpException(
|
||||
'The username/email and password combination provided was incorrect.',
|
||||
HttpStatus.UNAUTHORIZED
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
forgotPassword(email: string) {
|
||||
return this.usersService.generateResetToken(email);
|
||||
}
|
||||
|
||||
async resetPassword(resetPasswordDto: ResetPasswordDto) {
|
||||
const user = await this.usersService.findByResetToken(resetPasswordDto.resetToken);
|
||||
const hashedPassword = await bcrypt.hash(resetPasswordDto.password, 10);
|
||||
|
||||
await this.usersService.update(user.id, {
|
||||
password: hashedPassword,
|
||||
resetToken: null,
|
||||
});
|
||||
|
||||
try {
|
||||
this.schedulerRegistry.deleteTimeout(`clear-resetToken-${user.id}`);
|
||||
} catch {
|
||||
// pass through
|
||||
}
|
||||
}
|
||||
|
||||
removeUser(id: number) {
|
||||
return this.usersService.remove(id);
|
||||
}
|
||||
|
||||
getAccessToken(id: number) {
|
||||
const expiresIn = this.configService.get<number>('auth.jwtExpiryTime');
|
||||
|
||||
return this.jwtService.sign({ id }, { expiresIn });
|
||||
}
|
||||
|
||||
getUserFromAccessToken(accessToken: string) {
|
||||
const payload: User = this.jwtService.verify(accessToken, {
|
||||
secret: this.configService.get<string>('auth.jwtSecret'),
|
||||
});
|
||||
|
||||
return this.usersService.findById(payload.id);
|
||||
}
|
||||
|
||||
async authenticateWithGoogle(googleAccessToken: string) {
|
||||
const clientID = this.configService.get<string>('google.clientID');
|
||||
const clientSecret = this.configService.get<string>('google.clientSecret');
|
||||
|
||||
const OAuthClient = new google.auth.OAuth2(clientID, clientSecret);
|
||||
OAuthClient.setCredentials({ access_token: googleAccessToken });
|
||||
|
||||
const { email } = await OAuthClient.getTokenInfo(googleAccessToken);
|
||||
|
||||
try {
|
||||
const user = await this.usersService.findByEmail(email);
|
||||
|
||||
return user;
|
||||
} catch (error: any) {
|
||||
if (error.status !== HttpStatus.NOT_FOUND) {
|
||||
throw new Error('Something went wrong, please try again later.');
|
||||
}
|
||||
|
||||
const UserInfoClient = google.oauth2('v2').userinfo;
|
||||
const { data } = await UserInfoClient.get({ auth: OAuthClient });
|
||||
const username = data.email.split('@').at(0);
|
||||
|
||||
const createUserDto: CreateGoogleUserDto = {
|
||||
name: `${data.given_name} ${data.family_name}`,
|
||||
username,
|
||||
email: data.email,
|
||||
provider: 'google',
|
||||
};
|
||||
|
||||
return this.usersService.create(createUserDto);
|
||||
}
|
||||
}
|
||||
}
|
||||
7
server/src/auth/dto/forgot-password.dto.ts
Normal file
7
server/src/auth/dto/forgot-password.dto.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class ForgotPasswordDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
email: string;
|
||||
}
|
||||
10
server/src/auth/dto/login.dto.ts
Normal file
10
server/src/auth/dto/login.dto.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { IsNotEmpty, IsString, MinLength } from 'class-validator';
|
||||
|
||||
export class LoginDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
identifier: string;
|
||||
|
||||
@MinLength(6)
|
||||
password: string;
|
||||
}
|
||||
18
server/src/auth/dto/register.dto.ts
Normal file
18
server/src/auth/dto/register.dto.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
|
||||
|
||||
export class RegisterDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
username: string;
|
||||
|
||||
@IsEmail()
|
||||
@IsNotEmpty()
|
||||
email: string;
|
||||
|
||||
@MinLength(6)
|
||||
password: string;
|
||||
}
|
||||
11
server/src/auth/dto/reset-password.dto.ts
Normal file
11
server/src/auth/dto/reset-password.dto.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { IsNotEmpty, IsString, MinLength } from 'class-validator';
|
||||
|
||||
export class ResetPasswordDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
resetToken: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(6)
|
||||
password: string;
|
||||
}
|
||||
5
server/src/auth/guards/jwt.guard.ts
Normal file
5
server/src/auth/guards/jwt.guard.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') {}
|
||||
5
server/src/auth/guards/local.guard.ts
Normal file
5
server/src/auth/guards/local.guard.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export class LocalAuthGuard extends AuthGuard('local') {}
|
||||
11
server/src/auth/guards/optional-jwt.guard.ts
Normal file
11
server/src/auth/guards/optional-jwt.guard.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
import { User } from '@/users/entities/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class OptionalJwtAuthGuard extends AuthGuard('jwt') {
|
||||
handleRequest<TUser = User>(err: Error, user: TUser): TUser {
|
||||
return user;
|
||||
}
|
||||
}
|
||||
22
server/src/auth/strategy/jwt.strategy.ts
Normal file
22
server/src/auth/strategy/jwt.strategy.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
|
||||
import { User } from '@/users/entities/user.entity';
|
||||
import { UsersService } from '@/users/users.service';
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(configService: ConfigService, private readonly usersService: UsersService) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
secretOrKey: configService.get<string>('auth.jwtSecret'),
|
||||
ignoreExpiration: false,
|
||||
});
|
||||
}
|
||||
|
||||
validate({ id }: User): Promise<User> {
|
||||
return this.usersService.findById(id);
|
||||
}
|
||||
}
|
||||
18
server/src/auth/strategy/local.strategy.ts
Normal file
18
server/src/auth/strategy/local.strategy.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Strategy } from 'passport-local';
|
||||
|
||||
import { User } from '@/users/entities/user.entity';
|
||||
|
||||
import { AuthService } from '../auth.service';
|
||||
|
||||
@Injectable()
|
||||
export class LocalStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(private authService: AuthService) {
|
||||
super({ usernameField: 'identifier' });
|
||||
}
|
||||
|
||||
async validate(identifier: string, password: string): Promise<User> {
|
||||
return this.authService.getUser(identifier, password);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user