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
|
@@ -1,45 +1,45 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import { onMount } from 'svelte'
|
|
3
|
+
import { goto } from '$app/navigation'
|
|
4
|
+
import { page } from '$app/stores'
|
|
5
|
+
import { loginSession } from '../../stores'
|
|
6
|
+
import { focusOnFirstError } from '$lib/focus'
|
|
7
|
+
import { initializeGoogleAccounts, renderGoogleButton } from '$lib/google'
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
let focusedField: HTMLInputElement
|
|
10
|
+
let message: string
|
|
11
|
+
const credentials: Credentials = {
|
|
12
|
+
email: '',
|
|
13
|
+
password: ''
|
|
14
|
+
}
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
async function login() {
|
|
17
|
+
message = ''
|
|
18
|
+
const form = <HTMLFormElement>document.getElementById('signIn')
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
20
|
+
if (form.checkValidity()) {
|
|
21
|
+
try {
|
|
22
|
+
await loginLocal(credentials)
|
|
23
|
+
} catch (err) {
|
|
24
|
+
if (err instanceof Error) {
|
|
25
|
+
console.error('Login error', err.message)
|
|
26
|
+
message = err.message
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
} else {
|
|
30
|
+
form.classList.add('was-validated')
|
|
31
|
+
focusOnFirstError(form)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
onMount(() => {
|
|
36
|
+
initializeGoogleAccounts()
|
|
37
|
+
renderGoogleButton()
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
focusedField.focus()
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
async function loginLocal(credentials: Credentials) {
|
|
43
43
|
try {
|
|
44
44
|
const res = await fetch('/auth/login', {
|
|
45
45
|
method: 'POST',
|
|
@@ -52,18 +52,18 @@
|
|
|
52
52
|
if (res.ok) {
|
|
53
53
|
loginSession.set(fromEndpoint.user)
|
|
54
54
|
const { role } = fromEndpoint.user
|
|
55
|
-
|
|
55
|
+
const referrer = $page.url.searchParams.get('referrer')
|
|
56
56
|
if (referrer) goto(referrer)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
57
|
+
switch (role) {
|
|
58
|
+
case 'teacher':
|
|
59
|
+
goto('/teachers')
|
|
60
|
+
break
|
|
61
|
+
case 'admin':
|
|
62
|
+
goto('/admin')
|
|
63
|
+
break
|
|
64
|
+
default:
|
|
65
|
+
goto('/')
|
|
66
|
+
}
|
|
67
67
|
} else {
|
|
68
68
|
throw new Error(fromEndpoint.message)
|
|
69
69
|
}
|
|
@@ -76,89 +76,109 @@
|
|
|
76
76
|
</script>
|
|
77
77
|
|
|
78
78
|
<svelte:head>
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
<title>Login Form</title>
|
|
80
|
+
<meta name="robots" content="noindex, nofollow" />
|
|
81
81
|
</svelte:head>
|
|
82
82
|
|
|
83
83
|
<div class="d-flex justify-content-center mt-5">
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
84
|
+
<div class="card">
|
|
85
|
+
<div class="card-body">
|
|
86
|
+
<form id="signIn" autocomplete="on" novalidate>
|
|
87
|
+
<h4><strong>Sign In</strong></h4>
|
|
88
|
+
<p>Welcome back.</p>
|
|
89
|
+
<div>
|
|
90
|
+
<div class="mb-1">
|
|
91
|
+
<div id="googleButton"></div>
|
|
92
|
+
</div>
|
|
93
|
+
<div class="text-centered">
|
|
94
|
+
<div class="strike">
|
|
95
|
+
<span>or</span>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="mb-3">
|
|
99
|
+
<label class="form-label" for="email">Email</label>
|
|
100
|
+
<input
|
|
101
|
+
type="email"
|
|
102
|
+
class="form-control"
|
|
103
|
+
bind:this={focusedField}
|
|
104
|
+
bind:value={credentials.email}
|
|
105
|
+
required
|
|
106
|
+
placeholder="Email"
|
|
107
|
+
autocomplete="email"
|
|
108
|
+
/>
|
|
109
|
+
<div class="invalid-feedback">Email address required</div>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="mb-3">
|
|
112
|
+
<label class="form-label" for="password">Password</label>
|
|
113
|
+
<input
|
|
114
|
+
class="form-control"
|
|
115
|
+
type="password"
|
|
116
|
+
bind:value={credentials.password}
|
|
117
|
+
required
|
|
118
|
+
minlength="8"
|
|
119
|
+
maxlength="80"
|
|
120
|
+
placeholder="Password"
|
|
121
|
+
autocomplete="current-password"
|
|
122
|
+
/>
|
|
123
|
+
<div class="invalid-feedback">Password with 8 chars or more required</div>
|
|
124
|
+
<div class="form-text">
|
|
125
|
+
Password minimum length 8, must have one capital letter, 1 number, and one unique
|
|
126
|
+
character.
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
<div>
|
|
131
|
+
<a href="/forgot" class="text-black-50">Forgot Password?</a><br />
|
|
132
|
+
<br />
|
|
133
|
+
</div>
|
|
134
|
+
{#if message}
|
|
135
|
+
<p class="text-danger">{message}</p>
|
|
136
|
+
{/if}
|
|
137
|
+
<div class="d-grid gap-2">
|
|
138
|
+
<button on:click|preventDefault={login} class="btn btn-primary btn-lg">Sign In</button>
|
|
139
|
+
</div>
|
|
140
|
+
</form>
|
|
141
|
+
</div>
|
|
142
|
+
<div class="card-footer text-center bg-white">
|
|
143
|
+
<a href="/register" class="text-black-50">Don't have an account?</a>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
126
146
|
</div>
|
|
127
147
|
|
|
128
148
|
<style lang="scss">
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
149
|
+
.card-body {
|
|
150
|
+
width: 25rem;
|
|
151
|
+
}
|
|
132
152
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
153
|
+
.strike {
|
|
154
|
+
display: block;
|
|
155
|
+
text-align: center;
|
|
156
|
+
overflow: hidden;
|
|
157
|
+
white-space: nowrap;
|
|
158
|
+
}
|
|
139
159
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
160
|
+
.strike > span {
|
|
161
|
+
position: relative;
|
|
162
|
+
display: inline-block;
|
|
163
|
+
}
|
|
144
164
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
165
|
+
.strike > span:before,
|
|
166
|
+
.strike > span:after {
|
|
167
|
+
content: '';
|
|
168
|
+
position: absolute;
|
|
169
|
+
top: 50%;
|
|
170
|
+
width: 9999px;
|
|
171
|
+
height: 1px;
|
|
172
|
+
background: darkgray;
|
|
173
|
+
}
|
|
154
174
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
175
|
+
.strike > span:before {
|
|
176
|
+
right: 100%;
|
|
177
|
+
margin-right: 10px;
|
|
178
|
+
}
|
|
159
179
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
</style>
|
|
180
|
+
.strike > span:after {
|
|
181
|
+
left: 100%;
|
|
182
|
+
margin-left: 10px;
|
|
183
|
+
}
|
|
184
|
+
</style>
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { redirect } from '@sveltejs/kit'
|
|
1
|
+
import { redirect } from '@sveltejs/kit'
|
|
2
2
|
import type { PageServerLoad } from './$types'
|
|
3
3
|
|
|
4
4
|
export const load: PageServerLoad = async ({ locals }) => {
|
|
5
|
-
|
|
5
|
+
const { user } = locals // populated by /src/hooks.ts
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
const authorized = ['admin', 'teacher', 'student'] // must be logged-in
|
|
8
|
+
if (!user || !authorized.includes(user.role)) {
|
|
9
|
+
redirect(302, '/login?referrer=/profile')
|
|
10
|
+
}
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
return {
|
|
13
|
+
user
|
|
14
|
+
}
|
|
15
15
|
}
|
|
@@ -1,109 +1,163 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { PageData } from './$types'
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { onMount } from 'svelte'
|
|
4
|
+
import { focusOnFirstError } from '$lib/focus'
|
|
5
|
+
import { loginSession } from '../../stores'
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
export let data: PageData
|
|
8
|
+
const { user }: { user: User } = data
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
let focusedField: HTMLInputElement
|
|
11
|
+
let message: string
|
|
12
|
+
let confirmPassword: HTMLInputElement
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
onMount(() => {
|
|
15
|
+
focusedField.focus()
|
|
16
16
|
})
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
async function update() {
|
|
19
|
+
message = ''
|
|
20
|
+
const form = <HTMLFormElement>document.getElementById('profile')
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
if (!user?.email?.includes('gmail.com') && !passwordMatch()) {
|
|
23
|
+
confirmPassword.classList.add('is-invalid')
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
27
|
+
if (form.checkValidity()) {
|
|
28
|
+
const url = '/api/v1/user'
|
|
29
|
+
const res = await fetch(url, {
|
|
30
|
+
method: 'PUT',
|
|
31
|
+
headers: {
|
|
32
|
+
'Content-Type': 'application/json'
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify(user)
|
|
35
|
+
})
|
|
36
|
+
const reply = await res.json()
|
|
37
|
+
message = reply.message
|
|
38
|
+
$loginSession = JSON.parse(JSON.stringify(user)) // update loginSession store
|
|
39
|
+
} else {
|
|
40
|
+
form.classList.add('was-validated')
|
|
41
|
+
focusOnFirstError(form)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return user.password == confirmPassword.value
|
|
49
|
-
}
|
|
45
|
+
const passwordMatch = () => {
|
|
46
|
+
if (!user.password) user.password = ''
|
|
47
|
+
return user.password == confirmPassword.value
|
|
48
|
+
}
|
|
50
49
|
</script>
|
|
51
50
|
|
|
52
51
|
<svelte:head>
|
|
53
|
-
|
|
52
|
+
<title>Profile</title>
|
|
54
53
|
</svelte:head>
|
|
55
54
|
|
|
56
55
|
<div class="d-flex justify-content-center my-3">
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
56
|
+
<div class="card login">
|
|
57
|
+
<div class="card-body">
|
|
58
|
+
<h4><strong>Profile</strong></h4>
|
|
59
|
+
<p>Update your information.</p>
|
|
60
|
+
<form id="profile" autocomplete="on" novalidate class="mt-3">
|
|
61
|
+
{#if !user?.email?.includes('gmail.com')}
|
|
62
|
+
<div class="mb-3">
|
|
63
|
+
<label class="form-label" for="email">Email</label>
|
|
64
|
+
<input
|
|
65
|
+
bind:this={focusedField}
|
|
66
|
+
type="email"
|
|
67
|
+
class="form-control"
|
|
68
|
+
bind:value={user.email}
|
|
69
|
+
required
|
|
70
|
+
placeholder="Email"
|
|
71
|
+
id="email"
|
|
72
|
+
autocomplete="email"
|
|
73
|
+
/>
|
|
74
|
+
<div class="invalid-feedback">Email address required</div>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="mb-3">
|
|
77
|
+
<label class="form-label" for="password">Password</label>
|
|
78
|
+
<input
|
|
79
|
+
type="password"
|
|
80
|
+
id="password"
|
|
81
|
+
class="form-control"
|
|
82
|
+
bind:value={user.password}
|
|
83
|
+
minlength="8"
|
|
84
|
+
maxlength="80"
|
|
85
|
+
placeholder="Password"
|
|
86
|
+
/>
|
|
87
|
+
<div class="invalid-feedback">Password with 8 chars or more required</div>
|
|
88
|
+
<div class="form-text">
|
|
89
|
+
Password minimum length 8, must have one capital letter, 1 number, and one unique
|
|
90
|
+
character.
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
<div class="mb-3">
|
|
94
|
+
<label class="form-label" for="password">Confirm password</label>
|
|
95
|
+
<input
|
|
96
|
+
type="password"
|
|
97
|
+
id="password"
|
|
98
|
+
class="form-control"
|
|
99
|
+
bind:this={confirmPassword}
|
|
100
|
+
required={!!user.password}
|
|
101
|
+
minlength="8"
|
|
102
|
+
maxlength="80"
|
|
103
|
+
placeholder="Password (again)"
|
|
104
|
+
autocomplete="new-password"
|
|
105
|
+
/>
|
|
106
|
+
<div class="form-text">
|
|
107
|
+
Password minimum length 8, must have one capital letter, 1 number, and one unique
|
|
108
|
+
character.
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
{/if}
|
|
112
|
+
<div class="mb-3">
|
|
113
|
+
<label class="form-label" for="firstName">First name</label>
|
|
114
|
+
<input
|
|
115
|
+
bind:this={focusedField}
|
|
116
|
+
bind:value={user.firstName}
|
|
117
|
+
class="form-control"
|
|
118
|
+
id="firstName"
|
|
119
|
+
required
|
|
120
|
+
placeholder="First name"
|
|
121
|
+
autocomplete="given-name"
|
|
122
|
+
/>
|
|
123
|
+
<div class="invalid-feedback">First name required</div>
|
|
124
|
+
</div>
|
|
125
|
+
<div class="mb-3">
|
|
126
|
+
<label class="form-label" for="lastName">Last name</label>
|
|
127
|
+
<input
|
|
128
|
+
bind:value={user.lastName}
|
|
129
|
+
class="form-control"
|
|
130
|
+
id="lastName"
|
|
131
|
+
required
|
|
132
|
+
placeholder="Last name"
|
|
133
|
+
autocomplete="family-name"
|
|
134
|
+
/>
|
|
135
|
+
<div class="invalid-feedback">Last name required</div>
|
|
136
|
+
</div>
|
|
137
|
+
<div class="mb-3">
|
|
138
|
+
<label class="form-label" for="phone">Phone</label>
|
|
139
|
+
<input
|
|
140
|
+
type="tel"
|
|
141
|
+
bind:value={user.phone}
|
|
142
|
+
id="phone"
|
|
143
|
+
class="form-control"
|
|
144
|
+
placeholder="Phone"
|
|
145
|
+
autocomplete="tel-local"
|
|
146
|
+
/>
|
|
147
|
+
</div>
|
|
94
148
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
149
|
+
{#if message}
|
|
150
|
+
<p>{message}</p>
|
|
151
|
+
{/if}
|
|
98
152
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
153
|
+
<button type="button" on:click={update} class="btn btn-primary btn-lg">Update</button>
|
|
154
|
+
</form>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
103
157
|
</div>
|
|
104
158
|
|
|
105
159
|
<style lang="scss">
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
</style>
|
|
160
|
+
.card-body {
|
|
161
|
+
width: 25rem;
|
|
162
|
+
}
|
|
163
|
+
</style>
|
|
@@ -3,8 +3,9 @@ import type { PageServerLoad } from './$types'
|
|
|
3
3
|
|
|
4
4
|
export const load: PageServerLoad = ({ locals }) => {
|
|
5
5
|
const { user } = locals
|
|
6
|
-
if (user) {
|
|
7
|
-
|
|
6
|
+
if (user) {
|
|
7
|
+
// Redirect to home if user is logged in already
|
|
8
|
+
redirect(302, '/')
|
|
8
9
|
}
|
|
9
10
|
return {}
|
|
10
11
|
}
|