Skip to content

Commit

Permalink
feat!: reset password with email
Browse files Browse the repository at this point in the history
  • Loading branch information
stonith404 committed Feb 9, 2023
1 parent 8ab359b commit 5d1a7f0
Show file tree
Hide file tree
Showing 20 changed files with 460 additions and 157 deletions.
@@ -0,0 +1,14 @@
-- CreateTable
CREATE TABLE "ResetPasswordToken" (
"token" TEXT NOT NULL PRIMARY KEY,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expiresAt" DATETIME NOT NULL,
"userId" TEXT NOT NULL,
CONSTRAINT "ResetPasswordToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);

-- Disable TOTP as secret isn't encrypted anymore
UPDATE User SET totpEnabled=false, totpSecret=null, totpVerified=false WHERE totpSecret IS NOT NULL;

-- CreateIndex
CREATE UNIQUE INDEX "ResetPasswordToken_userId_key" ON "ResetPasswordToken"("userId");
17 changes: 14 additions & 3 deletions backend/prisma/schema.prisma
Expand Up @@ -22,9 +22,10 @@ model User {
loginTokens LoginToken[]
reverseShares ReverseShare[]
totpEnabled Boolean @default(false)
totpVerified Boolean @default(false)
totpSecret String?
totpEnabled Boolean @default(false)
totpVerified Boolean @default(false)
totpSecret String?
resetPasswordToken ResetPasswordToken?
}

model RefreshToken {
Expand All @@ -49,6 +50,16 @@ model LoginToken {
used Boolean @default(false)
}

model ResetPasswordToken {
token String @id @default(uuid())
createdAt DateTime @default(now())
expiresAt DateTime
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model Share {
id String @id @default(uuid())
createdAt DateTime @default(now())
Expand Down
59 changes: 35 additions & 24 deletions backend/prisma/seed/config.seed.ts
Expand Up @@ -21,15 +21,6 @@ const configVariables: Prisma.ConfigCreateInput[] = [
category: "internal",
locked: true,
},
{
order: 0,
key: "TOTP_SECRET",
description: "A 16 byte random string used to generate TOTP secrets",
type: "string",
value: crypto.randomBytes(16).toString("base64"),
category: "internal",
locked: true,
},
{
order: 1,
key: "APP_URL",
Expand Down Expand Up @@ -89,6 +80,15 @@ const configVariables: Prisma.ConfigCreateInput[] = [
},
{
order: 7,
key: "SHARE_RECEPIENTS_EMAIL_SUBJECT",
description:
"Subject of the email which gets sent to the share recipients.",
type: "string",
value: "Files shared with you",
category: "email",
},
{
order: 8,
key: "SHARE_RECEPIENTS_EMAIL_MESSAGE",
description:
"Message which gets sent to the share recipients. {creator} and {shareUrl} will be replaced with the creator's name and the share URL.",
Expand All @@ -98,16 +98,16 @@ const configVariables: Prisma.ConfigCreateInput[] = [
category: "email",
},
{
order: 8,
key: "SHARE_RECEPIENTS_EMAIL_SUBJECT",
order: 9,
key: "REVERSE_SHARE_EMAIL_SUBJECT",
description:
"Subject of the email which gets sent to the share recipients.",
"Subject of the email which gets sent when someone created a share with your reverse share link.",
type: "string",
value: "Files shared with you",
value: "Reverse share link used",
category: "email",
},
{
order: 9,
order: 10,
key: "REVERSE_SHARE_EMAIL_MESSAGE",
description:
"Message which gets sent when someone created a share with your reverse share link. {shareUrl} will be replaced with the creator's name and the share URL.",
Expand All @@ -117,16 +117,27 @@ const configVariables: Prisma.ConfigCreateInput[] = [
category: "email",
},
{
order: 10,
key: "REVERSE_SHARE_EMAIL_SUBJECT",
order: 11,
key: "RESET_PASSWORD_EMAIL_SUBJECT",
description:
"Subject of the email which gets sent when someone created a share with your reverse share link.",
"Subject of the email which gets sent when a user requests a password reset.",
type: "string",
value: "Reverse share link used",
value: "Pingvin Share password reset",
category: "email",
},
{
order: 11,
order: 12,
key: "RESET_PASSWORD_EMAIL_MESSAGE",
description:
"Message which gets sent when a user requests a password reset. {url} will be replaced with the reset password URL.",
type: "text",
value:
"Hey!\nYou requested a password reset. Click this link to reset your password: {url}\nThe link expires in a hour.\nPingvin Share 🐧",
category: "email",
},

{
order: 13,
key: "SMTP_ENABLED",
description:
"Whether SMTP is enabled. Only set this to true if you entered the host, port, email, user and password of your SMTP server.",
Expand All @@ -136,39 +147,39 @@ const configVariables: Prisma.ConfigCreateInput[] = [
secret: false,
},
{
order: 12,
order: 14,
key: "SMTP_HOST",
description: "Host of the SMTP server",
type: "string",
value: "",
category: "smtp",
},
{
order: 13,
order: 15,
key: "SMTP_PORT",
description: "Port of the SMTP server",
type: "number",
value: "0",
category: "smtp",
},
{
order: 14,
order: 16,
key: "SMTP_EMAIL",
description: "Email address which the emails get sent from",
type: "string",
value: "",
category: "smtp",
},
{
order: 15,
order: 17,
key: "SMTP_USERNAME",
description: "Username of the SMTP server",
type: "string",
value: "",
category: "smtp",
},
{
order: 16,
order: 18,
key: "SMTP_PASSWORD",
description: "Password of the SMTP server",
type: "string",
Expand Down
22 changes: 19 additions & 3 deletions backend/src/auth/auth.controller.ts
Expand Up @@ -3,6 +3,7 @@ import {
Controller,
ForbiddenException,
HttpCode,
Param,
Patch,
Post,
Req,
Expand All @@ -21,6 +22,7 @@ import { AuthRegisterDTO } from "./dto/authRegister.dto";
import { AuthSignInDTO } from "./dto/authSignIn.dto";
import { AuthSignInTotpDTO } from "./dto/authSignInTotp.dto";
import { EnableTotpDTO } from "./dto/enableTotp.dto";
import { ResetPasswordDTO } from "./dto/resetPassword.dto";
import { TokenDTO } from "./dto/token.dto";
import { UpdatePasswordDTO } from "./dto/updatePassword.dto";
import { VerifyTotpDTO } from "./dto/verifyTotp.dto";
Expand All @@ -34,8 +36,8 @@ export class AuthController {
private config: ConfigService
) {}

@Throttle(10, 5 * 60)
@Post("signUp")
@Throttle(10, 5 * 60)
async signUp(
@Body() dto: AuthRegisterDTO,
@Res({ passthrough: true }) response: Response
Expand All @@ -54,8 +56,8 @@ export class AuthController {
return result;
}

@Throttle(10, 5 * 60)
@Post("signIn")
@Throttle(10, 5 * 60)
@HttpCode(200)
async signIn(
@Body() dto: AuthSignInDTO,
Expand All @@ -74,8 +76,8 @@ export class AuthController {
return result;
}

@Throttle(10, 5 * 60)
@Post("signIn/totp")
@Throttle(10, 5 * 60)
@HttpCode(200)
async signInTotp(
@Body() dto: AuthSignInTotpDTO,
Expand All @@ -92,6 +94,20 @@ export class AuthController {
return new TokenDTO().from(result);
}

@Post("resetPassword/:email")
@Throttle(5, 5 * 60)
@HttpCode(204)
async requestResetPassword(@Param("email") email: string) {
return await this.authService.requestResetPassword(email);
}

@Post("resetPassword")
@Throttle(5, 5 * 60)
@HttpCode(204)
async resetPassword(@Body() dto: ResetPasswordDTO) {
return await this.authService.resetPassword(dto.token, dto.password);
}

@Patch("password")
@UseGuards(JwtGuard)
async updatePassword(
Expand Down
3 changes: 2 additions & 1 deletion backend/src/auth/auth.module.ts
@@ -1,12 +1,13 @@
import { Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
import { EmailModule } from "src/email/email.module";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { AuthTotpService } from "./authTotp.service";
import { JwtStrategy } from "./strategy/jwt.strategy";

@Module({
imports: [JwtModule.register({})],
imports: [JwtModule.register({}), EmailModule],
controllers: [AuthController],
providers: [AuthService, AuthTotpService, JwtStrategy],
exports: [AuthService],
Expand Down
48 changes: 47 additions & 1 deletion backend/src/auth/auth.service.ts
Expand Up @@ -10,6 +10,7 @@ import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
import * as argon from "argon2";
import * as moment from "moment";
import { ConfigService } from "src/config/config.service";
import { EmailService } from "src/email/email.service";
import { PrismaService } from "src/prisma/prisma.service";
import { AuthRegisterDTO } from "./dto/authRegister.dto";
import { AuthSignInDTO } from "./dto/authSignIn.dto";
Expand All @@ -19,7 +20,8 @@ export class AuthService {
constructor(
private prisma: PrismaService,
private jwtService: JwtService,
private config: ConfigService
private config: ConfigService,
private emailService: EmailService
) {}

async signUp(dto: AuthRegisterDTO) {
Expand Down Expand Up @@ -87,6 +89,50 @@ export class AuthService {
return { accessToken, refreshToken };
}

async requestResetPassword(email: string) {
const user = await this.prisma.user.findFirst({
where: { email },
include: { resetPasswordToken: true },
});

if (!user) throw new BadRequestException("User not found");

// Delete old reset password token
if (user.resetPasswordToken) {
await this.prisma.resetPasswordToken.delete({
where: { token: user.resetPasswordToken.token },
});
}

const { token } = await this.prisma.resetPasswordToken.create({
data: {
expiresAt: moment().add(1, "hour").toDate(),
user: { connect: { id: user.id } },
},
});

await this.emailService.sendResetPasswordEmail(user.email, token);
}

async resetPassword(token: string, newPassword: string) {
const user = await this.prisma.user.findFirst({
where: { resetPasswordToken: { token } },
});

if (!user) throw new BadRequestException("Token invalid or expired");

const newPasswordHash = await argon.hash(newPassword);

await this.prisma.resetPasswordToken.delete({
where: { token },
});

await this.prisma.user.update({
where: { id: user.id },
data: { password: newPasswordHash },
});
}

async updatePassword(user: User, oldPassword: string, newPassword: string) {
if (!(await argon.verify(user.password, oldPassword)))
throw new ForbiddenException("Invalid password");
Expand Down

0 comments on commit 5d1a7f0

Please sign in to comment.