sveltekit-auth-example 1.0.7 → 1.0.10
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 +5 -5
- package/package.json +24 -20
- package/src/{global.d.ts → app.d.ts} +45 -10
- package/src/app.html +3 -3
- 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 +0 -5
- package/tsconfig.json +7 -26
- package/vite.config.js +16 -0
package/.eslintrc.cjs
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
# 1.0.10
|
|
2
|
+
* Bump dependencies
|
|
3
|
+
* Adjust for changes to SvelteKit
|
|
4
|
+
* Improve typings
|
|
5
|
+
|
|
6
|
+
# 1.0.9
|
|
7
|
+
* Bump dependencies
|
|
8
|
+
* Adjust for changes to SvelteKit with respect to vite
|
|
9
|
+
|
|
1
10
|
# 1.0.7
|
|
2
11
|
* Bump dependencies and verify against latest SvelteKit
|
|
3
12
|
* Additional changes for register PostgreSQL function
|
package/README.md
CHANGED
|
@@ -30,9 +30,9 @@ Pages use the session.user.role to determine whether they are authorized. While
|
|
|
30
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.
|
|
31
31
|
|
|
32
32
|
## Prerequisites
|
|
33
|
-
- PostgreSQL
|
|
34
|
-
- Node.js 16.
|
|
35
|
-
- npm 8.
|
|
33
|
+
- PostgreSQL 14 or higher
|
|
34
|
+
- Node.js 16.16.0 or higher
|
|
35
|
+
- npm 8.14.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
|
|
|
@@ -53,7 +53,7 @@ npm install
|
|
|
53
53
|
psql -d postgres -f db_create.sql
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
2. Create a **Google API client ID** per [these instructions](https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid).
|
|
56
|
+
2. Create a **Google API client ID** per [these instructions](https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid). Make sure you include `http://localhost:3000`, `http://localhost` in the Authorized JavaScript origins and `http://localhost:3000/auth/google/callback` in the Authorized redirect URIs for your Client ID for Web application. ** Do not access the site using http://127.0.0.1:3000 ** - use `http://localhost:3000` or it will not work.
|
|
57
57
|
|
|
58
58
|
3. Create an **.env** file at the top level of the project with the following values (substituting your own id and PostgreSQL username and password):
|
|
59
59
|
```bash
|
|
@@ -83,4 +83,4 @@ The db_create.sql script adds three users to the database with obvious roles:
|
|
|
83
83
|
|
|
84
84
|
## My ask of you
|
|
85
85
|
|
|
86
|
-
Please report any issues or areas where the code can be optimized.
|
|
86
|
+
Please report any issues or areas where the code can be optimized.
|
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.10",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "Nate Stuyvesant",
|
|
7
7
|
"license": "https://github.com/nstuyvesant/sveltekit-auth-example/blob/master/LICENSE",
|
|
@@ -22,24 +22,25 @@
|
|
|
22
22
|
"example"
|
|
23
23
|
],
|
|
24
24
|
"scripts": {
|
|
25
|
-
"
|
|
25
|
+
"start": "node build",
|
|
26
|
+
"dev": "vite dev",
|
|
26
27
|
"serve": "npm run dev -- --open",
|
|
27
|
-
"build": "
|
|
28
|
-
"preview": "
|
|
28
|
+
"build": "vite build",
|
|
29
|
+
"preview": "vite preview",
|
|
29
30
|
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
30
31
|
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
|
31
32
|
"lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
|
|
32
33
|
"format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ."
|
|
33
34
|
},
|
|
34
35
|
"engines": {
|
|
35
|
-
"node": "~
|
|
36
|
-
"npm": "^8.
|
|
36
|
+
"node": "~18.7.0",
|
|
37
|
+
"npm": "^8.15.1"
|
|
37
38
|
},
|
|
38
39
|
"type": "module",
|
|
39
40
|
"dependencies": {
|
|
40
41
|
"cookie": "^0.5.0",
|
|
41
|
-
"dotenv": "^16.0.
|
|
42
|
-
"google-auth-library": "^8.
|
|
42
|
+
"dotenv": "^16.0.1",
|
|
43
|
+
"google-auth-library": "^8.1.1",
|
|
43
44
|
"jsonwebtoken": "^8.5.1",
|
|
44
45
|
"pg": "^8.7.3",
|
|
45
46
|
"pg-native": "^3.0.0"
|
|
@@ -47,22 +48,25 @@
|
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"@sveltejs/adapter-node": "latest",
|
|
49
50
|
"@sveltejs/kit": "latest",
|
|
51
|
+
"@types/bootstrap": "5.2.1",
|
|
52
|
+
"@types/cookie": "^0.5.1",
|
|
50
53
|
"@types/jsonwebtoken": "^8.5.8",
|
|
51
54
|
"@types/pg": "^8.6.5",
|
|
52
|
-
"@typescript-eslint/eslint-plugin": "^5.
|
|
53
|
-
"@typescript-eslint/parser": "^5.
|
|
54
|
-
"bootstrap": "^5.
|
|
55
|
-
"bootstrap-icons": "^1.
|
|
56
|
-
"eslint": "^8.
|
|
55
|
+
"@typescript-eslint/eslint-plugin": "^5.32.0",
|
|
56
|
+
"@typescript-eslint/parser": "^5.32.0",
|
|
57
|
+
"bootstrap": "^5.2.0",
|
|
58
|
+
"bootstrap-icons": "^1.9.1",
|
|
59
|
+
"eslint": "^8.21.0",
|
|
57
60
|
"eslint-config-prettier": "^8.5.0",
|
|
58
|
-
"eslint-plugin-svelte3": "^
|
|
59
|
-
"prettier": "^2.
|
|
61
|
+
"eslint-plugin-svelte3": "^4.0.0",
|
|
62
|
+
"prettier": "^2.7.1",
|
|
60
63
|
"prettier-plugin-svelte": "^2.7.0",
|
|
61
|
-
"sass": "^1.
|
|
62
|
-
"svelte": "^3.
|
|
63
|
-
"svelte-check": "^2.
|
|
64
|
-
"svelte-preprocess": "^4.10.
|
|
64
|
+
"sass": "^1.54.0",
|
|
65
|
+
"svelte": "^3.49.0",
|
|
66
|
+
"svelte-check": "^2.8.0",
|
|
67
|
+
"svelte-preprocess": "^4.10.7",
|
|
65
68
|
"tslib": "^2.4.0",
|
|
66
|
-
"typescript": "^4.
|
|
69
|
+
"typescript": "^4.7.4",
|
|
70
|
+
"vite": "^3.0.4"
|
|
67
71
|
}
|
|
68
72
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/// <reference types="@sveltejs/kit" />
|
|
2
|
+
/// <reference types="bootstrap" />
|
|
2
3
|
|
|
3
4
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
4
5
|
|
|
@@ -12,18 +13,12 @@ declare namespace App {
|
|
|
12
13
|
// interface Platform {}
|
|
13
14
|
|
|
14
15
|
interface Session {
|
|
15
|
-
reservationDate: Date
|
|
16
|
-
scheduledClass?: ScheduledClass
|
|
17
16
|
user?: User
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
// interface Stuff {}
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
interface ImportMetaEnv {
|
|
24
|
-
VITE_GOOGLE_CLIENT_ID: string
|
|
25
|
-
}
|
|
26
|
-
|
|
27
22
|
type AuthenticationResult = {
|
|
28
23
|
statusCode: number
|
|
29
24
|
status: string
|
|
@@ -36,14 +31,31 @@ type Credentials = {
|
|
|
36
31
|
password: string
|
|
37
32
|
}
|
|
38
33
|
|
|
34
|
+
interface GoogleCredentialResponse {
|
|
35
|
+
credential: string
|
|
36
|
+
select_by:
|
|
37
|
+
| 'auto'
|
|
38
|
+
| 'user'
|
|
39
|
+
| 'user_1tap'
|
|
40
|
+
| 'user_2tap'
|
|
41
|
+
| 'btn'
|
|
42
|
+
| 'btn_confirm'
|
|
43
|
+
| 'btn_add_session'
|
|
44
|
+
| 'btn_confirm_add_session'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface ImportMetaEnv {
|
|
48
|
+
VITE_GOOGLE_CLIENT_ID: string
|
|
49
|
+
}
|
|
50
|
+
|
|
39
51
|
type MessageAddressee = {
|
|
40
52
|
email: string
|
|
41
53
|
name?: string
|
|
42
54
|
}
|
|
43
55
|
|
|
44
56
|
type Message = {
|
|
45
|
-
sender?: MessageAddressee
|
|
46
|
-
to
|
|
57
|
+
sender?: MessageAddressee
|
|
58
|
+
to?: MessageAddressee[]
|
|
47
59
|
subject: string
|
|
48
60
|
htmlContent?: string
|
|
49
61
|
textContent?: string
|
|
@@ -51,9 +63,30 @@ type Message = {
|
|
|
51
63
|
contact?: Person
|
|
52
64
|
}
|
|
53
65
|
|
|
66
|
+
interface SendInBlueContact {
|
|
67
|
+
updateEnabled: boolean
|
|
68
|
+
email: string
|
|
69
|
+
emailBlacklisted: boolean
|
|
70
|
+
attributes: {
|
|
71
|
+
NAME: string
|
|
72
|
+
SURNAME: string
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface SendInBlueMessage extends Message {
|
|
77
|
+
sender: MessageAddressee
|
|
78
|
+
to: MessageAddressee[]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface SendInBlueRequest extends RequestInit {
|
|
82
|
+
headers: {
|
|
83
|
+
'api-key': string
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
54
87
|
type User = {
|
|
55
|
-
id
|
|
56
|
-
role
|
|
88
|
+
id: number
|
|
89
|
+
role: 'student' | 'teacher' | 'admin'
|
|
57
90
|
password?: string
|
|
58
91
|
firstName?: string
|
|
59
92
|
lastName?: string
|
|
@@ -68,4 +101,6 @@ type UserSession = {
|
|
|
68
101
|
|
|
69
102
|
interface Window {
|
|
70
103
|
google?: any
|
|
104
|
+
grecaptcha: any
|
|
105
|
+
bootstrap: Bootstrap
|
|
71
106
|
}
|
package/src/app.html
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
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
|
-
%
|
|
7
|
+
%sveltekit.head%
|
|
8
8
|
</head>
|
|
9
9
|
<body>
|
|
10
|
-
<div id="svelte">%
|
|
10
|
+
<div id="svelte">%sveltekit.body%</div>
|
|
11
11
|
</body>
|
|
12
|
-
</html>
|
|
12
|
+
</html>
|
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
package/tsconfig.json
CHANGED
|
@@ -1,32 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"extends": "./.svelte-kit/tsconfig.json",
|
|
3
3
|
"compilerOptions": {
|
|
4
|
-
"moduleResolution": "node",
|
|
5
|
-
"module": "es2020",
|
|
6
|
-
"lib": ["es2020", "DOM"],
|
|
7
|
-
"target": "es2019",
|
|
8
|
-
/**
|
|
9
|
-
svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript
|
|
10
|
-
to enforce using \`import type\` instead of \`import\` for Types.
|
|
11
|
-
*/
|
|
12
|
-
"importsNotUsedAsValues": "error",
|
|
13
|
-
"isolatedModules": true,
|
|
14
|
-
"resolveJsonModule": true,
|
|
15
|
-
/**
|
|
16
|
-
To have warnings/errors of the Svelte compiler at the correct position,
|
|
17
|
-
enable source maps by default.
|
|
18
|
-
*/
|
|
19
|
-
"sourceMap": true,
|
|
20
|
-
"esModuleInterop": true,
|
|
21
|
-
"skipLibCheck": true,
|
|
22
|
-
"forceConsistentCasingInFileNames": true,
|
|
23
|
-
"baseUrl": ".",
|
|
24
4
|
"allowJs": true,
|
|
25
5
|
"checkJs": true,
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"forceConsistentCasingInFileNames": true,
|
|
8
|
+
"resolveJsonModule": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"strict": true
|
|
12
|
+
}
|
|
32
13
|
}
|
package/vite.config.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { sveltekit } from '@sveltejs/kit/vite'
|
|
2
|
+
|
|
3
|
+
/** @type {import('vite').UserConfig} */
|
|
4
|
+
const config = {
|
|
5
|
+
plugins: [sveltekit()],
|
|
6
|
+
serviceWorker: {
|
|
7
|
+
files: (filepath) => !/\.DS_Store/.test(filepath)
|
|
8
|
+
},
|
|
9
|
+
server: {
|
|
10
|
+
host: 'localhost',
|
|
11
|
+
port: 3000,
|
|
12
|
+
open: 'http://localhost:3000'
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default config
|