sveltekit-auth-example 5.5.0 → 5.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -1
- package/README.md +12 -12
- package/db_create.sh +13 -0
- package/db_create.sql +15 -376
- package/db_schema.sql +369 -0
- package/package.json +5 -2
- package/src/app.d.ts +22 -1
- package/src/hooks.server.ts +47 -12
- package/src/lib/app-state.svelte.ts +8 -0
- package/src/lib/auth-redirect.ts +4 -0
- package/src/lib/focus.ts +8 -0
- package/src/lib/google.ts +17 -0
- package/src/lib/server/db.ts +9 -0
- package/src/lib/server/email/index.ts +1 -0
- package/src/lib/server/email/mfa-code.ts +22 -0
- package/src/lib/server/email/password-reset.ts +6 -0
- package/src/lib/server/email/verify-email.ts +6 -0
- package/src/lib/server/sendgrid.ts +9 -0
- package/src/routes/+layout.server.ts +10 -1
- package/src/routes/+layout.svelte +103 -28
- package/src/routes/admin/+page.server.ts +8 -0
- package/src/routes/api/v1/user/+server.ts +20 -0
- package/src/routes/auth/[slug]/+server.ts +9 -2
- package/src/routes/auth/forgot/+server.ts +10 -0
- package/src/routes/auth/google/+server.ts +35 -4
- package/src/routes/auth/login/+server.ts +67 -10
- package/src/routes/auth/logout/+server.ts +10 -0
- package/src/routes/auth/mfa/+server.ts +75 -0
- package/src/routes/auth/register/+server.ts +21 -1
- package/src/routes/auth/reset/+server.ts +15 -0
- package/src/routes/auth/reset/[token]/+page.svelte +16 -8
- package/src/routes/auth/reset/[token]/+page.ts +8 -0
- package/src/routes/auth/verify/[token]/+server.ts +12 -1
- package/src/routes/forgot/+page.svelte +13 -8
- package/src/routes/layout.css +3 -3
- package/src/routes/login/+page.server.ts +8 -0
- package/src/routes/login/+page.svelte +222 -77
- package/src/routes/profile/+page.server.ts +9 -0
- package/src/routes/profile/+page.svelte +32 -12
- package/src/routes/register/+page.server.ts +8 -1
- package/src/routes/register/+page.svelte +160 -122
- package/src/routes/teachers/+page.server.ts +9 -0
- package/src/service-worker.ts +17 -1
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
|
|
26
26
|
let formEl: HTMLFormElement | undefined = $state()
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Validates the registration form and, if valid, delegates to {@link registerLocal}.
|
|
30
|
+
* Checks that passwords match before form validation runs.
|
|
31
|
+
*/
|
|
28
32
|
async function register() {
|
|
29
33
|
const form = formEl!
|
|
30
34
|
message = ''
|
|
@@ -57,6 +61,16 @@
|
|
|
57
61
|
focusedField?.focus()
|
|
58
62
|
})
|
|
59
63
|
|
|
64
|
+
/**
|
|
65
|
+
* POSTs the new user data to `/auth/register`.
|
|
66
|
+
*
|
|
67
|
+
* The server ignores the `role` field and always assigns the lowest privilege
|
|
68
|
+
* (`student`). On success with `emailVerification: true`, sets
|
|
69
|
+
* `emailVerificationSent` to show the confirmation message instead of the form.
|
|
70
|
+
*
|
|
71
|
+
* @param user - The user object collected from the registration form.
|
|
72
|
+
* @throws {Error} With a user-friendly message on HTTP errors.
|
|
73
|
+
*/
|
|
60
74
|
async function registerLocal(user: User) {
|
|
61
75
|
loading = true
|
|
62
76
|
try {
|
|
@@ -69,8 +83,7 @@
|
|
|
69
83
|
})
|
|
70
84
|
const fromEndpoint = await res.json()
|
|
71
85
|
if (!res.ok) {
|
|
72
|
-
if (res.status == 409)
|
|
73
|
-
throw new Error('Sorry, that email address is already in use.')
|
|
86
|
+
if (res.status == 409) throw new Error('Sorry, that email address is already in use.')
|
|
74
87
|
throw new Error(fromEndpoint.message || res.statusText)
|
|
75
88
|
}
|
|
76
89
|
if (fromEndpoint.emailVerification) {
|
|
@@ -87,6 +100,7 @@
|
|
|
87
100
|
}
|
|
88
101
|
}
|
|
89
102
|
|
|
103
|
+
/** Returns `true` if the password and confirm-password fields match. */
|
|
90
104
|
const passwordMatch = () => {
|
|
91
105
|
if (!user) return false // placate TypeScript
|
|
92
106
|
if (!user.password) user.password = ''
|
|
@@ -101,138 +115,162 @@
|
|
|
101
115
|
{#if emailVerificationSent}
|
|
102
116
|
<div class="tw:mx-auto tw:mt-20 tw:max-w-sm tw:space-y-4">
|
|
103
117
|
<h4>Check your email</h4>
|
|
104
|
-
<p>
|
|
118
|
+
<p>
|
|
119
|
+
We've sent a verification link to your email address. Please check your inbox (and junk
|
|
120
|
+
folder) to complete your registration.
|
|
121
|
+
</p>
|
|
105
122
|
<a href="/login" class="btn-primary tw:block tw:text-center tw:no-underline">Back to login</a>
|
|
106
123
|
</div>
|
|
107
124
|
{:else}
|
|
108
|
-
<form
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
125
|
+
<form
|
|
126
|
+
bind:this={formEl}
|
|
127
|
+
autocomplete="on"
|
|
128
|
+
novalidate
|
|
129
|
+
class="tw:mx-auto tw:my-8 tw:max-w-sm tw:space-y-4"
|
|
130
|
+
class:submitted
|
|
131
|
+
onsubmit={e => {
|
|
132
|
+
e.preventDefault()
|
|
133
|
+
register()
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
<h4>Register</h4>
|
|
117
137
|
<p>Welcome to our community.</p>
|
|
118
138
|
|
|
119
139
|
<div class="tw:relative tw:w-full">
|
|
120
140
|
<!-- Real Google button: invisible but receives clicks -->
|
|
121
|
-
<div id="googleButton" class="tw:
|
|
141
|
+
<div id="googleButton" class="tw:w-full tw:opacity-0"></div>
|
|
122
142
|
<!-- Visual overlay: looks good, no pointer events -->
|
|
123
|
-
<div
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
143
|
+
<div
|
|
144
|
+
class="tw:pointer-events-none tw:absolute tw:inset-0 tw:flex tw:items-center tw:justify-center tw:gap-3 tw:rounded tw:border tw:border-gray-300 tw:bg-white tw:text-sm tw:font-medium tw:text-gray-700 tw:dark:border-gray-600 tw:dark:bg-gray-800 tw:dark:text-gray-200"
|
|
145
|
+
>
|
|
146
|
+
<svg
|
|
147
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
148
|
+
viewBox="0 0 24 24"
|
|
149
|
+
width="18"
|
|
150
|
+
height="18"
|
|
151
|
+
aria-hidden="true"
|
|
152
|
+
>
|
|
153
|
+
<path
|
|
154
|
+
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
155
|
+
fill="#4285F4"
|
|
156
|
+
/>
|
|
157
|
+
<path
|
|
158
|
+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
159
|
+
fill="#34A853"
|
|
160
|
+
/>
|
|
161
|
+
<path
|
|
162
|
+
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z"
|
|
163
|
+
fill="#FBBC05"
|
|
164
|
+
/>
|
|
165
|
+
<path
|
|
166
|
+
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
167
|
+
fill="#EA4335"
|
|
168
|
+
/>
|
|
129
169
|
</svg>
|
|
130
170
|
Sign in with Google
|
|
131
171
|
</div>
|
|
132
172
|
</div>
|
|
133
173
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
<
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
174
|
+
<label class="tw:block tw:text-sm tw:font-medium" for="email">
|
|
175
|
+
Email
|
|
176
|
+
<input
|
|
177
|
+
bind:this={focusedField}
|
|
178
|
+
type="email"
|
|
179
|
+
class="form-input-validated"
|
|
180
|
+
bind:value={user.email}
|
|
181
|
+
required
|
|
182
|
+
placeholder="Email"
|
|
183
|
+
id="email"
|
|
184
|
+
autocomplete="email"
|
|
185
|
+
/>
|
|
186
|
+
<span class="form-error">Email address required</span>
|
|
187
|
+
</label>
|
|
188
|
+
|
|
189
|
+
<label class="tw:block tw:text-sm tw:font-medium" for="password">
|
|
190
|
+
Password
|
|
191
|
+
<input
|
|
192
|
+
type="password"
|
|
193
|
+
id="password"
|
|
194
|
+
class="form-input-validated"
|
|
195
|
+
bind:value={user.password}
|
|
196
|
+
required
|
|
197
|
+
minlength="8"
|
|
198
|
+
maxlength="80"
|
|
199
|
+
pattern={passwordPattern}
|
|
200
|
+
placeholder="Password"
|
|
201
|
+
autocomplete="new-password"
|
|
202
|
+
/>
|
|
203
|
+
<span class="form-error"
|
|
204
|
+
>Must be 8+ characters with a capital letter, number, and special character</span
|
|
205
|
+
>
|
|
206
|
+
<span class="tw:text-xs tw:text-gray-500">
|
|
207
|
+
Minimum 8 characters, one capital letter, one number, one special character.
|
|
208
|
+
</span>
|
|
209
|
+
</label>
|
|
210
|
+
|
|
211
|
+
<label class="tw:block tw:text-sm tw:font-medium" for="confirmPassword">
|
|
212
|
+
Confirm password
|
|
213
|
+
<input
|
|
214
|
+
type="password"
|
|
215
|
+
id="confirmPassword"
|
|
216
|
+
class="form-input"
|
|
217
|
+
class:tw:border-red-500={passwordMismatch}
|
|
218
|
+
bind:this={confirmPassword}
|
|
219
|
+
required
|
|
220
|
+
minlength="8"
|
|
221
|
+
maxlength="80"
|
|
222
|
+
placeholder="Password (again)"
|
|
223
|
+
autocomplete="new-password"
|
|
224
|
+
/>
|
|
225
|
+
{#if passwordMismatch}
|
|
226
|
+
<span class="tw:mt-0.5 tw:text-xs tw:text-red-600">Passwords must match</span>
|
|
227
|
+
{/if}
|
|
228
|
+
</label>
|
|
229
|
+
|
|
230
|
+
<label class="tw:block tw:text-sm tw:font-medium" for="firstName">
|
|
231
|
+
First name
|
|
232
|
+
<input
|
|
233
|
+
bind:value={user.firstName}
|
|
234
|
+
class="form-input-validated"
|
|
235
|
+
id="firstName"
|
|
236
|
+
placeholder="First name"
|
|
237
|
+
required
|
|
238
|
+
autocomplete="given-name"
|
|
239
|
+
/>
|
|
240
|
+
<span class="form-error">First name required</span>
|
|
241
|
+
</label>
|
|
242
|
+
|
|
243
|
+
<label class="tw:block tw:text-sm tw:font-medium" for="lastName">
|
|
244
|
+
Last name
|
|
245
|
+
<input
|
|
246
|
+
bind:value={user.lastName}
|
|
247
|
+
class="form-input-validated"
|
|
248
|
+
id="lastName"
|
|
249
|
+
placeholder="Last name"
|
|
250
|
+
required
|
|
251
|
+
autocomplete="family-name"
|
|
252
|
+
/>
|
|
253
|
+
<span class="form-error">Last name required</span>
|
|
254
|
+
</label>
|
|
255
|
+
|
|
256
|
+
<label class="tw:block tw:text-sm tw:font-medium" for="phone">
|
|
257
|
+
Phone
|
|
258
|
+
<input
|
|
259
|
+
type="tel"
|
|
260
|
+
bind:value={user.phone}
|
|
261
|
+
id="phone"
|
|
262
|
+
class="form-input"
|
|
263
|
+
placeholder="Phone"
|
|
264
|
+
autocomplete="tel-local"
|
|
265
|
+
/>
|
|
266
|
+
</label>
|
|
267
|
+
|
|
268
|
+
{#if message}
|
|
269
|
+
<p class="tw:text-red-600">{message}</p>
|
|
185
270
|
{/if}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
bind:value={user.firstName}
|
|
192
|
-
class="form-input-validated"
|
|
193
|
-
id="firstName"
|
|
194
|
-
placeholder="First name"
|
|
195
|
-
required
|
|
196
|
-
autocomplete="given-name"
|
|
197
|
-
/>
|
|
198
|
-
<span class="form-error">First name required</span>
|
|
199
|
-
</label>
|
|
200
|
-
|
|
201
|
-
<label class="tw:block tw:text-sm tw:font-medium" for="lastName">
|
|
202
|
-
Last name
|
|
203
|
-
<input
|
|
204
|
-
bind:value={user.lastName}
|
|
205
|
-
class="form-input-validated"
|
|
206
|
-
id="lastName"
|
|
207
|
-
placeholder="Last name"
|
|
208
|
-
required
|
|
209
|
-
autocomplete="family-name"
|
|
210
|
-
/>
|
|
211
|
-
<span class="form-error">Last name required</span>
|
|
212
|
-
</label>
|
|
213
|
-
|
|
214
|
-
<label class="tw:block tw:text-sm tw:font-medium" for="phone">
|
|
215
|
-
Phone
|
|
216
|
-
<input
|
|
217
|
-
type="tel"
|
|
218
|
-
bind:value={user.phone}
|
|
219
|
-
id="phone"
|
|
220
|
-
class="form-input"
|
|
221
|
-
placeholder="Phone"
|
|
222
|
-
autocomplete="tel-local"
|
|
223
|
-
/>
|
|
224
|
-
</label>
|
|
225
|
-
|
|
226
|
-
{#if message}
|
|
227
|
-
<p class="tw:text-red-600">{message}</p>
|
|
228
|
-
{/if}
|
|
229
|
-
|
|
230
|
-
<button
|
|
231
|
-
type="submit"
|
|
232
|
-
class="btn-primary"
|
|
233
|
-
disabled={loading}
|
|
234
|
-
>
|
|
235
|
-
{loading ? 'Creating account...' : 'Register'}
|
|
236
|
-
</button>
|
|
237
|
-
</form>
|
|
271
|
+
|
|
272
|
+
<button type="submit" class="btn-primary" disabled={loading}>
|
|
273
|
+
{loading ? 'Creating account...' : 'Register'}
|
|
274
|
+
</button>
|
|
275
|
+
</form>
|
|
238
276
|
{/if}
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { redirect } from '@sveltejs/kit'
|
|
2
2
|
import type { PageServerLoad } from './$types'
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Page server load function for the teachers route.
|
|
6
|
+
*
|
|
7
|
+
* Restricts access to users with the `teacher` or `admin` role. Unauthenticated
|
|
8
|
+
* or unauthorized users are redirected to the login page with a `referrer`
|
|
9
|
+
* parameter so they are returned here after logging in.
|
|
10
|
+
*
|
|
11
|
+
* @returns An object with a placeholder `message` for teacher/admin-only server content.
|
|
12
|
+
*/
|
|
4
13
|
export const load: PageServerLoad = async ({ locals }) => {
|
|
5
14
|
const { user } = locals
|
|
6
15
|
const authorized = ['admin', 'teacher']
|
package/src/service-worker.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { build, files, version } from '$service-worker'
|
|
|
7
7
|
|
|
8
8
|
const sw = self as unknown as ServiceWorkerGlobalScope
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
/** Unique cache name for this deployment, keyed by the SvelteKit build version. */
|
|
11
11
|
const CACHE = `cache-${version}`
|
|
12
12
|
|
|
13
13
|
const ASSETS = [
|
|
@@ -15,6 +15,10 @@ const ASSETS = [
|
|
|
15
15
|
...files // everything in `static`
|
|
16
16
|
]
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* On install, opens the versioned cache and pre-caches all build artifacts
|
|
20
|
+
* and static files so they are available offline.
|
|
21
|
+
*/
|
|
18
22
|
sw.addEventListener('install', event => {
|
|
19
23
|
// Create a new cache and add all files to it
|
|
20
24
|
async function addFilesToCache() {
|
|
@@ -25,6 +29,10 @@ sw.addEventListener('install', event => {
|
|
|
25
29
|
event.waitUntil(addFilesToCache())
|
|
26
30
|
})
|
|
27
31
|
|
|
32
|
+
/**
|
|
33
|
+
* On activate, removes all caches from previous deployments, keeping only
|
|
34
|
+
* the cache for the current build version.
|
|
35
|
+
*/
|
|
28
36
|
sw.addEventListener('activate', event => {
|
|
29
37
|
// Remove previous cached data from disk
|
|
30
38
|
async function deleteOldCaches() {
|
|
@@ -36,6 +44,14 @@ sw.addEventListener('activate', event => {
|
|
|
36
44
|
event.waitUntil(deleteOldCaches())
|
|
37
45
|
})
|
|
38
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Intercepts GET requests and applies a cache-first strategy for pre-cached
|
|
49
|
+
* assets, falling back to network-first (with cache fallback) for all other
|
|
50
|
+
* requests.
|
|
51
|
+
*
|
|
52
|
+
* API (`/api`), auth (`/auth`), and non-HTTP requests are bypassed and go
|
|
53
|
+
* directly to the network.
|
|
54
|
+
*/
|
|
39
55
|
sw.addEventListener('fetch', event => {
|
|
40
56
|
// ignore POST requests etc
|
|
41
57
|
if (event.request.method !== 'GET') return
|