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 CHANGED
@@ -10,7 +10,7 @@ module.exports = {
10
10
  },
11
11
  parserOptions: {
12
12
  sourceType: 'module',
13
- ecmaVersion: 2019
13
+ ecmaVersion: 2020
14
14
  },
15
15
  env: {
16
16
  browser: true,
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 13 or higher
34
- - Node.js 16.13.0 or higher
35
- - npm 8.1.0 or higher
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. I am still learning Svelte and SvelteKit. All feedback is appreciated.
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.7",
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
- "dev": "svelte-kit dev",
25
+ "start": "node build",
26
+ "dev": "vite dev",
26
27
  "serve": "npm run dev -- --open",
27
- "build": "svelte-kit build",
28
- "preview": "svelte-kit 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": "~16.15.0",
36
- "npm": "^8.8.0"
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.0",
42
- "google-auth-library": "^8.0.2",
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.22.0",
53
- "@typescript-eslint/parser": "^5.22.0",
54
- "bootstrap": "^5.1.3",
55
- "bootstrap-icons": "^1.8.1",
56
- "eslint": "^8.14.0",
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": "^3.4.1",
59
- "prettier": "^2.6.2",
61
+ "eslint-plugin-svelte3": "^4.0.0",
62
+ "prettier": "^2.7.1",
60
63
  "prettier-plugin-svelte": "^2.7.0",
61
- "sass": "^1.51.0",
62
- "svelte": "^3.48.0",
63
- "svelte-check": "^2.7.0",
64
- "svelte-preprocess": "^4.10.6",
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.6.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: MessageAddressee[]
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?: number
56
- role?: 'student' | 'teacher' | 'admin'
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
- %svelte.head%
7
+ %sveltekit.head%
8
8
  </head>
9
9
  <body>
10
- <div id="svelte">%svelte.body%</div>
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['Set-Cookie'] = `session=; Path=/; HttpOnly; SameSite=Lax; Expires=${new Date().toUTCString()}`
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
- console.error('Login error', err)
109
- throw new Error(err.message)
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
- console.error('Login error', err)
135
- throw new Error(err.message)
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
- type FocusResult = (formName: HTMLFormElement) => void
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
- export const load = ({ error, status }) => {
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}: ${error.message}`
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 sessionValue
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 pgNativePool = new pg.native.Pool({
8
+ const pool = new pg.Pool({
9
9
  max: 10, // default
10
- connectionString: process.env['DATABASE_URL'],
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?) => pgNativePool.query(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['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'])
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 = SEND_IN_BLUE_FROM
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 })
@@ -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?.role)) {
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>
@@ -0,0 +1,5 @@
1
+ import type { RequestEvent } from "@sveltejs/kit"
2
+
3
+ export function requestAuthorized(event: RequestEvent, roles: string[]): boolean {
4
+ return !!event.locals.user && roles.includes(event.locals.user.role)
5
+ }
@@ -1,9 +1,8 @@
1
1
  import type { RequestHandler } from '@sveltejs/kit'
2
+ import { requestAuthorized } from './_auth'
2
3
 
3
- export const get: RequestHandler = async event => {
4
- const authorized = ['admin']
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 get: RequestHandler = async event=> {
4
- const authorized = ['admin', 'teacher']
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 put: RequestHandler = async event => {
5
- const authorized = ['admin', 'teacher', 'student']
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: {
@@ -1,7 +1,7 @@
1
1
  import type { RequestHandler } from '@sveltejs/kit'
2
2
  import { query } from '../_db'
3
3
 
4
- export const post: RequestHandler = async event => {
4
+ export const POST: RequestHandler = async event => {
5
5
  const { slug } = event.params
6
6
 
7
7
  let result
@@ -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['DOMAIN']
10
- const JWT_SECRET: Secret = process.env['JWT_SECRET']
9
+ const DOMAIN = process.env.DOMAIN
10
+ const JWT_SECRET = process.env.JWT_SECRET
11
11
 
12
- export const post: RequestHandler = async event => {
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 (error) {
22
- throw new Error(`Google user could not be authenticated: ${error.message}`)
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 (error) {
33
- throw new Error(`GMail user could not be upserted: ${error.message}`)
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 post: RequestHandler = async event => {
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 (error) {
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: error.message
68
+ message: message
62
69
  }
63
70
  }
64
71
  }
@@ -34,7 +34,7 @@
34
34
 
35
35
  const resetPassword = async () => {
36
36
  message = ''
37
- const form = document.forms['reset']
37
+ const form = <HTMLFormElement> document.getElementById('reset')
38
38
 
39
39
  if (!passwordMatch()) {
40
40
  confirmPassword.classList.add('is-invalid')
@@ -6,9 +6,9 @@ import { query } from '../../_db'
6
6
 
7
7
  dotenv.config()
8
8
 
9
- const JWT_SECRET = process.env['JWT_SECRET']
9
+ const JWT_SECRET = <jwt.Secret> process.env.JWT_SECRET
10
10
 
11
- export const put: RequestHandler = async event => {
11
+ export const PUT: RequestHandler = async event => {
12
12
  const body = await event.request.json()
13
13
  const { token, password } = body
14
14
 
@@ -14,7 +14,7 @@
14
14
 
15
15
  const sendPasswordReset = async () => {
16
16
  message = ''
17
- const form = document.forms['forgot']
17
+ const form = <HTMLFormElement> document.getElementById('forgot')
18
18
 
19
19
  if (form.checkValidity()) {
20
20
  if (email.toLowerCase().includes('gmail.com')) {
@@ -16,14 +16,16 @@
16
16
 
17
17
  async function login() {
18
18
  message = ''
19
- const form = document.forms['signIn']
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
- console.error('Login error', err.message)
26
- message = err.message
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?.role)) {
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.forms['profile']
38
+ const form = <HTMLFormElement> document.getElementById('profile')
34
39
 
35
- if (!user.email.includes('gmail.com') && !passwordMatch()) {
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.email.includes('gmail.com')}
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.forms['register']
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
- console.error('Login error', err.message)
50
- message = err.message
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?.role)) {
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
@@ -33,11 +33,6 @@ const config = {
33
33
  'base-uri': ['self'],
34
34
  // 'require-trusted-types-for': ["'script'"] // will require effort to get this working
35
35
  }
36
- },
37
- vite: {
38
- serviceWorker: {
39
- files: (filepath) => !/\.DS_Store/.test(filepath)
40
- }
41
36
  }
42
37
  }
43
38
  }
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
- "paths": {
27
- "$lib": ["src/lib"],
28
- "$lib/*": ["src/lib/*"]
29
- }
30
- },
31
- "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.svelte"]
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