sveltekit-auth-example 1.0.19 → 1.0.20
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 +6 -2
- package/README.md +2 -1
- package/package.json +6 -6
- package/src/app.d.ts +10 -8
- package/src/{hooks.ts → hooks.server.ts} +0 -0
- package/src/lib/auth.ts +114 -114
- package/src/routes/+layout.svelte +46 -10
- package/src/routes/admin/+page.server.ts +0 -1
- package/src/routes/register/+page.svelte +44 -41
- package/src/service-worker.ts +74 -0
- package/src/stores.ts +5 -11
- package/svelte.config.js +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
# Backlog
|
|
2
|
-
* Add username and Avatar icon to menu bar
|
|
3
|
-
* Consider not setting defaultUser in loginSession as it would simplify +layout.svelte.
|
|
4
2
|
* Refactor $env/dynamic/private and public
|
|
5
3
|
* Add password complexity checking on /register and /profile pages (only checks for length currently despite what the pages say)
|
|
6
4
|
|
|
5
|
+
# 1.0.20
|
|
6
|
+
* Bump dependencies
|
|
7
|
+
* Add service-worker
|
|
8
|
+
* Add dropdown, avatarm and user's first name to navbar once user is logged in
|
|
9
|
+
* Refactor user session and update typing
|
|
10
|
+
|
|
7
11
|
# 1.0.19
|
|
8
12
|
* Added SvelteKit's cookies implementation in RequestEvent
|
|
9
13
|
* [Bug] Logout then go to http://localhost/admin gives error on auth.ts:39
|
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# SvelteKit Authentication and Authorization Example
|
|
2
2
|
|
|
3
3
|
This is an example of how to register, authenticate, and update users and limit their access to
|
|
4
|
-
areas of the website by role (admin, teacher, student).
|
|
4
|
+
areas of the website by role (admin, teacher, student). As almost every recent release of SvelteKit introduced breaking changes, this project attempts to
|
|
5
|
+
maintain compatibility with the latest release.
|
|
5
6
|
|
|
6
7
|
It's a Single Page App (SPA) built with SvelteKit and a PostgreSQL database back-end. Code is TypeScript and the website is styled using Bootstrap. PostgreSQL functions handle password hashing and UUID generation for the session ID. Unlike most authentication examples, this SPA does not use callbacks that redirect back to the site (causing the website to be reloaded with a visual flash).
|
|
7
8
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sveltekit-auth-example",
|
|
3
3
|
"description": "SvelteKit Authentication Example",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.20",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "Nate Stuyvesant",
|
|
7
7
|
"license": "https://github.com/nstuyvesant/sveltekit-auth-example/blob/master/LICENSE",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"format": "prettier --write ."
|
|
34
34
|
},
|
|
35
35
|
"engines": {
|
|
36
|
-
"node": "~18.
|
|
36
|
+
"node": "~18.9.0",
|
|
37
37
|
"npm": "^8.19.1"
|
|
38
38
|
},
|
|
39
39
|
"type": "module",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@sveltejs/adapter-node": "latest",
|
|
48
48
|
"@sveltejs/kit": "latest",
|
|
49
|
-
"@types/bootstrap": "5.2.
|
|
49
|
+
"@types/bootstrap": "5.2.4",
|
|
50
50
|
"@types/cookie": "^0.5.1",
|
|
51
51
|
"@types/google.accounts": "0.0.2",
|
|
52
52
|
"@types/jsonwebtoken": "^8.5.9",
|
|
@@ -60,12 +60,12 @@
|
|
|
60
60
|
"eslint-plugin-svelte3": "^4.0.0",
|
|
61
61
|
"prettier": "^2.7.1",
|
|
62
62
|
"prettier-plugin-svelte": "^2.7.0",
|
|
63
|
-
"sass": "^1.54.
|
|
64
|
-
"svelte": "^3.50.
|
|
63
|
+
"sass": "^1.54.9",
|
|
64
|
+
"svelte": "^3.50.1",
|
|
65
65
|
"svelte-check": "^2.9.0",
|
|
66
66
|
"svelte-preprocess": "^4.10.7",
|
|
67
67
|
"tslib": "^2.4.0",
|
|
68
|
-
"typescript": "^4.
|
|
68
|
+
"typescript": "^4.8.3",
|
|
69
69
|
"vite": "^3.1.0"
|
|
70
70
|
}
|
|
71
71
|
}
|
package/src/app.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
|
|
1
3
|
/// <reference types="bootstrap" />
|
|
2
4
|
/// <reference types="google.accounts" />
|
|
3
5
|
|
|
@@ -16,14 +18,14 @@ declare namespace App {
|
|
|
16
18
|
// interface PublicEnv {} // $env/dynamic/public
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
interface AuthenticationResult {
|
|
20
22
|
statusCode: number
|
|
21
23
|
status: string
|
|
22
24
|
user: User
|
|
23
25
|
sessionId: string
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
interface Credentials {
|
|
27
29
|
email: string
|
|
28
30
|
password: string
|
|
29
31
|
}
|
|
@@ -45,12 +47,12 @@ interface ImportMetaEnv {
|
|
|
45
47
|
VITE_GOOGLE_CLIENT_ID: string
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
interface MessageAddressee {
|
|
49
51
|
email: string
|
|
50
52
|
name?: string
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
|
|
55
|
+
interface Message {
|
|
54
56
|
sender?: MessageAddressee
|
|
55
57
|
to?: MessageAddressee[]
|
|
56
58
|
subject: string
|
|
@@ -81,7 +83,7 @@ interface SendInBlueRequest extends RequestInit {
|
|
|
81
83
|
}
|
|
82
84
|
}
|
|
83
85
|
|
|
84
|
-
|
|
86
|
+
interface UserProperties {
|
|
85
87
|
id: number
|
|
86
88
|
role: 'student' | 'teacher' | 'admin'
|
|
87
89
|
password?: string
|
|
@@ -91,13 +93,13 @@ type User = {
|
|
|
91
93
|
phone?: string
|
|
92
94
|
}
|
|
93
95
|
|
|
94
|
-
type
|
|
96
|
+
type User = UserProperties | undefined
|
|
97
|
+
|
|
98
|
+
interface UserSession {
|
|
95
99
|
id: string,
|
|
96
100
|
user: User
|
|
97
101
|
}
|
|
98
102
|
|
|
99
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
100
|
-
|
|
101
103
|
interface Window {
|
|
102
104
|
google?: any
|
|
103
105
|
grecaptcha: any
|
|
File without changes
|
package/src/lib/auth.ts
CHANGED
|
@@ -1,130 +1,130 @@
|
|
|
1
|
-
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
|
|
3
|
+
import type { Page } from '@sveltejs/kit'
|
|
2
4
|
import type { Readable, Writable } from 'svelte/store'
|
|
3
5
|
import { config } from '$lib/config'
|
|
4
|
-
import { defaultUser } from '../stores'
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
export default function useAuth(
|
|
8
|
+
page: Readable<Page>,
|
|
9
|
+
loginSession: Writable<User>,
|
|
10
|
+
goto: (
|
|
11
|
+
url: string | URL,
|
|
12
|
+
opts?: { replaceState?: boolean; noscroll?: boolean; keepfocus?: boolean; state?: any }
|
|
13
|
+
) => Promise<any>
|
|
14
|
+
) {
|
|
15
|
+
let user: User
|
|
16
|
+
loginSession.subscribe((value) => {
|
|
17
|
+
user = value
|
|
18
|
+
})
|
|
12
19
|
|
|
13
|
-
|
|
14
|
-
|
|
20
|
+
let referrer: string | null
|
|
21
|
+
page.subscribe((value) => {
|
|
15
22
|
referrer = value.url.searchParams.get('referrer')
|
|
16
23
|
})
|
|
17
24
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (res.ok) {
|
|
28
|
-
const fromEndpoint = await res.json()
|
|
29
|
-
loginSession.set(fromEndpoint.user) // update loginSession store
|
|
30
|
-
const { role } = fromEndpoint.user
|
|
31
|
-
if (referrer) return goto(referrer)
|
|
32
|
-
if (role === 'teacher') return goto('/teachers')
|
|
33
|
-
if (role === 'admin') return goto('/admin')
|
|
34
|
-
if (location.pathname === '/login') goto('/') // logged in so go home
|
|
35
|
-
}
|
|
36
|
-
}
|
|
25
|
+
async function googleCallback(response: GoogleCredentialResponse) {
|
|
26
|
+
const res = await fetch('/auth/google', {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: {
|
|
29
|
+
'Content-Type': 'application/json'
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify({ token: response.credential })
|
|
32
|
+
})
|
|
37
33
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
if (res.ok) {
|
|
35
|
+
const fromEndpoint = await res.json()
|
|
36
|
+
loginSession.set(fromEndpoint.user) // update loginSession store
|
|
37
|
+
const { role } = fromEndpoint.user
|
|
38
|
+
if (referrer) return goto(referrer)
|
|
39
|
+
if (role === 'teacher') return goto('/teachers')
|
|
40
|
+
if (role === 'admin') return goto('/admin')
|
|
41
|
+
if (location.pathname === '/login') goto('/') // logged in so go home
|
|
42
|
+
}
|
|
43
|
+
}
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
theme: 'filled_blue',
|
|
46
|
-
size: 'large',
|
|
47
|
-
width: '367'
|
|
48
|
-
}
|
|
49
|
-
)
|
|
50
|
-
}
|
|
45
|
+
function initializeSignInWithGoogle(htmlId?: string) {
|
|
46
|
+
const { id } = window.google.accounts // assumes <script src="https://accounts.google.com/gsi/client" async defer></script> is in app.html
|
|
47
|
+
id.initialize({ client_id: config.googleClientId, callback: googleCallback })
|
|
51
48
|
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
if (htmlId) {
|
|
50
|
+
// render button instead of prompt
|
|
51
|
+
return id.renderButton(document.getElementById(htmlId), {
|
|
52
|
+
theme: 'filled_blue',
|
|
53
|
+
size: 'large',
|
|
54
|
+
width: '367'
|
|
55
|
+
})
|
|
56
|
+
}
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
...s,
|
|
58
|
-
user
|
|
59
|
-
}))
|
|
60
|
-
}
|
|
58
|
+
if (!user) id.prompt()
|
|
59
|
+
}
|
|
61
60
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
61
|
+
async function registerLocal(user: User) {
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetch('/auth/register', {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
body: JSON.stringify(user), // server needs to ignore user.role and always set it to 'student'
|
|
66
|
+
headers: {
|
|
67
|
+
'Content-Type': 'application/json'
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
if (!res.ok) {
|
|
71
|
+
if (res.status == 401)
|
|
72
|
+
// user already existed and passwords didn't match (otherwise, we login the user)
|
|
73
|
+
throw new Error('Sorry, that username is already in use.')
|
|
74
|
+
throw new Error(res.statusText) // should only occur if there's a database error
|
|
75
|
+
}
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
77
|
+
// res.ok
|
|
78
|
+
const fromEndpoint = await res.json()
|
|
79
|
+
loginSession.set(fromEndpoint.user) // update store so user is logged in
|
|
80
|
+
goto('/')
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.error('Register error', err)
|
|
83
|
+
if (err instanceof Error) {
|
|
84
|
+
throw new Error(err.message)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
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
|
-
|
|
89
|
+
async function loginLocal(credentials: Credentials) {
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetch('/auth/login', {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
body: JSON.stringify(credentials),
|
|
94
|
+
headers: {
|
|
95
|
+
'Content-Type': 'application/json'
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
const fromEndpoint = await res.json()
|
|
99
|
+
if (res.ok) {
|
|
100
|
+
loginSession.set(fromEndpoint.user)
|
|
101
|
+
const { role } = fromEndpoint.user
|
|
102
|
+
if (referrer) return goto(referrer)
|
|
103
|
+
if (role === 'teacher') return goto('/teachers')
|
|
104
|
+
if (role === 'admin') return goto('/admin')
|
|
105
|
+
return goto('/')
|
|
106
|
+
} else {
|
|
107
|
+
throw new Error(fromEndpoint.message)
|
|
108
|
+
}
|
|
109
|
+
} catch (err) {
|
|
110
|
+
if (err instanceof Error) {
|
|
111
|
+
console.error('Login error', err)
|
|
112
|
+
throw new Error(err.message)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
117
|
+
async function logout() {
|
|
118
|
+
// Request server delete httpOnly cookie called loginSession
|
|
119
|
+
const url = '/auth/logout'
|
|
120
|
+
const res = await fetch(url, {
|
|
121
|
+
method: 'POST'
|
|
122
|
+
})
|
|
123
|
+
if (res.ok) {
|
|
124
|
+
loginSession.set(undefined) // delete loginSession.user from
|
|
125
|
+
goto('/login')
|
|
126
|
+
} else console.error(`Logout not successful: ${res.statusText} (${res.status})`)
|
|
127
|
+
}
|
|
128
128
|
|
|
129
|
-
|
|
130
|
-
}
|
|
129
|
+
return { initializeSignInWithGoogle, registerLocal, loginLocal, logout }
|
|
130
|
+
}
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
onMount(async () => {
|
|
22
22
|
await import('bootstrap/js/dist/collapse') // lots of ways to load Bootstrap but prefer this approach to avoid SSR issues
|
|
23
|
+
await import('bootstrap/js/dist/dropdown')
|
|
23
24
|
Toast = (await import('bootstrap/js/dist/toast')).default
|
|
24
25
|
initializeSignInWithGoogle()
|
|
25
26
|
})
|
|
@@ -37,20 +38,50 @@
|
|
|
37
38
|
|
|
38
39
|
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
|
39
40
|
<div class="container">
|
|
40
|
-
<a class="navbar-brand" href="/">Auth</a>
|
|
41
|
+
<a class="navbar-brand" href="/">SvelteKit-Auth-Example</a>
|
|
41
42
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
|
42
43
|
<span class="navbar-toggler-icon"></span>
|
|
43
44
|
</button>
|
|
44
45
|
<div class="collapse navbar-collapse" id="navbarMain">
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
<a class="nav-link" href="/
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
|
|
47
|
+
<ul class="navbar-nav me-5">
|
|
48
|
+
<li class="nav-item"><a class="nav-link active" aria-current="page" href="/">Home</a></li>
|
|
49
|
+
<li class="nav-item"><a class="nav-link" href="/info">Info</a></li>
|
|
50
|
+
|
|
51
|
+
{#if $loginSession}
|
|
52
|
+
{#if $loginSession.role == 'admin'}
|
|
53
|
+
<li class="nav-item"><a class="nav-link" href="/admin">Admin</a></li>
|
|
54
|
+
{/if}
|
|
55
|
+
{#if $loginSession.role != 'student'}
|
|
56
|
+
<li class="nav-item"><a class="nav-link" href="/teachers">Teachers</a></li>
|
|
57
|
+
{/if}
|
|
58
|
+
{/if}
|
|
59
|
+
</ul>
|
|
60
|
+
<ul class="navbar-nav">
|
|
61
|
+
{#if $loginSession}
|
|
62
|
+
<li class="nav-item dropdown">
|
|
63
|
+
<a class="nav-link dropdown-toggle" href={'#'} role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
|
64
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="avatar" viewBox="0 0 16 16">
|
|
65
|
+
<path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
|
|
66
|
+
<path fill-rule="evenodd" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1z"/>
|
|
67
|
+
</svg>
|
|
68
|
+
{$loginSession.firstName}
|
|
69
|
+
</a>
|
|
70
|
+
<ul class="dropdown-menu">
|
|
71
|
+
<li>
|
|
72
|
+
<a class="dropdown-item" href="/profile">Profile</a>
|
|
73
|
+
</li>
|
|
74
|
+
<li>
|
|
75
|
+
<a on:click|preventDefault={logout} class="dropdown-item" class:d-none={!$loginSession || $loginSession.id === 0} href={'#'}>Logout</a>
|
|
76
|
+
</li>
|
|
77
|
+
</ul>
|
|
78
|
+
</li>
|
|
79
|
+
{:else}
|
|
80
|
+
<li class="nav-item">
|
|
81
|
+
<a class="nav-link" href="/login">Login</a>
|
|
82
|
+
</li>
|
|
83
|
+
{/if}
|
|
84
|
+
</ul>
|
|
54
85
|
</div>
|
|
55
86
|
</div>
|
|
56
87
|
</nav>
|
|
@@ -80,4 +111,9 @@
|
|
|
80
111
|
.toast {
|
|
81
112
|
z-index: 9999;
|
|
82
113
|
}
|
|
114
|
+
|
|
115
|
+
.avatar {
|
|
116
|
+
position: relative;
|
|
117
|
+
top: -1.5px;
|
|
118
|
+
}
|
|
83
119
|
</style>
|
|
@@ -4,7 +4,6 @@ import type { PageServerLoad } from './$types'
|
|
|
4
4
|
export const load: PageServerLoad = async ({locals})=> {
|
|
5
5
|
const { user } = locals
|
|
6
6
|
const authorized = ['admin']
|
|
7
|
-
console.log('admin/+page.server.ts', user)
|
|
8
7
|
if (!user || !authorized.includes(user.role)) {
|
|
9
8
|
throw redirect(302, '/login?referrer=/admin')
|
|
10
9
|
}
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
const passwordMatch = () => {
|
|
57
|
+
if (!user) return false // placate TypeScript
|
|
57
58
|
if (!user.password) user.password = ''
|
|
58
59
|
return user.password == confirmPassword.value
|
|
59
60
|
}
|
|
@@ -68,47 +69,49 @@
|
|
|
68
69
|
<div class="card-body">
|
|
69
70
|
<h4><strong>Register</strong></h4>
|
|
70
71
|
<p>Welcome to our community.</p>
|
|
71
|
-
|
|
72
|
-
<
|
|
73
|
-
<div
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
72
|
+
{#if user}
|
|
73
|
+
<form id="register" autocomplete="on" novalidate class="mt-3">
|
|
74
|
+
<div class="mb-3">
|
|
75
|
+
<div id="googleButton"></div>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="mb-3">
|
|
78
|
+
<label class="form-label" for="email">Email</label>
|
|
79
|
+
<input bind:this={focusedField} type="email" class="form-control" bind:value={user.email} required placeholder="Email" id="email" autocomplete="email"/>
|
|
80
|
+
<div class="invalid-feedback">Email address required</div>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="mb-3">
|
|
83
|
+
<label class="form-label" for="password">Password</label>
|
|
84
|
+
<input type="password" id="password" class="form-control" bind:value={user.password} required minlength="8" maxlength="80" placeholder="Password" autocomplete="new-password"/>
|
|
85
|
+
<div class="invalid-feedback">Password with 8 chars or more required</div>
|
|
86
|
+
<div class="form-text">Password minimum length 8, must have one capital letter, 1 number, and one unique character.</div>
|
|
87
|
+
</div>
|
|
88
|
+
<div class="mb-3">
|
|
89
|
+
<label class="form-label" for="password">Confirm password</label>
|
|
90
|
+
<input type="password" id="password" class="form-control" bind:this={confirmPassword} required minlength="8" maxlength="80" placeholder="Password (again)" autocomplete="new-password"/>
|
|
91
|
+
<div class="form-text">Password minimum length 8, must have one capital letter, 1 number, and one unique character.</div>
|
|
92
|
+
</div>
|
|
93
|
+
<div class="mb-3">
|
|
94
|
+
<label class="form-label" for="firstName">First name</label>
|
|
95
|
+
<input bind:value={user.firstName} class="form-control" id="firstName" placeholder="First name" required autocomplete="given-name"/>
|
|
96
|
+
<div class="invalid-feedback">First name required</div>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="mb-3">
|
|
99
|
+
<label class="form-label" for="lastName">Last name</label>
|
|
100
|
+
<input bind:value={user.lastName} class="form-control" id="lastName" placeholder="Last name" required autocomplete="family-name"/>
|
|
101
|
+
<div class="invalid-feedback">Last name required</div>
|
|
102
|
+
</div>
|
|
103
|
+
<div class="mb-3">
|
|
104
|
+
<label class="form-label" for="phone">Phone</label>
|
|
105
|
+
<input type="tel" bind:value={user.phone} id="phone" class="form-control" placeholder="Phone" autocomplete="tel-local"/>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
{#if message}
|
|
109
|
+
<p class="text-danger">{message}</p>
|
|
110
|
+
{/if}
|
|
111
|
+
|
|
112
|
+
<button type="button" on:click={register} class="btn btn-primary btn-lg">Register</button>
|
|
113
|
+
</form>
|
|
114
|
+
{/if}
|
|
112
115
|
</div>
|
|
113
116
|
</div>
|
|
114
117
|
</div>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/// <reference lib="webworker" />
|
|
2
|
+
import { build, files, version } from '$service-worker'
|
|
3
|
+
|
|
4
|
+
const worker = <ServiceWorkerGlobalScope> <unknown> self
|
|
5
|
+
const cacheName = `cache${version}`
|
|
6
|
+
const toCache = build.concat(files)
|
|
7
|
+
const staticAssets = new Set(toCache)
|
|
8
|
+
|
|
9
|
+
worker.addEventListener('install', event => {
|
|
10
|
+
// console.log('[Service Worker] Installation')
|
|
11
|
+
event.waitUntil(
|
|
12
|
+
caches
|
|
13
|
+
.open(cacheName)
|
|
14
|
+
.then(cache => cache.addAll(toCache))
|
|
15
|
+
.then(() => {
|
|
16
|
+
worker.skipWaiting()
|
|
17
|
+
})
|
|
18
|
+
.catch(error => console.error(error))
|
|
19
|
+
)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
worker.addEventListener('activate', event => {
|
|
23
|
+
// console.log('[Service Worker] Activation')
|
|
24
|
+
event.waitUntil(
|
|
25
|
+
caches.keys()
|
|
26
|
+
.then(async (keys) => {
|
|
27
|
+
for (const key of keys) {
|
|
28
|
+
if (key !== cacheName) await caches.delete(key)
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
)
|
|
32
|
+
worker.clients.claim() // or should this be inside the caches.keys().then()?
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// Fetch from network into cache and fall back to cache if user offline
|
|
36
|
+
async function fetchAndCache(request: Request) {
|
|
37
|
+
const cache = await caches.open(`offline${version}`)
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch(request)
|
|
41
|
+
cache.put(request, response.clone())
|
|
42
|
+
return response
|
|
43
|
+
} catch (err) {
|
|
44
|
+
const response = await cache.match(request)
|
|
45
|
+
if (response) return response
|
|
46
|
+
throw err
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
worker.addEventListener('fetch', event => {
|
|
51
|
+
if (event.request.method !== 'GET' || event.request.headers.has('range')) return
|
|
52
|
+
|
|
53
|
+
const url = new URL(event.request.url)
|
|
54
|
+
// console.log(`[Service Worker] Fetch ${url}`)
|
|
55
|
+
|
|
56
|
+
// don't try to handle data: or blob: URIs
|
|
57
|
+
const isHttp = url.protocol.startsWith('http')
|
|
58
|
+
const isDevServerRequest = url.hostname === self.location.hostname && url.port !== self.location.port
|
|
59
|
+
const isStaticAsset = url.host === self.location.host && staticAssets.has(url.pathname)
|
|
60
|
+
const skipBecauseUncached = event.request.cache === 'only-if-cached' && !isStaticAsset
|
|
61
|
+
|
|
62
|
+
if (isHttp && !isDevServerRequest && !skipBecauseUncached) {
|
|
63
|
+
event.respondWith(
|
|
64
|
+
(async () => {
|
|
65
|
+
// always serve static files and bundler-generated assets from cache.
|
|
66
|
+
// if your application has other URLs with data that will never change,
|
|
67
|
+
// set this variable to true for them and they will only be fetched once.
|
|
68
|
+
const cachedAsset = isStaticAsset && (await caches.match(event.request))
|
|
69
|
+
|
|
70
|
+
return cachedAsset || fetchAndCache(event.request)
|
|
71
|
+
})()
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
})
|
package/src/stores.ts
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
|
-
import { writable } from 'svelte/store'
|
|
1
|
+
import { writable, type Writable } from 'svelte/store'
|
|
2
2
|
|
|
3
3
|
export const toast = writable({
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
title: '',
|
|
5
|
+
body: '',
|
|
6
|
+
isOpen: false
|
|
7
7
|
})
|
|
8
8
|
|
|
9
9
|
// While server determines whether the user is logged in by examining RequestEvent.locals.user, the
|
|
10
10
|
// loginSession is updated so all parts of the SPA client-side see the user and role.
|
|
11
|
-
|
|
12
|
-
export const defaultUser: User = {
|
|
13
|
-
id: 0, // the not-logged-in user id
|
|
14
|
-
role: 'student' // default role for users who are not logged in
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export const loginSession = writable(defaultUser)
|
|
11
|
+
export const loginSession = <Writable<User>> writable(undefined)
|