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.
Files changed (41) hide show
  1. package/.eslintrc.cjs +1 -1
  2. package/.prettierignore +13 -0
  3. package/.prettierrc +4 -1
  4. package/CHANGELOG.md +14 -1
  5. package/README.md +16 -19
  6. package/package.json +19 -18
  7. package/src/app.d.ts +5 -5
  8. package/src/app.html +1 -0
  9. package/src/hooks.ts +3 -13
  10. package/src/lib/auth.ts +49 -75
  11. package/src/routes/+error.svelte +6 -0
  12. package/src/routes/+layout.server.ts +9 -0
  13. package/src/routes/{__layout.svelte → +layout.svelte} +18 -14
  14. package/src/routes/{index.svelte → +page.svelte} +0 -0
  15. package/src/routes/admin/+page.server.ts +14 -0
  16. package/src/routes/admin/+page.svelte +12 -0
  17. package/src/routes/api/v1/user/+server.ts +21 -0
  18. package/src/routes/auth/[slug]/+server.ts +66 -0
  19. package/src/routes/auth/{forgot.ts → forgot/+server.ts} +4 -6
  20. package/src/routes/auth/{google.ts → google/+server.ts} +16 -20
  21. package/src/routes/auth/reset/{index.ts → +server.ts} +10 -13
  22. package/src/routes/auth/reset/{[token].svelte → [token]/+page.svelte} +4 -15
  23. package/src/routes/auth/reset/[token]/+page.ts +7 -0
  24. package/src/routes/{forgot.svelte → forgot/+page.svelte} +2 -2
  25. package/src/routes/{info.svelte → info/+page.svelte} +0 -0
  26. package/src/routes/{login.svelte → login/+page.svelte} +3 -3
  27. package/src/routes/profile/+page.server.ts +15 -0
  28. package/src/routes/{profile.svelte → profile/+page.svelte} +7 -25
  29. package/src/routes/register/+page.server.ts +10 -0
  30. package/src/routes/{register.svelte → register/+page.svelte} +6 -23
  31. package/src/routes/teachers/+page.server.ts +13 -0
  32. package/src/routes/teachers/+page.svelte +12 -0
  33. package/src/stores.ts +11 -1
  34. package/src/routes/__error.svelte +0 -19
  35. package/src/routes/admin.svelte +0 -40
  36. package/src/routes/api/v1/_auth.ts +0 -5
  37. package/src/routes/api/v1/admin.ts +0 -20
  38. package/src/routes/api/v1/teacher.ts +0 -20
  39. package/src/routes/api/v1/user.ts +0 -35
  40. package/src/routes/auth/[slug].ts +0 -93
  41. package/src/routes/teachers.svelte +0 -39
package/.eslintrc.cjs CHANGED
@@ -17,4 +17,4 @@ module.exports = {
17
17
  es2017: true,
18
18
  node: true
19
19
  }
20
- };
20
+ }
@@ -0,0 +1,13 @@
1
+ .DS_Store
2
+ node_modules
3
+ /build
4
+ /.svelte-kit
5
+ /package
6
+ .env
7
+ .env.*
8
+ !.env.example
9
+
10
+ # Ignore files for PNPM, NPM and YARN
11
+ pnpm-lock.yaml
12
+ package-lock.json
13
+ yarn.lock
package/.prettierrc CHANGED
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "useTabs": true,
3
+ "semi": false,
3
4
  "singleQuote": true,
4
5
  "trailingComma": "none",
5
- "printWidth": 100
6
+ "printWidth": 100,
7
+ "pluginSearchDirs": ["."],
8
+ "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
6
9
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
- # Backlog for 1.0.14
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 an 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.
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 SvelteKit's session store.
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/__layout.svelte.
18
- - **One Tap** "dialog" is displayed on the Home page (/src/routes/index.svelte) while the **Sign in with Google** button is on the login page (/src/routes/login.svelte).
19
- - Clicking either button opens a new window asking the user to authorize this website. If they OK it, a JWT is sent to a callback function.
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 SvelteKit's session store.
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
- Pages use the session.user.role to determine whether they are authorized. While a malicious user could alter the client-side session store to see pages they should not, the dynamic data in those restricted pages is served via endpoints which check request.locals.user to determine whether to grant access.
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 using a macOS, Linux or UNIX command-line:
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 or areas where the code can be optimized.
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.13",
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
- "check": "svelte-check --tsconfig ./tsconfig.json",
31
- "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
32
- "lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
33
- "format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ."
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.7.0",
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.2.0",
43
+ "google-auth-library": "^8.4.0",
44
44
  "jsonwebtoken": "^8.5.1",
45
- "pg": "^8.7.3"
45
+ "pg": "^8.8.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@sveltejs/adapter-node": "latest",
49
- "@sveltejs/kit": "1.0.0-next.405",
50
- "@types/bootstrap": "5.2.2",
49
+ "@sveltejs/kit": "latest",
50
+ "@types/bootstrap": "5.2.3",
51
51
  "@types/cookie": "^0.5.1",
52
- "@types/jsonwebtoken": "^8.5.8",
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.33.1",
55
- "@typescript-eslint/parser": "^5.33.1",
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.22.0",
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.4",
64
+ "sass": "^1.54.5",
64
65
  "svelte": "^3.49.0",
65
- "svelte-check": "^2.8.1",
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.8"
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/typescript
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
- interface Session {
13
- user?: User
14
- }
14
+ // interface PrivateEnv {} // $env/dynamic/private
15
15
 
16
- // interface Stuff {}
16
+ // interface PublicEnv {} // $env/dynamic/public
17
17
  }
18
18
 
19
19
  type AuthenticationResult = {
package/src/app.html CHANGED
@@ -4,6 +4,7 @@
4
4
  <meta charset="utf-8" />
5
5
  <link rel="icon" href="/favicon.png" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <script src="https://accounts.google.com/gsi/client" async defer></script>
7
8
  %sveltekit.head%
8
9
  </head>
9
10
  <body>
package/src/hooks.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as cookie from 'cookie'
2
- import type { Handle, GetSession, RequestEvent } from '@sveltejs/kit'
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
- /* eslint-disable @typescript-eslint/no-explicit-any */
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
- type Page = Readable<{
6
- url: URL;
7
- params: Record<string, string>;
8
- stuff: App.Stuff;
9
- status: number;
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
- session.set({ user: fromEndpoint.user })
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
- // Don't stay on login if successfully authenticated
87
- if (window.location.pathname === '/login') goto('/')
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
- const fromEndpoint = await res.json()
101
- if (res.ok) {
102
- session.set({ user: fromEndpoint.user })
103
- goto('/')
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
- setSessionUser(fromEndpoint.user)
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 session
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
- session.set({}) // delete session.user from
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 { loadScript, initializeSignInWithGoogle, registerLocal, loginLocal, logout }
129
+ return { initializeSignInWithGoogle, registerLocal, loginLocal, logout }
156
130
  }
@@ -0,0 +1,6 @@
1
+ <script lang="ts" context="module">
2
+ import { page } from '$app/stores'
3
+ </script>
4
+
5
+ <h1>{$page.status}</h1>
6
+ <h4>{$page.error?.message}</h4>
@@ -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, session } from '$app/stores'
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 { loadScript, initializeSignInWithGoogle, logout } = useAuth(page, session, goto)
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={!$session.user} href="/profile">Profile</a>
42
- <a class="nav-link" class:d-none={!$session.user || $session.user?.role !== 'admin'} href="/admin">Admin</a>
43
- <a class="nav-link" class:d-none={!$session.user || $session.user?.role === 'student'} href="/teachers">Teachers</a>
44
- <a class="nav-link" class:d-none={!!$session.user} href="/login">Login</a>
45
- <a on:click|preventDefault={logout} class="nav-link" class:d-none={!$session.user} href={'#'}>Logout</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>
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,12 @@
1
+ <script lang="ts">
2
+ import type { PageData } from './$types'
3
+ export let data: PageData
4
+ </script>
5
+
6
+ <svelte:head>
7
+ <title>Administration</title>
8
+ </svelte:head>
9
+
10
+ <h1>Admin</h1>
11
+ <h4>Admin Role</h4>
12
+ <p>{data.message}</p>
@@ -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
+ }