sveltekit-auth-example 1.0.13 → 1.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.cjs +1 -1
- package/.prettierignore +13 -0
- package/.prettierrc +4 -1
- package/CHANGELOG.md +14 -1
- package/README.md +16 -19
- package/package.json +19 -18
- package/src/app.d.ts +5 -5
- package/src/app.html +1 -0
- package/src/hooks.ts +3 -13
- package/src/lib/auth.ts +49 -75
- package/src/routes/+error.svelte +6 -0
- package/src/routes/+layout.server.ts +9 -0
- package/src/routes/{__layout.svelte → +layout.svelte} +18 -14
- package/src/routes/{index.svelte → +page.svelte} +0 -0
- package/src/routes/admin/+page.server.ts +14 -0
- package/src/routes/admin/+page.svelte +12 -0
- package/src/routes/api/v1/user/+server.ts +21 -0
- package/src/routes/auth/[slug]/+server.ts +66 -0
- package/src/routes/auth/{forgot.ts → forgot/+server.ts} +4 -6
- package/src/routes/auth/{google.ts → google/+server.ts} +16 -20
- package/src/routes/auth/reset/{index.ts → +server.ts} +10 -13
- package/src/routes/auth/reset/{[token].svelte → [token]/+page.svelte} +4 -15
- package/src/routes/auth/reset/[token]/+page.ts +7 -0
- package/src/routes/{forgot.svelte → forgot/+page.svelte} +2 -2
- package/src/routes/{info.svelte → info/+page.svelte} +0 -0
- package/src/routes/{login.svelte → login/+page.svelte} +3 -3
- package/src/routes/profile/+page.server.ts +15 -0
- package/src/routes/{profile.svelte → profile/+page.svelte} +7 -25
- package/src/routes/register/+page.server.ts +10 -0
- package/src/routes/{register.svelte → register/+page.svelte} +6 -23
- package/src/routes/teachers/+page.server.ts +13 -0
- package/src/routes/teachers/+page.svelte +12 -0
- package/src/stores.ts +11 -1
- package/src/routes/__error.svelte +0 -19
- package/src/routes/admin.svelte +0 -40
- package/src/routes/api/v1/_auth.ts +0 -5
- package/src/routes/api/v1/admin.ts +0 -20
- package/src/routes/api/v1/teacher.ts +0 -20
- package/src/routes/api/v1/user.ts +0 -35
- package/src/routes/auth/[slug].ts +0 -93
- package/src/routes/teachers.svelte +0 -39
package/.eslintrc.cjs
CHANGED
package/.prettierignore
ADDED
package/.prettierrc
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
|
-
# Backlog
|
|
1
|
+
# Backlog
|
|
2
|
+
* Consider not setting defaultUser in loginSession as it would simplify +layout.svelte.
|
|
3
|
+
* Refactor $env/dynamic/private and public
|
|
4
|
+
* Add password complexity checking on /register and /profile pages (only checks for length currently despite what the pages say)
|
|
5
|
+
|
|
6
|
+
# 1.0.16
|
|
7
|
+
* [Bug] Fixed LayoutServerLoad typing
|
|
8
|
+
|
|
9
|
+
# 1.0.15
|
|
10
|
+
* [Bug] Replaced use of Action type in +server.ts files (only works for +page.server.ts)
|
|
11
|
+
|
|
12
|
+
# 1.0.14
|
|
2
13
|
* Refactor routing to be folder, not file-based - https://github.com/sveltejs/kit/discussions/5774 (file system router). More info: https://github.com/sveltejs/kit/discussions/5774#discussioncomment-3294867
|
|
14
|
+
* Move bootstrap SCSS import to JavaScript in +layout.svelte
|
|
15
|
+
* Refactor as session was removed in https://github.com/sveltejs/kit/discussions/5883
|
|
3
16
|
|
|
4
17
|
# 1.0.13
|
|
5
18
|
* Bump dependencies
|
package/README.md
CHANGED
|
@@ -3,31 +3,28 @@
|
|
|
3
3
|
This is an example of how to register, authenticate, and update users and limit their access to
|
|
4
4
|
areas of the website by role (admin, teacher, student).
|
|
5
5
|
|
|
6
|
-
It's
|
|
7
|
-
|
|
8
|
-
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). Because of that, **getSession()** in hooks.ts is not useful. Instead, the client makes REST calls, gets the user in the body of the response (if successful) and adds the user to the client-side session store.
|
|
6
|
+
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).
|
|
9
7
|
|
|
10
8
|
The website supports two types of authentication:
|
|
11
9
|
1. **Local accounts** via username (email) and password
|
|
12
|
-
- The login form (/src/routes/login.svelte) sends the login info as JSON to endpoint /auth/login
|
|
10
|
+
- The login form (/src/routes/login/+page.svelte) sends the login info as JSON to endpoint /auth/login
|
|
13
11
|
- The endpoint passes the JSON to PostgreSQL function authenticate(json) which hashes the password and compares it to the stored hashed password in the users table. The function returns JSON containing a session ID (v4 UUID) and user object (sans password).
|
|
14
|
-
- The endpoint sends session ID as an httpOnly SameSite cookie and the user object in the body of the response.
|
|
15
|
-
- The client stores the user object in
|
|
12
|
+
- The endpoint sends this session ID as an httpOnly SameSite cookie and the user object in the body of the response.
|
|
13
|
+
- The client stores the user object in the loginSession store.
|
|
14
|
+
- Further requests to the server include the session cookie. The hooks.ts handle() method extracts the session cookie, looks up the user and attaches it to RequestEvent.locals so server-side code can check locals.user.role to see if the request is authorized and return an HTTP 401 status if not.
|
|
16
15
|
2. **Sign in with Google**
|
|
17
|
-
- **Sign in with Google** is initialized in /src/routes
|
|
18
|
-
- **One Tap**
|
|
19
|
-
-
|
|
16
|
+
- **Sign in with Google** is initialized in /src/routes/+layout.svelte.
|
|
17
|
+
- **Google One Tap** prompt is displayed on the initially loaded page unless [Intelligent Tracking Prevention is enabled in the browser](https://developers.google.com/identity/gsi/web/guides/features#upgraded_ux_on_itp_browsers).
|
|
18
|
+
- **Sign in with Google** button is on the login page (/src/routes/login/+page.svelte) and register page (/src/routes/register/+page.svelte).
|
|
19
|
+
- Clicking either button opens a new window asking the user to authorize this website. If the user OKs it, a JSON Web Token (JWT) is sent to a callback function.
|
|
20
20
|
- The callback function (in /src/lib/auth.ts) sends the JWT to an endpoint on this server /auth/google.
|
|
21
21
|
- The endpoint decodes and validates the user information then calls the PostgreSQL function start_gmail_user_session to upsert the user to the database returing a session id in an httpOnly SameSite cookie and user in the body of the response.
|
|
22
|
-
- The client stores the user object in
|
|
23
|
-
|
|
24
|
-
As the client calls endpoints, each request to the server includes the session ID in the httpOnly cookie. The handle() function in hooks.ts checks the database for this session ID. If it exists and is not expired, it attaches the user (including role) to request.locals. Each endpoint can then examine request.locals.user.role to determine whether to respond or return a 401.
|
|
25
|
-
|
|
26
|
-
> There is some overhead to checking the user session in a database each time versus using a JSON web token; however, validating each request avoids problems discussed in [this article](https://redis.com/blog/json-web-tokens-jwt-are-dangerous-for-user-sessions/) and [this one](https://scotch.io/bar-talk/why-jwts-suck-as-session-tokens). For a high-volume website, I would use Redis or the equivalent.
|
|
22
|
+
- The client stores the user object in the loginSession store.
|
|
23
|
+
- Further requests to the server work identically to local accounts above.
|
|
27
24
|
|
|
28
|
-
|
|
25
|
+
> There is some overhead to checking the user session in a database each time versus using a JWT; however, validating each request avoids problems discussed in [this article](https://redis.com/blog/json-web-tokens-jwt-are-dangerous-for-user-sessions/) and [this one](https://scotch.io/bar-talk/why-jwts-suck-as-session-tokens). For a high-volume website, I would use Redis or the equivalent.
|
|
29
26
|
|
|
30
|
-
The forgot password functionality uses SendInBlue 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.
|
|
27
|
+
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).
|
|
31
28
|
|
|
32
29
|
## Prerequisites
|
|
33
30
|
- PostgreSQL 14 or higher
|
|
@@ -38,7 +35,7 @@ The forgot password functionality uses SendInBlue to send the email. You would n
|
|
|
38
35
|
|
|
39
36
|
## Setting up the project
|
|
40
37
|
|
|
41
|
-
Here are the steps
|
|
38
|
+
Here are the steps:
|
|
42
39
|
|
|
43
40
|
1. Get the project and setup the database
|
|
44
41
|
```bash
|
|
@@ -49,7 +46,7 @@ git clone https://github.com/nstuyvesant/sveltekit-auth-example.git
|
|
|
49
46
|
cd /sveltekit-auth-example
|
|
50
47
|
npm install
|
|
51
48
|
|
|
52
|
-
# Create PostgreSQL database
|
|
49
|
+
# Create PostgreSQL database (only works if you installed PostgreSQL)
|
|
53
50
|
psql -d postgres -f db_create.sql
|
|
54
51
|
```
|
|
55
52
|
|
|
@@ -83,4 +80,4 @@ The db_create.sql script adds three users to the database with obvious roles:
|
|
|
83
80
|
|
|
84
81
|
## My ask of you
|
|
85
82
|
|
|
86
|
-
Please report any issues
|
|
83
|
+
Please report any issues [here](https://github.com/nstuyvesant/sveltekit-auth-example/issues). [Pull requests](https://github.com/nstuyvesant/sveltekit-auth-example/pulls) are encouraged especially as SvelteKit is evolving rapidly.
|
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.16",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "Nate Stuyvesant",
|
|
7
7
|
"license": "https://github.com/nstuyvesant/sveltekit-auth-example/blob/master/LICENSE",
|
|
@@ -24,48 +24,49 @@
|
|
|
24
24
|
"scripts": {
|
|
25
25
|
"start": "node build",
|
|
26
26
|
"dev": "vite dev",
|
|
27
|
-
"serve": "npm run dev -- --open",
|
|
28
27
|
"build": "vite build",
|
|
28
|
+
"package": "svelte-kit package",
|
|
29
29
|
"preview": "vite preview",
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
31
|
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
32
|
+
"lint": "prettier --check . && eslint .",
|
|
33
|
+
"format": "prettier --write ."
|
|
34
34
|
},
|
|
35
35
|
"engines": {
|
|
36
|
-
"node": "~18.
|
|
36
|
+
"node": "~18.8.0",
|
|
37
37
|
"npm": "^8.18.0"
|
|
38
38
|
},
|
|
39
39
|
"type": "module",
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"cookie": "^0.5.0",
|
|
42
42
|
"dotenv": "^16.0.1",
|
|
43
|
-
"google-auth-library": "^8.
|
|
43
|
+
"google-auth-library": "^8.4.0",
|
|
44
44
|
"jsonwebtoken": "^8.5.1",
|
|
45
|
-
"pg": "^8.
|
|
45
|
+
"pg": "^8.8.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@sveltejs/adapter-node": "latest",
|
|
49
|
-
"@sveltejs/kit": "
|
|
50
|
-
"@types/bootstrap": "5.2.
|
|
49
|
+
"@sveltejs/kit": "latest",
|
|
50
|
+
"@types/bootstrap": "5.2.3",
|
|
51
51
|
"@types/cookie": "^0.5.1",
|
|
52
|
-
"@types/
|
|
52
|
+
"@types/google.accounts": "0.0.2",
|
|
53
|
+
"@types/jsonwebtoken": "^8.5.9",
|
|
53
54
|
"@types/pg": "^8.6.5",
|
|
54
|
-
"@typescript-eslint/eslint-plugin": "^5.
|
|
55
|
-
"@typescript-eslint/parser": "^5.
|
|
55
|
+
"@typescript-eslint/eslint-plugin": "^5.35.0",
|
|
56
|
+
"@typescript-eslint/parser": "^5.35.0",
|
|
56
57
|
"bootstrap": "^5.2.0",
|
|
57
58
|
"bootstrap-icons": "^1.9.1",
|
|
58
|
-
"eslint": "^8.
|
|
59
|
+
"eslint": "^8.23.0",
|
|
59
60
|
"eslint-config-prettier": "^8.5.0",
|
|
60
61
|
"eslint-plugin-svelte3": "^4.0.0",
|
|
61
62
|
"prettier": "^2.7.1",
|
|
62
63
|
"prettier-plugin-svelte": "^2.7.0",
|
|
63
|
-
"sass": "^1.54.
|
|
64
|
+
"sass": "^1.54.5",
|
|
64
65
|
"svelte": "^3.49.0",
|
|
65
|
-
"svelte-check": "^2.
|
|
66
|
+
"svelte-check": "^2.9.0",
|
|
66
67
|
"svelte-preprocess": "^4.10.7",
|
|
67
68
|
"tslib": "^2.4.0",
|
|
68
69
|
"typescript": "^4.7.4",
|
|
69
|
-
"vite": "^3.0.
|
|
70
|
+
"vite": "^3.0.9"
|
|
70
71
|
}
|
|
71
72
|
}
|
package/src/app.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/// <reference types="bootstrap" />
|
|
2
|
+
/// <reference types="google.accounts" />
|
|
2
3
|
|
|
3
|
-
// See https://kit.svelte.dev/docs/
|
|
4
|
+
// See https://kit.svelte.dev/docs/types#app
|
|
4
5
|
// for information about these interfaces
|
|
6
|
+
// and what to do when importing types
|
|
5
7
|
declare namespace App {
|
|
6
8
|
interface Locals {
|
|
7
9
|
user: User
|
|
@@ -9,11 +11,9 @@ declare namespace App {
|
|
|
9
11
|
|
|
10
12
|
// interface Platform {}
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
user?: User
|
|
14
|
-
}
|
|
14
|
+
// interface PrivateEnv {} // $env/dynamic/private
|
|
15
15
|
|
|
16
|
-
// interface
|
|
16
|
+
// interface PublicEnv {} // $env/dynamic/public
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
type AuthenticationResult = {
|
package/src/app.html
CHANGED
package/src/hooks.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as cookie from 'cookie'
|
|
2
|
-
import type { Handle,
|
|
2
|
+
import type { Handle, RequestEvent } from '@sveltejs/kit'
|
|
3
3
|
import { query } from './routes/_db'
|
|
4
4
|
|
|
5
5
|
// Attach authorization to each server request (role may have changed)
|
|
@@ -8,7 +8,7 @@ async function attachUserToRequest(sessionId: string, event: RequestEvent) {
|
|
|
8
8
|
SELECT * FROM get_session($1);`
|
|
9
9
|
const { rows } = await query(sql, [sessionId])
|
|
10
10
|
if (rows?.length > 0) {
|
|
11
|
-
event.locals.user = rows[0].get_session
|
|
11
|
+
event.locals.user = <User> rows[0].get_session
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -27,19 +27,9 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|
|
27
27
|
await attachUserToRequest(cookies.session, event)
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const response = await resolve(event
|
|
31
|
-
ssr: !event.request.url.includes('/admin')
|
|
32
|
-
})
|
|
30
|
+
const response = await resolve(event)
|
|
33
31
|
|
|
34
32
|
// after endpoint or page is called
|
|
35
33
|
deleteCookieIfNoUser(event, response)
|
|
36
34
|
return response
|
|
37
35
|
}
|
|
38
|
-
|
|
39
|
-
// Only useful for authentication schemes that redirect back to the website - not
|
|
40
|
-
// an SPA with client-side routing that handles authentication seamlessly
|
|
41
|
-
export const getSession: GetSession = event => {
|
|
42
|
-
return event.locals.user ?
|
|
43
|
-
{ user: event.locals.user }
|
|
44
|
-
: {}
|
|
45
|
-
}
|
package/src/lib/auth.ts
CHANGED
|
@@ -1,21 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
import type { Page } from '@sveltejs/kit'
|
|
2
2
|
import type { Readable, Writable } from 'svelte/store'
|
|
3
3
|
import { config } from '$lib/config'
|
|
4
|
+
import { defaultUser } from '../stores'
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
error: Error | null;
|
|
11
|
-
}>
|
|
12
|
-
|
|
13
|
-
export default function useAuth(page: Page, session: Writable<any>, goto: (url: string | URL, opts?: { replaceState?: boolean; noscroll?: boolean; keepfocus?: boolean; state?: any; }) => Promise<any>) {
|
|
14
|
-
|
|
15
|
-
// Required to use session.set()
|
|
16
|
-
let sessionValue: App.Session
|
|
17
|
-
session.subscribe(value => {
|
|
18
|
-
sessionValue = value
|
|
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
|
|
19
11
|
})
|
|
20
12
|
|
|
21
13
|
let referrer: string | null
|
|
@@ -23,50 +15,6 @@ export default function useAuth(page: Page, session: Writable<any>, goto: (url:
|
|
|
23
15
|
referrer = value.url.searchParams.get('referrer')
|
|
24
16
|
})
|
|
25
17
|
|
|
26
|
-
const loadScript = () => new Promise( (resolve, reject) => {
|
|
27
|
-
const script = document.createElement('script')
|
|
28
|
-
script.id = 'gsiScript'
|
|
29
|
-
script.async = true
|
|
30
|
-
script.src = 'https://accounts.google.com/gsi/client'
|
|
31
|
-
script.onerror = (error) => reject(error)
|
|
32
|
-
script.onload = () => resolve(script)
|
|
33
|
-
document.body.appendChild(script)
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
function googleAccountsIdInitialize() {
|
|
37
|
-
return window.google.accounts.id.initialize({
|
|
38
|
-
client_id: config.googleClientId,
|
|
39
|
-
callback: googleCallback
|
|
40
|
-
})
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function googleAccountsIdRenderButton(htmlId: string) {
|
|
44
|
-
return window.google.accounts.id.renderButton(
|
|
45
|
-
document.getElementById(htmlId), {
|
|
46
|
-
theme: 'filled_blue',
|
|
47
|
-
size: 'large',
|
|
48
|
-
width: '367'
|
|
49
|
-
}
|
|
50
|
-
)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function initializeSignInWithGoogle(htmlId?: string) {
|
|
54
|
-
googleAccountsIdInitialize()
|
|
55
|
-
|
|
56
|
-
if (htmlId) {
|
|
57
|
-
return googleAccountsIdRenderButton(htmlId)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (!sessionValue.user) window.google.accounts.id.prompt()
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function setSessionUser(user: User | null) {
|
|
64
|
-
session.update(s => ({
|
|
65
|
-
...s,
|
|
66
|
-
user
|
|
67
|
-
}))
|
|
68
|
-
}
|
|
69
|
-
|
|
70
18
|
async function googleCallback(response: GoogleCredentialResponse) {
|
|
71
19
|
const res = await fetch('/auth/google', {
|
|
72
20
|
method: 'POST',
|
|
@@ -75,38 +23,64 @@ export default function useAuth(page: Page, session: Writable<any>, goto: (url:
|
|
|
75
23
|
},
|
|
76
24
|
body: JSON.stringify({ token: response.credential })
|
|
77
25
|
})
|
|
78
|
-
const fromEndpoint = await res.json()
|
|
79
26
|
|
|
80
27
|
if (res.ok) {
|
|
81
|
-
|
|
28
|
+
const fromEndpoint = await res.json()
|
|
29
|
+
loginSession.set(fromEndpoint.user) // update loginSession store
|
|
82
30
|
const { role } = fromEndpoint.user
|
|
83
31
|
if (referrer) return goto(referrer)
|
|
84
32
|
if (role === 'teacher') return goto('/teachers')
|
|
85
33
|
if (role === 'admin') return goto('/admin')
|
|
86
|
-
|
|
87
|
-
|
|
34
|
+
if (location.pathname === '/login') goto('/') // logged in so go home
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
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 })
|
|
41
|
+
|
|
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
|
+
)
|
|
88
50
|
}
|
|
51
|
+
|
|
52
|
+
if (!user) id.prompt()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function setloginSession(user: User | null) {
|
|
56
|
+
loginSession.update(s => ({
|
|
57
|
+
...s,
|
|
58
|
+
user
|
|
59
|
+
}))
|
|
89
60
|
}
|
|
90
61
|
|
|
91
62
|
async function registerLocal(user: User) {
|
|
92
63
|
try {
|
|
93
64
|
const res = await fetch('/auth/register', {
|
|
94
65
|
method: 'POST',
|
|
95
|
-
body: JSON.stringify(user),
|
|
66
|
+
body: JSON.stringify(user), // server needs to ignore user.role and always set it to 'student'
|
|
96
67
|
headers: {
|
|
97
68
|
'Content-Type': 'application/json'
|
|
98
69
|
}
|
|
99
70
|
})
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
} else {
|
|
105
|
-
throw new Error(fromEndpoint.message)
|
|
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
|
|
106
75
|
}
|
|
76
|
+
|
|
77
|
+
// res.ok
|
|
78
|
+
const fromEndpoint = await res.json()
|
|
79
|
+
loginSession.set(fromEndpoint.user) // update store so user is logged in
|
|
80
|
+
goto('/')
|
|
107
81
|
} catch (err) {
|
|
82
|
+
console.error('Register error', err)
|
|
108
83
|
if (err instanceof Error) {
|
|
109
|
-
console.error('Login error', err)
|
|
110
84
|
throw new Error(err.message)
|
|
111
85
|
}
|
|
112
86
|
}
|
|
@@ -123,7 +97,7 @@ export default function useAuth(page: Page, session: Writable<any>, goto: (url:
|
|
|
123
97
|
})
|
|
124
98
|
const fromEndpoint = await res.json()
|
|
125
99
|
if (res.ok) {
|
|
126
|
-
|
|
100
|
+
setloginSession(fromEndpoint.user)
|
|
127
101
|
const { role } = fromEndpoint.user
|
|
128
102
|
if (referrer) return goto(referrer)
|
|
129
103
|
if (role === 'teacher') return goto('/teachers')
|
|
@@ -141,16 +115,16 @@ export default function useAuth(page: Page, session: Writable<any>, goto: (url:
|
|
|
141
115
|
}
|
|
142
116
|
|
|
143
117
|
async function logout() {
|
|
144
|
-
// Request server delete httpOnly cookie called
|
|
118
|
+
// Request server delete httpOnly cookie called loginSession
|
|
145
119
|
const url = '/auth/logout'
|
|
146
120
|
const res = await fetch(url, {
|
|
147
121
|
method: 'POST'
|
|
148
122
|
})
|
|
149
123
|
if (res.ok) {
|
|
150
|
-
|
|
124
|
+
loginSession.set(defaultUser) // delete loginSession.user from
|
|
151
125
|
goto('/login')
|
|
152
126
|
} else console.error(`Logout not successful: ${res.statusText} (${res.status})`)
|
|
153
127
|
}
|
|
154
128
|
|
|
155
|
-
return {
|
|
129
|
+
return { initializeSignInWithGoogle, registerLocal, loginLocal, logout }
|
|
156
130
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LayoutServerLoad } from './$types'
|
|
2
|
+
|
|
3
|
+
export const load: LayoutServerLoad = (event) => {
|
|
4
|
+
const locals = event.locals
|
|
5
|
+
const { user }: { user: User } = locals // locals.user set by hooks.ts/handle(), undefined if not logged in
|
|
6
|
+
return {
|
|
7
|
+
user
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount } from 'svelte'
|
|
3
|
+
import type { LayoutServerData } from './$types'
|
|
3
4
|
import { goto } from '$app/navigation'
|
|
4
|
-
import { page
|
|
5
|
-
import { toast } from '../stores'
|
|
5
|
+
import { page } from '$app/stores'
|
|
6
|
+
import { loginSession, toast } from '../stores'
|
|
6
7
|
import useAuth from '$lib/auth'
|
|
8
|
+
import 'bootstrap/scss/bootstrap.scss' // preferred way to load Bootstrap SCSS for hot module reloading
|
|
9
|
+
|
|
10
|
+
export let data: LayoutServerData
|
|
11
|
+
|
|
12
|
+
// If returning from different website, runs once (as it's an SPA) to restore user session if session cookie is still valid
|
|
13
|
+
const { user } = data
|
|
14
|
+
$loginSession = user
|
|
7
15
|
|
|
8
16
|
// Vue.js Composition API style
|
|
9
|
-
const {
|
|
17
|
+
const { initializeSignInWithGoogle, logout } = useAuth(page, loginSession, goto)
|
|
10
18
|
|
|
11
19
|
let Toast: any
|
|
12
20
|
|
|
13
|
-
onMount(async() => {
|
|
14
|
-
await import('bootstrap/js/dist/collapse')
|
|
21
|
+
onMount(async () => {
|
|
22
|
+
await import('bootstrap/js/dist/collapse') // lots of ways to load Bootstrap but prefer this approach to avoid SSR issues
|
|
15
23
|
Toast = (await import('bootstrap/js/dist/toast')).default
|
|
16
|
-
await loadScript()
|
|
17
24
|
initializeSignInWithGoogle()
|
|
18
25
|
})
|
|
19
26
|
|
|
@@ -38,11 +45,11 @@
|
|
|
38
45
|
<div class="navbar-nav">
|
|
39
46
|
<a class="nav-link active" aria-current="page" href="/">Home</a>
|
|
40
47
|
<a class="nav-link" href="/info">Info</a>
|
|
41
|
-
<a class="nav-link" class:d-none={!$
|
|
42
|
-
<a class="nav-link" class:d-none={!$
|
|
43
|
-
<a class="nav-link" class:d-none={!$
|
|
44
|
-
<a class="nav-link" class:d-none={!!$
|
|
45
|
-
<a on:click|preventDefault={logout} class="nav-link" class:d-none={!$
|
|
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>
|
|
46
53
|
</div>
|
|
47
54
|
</div>
|
|
48
55
|
</div>
|
|
@@ -64,9 +71,6 @@
|
|
|
64
71
|
</main>
|
|
65
72
|
|
|
66
73
|
<style lang="scss" global>
|
|
67
|
-
// Load Bootstrap's SCSS
|
|
68
|
-
@import 'bootstrap/scss/bootstrap';
|
|
69
|
-
|
|
70
74
|
// Make Retina displays crisper
|
|
71
75
|
* {
|
|
72
76
|
-webkit-font-smoothing: antialiased;
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { redirect } from '@sveltejs/kit'
|
|
2
|
+
import type { PageServerLoad } from './$types'
|
|
3
|
+
|
|
4
|
+
export const load: PageServerLoad = async ({locals})=> {
|
|
5
|
+
const { user } = locals
|
|
6
|
+
const authorized = ['admin']
|
|
7
|
+
if (user && !authorized.includes(user.role)) {
|
|
8
|
+
throw redirect(302, '/login?referrer=/admin');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
message: 'Admin-only content from endpoint.'
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { error, json} from '@sveltejs/kit'
|
|
2
|
+
import type { RequestHandler } from './$types'
|
|
3
|
+
import { query } from '../../../_db'
|
|
4
|
+
|
|
5
|
+
export const PUT: RequestHandler = async event => {
|
|
6
|
+
const { user } = event.locals
|
|
7
|
+
|
|
8
|
+
if (!user)
|
|
9
|
+
throw error(401, 'Unauthorized - must be logged-in.')
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const userUpdate = await event.request.json()
|
|
13
|
+
await query(`CALL update_user($1, $2);`, [user.id, JSON.stringify(userUpdate)])
|
|
14
|
+
} catch (err) {
|
|
15
|
+
throw error(503, 'Could not communicate with database.')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return json({
|
|
19
|
+
message: 'Successfully updated user profile.'
|
|
20
|
+
})
|
|
21
|
+
}
|