sveltekit-auth-example 2.0.0 → 2.0.2
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/.eslintrc.cjs +19 -7
- package/.prettierignore +1 -0
- package/.yarn/install-state.gz +0 -0
- package/CHANGELOG.md +164 -98
- package/README.md +6 -1
- package/package.json +67 -66
- package/prettier.config.mjs +15 -0
- package/src/app.d.ts +31 -29
- package/src/app.html +8 -3
- package/src/hooks.server.ts +15 -15
- package/src/lib/focus.ts +6 -6
- package/src/lib/google.ts +48 -49
- package/src/lib/server/db.ts +7 -6
- package/src/lib/server/sendgrid.ts +11 -11
- package/src/routes/+error.svelte +1 -1
- package/src/routes/+layout.server.ts +5 -5
- package/src/routes/+layout.svelte +133 -100
- package/src/routes/admin/+page.server.ts +1 -1
- package/src/routes/admin/+page.svelte +2 -2
- package/src/routes/api/v1/user/+server.ts +13 -14
- package/src/routes/auth/[slug]/+server.ts +11 -6
- package/src/routes/auth/forgot/+server.ts +23 -23
- package/src/routes/auth/google/+server.ts +44 -45
- package/src/routes/auth/reset/+server.ts +1 -1
- package/src/routes/auth/reset/[token]/+page.svelte +117 -95
- package/src/routes/auth/reset/[token]/+page.ts +4 -4
- package/src/routes/forgot/+page.svelte +74 -63
- package/src/routes/info/+page.svelte +1 -1
- package/src/routes/login/+page.svelte +140 -120
- package/src/routes/profile/+page.server.ts +9 -9
- package/src/routes/profile/+page.svelte +142 -88
- package/src/routes/register/+page.server.ts +3 -2
- package/src/routes/register/+page.svelte +159 -104
- package/src/routes/teachers/+page.server.ts +5 -5
- package/src/routes/teachers/+page.svelte +2 -2
- package/src/stores.ts +1 -1
- package/svelte.config.js +1 -1
- package/.prettierrc +0 -9
|
@@ -2,20 +2,19 @@ import { error, json } from '@sveltejs/kit'
|
|
|
2
2
|
import type { RequestHandler } from './$types'
|
|
3
3
|
import { query } from '$lib/server/db'
|
|
4
4
|
|
|
5
|
-
export const PUT: RequestHandler = async event => {
|
|
6
|
-
|
|
5
|
+
export const PUT: RequestHandler = async (event) => {
|
|
6
|
+
const { user } = event.locals
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
error(401, 'Unauthorized - must be logged-in.');
|
|
8
|
+
if (!user) error(401, 'Unauthorized - must be logged-in.')
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
try {
|
|
11
|
+
const userUpdate = await event.request.json()
|
|
12
|
+
await query(`CALL update_user($1, $2);`, [user.id, JSON.stringify(userUpdate)])
|
|
13
|
+
} catch (err) {
|
|
14
|
+
error(503, 'Could not communicate with database.')
|
|
15
|
+
}
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
17
|
+
return json({
|
|
18
|
+
message: 'Successfully updated user profile.'
|
|
19
|
+
})
|
|
20
|
+
}
|
|
@@ -12,7 +12,8 @@ export const POST: RequestHandler = async (event) => {
|
|
|
12
12
|
try {
|
|
13
13
|
switch (slug) {
|
|
14
14
|
case 'logout':
|
|
15
|
-
if (event.locals.user) {
|
|
15
|
+
if (event.locals.user) {
|
|
16
|
+
// else they are logged out / session ended
|
|
16
17
|
sql = `CALL delete_session($1);`
|
|
17
18
|
result = await query(sql, [event.locals.user.id])
|
|
18
19
|
}
|
|
@@ -26,7 +27,7 @@ export const POST: RequestHandler = async (event) => {
|
|
|
26
27
|
sql = `SELECT register($1) AS "authenticationResult";`
|
|
27
28
|
break
|
|
28
29
|
default:
|
|
29
|
-
error(404, 'Invalid endpoint.')
|
|
30
|
+
error(404, 'Invalid endpoint.')
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
// Only /auth/login and /auth/register at this point
|
|
@@ -34,21 +35,25 @@ export const POST: RequestHandler = async (event) => {
|
|
|
34
35
|
|
|
35
36
|
// While client checks for these to be non-null, register() in the database does not
|
|
36
37
|
if (slug == 'register' && (!body.email || !body.password || !body.firstName || !body.lastName))
|
|
37
|
-
error(400, 'Please supply all required fields: email, password, first and last name.')
|
|
38
|
+
error(400, 'Please supply all required fields: email, password, first and last name.')
|
|
38
39
|
|
|
39
40
|
result = await query(sql, [JSON.stringify(body)])
|
|
40
41
|
} catch (err) {
|
|
41
|
-
error(503, 'Could not communicate with database.')
|
|
42
|
+
error(503, 'Could not communicate with database.')
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
const { authenticationResult }: { authenticationResult: AuthenticationResult } = result.rows[0]
|
|
45
46
|
|
|
46
47
|
if (!authenticationResult.user)
|
|
47
48
|
// includes when a user tries to register an existing email account with wrong password
|
|
48
|
-
error(authenticationResult.statusCode, authenticationResult.status)
|
|
49
|
+
error(authenticationResult.statusCode, authenticationResult.status)
|
|
49
50
|
|
|
50
51
|
// Ensures hooks.server.ts:handle() will not delete session cookie
|
|
51
52
|
event.locals.user = authenticationResult.user
|
|
52
|
-
cookies.set('session', authenticationResult.sessionId, {
|
|
53
|
+
cookies.set('session', authenticationResult.sessionId, {
|
|
54
|
+
httpOnly: true,
|
|
55
|
+
sameSite: 'lax',
|
|
56
|
+
path: '/'
|
|
57
|
+
})
|
|
53
58
|
return json({ message: authenticationResult.status, user: authenticationResult.user })
|
|
54
59
|
}
|
|
@@ -6,32 +6,32 @@ import { JWT_SECRET, DOMAIN, SENDGRID_SENDER } from '$env/static/private'
|
|
|
6
6
|
import { query } from '$lib/server/db'
|
|
7
7
|
import { sendMessage } from '$lib/server/sendgrid'
|
|
8
8
|
|
|
9
|
-
export const POST: RequestHandler = async event => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
export const POST: RequestHandler = async (event) => {
|
|
10
|
+
const body = await event.request.json()
|
|
11
|
+
const sql = `SELECT id as "userId" FROM users WHERE email = $1 LIMIT 1;`
|
|
12
|
+
const { rows } = await query(sql, [body.email])
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
if (rows.length > 0) {
|
|
15
|
+
const { userId } = rows[0]
|
|
16
|
+
// Create JWT with userId expiring in 30 mins
|
|
17
|
+
const secret = JWT_SECRET
|
|
18
|
+
const token = jwt.sign({ subject: userId }, <Secret>secret, {
|
|
19
|
+
expiresIn: '30m'
|
|
20
|
+
})
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
// Email URL with token to user
|
|
23
|
+
const message: MailDataRequired = {
|
|
24
|
+
to: { email: body.email },
|
|
25
|
+
from: SENDGRID_SENDER,
|
|
26
|
+
subject: 'Password reset',
|
|
27
|
+
categories: ['account'],
|
|
28
|
+
html: `
|
|
29
29
|
<a href="${DOMAIN}/auth/reset/${token}">Reset my password</a>. Your browser will open and ask you to provide a
|
|
30
30
|
new password with a confirmation then redirect you to your login page.
|
|
31
31
|
`
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
}
|
|
33
|
+
sendMessage(message)
|
|
34
|
+
}
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
}
|
|
36
|
+
return new Response(undefined, { status: 204 })
|
|
37
|
+
}
|
|
@@ -6,58 +6,57 @@ import { PUBLIC_GOOGLE_CLIENT_ID } from '$env/static/public'
|
|
|
6
6
|
|
|
7
7
|
// Verify JWT per https://developers.google.com/identity/gsi/web/guides/verify-google-id-token
|
|
8
8
|
async function getGoogleUserFromJWT(token: string): Promise<Partial<User>> {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
9
|
+
try {
|
|
10
|
+
const client = new OAuth2Client(PUBLIC_GOOGLE_CLIENT_ID)
|
|
11
|
+
const ticket = await client.verifyIdToken({
|
|
12
|
+
idToken: token,
|
|
13
|
+
audience: PUBLIC_GOOGLE_CLIENT_ID
|
|
14
|
+
})
|
|
15
|
+
const payload = ticket.getPayload()
|
|
16
|
+
if (!payload) error(500, 'Google authentication did not get the expected payload')
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
firstName: payload['given_name'] || 'UnknownFirstName',
|
|
20
|
+
lastName: payload['family_name'] || 'UnknownLastName',
|
|
21
|
+
email: payload['email']
|
|
22
|
+
}
|
|
23
|
+
} catch (err) {
|
|
24
|
+
let message = ''
|
|
25
|
+
if (err instanceof Error) message = err.message
|
|
26
|
+
error(500, `Google user could not be authenticated: ${message}`)
|
|
27
|
+
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
// Upsert user and get session ID
|
|
31
31
|
async function upsertGoogleUser(user: Partial<User>): Promise<UserSession> {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
32
|
+
try {
|
|
33
|
+
const sql = `SELECT start_gmail_user_session($1) AS user_session;`
|
|
34
|
+
const { rows } = await query(sql, [JSON.stringify(user)])
|
|
35
|
+
return <UserSession>rows[0].user_session
|
|
36
|
+
} catch (err) {
|
|
37
|
+
let message = ''
|
|
38
|
+
if (err instanceof Error) message = err.message
|
|
39
|
+
error(500, `Gmail user could not be upserted: ${message}`)
|
|
40
|
+
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// Returns local user if Google user authenticated (and authorized our app)
|
|
44
|
-
export const POST: RequestHandler = async event => {
|
|
45
|
-
|
|
44
|
+
export const POST: RequestHandler = async (event) => {
|
|
45
|
+
const { cookies } = event
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
try {
|
|
48
|
+
const { token } = await event.request.json()
|
|
49
|
+
const user = await getGoogleUserFromJWT(token)
|
|
50
|
+
const userSession = await upsertGoogleUser(user)
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
// Prevent hooks.server.ts's handler() from deleting cookie thinking no one has authenticated
|
|
53
|
+
event.locals.user = userSession.user
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
55
|
+
cookies.set('session', userSession.id, { httpOnly: true, sameSite: 'lax', path: '/' })
|
|
56
|
+
return json({ message: 'Successful Google Sign-In.', user: userSession.user })
|
|
57
|
+
} catch (err) {
|
|
58
|
+
let message = ''
|
|
59
|
+
if (err instanceof Error) message = err.message
|
|
60
|
+
error(401, message)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -11,7 +11,7 @@ export const PUT: RequestHandler = async (event) => {
|
|
|
11
11
|
|
|
12
12
|
// Check the validity of the token and extract userId
|
|
13
13
|
try {
|
|
14
|
-
const decoded = <JwtPayload>
|
|
14
|
+
const decoded = <JwtPayload>jwt.verify(token, <jwt.Secret>JWT_SECRET)
|
|
15
15
|
const userId = decoded.subject
|
|
16
16
|
|
|
17
17
|
// Update the database with the new password
|
|
@@ -1,105 +1,127 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
}
|
|
2
|
+
import type { PageData } from './$types'
|
|
3
|
+
import { onMount } from 'svelte'
|
|
4
|
+
import { goto } from '$app/navigation'
|
|
5
|
+
import { toast } from '../../../../stores'
|
|
6
|
+
import { focusOnFirstError } from '$lib/focus'
|
|
7
|
+
|
|
8
|
+
export let data: PageData
|
|
9
|
+
|
|
10
|
+
let focusedField: HTMLInputElement
|
|
11
|
+
let password: string
|
|
12
|
+
let confirmPassword: HTMLInputElement
|
|
13
|
+
let message: string
|
|
14
|
+
|
|
15
|
+
onMount(() => {
|
|
16
|
+
focusedField.focus()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const passwordMatch = () => {
|
|
20
|
+
if (!password) password = ''
|
|
21
|
+
return password == confirmPassword.value
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const resetPassword = async () => {
|
|
25
|
+
message = ''
|
|
26
|
+
const form = <HTMLFormElement>document.getElementById('reset')
|
|
27
|
+
|
|
28
|
+
if (!passwordMatch()) {
|
|
29
|
+
confirmPassword.classList.add('is-invalid')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (form.checkValidity()) {
|
|
33
|
+
const url = `/auth/reset`
|
|
34
|
+
const res = await fetch(url, {
|
|
35
|
+
method: 'PUT',
|
|
36
|
+
headers: {
|
|
37
|
+
'Content-Type': 'application/json'
|
|
38
|
+
},
|
|
39
|
+
body: JSON.stringify({
|
|
40
|
+
token: data.token,
|
|
41
|
+
password
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
if (res.ok) {
|
|
46
|
+
$toast = {
|
|
47
|
+
title: 'Password Reset Succesful',
|
|
48
|
+
body: 'Your password was reset. Please login.',
|
|
49
|
+
isOpen: true
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
goto('/login')
|
|
53
|
+
} else {
|
|
54
|
+
const body = await res.json()
|
|
55
|
+
console.log('Failed reset', body)
|
|
56
|
+
message = body.message
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
form.classList.add('was-validated')
|
|
60
|
+
focusOnFirstError(form)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
66
63
|
</script>
|
|
67
64
|
|
|
68
65
|
<svelte:head>
|
|
69
|
-
|
|
66
|
+
<title>New Password</title>
|
|
70
67
|
</svelte:head>
|
|
71
68
|
|
|
72
69
|
<div class="d-flex justify-content-center mt-5">
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
70
|
+
<div class="card login">
|
|
71
|
+
<div class="card-body">
|
|
72
|
+
<form id="reset" autocomplete="on" novalidate>
|
|
73
|
+
<h4><strong>New Password</strong></h4>
|
|
74
|
+
<p>Please provide a new password.</p>
|
|
75
|
+
<div class="mb-3">
|
|
76
|
+
<label class="form-label" for="password">Password</label>
|
|
77
|
+
<input
|
|
78
|
+
class="form-control"
|
|
79
|
+
id="password"
|
|
80
|
+
type="password"
|
|
81
|
+
bind:value={password}
|
|
82
|
+
bind:this={focusedField}
|
|
83
|
+
minlength="8"
|
|
84
|
+
maxlength="80"
|
|
85
|
+
placeholder="Password"
|
|
86
|
+
autocomplete="new-password"
|
|
87
|
+
/>
|
|
88
|
+
<div class="invalid-feedback">Password with 8 chars or more required</div>
|
|
89
|
+
<div class="form-text">
|
|
90
|
+
Password minimum length 8, must have one capital letter, 1 number, and one unique
|
|
91
|
+
character.
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
<div class="mb-3">
|
|
95
|
+
<label class="form-label" for="passwordConfirm">Password (retype)</label>
|
|
96
|
+
<input
|
|
97
|
+
class="form-control"
|
|
98
|
+
id="passwordConfirm"
|
|
99
|
+
type="password"
|
|
100
|
+
required={!!password}
|
|
101
|
+
bind:this={confirmPassword}
|
|
102
|
+
minlength="8"
|
|
103
|
+
maxlength="80"
|
|
104
|
+
placeholder="Password (again)"
|
|
105
|
+
autocomplete="new-password"
|
|
106
|
+
/>
|
|
107
|
+
<div class="invalid-feedback">Passwords must match</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
{#if message}
|
|
111
|
+
<p class="text-danger">{message}</p>
|
|
112
|
+
{/if}
|
|
113
|
+
<div class="d-grid gap-2">
|
|
114
|
+
<button on:click|preventDefault={resetPassword} class="btn btn-primary btn-lg"
|
|
115
|
+
>Send Email</button
|
|
116
|
+
>
|
|
117
|
+
</div>
|
|
118
|
+
</form>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
99
121
|
</div>
|
|
100
122
|
|
|
101
123
|
<style lang="scss">
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
</style>
|
|
124
|
+
.card-body {
|
|
125
|
+
width: 25rem;
|
|
126
|
+
}
|
|
127
|
+
</style>
|
|
@@ -1,77 +1,88 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import { onMount } from 'svelte'
|
|
3
|
+
import { goto } from '$app/navigation'
|
|
4
|
+
import { toast } from '../../stores'
|
|
5
|
+
import { focusOnFirstError } from '$lib/focus'
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
let focusedField: HTMLInputElement
|
|
8
|
+
let email: string
|
|
9
|
+
let message: string
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
onMount(() => {
|
|
12
|
+
focusedField.focus()
|
|
13
|
+
})
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
const sendPasswordReset = async () => {
|
|
16
|
+
message = ''
|
|
17
|
+
const form = <HTMLFormElement>document.getElementById('forgot')
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
19
|
+
if (form.checkValidity()) {
|
|
20
|
+
if (email.toLowerCase().includes('gmail.com')) {
|
|
21
|
+
return (message = 'Gmail passwords must be reset on Manage Your Google Account.')
|
|
22
|
+
}
|
|
23
|
+
const url = `/auth/forgot`
|
|
24
|
+
const res = await fetch(url, {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: {
|
|
27
|
+
'Content-Type': 'application/json'
|
|
28
|
+
},
|
|
29
|
+
body: JSON.stringify({ email })
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
if (res.ok) {
|
|
33
|
+
$toast = {
|
|
34
|
+
title: 'Password Reset',
|
|
35
|
+
body: 'Please check your inbox for a password reset email (junk mail, too).',
|
|
36
|
+
isOpen: true
|
|
37
|
+
}
|
|
38
|
+
return goto('/')
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
form.classList.add('was-validated')
|
|
42
|
+
focusOnFirstError(form)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
45
|
</script>
|
|
46
46
|
|
|
47
47
|
<svelte:head>
|
|
48
|
-
|
|
48
|
+
<title>Forgot Password</title>
|
|
49
49
|
</svelte:head>
|
|
50
50
|
|
|
51
51
|
<div class="d-flex justify-content-center mt-5">
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
52
|
+
<div class="card">
|
|
53
|
+
<div class="card-body">
|
|
54
|
+
<form id="forgot" autocomplete="on" novalidate>
|
|
55
|
+
<h4><strong>Forgot password</strong></h4>
|
|
56
|
+
<p>Hey, you're human. We get it.</p>
|
|
57
|
+
<div class="mb-3">
|
|
58
|
+
<label class="form-label" for="email">Email</label>
|
|
59
|
+
<input
|
|
60
|
+
bind:this={focusedField}
|
|
61
|
+
bind:value={email}
|
|
62
|
+
type="email"
|
|
63
|
+
id="email"
|
|
64
|
+
class="form-control"
|
|
65
|
+
required
|
|
66
|
+
placeholder="Email"
|
|
67
|
+
autocomplete="email"
|
|
68
|
+
/>
|
|
69
|
+
<div class="invalid-feedback">Email address required</div>
|
|
70
|
+
</div>
|
|
71
|
+
{#if message}
|
|
72
|
+
<p class="text-danger">{message}</p>
|
|
73
|
+
{/if}
|
|
74
|
+
<div class="d-grid gap-2">
|
|
75
|
+
<button on:click|preventDefault={sendPasswordReset} class="btn btn-primary btn-lg"
|
|
76
|
+
>Send Email</button
|
|
77
|
+
>
|
|
78
|
+
</div>
|
|
79
|
+
</form>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
71
82
|
</div>
|
|
72
83
|
|
|
73
84
|
<style lang="scss">
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
</style>
|
|
85
|
+
.card-body {
|
|
86
|
+
width: 25rem;
|
|
87
|
+
}
|
|
88
|
+
</style>
|