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 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.19",
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.8.0",
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.3",
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.8",
64
- "svelte": "^3.50.0",
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.7.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
- type AuthenticationResult = {
21
+ interface AuthenticationResult {
20
22
  statusCode: number
21
23
  status: string
22
24
  user: User
23
25
  sessionId: string
24
26
  }
25
27
 
26
- type Credentials = {
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
- type MessageAddressee = {
50
+ interface MessageAddressee {
49
51
  email: string
50
52
  name?: string
51
53
  }
52
54
 
53
- type Message = {
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
- type User = {
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 UserSession = {
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
- import type { Page } from '@sveltejs/kit'
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
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
- export default function useAuth(page: Readable<Page>, loginSession: Writable<User>, goto: (url: string | URL, opts?: { replaceState?: boolean; noscroll?: boolean; keepfocus?: boolean; state?: any; }) => Promise<any>) {
8
- let user: User
9
- loginSession.subscribe(value => {
10
- user = value
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
- let referrer: string | null
14
- page.subscribe(value => {
20
+ let referrer: string | null
21
+ page.subscribe((value) => {
15
22
  referrer = value.url.searchParams.get('referrer')
16
23
  })
17
24
 
18
- async function googleCallback(response: GoogleCredentialResponse) {
19
- const res = await fetch('/auth/google', {
20
- method: 'POST',
21
- headers: {
22
- 'Content-Type': 'application/json'
23
- },
24
- body: JSON.stringify({ token: response.credential })
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
- function initializeSignInWithGoogle(htmlId?: string) {
39
- const { id } = window.google.accounts // assumes <script src="https://accounts.google.com/gsi/client" async defer></script> is in app.html
40
- id.initialize({ client_id: config.googleClientId, callback: googleCallback })
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
- if (htmlId) { // render button instead of prompt
43
- return id.renderButton(
44
- document.getElementById(htmlId), {
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
- if (!user) id.prompt()
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
- function setloginSession(user: User | null) {
56
- loginSession.update(s => ({
57
- ...s,
58
- user
59
- }))
60
- }
58
+ if (!user) id.prompt()
59
+ }
61
60
 
62
- async function registerLocal(user: User) {
63
- try {
64
- const res = await fetch('/auth/register', {
65
- method: 'POST',
66
- body: JSON.stringify(user), // server needs to ignore user.role and always set it to 'student'
67
- headers: {
68
- 'Content-Type': 'application/json'
69
- }
70
- })
71
- if (!res.ok) {
72
- if (res.status == 401) // 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
- }
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
- // 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
- }
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
- 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
- setloginSession(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
- }
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
- 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(defaultUser) // delete loginSession.user from
125
- goto('/login')
126
- } else console.error(`Logout not successful: ${res.statusText} (${res.status})`)
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
- return { initializeSignInWithGoogle, registerLocal, loginLocal, logout }
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
- <div class="navbar-nav">
46
- <a class="nav-link active" aria-current="page" href="/">Home</a>
47
- <a class="nav-link" href="/info">Info</a>
48
- <a class="nav-link" class:d-none={!$loginSession || $loginSession.id === 0} href="/profile">Profile</a>
49
- <a class="nav-link" class:d-none={!$loginSession || $loginSession?.role !== 'admin'} href="/admin">Admin</a>
50
- <a class="nav-link" class:d-none={!$loginSession || $loginSession?.role === 'student'} href="/teachers">Teachers</a>
51
- <a class="nav-link" class:d-none={!!$loginSession} href="/login">Login</a>
52
- <a on:click|preventDefault={logout} class="nav-link" class:d-none={!$loginSession || $loginSession.id === 0} href={'#'}>Logout</a>
53
- </div>
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
- <form id="register" autocomplete="on" novalidate class="mt-3">
72
- <div class="mb-3">
73
- <div id="googleButton"></div>
74
- </div>
75
- <div class="mb-3">
76
- <label class="form-label" for="email">Email</label>
77
- <input bind:this={focusedField} type="email" class="form-control" bind:value={user.email} required placeholder="Email" id="email" autocomplete="email"/>
78
- <div class="invalid-feedback">Email address required</div>
79
- </div>
80
- <div class="mb-3">
81
- <label class="form-label" for="password">Password</label>
82
- <input type="password" id="password" class="form-control" bind:value={user.password} required minlength="8" maxlength="80" placeholder="Password" autocomplete="new-password"/>
83
- <div class="invalid-feedback">Password with 8 chars or more required</div>
84
- <div class="form-text">Password minimum length 8, must have one capital letter, 1 number, and one unique character.</div>
85
- </div>
86
- <div class="mb-3">
87
- <label class="form-label" for="password">Confirm password</label>
88
- <input type="password" id="password" class="form-control" bind:this={confirmPassword} required minlength="8" maxlength="80" placeholder="Password (again)" autocomplete="new-password"/>
89
- <div class="form-text">Password minimum length 8, must have one capital letter, 1 number, and one unique character.</div>
90
- </div>
91
- <div class="mb-3">
92
- <label class="form-label" for="firstName">First name</label>
93
- <input bind:value={user.firstName} class="form-control" id="firstName" placeholder="First name" required autocomplete="given-name"/>
94
- <div class="invalid-feedback">First name required</div>
95
- </div>
96
- <div class="mb-3">
97
- <label class="form-label" for="lastName">Last name</label>
98
- <input bind:value={user.lastName} class="form-control" id="lastName" placeholder="Last name" required autocomplete="family-name"/>
99
- <div class="invalid-feedback">Last name required</div>
100
- </div>
101
- <div class="mb-3">
102
- <label class="form-label" for="phone">Phone</label>
103
- <input type="tel" bind:value={user.phone} id="phone" class="form-control" placeholder="Phone" autocomplete="tel-local"/>
104
- </div>
105
-
106
- {#if message}
107
- <p class="text-danger">{message}</p>
108
- {/if}
109
-
110
- <button type="button" on:click={register} class="btn btn-primary btn-lg">Register</button>
111
- </form>
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
- title: '',
5
- body: '',
6
- isOpen: false
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)
package/svelte.config.js CHANGED
@@ -31,6 +31,9 @@ const config = {
31
31
  'object-src': ['none'],
32
32
  'base-uri': ['self']
33
33
  }
34
+ },
35
+ files: {
36
+ serviceWorker: 'src/service-worker.ts'
34
37
  }
35
38
  }
36
39
  }