sveltekit-auth-example 1.0.18 → 1.0.21
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/.env-sample +8 -0
- package/CHANGELOG.md +14 -4
- package/README.md +6 -5
- package/package.json +7 -11
- package/src/app.d.ts +23 -15
- package/src/{hooks.ts → hooks.server.ts} +7 -12
- package/src/lib/auth.ts +115 -115
- package/src/routes/+layout.svelte +46 -10
- package/src/routes/_db.ts +2 -4
- package/src/routes/_send-in-blue.ts +3 -10
- package/src/routes/admin/+page.server.ts +2 -2
- package/src/routes/auth/[slug]/+server.ts +2 -1
- package/src/routes/auth/forgot/+server.ts +1 -5
- package/src/routes/auth/google/+server.ts +9 -11
- package/src/routes/auth/reset/+server.ts +27 -28
- package/src/routes/profile/+page.server.ts +1 -1
- package/src/routes/register/+page.svelte +44 -41
- package/src/routes/teachers/+page.server.ts +2 -1
- package/src/service-worker.ts +74 -0
- package/src/stores.ts +5 -11
- package/svelte.config.js +3 -0
- package/src/lib/config.ts +0 -4
package/.env-sample
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
DATABASE_URL=postgres://REPLACE_WITH_USER:REPLACE_WITH_PASSWORD@localhost:5432/auth
|
|
2
|
+
DOMAIN=http://localhost:3000
|
|
3
|
+
JWT_SECRET=replace_with_your_own
|
|
4
|
+
SEND_IN_BLUE_URL=https://api.sendinblue.com
|
|
5
|
+
SEND_IN_BLUE_KEY=REPLACE_WITH_YOUR_OWN
|
|
6
|
+
SEND_IN_BLUE_FROM='{ "email":"sender@example.com", "name":"First Last" }'
|
|
7
|
+
SEND_IN_BLUE_ADMINS='{ "email":"admin@example.com", "name":"First Last" }'
|
|
8
|
+
PUBLIC_GOOGLE_CLIENT_ID=REPLACE_WITH_YOUR_OWN
|
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
# Backlog
|
|
2
|
-
* Add username and Avatar icon to menu bar
|
|
3
|
-
* [Possible Bug] Getting HTTP 401 on https://play.google.com/log?format=json&hasfast=true&authuser=0 from google-auth-library. As I didn't explicitly request logging, it could be that Safari is preventing Google from further invading our privacy. Will require some investigation. The site works regardless.
|
|
4
|
-
* Consider not setting defaultUser in loginSession as it would simplify +layout.svelte.
|
|
5
|
-
* Refactor $env/dynamic/private and public
|
|
6
2
|
* Add password complexity checking on /register and /profile pages (only checks for length currently despite what the pages say)
|
|
7
3
|
|
|
4
|
+
# 1.0.21
|
|
5
|
+
* Refactor to use $env/static/private and public, dropping dotenv dependency
|
|
6
|
+
* Remove @types/cookie and bootstrap-icons dependencies
|
|
7
|
+
|
|
8
|
+
# 1.0.20
|
|
9
|
+
* Bump dependencies
|
|
10
|
+
* Add service-worker
|
|
11
|
+
* Add dropdown, avatarm and user's first name to navbar once user is logged in
|
|
12
|
+
* Refactor user session and update typing
|
|
13
|
+
|
|
14
|
+
# 1.0.19
|
|
15
|
+
* Added SvelteKit's cookies implementation in RequestEvent
|
|
16
|
+
* [Bug] Logout then go to http://localhost/admin gives error on auth.ts:39
|
|
17
|
+
|
|
8
18
|
# 1.0.18
|
|
9
19
|
* Bump dependencies
|
|
10
20
|
|
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
|
|
|
@@ -27,9 +28,9 @@ The website supports two types of authentication:
|
|
|
27
28
|
The forgot password functionality uses [**SendInBlue**](https://www.sendinblue.com) to send the email. You would need to have a **SendInBlue** account and set three environmental variables. Email sending is in /src/routes/auth/forgot.ts. This code could easily be replaced by nodemailer or something similar. Note: I have no affliation with **SendInBlue** (just happen to be familiar with their API because of another project).
|
|
28
29
|
|
|
29
30
|
## Prerequisites
|
|
30
|
-
- PostgreSQL 14 or higher
|
|
31
|
-
- Node.js 18.
|
|
32
|
-
- npm 8.
|
|
31
|
+
- PostgreSQL 14.5 or higher
|
|
32
|
+
- Node.js 18.9.0 or higher
|
|
33
|
+
- npm 8.19.1 or higher
|
|
33
34
|
- Google API client
|
|
34
35
|
- SendInBlue account (only used for emailing password reset link - the sample can run without it but forgot password will not work)
|
|
35
36
|
|
|
@@ -61,7 +62,7 @@ SEND_IN_BLUE_URL=https://api.sendinblue.com
|
|
|
61
62
|
SEND_IN_BLUE_KEY=replace_with_your_own
|
|
62
63
|
SEND_IN_BLUE_FROM='{ "email":"jdoe@example.com", "name":"John Doe" }'
|
|
63
64
|
SEND_IN_BLUE_ADMINS='{ "email":"jdoe@example.com", "name":"John Doe" }'
|
|
64
|
-
|
|
65
|
+
PUBLIC_GOOGLE_CLIENT_ID=replace_with_your_own
|
|
65
66
|
```
|
|
66
67
|
|
|
67
68
|
## Run locally
|
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.21",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "Nate Stuyvesant",
|
|
7
7
|
"license": "https://github.com/nstuyvesant/sveltekit-auth-example/blob/master/LICENSE",
|
|
@@ -33,13 +33,11 @@
|
|
|
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",
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"cookie": "^0.5.0",
|
|
42
|
-
"dotenv": "^16.0.2",
|
|
43
41
|
"google-auth-library": "^8.5.1",
|
|
44
42
|
"jsonwebtoken": "^8.5.1",
|
|
45
43
|
"pg": "^8.8.0"
|
|
@@ -47,26 +45,24 @@
|
|
|
47
45
|
"devDependencies": {
|
|
48
46
|
"@sveltejs/adapter-node": "latest",
|
|
49
47
|
"@sveltejs/kit": "latest",
|
|
50
|
-
"@types/bootstrap": "5.2.
|
|
51
|
-
"@types/cookie": "^0.5.1",
|
|
48
|
+
"@types/bootstrap": "5.2.4",
|
|
52
49
|
"@types/google.accounts": "0.0.2",
|
|
53
50
|
"@types/jsonwebtoken": "^8.5.9",
|
|
54
51
|
"@types/pg": "^8.6.5",
|
|
55
52
|
"@typescript-eslint/eslint-plugin": "^5.36.1",
|
|
56
53
|
"@typescript-eslint/parser": "^5.36.1",
|
|
57
|
-
"bootstrap": "^5.2.
|
|
58
|
-
"bootstrap-icons": "^1.9.1",
|
|
54
|
+
"bootstrap": "^5.2.1",
|
|
59
55
|
"eslint": "^8.23.0",
|
|
60
56
|
"eslint-config-prettier": "^8.5.0",
|
|
61
57
|
"eslint-plugin-svelte3": "^4.0.0",
|
|
62
58
|
"prettier": "^2.7.1",
|
|
63
59
|
"prettier-plugin-svelte": "^2.7.0",
|
|
64
|
-
"sass": "^1.54.
|
|
65
|
-
"svelte": "^3.50.
|
|
60
|
+
"sass": "^1.54.9",
|
|
61
|
+
"svelte": "^3.50.1",
|
|
66
62
|
"svelte-check": "^2.9.0",
|
|
67
63
|
"svelte-preprocess": "^4.10.7",
|
|
68
64
|
"tslib": "^2.4.0",
|
|
69
|
-
"typescript": "^4.
|
|
65
|
+
"typescript": "^4.8.3",
|
|
70
66
|
"vite": "^3.1.0"
|
|
71
67
|
}
|
|
72
68
|
}
|
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
|
|
|
@@ -11,19 +13,29 @@ declare namespace App {
|
|
|
11
13
|
|
|
12
14
|
// interface Platform {}
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
interface PrivateEnv { // $env/dynamic/private
|
|
17
|
+
DATABASE_URL: string
|
|
18
|
+
DOMAIN: string
|
|
19
|
+
JWT_SECRET: string
|
|
20
|
+
SEND_IN_BLUE_URL: string
|
|
21
|
+
SEND_IN_BLUE_KEY: string
|
|
22
|
+
SEND_IN_BLUE_FROM: string
|
|
23
|
+
SEND_IN_BLUE_ADMINS: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface PublicEnv { // $env/dynamic/public
|
|
27
|
+
PUBLIC_GOOGLE_CLIENT_ID: string
|
|
28
|
+
}
|
|
17
29
|
}
|
|
18
30
|
|
|
19
|
-
|
|
31
|
+
interface AuthenticationResult {
|
|
20
32
|
statusCode: number
|
|
21
33
|
status: string
|
|
22
34
|
user: User
|
|
23
35
|
sessionId: string
|
|
24
36
|
}
|
|
25
37
|
|
|
26
|
-
|
|
38
|
+
interface Credentials {
|
|
27
39
|
email: string
|
|
28
40
|
password: string
|
|
29
41
|
}
|
|
@@ -41,16 +53,12 @@ interface GoogleCredentialResponse {
|
|
|
41
53
|
| 'btn_confirm_add_session'
|
|
42
54
|
}
|
|
43
55
|
|
|
44
|
-
interface
|
|
45
|
-
VITE_GOOGLE_CLIENT_ID: string
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
type MessageAddressee = {
|
|
56
|
+
interface MessageAddressee {
|
|
49
57
|
email: string
|
|
50
58
|
name?: string
|
|
51
59
|
}
|
|
52
60
|
|
|
53
|
-
|
|
61
|
+
interface Message {
|
|
54
62
|
sender?: MessageAddressee
|
|
55
63
|
to?: MessageAddressee[]
|
|
56
64
|
subject: string
|
|
@@ -81,7 +89,7 @@ interface SendInBlueRequest extends RequestInit {
|
|
|
81
89
|
}
|
|
82
90
|
}
|
|
83
91
|
|
|
84
|
-
|
|
92
|
+
interface UserProperties {
|
|
85
93
|
id: number
|
|
86
94
|
role: 'student' | 'teacher' | 'admin'
|
|
87
95
|
password?: string
|
|
@@ -91,13 +99,13 @@ type User = {
|
|
|
91
99
|
phone?: string
|
|
92
100
|
}
|
|
93
101
|
|
|
94
|
-
type
|
|
102
|
+
type User = UserProperties | undefined
|
|
103
|
+
|
|
104
|
+
interface UserSession {
|
|
95
105
|
id: string,
|
|
96
106
|
user: User
|
|
97
107
|
}
|
|
98
108
|
|
|
99
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
100
|
-
|
|
101
109
|
interface Window {
|
|
102
110
|
google?: any
|
|
103
111
|
grecaptcha: any
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import * as cookie from 'cookie'
|
|
2
1
|
import type { Handle, RequestEvent } from '@sveltejs/kit'
|
|
3
2
|
import { query } from './routes/_db'
|
|
4
3
|
|
|
5
4
|
// Attach authorization to each server request (role may have changed)
|
|
6
|
-
async function
|
|
5
|
+
async function attachUserToRequestEvent(sessionId: string, event: RequestEvent) {
|
|
7
6
|
const sql = `
|
|
8
7
|
SELECT * FROM get_session($1);`
|
|
9
8
|
const { rows } = await query(sql, [sessionId])
|
|
@@ -12,24 +11,20 @@ async function attachUserToRequest(sessionId: string, event: RequestEvent) {
|
|
|
12
11
|
}
|
|
13
12
|
}
|
|
14
13
|
|
|
15
|
-
function deleteCookieIfNoUser(event: RequestEvent, response: Response) {
|
|
16
|
-
if (!event.locals.user) {
|
|
17
|
-
response.headers.set('Set-Cookie', `session=; Path=/; HttpOnly; SameSite=Lax; Expires=${new Date().toUTCString()}`)
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
14
|
// Invoked for each endpoint called and initially for SSR router
|
|
22
15
|
export const handle: Handle = async ({ event, resolve }) => {
|
|
16
|
+
const { cookies } = event
|
|
17
|
+
const sessionId = cookies.get('session')
|
|
23
18
|
|
|
24
19
|
// before endpoint or page is called
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
await attachUserToRequest(cookies.session, event)
|
|
20
|
+
if (sessionId) {
|
|
21
|
+
await attachUserToRequestEvent(sessionId, event)
|
|
28
22
|
}
|
|
29
23
|
|
|
30
24
|
const response = await resolve(event)
|
|
31
25
|
|
|
32
26
|
// after endpoint or page is called
|
|
33
|
-
|
|
27
|
+
if (!event.locals.user) cookies.delete('session')
|
|
28
|
+
|
|
34
29
|
return response
|
|
35
30
|
}
|
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
|
-
import {
|
|
4
|
-
import { defaultUser } from '../stores'
|
|
5
|
+
import { PUBLIC_GOOGLE_CLIENT_ID } from '$env/static/public'
|
|
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: PUBLIC_GOOGLE_CLIENT_ID, 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>
|
package/src/routes/_db.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import dotenv from 'dotenv'
|
|
3
2
|
import type { QueryResult} from 'pg'
|
|
4
3
|
import pg from 'pg'
|
|
5
|
-
|
|
6
|
-
dotenv.config()
|
|
4
|
+
import { DATABASE_URL } from '$env/static/private'
|
|
7
5
|
|
|
8
6
|
const pool = new pg.Pool({
|
|
9
7
|
max: 10, // default
|
|
10
|
-
connectionString:
|
|
8
|
+
connectionString: DATABASE_URL,
|
|
11
9
|
ssl: {
|
|
12
10
|
rejectUnauthorized: false
|
|
13
11
|
}
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { SEND_IN_BLUE_KEY, SEND_IN_BLUE_URL, SEND_IN_BLUE_FROM, SEND_IN_BLUE_ADMINS } from '$env/static/private'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const SEND_IN_BLUE_KEY = process.env.SEND_IN_BLUE_KEY
|
|
6
|
-
const SEND_IN_BLUE_URL = process.env.SEND_IN_BLUE_URL
|
|
7
|
-
const SEND_IN_BLUE_FROM = <MessageAddressee> JSON.parse(process.env.SEND_IN_BLUE_FROM || '')
|
|
8
|
-
const SEND_IN_BLUE_ADMINS = <MessageAddressee> JSON.parse(process.env.SEND_IN_BLUE_ADMINS || '')
|
|
3
|
+
const sender = <MessageAddressee> JSON.parse(SEND_IN_BLUE_FROM || '')
|
|
4
|
+
const to = <MessageAddressee> JSON.parse(SEND_IN_BLUE_ADMINS || '')
|
|
9
5
|
|
|
10
6
|
// POST or PUT submission to SendInBlue
|
|
11
7
|
const submit = async (method: string, url: string, data: Partial<SendInBlueContact> | SendInBlueMessage) => {
|
|
@@ -23,7 +19,4 @@ const submit = async (method: string, url: string, data: Partial<SendInBlueConta
|
|
|
23
19
|
}
|
|
24
20
|
}
|
|
25
21
|
|
|
26
|
-
const sender = SEND_IN_BLUE_FROM
|
|
27
|
-
const to = SEND_IN_BLUE_ADMINS
|
|
28
|
-
|
|
29
22
|
export const sendMessage = async (message: Message) => submit('POST', '/v3/smtp/email', { sender, to: [to], ...message })
|
|
@@ -4,8 +4,8 @@ import type { PageServerLoad } from './$types'
|
|
|
4
4
|
export const load: PageServerLoad = async ({locals})=> {
|
|
5
5
|
const { user } = locals
|
|
6
6
|
const authorized = ['admin']
|
|
7
|
-
if (user
|
|
8
|
-
throw redirect(302, '/login?referrer=/admin')
|
|
7
|
+
if (!user || !authorized.includes(user.role)) {
|
|
8
|
+
throw redirect(302, '/login?referrer=/admin')
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
return {
|
|
@@ -16,11 +16,12 @@ export const POST: RequestHandler = async (event) => {
|
|
|
16
16
|
sql = `CALL delete_session($1);`
|
|
17
17
|
result = await query(sql, [event.locals.user.id])
|
|
18
18
|
}
|
|
19
|
-
return
|
|
19
|
+
return json({ message: 'Logout successful.' }, {
|
|
20
20
|
headers: {
|
|
21
21
|
'Set-Cookie': `session=; Path=/; SameSite=Lax; HttpOnly; Expires=${new Date().toUTCString()}`
|
|
22
22
|
}
|
|
23
23
|
})
|
|
24
|
+
|
|
24
25
|
case 'login':
|
|
25
26
|
sql = `SELECT authenticate($1) AS "authenticationResult";`
|
|
26
27
|
break
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import type { RequestHandler } from './$types'
|
|
2
|
+
import { JWT_SECRET, DOMAIN } from '$env/static/private'
|
|
2
3
|
import type { Secret } from 'jsonwebtoken'
|
|
3
4
|
import jwt from 'jsonwebtoken'
|
|
4
|
-
import dotenv from 'dotenv'
|
|
5
5
|
import { query } from '../../_db'
|
|
6
6
|
import { sendMessage } from '../../_send-in-blue'
|
|
7
7
|
|
|
8
|
-
dotenv.config()
|
|
9
|
-
const DOMAIN = process.env.DOMAIN
|
|
10
|
-
const JWT_SECRET = process.env.JWT_SECRET
|
|
11
|
-
|
|
12
8
|
export const POST: RequestHandler = async event => {
|
|
13
9
|
const body = await event.request.json()
|
|
14
10
|
const sql = `SELECT id as "userId" FROM users WHERE email = $1 LIMIT 1;`
|
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
import { error } from '@sveltejs/kit'
|
|
1
|
+
import { error, json } from '@sveltejs/kit'
|
|
2
2
|
import type { RequestHandler } from './$types'
|
|
3
3
|
import { OAuth2Client } from 'google-auth-library'
|
|
4
|
-
import { query } from '../../_db'
|
|
5
|
-
import {
|
|
4
|
+
import { query } from '../../_db'
|
|
5
|
+
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
9
|
try {
|
|
10
|
-
const
|
|
11
|
-
const client = new OAuth2Client(clientId)
|
|
10
|
+
const client = new OAuth2Client(PUBLIC_GOOGLE_CLIENT_ID)
|
|
12
11
|
const ticket = await client.verifyIdToken({
|
|
13
12
|
idToken: token,
|
|
14
|
-
audience:
|
|
13
|
+
audience: PUBLIC_GOOGLE_CLIENT_ID
|
|
15
14
|
});
|
|
16
15
|
const payload = ticket.getPayload()
|
|
17
16
|
if (!payload) throw error(500, 'Google authentication did not get the expected payload')
|
|
@@ -51,15 +50,14 @@ export const POST: RequestHandler = async event => {
|
|
|
51
50
|
// Prevent hooks.ts's handler() from deleting cookie thinking no one has authenticated
|
|
52
51
|
event.locals.user = userSession.user
|
|
53
52
|
|
|
54
|
-
return
|
|
53
|
+
return json({
|
|
55
54
|
message: 'Successful Google Sign-In.',
|
|
56
55
|
user: userSession.user
|
|
57
|
-
}
|
|
56
|
+
}, {
|
|
58
57
|
headers: {
|
|
59
58
|
'Set-Cookie': `session=${userSession.id}; Path=/; SameSite=Lax; HttpOnly;`}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
})
|
|
60
|
+
|
|
63
61
|
} catch (err) {
|
|
64
62
|
let message = ''
|
|
65
63
|
if (err instanceof Error) message = err.message
|
|
@@ -1,36 +1,35 @@
|
|
|
1
|
-
import { json
|
|
2
|
-
import dotenv from 'dotenv'
|
|
1
|
+
import { json } from '@sveltejs/kit'
|
|
3
2
|
import type { RequestHandler } from './$types'
|
|
4
|
-
import type
|
|
3
|
+
import type { JwtPayload } from 'jsonwebtoken'
|
|
5
4
|
import jwt from 'jsonwebtoken'
|
|
6
5
|
import { query } from '../../_db'
|
|
6
|
+
import { JWT_SECRET } from '$env/static/private'
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
export const PUT: RequestHandler = async (event) => {
|
|
9
|
+
const body = await event.request.json()
|
|
10
|
+
const { token, password } = body
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
// Check the validity of the token and extract userId
|
|
13
|
+
try {
|
|
14
|
+
const decoded = <JwtPayload> jwt.verify(token, <jwt.Secret> JWT_SECRET)
|
|
15
|
+
const userId = decoded.subject
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
// Update the database with the new password
|
|
18
|
+
const sql = `CALL reset_password($1, $2);`
|
|
19
|
+
await query(sql, [userId, password])
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return json$1({
|
|
31
|
-
message: 'Password reset token expired.'
|
|
32
|
-
}, {
|
|
33
|
-
status: 403
|
|
34
|
-
})
|
|
35
|
-
}
|
|
21
|
+
return json({
|
|
22
|
+
message: 'Password successfully reset.'
|
|
23
|
+
})
|
|
24
|
+
} catch (error) {
|
|
25
|
+
// Technically, I should check error.message to make sure it's not a DB issue
|
|
26
|
+
return json(
|
|
27
|
+
{
|
|
28
|
+
message: 'Password reset token expired.'
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
status: 403
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
}
|
|
36
35
|
}
|
|
@@ -5,7 +5,7 @@ export const load: PageServerLoad = async ({ locals }) => {
|
|
|
5
5
|
const { user } = locals // populated by /src/hooks.ts
|
|
6
6
|
|
|
7
7
|
const authorized = ['admin', 'teacher', 'student'] // must be logged-in
|
|
8
|
-
if (user
|
|
8
|
+
if (!user || !authorized.includes(user.role)) {
|
|
9
9
|
throw redirect(302, '/login?referrer=/profile')
|
|
10
10
|
}
|
|
11
11
|
|
|
@@ -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>
|
|
@@ -2,8 +2,9 @@ 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
|
|
5
6
|
const authorized = ['admin', 'teacher']
|
|
6
|
-
if (!
|
|
7
|
+
if (!user || !authorized.includes(user.role)) {
|
|
7
8
|
throw redirect(302, '/login?referrer=/teachers')
|
|
8
9
|
}
|
|
9
10
|
|
|
@@ -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)
|
package/svelte.config.js
CHANGED
package/src/lib/config.ts
DELETED