add feature: password resets via email

This commit is contained in:
maxDorninger
2025-06-29 16:26:01 +02:00
parent 1732bde281
commit 83521a36ff
7 changed files with 357 additions and 9 deletions

View File

@@ -0,0 +1,140 @@
<script lang="ts">
import logo from '$lib/images/logo.svg';
import background from '$lib/images/pawel-czerwinski-NTYYL9Eb9y8-unsplash.jpg?enhanced';
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "$lib/components/ui/card";
import { toast } from 'svelte-sonner';
import { env } from '$env/dynamic/public';
const apiUrl = env.PUBLIC_API_URL;
let email = $state('');
let isLoading = $state(false);
let isSuccess = $state(false);
async function requestPasswordReset() {
if (!email) {
toast.error('Please enter your email address.');
return;
}
isLoading = true;
try {
const response = await fetch(apiUrl + '/auth/forgot-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email }),
credentials: 'include'
});
if (response.ok) {
isSuccess = true;
toast.success('Password reset email sent! Check your inbox for instructions.');
} else {
const errorText = await response.text();
toast.error(`Failed to send reset email: ${errorText}`);
}
} catch (error) {
console.error('Error requesting password reset:', error);
toast.error('An error occurred while sending the reset email. Please try again.');
} finally {
isLoading = false;
}
}
const handleSubmit = (event: Event) => {
event.preventDefault();
requestPasswordReset();
};
</script>
<div class="grid min-h-svh lg:grid-cols-2">
<div class="flex flex-col gap-4 p-6 md:p-10">
<div class="flex justify-center gap-2 md:justify-start">
<a class="flex items-center gap-2 font-medium" href="/login">
<div class="flex size-16 items-center justify-center rounded-md text-primary-foreground">
<img alt="MediaManager Logo" class="size-12" src={logo} />
</div>
<h1 class="scale-110">Media Manager</h1>
</a>
</div>
<div class="flex flex-1 items-center justify-center">
<div class="w-full max-w-[90vw]">
<Card class="mx-auto max-w-sm">
<CardHeader>
<CardTitle class="text-2xl">Forgot Password</CardTitle>
<CardDescription>
{#if isSuccess}
We've sent a password reset link to your email address if a SMTP server is configured. Check your inbox and follow the instructions to reset your password.
If you didn't receive an email, please contact an administrator, the reset link will be in the logs of MediaManager.
{:else}
Enter your email address and we'll send you a link to reset your password.
{/if}
</CardDescription>
</CardHeader>
<CardContent>
{#if isSuccess}
<div class="space-y-4">
<div class="rounded-lg bg-green-50 p-4 text-center dark:bg-green-950">
<p class="text-sm text-green-700 dark:text-green-300">
Password reset email sent successfully!
</p>
</div>
<div class="text-center text-sm text-muted-foreground">
<p>Didn't receive the email? Check your spam folder or</p>
<button
class="text-primary hover:underline"
onclick={() => { isSuccess = false; email = ''; }}
>
try again
</button>
</div>
</div>
{:else}
<form class="grid gap-4" onsubmit={handleSubmit}>
<div class="grid gap-2">
<Label for="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
bind:value={email}
disabled={isLoading}
required
/>
</div>
<Button
type="submit"
class="w-full"
disabled={isLoading || !email}
>
{#if isLoading}
Sending Reset Email...
{:else}
Send Reset Email
{/if}
</Button>
</form>
{/if}
<div class="mt-4 text-center text-sm">
<a href="/login" class="text-primary hover:underline font-semibold">
Back to Login
</a>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
<div class="relative hidden lg:block">
<enhanced:img
src={background}
alt="background"
class="absolute inset-0 h-full w-full rounded-l-3xl object-cover dark:brightness-[0.8]"
/>
</div>
</div>

View File

@@ -0,0 +1,151 @@
<script lang="ts">
import logo from '$lib/images/logo.svg';
import background from '$lib/images/pawel-czerwinski-NTYYL9Eb9y8-unsplash.jpg?enhanced';
import { page } from '$app/state';
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "$lib/components/ui/card";
import { toast } from 'svelte-sonner';
import { goto } from '$app/navigation';
import { env } from '$env/dynamic/public';
import {ChevronLeft} from "lucide-svelte";
import {onMount} from "svelte";
const apiUrl = env.PUBLIC_API_URL;
let newPassword = $state('');
let confirmPassword = $state('');
let isLoading = $state(false);
let resetToken = $derived(page.data.token);
onMount(() => {
if (!resetToken) {
toast.error('Invalid or missing reset token.');
goto('/login');
}
});
async function resetPassword() {
if (newPassword !== confirmPassword) {
toast.error('Passwords do not match.');
return;
}
if (!resetToken) {
toast.error('Invalid or missing reset token.');
return;
}
isLoading = true;
try {
const response = await fetch(apiUrl+`/auth/reset-password`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ password: newPassword, token: resetToken }),
credentials: 'include'
});
if (response.ok) {
toast.success('Password reset successfully! You can now log in with your new password.');
goto('/login');
} else {
const errorText = await response.text();
toast.error(`Failed to reset password: ${errorText}`);
throw new Error(`Failed to reset password: ${errorText}`);
}
} catch (error) {
console.error('Error resetting password:', error);
toast.error(error instanceof Error ? error.message : 'An unknown error occurred.');
} finally {
isLoading = false;
}
}
const handleSubmit = (event: Event) => {
event.preventDefault();
resetPassword();
};
</script>
<div class="grid min-h-svh lg:grid-cols-2">
<div class="flex flex-col gap-4 p-6 md:p-10">
<div class="flex justify-center gap-2 md:justify-start">
<a class="flex items-center gap-2 font-medium" href="/login">
<div class="flex size-16 items-center justify-center rounded-md text-primary-foreground">
<img alt="MediaManager Logo" class="size-12" src={logo} />
</div>
<h1 class="scale-110">Media Manager</h1>
</a>
</div>
<div class="flex flex-1 items-center justify-center">
<div class="w-full max-w-[90vw]">
<Card class="mx-auto max-w-sm">
<CardHeader>
<CardTitle class="text-2xl">Reset Password</CardTitle>
<CardDescription>
Enter your new password below.
</CardDescription>
</CardHeader>
<CardContent>
<form class="grid gap-4" onsubmit={handleSubmit}>
<div class="grid gap-2">
<Label for="new-password">New Password</Label>
<Input
id="new-password"
type="password"
placeholder="Enter your new password"
bind:value={newPassword}
disabled={isLoading}
required
minlength="1"
/>
</div>
<div class="grid gap-2">
<Label for="confirm-password">Confirm Password</Label>
<Input
id="confirm-password"
type="password"
placeholder="Confirm your new password"
bind:value={confirmPassword}
disabled={isLoading}
required
minlength="1"
/>
</div>
<Button
type="submit"
class="w-full"
disabled={isLoading || !newPassword || !confirmPassword}
>
{#if isLoading}
Resetting Password...
{:else}
Reset Password
{/if}
</Button>
</form>
<div class="mt-4 text-center text-sm">
<a href="/login" class="text-primary hover:underline font-semibold">
Back to Login
</a>
<span class="mx-2 text-muted-foreground"></span>
<a href="/login/forgot-password" class="text-primary hover:underline">
Request New Reset Link
</a>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
<div class="relative hidden lg:block">
<enhanced:img
src={background}
alt="background"
class="absolute inset-0 h-full w-full rounded-l-3xl object-cover dark:brightness-[0.8]"
/>
</div>
</div>

View File

@@ -0,0 +1,6 @@
import type {PageLoad} from './$types';
export function load({ url }) {
console.log("got token: ",url.searchParams.get('token'));
return {token: url.searchParams.get('token') };
}