sveltekit-auth-example 1.0.24 → 1.0.26
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 +8 -0
- package/README.md +2 -2
- package/db_create.sql +2 -1
- package/package.json +11 -11
- package/src/app.d.ts +2 -1
- package/src/app.html +1 -1
- package/src/routes/+layout.svelte +50 -6
- package/src/routes/login/+page.svelte +34 -4
- package/src/routes/register/+page.svelte +33 -5
- package/src/lib/auth.ts +0 -130
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
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.26
|
|
5
|
+
* On the client, track whether the login session has expired and if so, clear $loginSession
|
|
6
|
+
* Update dependencies
|
|
7
|
+
|
|
8
|
+
# 1.0.25
|
|
9
|
+
* Bump dependencies
|
|
10
|
+
* Simplify Sign In With Google
|
|
11
|
+
|
|
4
12
|
# 1.0.24
|
|
5
13
|
* Bump dependencies
|
|
6
14
|
|
package/README.md
CHANGED
|
@@ -31,8 +31,8 @@ The forgot password / password reset functionality uses a JWT and [**SendInBlue*
|
|
|
31
31
|
|
|
32
32
|
## Prerequisites
|
|
33
33
|
- PostgreSQL 14.5 or higher
|
|
34
|
-
- Node.js 18.
|
|
35
|
-
- npm 8.19.
|
|
34
|
+
- Node.js 18.10.0 or higher
|
|
35
|
+
- npm 8.19.2 or higher
|
|
36
36
|
- Google API client
|
|
37
37
|
- SendInBlue account (only used for emailing password reset link - the sample can run without it but forgot password will not work)
|
|
38
38
|
|
package/db_create.sql
CHANGED
|
@@ -158,7 +158,8 @@ SELECT json_build_object(
|
|
|
158
158
|
'email', users.email,
|
|
159
159
|
'firstName', users.first_name,
|
|
160
160
|
'lastName', users.last_name,
|
|
161
|
-
'phone', users.phone
|
|
161
|
+
'phone', users.phone,
|
|
162
|
+
'expires', sessions.expires
|
|
162
163
|
) AS user
|
|
163
164
|
FROM sessions
|
|
164
165
|
INNER JOIN users ON sessions.user_id = users.id
|
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.26",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "Nate Stuyvesant",
|
|
7
7
|
"license": "https://github.com/nstuyvesant/sveltekit-auth-example/blob/master/LICENSE",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"format": "prettier --write ."
|
|
33
33
|
},
|
|
34
34
|
"engines": {
|
|
35
|
-
"node": "~18.
|
|
35
|
+
"node": "~18.10.0",
|
|
36
36
|
"npm": "^8.19.2"
|
|
37
37
|
},
|
|
38
38
|
"type": "module",
|
|
@@ -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.
|
|
50
|
-
"@typescript-eslint/parser": "^5.
|
|
49
|
+
"@typescript-eslint/eslint-plugin": "^5.38.1",
|
|
50
|
+
"@typescript-eslint/parser": "^5.38.1",
|
|
51
51
|
"bootstrap": "^5.2.1",
|
|
52
|
-
"eslint": "^8.
|
|
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.
|
|
55
|
+
"google-auth-library": "^8.5.2",
|
|
56
56
|
"jsonwebtoken": "^8.5.1",
|
|
57
57
|
"prettier": "^2.7.1",
|
|
58
|
-
"prettier-plugin-svelte": "^2.7.
|
|
59
|
-
"sass": "^1.
|
|
58
|
+
"prettier-plugin-svelte": "^2.7.1",
|
|
59
|
+
"sass": "^1.55.0",
|
|
60
60
|
"svelte": "^3.50.1",
|
|
61
|
-
"svelte-check": "^2.9.
|
|
61
|
+
"svelte-check": "^2.9.1",
|
|
62
62
|
"svelte-preprocess": "^4.10.7",
|
|
63
63
|
"tslib": "^2.4.0",
|
|
64
|
-
"typescript": "^4.8.
|
|
65
|
-
"vite": "^3.1.
|
|
64
|
+
"typescript": "^4.8.4",
|
|
65
|
+
"vite": "^3.1.4"
|
|
66
66
|
}
|
|
67
67
|
}
|
package/src/app.d.ts
CHANGED
|
@@ -91,6 +91,7 @@ interface SendInBlueRequest extends RequestInit {
|
|
|
91
91
|
|
|
92
92
|
interface UserProperties {
|
|
93
93
|
id: number
|
|
94
|
+
expires?: string // ISO-8601 datetime
|
|
94
95
|
role: 'student' | 'teacher' | 'admin'
|
|
95
96
|
password?: string
|
|
96
97
|
firstName?: string
|
|
@@ -99,7 +100,7 @@ interface UserProperties {
|
|
|
99
100
|
phone?: string
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
type User = UserProperties | undefined
|
|
103
|
+
type User = UserProperties | undefined | null
|
|
103
104
|
|
|
104
105
|
interface UserSession {
|
|
105
106
|
id: string,
|
package/src/app.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
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>
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount } from 'svelte'
|
|
3
3
|
import type { LayoutServerData } from './$types'
|
|
4
|
-
import { goto } from '$app/navigation'
|
|
4
|
+
import { goto, beforeNavigate } from '$app/navigation'
|
|
5
5
|
import { page } from '$app/stores'
|
|
6
6
|
import { loginSession, toast } from '../stores'
|
|
7
|
-
import
|
|
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,62 @@
|
|
|
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
|
|
|
18
|
+
beforeNavigate( () => {
|
|
19
|
+
let expirationDate = $loginSession?.expires ? new Date($loginSession.expires) : undefined
|
|
20
|
+
|
|
21
|
+
if (expirationDate && expirationDate < new Date()) {
|
|
22
|
+
console.log('Login session expired.')
|
|
23
|
+
$loginSession = null
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
21
27
|
onMount(async () => {
|
|
22
28
|
await import('bootstrap/js/dist/collapse') // lots of ways to load Bootstrap but prefer this approach to avoid SSR issues
|
|
23
29
|
await import('bootstrap/js/dist/dropdown')
|
|
24
30
|
Toast = (await import('bootstrap/js/dist/toast')).default
|
|
25
|
-
|
|
31
|
+
window.google.accounts.id.initialize({
|
|
32
|
+
client_id: PUBLIC_GOOGLE_CLIENT_ID,
|
|
33
|
+
callback: googleCallback
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
if (!$loginSession) window.google.accounts.id.prompt()
|
|
26
37
|
})
|
|
27
38
|
|
|
39
|
+
async function logout() {
|
|
40
|
+
// Request server delete httpOnly cookie called loginSession
|
|
41
|
+
const url = '/auth/logout'
|
|
42
|
+
const res = await fetch(url, {
|
|
43
|
+
method: 'POST'
|
|
44
|
+
})
|
|
45
|
+
if (res.ok) {
|
|
46
|
+
loginSession.set(undefined) // delete loginSession.user from
|
|
47
|
+
goto('/login')
|
|
48
|
+
} else console.error(`Logout not successful: ${res.statusText} (${res.status})`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function googleCallback(response: GoogleCredentialResponse) {
|
|
52
|
+
const res = await fetch('/auth/google', {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: {
|
|
55
|
+
'Content-Type': 'application/json'
|
|
56
|
+
},
|
|
57
|
+
body: JSON.stringify({ token: response.credential })
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
if (res.ok) {
|
|
61
|
+
const fromEndpoint = await res.json()
|
|
62
|
+
loginSession.set(fromEndpoint.user) // update loginSession store
|
|
63
|
+
const { role } = fromEndpoint.user
|
|
64
|
+
const referrer = $page.url.searchParams.get('referrer')
|
|
65
|
+
if (referrer) return goto(referrer)
|
|
66
|
+
if (role === 'teacher') return goto('/teachers')
|
|
67
|
+
if (role === 'admin') return goto('/admin')
|
|
68
|
+
if (location.pathname === '/login') goto('/') // logged in so go home
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
28
72
|
const openToast = (open: boolean) => {
|
|
29
73
|
if (open) {
|
|
30
74
|
const toastDiv = <HTMLDivElement> document.getElementById('authToast')
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
}
|