-
-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: added reset password page * feat: added reset token db model * feat: added reset password page server * feat: finished reset password flow * fix: return success regardless of user existing or not * fix: return fail if MAIL_ENABLED option is false * fix: only check for user if MAIL_ENABLED
- Loading branch information
Showing
10 changed files
with
350 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { env } from '$env/dynamic/public'; | ||
import prisma from '@db'; | ||
import { generatePasswordResetToken } from '../auth'; | ||
import { sendEmail } from './base'; | ||
|
||
export const sendResetEmail = async (userId: string) => { | ||
const user = await prisma.user.findUnique({ where: { id: userId } }); | ||
if (!user) return false; | ||
|
||
const resetToken = await generatePasswordResetToken(userId); | ||
|
||
const verifyUrl = `${env.PUBLIC_URL}/reset-password?token=${encodeURIComponent( | ||
resetToken | ||
)}&userId=${encodeURIComponent(userId)}`; | ||
|
||
const content = `To reset your password, please click the following link: ${verifyUrl} | ||
If you did not make this request, please disgregard this email.`; | ||
|
||
const subject = 'YABin: Password reset request'; | ||
|
||
const sent = await sendEmail(user.email, subject, content); | ||
if (!sent) return false; | ||
|
||
return true; | ||
}; |
19 changes: 19 additions & 0 deletions
19
src/lib/server/prisma/migrations/20231031235131_add_resettoken/migration.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
-- CreateTable | ||
CREATE TABLE "ResetToken" ( | ||
"id" TEXT NOT NULL, | ||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
"expiresAt" TIMESTAMP(3) NOT NULL, | ||
"token" TEXT NOT NULL, | ||
"userId" TEXT NOT NULL, | ||
|
||
CONSTRAINT "ResetToken_pkey" PRIMARY KEY ("id") | ||
); | ||
|
||
-- CreateIndex | ||
CREATE UNIQUE INDEX "ResetToken_token_key" ON "ResetToken"("token"); | ||
|
||
-- CreateIndex | ||
CREATE UNIQUE INDEX "ResetToken_userId_key" ON "ResetToken"("userId"); | ||
|
||
-- AddForeignKey | ||
ALTER TABLE "ResetToken" ADD CONSTRAINT "ResetToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
const emailRegex = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/g; | ||
const passwordLength = 8; | ||
const nameLength = 50; | ||
const usernameLength = 50; | ||
|
||
export function validateEmail(email: FormDataEntryValue) { | ||
if (emailRegex.test(email?.toString())) { | ||
return true; | ||
} else { | ||
throw new Error('Invalid email address'); | ||
} | ||
} | ||
|
||
export function validatePassword(password: FormDataEntryValue) { | ||
if (password.toString().length >= passwordLength) { | ||
return true; | ||
} else { | ||
throw new Error(`Password must be at least ${passwordLength} characters long`); | ||
} | ||
} | ||
|
||
export function validateName(name: FormDataEntryValue) { | ||
if (name.toString().length <= nameLength) { | ||
return true; | ||
} else { | ||
throw new Error(`Name is too long`); | ||
} | ||
} | ||
|
||
export function validateUsername(username: FormDataEntryValue) { | ||
if (username.toString().length <= usernameLength) { | ||
return true; | ||
} else { | ||
throw new Error(`Username is too long`); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import type { Actions, PageServerLoad } from './$types'; | ||
import { redirect, fail } from '@sveltejs/kit'; | ||
import prisma from '@db'; | ||
import { env } from '$env/dynamic/private'; | ||
import { sendResetEmail } from '$lib/server/email/reset-password'; | ||
|
||
export const load: PageServerLoad = async () => { | ||
if (env.MAIL_ENABLED === 'false') { | ||
throw redirect(303, '/'); | ||
} | ||
}; | ||
|
||
export const actions: Actions = { | ||
default: async ({ request }) => { | ||
const data = await request.formData(); | ||
|
||
const usernameOrEmail = data.get('username-email'); | ||
|
||
if (!usernameOrEmail) { | ||
return fail(400, { success: false, errors: ['All fields are required'] }); | ||
} | ||
|
||
if (env.MAIL_ENABLED === 'true') { | ||
const user = await prisma.user.findFirst({ | ||
where: { | ||
OR: [{ username: usernameOrEmail.toString() }, { email: usernameOrEmail.toString() }] | ||
} | ||
}); | ||
|
||
if (user) { | ||
sendResetEmail(user.id); | ||
} | ||
// Return success regardless of whether username/email is found or not | ||
return { success: true, message: 'Please check e-mail for a password reset link' }; | ||
} else { | ||
return fail(400, { success: false, errors: ['E-mail is disabled'] }); | ||
} | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
<script lang="ts"> | ||
import { enhance } from '$app/forms'; | ||
import type { ActionData } from './$types'; | ||
export let form: ActionData; | ||
</script> | ||
|
||
<div class="flex flex-col justify-center items-center"> | ||
<h1 class="text-4xl">Reset Password</h1> | ||
<div class="mt-6"> | ||
{#if form?.errors} | ||
<ul class="text-red-500 text-center"> | ||
{#each form.errors as error} | ||
<li>{error}</li> | ||
{/each} | ||
</ul> | ||
{/if} | ||
{#if form?.success} | ||
<div class="text-green-500 text-center">{form?.message}</div> | ||
{/if} | ||
|
||
<form method="post" class="mt-2 flex flex-col gap-4" use:enhance> | ||
<div> | ||
<label for="username-email" class="px-2 py-2">Username or E-mail</label> | ||
<input class="bg-dark px-2 py-1 w-full" type="text" id="username-email" name="username-email" placeholder="Username or E-mail" /> | ||
</div> | ||
|
||
<div class="flex flex-row items-center gap-4 mt-2"> | ||
<button class="bg-amber-500 text-black text-lg px-4 py-1">Reset Password</button> | ||
</div> | ||
|
||
</form> | ||
</div> | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import { redirect, error } from '@sveltejs/kit'; | ||
import type { PageServerLoad, Actions } from './$types'; | ||
import { fail } from '@sveltejs/kit'; | ||
import prisma from '@db'; | ||
import { hashPassword } from '$lib/crypto'; | ||
import { env } from '$env/dynamic/private'; | ||
import { validatePassword } from '$lib/server/validate'; | ||
|
||
export const load: PageServerLoad = async ({ url }) => { | ||
const userId = url.searchParams.get('userId'); | ||
const token = url.searchParams.get('token'); | ||
|
||
if (!userId || !token) { | ||
throw error(404, 'Not found'); | ||
} | ||
|
||
const user = await prisma.user.findUnique({ where: { id: userId } }); | ||
const resetToken = await prisma.resetToken.findUnique({ where: { token: token } }); | ||
|
||
if (!user || !resetToken) { | ||
throw error(404, 'Not found'); | ||
} | ||
|
||
if (resetToken.expiresAt <= new Date()) { | ||
throw error(404, 'Expired link'); | ||
} | ||
}; | ||
|
||
export const actions: Actions = { | ||
default: async ({ url, request }) => { | ||
const userId = url.searchParams.get('userId'); | ||
if (!userId) throw error(404, 'Not found'); | ||
|
||
const user = await prisma.user.findUnique({ where: { id: userId } }); | ||
if (!user) throw error(404, 'Not found'); | ||
|
||
const data = await request.formData(); | ||
const newPassword = data.get('new-password'); | ||
const cnfPassword = data.get('confirm-password'); | ||
const errors: string[] = []; | ||
|
||
if (!newPassword || !cnfPassword) { | ||
errors.push('All fields are required'); | ||
} | ||
|
||
if (newPassword !== cnfPassword) { | ||
errors.push('Passwords need to match'); | ||
} | ||
|
||
try { | ||
if (newPassword) validatePassword(newPassword); | ||
} catch (e: any) { | ||
errors.push(e.message); | ||
} | ||
|
||
if (newPassword) { | ||
const oldPasswordHash = user.password; | ||
const newPasswordHash = await hashPassword(newPassword.toString(), env.SALT); | ||
if (oldPasswordHash === newPasswordHash) { | ||
errors.push('Cannot use existing password'); | ||
} | ||
} | ||
|
||
if (errors.length > 0) { | ||
return fail(400, { success: false, errors }); | ||
} | ||
|
||
if (newPassword && cnfPassword) { | ||
const newPasswordHash = await hashPassword(newPassword.toString(), env.SALT); | ||
await prisma.user.update({ where: { id: userId }, data: { password: newPasswordHash } }); | ||
await prisma.resetToken.delete({ where: { userId: userId } }); | ||
throw redirect(303, '/login'); | ||
} | ||
|
||
return { success: false, errors: ['Unknown error'] }; | ||
} | ||
}; |
Oops, something went wrong.