sveltekit-auth-example 1.0.12 → 1.0.15

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.
Files changed (41) hide show
  1. package/.eslintrc.cjs +1 -1
  2. package/.prettierignore +13 -0
  3. package/.prettierrc +4 -1
  4. package/CHANGELOG.md +17 -0
  5. package/README.md +17 -20
  6. package/package.json +19 -18
  7. package/src/app.d.ts +5 -5
  8. package/src/app.html +1 -0
  9. package/src/hooks.ts +3 -13
  10. package/src/lib/auth.ts +49 -75
  11. package/src/routes/+error.svelte +6 -0
  12. package/src/routes/+layout.server.ts +8 -0
  13. package/src/routes/{__layout.svelte → +layout.svelte} +18 -14
  14. package/src/routes/{index.svelte → +page.svelte} +0 -0
  15. package/src/routes/admin/+page.server.ts +14 -0
  16. package/src/routes/admin/+page.svelte +12 -0
  17. package/src/routes/api/v1/user/+server.ts +20 -0
  18. package/src/routes/auth/[slug]/+server.ts +65 -0
  19. package/src/routes/auth/{forgot.ts → forgot/+server.ts} +3 -5
  20. package/src/routes/auth/{google.ts → google/+server.ts} +15 -20
  21. package/src/routes/auth/reset/{index.ts → +server.ts} +9 -12
  22. package/src/routes/auth/reset/{[token].svelte → [token]/+page.svelte} +4 -15
  23. package/src/routes/auth/reset/[token]/+page.ts +7 -0
  24. package/src/routes/{forgot.svelte → forgot/+page.svelte} +2 -2
  25. package/src/routes/{info.svelte → info/+page.svelte} +0 -0
  26. package/src/routes/{login.svelte → login/+page.svelte} +3 -3
  27. package/src/routes/profile/+page.server.ts +15 -0
  28. package/src/routes/{profile.svelte → profile/+page.svelte} +7 -25
  29. package/src/routes/register/+page.server.ts +10 -0
  30. package/src/routes/{register.svelte → register/+page.svelte} +6 -23
  31. package/src/routes/teachers/+page.server.ts +13 -0
  32. package/src/routes/teachers/+page.svelte +12 -0
  33. package/src/stores.ts +11 -1
  34. package/src/routes/__error.svelte +0 -19
  35. package/src/routes/admin.svelte +0 -40
  36. package/src/routes/api/v1/_auth.ts +0 -5
  37. package/src/routes/api/v1/admin.ts +0 -20
  38. package/src/routes/api/v1/teacher.ts +0 -20
  39. package/src/routes/api/v1/user.ts +0 -35
  40. package/src/routes/auth/[slug].ts +0 -93
  41. package/src/routes/teachers.svelte +0 -39
@@ -0,0 +1,65 @@
1
+ import { error, json, type RequestHandler } from '@sveltejs/kit'
2
+ import { query } from '../../_db'
3
+
4
+ export const POST: RequestHandler = async (event) => {
5
+ const { slug } = event.params
6
+
7
+ let result
8
+ let sql
9
+
10
+ try {
11
+ switch (slug) {
12
+ case 'logout':
13
+ if (event.locals.user) {
14
+ // if user is null, they are logged out anyway (session might have ended)
15
+ sql = `CALL delete_session($1);`
16
+ result = await query(sql, [event.locals.user.id])
17
+ }
18
+ return new Response(JSON.stringify({ message: 'Logout successful.' }), {
19
+ headers: {
20
+ 'Set-Cookie': `session=; Path=/; SameSite=Lax; HttpOnly; Expires=${new Date().toUTCString()}`
21
+ }
22
+ })
23
+ case 'login':
24
+ sql = `SELECT authenticate($1) AS "authenticationResult";`
25
+ break
26
+ case 'register':
27
+ sql = `SELECT register($1) AS "authenticationResult";`
28
+ break
29
+ default:
30
+ throw error(404, 'Invalid endpoint.')
31
+ }
32
+
33
+ // Only /auth/login and /auth/register at this point
34
+ const body = await event.request.json()
35
+
36
+ // While client checks for these to be non-null, register() in the database does not
37
+ if (slug == 'register' && (!body.email || !body.password || !body.firstName || !body.lastName))
38
+ throw error(400, 'Please supply all required fields: email, password, first and last name.')
39
+
40
+ result = await query(sql, [JSON.stringify(body)])
41
+ } catch (err) {
42
+ throw error(503, 'Could not communicate with database.')
43
+ }
44
+
45
+ const { authenticationResult }: { authenticationResult: AuthenticationResult } = result.rows[0]
46
+
47
+ if (!authenticationResult.user)
48
+ // includes when a user tries to register an existing email account with wrong password
49
+ throw error(authenticationResult.statusCode, authenticationResult.status)
50
+
51
+ // Ensures hooks.ts:handle() will not delete cookie just set
52
+ event.locals.user = authenticationResult.user
53
+
54
+ return json(
55
+ {
56
+ message: authenticationResult.status,
57
+ user: authenticationResult.user
58
+ },
59
+ {
60
+ headers: {
61
+ 'Set-Cookie': `session=${authenticationResult.sessionId}; Path=/; SameSite=Lax; HttpOnly;`
62
+ }
63
+ }
64
+ )
65
+ }
@@ -2,8 +2,8 @@ import type { RequestHandler } from '@sveltejs/kit'
2
2
  import type { Secret } from 'jsonwebtoken'
3
3
  import jwt from 'jsonwebtoken'
4
4
  import dotenv from 'dotenv'
5
- import { query } from '../_db'
6
- import { sendMessage } from '../_send-in-blue'
5
+ import { query } from '../../_db'
6
+ import { sendMessage } from '../../_send-in-blue'
7
7
 
8
8
  dotenv.config()
9
9
  const DOMAIN = process.env.DOMAIN
@@ -36,7 +36,5 @@ export const POST: RequestHandler = async event => {
36
36
  sendMessage(message)
37
37
  }
38
38
 
39
- return {
40
- status: 204
41
- }
39
+ return new Response(undefined, { status: 204 })
42
40
  }
@@ -1,6 +1,6 @@
1
- import type { RequestHandler } from '@sveltejs/kit'
1
+ import { error, type RequestHandler } from '@sveltejs/kit'
2
2
  import { OAuth2Client } from 'google-auth-library'
3
- import { query } from '../_db';
3
+ 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
@@ -13,7 +13,8 @@ async function getGoogleUserFromJWT(token: string): Promise<Partial<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
+ if (!payload) throw error(500, 'Google authentication did not get the expected payload')
17
+
17
18
  return {
18
19
  firstName: payload['given_name'] || 'UnknownFirstName',
19
20
  lastName: payload['family_name'] || 'UnknownLastName',
@@ -22,7 +23,7 @@ async function getGoogleUserFromJWT(token: string): Promise<Partial<User>> {
22
23
  } catch (err) {
23
24
  let message = ''
24
25
  if (err instanceof Error) message = err.message
25
- throw new Error(`Google user could not be authenticated: ${message}`)
26
+ throw error(500,`Google user could not be authenticated: ${message}`)
26
27
  }
27
28
  }
28
29
 
@@ -35,7 +36,7 @@ async function upsertGoogleUser(user: Partial<User>): Promise<UserSession> {
35
36
  } catch (err) {
36
37
  let message = ''
37
38
  if (err instanceof Error) message = err.message
38
- throw new Error(`GMail user could not be upserted: ${message}`)
39
+ throw error(500,`Gmail user could not be upserted: ${message}`)
39
40
  }
40
41
  }
41
42
 
@@ -49,24 +50,18 @@ export const POST: RequestHandler = async event => {
49
50
  // Prevent hooks.ts's handler() from deleting cookie thinking no one has authenticated
50
51
  event.locals.user = userSession.user
51
52
 
52
- return {
53
- status: 200,
54
- headers: { // database expires sessions in 2 hours
55
- 'Set-Cookie': `session=${userSession.id}; Path=/; SameSite=Lax; HttpOnly;`
56
- },
57
- body: {
58
- message: 'Successful Google Sign-In.',
59
- user: userSession.user
53
+ return new Response(JSON.stringify({
54
+ message: 'Successful Google Sign-In.',
55
+ user: userSession.user
56
+ }), {
57
+ headers: {
58
+ 'Set-Cookie': `session=${userSession.id}; Path=/; SameSite=Lax; HttpOnly;`}
60
59
  }
61
- }
60
+ )
61
+
62
62
  } catch (err) {
63
63
  let message = ''
64
64
  if (err instanceof Error) message = err.message
65
- return { // session cookie deleted by hooks.js handle()
66
- status: 401,
67
- body: {
68
- message: message
69
- }
70
- }
65
+ throw error(401, message)
71
66
  }
72
67
  }
@@ -1,3 +1,4 @@
1
+ import { json as json$1 } from '@sveltejs/kit';
1
2
  import dotenv from 'dotenv'
2
3
  import type { RequestHandler } from '@sveltejs/kit'
3
4
  import type { JwtPayload } from 'jsonwebtoken'
@@ -21,19 +22,15 @@ export const PUT: RequestHandler = async event => {
21
22
  const sql = `CALL reset_password($1, $2);`
22
23
  await query(sql, [userId, password])
23
24
 
24
- return {
25
- status: 200,
26
- body: {
27
- message: 'Password successfully reset.'
28
- }
29
- }
25
+ return json$1({
26
+ message: 'Password successfully reset.'
27
+ })
30
28
  } catch (error) {
31
29
  // Technically, I should check error.message to make sure it's not a DB issue
32
- return {
33
- status: 403,
34
- body: {
35
- message: 'Password reset token expired.'
36
- }
37
- }
30
+ return json$1({
31
+ message: 'Password reset token expired.'
32
+ }, {
33
+ status: 403
34
+ })
38
35
  }
39
36
  }
@@ -1,22 +1,11 @@
1
- <script context="module" lang="ts">
2
- import type { Load } from '@sveltejs/kit'
3
-
4
- export const load: Load = async event => {
5
- return {
6
- props: {
7
- token: event.params.token
8
- }
9
- }
10
- }
11
- </script>
12
-
13
1
  <script lang="ts">
2
+ import type { PageData } from './$types'
14
3
  import { onMount } from 'svelte'
15
4
  import { goto } from '$app/navigation'
16
- import { toast } from '../../../stores'
5
+ import { toast } from '../../../../stores'
17
6
  import { focusOnFirstError } from '$lib/focus'
18
7
 
19
- export let token: string
8
+ export let data: PageData
20
9
 
21
10
  let focusedField: HTMLInputElement
22
11
  let password: string
@@ -49,7 +38,7 @@
49
38
  'Content-Type': 'application/json'
50
39
  },
51
40
  body: JSON.stringify({
52
- token,
41
+ token: data.token,
53
42
  password
54
43
  })
55
44
  })
@@ -0,0 +1,7 @@
1
+ import type { PageLoad } from './$types'
2
+
3
+ export const load: PageLoad = async event => {
4
+ return {
5
+ token: event.params.token
6
+ }
7
+ }
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { onMount } from 'svelte'
3
3
  import { goto } from '$app/navigation'
4
- import { toast } from '../stores'
4
+ import { toast } from '../../stores'
5
5
  import { focusOnFirstError } from '$lib/focus'
6
6
 
7
7
  let focusedField: HTMLInputElement
@@ -18,7 +18,7 @@
18
18
 
19
19
  if (form.checkValidity()) {
20
20
  if (email.toLowerCase().includes('gmail.com')) {
21
- return message = 'GMail passwords must be reset on Manage Your Google Account.'
21
+ return message = 'Gmail passwords must be reset on Manage Your Google Account.'
22
22
  }
23
23
  const url = `/auth/forgot`
24
24
  const res = await fetch(url, {
File without changes
@@ -1,11 +1,12 @@
1
1
  <script lang="ts">
2
2
  import { onMount } from 'svelte'
3
3
  import { goto } from '$app/navigation'
4
- import { page, session } from '$app/stores'
4
+ import { page } from '$app/stores'
5
+ import { loginSession } from '../../stores'
5
6
  import useAuth from '$lib/auth'
6
7
  import { focusOnFirstError } from '$lib/focus'
7
8
 
8
- const { loadScript, initializeSignInWithGoogle, loginLocal } = useAuth(page, session, goto)
9
+ const { initializeSignInWithGoogle, loginLocal } = useAuth(page, loginSession, goto)
9
10
 
10
11
  let focusedField: HTMLInputElement
11
12
  let message: string
@@ -34,7 +35,6 @@
34
35
  }
35
36
 
36
37
  onMount(async() => {
37
- await loadScript() // probably cached
38
38
  initializeSignInWithGoogle('googleButton')
39
39
  focusedField.focus()
40
40
  })
@@ -0,0 +1,15 @@
1
+ import { redirect } from '@sveltejs/kit';
2
+ import type { PageServerLoad } from './$types'
3
+
4
+ export const load: PageServerLoad = async ({ locals }) => {
5
+ const { user } = locals // populated by /src/hooks.ts
6
+
7
+ const authorized = ['admin', 'teacher', 'student'] // must be logged-in
8
+ if (user && !authorized.includes(user.role)) {
9
+ throw redirect(302, '/login?referrer=/profile')
10
+ }
11
+
12
+ return {
13
+ user
14
+ }
15
+ }
@@ -1,33 +1,15 @@
1
- <script context="module" lang="ts">
2
- import type { Load } from '@sveltejs/kit'
3
-
4
- export const load: Load = ({ session }) => {
5
- const authorized = ['admin', 'teacher', 'student'] // must be logged-in
6
- if (session.user && !authorized.includes(session.user.role)) {
7
- return {
8
- status: 302,
9
- redirect: '/login?referrer=/profile'
10
- }
11
- }
12
-
13
- return {
14
- props: {
15
- // Clone session.user so unsaved changes are NOT retained
16
- user: JSON.parse(JSON.stringify(session.user))
17
- }
18
- }
19
- }
20
- </script>
21
-
22
1
  <script lang="ts">
2
+ import type { PageData } from './$types'
23
3
  import { onMount } from 'svelte'
24
- import { session } from '$app/stores'
25
- import { focusOnFirstError } from '$lib/focus';
4
+ import { focusOnFirstError } from '$lib/focus'
5
+ import { loginSession } from '../../stores'
6
+
7
+ export let data: PageData
8
+ const { user } : {user : User } = data
26
9
 
27
10
  let focusedField: HTMLInputElement
28
11
  let message: string
29
12
  let confirmPassword: HTMLInputElement
30
- export let user: User
31
13
 
32
14
  onMount(() => {
33
15
  focusedField.focus()
@@ -43,7 +25,6 @@
43
25
  }
44
26
 
45
27
  if (form.checkValidity()) {
46
- $session.user = JSON.parse(JSON.stringify(user)) // deep clone
47
28
  const url = '/api/v1/user'
48
29
  const res = await fetch(url, {
49
30
  method: 'PUT',
@@ -54,6 +35,7 @@
54
35
  })
55
36
  const reply = await res.json()
56
37
  message = reply.message
38
+ $loginSession = JSON.parse(JSON.stringify(user)) // update loginSession store
57
39
  } else {
58
40
  form.classList.add('was-validated')
59
41
  focusOnFirstError(form)
@@ -0,0 +1,10 @@
1
+ import { redirect } from '@sveltejs/kit';
2
+ import type { PageServerLoad } from './$types'
3
+
4
+ export const load: PageServerLoad = ({ locals }) => {
5
+ const { user } = locals
6
+ if (user) { // Redirect to home if user is logged in already
7
+ throw redirect(302, '/');
8
+ }
9
+ return {}
10
+ }
@@ -1,25 +1,12 @@
1
- <script context="module" lang="ts">
2
- import type { Load } from '@sveltejs/kit'
3
-
4
- export const load: Load = ({ session }) => {
5
- if (session.user) { // Do not display if user is logged in
6
- return {
7
- status: 302,
8
- redirect: '/'
9
- }
10
- }
11
- return {}
12
- }
13
- </script>
14
-
15
1
  <script lang="ts">
16
2
  import { onMount } from 'svelte'
17
3
  import { goto } from '$app/navigation'
18
- import { page, session } from '$app/stores'
4
+ import { page } from '$app/stores'
5
+ import { loginSession } from '../../stores'
19
6
  import useAuth from '$lib/auth'
20
7
  import { focusOnFirstError } from '$lib/focus'
21
8
 
22
- const { initializeSignInWithGoogle, registerLocal } = useAuth(page, session, goto)
9
+ const { initializeSignInWithGoogle, registerLocal } = useAuth(page, loginSession, goto)
23
10
 
24
11
  let focusedField: HTMLInputElement
25
12
 
@@ -50,7 +37,7 @@
50
37
  } catch (err) {
51
38
  if (err instanceof Error) {
52
39
  message = err.message
53
- console.error('Login error', message)
40
+ console.log('Login error', message)
54
41
  }
55
42
  }
56
43
  } else {
@@ -62,11 +49,7 @@
62
49
 
63
50
  onMount(() => {
64
51
  focusedField.focus()
65
- initializeSignInWithGoogle()
66
- window.google.accounts.id.renderButton(
67
- document.getElementById('googleButton'),
68
- { theme: 'filled_blue', size: 'large', width: '367' } // customization attributes
69
- )
52
+ initializeSignInWithGoogle('googleButton')
70
53
  })
71
54
 
72
55
 
@@ -121,7 +104,7 @@
121
104
  </div>
122
105
 
123
106
  {#if message}
124
- <p>{message}</p>
107
+ <p class="text-danger">{message}</p>
125
108
  {/if}
126
109
 
127
110
  <button type="button" on:click={register} class="btn btn-primary btn-lg">Register</button>
@@ -0,0 +1,13 @@
1
+ import { redirect } from '@sveltejs/kit'
2
+ import type { PageServerLoad } from './$types'
3
+
4
+ export const load: PageServerLoad = async ({locals}) => {
5
+ const authorized = ['admin', 'teacher']
6
+ if (!locals.user || !authorized.includes(locals.user.role)) {
7
+ throw redirect(302, '/login?referrer=/teachers')
8
+ }
9
+
10
+ return {
11
+ message: 'Teachers or Admin-only content from endpoint.'
12
+ }
13
+ }
@@ -0,0 +1,12 @@
1
+ <script lang="ts">
2
+ import type { PageData } from './$types'
3
+ export let data: PageData
4
+ </script>
5
+
6
+ <svelte:head>
7
+ <title>Teachers</title>
8
+ </svelte:head>
9
+
10
+ <h1>Teachers</h1>
11
+ <h4>Teacher Or Admin Role</h4>
12
+ <p>{data.message}</p>
package/src/stores.ts CHANGED
@@ -4,4 +4,14 @@ export const toast = writable({
4
4
  title: '',
5
5
  body: '',
6
6
  isOpen: false
7
- })
7
+ })
8
+
9
+ // While server determines whether the user is logged in by examining RequestEvent.locals.user, the
10
+ // loginSession is updated so all parts of the SPA client-side see the user and role.
11
+
12
+ export const defaultUser: User = {
13
+ id: 0, // the not-logged-in user id
14
+ role: 'student' // default role for users who are not logged in
15
+ }
16
+
17
+ export const loginSession = writable(defaultUser)
@@ -1,19 +0,0 @@
1
- <script lang="ts" context="module">
2
- import type { Load } from '@sveltejs/kit'
3
-
4
- export const load: Load = ({ error, status }) => {
5
- const { message } = <Error> error
6
- return {
7
- props: {
8
- errorMessage: `${status}: ${message}`
9
- }
10
- }
11
- }
12
- </script>
13
-
14
- <script lang="ts">
15
- export let errorMessage = ''
16
- </script>
17
-
18
- <h1>Error</h1>
19
- <h4>{errorMessage}</h4>
@@ -1,40 +0,0 @@
1
- <script context="module" lang="ts">
2
- import type { Load } from '@sveltejs/kit'
3
-
4
- export const load: Load = async ({ fetch, session }) => {
5
- const authorized = ['admin']
6
- if (session.user && !authorized.includes(session.user.role)) {
7
- return {
8
- status: 302,
9
- redirect: '/login?referrer=/admin'
10
- }
11
- }
12
-
13
- const url = '/api/v1/admin'
14
- const res = await fetch(url, {
15
- method: 'GET',
16
- headers: { 'Content-Type': 'application/json' }
17
- })
18
-
19
- // if !res.ok, error is returned as message
20
- const { message } = await res.json()
21
- return {
22
- props: {
23
- message
24
- }
25
- }
26
-
27
- }
28
- </script>
29
-
30
- <script lang="ts">
31
- export let message = ''
32
- </script>
33
-
34
- <svelte:head>
35
- <title>Administration</title>
36
- </svelte:head>
37
-
38
- <h1>Admin</h1>
39
- <h4>Admin Role</h4>
40
- <p>{message}</p>
@@ -1,5 +0,0 @@
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,20 +0,0 @@
1
- import type { RequestHandler } from '@sveltejs/kit'
2
- import { requestAuthorized } from './_auth'
3
-
4
- export const GET: RequestHandler = async event => {
5
- if (!requestAuthorized(event, ['admin'])) {
6
- return {
7
- status: 401,
8
- body: {
9
- message: 'Unauthorized - admin role required.'
10
- }
11
- }
12
- }
13
-
14
- return {
15
- status: 200,
16
- body: {
17
- message: 'Admin-only content from endpoint.'
18
- }
19
- }
20
- }
@@ -1,20 +0,0 @@
1
- import type { RequestHandler } from '@sveltejs/kit'
2
- import { requestAuthorized } from './_auth'
3
-
4
- export const GET: RequestHandler = async event=> {
5
- if (!requestAuthorized(event, ['admin', 'teacher'])) {
6
- return {
7
- status: 401,
8
- body: {
9
- message: 'Unauthorized - admin or teacher role required.'
10
- }
11
- }
12
- }
13
-
14
- return {
15
- status: 200,
16
- body: {
17
- message: 'Teachers or Admin-only content from endpoint.'
18
- }
19
- }
20
- }
@@ -1,35 +0,0 @@
1
- import type { RequestHandler } from '@sveltejs/kit'
2
- import { query } from '../../_db'
3
- import { requestAuthorized } from './_auth'
4
-
5
- export const PUT: RequestHandler = async event => {
6
- if (!requestAuthorized(event, ['admin', 'teacher', 'student'])) {
7
- return {
8
- status: 401,
9
- body: {
10
- message: 'Unauthorized - must be logged-in.'
11
- }
12
- }
13
- }
14
-
15
- const sql = `CALL update_user($1, $2);`
16
- try {
17
- // Only permit update of the authenticated user
18
- const body = await event.request.json()
19
- await query(sql, [event.locals.user.id, JSON.stringify(body)])
20
- } catch (error) {
21
- return {
22
- status: 503,
23
- body: {
24
- message: 'Could not communicate with database.'
25
- }
26
- }
27
- }
28
-
29
- return {
30
- status: 200,
31
- body: {
32
- message: 'Successfully updated user profile.'
33
- }
34
- }
35
- }