sveltekit-auth-example 1.0.23 → 1.0.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,13 @@
1
1
  # Backlog
2
2
  * Add password complexity checking on /register and /profile pages (only checks for length currently despite what the pages say)
3
3
 
4
+ # 1.0.25
5
+ * Bump dependencies
6
+ * Simplify Sign In With Google
7
+
8
+ # 1.0.24
9
+ * Bump dependencies
10
+
4
11
  # 1.0.23
5
12
  * Restructured server-side libraries to $lib/server based on https://github.com/sveltejs/kit/pull/6623
6
13
  * General cleanup
package/README.md CHANGED
@@ -2,10 +2,12 @@
2
2
 
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). As almost every recent release of SvelteKit introduced breaking changes, this project attempts to
5
- maintain compatibility with the latest release.
5
+ maintain compatibility with the latest release and leverage new APIs.
6
6
 
7
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).
8
8
 
9
+ The project includes a Content Security Policy (CSP) in svelte.config.js.
10
+
9
11
  The website supports two types of authentication:
10
12
  1. **Local accounts** via username (email) and password
11
13
  - The login form (/src/routes/login/+page.svelte) sends the login info as JSON to endpoint /auth/login
@@ -25,7 +27,7 @@ The website supports two types of authentication:
25
27
 
26
28
  > 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.
27
29
 
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).
30
+ The forgot password / password reset functionality uses a JWT and [**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** (used their API because on another project).
29
31
 
30
32
  ## Prerequisites
31
33
  - PostgreSQL 14.5 or higher
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.23",
4
+ "version": "1.0.25",
5
5
  "private": false,
6
6
  "author": "Nate Stuyvesant",
7
7
  "license": "https://github.com/nstuyvesant/sveltekit-auth-example/blob/master/LICENSE",
@@ -32,8 +32,8 @@
32
32
  "format": "prettier --write ."
33
33
  },
34
34
  "engines": {
35
- "node": "~18.9.0",
36
- "npm": "^8.19.1"
35
+ "node": "~18.9.1",
36
+ "npm": "^8.19.2"
37
37
  },
38
38
  "type": "module",
39
39
  "dependencies": {
@@ -46,22 +46,22 @@
46
46
  "@types/google.accounts": "0.0.2",
47
47
  "@types/jsonwebtoken": "^8.5.9",
48
48
  "@types/pg": "^8.6.5",
49
- "@typescript-eslint/eslint-plugin": "^5.37.0",
50
- "@typescript-eslint/parser": "^5.37.0",
49
+ "@typescript-eslint/eslint-plugin": "^5.38.0",
50
+ "@typescript-eslint/parser": "^5.38.0",
51
51
  "bootstrap": "^5.2.1",
52
- "eslint": "^8.23.1",
52
+ "eslint": "^8.24.0",
53
53
  "eslint-config-prettier": "^8.5.0",
54
54
  "eslint-plugin-svelte3": "^4.0.0",
55
- "google-auth-library": "^8.5.1",
55
+ "google-auth-library": "^8.5.2",
56
56
  "jsonwebtoken": "^8.5.1",
57
57
  "prettier": "^2.7.1",
58
58
  "prettier-plugin-svelte": "^2.7.0",
59
- "sass": "^1.54.9",
59
+ "sass": "^1.55.0",
60
60
  "svelte": "^3.50.1",
61
61
  "svelte-check": "^2.9.0",
62
62
  "svelte-preprocess": "^4.10.7",
63
63
  "tslib": "^2.4.0",
64
64
  "typescript": "^4.8.3",
65
- "vite": "^3.1.0"
65
+ "vite": "^3.1.3"
66
66
  }
67
67
  }
package/src/app.html CHANGED
@@ -2,9 +2,9 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
- <link rel="icon" href="/favicon.png" />
5
+ <link rel="icon" href="%sveltekit.assets%//favicon.png" sizes="any" />
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
+ <script nonce="%sveltekit.nonce%" src="https://accounts.google.com/gsi/client" async defer></script>
8
8
  %sveltekit.head%
9
9
  </head>
10
10
  <body>
@@ -4,7 +4,7 @@
4
4
  import { goto } from '$app/navigation'
5
5
  import { page } from '$app/stores'
6
6
  import { loginSession, toast } from '../stores'
7
- import useAuth from '$lib/auth'
7
+ import { PUBLIC_GOOGLE_CLIENT_ID } from '$env/static/public'
8
8
  import 'bootstrap/scss/bootstrap.scss' // preferred way to load Bootstrap SCSS for hot module reloading
9
9
 
10
10
  export let data: LayoutServerData
@@ -13,18 +13,53 @@
13
13
  const { user } = data
14
14
  $loginSession = user
15
15
 
16
- // Vue.js Composition API style
17
- const { initializeSignInWithGoogle, logout } = useAuth(page, loginSession, goto)
18
-
19
16
  let Toast: any
20
17
 
21
18
  onMount(async () => {
22
19
  await import('bootstrap/js/dist/collapse') // lots of ways to load Bootstrap but prefer this approach to avoid SSR issues
23
20
  await import('bootstrap/js/dist/dropdown')
24
21
  Toast = (await import('bootstrap/js/dist/toast')).default
25
- initializeSignInWithGoogle()
22
+ window.google.accounts.id.initialize({
23
+ client_id: PUBLIC_GOOGLE_CLIENT_ID,
24
+ callback: googleCallback
25
+ })
26
+
27
+ if (!$loginSession) window.google.accounts.id.prompt()
26
28
  })
27
29
 
30
+ async function logout() {
31
+ // Request server delete httpOnly cookie called loginSession
32
+ const url = '/auth/logout'
33
+ const res = await fetch(url, {
34
+ method: 'POST'
35
+ })
36
+ if (res.ok) {
37
+ loginSession.set(undefined) // delete loginSession.user from
38
+ goto('/login')
39
+ } else console.error(`Logout not successful: ${res.statusText} (${res.status})`)
40
+ }
41
+
42
+ async function googleCallback(response: GoogleCredentialResponse) {
43
+ const res = await fetch('/auth/google', {
44
+ method: 'POST',
45
+ headers: {
46
+ 'Content-Type': 'application/json'
47
+ },
48
+ body: JSON.stringify({ token: response.credential })
49
+ })
50
+
51
+ if (res.ok) {
52
+ const fromEndpoint = await res.json()
53
+ loginSession.set(fromEndpoint.user) // update loginSession store
54
+ const { role } = fromEndpoint.user
55
+ const referrer = $page.url.searchParams.get('referrer')
56
+ if (referrer) return goto(referrer)
57
+ if (role === 'teacher') return goto('/teachers')
58
+ if (role === 'admin') return goto('/admin')
59
+ if (location.pathname === '/login') goto('/') // logged in so go home
60
+ }
61
+ }
62
+
28
63
  const openToast = (open: boolean) => {
29
64
  if (open) {
30
65
  const toastDiv = <HTMLDivElement> document.getElementById('authToast')
@@ -1,7 +1,7 @@
1
1
  import { redirect } from '@sveltejs/kit'
2
2
  import type { PageServerLoad } from './$types'
3
3
 
4
- export const load: PageServerLoad = async ({locals})=> {
4
+ export const load: PageServerLoad = async ({ locals }) => {
5
5
  const { user } = locals
6
6
  const authorized = ['admin']
7
7
  if (!user || !authorized.includes(user.role)) {
@@ -9,6 +9,6 @@ export const load: PageServerLoad = async ({locals})=> {
9
9
  }
10
10
 
11
11
  return {
12
- message: 'Admin-only content from server.'
13
- }
12
+ message: 'Admin-only content from server.'
13
+ }
14
14
  }
@@ -3,11 +3,8 @@
3
3
  import { goto } from '$app/navigation'
4
4
  import { page } from '$app/stores'
5
5
  import { loginSession } from '../../stores'
6
- import useAuth from '$lib/auth'
7
6
  import { focusOnFirstError } from '$lib/focus'
8
7
 
9
- const { initializeSignInWithGoogle, loginLocal } = useAuth(page, loginSession, goto)
10
-
11
8
  let focusedField: HTMLInputElement
12
9
  let message: string
13
10
  const credentials: Credentials = {
@@ -35,9 +32,42 @@
35
32
  }
36
33
 
37
34
  onMount(async() => {
38
- initializeSignInWithGoogle('googleButton')
35
+ window.google.accounts.id.renderButton(document.getElementById('googleButton'), {
36
+ theme: 'filled_blue',
37
+ size: 'large',
38
+ width: '367'
39
+ })
39
40
  focusedField.focus()
40
41
  })
42
+
43
+ async function loginLocal(credentials: Credentials) {
44
+ try {
45
+ const res = await fetch('/auth/login', {
46
+ method: 'POST',
47
+ body: JSON.stringify(credentials),
48
+ headers: {
49
+ 'Content-Type': 'application/json'
50
+ }
51
+ })
52
+ const fromEndpoint = await res.json()
53
+ if (res.ok) {
54
+ loginSession.set(fromEndpoint.user)
55
+ const { role } = fromEndpoint.user
56
+ const referrer = $page.url.searchParams.get('referrer')
57
+ if (referrer) return goto(referrer)
58
+ if (role === 'teacher') return goto('/teachers')
59
+ if (role === 'admin') return goto('/admin')
60
+ return goto('/')
61
+ } else {
62
+ throw new Error(fromEndpoint.message)
63
+ }
64
+ } catch (err) {
65
+ if (err instanceof Error) {
66
+ console.error('Login error', err)
67
+ throw new Error(err.message)
68
+ }
69
+ }
70
+ }
41
71
  </script>
42
72
 
43
73
  <svelte:head>
@@ -1,13 +1,9 @@
1
1
  <script lang="ts">
2
2
  import { onMount } from 'svelte'
3
3
  import { goto } from '$app/navigation'
4
- import { page } from '$app/stores'
5
4
  import { loginSession } from '../../stores'
6
- import useAuth from '$lib/auth'
7
5
  import { focusOnFirstError } from '$lib/focus'
8
6
 
9
- const { initializeSignInWithGoogle, registerLocal } = useAuth(page, loginSession, goto)
10
-
11
7
  let focusedField: HTMLInputElement
12
8
 
13
9
  let user: User = {
@@ -49,9 +45,41 @@
49
45
 
50
46
  onMount(() => {
51
47
  focusedField.focus()
52
- initializeSignInWithGoogle('googleButton')
48
+ window.google.accounts.id.renderButton(document.getElementById('googleButton'), {
49
+ theme: 'filled_blue',
50
+ size: 'large',
51
+ width: '367'
52
+ })
53
53
  })
54
54
 
55
+ async function registerLocal(user: User) {
56
+ try {
57
+ const res = await fetch('/auth/register', {
58
+ method: 'POST',
59
+ body: JSON.stringify(user), // server ignores user.role - always set it to 'student' (lowest priv)
60
+ headers: {
61
+ 'Content-Type': 'application/json'
62
+ }
63
+ })
64
+ if (!res.ok) {
65
+ if (res.status == 401)
66
+ // user already existed and passwords didn't match (otherwise, we login the user)
67
+ throw new Error('Sorry, that username is already in use.')
68
+ throw new Error(res.statusText) // should only occur if there's a database error
69
+ }
70
+
71
+ // res.ok
72
+ const fromEndpoint = await res.json()
73
+ loginSession.set(fromEndpoint.user) // update store so user is logged in
74
+ goto('/')
75
+ } catch (err) {
76
+ console.error('Register error', err)
77
+ if (err instanceof Error) {
78
+ throw new Error(err.message)
79
+ }
80
+ }
81
+ }
82
+
55
83
  const passwordMatch = () => {
56
84
  if (!user) return false // placate TypeScript
57
85
  if (!user.password) user.password = ''
package/src/lib/auth.ts DELETED
@@ -1,130 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
-
3
- import type { Page } from '@sveltejs/kit'
4
- import type { Readable, Writable } from 'svelte/store'
5
- import { PUBLIC_GOOGLE_CLIENT_ID } from '$env/static/public'
6
-
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
- })
19
-
20
- let referrer: string | null
21
- page.subscribe((value) => {
22
- referrer = value.url.searchParams.get('referrer')
23
- })
24
-
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
- })
33
-
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
- }
44
-
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 })
48
-
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
- }
57
-
58
- if (!user) id.prompt()
59
- }
60
-
61
- async function registerLocal(user: User) {
62
- try {
63
- const res = await fetch('/auth/register', {
64
- method: 'POST',
65
- body: JSON.stringify(user), // server ignores user.role - always set it to 'student' (lowest priv)
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
-
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
-
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
-
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
-
129
- return { initializeSignInWithGoogle, registerLocal, loginLocal, logout }
130
- }