this repo has no description
1<script lang="ts">
2 import { navigate, routes, getFullUrl } from '../lib/router.svelte'
3 import { api, ApiError } from '../lib/api'
4 import { getAuthState } from '../lib/auth.svelte'
5 import { _ } from '../lib/i18n'
6 import type { Session } from '../lib/types/api'
7 import { unsafeAsEmail } from '../lib/types/branded'
8
9 const auth = $derived(getAuthState())
10
11 function getSession(): Session | null {
12 return auth.kind === 'authenticated' ? auth.session : null
13 }
14
15 const session = $derived(getSession())
16
17 let email = $state('')
18 let token = $state('')
19 let newPassword = $state('')
20 let confirmPassword = $state('')
21 let submitting = $state(false)
22 let error = $state<string | null>(null)
23 let success = $state<string | null>(null)
24 let tokenSent = $state(false)
25
26 $effect(() => {
27 if (session) {
28 navigate(routes.dashboard)
29 }
30 })
31
32 async function handleRequestReset(e: Event) {
33 e.preventDefault()
34 if (!email) return
35 submitting = true
36 error = null
37 success = null
38 try {
39 await api.requestPasswordReset(unsafeAsEmail(email))
40 tokenSent = true
41 success = $_('resetPassword.codeSent')
42 } catch (e) {
43 error = e instanceof ApiError ? e.message : 'Failed to send reset code'
44 } finally {
45 submitting = false
46 }
47 }
48
49 async function handleReset(e: Event) {
50 e.preventDefault()
51 if (!token || !newPassword || !confirmPassword) return
52 if (newPassword !== confirmPassword) {
53 error = $_('resetPassword.passwordsMismatch')
54 return
55 }
56 if (newPassword.length < 8) {
57 error = $_('resetPassword.passwordLength')
58 return
59 }
60 submitting = true
61 error = null
62 success = null
63 try {
64 await api.resetPassword(token, newPassword)
65 success = $_('resetPassword.success')
66 setTimeout(() => navigate(routes.login), 2000)
67 } catch (e) {
68 error = e instanceof ApiError ? e.message : 'Failed to reset password'
69 } finally {
70 submitting = false
71 }
72 }
73</script>
74
75<div class="reset-page">
76 {#if error}
77 <div class="message error">{error}</div>
78 {/if}
79 {#if success}
80 <div class="message success">{success}</div>
81 {/if}
82
83 {#if tokenSent}
84 <h1>{$_('resetPassword.title')}</h1>
85 <p class="subtitle">{$_('resetPassword.subtitle')}</p>
86
87 <form onsubmit={handleReset}>
88 <div class="field">
89 <label for="token">{$_('resetPassword.code')}</label>
90 <input
91 id="token"
92 type="text"
93 bind:value={token}
94 placeholder={$_('resetPassword.codePlaceholder')}
95 disabled={submitting}
96 required
97 />
98 </div>
99 <div class="field">
100 <label for="new-password">{$_('resetPassword.newPassword')}</label>
101 <input
102 id="new-password"
103 type="password"
104 bind:value={newPassword}
105 placeholder={$_('resetPassword.newPasswordPlaceholder')}
106 disabled={submitting}
107 required
108 minlength="8"
109 />
110 </div>
111 <div class="field">
112 <label for="confirm-password">{$_('resetPassword.confirmPassword')}</label>
113 <input
114 id="confirm-password"
115 type="password"
116 bind:value={confirmPassword}
117 placeholder={$_('resetPassword.confirmPasswordPlaceholder')}
118 disabled={submitting}
119 required
120 />
121 </div>
122 <button type="submit" disabled={submitting || !token || !newPassword || !confirmPassword}>
123 {submitting ? $_('resetPassword.resetting') : $_('resetPassword.resetButton')}
124 </button>
125 <button type="button" class="secondary" onclick={() => { tokenSent = false; token = ''; newPassword = ''; confirmPassword = '' }}>
126 {$_('resetPassword.requestNewCode')}
127 </button>
128 </form>
129 {:else}
130 <h1>{$_('resetPassword.forgotTitle')}</h1>
131 <p class="subtitle">{$_('resetPassword.forgotSubtitle')}</p>
132
133 <form onsubmit={handleRequestReset}>
134 <div class="field">
135 <label for="email">{$_('resetPassword.handleOrEmail')}</label>
136 <input
137 id="email"
138 type="text"
139 bind:value={email}
140 placeholder={$_('resetPassword.emailPlaceholder')}
141 disabled={submitting}
142 required
143 />
144 </div>
145 <button type="submit" disabled={submitting || !email}>
146 {submitting ? $_('resetPassword.sending') : $_('resetPassword.sendCode')}
147 </button>
148 </form>
149 {/if}
150
151 <p class="link-text">
152 <a href="/app/login">{$_('common.backToLogin')}</a>
153 </p>
154</div>
155
156<style>
157 .reset-page {
158 max-width: var(--width-sm);
159 margin: var(--space-9) auto;
160 padding: var(--space-7);
161 }
162
163 h1 {
164 margin: 0 0 var(--space-3) 0;
165 }
166
167 .subtitle {
168 color: var(--text-secondary);
169 margin: 0 0 var(--space-7) 0;
170 }
171
172 form {
173 display: flex;
174 flex-direction: column;
175 gap: var(--space-4);
176 }
177
178 .link-text {
179 text-align: center;
180 margin-top: var(--space-6);
181 color: var(--text-secondary);
182 }
183
184 .link-text a {
185 color: var(--accent);
186 }
187</style>