mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-18 10:41:56 +10:00
release: v4.1.0
This commit is contained in:
@ -11,6 +11,7 @@ import {
|
||||
Res,
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { ApiTags } from "@nestjs/swagger";
|
||||
import {
|
||||
authResponseSchema,
|
||||
@ -29,7 +30,6 @@ import { ErrorMessage } from "@reactive-resume/utils";
|
||||
import type { Response } from "express";
|
||||
|
||||
import { User } from "../user/decorators/user.decorator";
|
||||
import { UtilsService } from "../utils/utils.service";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { GitHubGuard } from "./guards/github.guard";
|
||||
import { GoogleGuard } from "./guards/google.guard";
|
||||
@ -45,7 +45,7 @@ import { payloadSchema } from "./utils/payload";
|
||||
export class AuthController {
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
private readonly utils: UtilsService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
private async exchangeToken(id: string, email: string, isTwoFactorAuth = false) {
|
||||
@ -72,7 +72,8 @@ export class AuthController {
|
||||
) {
|
||||
let status = "authenticated";
|
||||
|
||||
const redirectUrl = new URL(`${this.utils.getUrl()}/auth/callback`);
|
||||
const baseUrl = this.configService.get("PUBLIC_URL");
|
||||
const redirectUrl = new URL(`${baseUrl}/auth/callback`);
|
||||
|
||||
const { accessToken, refreshToken } = await this.exchangeToken(
|
||||
user.id,
|
||||
@ -252,7 +253,7 @@ export class AuthController {
|
||||
async forgotPassword(@Body() { email }: ForgotPasswordDto) {
|
||||
try {
|
||||
await this.authService.forgotPassword(email);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// pass
|
||||
}
|
||||
|
||||
@ -270,7 +271,7 @@ export class AuthController {
|
||||
await this.authService.resetPassword(token, password);
|
||||
|
||||
return { message: "Your password has been successfully reset." };
|
||||
} catch (error) {
|
||||
} catch {
|
||||
throw new BadRequestException(ErrorMessage.InvalidResetToken);
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,7 +42,7 @@ export class AuthModule {
|
||||
const callbackURL = configService.getOrThrow("GITHUB_CALLBACK_URL");
|
||||
|
||||
return new GitHubStrategy(clientID, clientSecret, callbackURL, userService);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return new DummyStrategy();
|
||||
}
|
||||
},
|
||||
@ -58,7 +58,7 @@ export class AuthModule {
|
||||
const callbackURL = configService.getOrThrow("GOOGLE_CALLBACK_URL");
|
||||
|
||||
return new GoogleStrategy(clientID, clientSecret, callbackURL, userService);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return new DummyStrategy();
|
||||
}
|
||||
},
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
|
||||
import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
@ -11,13 +13,11 @@ import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
||||
import { AuthProvidersDto, LoginDto, RegisterDto, UserWithSecrets } from "@reactive-resume/dto";
|
||||
import { ErrorMessage } from "@reactive-resume/utils";
|
||||
import * as bcryptjs from "bcryptjs";
|
||||
import { randomBytes } from "crypto";
|
||||
import { authenticator } from "otplib";
|
||||
|
||||
import { Config } from "../config/schema";
|
||||
import { MailService } from "../mail/mail.service";
|
||||
import { UserService } from "../user/user.service";
|
||||
import { UtilsService } from "../utils/utils.service";
|
||||
import { Payload } from "./utils/payload";
|
||||
|
||||
@Injectable()
|
||||
@ -27,7 +27,6 @@ export class AuthService {
|
||||
private readonly userService: UserService,
|
||||
private readonly mailService: MailService,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly utils: UtilsService,
|
||||
) {}
|
||||
|
||||
private hash(password: string): Promise<string> {
|
||||
@ -48,26 +47,26 @@ export class AuthService {
|
||||
|
||||
generateToken(grantType: "access" | "refresh" | "reset" | "verification", payload?: Payload) {
|
||||
switch (grantType) {
|
||||
case "access":
|
||||
case "access": {
|
||||
if (!payload) throw new InternalServerErrorException("InvalidTokenPayload");
|
||||
return this.jwtService.sign(payload, {
|
||||
secret: this.configService.getOrThrow("ACCESS_TOKEN_SECRET"),
|
||||
expiresIn: "15m", // 15 minutes
|
||||
});
|
||||
}
|
||||
|
||||
case "refresh":
|
||||
case "refresh": {
|
||||
if (!payload) throw new InternalServerErrorException("InvalidTokenPayload");
|
||||
return this.jwtService.sign(payload, {
|
||||
secret: this.configService.getOrThrow("REFRESH_TOKEN_SECRET"),
|
||||
expiresIn: "2d", // 2 days
|
||||
});
|
||||
}
|
||||
|
||||
case "reset":
|
||||
case "verification":
|
||||
case "verification": {
|
||||
return randomBytes(32).toString("base64url");
|
||||
|
||||
default:
|
||||
throw new InternalServerErrorException("InvalidGrantType: " + grantType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,7 +107,7 @@ export class AuthService {
|
||||
});
|
||||
|
||||
// Do not `await` this function, otherwise the user will have to wait for the email to be sent before the response is returned
|
||||
this.sendVerificationEmail(user.email);
|
||||
void this.sendVerificationEmail(user.email);
|
||||
|
||||
return user as UserWithSecrets;
|
||||
} catch (error) {
|
||||
@ -123,20 +122,16 @@ export class AuthService {
|
||||
|
||||
async authenticate({ identifier, password }: LoginDto) {
|
||||
try {
|
||||
const user = await this.userService.findOneByIdentifier(identifier);
|
||||
|
||||
if (!user) {
|
||||
throw new BadRequestException(ErrorMessage.InvalidCredentials);
|
||||
}
|
||||
const user = await this.userService.findOneByIdentifierOrThrow(identifier);
|
||||
|
||||
if (!user.secrets?.password) {
|
||||
throw new BadRequestException(ErrorMessage.OAuthUser);
|
||||
}
|
||||
|
||||
await this.validatePassword(password, user.secrets?.password);
|
||||
await this.validatePassword(password, user.secrets.password);
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
throw new BadRequestException(ErrorMessage.InvalidCredentials);
|
||||
}
|
||||
}
|
||||
@ -149,7 +144,8 @@ export class AuthService {
|
||||
secrets: { update: { resetToken: token } },
|
||||
});
|
||||
|
||||
const url = `${this.utils.getUrl()}/auth/reset-password?token=${token}`;
|
||||
const baseUrl = this.configService.get("PUBLIC_URL");
|
||||
const url = `${baseUrl}/auth/reset-password?token=${token}`;
|
||||
const subject = "Reset your Reactive Resume password";
|
||||
const text = `Please click on the link below to reset your password:\n\n${url}`;
|
||||
|
||||
@ -209,7 +205,8 @@ export class AuthService {
|
||||
secrets: { update: { verificationToken: token } },
|
||||
});
|
||||
|
||||
const url = `${this.utils.getUrl()}/auth/verify-email?token=${token}`;
|
||||
const baseUrl = this.configService.get("PUBLIC_URL");
|
||||
const url = `${baseUrl}/auth/verify-email?token=${token}`;
|
||||
const subject = "Verify your email address";
|
||||
const text = `Please verify your email address by clicking on the link below:\n\n${url}`;
|
||||
|
||||
@ -238,7 +235,7 @@ export class AuthService {
|
||||
// Two-Factor Authentication Flows
|
||||
async setup2FASecret(email: string) {
|
||||
// If the user already has 2FA enabled, throw an error
|
||||
const user = await this.userService.findOneByIdentifier(email);
|
||||
const user = await this.userService.findOneByIdentifierOrThrow(email);
|
||||
|
||||
if (user.twoFactorEnabled) {
|
||||
throw new BadRequestException(ErrorMessage.TwoFactorAlreadyEnabled);
|
||||
@ -255,7 +252,7 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async enable2FA(email: string, code: string) {
|
||||
const user = await this.userService.findOneByIdentifier(email);
|
||||
const user = await this.userService.findOneByIdentifierOrThrow(email);
|
||||
|
||||
// If the user already has 2FA enabled, throw an error
|
||||
if (user.twoFactorEnabled) {
|
||||
@ -268,7 +265,7 @@ export class AuthService {
|
||||
}
|
||||
|
||||
const verified = authenticator.verify({
|
||||
secret: user.secrets?.twoFactorSecret,
|
||||
secret: user.secrets.twoFactorSecret,
|
||||
token: code,
|
||||
});
|
||||
|
||||
@ -288,7 +285,7 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async disable2FA(email: string) {
|
||||
const user = await this.userService.findOneByIdentifier(email);
|
||||
const user = await this.userService.findOneByIdentifierOrThrow(email);
|
||||
|
||||
// If the user doesn't have 2FA enabled, throw an error
|
||||
if (!user.twoFactorEnabled) {
|
||||
@ -302,7 +299,7 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async verify2FACode(email: string, code: string) {
|
||||
const user = await this.userService.findOneByIdentifier(email);
|
||||
const user = await this.userService.findOneByIdentifierOrThrow(email);
|
||||
|
||||
// If the user doesn't have 2FA enabled, or does not have a 2FA secret set, throw an error
|
||||
if (!user.twoFactorEnabled || !user.secrets?.twoFactorSecret) {
|
||||
@ -310,7 +307,7 @@ export class AuthService {
|
||||
}
|
||||
|
||||
const verified = authenticator.verify({
|
||||
secret: user.secrets?.twoFactorSecret,
|
||||
secret: user.secrets.twoFactorSecret,
|
||||
token: code,
|
||||
});
|
||||
|
||||
@ -322,21 +319,21 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async useBackup2FACode(email: string, code: string) {
|
||||
const user = await this.userService.findOneByIdentifier(email);
|
||||
const user = await this.userService.findOneByIdentifierOrThrow(email);
|
||||
|
||||
// If the user doesn't have 2FA enabled, or does not have a 2FA secret set, throw an error
|
||||
if (!user.twoFactorEnabled || !user.secrets?.twoFactorSecret) {
|
||||
throw new BadRequestException(ErrorMessage.TwoFactorNotEnabled);
|
||||
}
|
||||
|
||||
const verified = user.secrets?.twoFactorBackupCodes.includes(code);
|
||||
const verified = user.secrets.twoFactorBackupCodes.includes(code);
|
||||
|
||||
if (!verified) {
|
||||
throw new BadRequestException(ErrorMessage.InvalidTwoFactorBackupCode);
|
||||
}
|
||||
|
||||
// Remove the used backup code from the database
|
||||
const backupCodes = user.secrets?.twoFactorBackupCodes.filter((c) => c !== code);
|
||||
const backupCodes = user.secrets.twoFactorBackupCodes.filter((c) => c !== code);
|
||||
await this.userService.updateByEmail(email, {
|
||||
secrets: { update: { twoFactorBackupCodes: backupCodes } },
|
||||
});
|
||||
|
||||
@ -4,10 +4,6 @@ import { Strategy } from "passport";
|
||||
|
||||
@Injectable()
|
||||
export class DummyStrategy extends PassportStrategy(Strategy, "dummy") {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
authenticate() {
|
||||
this.fail();
|
||||
}
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { BadRequestException, Injectable, UnauthorizedException } from "@nestjs/common";
|
||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import { User } from "@prisma/client";
|
||||
import { processUsername } from "@reactive-resume/utils";
|
||||
import { ErrorMessage } from "@reactive-resume/utils";
|
||||
import { ErrorMessage, processUsername } from "@reactive-resume/utils";
|
||||
import { Profile, Strategy, StrategyOptions } from "passport-github2";
|
||||
|
||||
import { UserService } from "@/server/user/user.service";
|
||||
@ -31,17 +30,17 @@ export class GitHubStrategy extends PassportStrategy(Strategy, "github") {
|
||||
|
||||
let user: User | null = null;
|
||||
|
||||
if (!email || !username) throw new BadRequestException();
|
||||
if (!email) throw new BadRequestException();
|
||||
|
||||
try {
|
||||
const user =
|
||||
(await this.userService.findOneByIdentifier(email)) ||
|
||||
(await this.userService.findOneByIdentifier(username));
|
||||
(await this.userService.findOneByIdentifier(email)) ??
|
||||
(username && (await this.userService.findOneByIdentifier(username)));
|
||||
|
||||
if (!user) throw new UnauthorizedException();
|
||||
if (!user) throw new Error("User not found.");
|
||||
|
||||
done(null, user);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
try {
|
||||
user = await this.userService.create({
|
||||
email,
|
||||
@ -55,7 +54,7 @@ export class GitHubStrategy extends PassportStrategy(Strategy, "github") {
|
||||
});
|
||||
|
||||
done(null, user);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
throw new BadRequestException(ErrorMessage.UserAlreadyExists);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { BadRequestException, Injectable, UnauthorizedException } from "@nestjs/common";
|
||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import { User } from "@prisma/client";
|
||||
import { processUsername } from "@reactive-resume/utils";
|
||||
import { ErrorMessage } from "@reactive-resume/utils";
|
||||
import { ErrorMessage, processUsername } from "@reactive-resume/utils";
|
||||
import { Profile, Strategy, StrategyOptions, VerifyCallback } from "passport-google-oauth20";
|
||||
|
||||
import { UserService } from "@/server/user/user.service";
|
||||
@ -34,12 +33,14 @@ export class GoogleStrategy extends PassportStrategy(Strategy, "google") {
|
||||
if (!email) throw new BadRequestException();
|
||||
|
||||
try {
|
||||
const user = await this.userService.findOneByIdentifier(email);
|
||||
const user =
|
||||
(await this.userService.findOneByIdentifier(email)) ??
|
||||
(username && (await this.userService.findOneByIdentifier(username)));
|
||||
|
||||
if (!user) throw new UnauthorizedException();
|
||||
if (!user) throw new Error("User not found.");
|
||||
|
||||
done(null, user);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
try {
|
||||
user = await this.userService.create({
|
||||
email,
|
||||
@ -53,7 +54,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, "google") {
|
||||
});
|
||||
|
||||
done(null, user);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
throw new BadRequestException(ErrorMessage.UserAlreadyExists);
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, "jwt") {
|
||||
private readonly configService: ConfigService<Config>,
|
||||
private readonly userService: UserService,
|
||||
) {
|
||||
const extractors = [(request: Request) => request?.cookies?.Authentication];
|
||||
const extractors = [(request: Request) => request.cookies.Authentication];
|
||||
|
||||
super({
|
||||
secretOrKey: configService.get<string>("ACCESS_TOKEN_SECRET"),
|
||||
|
||||
@ -14,7 +14,7 @@ export class LocalStrategy extends PassportStrategy(Strategy, "local") {
|
||||
async validate(identifier: string, password: string) {
|
||||
try {
|
||||
return await this.authService.authenticate({ identifier, password });
|
||||
} catch (error) {
|
||||
} catch {
|
||||
throw new BadRequestException(ErrorMessage.InvalidCredentials);
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ export class RefreshStrategy extends PassportStrategy(Strategy, "refresh") {
|
||||
private readonly configService: ConfigService<Config>,
|
||||
private readonly authService: AuthService,
|
||||
) {
|
||||
const extractors = [(request: Request) => request?.cookies?.Refresh];
|
||||
const extractors = [(request: Request) => request.cookies.Refresh];
|
||||
|
||||
super({
|
||||
secretOrKey: configService.getOrThrow<string>("REFRESH_TOKEN_SECRET"),
|
||||
@ -26,7 +26,7 @@ export class RefreshStrategy extends PassportStrategy(Strategy, "refresh") {
|
||||
}
|
||||
|
||||
async validate(request: Request, payload: Payload) {
|
||||
const refreshToken = request.cookies?.Refresh;
|
||||
const refreshToken = request.cookies.Refresh;
|
||||
|
||||
return this.authService.validateRefreshToken(payload, refreshToken);
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ export class TwoFactorStrategy extends PassportStrategy(Strategy, "two-factor")
|
||||
private readonly configService: ConfigService<Config>,
|
||||
private readonly userService: UserService,
|
||||
) {
|
||||
const extractors = [(request: Request) => request?.cookies?.Authentication];
|
||||
const extractors = [(request: Request) => request.cookies.Authentication];
|
||||
|
||||
super({
|
||||
secretOrKey: configService.get<string>("ACCESS_TOKEN_SECRET"),
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { InternalServerErrorException } from "@nestjs/common";
|
||||
import { CookieOptions } from "express";
|
||||
|
||||
export const getCookieOptions = (grantType: "access" | "refresh"): CookieOptions => {
|
||||
@ -13,14 +12,10 @@ export const getCookieOptions = (grantType: "access" | "refresh"): CookieOptions
|
||||
}
|
||||
|
||||
// Options For Refresh Token
|
||||
if (grantType === "refresh") {
|
||||
return {
|
||||
httpOnly: true,
|
||||
sameSite: "strict",
|
||||
secure: (process.env.PUBLIC_URL ?? "").includes("https://"),
|
||||
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 2), // 2 days from now
|
||||
};
|
||||
}
|
||||
|
||||
throw new InternalServerErrorException("InvalidGrantType: " + grantType);
|
||||
return {
|
||||
httpOnly: true,
|
||||
sameSite: "strict",
|
||||
secure: (process.env.PUBLIC_URL ?? "").includes("https://"),
|
||||
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 2), // 2 days from now
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user