sveltekit-auth-example 5.0.4 → 5.1.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/.editorconfig +9 -3
- package/{.env.sample → .env.example} +1 -0
- package/.prettierignore +1 -1
- package/.vscode/mcp.json +13 -0
- package/.vscode/settings.json +7 -5
- package/.yarn/releases/yarn-4.13.0.cjs +940 -0
- package/.yarnrc.yml +1 -1
- package/AGENTS.md +23 -0
- package/CHANGELOG.md +8 -0
- package/README.md +2 -3
- package/db_create.sql +98 -49
- package/{eslint.config.js → eslint.config.mjs} +4 -3
- package/package.json +34 -32
- package/playwright.config.ts +24 -0
- package/prettier.config.mjs +14 -5
- package/src/app.d.ts +1 -1
- package/src/app.html +1 -1
- package/src/hooks.server.ts +47 -9
- package/src/lib/app-state.svelte.ts +19 -0
- package/src/lib/auth-redirect.ts +25 -0
- package/src/lib/google.ts +7 -26
- package/src/lib/server/db.ts +63 -10
- package/src/lib/server/sendgrid.ts +5 -9
- package/src/routes/+error.svelte +3 -3
- package/src/routes/+layout.svelte +91 -125
- package/src/routes/api/v1/user/+server.ts +16 -0
- package/src/routes/auth/[slug]/+server.ts +5 -56
- package/src/routes/auth/forgot/+server.ts +6 -1
- package/src/routes/auth/google/+server.ts +1 -1
- package/src/routes/auth/login/+server.ts +68 -0
- package/src/routes/auth/logout/+server.ts +19 -0
- package/src/routes/auth/register/+server.ts +64 -0
- package/src/routes/auth/reset/+server.ts +7 -0
- package/src/routes/auth/reset/[token]/+page.svelte +102 -84
- package/src/routes/auth/verify/[token]/+server.ts +48 -0
- package/src/routes/forgot/+page.svelte +64 -54
- package/src/routes/layout.css +63 -0
- package/src/routes/login/+page.server.ts +9 -0
- package/src/routes/login/+page.svelte +73 -115
- package/src/routes/profile/+page.svelte +174 -123
- package/src/routes/register/+page.svelte +147 -125
- package/src/service-worker.ts +22 -4
- package/svelte.config.js +13 -1
- package/tsconfig.json +3 -1
- package/vite.config.ts +5 -1
- package/.yarn/releases/yarn-4.9.2.cjs +0 -942
- package/src/stores.ts +0 -13
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount } from 'svelte'
|
|
3
|
-
import { goto } from '$app/navigation'
|
|
4
|
-
import { loginSession } from '../../stores'
|
|
5
3
|
import { focusOnFirstError } from '$lib/focus'
|
|
6
4
|
import { initializeGoogleAccounts, renderGoogleButton } from '$lib/google'
|
|
7
5
|
|
|
8
6
|
let focusedField: HTMLInputElement | undefined = $state()
|
|
7
|
+
// Pattern stored as a variable to avoid Svelte parsing `{8,}` as a template expression
|
|
8
|
+
const passwordPattern = '(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9]).{8,}'
|
|
9
9
|
|
|
10
10
|
let user: User = $state({
|
|
11
11
|
id: 0,
|
|
@@ -18,13 +18,21 @@
|
|
|
18
18
|
})
|
|
19
19
|
let confirmPassword: HTMLInputElement | undefined = $state()
|
|
20
20
|
let message = $state('')
|
|
21
|
+
let submitted = $state(false)
|
|
22
|
+
let passwordMismatch = $state(false)
|
|
23
|
+
let loading = $state(false)
|
|
24
|
+
let emailVerificationSent = $state(false)
|
|
25
|
+
|
|
26
|
+
let formEl: HTMLFormElement | undefined = $state()
|
|
21
27
|
|
|
22
28
|
async function register() {
|
|
23
|
-
const form =
|
|
29
|
+
const form = formEl!
|
|
24
30
|
message = ''
|
|
31
|
+
submitted = false
|
|
32
|
+
passwordMismatch = false
|
|
25
33
|
|
|
26
34
|
if (!passwordMatch()) {
|
|
27
|
-
|
|
35
|
+
passwordMismatch = true
|
|
28
36
|
return
|
|
29
37
|
}
|
|
30
38
|
|
|
@@ -38,7 +46,7 @@
|
|
|
38
46
|
}
|
|
39
47
|
}
|
|
40
48
|
} else {
|
|
41
|
-
|
|
49
|
+
submitted = true
|
|
42
50
|
focusOnFirstError(form)
|
|
43
51
|
}
|
|
44
52
|
}
|
|
@@ -51,6 +59,7 @@
|
|
|
51
59
|
})
|
|
52
60
|
|
|
53
61
|
async function registerLocal(user: User) {
|
|
62
|
+
loading = true
|
|
54
63
|
try {
|
|
55
64
|
const res = await fetch('/auth/register', {
|
|
56
65
|
method: 'POST',
|
|
@@ -59,22 +68,23 @@
|
|
|
59
68
|
'Content-Type': 'application/json'
|
|
60
69
|
}
|
|
61
70
|
})
|
|
71
|
+
const fromEndpoint = await res.json()
|
|
62
72
|
if (!res.ok) {
|
|
63
|
-
if (res.status ==
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
if (res.status == 409)
|
|
74
|
+
throw new Error('Sorry, that email address is already in use.')
|
|
75
|
+
throw new Error(fromEndpoint.message || res.statusText)
|
|
76
|
+
}
|
|
77
|
+
if (fromEndpoint.emailVerification) {
|
|
78
|
+
emailVerificationSent = true
|
|
79
|
+
return
|
|
67
80
|
}
|
|
68
|
-
|
|
69
|
-
// res.ok
|
|
70
|
-
const fromEndpoint = await res.json()
|
|
71
|
-
loginSession.set(fromEndpoint.user) // update store so user is logged in
|
|
72
|
-
goto('/')
|
|
73
81
|
} catch (err) {
|
|
74
82
|
console.error('Register error', err)
|
|
75
83
|
if (err instanceof Error) {
|
|
76
84
|
throw new Error(err.message)
|
|
77
85
|
}
|
|
86
|
+
} finally {
|
|
87
|
+
loading = false
|
|
78
88
|
}
|
|
79
89
|
}
|
|
80
90
|
|
|
@@ -89,116 +99,128 @@
|
|
|
89
99
|
<title>Register</title>
|
|
90
100
|
</svelte:head>
|
|
91
101
|
|
|
92
|
-
|
|
93
|
-
<div class="
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
{#if user}
|
|
98
|
-
<form id="register" autocomplete="on" novalidate class="mt-3">
|
|
99
|
-
<div class="mb-3">
|
|
100
|
-
<div id="googleButton"></div>
|
|
101
|
-
</div>
|
|
102
|
-
<div class="mb-3">
|
|
103
|
-
<label class="form-label" for="email">Email</label>
|
|
104
|
-
<input
|
|
105
|
-
bind:this={focusedField}
|
|
106
|
-
type="email"
|
|
107
|
-
class="form-control"
|
|
108
|
-
bind:value={user.email}
|
|
109
|
-
required
|
|
110
|
-
placeholder="Email"
|
|
111
|
-
id="email"
|
|
112
|
-
autocomplete="email"
|
|
113
|
-
/>
|
|
114
|
-
<div class="invalid-feedback">Email address required</div>
|
|
115
|
-
</div>
|
|
116
|
-
<div class="mb-3">
|
|
117
|
-
<label class="form-label" for="password">Password</label>
|
|
118
|
-
<input
|
|
119
|
-
type="password"
|
|
120
|
-
id="password"
|
|
121
|
-
class="form-control"
|
|
122
|
-
bind:value={user.password}
|
|
123
|
-
required
|
|
124
|
-
minlength="8"
|
|
125
|
-
maxlength="80"
|
|
126
|
-
placeholder="Password"
|
|
127
|
-
autocomplete="new-password"
|
|
128
|
-
/>
|
|
129
|
-
<div class="invalid-feedback">Password with 8 chars or more required</div>
|
|
130
|
-
<div class="form-text">
|
|
131
|
-
Password minimum length 8, must have one capital letter, 1 number, and one unique
|
|
132
|
-
character.
|
|
133
|
-
</div>
|
|
134
|
-
</div>
|
|
135
|
-
<div class="mb-3">
|
|
136
|
-
<label class="form-label" for="password">Confirm password</label>
|
|
137
|
-
<input
|
|
138
|
-
type="password"
|
|
139
|
-
id="password"
|
|
140
|
-
class="form-control"
|
|
141
|
-
bind:this={confirmPassword}
|
|
142
|
-
required
|
|
143
|
-
minlength="8"
|
|
144
|
-
maxlength="80"
|
|
145
|
-
placeholder="Password (again)"
|
|
146
|
-
autocomplete="new-password"
|
|
147
|
-
/>
|
|
148
|
-
<div class="form-text">
|
|
149
|
-
Password minimum length 8, must have one capital letter, 1 number, and one unique
|
|
150
|
-
character.
|
|
151
|
-
</div>
|
|
152
|
-
</div>
|
|
153
|
-
<div class="mb-3">
|
|
154
|
-
<label class="form-label" for="firstName">First name</label>
|
|
155
|
-
<input
|
|
156
|
-
bind:value={user.firstName}
|
|
157
|
-
class="form-control"
|
|
158
|
-
id="firstName"
|
|
159
|
-
placeholder="First name"
|
|
160
|
-
required
|
|
161
|
-
autocomplete="given-name"
|
|
162
|
-
/>
|
|
163
|
-
<div class="invalid-feedback">First name required</div>
|
|
164
|
-
</div>
|
|
165
|
-
<div class="mb-3">
|
|
166
|
-
<label class="form-label" for="lastName">Last name</label>
|
|
167
|
-
<input
|
|
168
|
-
bind:value={user.lastName}
|
|
169
|
-
class="form-control"
|
|
170
|
-
id="lastName"
|
|
171
|
-
placeholder="Last name"
|
|
172
|
-
required
|
|
173
|
-
autocomplete="family-name"
|
|
174
|
-
/>
|
|
175
|
-
<div class="invalid-feedback">Last name required</div>
|
|
176
|
-
</div>
|
|
177
|
-
<div class="mb-3">
|
|
178
|
-
<label class="form-label" for="phone">Phone</label>
|
|
179
|
-
<input
|
|
180
|
-
type="tel"
|
|
181
|
-
bind:value={user.phone}
|
|
182
|
-
id="phone"
|
|
183
|
-
class="form-control"
|
|
184
|
-
placeholder="Phone"
|
|
185
|
-
autocomplete="tel-local"
|
|
186
|
-
/>
|
|
187
|
-
</div>
|
|
188
|
-
|
|
189
|
-
{#if message}
|
|
190
|
-
<p class="text-danger">{message}</p>
|
|
191
|
-
{/if}
|
|
192
|
-
|
|
193
|
-
<button onclick={register} type="button" class="btn btn-primary btn-lg">Register</button>
|
|
194
|
-
</form>
|
|
195
|
-
{/if}
|
|
196
|
-
</div>
|
|
102
|
+
{#if emailVerificationSent}
|
|
103
|
+
<div class="tw:mx-auto tw:mt-20 tw:max-w-sm tw:space-y-4">
|
|
104
|
+
<h4>Check your email</h4>
|
|
105
|
+
<p>We've sent a verification link to your email address. Please check your inbox (and junk folder) to complete your registration.</p>
|
|
106
|
+
<a href="/login" class="btn-primary tw:block tw:text-center tw:no-underline">Back to login</a>
|
|
197
107
|
</div>
|
|
198
|
-
|
|
108
|
+
{:else}
|
|
109
|
+
<form
|
|
110
|
+
bind:this={formEl}
|
|
111
|
+
autocomplete="on"
|
|
112
|
+
novalidate
|
|
113
|
+
class="tw:mx-auto tw:my-8 tw:max-w-sm tw:space-y-4"
|
|
114
|
+
class:submitted
|
|
115
|
+
onsubmit={(e) => { e.preventDefault(); register() }}
|
|
116
|
+
>
|
|
117
|
+
<h4>Register</h4>
|
|
118
|
+
<p>Welcome to our community.</p>
|
|
199
119
|
|
|
200
|
-
<
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
120
|
+
<div id="googleButton" class="tw:w-full"></div>
|
|
121
|
+
|
|
122
|
+
<label class="tw:block tw:text-sm tw:font-medium" for="email">
|
|
123
|
+
Email
|
|
124
|
+
<input
|
|
125
|
+
bind:this={focusedField}
|
|
126
|
+
type="email"
|
|
127
|
+
class="form-input-validated"
|
|
128
|
+
bind:value={user.email}
|
|
129
|
+
required
|
|
130
|
+
placeholder="Email"
|
|
131
|
+
id="email"
|
|
132
|
+
autocomplete="email"
|
|
133
|
+
/>
|
|
134
|
+
<span class="form-error">Email address required</span>
|
|
135
|
+
</label>
|
|
136
|
+
|
|
137
|
+
<label class="tw:block tw:text-sm tw:font-medium" for="password">
|
|
138
|
+
Password
|
|
139
|
+
<input
|
|
140
|
+
type="password"
|
|
141
|
+
id="password"
|
|
142
|
+
class="form-input-validated"
|
|
143
|
+
bind:value={user.password}
|
|
144
|
+
required
|
|
145
|
+
minlength="8"
|
|
146
|
+
maxlength="80"
|
|
147
|
+
pattern={passwordPattern}
|
|
148
|
+
placeholder="Password"
|
|
149
|
+
autocomplete="new-password"
|
|
150
|
+
/>
|
|
151
|
+
<span class="form-error">Must be 8+ characters with a capital letter, number, and special character</span>
|
|
152
|
+
<span class="tw:text-xs tw:text-gray-500">
|
|
153
|
+
Minimum 8 characters, one capital letter, one number, one special character.
|
|
154
|
+
</span>
|
|
155
|
+
</label>
|
|
156
|
+
|
|
157
|
+
<label class="tw:block tw:text-sm tw:font-medium" for="confirmPassword">
|
|
158
|
+
Confirm password
|
|
159
|
+
<input
|
|
160
|
+
type="password"
|
|
161
|
+
id="confirmPassword"
|
|
162
|
+
class="form-input"
|
|
163
|
+
class:tw:border-red-500={passwordMismatch}
|
|
164
|
+
bind:this={confirmPassword}
|
|
165
|
+
required
|
|
166
|
+
minlength="8"
|
|
167
|
+
maxlength="80"
|
|
168
|
+
placeholder="Password (again)"
|
|
169
|
+
autocomplete="new-password"
|
|
170
|
+
/>
|
|
171
|
+
{#if passwordMismatch}
|
|
172
|
+
<span class="tw:text-xs tw:text-red-600 tw:mt-0.5">Passwords must match</span>
|
|
173
|
+
{/if}
|
|
174
|
+
</label>
|
|
175
|
+
|
|
176
|
+
<label class="tw:block tw:text-sm tw:font-medium" for="firstName">
|
|
177
|
+
First name
|
|
178
|
+
<input
|
|
179
|
+
bind:value={user.firstName}
|
|
180
|
+
class="form-input-validated"
|
|
181
|
+
id="firstName"
|
|
182
|
+
placeholder="First name"
|
|
183
|
+
required
|
|
184
|
+
autocomplete="given-name"
|
|
185
|
+
/>
|
|
186
|
+
<span class="form-error">First name required</span>
|
|
187
|
+
</label>
|
|
188
|
+
|
|
189
|
+
<label class="tw:block tw:text-sm tw:font-medium" for="lastName">
|
|
190
|
+
Last name
|
|
191
|
+
<input
|
|
192
|
+
bind:value={user.lastName}
|
|
193
|
+
class="form-input-validated"
|
|
194
|
+
id="lastName"
|
|
195
|
+
placeholder="Last name"
|
|
196
|
+
required
|
|
197
|
+
autocomplete="family-name"
|
|
198
|
+
/>
|
|
199
|
+
<span class="form-error">Last name required</span>
|
|
200
|
+
</label>
|
|
201
|
+
|
|
202
|
+
<label class="tw:block tw:text-sm tw:font-medium" for="phone">
|
|
203
|
+
Phone
|
|
204
|
+
<input
|
|
205
|
+
type="tel"
|
|
206
|
+
bind:value={user.phone}
|
|
207
|
+
id="phone"
|
|
208
|
+
class="form-input"
|
|
209
|
+
placeholder="Phone"
|
|
210
|
+
autocomplete="tel-local"
|
|
211
|
+
/>
|
|
212
|
+
</label>
|
|
213
|
+
|
|
214
|
+
{#if message}
|
|
215
|
+
<p class="tw:text-red-600">{message}</p>
|
|
216
|
+
{/if}
|
|
217
|
+
|
|
218
|
+
<button
|
|
219
|
+
type="submit"
|
|
220
|
+
class="btn-primary"
|
|
221
|
+
disabled={loading}
|
|
222
|
+
>
|
|
223
|
+
{loading ? 'Creating account...' : 'Register'}
|
|
224
|
+
</button>
|
|
225
|
+
</form>
|
|
226
|
+
{/if}
|
package/src/service-worker.ts
CHANGED
|
@@ -40,13 +40,23 @@ sw.addEventListener('fetch', event => {
|
|
|
40
40
|
// ignore POST requests etc
|
|
41
41
|
if (event.request.method !== 'GET') return
|
|
42
42
|
|
|
43
|
+
const url = new URL(event.request.url)
|
|
44
|
+
|
|
45
|
+
// Don't intercept API, auth, or non-HTTP requests — let them go straight to the network
|
|
46
|
+
const bypass =
|
|
47
|
+
url.pathname.startsWith('/api') ||
|
|
48
|
+
url.pathname.startsWith('/auth') ||
|
|
49
|
+
(url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
50
|
+
|
|
51
|
+
if (bypass) return
|
|
52
|
+
|
|
43
53
|
async function respond() {
|
|
44
|
-
const url = new URL(event.request.url)
|
|
45
54
|
const cache = await caches.open(CACHE)
|
|
46
55
|
|
|
47
56
|
// `build`/`files` can always be served from the cache
|
|
48
57
|
if (ASSETS.includes(url.pathname)) {
|
|
49
|
-
|
|
58
|
+
const response = await cache.match(url.pathname)
|
|
59
|
+
if (response) return response
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
// for everything else, try the network first, but
|
|
@@ -54,13 +64,21 @@ sw.addEventListener('fetch', event => {
|
|
|
54
64
|
try {
|
|
55
65
|
const response = await fetch(event.request)
|
|
56
66
|
|
|
67
|
+
// if we're offline, fetch can return a value that is not a Response
|
|
68
|
+
// instead of throwing - and we can't pass this non-Response to respondWith
|
|
69
|
+
if (!(response instanceof Response)) {
|
|
70
|
+
throw new Error('invalid response from fetch')
|
|
71
|
+
}
|
|
72
|
+
|
|
57
73
|
if (response.status === 200) {
|
|
58
74
|
cache.put(event.request, response.clone())
|
|
59
75
|
}
|
|
60
76
|
|
|
61
77
|
return response
|
|
62
|
-
} catch {
|
|
63
|
-
|
|
78
|
+
} catch (err) {
|
|
79
|
+
const response = await cache.match(event.request)
|
|
80
|
+
if (response) return response
|
|
81
|
+
throw err
|
|
64
82
|
}
|
|
65
83
|
}
|
|
66
84
|
|
package/svelte.config.js
CHANGED
|
@@ -15,7 +15,19 @@ if (!production) baseCsp.push('ws://localhost:3000')
|
|
|
15
15
|
|
|
16
16
|
/** @type {import('@sveltejs/kit').Config} */
|
|
17
17
|
const config = {
|
|
18
|
-
preprocess:
|
|
18
|
+
preprocess: [
|
|
19
|
+
vitePreprocess(),
|
|
20
|
+
{
|
|
21
|
+
name: 'announcer-styles-to-tailwind',
|
|
22
|
+
markup: ({ content: code }) => {
|
|
23
|
+
code = code.replace(
|
|
24
|
+
/(<div id="svelte-announcer"[^>]*?)\s+style="[^"]*"/,
|
|
25
|
+
'$1 class="tw:absolute tw:left-0 tw:top-0 tw:[clip:rect(0,0,0,0)] tw:[clip-path:inset(50%)] tw:overflow-hidden tw:whitespace-nowrap tw:w-px tw:h-px"'
|
|
26
|
+
)
|
|
27
|
+
return { code }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
],
|
|
19
31
|
|
|
20
32
|
kit: {
|
|
21
33
|
adapter: adapter({
|
package/tsconfig.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"extends": "./.svelte-kit/tsconfig.json",
|
|
3
3
|
"compilerOptions": {
|
|
4
|
+
"rewriteRelativeImportExtensions": true,
|
|
4
5
|
"allowJs": true,
|
|
5
6
|
"checkJs": true,
|
|
6
7
|
"esModuleInterop": true,
|
|
@@ -8,6 +9,7 @@
|
|
|
8
9
|
"resolveJsonModule": true,
|
|
9
10
|
"skipLibCheck": true,
|
|
10
11
|
"sourceMap": true,
|
|
11
|
-
"strict": true
|
|
12
|
+
"strict": true,
|
|
13
|
+
"moduleResolution": "bundler"
|
|
12
14
|
}
|
|
13
15
|
}
|
package/vite.config.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { sveltekit } from '@sveltejs/kit/vite'
|
|
2
2
|
import { defineConfig } from 'vite'
|
|
3
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
3
4
|
|
|
4
5
|
export default defineConfig({
|
|
5
6
|
build: {
|
|
6
7
|
sourcemap: true
|
|
7
8
|
},
|
|
8
|
-
plugins: [sveltekit()],
|
|
9
|
+
plugins: [sveltekit(), tailwindcss()],
|
|
10
|
+
test: {
|
|
11
|
+
include: ['src/**/*.unit.test.ts', 'tests/**/*.unit.test.ts']
|
|
12
|
+
},
|
|
9
13
|
server: {
|
|
10
14
|
host: 'localhost',
|
|
11
15
|
port: 3000,
|