sveltekit-auth-example 1.0.9 → 1.0.12
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/CHANGELOG.md +9 -0
- package/README.md +2 -2
- package/package.json +12 -12
- package/src/{global.d.ts → app.d.ts} +47 -13
- package/src/hooks.ts +4 -2
- package/src/lib/auth.ts +13 -9
- package/src/lib/focus.ts +2 -4
- package/src/routes/__error.svelte +6 -3
- package/src/routes/__layout.svelte +1 -6
- package/src/routes/_db.ts +3 -3
- package/src/routes/_send-in-blue.ts +8 -8
- package/src/routes/admin.svelte +3 -3
- package/src/routes/api/v1/_auth.ts +5 -0
- package/src/routes/api/v1/admin.ts +3 -4
- package/src/routes/api/v1/teacher.ts +3 -4
- package/src/routes/api/v1/user.ts +3 -4
- package/src/routes/auth/[slug].ts +1 -1
- package/src/routes/auth/forgot.ts +4 -4
- package/src/routes/auth/google.ts +18 -11
- package/src/routes/auth/reset/[token].svelte +1 -1
- package/src/routes/auth/reset/index.ts +2 -2
- package/src/routes/forgot.svelte +1 -1
- package/src/routes/login.svelte +5 -3
- package/src/routes/profile.svelte +10 -5
- package/src/routes/register.svelte +9 -5
- package/src/routes/teachers.svelte +3 -3
- package/svelte.config.js +1 -4
package/.eslintrc.cjs
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
# 1.0.12
|
|
2
|
+
* Remove unnecessary reference from app.d.ts
|
|
3
|
+
* Remove commented lines in svelte.config.js
|
|
4
|
+
|
|
5
|
+
# 1.0.10
|
|
6
|
+
* Bump dependencies
|
|
7
|
+
* Adjust for changes to SvelteKit
|
|
8
|
+
* Improve typings
|
|
9
|
+
|
|
1
10
|
# 1.0.9
|
|
2
11
|
* Bump dependencies
|
|
3
12
|
* Adjust for changes to SvelteKit with respect to vite
|
package/README.md
CHANGED
|
@@ -31,8 +31,8 @@ The forgot password functionality uses SendInBlue to send the email. You would n
|
|
|
31
31
|
|
|
32
32
|
## Prerequisites
|
|
33
33
|
- PostgreSQL 14 or higher
|
|
34
|
-
- Node.js
|
|
35
|
-
- npm 8.
|
|
34
|
+
- Node.js 18.7.0 or higher
|
|
35
|
+
- npm 8.15.0 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/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.12",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "Nate Stuyvesant",
|
|
7
7
|
"license": "https://github.com/nstuyvesant/sveltekit-auth-example/blob/master/LICENSE",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ."
|
|
34
34
|
},
|
|
35
35
|
"engines": {
|
|
36
|
-
"node": "~
|
|
37
|
-
"npm": "^8.
|
|
36
|
+
"node": "~18.7.0",
|
|
37
|
+
"npm": "^8.15.1"
|
|
38
38
|
},
|
|
39
39
|
"type": "module",
|
|
40
40
|
"dependencies": {
|
|
@@ -42,30 +42,30 @@
|
|
|
42
42
|
"dotenv": "^16.0.1",
|
|
43
43
|
"google-auth-library": "^8.1.1",
|
|
44
44
|
"jsonwebtoken": "^8.5.1",
|
|
45
|
-
"pg": "^8.7.3"
|
|
46
|
-
"pg-native": "^3.0.0"
|
|
45
|
+
"pg": "^8.7.3"
|
|
47
46
|
},
|
|
48
47
|
"devDependencies": {
|
|
49
48
|
"@sveltejs/adapter-node": "latest",
|
|
50
49
|
"@sveltejs/kit": "latest",
|
|
50
|
+
"@types/bootstrap": "5.2.1",
|
|
51
51
|
"@types/cookie": "^0.5.1",
|
|
52
52
|
"@types/jsonwebtoken": "^8.5.8",
|
|
53
53
|
"@types/pg": "^8.6.5",
|
|
54
|
-
"@typescript-eslint/eslint-plugin": "^5.
|
|
55
|
-
"@typescript-eslint/parser": "^5.
|
|
56
|
-
"bootstrap": "^5.
|
|
57
|
-
"bootstrap-icons": "^1.9.
|
|
58
|
-
"eslint": "^8.
|
|
54
|
+
"@typescript-eslint/eslint-plugin": "^5.32.0",
|
|
55
|
+
"@typescript-eslint/parser": "^5.32.0",
|
|
56
|
+
"bootstrap": "^5.2.0",
|
|
57
|
+
"bootstrap-icons": "^1.9.1",
|
|
58
|
+
"eslint": "^8.21.0",
|
|
59
59
|
"eslint-config-prettier": "^8.5.0",
|
|
60
60
|
"eslint-plugin-svelte3": "^4.0.0",
|
|
61
61
|
"prettier": "^2.7.1",
|
|
62
62
|
"prettier-plugin-svelte": "^2.7.0",
|
|
63
|
-
"sass": "^1.
|
|
63
|
+
"sass": "^1.54.0",
|
|
64
64
|
"svelte": "^3.49.0",
|
|
65
65
|
"svelte-check": "^2.8.0",
|
|
66
66
|
"svelte-preprocess": "^4.10.7",
|
|
67
67
|
"tslib": "^2.4.0",
|
|
68
68
|
"typescript": "^4.7.4",
|
|
69
|
-
"vite": "^3.0.
|
|
69
|
+
"vite": "^3.0.4"
|
|
70
70
|
}
|
|
71
71
|
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
/// <reference types="
|
|
2
|
-
|
|
3
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
1
|
+
/// <reference types="bootstrap" />
|
|
4
2
|
|
|
5
3
|
// See https://kit.svelte.dev/docs/typescript
|
|
6
4
|
// for information about these interfaces
|
|
@@ -12,18 +10,12 @@ declare namespace App {
|
|
|
12
10
|
// interface Platform {}
|
|
13
11
|
|
|
14
12
|
interface Session {
|
|
15
|
-
reservationDate: Date
|
|
16
|
-
scheduledClass?: ScheduledClass
|
|
17
13
|
user?: User
|
|
18
14
|
}
|
|
19
15
|
|
|
20
16
|
// interface Stuff {}
|
|
21
17
|
}
|
|
22
18
|
|
|
23
|
-
interface ImportMetaEnv {
|
|
24
|
-
VITE_GOOGLE_CLIENT_ID: string
|
|
25
|
-
}
|
|
26
|
-
|
|
27
19
|
type AuthenticationResult = {
|
|
28
20
|
statusCode: number
|
|
29
21
|
status: string
|
|
@@ -36,14 +28,31 @@ type Credentials = {
|
|
|
36
28
|
password: string
|
|
37
29
|
}
|
|
38
30
|
|
|
31
|
+
interface GoogleCredentialResponse {
|
|
32
|
+
credential: string
|
|
33
|
+
select_by:
|
|
34
|
+
| 'auto'
|
|
35
|
+
| 'user'
|
|
36
|
+
| 'user_1tap'
|
|
37
|
+
| 'user_2tap'
|
|
38
|
+
| 'btn'
|
|
39
|
+
| 'btn_confirm'
|
|
40
|
+
| 'btn_add_session'
|
|
41
|
+
| 'btn_confirm_add_session'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface ImportMetaEnv {
|
|
45
|
+
VITE_GOOGLE_CLIENT_ID: string
|
|
46
|
+
}
|
|
47
|
+
|
|
39
48
|
type MessageAddressee = {
|
|
40
49
|
email: string
|
|
41
50
|
name?: string
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
type Message = {
|
|
45
|
-
sender?: MessageAddressee
|
|
46
|
-
to
|
|
54
|
+
sender?: MessageAddressee
|
|
55
|
+
to?: MessageAddressee[]
|
|
47
56
|
subject: string
|
|
48
57
|
htmlContent?: string
|
|
49
58
|
textContent?: string
|
|
@@ -51,9 +60,30 @@ type Message = {
|
|
|
51
60
|
contact?: Person
|
|
52
61
|
}
|
|
53
62
|
|
|
63
|
+
interface SendInBlueContact {
|
|
64
|
+
updateEnabled: boolean
|
|
65
|
+
email: string
|
|
66
|
+
emailBlacklisted: boolean
|
|
67
|
+
attributes: {
|
|
68
|
+
NAME: string
|
|
69
|
+
SURNAME: string
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface SendInBlueMessage extends Message {
|
|
74
|
+
sender: MessageAddressee
|
|
75
|
+
to: MessageAddressee[]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface SendInBlueRequest extends RequestInit {
|
|
79
|
+
headers: {
|
|
80
|
+
'api-key': string
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
54
84
|
type User = {
|
|
55
|
-
id
|
|
56
|
-
role
|
|
85
|
+
id: number
|
|
86
|
+
role: 'student' | 'teacher' | 'admin'
|
|
57
87
|
password?: string
|
|
58
88
|
firstName?: string
|
|
59
89
|
lastName?: string
|
|
@@ -66,6 +96,10 @@ type UserSession = {
|
|
|
66
96
|
user: User
|
|
67
97
|
}
|
|
68
98
|
|
|
99
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
100
|
+
|
|
69
101
|
interface Window {
|
|
70
102
|
google?: any
|
|
103
|
+
grecaptcha: any
|
|
104
|
+
bootstrap: Bootstrap
|
|
71
105
|
}
|
package/src/hooks.ts
CHANGED
|
@@ -14,7 +14,7 @@ async function attachUserToRequest(sessionId: string, event: RequestEvent) {
|
|
|
14
14
|
|
|
15
15
|
function deleteCookieIfNoUser(event: RequestEvent, response: Response) {
|
|
16
16
|
if (!event.locals.user) {
|
|
17
|
-
response.headers
|
|
17
|
+
response.headers.set('Set-Cookie', `session=; Path=/; HttpOnly; SameSite=Lax; Expires=${new Date().toUTCString()}`)
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -27,7 +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
|
|
30
|
+
const response = await resolve(event, {
|
|
31
|
+
ssr: !event.request.url.includes('/admin')
|
|
32
|
+
})
|
|
31
33
|
|
|
32
34
|
// after endpoint or page is called
|
|
33
35
|
deleteCookieIfNoUser(event, response)
|
package/src/lib/auth.ts
CHANGED
|
@@ -10,15 +10,15 @@ type Page = Readable<{
|
|
|
10
10
|
error: Error | null;
|
|
11
11
|
}>
|
|
12
12
|
|
|
13
|
-
export default function useAuth(page: Page, session: Writable<any>, goto) {
|
|
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
14
|
|
|
15
15
|
// Required to use session.set()
|
|
16
|
-
let sessionValue
|
|
16
|
+
let sessionValue: App.Session
|
|
17
17
|
session.subscribe(value => {
|
|
18
18
|
sessionValue = value
|
|
19
19
|
})
|
|
20
20
|
|
|
21
|
-
let referrer
|
|
21
|
+
let referrer: string | null
|
|
22
22
|
page.subscribe(value => {
|
|
23
23
|
referrer = value.url.searchParams.get('referrer')
|
|
24
24
|
})
|
|
@@ -67,7 +67,7 @@ export default function useAuth(page: Page, session: Writable<any>, goto) {
|
|
|
67
67
|
}))
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
async function googleCallback(response) {
|
|
70
|
+
async function googleCallback(response: GoogleCredentialResponse) {
|
|
71
71
|
const res = await fetch('/auth/google', {
|
|
72
72
|
method: 'POST',
|
|
73
73
|
headers: {
|
|
@@ -105,12 +105,14 @@ export default function useAuth(page: Page, session: Writable<any>, goto) {
|
|
|
105
105
|
throw new Error(fromEndpoint.message)
|
|
106
106
|
}
|
|
107
107
|
} catch (err) {
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
if (err instanceof Error) {
|
|
109
|
+
console.error('Login error', err)
|
|
110
|
+
throw new Error(err.message)
|
|
111
|
+
}
|
|
110
112
|
}
|
|
111
113
|
}
|
|
112
114
|
|
|
113
|
-
async function loginLocal(credentials) {
|
|
115
|
+
async function loginLocal(credentials: Credentials) {
|
|
114
116
|
try {
|
|
115
117
|
const res = await fetch('/auth/login', {
|
|
116
118
|
method: 'POST',
|
|
@@ -131,8 +133,10 @@ export default function useAuth(page: Page, session: Writable<any>, goto) {
|
|
|
131
133
|
throw new Error(fromEndpoint.message)
|
|
132
134
|
}
|
|
133
135
|
} catch (err) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
+
if (err instanceof Error) {
|
|
137
|
+
console.error('Login error', err)
|
|
138
|
+
throw new Error(err.message)
|
|
139
|
+
}
|
|
136
140
|
}
|
|
137
141
|
}
|
|
138
142
|
|
package/src/lib/focus.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export const focusOnFirstError: FocusResult = (form: HTMLFormElement) => {
|
|
1
|
+
export const focusOnFirstError = (form: HTMLFormElement) => {
|
|
4
2
|
for(const field of form.elements) {
|
|
5
|
-
if (!field.checkValidity()) {
|
|
3
|
+
if (field instanceof HTMLInputElement && !field.checkValidity()) {
|
|
6
4
|
field.focus()
|
|
7
5
|
break
|
|
8
6
|
}
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
<script lang="ts" context="module">
|
|
2
|
-
|
|
2
|
+
import type { Load } from '@sveltejs/kit'
|
|
3
|
+
|
|
4
|
+
export const load: Load = ({ error, status }) => {
|
|
5
|
+
const { message } = <Error> error
|
|
3
6
|
return {
|
|
4
7
|
props: {
|
|
5
|
-
errorMessage: `${status}: ${
|
|
8
|
+
errorMessage: `${status}: ${message}`
|
|
6
9
|
}
|
|
7
10
|
}
|
|
8
11
|
}
|
|
9
12
|
</script>
|
|
10
13
|
|
|
11
14
|
<script lang="ts">
|
|
12
|
-
export let errorMessage
|
|
15
|
+
export let errorMessage = ''
|
|
13
16
|
</script>
|
|
14
17
|
|
|
15
18
|
<h1>Error</h1>
|
|
@@ -8,12 +8,7 @@
|
|
|
8
8
|
// Vue.js Composition API style
|
|
9
9
|
const { loadScript, initializeSignInWithGoogle, logout } = useAuth(page, session, goto)
|
|
10
10
|
|
|
11
|
-
let
|
|
12
|
-
session.subscribe(value => {
|
|
13
|
-
sessionValue = value
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
let Toast
|
|
11
|
+
let Toast: any
|
|
17
12
|
|
|
18
13
|
onMount(async() => {
|
|
19
14
|
await import('bootstrap/js/dist/collapse')
|
package/src/routes/_db.ts
CHANGED
|
@@ -5,13 +5,13 @@ import pg from 'pg'
|
|
|
5
5
|
|
|
6
6
|
dotenv.config()
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const pool = new pg.Pool({
|
|
9
9
|
max: 10, // default
|
|
10
|
-
connectionString: process.env
|
|
10
|
+
connectionString: process.env.DATABASE_URL,
|
|
11
11
|
ssl: {
|
|
12
12
|
rejectUnauthorized: false
|
|
13
13
|
}
|
|
14
14
|
})
|
|
15
15
|
|
|
16
16
|
type PostgresQueryResult = (sql: string, params?: any[]) => Promise<QueryResult<any>>
|
|
17
|
-
export const query: PostgresQueryResult = (sql, params?) =>
|
|
17
|
+
export const query: PostgresQueryResult = (sql, params?) => pool.query(sql, params)
|
|
@@ -2,14 +2,14 @@ import dotenv from 'dotenv'
|
|
|
2
2
|
|
|
3
3
|
dotenv.config()
|
|
4
4
|
|
|
5
|
-
const SEND_IN_BLUE_KEY = process.env
|
|
6
|
-
const SEND_IN_BLUE_URL = process.env
|
|
7
|
-
const SEND_IN_BLUE_FROM = <MessageAddressee> JSON.parse(process.env
|
|
8
|
-
const SEND_IN_BLUE_ADMINS = <MessageAddressee> JSON.parse(process.env
|
|
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 || '')
|
|
9
9
|
|
|
10
10
|
// POST or PUT submission to SendInBlue
|
|
11
|
-
const submit = async (method, url, data) => {
|
|
12
|
-
const response: Response = await fetch(`${SEND_IN_BLUE_URL}${url}`, {
|
|
11
|
+
const submit = async (method: string, url: string, data: Partial<SendInBlueContact> | SendInBlueMessage) => {
|
|
12
|
+
const response: Response = await fetch(`${SEND_IN_BLUE_URL}${url}`, <SendInBlueRequest> {
|
|
13
13
|
method,
|
|
14
14
|
headers: {
|
|
15
15
|
'Content-Type': 'application/json',
|
|
@@ -23,7 +23,7 @@ const submit = async (method, url, data) => {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const sender =
|
|
26
|
+
const sender = SEND_IN_BLUE_FROM
|
|
27
27
|
const to = SEND_IN_BLUE_ADMINS
|
|
28
28
|
|
|
29
|
-
export const sendMessage = async (message: Message) => submit('POST', '/v3/smtp/email', { sender, to: [to], ...message })
|
|
29
|
+
export const sendMessage = async (message: Message) => submit('POST', '/v3/smtp/email', { sender, to: [to], ...message })
|
package/src/routes/admin.svelte
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<script context="module" lang="ts">
|
|
2
2
|
import type { Load } from '@sveltejs/kit'
|
|
3
3
|
|
|
4
|
-
export const load: Load = async ({ session }) => {
|
|
4
|
+
export const load: Load = async ({ fetch, session }) => {
|
|
5
5
|
const authorized = ['admin']
|
|
6
|
-
if (!authorized.includes(session.user
|
|
6
|
+
if (session.user && !authorized.includes(session.user.role)) {
|
|
7
7
|
return {
|
|
8
8
|
status: 302,
|
|
9
9
|
redirect: '/login?referrer=/admin'
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
</script>
|
|
29
29
|
|
|
30
30
|
<script lang="ts">
|
|
31
|
-
export let message
|
|
31
|
+
export let message = ''
|
|
32
32
|
</script>
|
|
33
33
|
|
|
34
34
|
<svelte:head>
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { RequestHandler } from '@sveltejs/kit'
|
|
2
|
+
import { requestAuthorized } from './_auth'
|
|
2
3
|
|
|
3
|
-
export const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
if (!event.locals.user || !authorized.includes(event.locals.user.role)) {
|
|
4
|
+
export const GET: RequestHandler = async event => {
|
|
5
|
+
if (!requestAuthorized(event, ['admin'])) {
|
|
7
6
|
return {
|
|
8
7
|
status: 401,
|
|
9
8
|
body: {
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { RequestHandler } from '@sveltejs/kit'
|
|
2
|
+
import { requestAuthorized } from './_auth'
|
|
2
3
|
|
|
3
|
-
export const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
if (!event.locals.user || !authorized.includes(event.locals.user.role)) {
|
|
4
|
+
export const GET: RequestHandler = async event=> {
|
|
5
|
+
if (!requestAuthorized(event, ['admin', 'teacher'])) {
|
|
7
6
|
return {
|
|
8
7
|
status: 401,
|
|
9
8
|
body: {
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import type { RequestHandler } from '@sveltejs/kit'
|
|
2
2
|
import { query } from '../../_db'
|
|
3
|
+
import { requestAuthorized } from './_auth'
|
|
3
4
|
|
|
4
|
-
export const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
if (!event.locals.user || !authorized.includes(event.locals.user.role)) {
|
|
5
|
+
export const PUT: RequestHandler = async event => {
|
|
6
|
+
if (!requestAuthorized(event, ['admin', 'teacher', 'student'])) {
|
|
8
7
|
return {
|
|
9
8
|
status: 401,
|
|
10
9
|
body: {
|
|
@@ -6,10 +6,10 @@ import { query } from '../_db'
|
|
|
6
6
|
import { sendMessage } from '../_send-in-blue'
|
|
7
7
|
|
|
8
8
|
dotenv.config()
|
|
9
|
-
const DOMAIN = process.env
|
|
10
|
-
const JWT_SECRET
|
|
9
|
+
const DOMAIN = process.env.DOMAIN
|
|
10
|
+
const JWT_SECRET = process.env.JWT_SECRET
|
|
11
11
|
|
|
12
|
-
export const
|
|
12
|
+
export const POST: RequestHandler = async event => {
|
|
13
13
|
const body = await event.request.json()
|
|
14
14
|
const sql = `SELECT id as "userId" FROM users WHERE email = $1 LIMIT 1;`
|
|
15
15
|
const { rows } = await query(sql, [body.email])
|
|
@@ -18,7 +18,7 @@ export const post: RequestHandler = async event => {
|
|
|
18
18
|
const { userId } = rows[0]
|
|
19
19
|
// Create JWT with userId expiring in 30 mins
|
|
20
20
|
const secret = JWT_SECRET
|
|
21
|
-
const token = jwt.sign({ subject: userId }, secret, {
|
|
21
|
+
const token = jwt.sign({ subject: userId }, <Secret> secret, {
|
|
22
22
|
expiresIn: '30m'
|
|
23
23
|
})
|
|
24
24
|
|
|
@@ -4,7 +4,7 @@ import { query } from '../_db';
|
|
|
4
4
|
import { config } from '$lib/config'
|
|
5
5
|
|
|
6
6
|
// Verify JWT per https://developers.google.com/identity/gsi/web/guides/verify-google-id-token
|
|
7
|
-
async function getGoogleUserFromJWT(token: string): Promise<User
|
|
7
|
+
async function getGoogleUserFromJWT(token: string): Promise<Partial<User>> {
|
|
8
8
|
try {
|
|
9
9
|
const clientId = config.googleClientId
|
|
10
10
|
const client = new OAuth2Client(clientId)
|
|
@@ -13,29 +13,34 @@ async function getGoogleUserFromJWT(token: string): Promise<User> {
|
|
|
13
13
|
audience: clientId
|
|
14
14
|
});
|
|
15
15
|
const payload = ticket.getPayload()
|
|
16
|
+
if (!payload) throw new Error('Google authentication did not get the expected payload')
|
|
16
17
|
return {
|
|
17
|
-
firstName: payload['given_name'],
|
|
18
|
-
lastName: payload['family_name'],
|
|
18
|
+
firstName: payload['given_name'] || 'UnknownFirstName',
|
|
19
|
+
lastName: payload['family_name'] || 'UnknownLastName',
|
|
19
20
|
email: payload['email']
|
|
20
21
|
}
|
|
21
|
-
} catch (
|
|
22
|
-
|
|
22
|
+
} catch (err) {
|
|
23
|
+
let message = ''
|
|
24
|
+
if (err instanceof Error) message = err.message
|
|
25
|
+
throw new Error(`Google user could not be authenticated: ${message}`)
|
|
23
26
|
}
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
// Upsert user and get session ID
|
|
27
|
-
async function upsertGoogleUser(user: User): Promise<UserSession> {
|
|
30
|
+
async function upsertGoogleUser(user: Partial<User>): Promise<UserSession> {
|
|
28
31
|
try {
|
|
29
32
|
const sql = `SELECT start_gmail_user_session($1) AS user_session;`
|
|
30
33
|
const { rows } = await query(sql, [JSON.stringify(user)])
|
|
31
34
|
return <UserSession> rows[0].user_session
|
|
32
|
-
} catch (
|
|
33
|
-
|
|
35
|
+
} catch (err) {
|
|
36
|
+
let message = ''
|
|
37
|
+
if (err instanceof Error) message = err.message
|
|
38
|
+
throw new Error(`GMail user could not be upserted: ${message}`)
|
|
34
39
|
}
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
// Returns local user if Google user authenticated (and authorized our app)
|
|
38
|
-
export const
|
|
43
|
+
export const POST: RequestHandler = async event => {
|
|
39
44
|
try {
|
|
40
45
|
const { token } = await event.request.json()
|
|
41
46
|
const user = await getGoogleUserFromJWT(token)
|
|
@@ -54,11 +59,13 @@ export const post: RequestHandler = async event => {
|
|
|
54
59
|
user: userSession.user
|
|
55
60
|
}
|
|
56
61
|
}
|
|
57
|
-
} catch (
|
|
62
|
+
} catch (err) {
|
|
63
|
+
let message = ''
|
|
64
|
+
if (err instanceof Error) message = err.message
|
|
58
65
|
return { // session cookie deleted by hooks.js handle()
|
|
59
66
|
status: 401,
|
|
60
67
|
body: {
|
|
61
|
-
message:
|
|
68
|
+
message: message
|
|
62
69
|
}
|
|
63
70
|
}
|
|
64
71
|
}
|
|
@@ -6,9 +6,9 @@ import { query } from '../../_db'
|
|
|
6
6
|
|
|
7
7
|
dotenv.config()
|
|
8
8
|
|
|
9
|
-
const JWT_SECRET = process.env
|
|
9
|
+
const JWT_SECRET = <jwt.Secret> process.env.JWT_SECRET
|
|
10
10
|
|
|
11
|
-
export const
|
|
11
|
+
export const PUT: RequestHandler = async event => {
|
|
12
12
|
const body = await event.request.json()
|
|
13
13
|
const { token, password } = body
|
|
14
14
|
|
package/src/routes/forgot.svelte
CHANGED
package/src/routes/login.svelte
CHANGED
|
@@ -16,14 +16,16 @@
|
|
|
16
16
|
|
|
17
17
|
async function login() {
|
|
18
18
|
message = ''
|
|
19
|
-
const form = document.
|
|
19
|
+
const form = <HTMLFormElement> document.getElementById('signIn')
|
|
20
20
|
|
|
21
21
|
if (form.checkValidity()) {
|
|
22
22
|
try {
|
|
23
23
|
await loginLocal(credentials)
|
|
24
24
|
} catch (err) {
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
if (err instanceof Error) {
|
|
26
|
+
console.error('Login error', err.message)
|
|
27
|
+
message = err.message
|
|
28
|
+
}
|
|
27
29
|
}
|
|
28
30
|
} else {
|
|
29
31
|
form.classList.add('was-validated')
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
export const load: Load = ({ session }) => {
|
|
5
5
|
const authorized = ['admin', 'teacher', 'student'] // must be logged-in
|
|
6
|
-
if (!authorized.includes(session.user
|
|
6
|
+
if (session.user && !authorized.includes(session.user.role)) {
|
|
7
7
|
return {
|
|
8
8
|
status: 302,
|
|
9
9
|
redirect: '/login?referrer=/profile'
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
</script>
|
|
21
21
|
|
|
22
22
|
<script lang="ts">
|
|
23
|
+
import { onMount } from 'svelte'
|
|
23
24
|
import { session } from '$app/stores'
|
|
24
25
|
import { focusOnFirstError } from '$lib/focus';
|
|
25
26
|
|
|
@@ -28,11 +29,15 @@
|
|
|
28
29
|
let confirmPassword: HTMLInputElement
|
|
29
30
|
export let user: User
|
|
30
31
|
|
|
32
|
+
onMount(() => {
|
|
33
|
+
focusedField.focus()
|
|
34
|
+
})
|
|
35
|
+
|
|
31
36
|
async function update() {
|
|
32
37
|
message = ''
|
|
33
|
-
const form = document.
|
|
38
|
+
const form = <HTMLFormElement> document.getElementById('profile')
|
|
34
39
|
|
|
35
|
-
if (!user
|
|
40
|
+
if (!user?.email?.includes('gmail.com') && !passwordMatch()) {
|
|
36
41
|
confirmPassword.classList.add('is-invalid')
|
|
37
42
|
return
|
|
38
43
|
}
|
|
@@ -72,7 +77,7 @@
|
|
|
72
77
|
<h4><strong>Profile</strong></h4>
|
|
73
78
|
<p>Update your information.</p>
|
|
74
79
|
<form id="profile" autocomplete="on" novalidate class="mt-3">
|
|
75
|
-
{#if !user
|
|
80
|
+
{#if !user?.email?.includes('gmail.com')}
|
|
76
81
|
<div class="mb-3">
|
|
77
82
|
<label class="form-label" for="email">Email</label>
|
|
78
83
|
<input bind:this={focusedField} type="email" class="form-control" bind:value={user.email} required placeholder="Email" id="email" autocomplete="email"/>
|
|
@@ -92,7 +97,7 @@
|
|
|
92
97
|
{/if}
|
|
93
98
|
<div class="mb-3">
|
|
94
99
|
<label class="form-label" for="firstName">First name</label>
|
|
95
|
-
<input bind:value={user.firstName} class="form-control" id="firstName" required placeholder="First name" autocomplete="given-name"/>
|
|
100
|
+
<input bind:this={focusedField} bind:value={user.firstName} class="form-control" id="firstName" required placeholder="First name" autocomplete="given-name"/>
|
|
96
101
|
<div class="invalid-feedback">First name required</div>
|
|
97
102
|
</div>
|
|
98
103
|
<div class="mb-3">
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
|
|
24
24
|
let focusedField: HTMLInputElement
|
|
25
25
|
|
|
26
|
-
let user = {
|
|
26
|
+
let user: User = {
|
|
27
|
+
id: 0,
|
|
28
|
+
role: 'student',
|
|
27
29
|
firstName: '',
|
|
28
30
|
lastName: '',
|
|
29
31
|
password: '',
|
|
@@ -34,7 +36,7 @@
|
|
|
34
36
|
let message: string
|
|
35
37
|
|
|
36
38
|
async function register() {
|
|
37
|
-
const form = document.
|
|
39
|
+
const form = <HTMLFormElement> document.getElementById('register')
|
|
38
40
|
message = ''
|
|
39
41
|
|
|
40
42
|
if (!passwordMatch()) {
|
|
@@ -46,8 +48,10 @@
|
|
|
46
48
|
try {
|
|
47
49
|
await registerLocal(user)
|
|
48
50
|
} catch (err) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
if (err instanceof Error) {
|
|
52
|
+
message = err.message
|
|
53
|
+
console.error('Login error', message)
|
|
54
|
+
}
|
|
51
55
|
}
|
|
52
56
|
} else {
|
|
53
57
|
form.classList.add('was-validated')
|
|
@@ -59,7 +63,7 @@
|
|
|
59
63
|
onMount(() => {
|
|
60
64
|
focusedField.focus()
|
|
61
65
|
initializeSignInWithGoogle()
|
|
62
|
-
google.accounts.id.renderButton(
|
|
66
|
+
window.google.accounts.id.renderButton(
|
|
63
67
|
document.getElementById('googleButton'),
|
|
64
68
|
{ theme: 'filled_blue', size: 'large', width: '367' } // customization attributes
|
|
65
69
|
)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<script context="module" lang="ts">
|
|
2
2
|
import type { Load } from '@sveltejs/kit'
|
|
3
3
|
|
|
4
|
-
export const load: Load = async ({ session }) => {
|
|
4
|
+
export const load: Load = async ({ fetch, session }) => {
|
|
5
5
|
const authorized = ['admin', 'teacher']
|
|
6
|
-
if (!authorized.includes(session.user
|
|
6
|
+
if (!session.user || !authorized.includes(session.user.role)) {
|
|
7
7
|
return {
|
|
8
8
|
status: 302,
|
|
9
9
|
redirect: '/login?referrer=/teachers'
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
</script>
|
|
28
28
|
|
|
29
29
|
<script lang="ts">
|
|
30
|
-
export let message
|
|
30
|
+
export let message = ''
|
|
31
31
|
</script>
|
|
32
32
|
|
|
33
33
|
<svelte:head>
|
package/svelte.config.js
CHANGED
|
@@ -5,8 +5,6 @@ const production = process.env.NODE_ENV === 'production'
|
|
|
5
5
|
|
|
6
6
|
const baseCsp = [
|
|
7
7
|
'self',
|
|
8
|
-
'ws://127.0.0.1:3000/',
|
|
9
|
-
// 'strict-dynamic', // issues with datepicker on classes, add to calendar scripts
|
|
10
8
|
'https://www.gstatic.com/recaptcha/', // recaptcha
|
|
11
9
|
'https://accounts.google.com/gsi/', // sign-in w/google
|
|
12
10
|
'https://www.google.com/recaptcha/', // recapatcha
|
|
@@ -31,8 +29,7 @@ const config = {
|
|
|
31
29
|
'img-src': ['data:', 'blob:', ...baseCsp],
|
|
32
30
|
'style-src': ['unsafe-inline', ...baseCsp],
|
|
33
31
|
'object-src': ['none'],
|
|
34
|
-
'base-uri': ['self']
|
|
35
|
-
// 'require-trusted-types-for': ["'script'"] // will require effort to get this working
|
|
32
|
+
'base-uri': ['self']
|
|
36
33
|
}
|
|
37
34
|
}
|
|
38
35
|
}
|