fix(auth): add missing reset-password page to 13 apps

All SvelteKit web apps now have complete auth flows:
- login, register, forgot-password, and reset-password

Changes:
- Add reset-password page to: chat, clock, contacts, context,
  manadeck, nutriphi, planta, presi, questions, skilltree,
  todo, zitare, photos
- Add forgot-password page to photos (was also missing)
- Add resetPasswordWithToken() method to all 13 auth stores
- Each page customized with app-specific logo, colors, branding

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-19 10:13:18 +01:00
parent 8e4b331cb3
commit 0e496f7a00
27 changed files with 2797 additions and 0 deletions

View file

@ -218,6 +218,27 @@ export const authStore = {
}
},
/**
* Reset password with token (from reset email link)
*/
async resetPasswordWithToken(token: string, newPassword: string) {
const authService = getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server' };
}
try {
const result = await authService.resetPassword(token, newPassword);
if (!result.success) {
return { success: false, error: result.error || 'Failed to reset password' };
}
return { success: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return { success: false, error: errorMessage };
}
},
/**
* Resend verification email
*/

View file

@ -0,0 +1,174 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { ChatLogo } from '@manacore/shared-branding';
import { authStore } from '$lib/stores/auth.svelte';
let loading = $state(false);
let hasToken = $state(false);
let token = $state<string | null>(null);
let password = $state('');
let confirmPassword = $state('');
let error = $state<string | null>(null);
let success = $state(false);
onMount(() => {
token = $page.url.searchParams.get('token');
hasToken = !!token;
});
async function handleSubmit(e: Event) {
e.preventDefault();
error = null;
if (!token) {
error = 'Invalid or missing token';
return;
}
if (password !== confirmPassword) {
error = 'Passwords do not match';
return;
}
if (password.length < 8) {
error = 'Password must be at least 8 characters';
return;
}
loading = true;
try {
const result = await authStore.resetPasswordWithToken(token, password);
if (!result.success) {
error = result.error || 'Failed to reset password';
} else {
success = true;
setTimeout(() => goto('/login'), 3000);
}
} catch (err) {
error = err instanceof Error ? err.message : 'An error occurred';
} finally {
loading = false;
}
}
</script>
<svelte:head>
<title>Reset Password - ManaChat</title>
</svelte:head>
<div
class="flex min-h-screen flex-col bg-gradient-to-b from-sky-100 to-white dark:from-slate-900 dark:to-slate-800"
>
<header class="flex items-center justify-between p-4">
<a href="/" class="flex items-center gap-2">
<ChatLogo class="h-8 w-8" />
<span class="text-xl font-semibold" style="color: #0ea5e9">ManaChat</span>
</a>
</header>
<main class="flex flex-1 items-center justify-center p-4">
<div class="w-full max-w-md">
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Reset Password</h1>
<p class="mt-2 text-gray-600 dark:text-gray-400">
{#if success}Password reset successfully
{:else if hasToken}Enter your new password
{:else}Invalid or missing token{/if}
</p>
</div>
{#if success}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<div class="text-center">
<div class="mb-4 text-6xl">&#x2705;</div>
<p class="mb-6 text-gray-600 dark:text-gray-400">
Your password has been reset successfully. You will be redirected to the login page
shortly.
</p>
<a
href="/login"
class="inline-block rounded-lg px-6 py-3 font-medium text-white transition-colors"
style="background-color: #0ea5e9"
>
Go to login
</a>
</div>
</div>
{:else if hasToken}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<form onsubmit={handleSubmit}>
{#if error}
<div
class="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-400"
>
{error}
</div>
{/if}
<div class="space-y-4">
<div>
<label
for="password"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
>New Password</label
>
<input
type="password"
id="password"
autocomplete="new-password"
placeholder="Enter new password"
required
minlength={8}
bind:value={password}
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:outline-none focus:ring-2 dark:border-slate-600 dark:bg-slate-700 dark:text-white dark:placeholder-gray-400"
style="--tw-ring-color: #0ea5e980; border-color: #0ea5e9"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">At least 8 characters</p>
</div>
<div>
<label
for="confirmPassword"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
>Confirm Password</label
>
<input
type="password"
id="confirmPassword"
autocomplete="new-password"
placeholder="Confirm password"
required
minlength={8}
bind:value={confirmPassword}
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:outline-none focus:ring-2 dark:border-slate-600 dark:bg-slate-700 dark:text-white dark:placeholder-gray-400"
/>
</div>
<button
type="submit"
disabled={loading}
class="w-full rounded-lg px-4 py-3 font-medium text-white transition-colors disabled:cursor-not-allowed disabled:opacity-50"
style="background-color: #0ea5e9"
>
{loading ? 'Updating password...' : 'Update Password'}
</button>
</div>
</form>
</div>
{:else}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<div class="text-center">
<div class="mb-4 text-6xl">&#x26A0;&#xFE0F;</div>
<p class="mb-6 text-gray-600 dark:text-gray-400">
This password reset link is invalid or has expired.
</p>
<a
href="/forgot-password"
class="inline-block rounded-lg px-6 py-3 font-medium text-white transition-colors"
style="background-color: #0ea5e9"
>
Request a new link
</a>
</div>
</div>
{/if}
</div>
</main>
</div>

View file

@ -217,6 +217,27 @@ export const authStore = {
}
},
/**
* Reset password with token (from reset email link)
*/
async resetPasswordWithToken(token: string, newPassword: string) {
const authService = getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server' };
}
try {
const result = await authService.resetPassword(token, newPassword);
if (!result.success) {
return { success: false, error: result.error || 'Failed to reset password' };
}
return { success: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return { success: false, error: errorMessage };
}
},
/**
* Resend verification email
*/

View file

@ -0,0 +1,174 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { ClockLogo } from '@manacore/shared-branding';
import { authStore } from '$lib/stores/auth.svelte';
let loading = $state(false);
let hasToken = $state(false);
let token = $state<string | null>(null);
let password = $state('');
let confirmPassword = $state('');
let error = $state<string | null>(null);
let success = $state(false);
onMount(() => {
token = $page.url.searchParams.get('token');
hasToken = !!token;
});
async function handleSubmit(e: Event) {
e.preventDefault();
error = null;
if (!token) {
error = 'Invalid or missing token';
return;
}
if (password !== confirmPassword) {
error = 'Passwords do not match';
return;
}
if (password.length < 8) {
error = 'Password must be at least 8 characters';
return;
}
loading = true;
try {
const result = await authStore.resetPasswordWithToken(token, password);
if (!result.success) {
error = result.error || 'Failed to reset password';
} else {
success = true;
setTimeout(() => goto('/login'), 3000);
}
} catch (err) {
error = err instanceof Error ? err.message : 'An error occurred';
} finally {
loading = false;
}
}
</script>
<svelte:head>
<title>Reset Password - Clock</title>
</svelte:head>
<div
class="flex min-h-screen flex-col bg-gradient-to-b from-amber-100 to-white dark:from-stone-900 dark:to-stone-800"
>
<header class="flex items-center justify-between p-4">
<a href="/" class="flex items-center gap-2">
<ClockLogo class="h-8 w-8" />
<span class="text-xl font-semibold" style="color: #f59e0b">Clock</span>
</a>
</header>
<main class="flex flex-1 items-center justify-center p-4">
<div class="w-full max-w-md">
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Reset Password</h1>
<p class="mt-2 text-gray-600 dark:text-gray-400">
{#if success}Password reset successfully
{:else if hasToken}Enter your new password
{:else}Invalid or missing token{/if}
</p>
</div>
{#if success}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<div class="text-center">
<div class="mb-4 text-6xl">&#x2705;</div>
<p class="mb-6 text-gray-600 dark:text-gray-400">
Your password has been reset successfully. You will be redirected to the login page
shortly.
</p>
<a
href="/login"
class="inline-block rounded-lg px-6 py-3 font-medium text-white transition-colors"
style="background-color: #f59e0b"
>
Go to login
</a>
</div>
</div>
{:else if hasToken}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<form onsubmit={handleSubmit}>
{#if error}
<div
class="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-400"
>
{error}
</div>
{/if}
<div class="space-y-4">
<div>
<label
for="password"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
>New Password</label
>
<input
type="password"
id="password"
autocomplete="new-password"
placeholder="Enter new password"
required
minlength={8}
bind:value={password}
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:outline-none focus:ring-2 dark:border-slate-600 dark:bg-slate-700 dark:text-white dark:placeholder-gray-400"
style="--tw-ring-color: #f59e0b80; border-color: #f59e0b"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">At least 8 characters</p>
</div>
<div>
<label
for="confirmPassword"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
>Confirm Password</label
>
<input
type="password"
id="confirmPassword"
autocomplete="new-password"
placeholder="Confirm password"
required
minlength={8}
bind:value={confirmPassword}
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:outline-none focus:ring-2 dark:border-slate-600 dark:bg-slate-700 dark:text-white dark:placeholder-gray-400"
/>
</div>
<button
type="submit"
disabled={loading}
class="w-full rounded-lg px-4 py-3 font-medium text-white transition-colors disabled:cursor-not-allowed disabled:opacity-50"
style="background-color: #f59e0b"
>
{loading ? 'Updating password...' : 'Update Password'}
</button>
</div>
</form>
</div>
{:else}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<div class="text-center">
<div class="mb-4 text-6xl">&#x26A0;&#xFE0F;</div>
<p class="mb-6 text-gray-600 dark:text-gray-400">
This password reset link is invalid or has expired.
</p>
<a
href="/forgot-password"
class="inline-block rounded-lg px-6 py-3 font-medium text-white transition-colors"
style="background-color: #f59e0b"
>
Request a new link
</a>
</div>
</div>
{/if}
</div>
</main>
</div>

View file

@ -218,6 +218,27 @@ export const authStore = {
}
},
/**
* Reset password with token (from reset email link)
*/
async resetPasswordWithToken(token: string, newPassword: string) {
const authService = getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server' };
}
try {
const result = await authService.resetPassword(token, newPassword);
if (!result.success) {
return { success: false, error: result.error || 'Failed to reset password' };
}
return { success: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return { success: false, error: errorMessage };
}
},
/**
* Resend verification email
*/

View file

@ -0,0 +1,174 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { ContactsLogo } from '@manacore/shared-branding';
import { authStore } from '$lib/stores/auth.svelte';
let loading = $state(false);
let hasToken = $state(false);
let token = $state<string | null>(null);
let password = $state('');
let confirmPassword = $state('');
let error = $state<string | null>(null);
let success = $state(false);
onMount(() => {
token = $page.url.searchParams.get('token');
hasToken = !!token;
});
async function handleSubmit(e: Event) {
e.preventDefault();
error = null;
if (!token) {
error = 'Invalid or missing token';
return;
}
if (password !== confirmPassword) {
error = 'Passwords do not match';
return;
}
if (password.length < 8) {
error = 'Password must be at least 8 characters';
return;
}
loading = true;
try {
const result = await authStore.resetPasswordWithToken(token, password);
if (!result.success) {
error = result.error || 'Failed to reset password';
} else {
success = true;
setTimeout(() => goto('/login'), 3000);
}
} catch (err) {
error = err instanceof Error ? err.message : 'An error occurred';
} finally {
loading = false;
}
}
</script>
<svelte:head>
<title>Reset Password - Contacts</title>
</svelte:head>
<div
class="flex min-h-screen flex-col bg-gradient-to-b from-blue-100 to-white dark:from-slate-900 dark:to-slate-800"
>
<header class="flex items-center justify-between p-4">
<a href="/" class="flex items-center gap-2">
<ContactsLogo class="h-8 w-8" />
<span class="text-xl font-semibold" style="color: #3b82f6">Contacts</span>
</a>
</header>
<main class="flex flex-1 items-center justify-center p-4">
<div class="w-full max-w-md">
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Reset Password</h1>
<p class="mt-2 text-gray-600 dark:text-gray-400">
{#if success}Password reset successfully
{:else if hasToken}Enter your new password
{:else}Invalid or missing token{/if}
</p>
</div>
{#if success}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<div class="text-center">
<div class="mb-4 text-6xl">&#x2705;</div>
<p class="mb-6 text-gray-600 dark:text-gray-400">
Your password has been reset successfully. You will be redirected to the login page
shortly.
</p>
<a
href="/login"
class="inline-block rounded-lg px-6 py-3 font-medium text-white transition-colors"
style="background-color: #3b82f6"
>
Go to login
</a>
</div>
</div>
{:else if hasToken}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<form onsubmit={handleSubmit}>
{#if error}
<div
class="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-400"
>
{error}
</div>
{/if}
<div class="space-y-4">
<div>
<label
for="password"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
>New Password</label
>
<input
type="password"
id="password"
autocomplete="new-password"
placeholder="Enter new password"
required
minlength={8}
bind:value={password}
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:outline-none focus:ring-2 dark:border-slate-600 dark:bg-slate-700 dark:text-white dark:placeholder-gray-400"
style="--tw-ring-color: #3b82f680; border-color: #3b82f6"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">At least 8 characters</p>
</div>
<div>
<label
for="confirmPassword"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
>Confirm Password</label
>
<input
type="password"
id="confirmPassword"
autocomplete="new-password"
placeholder="Confirm password"
required
minlength={8}
bind:value={confirmPassword}
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:outline-none focus:ring-2 dark:border-slate-600 dark:bg-slate-700 dark:text-white dark:placeholder-gray-400"
/>
</div>
<button
type="submit"
disabled={loading}
class="w-full rounded-lg px-4 py-3 font-medium text-white transition-colors disabled:cursor-not-allowed disabled:opacity-50"
style="background-color: #3b82f6"
>
{loading ? 'Updating password...' : 'Update Password'}
</button>
</div>
</form>
</div>
{:else}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<div class="text-center">
<div class="mb-4 text-6xl">&#x26A0;&#xFE0F;</div>
<p class="mb-6 text-gray-600 dark:text-gray-400">
This password reset link is invalid or has expired.
</p>
<a
href="/forgot-password"
class="inline-block rounded-lg px-6 py-3 font-medium text-white transition-colors"
style="background-color: #3b82f6"
>
Request a new link
</a>
</div>
</div>
{/if}
</div>
</main>
</div>

View file

@ -0,0 +1,236 @@
/**
* Auth Store - Manages authentication state using Svelte 5 runes
* Uses Mana Core Auth
*/
import { browser } from '$app/environment';
import { initializeWebAuth, type UserData } from '@manacore/shared-auth';
// Get auth URL dynamically at runtime - fallback for SSR and client
function getAuthUrl(): string {
if (browser && typeof window !== 'undefined') {
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
.__PUBLIC_MANA_CORE_AUTH_URL__;
return injectedUrl || 'http://localhost:3001';
}
return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
}
// Lazy initialization to avoid SSR issues with localStorage
let _authService: ReturnType<typeof initializeWebAuth>['authService'] | null = null;
let _tokenManager: ReturnType<typeof initializeWebAuth>['tokenManager'] | null = null;
function getAuthService() {
if (!browser) return null;
if (!_authService) {
const auth = initializeWebAuth({
baseUrl: getAuthUrl(),
});
_authService = auth.authService;
_tokenManager = auth.tokenManager;
}
return _authService;
}
function getTokenManager() {
if (!browser) return null;
getAuthService();
return _tokenManager;
}
// State
let user = $state<UserData | null>(null);
let loading = $state(true);
let initialized = $state(false);
export const authStore = {
get user() {
return user;
},
get loading() {
return loading;
},
get isAuthenticated() {
return !!user;
},
get initialized() {
return initialized;
},
async initialize() {
if (initialized) return;
const authService = getAuthService();
if (!authService) {
initialized = true;
loading = false;
return;
}
loading = true;
try {
let authenticated = await authService.isAuthenticated();
if (!authenticated) {
const ssoResult = await authService.trySSO();
if (ssoResult.success) {
authenticated = true;
}
}
if (authenticated) {
const userData = await authService.getUserFromToken();
user = userData;
}
initialized = true;
} catch (error) {
console.error('Failed to initialize auth:', error);
user = null;
} finally {
loading = false;
}
},
async signIn(email: string, password: string) {
const authService = getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server' };
}
try {
const result = await authService.signIn(email, password);
if (!result.success) {
return { success: false, error: result.error || 'Login failed' };
}
const userData = await authService.getUserFromToken();
user = userData;
return { success: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return { success: false, error: errorMessage };
}
},
async signUp(email: string, password: string) {
const authService = getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server', needsVerification: false };
}
try {
const sourceAppUrl = browser ? window.location.origin : undefined;
const result = await authService.signUp(email, password, sourceAppUrl);
if (!result.success) {
return { success: false, error: result.error || 'Signup failed', needsVerification: false };
}
if (result.needsVerification) {
return { success: true, needsVerification: true };
}
const signInResult = await this.signIn(email, password);
return { ...signInResult, needsVerification: false };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return { success: false, error: errorMessage, needsVerification: false };
}
},
async signOut() {
const authService = getAuthService();
if (!authService) {
user = null;
return;
}
try {
await authService.signOut();
user = null;
} catch (error) {
console.error('Sign out error:', error);
user = null;
}
},
async resetPassword(email: string) {
const authService = getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server' };
}
try {
const result = await authService.forgotPassword(email);
if (!result.success) {
return { success: false, error: result.error || 'Password reset failed' };
}
return { success: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return { success: false, error: errorMessage };
}
},
/**
* Reset password with token (from reset email link)
*/
async resetPasswordWithToken(token: string, newPassword: string) {
const authService = getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server' };
}
try {
const result = await authService.resetPassword(token, newPassword);
if (!result.success) {
return { success: false, error: result.error || 'Failed to reset password' };
}
return { success: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return { success: false, error: errorMessage };
}
},
async resendVerificationEmail(email: string) {
const authService = getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server' };
}
try {
const sourceAppUrl = browser ? window.location.origin : undefined;
const result = await authService.resendVerificationEmail(email, sourceAppUrl);
if (!result.success) {
return { success: false, error: result.error || 'Failed to resend verification email' };
}
return { success: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return { success: false, error: errorMessage };
}
},
async getAccessToken() {
const authService = getAuthService();
if (!authService) {
return null;
}
return await authService.getAppToken();
},
async getValidToken(): Promise<string | null> {
const tokenManager = getTokenManager();
if (!tokenManager) {
return null;
}
return await tokenManager.getValidToken();
},
};

View file

@ -0,0 +1,174 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { ContextLogo } from '@manacore/shared-branding';
import { authStore } from '$lib/stores/auth.svelte';
let loading = $state(false);
let hasToken = $state(false);
let token = $state<string | null>(null);
let password = $state('');
let confirmPassword = $state('');
let error = $state<string | null>(null);
let success = $state(false);
onMount(() => {
token = $page.url.searchParams.get('token');
hasToken = !!token;
});
async function handleSubmit(e: Event) {
e.preventDefault();
error = null;
if (!token) {
error = 'Invalid or missing token';
return;
}
if (password !== confirmPassword) {
error = 'Passwords do not match';
return;
}
if (password.length < 8) {
error = 'Password must be at least 8 characters';
return;
}
loading = true;
try {
const result = await authStore.resetPasswordWithToken(token, password);
if (!result.success) {
error = result.error || 'Failed to reset password';
} else {
success = true;
setTimeout(() => goto('/login'), 3000);
}
} catch (err) {
error = err instanceof Error ? err.message : 'An error occurred';
} finally {
loading = false;
}
}
</script>
<svelte:head>
<title>Reset Password - Context</title>
</svelte:head>
<div
class="flex min-h-screen flex-col bg-gradient-to-b from-sky-100 to-white dark:from-slate-900 dark:to-slate-800"
>
<header class="flex items-center justify-between p-4">
<a href="/" class="flex items-center gap-2">
<ContextLogo class="h-8 w-8" />
<span class="text-xl font-semibold" style="color: #0ea5e9">Context</span>
</a>
</header>
<main class="flex flex-1 items-center justify-center p-4">
<div class="w-full max-w-md">
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Reset Password</h1>
<p class="mt-2 text-gray-600 dark:text-gray-400">
{#if success}Password reset successfully
{:else if hasToken}Enter your new password
{:else}Invalid or missing token{/if}
</p>
</div>
{#if success}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<div class="text-center">
<div class="mb-4 text-6xl">&#x2705;</div>
<p class="mb-6 text-gray-600 dark:text-gray-400">
Your password has been reset successfully. You will be redirected to the login page
shortly.
</p>
<a
href="/login"
class="inline-block rounded-lg px-6 py-3 font-medium text-white transition-colors"
style="background-color: #0ea5e9"
>
Go to login
</a>
</div>
</div>
{:else if hasToken}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<form onsubmit={handleSubmit}>
{#if error}
<div
class="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-400"
>
{error}
</div>
{/if}
<div class="space-y-4">
<div>
<label
for="password"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
>New Password</label
>
<input
type="password"
id="password"
autocomplete="new-password"
placeholder="Enter new password"
required
minlength={8}
bind:value={password}
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:outline-none focus:ring-2 dark:border-slate-600 dark:bg-slate-700 dark:text-white dark:placeholder-gray-400"
style="--tw-ring-color: #0ea5e980; border-color: #0ea5e9"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">At least 8 characters</p>
</div>
<div>
<label
for="confirmPassword"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
>Confirm Password</label
>
<input
type="password"
id="confirmPassword"
autocomplete="new-password"
placeholder="Confirm password"
required
minlength={8}
bind:value={confirmPassword}
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:outline-none focus:ring-2 dark:border-slate-600 dark:bg-slate-700 dark:text-white dark:placeholder-gray-400"
/>
</div>
<button
type="submit"
disabled={loading}
class="w-full rounded-lg px-4 py-3 font-medium text-white transition-colors disabled:cursor-not-allowed disabled:opacity-50"
style="background-color: #0ea5e9"
>
{loading ? 'Updating password...' : 'Update Password'}
</button>
</div>
</form>
</div>
{:else}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<div class="text-center">
<div class="mb-4 text-6xl">&#x26A0;&#xFE0F;</div>
<p class="mb-6 text-gray-600 dark:text-gray-400">
This password reset link is invalid or has expired.
</p>
<a
href="/forgot-password"
class="inline-block rounded-lg px-6 py-3 font-medium text-white transition-colors"
style="background-color: #0ea5e9"
>
Request a new link
</a>
</div>
</div>
{/if}
</div>
</main>
</div>

View file

@ -126,6 +126,22 @@ export const authStore = {
return authService.forgotPassword(email);
},
/**
* Reset password with token (from reset email link)
*/
async resetPasswordWithToken(token: string, newPassword: string) {
try {
const result = await authService.resetPassword(token, newPassword);
if (!result.success) {
return { success: false, error: result.error || 'Failed to reset password' };
}
return { success: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return { success: false, error: errorMessage };
}
},
/**
* Resend verification email
*/

View file

@ -0,0 +1,174 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { ManaDeckLogo } from '@manacore/shared-branding';
import { authStore } from '$lib/stores/auth.svelte';
let loading = $state(false);
let hasToken = $state(false);
let token = $state<string | null>(null);
let password = $state('');
let confirmPassword = $state('');
let error = $state<string | null>(null);
let success = $state(false);
onMount(() => {
token = $page.url.searchParams.get('token');
hasToken = !!token;
});
async function handleSubmit(e: Event) {
e.preventDefault();
error = null;
if (!token) {
error = 'Invalid or missing token';
return;
}
if (password !== confirmPassword) {
error = 'Passwords do not match';
return;
}
if (password.length < 8) {
error = 'Password must be at least 8 characters';
return;
}
loading = true;
try {
const result = await authStore.resetPasswordWithToken(token, password);
if (!result.success) {
error = result.error || 'Failed to reset password';
} else {
success = true;
setTimeout(() => goto('/login'), 3000);
}
} catch (err) {
error = err instanceof Error ? err.message : 'An error occurred';
} finally {
loading = false;
}
}
</script>
<svelte:head>
<title>Reset Password - ManaDeck</title>
</svelte:head>
<div
class="flex min-h-screen flex-col bg-gradient-to-b from-violet-100 to-white dark:from-slate-900 dark:to-slate-800"
>
<header class="flex items-center justify-between p-4">
<a href="/" class="flex items-center gap-2">
<ManaDeckLogo class="h-8 w-8" />
<span class="text-xl font-semibold" style="color: #8b5cf6">ManaDeck</span>
</a>
</header>
<main class="flex flex-1 items-center justify-center p-4">
<div class="w-full max-w-md">
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Reset Password</h1>
<p class="mt-2 text-gray-600 dark:text-gray-400">
{#if success}Password reset successfully
{:else if hasToken}Enter your new password
{:else}Invalid or missing token{/if}
</p>
</div>
{#if success}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<div class="text-center">
<div class="mb-4 text-6xl">&#x2705;</div>
<p class="mb-6 text-gray-600 dark:text-gray-400">
Your password has been reset successfully. You will be redirected to the login page
shortly.
</p>
<a
href="/login"
class="inline-block rounded-lg px-6 py-3 font-medium text-white transition-colors"
style="background-color: #8b5cf6"
>
Go to login
</a>
</div>
</div>
{:else if hasToken}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<form onsubmit={handleSubmit}>
{#if error}
<div
class="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-400"
>
{error}
</div>
{/if}
<div class="space-y-4">
<div>
<label
for="password"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
>New Password</label
>
<input
type="password"
id="password"
autocomplete="new-password"
placeholder="Enter new password"
required
minlength={8}
bind:value={password}
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:outline-none focus:ring-2 dark:border-slate-600 dark:bg-slate-700 dark:text-white dark:placeholder-gray-400"
style="--tw-ring-color: #8b5cf680; border-color: #8b5cf6"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">At least 8 characters</p>
</div>
<div>
<label
for="confirmPassword"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
>Confirm Password</label
>
<input
type="password"
id="confirmPassword"
autocomplete="new-password"
placeholder="Confirm password"
required
minlength={8}
bind:value={confirmPassword}
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:outline-none focus:ring-2 dark:border-slate-600 dark:bg-slate-700 dark:text-white dark:placeholder-gray-400"
/>
</div>
<button
type="submit"
disabled={loading}
class="w-full rounded-lg px-4 py-3 font-medium text-white transition-colors disabled:cursor-not-allowed disabled:opacity-50"
style="background-color: #8b5cf6"
>
{loading ? 'Updating password...' : 'Update Password'}
</button>
</div>
</form>
</div>
{:else}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<div class="text-center">
<div class="mb-4 text-6xl">&#x26A0;&#xFE0F;</div>
<p class="mb-6 text-gray-600 dark:text-gray-400">
This password reset link is invalid or has expired.
</p>
<a
href="/forgot-password"
class="inline-block rounded-lg px-6 py-3 font-medium text-white transition-colors"
style="background-color: #8b5cf6"
>
Request a new link
</a>
</div>
</div>
{/if}
</div>
</main>
</div>

View file

@ -215,6 +215,27 @@ export const authStore = {
}
},
/**
* Reset password with token (from reset email link)
*/
async resetPasswordWithToken(token: string, newPassword: string) {
const authService = getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server' };
}
try {
const result = await authService.resetPassword(token, newPassword);
if (!result.success) {
return { success: false, error: result.error || 'Failed to reset password' };
}
return { success: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return { success: false, error: errorMessage };
}
},
/**
* Resend verification email
*/

View file

@ -0,0 +1,174 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { NutriPhiLogo } from '@manacore/shared-branding';
import { authStore } from '$lib/stores/auth.svelte';
let loading = $state(false);
let hasToken = $state(false);
let token = $state<string | null>(null);
let password = $state('');
let confirmPassword = $state('');
let error = $state<string | null>(null);
let success = $state(false);
onMount(() => {
token = $page.url.searchParams.get('token');
hasToken = !!token;
});
async function handleSubmit(e: Event) {
e.preventDefault();
error = null;
if (!token) {
error = 'Invalid or missing token';
return;
}
if (password !== confirmPassword) {
error = 'Passwords do not match';
return;
}
if (password.length < 8) {
error = 'Password must be at least 8 characters';
return;
}
loading = true;
try {
const result = await authStore.resetPasswordWithToken(token, password);
if (!result.success) {
error = result.error || 'Failed to reset password';
} else {
success = true;
setTimeout(() => goto('/login'), 3000);
}
} catch (err) {
error = err instanceof Error ? err.message : 'An error occurred';
} finally {
loading = false;
}
}
</script>
<svelte:head>
<title>Reset Password - NutriPhi</title>
</svelte:head>
<div
class="flex min-h-screen flex-col bg-gradient-to-b from-green-100 to-white dark:from-green-950 dark:to-slate-800"
>
<header class="flex items-center justify-between p-4">
<a href="/" class="flex items-center gap-2">
<NutriPhiLogo class="h-8 w-8" />
<span class="text-xl font-semibold" style="color: #22C55E">NutriPhi</span>
</a>
</header>
<main class="flex flex-1 items-center justify-center p-4">
<div class="w-full max-w-md">
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Reset Password</h1>
<p class="mt-2 text-gray-600 dark:text-gray-400">
{#if success}Password reset successfully
{:else if hasToken}Enter your new password
{:else}Invalid or missing token{/if}
</p>
</div>
{#if success}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<div class="text-center">
<div class="mb-4 text-6xl">&#x2705;</div>
<p class="mb-6 text-gray-600 dark:text-gray-400">
Your password has been reset successfully. You will be redirected to the login page
shortly.
</p>
<a
href="/login"
class="inline-block rounded-lg px-6 py-3 font-medium text-white transition-colors"
style="background-color: #22C55E"
>
Go to login
</a>
</div>
</div>
{:else if hasToken}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<form onsubmit={handleSubmit}>
{#if error}
<div
class="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-400"
>
{error}
</div>
{/if}
<div class="space-y-4">
<div>
<label
for="password"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
>New Password</label
>
<input
type="password"
id="password"
autocomplete="new-password"
placeholder="Enter new password"
required
minlength={8}
bind:value={password}
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:outline-none focus:ring-2 dark:border-slate-600 dark:bg-slate-700 dark:text-white dark:placeholder-gray-400"
style="--tw-ring-color: #22C55E80; border-color: #22C55E"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">At least 8 characters</p>
</div>
<div>
<label
for="confirmPassword"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
>Confirm Password</label
>
<input
type="password"
id="confirmPassword"
autocomplete="new-password"
placeholder="Confirm password"
required
minlength={8}
bind:value={confirmPassword}
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:outline-none focus:ring-2 dark:border-slate-600 dark:bg-slate-700 dark:text-white dark:placeholder-gray-400"
/>
</div>
<button
type="submit"
disabled={loading}
class="w-full rounded-lg px-4 py-3 font-medium text-white transition-colors disabled:cursor-not-allowed disabled:opacity-50"
style="background-color: #22C55E"
>
{loading ? 'Updating password...' : 'Update Password'}
</button>
</div>
</form>
</div>
{:else}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<div class="text-center">
<div class="mb-4 text-6xl">&#x26A0;&#xFE0F;</div>
<p class="mb-6 text-gray-600 dark:text-gray-400">
This password reset link is invalid or has expired.
</p>
<a
href="/forgot-password"
class="inline-block rounded-lg px-6 py-3 font-medium text-white transition-colors"
style="background-color: #22C55E"
>
Request a new link
</a>
</div>
</div>
{/if}
</div>
</main>
</div>

View file

@ -190,6 +190,27 @@ export const authStore = {
}
},
/**
* Reset password with token (from reset email link)
*/
async resetPasswordWithToken(token: string, newPassword: string) {
const authService = getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server' };
}
try {
const result = await authService.resetPassword(token, newPassword);
if (!result.success) {
return { success: false, error: result.error || 'Failed to reset password' };
}
return { success: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return { success: false, error: errorMessage };
}
},
async resendVerificationEmail(email: string) {
const authService = getAuthService();
if (!authService) {

View file

@ -0,0 +1,29 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { ForgotPasswordPage } from '@manacore/shared-auth-ui';
import { getForgotPasswordTranslations } from '@manacore/shared-i18n';
import { ManaCoreLogo } from '@manacore/shared-branding';
import { authStore } from '$lib/stores/auth.svelte';
const translations = getForgotPasswordTranslations('en');
async function handleForgotPassword(email: string) {
return authStore.resetPassword(email);
}
</script>
<svelte:head>
<title>Forgot Password - Photos</title>
</svelte:head>
<ForgotPasswordPage
appName="Photos"
logo={ManaCoreLogo}
primaryColor="#8b5cf6"
onForgotPassword={handleForgotPassword}
{goto}
loginPath="/login"
lightBackground="#faf5ff"
darkBackground="#1e1b4b"
{translations}
/>

View file

@ -0,0 +1,174 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { ManaCoreLogo } from '@manacore/shared-branding';
import { authStore } from '$lib/stores/auth.svelte';
let loading = $state(false);
let hasToken = $state(false);
let token = $state<string | null>(null);
let password = $state('');
let confirmPassword = $state('');
let error = $state<string | null>(null);
let success = $state(false);
onMount(() => {
token = $page.url.searchParams.get('token');
hasToken = !!token;
});
async function handleSubmit(e: Event) {
e.preventDefault();
error = null;
if (!token) {
error = 'Invalid or missing token';
return;
}
if (password !== confirmPassword) {
error = 'Passwords do not match';
return;
}
if (password.length < 8) {
error = 'Password must be at least 8 characters';
return;
}
loading = true;
try {
const result = await authStore.resetPasswordWithToken(token, password);
if (!result.success) {
error = result.error || 'Failed to reset password';
} else {
success = true;
setTimeout(() => goto('/login'), 3000);
}
} catch (err) {
error = err instanceof Error ? err.message : 'An error occurred';
} finally {
loading = false;
}
}
</script>
<svelte:head>
<title>Reset Password - Photos</title>
</svelte:head>
<div
class="flex min-h-screen flex-col bg-gradient-to-b from-violet-100 to-white dark:from-slate-900 dark:to-slate-800"
>
<header class="flex items-center justify-between p-4">
<a href="/" class="flex items-center gap-2">
<ManaCoreLogo class="h-8 w-8" />
<span class="text-xl font-semibold" style="color: #8b5cf6">Photos</span>
</a>
</header>
<main class="flex flex-1 items-center justify-center p-4">
<div class="w-full max-w-md">
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Reset Password</h1>
<p class="mt-2 text-gray-600 dark:text-gray-400">
{#if success}Password reset successfully
{:else if hasToken}Enter your new password
{:else}Invalid or missing token{/if}
</p>
</div>
{#if success}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<div class="text-center">
<div class="mb-4 text-6xl">&#x2705;</div>
<p class="mb-6 text-gray-600 dark:text-gray-400">
Your password has been reset successfully. You will be redirected to the login page
shortly.
</p>
<a
href="/login"
class="inline-block rounded-lg px-6 py-3 font-medium text-white transition-colors"
style="background-color: #8b5cf6"
>
Go to login
</a>
</div>
</div>
{:else if hasToken}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<form onsubmit={handleSubmit}>
{#if error}
<div
class="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-400"
>
{error}
</div>
{/if}
<div class="space-y-4">
<div>
<label
for="password"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
>New Password</label
>
<input
type="password"
id="password"
autocomplete="new-password"
placeholder="Enter new password"
required
minlength={8}
bind:value={password}
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:outline-none focus:ring-2 dark:border-slate-600 dark:bg-slate-700 dark:text-white dark:placeholder-gray-400"
style="--tw-ring-color: #8b5cf680; border-color: #8b5cf6"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">At least 8 characters</p>
</div>
<div>
<label
for="confirmPassword"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
>Confirm Password</label
>
<input
type="password"
id="confirmPassword"
autocomplete="new-password"
placeholder="Confirm password"
required
minlength={8}
bind:value={confirmPassword}
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:outline-none focus:ring-2 dark:border-slate-600 dark:bg-slate-700 dark:text-white dark:placeholder-gray-400"
/>
</div>
<button
type="submit"
disabled={loading}
class="w-full rounded-lg px-4 py-3 font-medium text-white transition-colors disabled:cursor-not-allowed disabled:opacity-50"
style="background-color: #8b5cf6"
>
{loading ? 'Updating password...' : 'Update Password'}
</button>
</div>
</form>
</div>
{:else}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<div class="text-center">
<div class="mb-4 text-6xl">&#x26A0;&#xFE0F;</div>
<p class="mb-6 text-gray-600 dark:text-gray-400">
This password reset link is invalid or has expired.
</p>
<a
href="/forgot-password"
class="inline-block rounded-lg px-6 py-3 font-medium text-white transition-colors"
style="background-color: #8b5cf6"
>
Request a new link
</a>
</div>
</div>
{/if}
</div>
</main>
</div>

View file

@ -177,6 +177,50 @@ export const authStore = {
}
},
/**
* Send password reset email
*/
async resetPassword(email: string) {
const authService = getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server' };
}
try {
const result = await authService.forgotPassword(email);
if (!result.success) {
return { success: false, error: result.error || 'Password reset failed' };
}
return { success: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return { success: false, error: errorMessage };
}
},
/**
* Reset password with token (from reset email link)
*/
async resetPasswordWithToken(token: string, newPassword: string) {
const authService = getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server' };
}
try {
const result = await authService.resetPassword(token, newPassword);
if (!result.success) {
return { success: false, error: result.error || 'Failed to reset password' };
}
return { success: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return { success: false, error: errorMessage };
}
},
async getValidToken(): Promise<string | null> {
const tokenManager = getTokenManager();
if (!tokenManager) {

View file

@ -0,0 +1,174 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { PlantaLogo } from '@manacore/shared-branding';
import { authStore } from '$lib/stores/auth.svelte';
let loading = $state(false);
let hasToken = $state(false);
let token = $state<string | null>(null);
let password = $state('');
let confirmPassword = $state('');
let error = $state<string | null>(null);
let success = $state(false);
onMount(() => {
token = $page.url.searchParams.get('token');
hasToken = !!token;
});
async function handleSubmit(e: Event) {
e.preventDefault();
error = null;
if (!token) {
error = 'Invalid or missing token';
return;
}
if (password !== confirmPassword) {
error = 'Passwords do not match';
return;
}
if (password.length < 8) {
error = 'Password must be at least 8 characters';
return;
}
loading = true;
try {
const result = await authStore.resetPasswordWithToken(token, password);
if (!result.success) {
error = result.error || 'Failed to reset password';
} else {
success = true;
setTimeout(() => goto('/login'), 3000);
}
} catch (err) {
error = err instanceof Error ? err.message : 'An error occurred';
} finally {
loading = false;
}
}
</script>
<svelte:head>
<title>Reset Password - Planta</title>
</svelte:head>
<div
class="flex min-h-screen flex-col bg-gradient-to-b from-green-100 to-white dark:from-green-950 dark:to-slate-800"
>
<header class="flex items-center justify-between p-4">
<a href="/" class="flex items-center gap-2">
<PlantaLogo class="h-8 w-8" />
<span class="text-xl font-semibold" style="color: #22c55e">Planta</span>
</a>
</header>
<main class="flex flex-1 items-center justify-center p-4">
<div class="w-full max-w-md">
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Reset Password</h1>
<p class="mt-2 text-gray-600 dark:text-gray-400">
{#if success}Password reset successfully
{:else if hasToken}Enter your new password
{:else}Invalid or missing token{/if}
</p>
</div>
{#if success}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<div class="text-center">
<div class="mb-4 text-6xl">&#x2705;</div>
<p class="mb-6 text-gray-600 dark:text-gray-400">
Your password has been reset successfully. You will be redirected to the login page
shortly.
</p>
<a
href="/login"
class="inline-block rounded-lg px-6 py-3 font-medium text-white transition-colors"
style="background-color: #22c55e"
>
Go to login
</a>
</div>
</div>
{:else if hasToken}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<form onsubmit={handleSubmit}>
{#if error}
<div
class="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-400"
>
{error}
</div>
{/if}
<div class="space-y-4">
<div>
<label
for="password"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
>New Password</label
>
<input
type="password"
id="password"
autocomplete="new-password"
placeholder="Enter new password"
required
minlength={8}
bind:value={password}
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:outline-none focus:ring-2 dark:border-slate-600 dark:bg-slate-700 dark:text-white dark:placeholder-gray-400"
style="--tw-ring-color: #22c55e80; border-color: #22c55e"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">At least 8 characters</p>
</div>
<div>
<label
for="confirmPassword"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
>Confirm Password</label
>
<input
type="password"
id="confirmPassword"
autocomplete="new-password"
placeholder="Confirm password"
required
minlength={8}
bind:value={confirmPassword}
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:outline-none focus:ring-2 dark:border-slate-600 dark:bg-slate-700 dark:text-white dark:placeholder-gray-400"
/>
</div>
<button
type="submit"
disabled={loading}
class="w-full rounded-lg px-4 py-3 font-medium text-white transition-colors disabled:cursor-not-allowed disabled:opacity-50"
style="background-color: #22c55e"
>
{loading ? 'Updating password...' : 'Update Password'}
</button>
</div>
</form>
</div>
{:else}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<div class="text-center">
<div class="mb-4 text-6xl">&#x26A0;&#xFE0F;</div>
<p class="mb-6 text-gray-600 dark:text-gray-400">
This password reset link is invalid or has expired.
</p>
<a
href="/forgot-password"
class="inline-block rounded-lg px-6 py-3 font-medium text-white transition-colors"
style="background-color: #22c55e"
>
Request a new link
</a>
</div>
</div>
{/if}
</div>
</main>
</div>

View file

@ -193,6 +193,27 @@ export const auth = {
}
},
/**
* Reset password with token (from reset email link)
*/
async resetPasswordWithToken(token: string, newPassword: string) {
const authService = getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server' };
}
try {
const result = await authService.resetPassword(token, newPassword);
if (!result.success) {
return { success: false, error: result.error || 'Failed to reset password' };
}
return { success: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return { success: false, error: errorMessage };
}
},
/**
* Get access token for API calls
*/

View file

@ -0,0 +1,174 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { PresiLogo } from '@manacore/shared-branding';
import { auth } from '$lib/stores/auth.svelte';
let loading = $state(false);
let hasToken = $state(false);
let token = $state<string | null>(null);
let password = $state('');
let confirmPassword = $state('');
let error = $state<string | null>(null);
let success = $state(false);
onMount(() => {
token = $page.url.searchParams.get('token');
hasToken = !!token;
});
async function handleSubmit(e: Event) {
e.preventDefault();
error = null;
if (!token) {
error = 'Invalid or missing token';
return;
}
if (password !== confirmPassword) {
error = 'Passwords do not match';
return;
}
if (password.length < 8) {
error = 'Password must be at least 8 characters';
return;
}
loading = true;
try {
const result = await auth.resetPasswordWithToken(token, password);
if (!result.success) {
error = result.error || 'Failed to reset password';
} else {
success = true;
setTimeout(() => goto('/login'), 3000);
}
} catch (err) {
error = err instanceof Error ? err.message : 'An error occurred';
} finally {
loading = false;
}
}
</script>
<svelte:head>
<title>Reset Password - Presi</title>
</svelte:head>
<div
class="flex min-h-screen flex-col bg-gradient-to-b from-orange-100 to-white dark:from-stone-900 dark:to-stone-800"
>
<header class="flex items-center justify-between p-4">
<a href="/" class="flex items-center gap-2">
<PresiLogo class="h-8 w-8" />
<span class="text-xl font-semibold" style="color: #f97316">Presi</span>
</a>
</header>
<main class="flex flex-1 items-center justify-center p-4">
<div class="w-full max-w-md">
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Reset Password</h1>
<p class="mt-2 text-gray-600 dark:text-gray-400">
{#if success}Password reset successfully
{:else if hasToken}Enter your new password
{:else}Invalid or missing token{/if}
</p>
</div>
{#if success}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<div class="text-center">
<div class="mb-4 text-6xl">&#x2705;</div>
<p class="mb-6 text-gray-600 dark:text-gray-400">
Your password has been reset successfully. You will be redirected to the login page
shortly.
</p>
<a
href="/login"
class="inline-block rounded-lg px-6 py-3 font-medium text-white transition-colors"
style="background-color: #f97316"
>
Go to login
</a>
</div>
</div>
{:else if hasToken}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<form onsubmit={handleSubmit}>
{#if error}
<div
class="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-400"
>
{error}
</div>
{/if}
<div class="space-y-4">
<div>
<label
for="password"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
>New Password</label
>
<input
type="password"
id="password"
autocomplete="new-password"
placeholder="Enter new password"
required
minlength={8}
bind:value={password}
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:outline-none focus:ring-2 dark:border-slate-600 dark:bg-slate-700 dark:text-white dark:placeholder-gray-400"
style="--tw-ring-color: #f9731680; border-color: #f97316"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">At least 8 characters</p>
</div>
<div>
<label
for="confirmPassword"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
>Confirm Password</label
>
<input
type="password"
id="confirmPassword"
autocomplete="new-password"
placeholder="Confirm password"
required
minlength={8}
bind:value={confirmPassword}
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:outline-none focus:ring-2 dark:border-slate-600 dark:bg-slate-700 dark:text-white dark:placeholder-gray-400"
/>
</div>
<button
type="submit"
disabled={loading}
class="w-full rounded-lg px-4 py-3 font-medium text-white transition-colors disabled:cursor-not-allowed disabled:opacity-50"
style="background-color: #f97316"
>
{loading ? 'Updating password...' : 'Update Password'}
</button>
</div>
</form>
</div>
{:else}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<div class="text-center">
<div class="mb-4 text-6xl">&#x26A0;&#xFE0F;</div>
<p class="mb-6 text-gray-600 dark:text-gray-400">
This password reset link is invalid or has expired.
</p>
<a
href="/forgot-password"
class="inline-block rounded-lg px-6 py-3 font-medium text-white transition-colors"
style="background-color: #f97316"
>
Request a new link
</a>
</div>
</div>
{/if}
</div>
</main>
</div>

View file

@ -192,6 +192,27 @@ export const authStore = {
}
},
/**
* Reset password with token (from reset email link)
*/
async resetPasswordWithToken(token: string, newPassword: string) {
const authService = getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server' };
}
try {
const result = await authService.resetPassword(token, newPassword);
if (!result.success) {
return { success: false, error: result.error || 'Failed to reset password' };
}
return { success: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return { success: false, error: errorMessage };
}
},
async getValidToken(): Promise<string | null> {
const tokenManager = getTokenManager();
if (!tokenManager) {

View file

@ -0,0 +1,174 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { QuestionsLogo } from '@manacore/shared-branding';
import { authStore } from '$lib/stores/auth.svelte';
let loading = $state(false);
let hasToken = $state(false);
let token = $state<string | null>(null);
let password = $state('');
let confirmPassword = $state('');
let error = $state<string | null>(null);
let success = $state(false);
onMount(() => {
token = $page.url.searchParams.get('token');
hasToken = !!token;
});
async function handleSubmit(e: Event) {
e.preventDefault();
error = null;
if (!token) {
error = 'Invalid or missing token';
return;
}
if (password !== confirmPassword) {
error = 'Passwords do not match';
return;
}
if (password.length < 8) {
error = 'Password must be at least 8 characters';
return;
}
loading = true;
try {
const result = await authStore.resetPasswordWithToken(token, password);
if (!result.success) {
error = result.error || 'Failed to reset password';
} else {
success = true;
setTimeout(() => goto('/login'), 3000);
}
} catch (err) {
error = err instanceof Error ? err.message : 'An error occurred';
} finally {
loading = false;
}
}
</script>
<svelte:head>
<title>Reset Password - Questions</title>
</svelte:head>
<div
class="flex min-h-screen flex-col bg-gradient-to-b from-violet-100 to-white dark:from-slate-900 dark:to-slate-800"
>
<header class="flex items-center justify-between p-4">
<a href="/" class="flex items-center gap-2">
<QuestionsLogo class="h-8 w-8" />
<span class="text-xl font-semibold" style="color: #8b5cf6">Questions</span>
</a>
</header>
<main class="flex flex-1 items-center justify-center p-4">
<div class="w-full max-w-md">
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Reset Password</h1>
<p class="mt-2 text-gray-600 dark:text-gray-400">
{#if success}Password reset successfully
{:else if hasToken}Enter your new password
{:else}Invalid or missing token{/if}
</p>
</div>
{#if success}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<div class="text-center">
<div class="mb-4 text-6xl">&#x2705;</div>
<p class="mb-6 text-gray-600 dark:text-gray-400">
Your password has been reset successfully. You will be redirected to the login page
shortly.
</p>
<a
href="/login"
class="inline-block rounded-lg px-6 py-3 font-medium text-white transition-colors"
style="background-color: #8b5cf6"
>
Go to login
</a>
</div>
</div>
{:else if hasToken}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<form onsubmit={handleSubmit}>
{#if error}
<div
class="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-400"
>
{error}
</div>
{/if}
<div class="space-y-4">
<div>
<label
for="password"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
>New Password</label
>
<input
type="password"
id="password"
autocomplete="new-password"
placeholder="Enter new password"
required
minlength={8}
bind:value={password}
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:outline-none focus:ring-2 dark:border-slate-600 dark:bg-slate-700 dark:text-white dark:placeholder-gray-400"
style="--tw-ring-color: #8b5cf680; border-color: #8b5cf6"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">At least 8 characters</p>
</div>
<div>
<label
for="confirmPassword"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
>Confirm Password</label
>
<input
type="password"
id="confirmPassword"
autocomplete="new-password"
placeholder="Confirm password"
required
minlength={8}
bind:value={confirmPassword}
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:outline-none focus:ring-2 dark:border-slate-600 dark:bg-slate-700 dark:text-white dark:placeholder-gray-400"
/>
</div>
<button
type="submit"
disabled={loading}
class="w-full rounded-lg px-4 py-3 font-medium text-white transition-colors disabled:cursor-not-allowed disabled:opacity-50"
style="background-color: #8b5cf6"
>
{loading ? 'Updating password...' : 'Update Password'}
</button>
</div>
</form>
</div>
{:else}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<div class="text-center">
<div class="mb-4 text-6xl">&#x26A0;&#xFE0F;</div>
<p class="mb-6 text-gray-600 dark:text-gray-400">
This password reset link is invalid or has expired.
</p>
<a
href="/forgot-password"
class="inline-block rounded-lg px-6 py-3 font-medium text-white transition-colors"
style="background-color: #8b5cf6"
>
Request a new link
</a>
</div>
</div>
{/if}
</div>
</main>
</div>

View file

@ -210,6 +210,27 @@ export const authStore = {
}
},
/**
* Reset password with token (from reset email link)
*/
async resetPasswordWithToken(token: string, newPassword: string) {
const authService = getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server' };
}
try {
const result = await authService.resetPassword(token, newPassword);
if (!result.success) {
return { success: false, error: result.error || 'Failed to reset password' };
}
return { success: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return { success: false, error: errorMessage };
}
},
async getAccessToken() {
const authService = getAuthService();
if (!authService) {

View file

@ -0,0 +1,174 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { SkillTreeLogo } from '@manacore/shared-branding';
import { authStore } from '$lib/stores/auth.svelte';
let loading = $state(false);
let hasToken = $state(false);
let token = $state<string | null>(null);
let password = $state('');
let confirmPassword = $state('');
let error = $state<string | null>(null);
let success = $state(false);
onMount(() => {
token = $page.url.searchParams.get('token');
hasToken = !!token;
});
async function handleSubmit(e: Event) {
e.preventDefault();
error = null;
if (!token) {
error = 'Invalid or missing token';
return;
}
if (password !== confirmPassword) {
error = 'Passwords do not match';
return;
}
if (password.length < 8) {
error = 'Password must be at least 8 characters';
return;
}
loading = true;
try {
const result = await authStore.resetPasswordWithToken(token, password);
if (!result.success) {
error = result.error || 'Failed to reset password';
} else {
success = true;
setTimeout(() => goto('/login'), 3000);
}
} catch (err) {
error = err instanceof Error ? err.message : 'An error occurred';
} finally {
loading = false;
}
}
</script>
<svelte:head>
<title>Reset Password - SkillTree</title>
</svelte:head>
<div
class="flex min-h-screen flex-col bg-gradient-to-b from-emerald-100 to-white dark:from-emerald-950 dark:to-slate-800"
>
<header class="flex items-center justify-between p-4">
<a href="/" class="flex items-center gap-2">
<SkillTreeLogo class="h-8 w-8" />
<span class="text-xl font-semibold" style="color: #10b981">SkillTree</span>
</a>
</header>
<main class="flex flex-1 items-center justify-center p-4">
<div class="w-full max-w-md">
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Reset Password</h1>
<p class="mt-2 text-gray-600 dark:text-gray-400">
{#if success}Password reset successfully
{:else if hasToken}Enter your new password
{:else}Invalid or missing token{/if}
</p>
</div>
{#if success}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<div class="text-center">
<div class="mb-4 text-6xl">&#x2705;</div>
<p class="mb-6 text-gray-600 dark:text-gray-400">
Your password has been reset successfully. You will be redirected to the login page
shortly.
</p>
<a
href="/login"
class="inline-block rounded-lg px-6 py-3 font-medium text-white transition-colors"
style="background-color: #10b981"
>
Go to login
</a>
</div>
</div>
{:else if hasToken}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<form onsubmit={handleSubmit}>
{#if error}
<div
class="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-400"
>
{error}
</div>
{/if}
<div class="space-y-4">
<div>
<label
for="password"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
>New Password</label
>
<input
type="password"
id="password"
autocomplete="new-password"
placeholder="Enter new password"
required
minlength={8}
bind:value={password}
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:outline-none focus:ring-2 dark:border-slate-600 dark:bg-slate-700 dark:text-white dark:placeholder-gray-400"
style="--tw-ring-color: #10b98180; border-color: #10b981"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">At least 8 characters</p>
</div>
<div>
<label
for="confirmPassword"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
>Confirm Password</label
>
<input
type="password"
id="confirmPassword"
autocomplete="new-password"
placeholder="Confirm password"
required
minlength={8}
bind:value={confirmPassword}
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:outline-none focus:ring-2 dark:border-slate-600 dark:bg-slate-700 dark:text-white dark:placeholder-gray-400"
/>
</div>
<button
type="submit"
disabled={loading}
class="w-full rounded-lg px-4 py-3 font-medium text-white transition-colors disabled:cursor-not-allowed disabled:opacity-50"
style="background-color: #10b981"
>
{loading ? 'Updating password...' : 'Update Password'}
</button>
</div>
</form>
</div>
{:else}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<div class="text-center">
<div class="mb-4 text-6xl">&#x26A0;&#xFE0F;</div>
<p class="mb-6 text-gray-600 dark:text-gray-400">
This password reset link is invalid or has expired.
</p>
<a
href="/forgot-password"
class="inline-block rounded-lg px-6 py-3 font-medium text-white transition-colors"
style="background-color: #10b981"
>
Request a new link
</a>
</div>
</div>
{/if}
</div>
</main>
</div>

View file

@ -240,6 +240,27 @@ export const authStore = {
}
},
/**
* Reset password with token (from reset email link)
*/
async resetPasswordWithToken(token: string, newPassword: string) {
const authService = getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server' };
}
try {
const result = await authService.resetPassword(token, newPassword);
if (!result.success) {
return { success: false, error: result.error || 'Failed to reset password' };
}
return { success: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return { success: false, error: errorMessage };
}
},
/**
* Get access token for API calls (raw token, no refresh)
* @deprecated Use getValidToken() instead for automatic refresh

View file

@ -0,0 +1,174 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { TodoLogo } from '@manacore/shared-branding';
import { authStore } from '$lib/stores/auth.svelte';
let loading = $state(false);
let hasToken = $state(false);
let token = $state<string | null>(null);
let password = $state('');
let confirmPassword = $state('');
let error = $state<string | null>(null);
let success = $state(false);
onMount(() => {
token = $page.url.searchParams.get('token');
hasToken = !!token;
});
async function handleSubmit(e: Event) {
e.preventDefault();
error = null;
if (!token) {
error = 'Invalid or missing token';
return;
}
if (password !== confirmPassword) {
error = 'Passwords do not match';
return;
}
if (password.length < 8) {
error = 'Password must be at least 8 characters';
return;
}
loading = true;
try {
const result = await authStore.resetPasswordWithToken(token, password);
if (!result.success) {
error = result.error || 'Failed to reset password';
} else {
success = true;
setTimeout(() => goto('/login'), 3000);
}
} catch (err) {
error = err instanceof Error ? err.message : 'An error occurred';
} finally {
loading = false;
}
}
</script>
<svelte:head>
<title>Reset Password - Todo</title>
</svelte:head>
<div
class="flex min-h-screen flex-col bg-gradient-to-b from-violet-100 to-white dark:from-slate-900 dark:to-slate-800"
>
<header class="flex items-center justify-between p-4">
<a href="/" class="flex items-center gap-2">
<TodoLogo class="h-8 w-8" />
<span class="text-xl font-semibold" style="color: #8b5cf6">Todo</span>
</a>
</header>
<main class="flex flex-1 items-center justify-center p-4">
<div class="w-full max-w-md">
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Reset Password</h1>
<p class="mt-2 text-gray-600 dark:text-gray-400">
{#if success}Password reset successfully
{:else if hasToken}Enter your new password
{:else}Invalid or missing token{/if}
</p>
</div>
{#if success}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<div class="text-center">
<div class="mb-4 text-6xl">&#x2705;</div>
<p class="mb-6 text-gray-600 dark:text-gray-400">
Your password has been reset successfully. You will be redirected to the login page
shortly.
</p>
<a
href="/login"
class="inline-block rounded-lg px-6 py-3 font-medium text-white transition-colors"
style="background-color: #8b5cf6"
>
Go to login
</a>
</div>
</div>
{:else if hasToken}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<form onsubmit={handleSubmit}>
{#if error}
<div
class="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-400"
>
{error}
</div>
{/if}
<div class="space-y-4">
<div>
<label
for="password"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
>New Password</label
>
<input
type="password"
id="password"
autocomplete="new-password"
placeholder="Enter new password"
required
minlength={8}
bind:value={password}
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:outline-none focus:ring-2 dark:border-slate-600 dark:bg-slate-700 dark:text-white dark:placeholder-gray-400"
style="--tw-ring-color: #8b5cf680; border-color: #8b5cf6"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">At least 8 characters</p>
</div>
<div>
<label
for="confirmPassword"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
>Confirm Password</label
>
<input
type="password"
id="confirmPassword"
autocomplete="new-password"
placeholder="Confirm password"
required
minlength={8}
bind:value={confirmPassword}
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:outline-none focus:ring-2 dark:border-slate-600 dark:bg-slate-700 dark:text-white dark:placeholder-gray-400"
/>
</div>
<button
type="submit"
disabled={loading}
class="w-full rounded-lg px-4 py-3 font-medium text-white transition-colors disabled:cursor-not-allowed disabled:opacity-50"
style="background-color: #8b5cf6"
>
{loading ? 'Updating password...' : 'Update Password'}
</button>
</div>
</form>
</div>
{:else}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<div class="text-center">
<div class="mb-4 text-6xl">&#x26A0;&#xFE0F;</div>
<p class="mb-6 text-gray-600 dark:text-gray-400">
This password reset link is invalid or has expired.
</p>
<a
href="/forgot-password"
class="inline-block rounded-lg px-6 py-3 font-medium text-white transition-colors"
style="background-color: #8b5cf6"
>
Request a new link
</a>
</div>
</div>
{/if}
</div>
</main>
</div>

View file

@ -218,6 +218,27 @@ export const authStore = {
}
},
/**
* Reset password with token (from reset email link)
*/
async resetPasswordWithToken(token: string, newPassword: string) {
const authService = getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server' };
}
try {
const result = await authService.resetPassword(token, newPassword);
if (!result.success) {
return { success: false, error: result.error || 'Failed to reset password' };
}
return { success: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return { success: false, error: errorMessage };
}
},
/**
* Get access token for API calls (raw token, no refresh)
* @deprecated Use getValidToken() instead for automatic refresh

View file

@ -0,0 +1,174 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { ZitareLogo } from '@manacore/shared-branding';
import { authStore } from '$lib/stores/auth.svelte';
let loading = $state(false);
let hasToken = $state(false);
let token = $state<string | null>(null);
let password = $state('');
let confirmPassword = $state('');
let error = $state<string | null>(null);
let success = $state(false);
onMount(() => {
token = $page.url.searchParams.get('token');
hasToken = !!token;
});
async function handleSubmit(e: Event) {
e.preventDefault();
error = null;
if (!token) {
error = 'Invalid or missing token';
return;
}
if (password !== confirmPassword) {
error = 'Passwords do not match';
return;
}
if (password.length < 8) {
error = 'Password must be at least 8 characters';
return;
}
loading = true;
try {
const result = await authStore.resetPasswordWithToken(token, password);
if (!result.success) {
error = result.error || 'Failed to reset password';
} else {
success = true;
setTimeout(() => goto('/login'), 3000);
}
} catch (err) {
error = err instanceof Error ? err.message : 'An error occurred';
} finally {
loading = false;
}
}
</script>
<svelte:head>
<title>Reset Password - Zitare</title>
</svelte:head>
<div
class="flex min-h-screen flex-col bg-gradient-to-b from-amber-100 to-white dark:from-stone-900 dark:to-stone-800"
>
<header class="flex items-center justify-between p-4">
<a href="/" class="flex items-center gap-2">
<ZitareLogo class="h-8 w-8" />
<span class="text-xl font-semibold" style="color: #f59e0b">Zitare</span>
</a>
</header>
<main class="flex flex-1 items-center justify-center p-4">
<div class="w-full max-w-md">
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Reset Password</h1>
<p class="mt-2 text-gray-600 dark:text-gray-400">
{#if success}Password reset successfully
{:else if hasToken}Enter your new password
{:else}Invalid or missing token{/if}
</p>
</div>
{#if success}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<div class="text-center">
<div class="mb-4 text-6xl">&#x2705;</div>
<p class="mb-6 text-gray-600 dark:text-gray-400">
Your password has been reset successfully. You will be redirected to the login page
shortly.
</p>
<a
href="/login"
class="inline-block rounded-lg px-6 py-3 font-medium text-white transition-colors"
style="background-color: #f59e0b"
>
Go to login
</a>
</div>
</div>
{:else if hasToken}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<form onsubmit={handleSubmit}>
{#if error}
<div
class="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-800 dark:bg-red-900/20 dark:text-red-400"
>
{error}
</div>
{/if}
<div class="space-y-4">
<div>
<label
for="password"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
>New Password</label
>
<input
type="password"
id="password"
autocomplete="new-password"
placeholder="Enter new password"
required
minlength={8}
bind:value={password}
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:outline-none focus:ring-2 dark:border-slate-600 dark:bg-slate-700 dark:text-white dark:placeholder-gray-400"
style="--tw-ring-color: #f59e0b80; border-color: #f59e0b"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">At least 8 characters</p>
</div>
<div>
<label
for="confirmPassword"
class="mb-2 block text-sm font-medium text-gray-900 dark:text-gray-100"
>Confirm Password</label
>
<input
type="password"
id="confirmPassword"
autocomplete="new-password"
placeholder="Confirm password"
required
minlength={8}
bind:value={confirmPassword}
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 focus:outline-none focus:ring-2 dark:border-slate-600 dark:bg-slate-700 dark:text-white dark:placeholder-gray-400"
/>
</div>
<button
type="submit"
disabled={loading}
class="w-full rounded-lg px-4 py-3 font-medium text-white transition-colors disabled:cursor-not-allowed disabled:opacity-50"
style="background-color: #f59e0b"
>
{loading ? 'Updating password...' : 'Update Password'}
</button>
</div>
</form>
</div>
{:else}
<div class="rounded-xl bg-white p-8 shadow-lg dark:bg-slate-800">
<div class="text-center">
<div class="mb-4 text-6xl">&#x26A0;&#xFE0F;</div>
<p class="mb-6 text-gray-600 dark:text-gray-400">
This password reset link is invalid or has expired.
</p>
<a
href="/forgot-password"
class="inline-block rounded-lg px-6 py-3 font-medium text-white transition-colors"
style="background-color: #f59e0b"
>
Request a new link
</a>
</div>
</div>
{/if}
</div>
</main>
</div>