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