This commit is contained in:
2026-03-18 15:33:24 +01:00
parent 5e7a6808ab
commit 867c51062b
39 changed files with 6499 additions and 161 deletions

View File

@@ -0,0 +1,151 @@
<script>
import { API_URL, WS_URL } from '$lib/api.js';
import { apiFetch } from '$lib/api.js';
let email = $state('');
let submitted = $state(false);
let loading = $state(false);
let error = $state('');
async function submit() {
error = '';
if (!email.trim()) { error = 'Email is required'; return; }
loading = true;
try {
const res = await fetch(`${API_URL}/auth/forgot-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
const data = await res.json();
if (!res.ok) { error = data.detail; return; }
submitted = true;
} catch {
error = 'Something went wrong. Please try again.';
} finally {
loading = false;
}
}
</script>
<main>
<div class="card">
{#if submitted}
<h1 class="title">Check Your Email</h1>
<p class="hint">If that email address is registered, you'll receive a password reset link shortly.</p>
{:else}
<h1 class="title">Forgot Password</h1>
<p class="hint">Enter your email address and we'll send you a reset link.</p>
<input type="email" placeholder="Email address" bind:value={email} />
<p class="error">{error}</p>
<button class="btn" onclick={submit} disabled={loading}>
{loading ? 'Sending...' : 'Send Reset Link'}
</button>
<a href="/auth" class="back-link">← Back to login</a>
{/if}
</div>
</main>
<style>
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700&family=Crimson+Text:ital,wght@0,400;0,600;1,400&display=swap');
main {
min-height: 100vh;
background: #0d0a04;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.card {
width: 340px;
background: #2e1c05;
border: 2px solid #6b4c1e;
border-radius: 12px;
padding: 2rem;
display: flex;
flex-direction: column;
gap: 1.2rem;
}
.title {
font-family: 'Cinzel', serif;
font-size: 20px;
color: #f0d080;
text-align: center;
margin: 0;
}
.hint {
font-family: 'Crimson Text', serif;
font-size: 15px;
color: rgba(245, 208, 96, 0.7);
margin: 0;
text-align: center;
line-height: 1.6;
}
input {
width: 100%;
padding: 9px 12px;
background: #1a1008;
border: 1.5px solid #8b6420;
border-radius: 6px;
color: #f0d080;
font-family: 'Crimson Text', serif;
font-size: 15px;
box-sizing: border-box;
outline: none;
}
input::placeholder {
color: rgba(240, 180, 80, 0.4);
}
input:focus { border-color: #f5d060; }
button {
width: 100%;
padding: 10px;
background: #6b4c1e;
color: #f0d080;
border: 1.5px solid #8b6420;
border-radius: 6px;
font-family: 'Cinzel', serif;
font-size: 13px;
cursor: pointer;
transition: background 0.15s;
}
button:hover:not(:disabled) {
background: #8b6420;
}
button:disabled {
opacity: 0.5;
cursor: default;
}
.back-link {
font-family: 'Crimson Text', serif;
font-size: 14px;
color: rgba(245, 208, 96, 0.5);
text-align: center;
text-decoration: none;
transition: color 0.15s;
}
.back-link:hover { color: #f5d060; }
.error {
font-family: 'Crimson Text', serif;
font-size: 14px;
color: #f06060;
margin: 0;
min-height: 1.4em;
text-align: center;
}
</style>

View File

@@ -0,0 +1,172 @@
<script>
import { API_URL, WS_URL } from '$lib/api.js';
import { apiFetch } from '$lib/api.js';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
let newPassword = $state('');
let confirmPassword = $state('');
let error = $state('');
let success = $state(false);
let loading = $state(false);
let token = $derived($page.url.searchParams.get('token') ?? '');
function validate() {
if (newPassword.length < 8) return 'Password must be at least 8 characters';
if (newPassword.length > 256) return 'Password must be 256 characters or fewer';
if (newPassword !== confirmPassword) return 'Passwords do not match';
return null;
}
async function submit() {
error = '';
const validationError = validate();
if (validationError) { error = validationError; return; }
loading = true;
try {
const res = await fetch(`${API_URL}/auth/reset-password-with-token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, new_password: newPassword }),
});
const data = await res.json();
if (!res.ok) { error = data.detail; return; }
success = true;
} catch {
error = 'Something went wrong. Please try again.';
} finally {
loading = false;
}
}
</script>
<main>
<div class="card">
{#if !token}
<h1 class="title">Invalid Link</h1>
<p class="hint">This reset link is invalid. Please request a new one.</p>
<a href="/forgot-password" class="btn" style="text-align:center; text-decoration:none;">Request New Link</a>
{:else if success}
<h1 class="title">Password Updated</h1>
<p class="hint">Your password has been changed. You can now log in.</p>
<button class="btn" onclick={() => goto('/auth')}>Go to Login</button>
{:else}
<h1 class="title">Set New Password</h1>
<div class="fields">
<label class="field-label" for="new">New Password</label>
<input id="new" type="password" placeholder="At least 8 characters" bind:value={newPassword} />
<label class="field-label" for="confirm">Confirm Password</label>
<input id="confirm" type="password" placeholder="Repeat new password" bind:value={confirmPassword} />
</div>
<p class="error">{error}</p>
<button class="btn" onclick={submit} disabled={loading}>
{loading ? 'Updating...' : 'Set New Password'}
</button>
{/if}
</div>
</main>
<style>
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700&family=Crimson+Text:ital,wght@0,400;0,600;1,400&display=swap');
main {
min-height: 100vh;
background: #0d0a04;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.card {
width: 380px;
background: #3d2507;
border: 2px solid #c8861a;
border-radius: 12px;
padding: 2rem;
display: flex;
flex-direction: column;
gap: 1.2rem;
}
.title {
font-family: 'Cinzel', serif;
font-size: 20px;
font-weight: 700;
color: #f5d060;
margin: 0;
text-align: center;
}
.hint {
font-family: 'Crimson Text', serif;
font-size: 15px;
color: rgba(245, 208, 96, 0.7);
margin: 0;
text-align: center;
line-height: 1.6;
}
.fields {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.field-label {
font-family: 'Cinzel', serif;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: rgba(245, 208, 96, 0.5);
}
input {
width: 100%;
padding: 9px 12px;
background: #221508;
border: 1.5px solid #c8861a;
border-radius: 6px;
color: #f5d060;
font-family: 'Crimson Text', serif;
font-size: 15px;
box-sizing: border-box;
outline: none;
margin-bottom: 0.4rem;
}
input:focus { border-color: #f5d060; }
input::placeholder { color: rgba(245, 208, 96, 0.35); }
.btn {
width: 100%;
padding: 10px;
background: #c8861a;
color: #fff8e0;
border: none;
border-radius: 6px;
font-family: 'Cinzel', serif;
font-size: 13px;
font-weight: 700;
cursor: pointer;
transition: background 0.15s;
display: block;
}
.btn:hover:not(:disabled) { background: #e09820; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.error {
font-family: 'Crimson Text', serif;
font-size: 14px;
color: #f06060;
margin: 0;
min-height: 1.4em;
text-align: center;
}
</style>