sveltekit-auth-example 2.0.1 → 2.0.2
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 +19 -7
- package/.prettierignore +1 -0
- package/.yarn/install-state.gz +0 -0
- package/CHANGELOG.md +162 -100
- package/README.md +6 -1
- package/package.json +67 -67
- package/prettier.config.mjs +7 -6
- package/src/app.d.ts +31 -29
- package/src/app.html +8 -3
- package/src/hooks.server.ts +15 -15
- package/src/lib/focus.ts +6 -6
- package/src/lib/google.ts +48 -49
- package/src/lib/server/db.ts +7 -6
- package/src/lib/server/sendgrid.ts +11 -11
- package/src/routes/+error.svelte +1 -1
- package/src/routes/+layout.server.ts +5 -5
- package/src/routes/+layout.svelte +133 -100
- package/src/routes/admin/+page.server.ts +1 -1
- package/src/routes/admin/+page.svelte +2 -2
- package/src/routes/api/v1/user/+server.ts +13 -14
- package/src/routes/auth/[slug]/+server.ts +11 -6
- package/src/routes/auth/forgot/+server.ts +23 -23
- package/src/routes/auth/google/+server.ts +44 -45
- package/src/routes/auth/reset/+server.ts +1 -1
- package/src/routes/auth/reset/[token]/+page.svelte +117 -95
- package/src/routes/auth/reset/[token]/+page.ts +4 -4
- package/src/routes/forgot/+page.svelte +74 -63
- package/src/routes/info/+page.svelte +1 -1
- package/src/routes/login/+page.svelte +140 -120
- package/src/routes/profile/+page.server.ts +9 -9
- package/src/routes/profile/+page.svelte +142 -88
- package/src/routes/register/+page.server.ts +3 -2
- package/src/routes/register/+page.svelte +159 -104
- package/src/routes/teachers/+page.server.ts +5 -5
- package/src/routes/teachers/+page.svelte +2 -2
- package/src/stores.ts +1 -1
- package/svelte.config.js +1 -1
package/src/app.d.ts
CHANGED
|
@@ -5,50 +5,52 @@
|
|
|
5
5
|
// and what to do when importing types
|
|
6
6
|
declare namespace App {
|
|
7
7
|
interface Locals {
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
user: User
|
|
9
|
+
}
|
|
10
10
|
|
|
11
11
|
// interface Platform {}
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
13
|
+
interface PrivateEnv {
|
|
14
|
+
// $env/static/private
|
|
15
|
+
DATABASE_URL: string
|
|
16
|
+
DOMAIN: string
|
|
17
|
+
JWT_SECRET: string
|
|
18
|
+
SENDGRID_KEY: string
|
|
19
|
+
SENDGRID_SENDER: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface PublicEnv {
|
|
23
|
+
// $env/static/public
|
|
24
|
+
PUBLIC_GOOGLE_CLIENT_ID: string
|
|
25
|
+
}
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
interface AuthenticationResult {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
statusCode: NumericRange<400, 599>
|
|
30
|
+
status: string
|
|
31
|
+
user: User
|
|
32
|
+
sessionId: string
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
interface Credentials {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
email: string
|
|
37
|
+
password: string
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
interface UserProperties {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
id: number
|
|
42
|
+
expires?: string // ISO-8601 datetime
|
|
43
|
+
role: 'student' | 'teacher' | 'admin'
|
|
44
|
+
password?: string
|
|
45
|
+
firstName?: string
|
|
46
|
+
lastName?: string
|
|
47
|
+
email?: string
|
|
48
|
+
phone?: string
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
type User = UserProperties | undefined | null
|
|
50
52
|
|
|
51
53
|
interface UserSession {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
id: string
|
|
55
|
+
user: User
|
|
54
56
|
}
|
package/src/app.html
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
<!
|
|
1
|
+
<!doctype html>
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" sizes="any" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
-
<script
|
|
7
|
+
<script
|
|
8
|
+
nonce="%sveltekit.nonce%"
|
|
9
|
+
src="https://accounts.google.com/gsi/client"
|
|
10
|
+
async
|
|
11
|
+
defer
|
|
12
|
+
></script>
|
|
8
13
|
%sveltekit.head%
|
|
9
14
|
</head>
|
|
10
15
|
<body>
|
|
11
16
|
<div id="svelte">%sveltekit.body%</div>
|
|
12
17
|
</body>
|
|
13
|
-
</html>
|
|
18
|
+
</html>
|
package/src/hooks.server.ts
CHANGED
|
@@ -3,28 +3,28 @@ import { query } from '$lib/server/db'
|
|
|
3
3
|
|
|
4
4
|
// Attach authorization to each server request (role may have changed)
|
|
5
5
|
async function attachUserToRequestEvent(sessionId: string, event: RequestEvent) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
const sql = `SELECT * FROM get_session($1);`
|
|
7
|
+
const { rows } = await query(sql, [sessionId])
|
|
8
|
+
if (rows?.length > 0) {
|
|
9
|
+
event.locals.user = <User>rows[0].get_session
|
|
10
|
+
}
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
// Invoked for each endpoint called and initially for SSR router
|
|
14
14
|
export const handle: Handle = async ({ event, resolve }) => {
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
const { cookies } = event
|
|
16
|
+
const sessionId = cookies.get('session')
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
// before endpoint or page is called
|
|
19
|
+
if (sessionId) {
|
|
20
|
+
await attachUserToRequestEvent(sessionId, event)
|
|
21
|
+
}
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
if (!event.locals.user) cookies.delete('session', { path: '/' })
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
const response = await resolve(event)
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
// after endpoint or page is called
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
return response
|
|
30
30
|
}
|
package/src/lib/focus.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export const focusOnFirstError = (form: HTMLFormElement) => {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
for (const field of form.elements) {
|
|
3
|
+
if (field instanceof HTMLInputElement && !field.checkValidity()) {
|
|
4
|
+
field.focus()
|
|
5
|
+
break
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
8
|
}
|
package/src/lib/google.ts
CHANGED
|
@@ -4,58 +4,57 @@ import { PUBLIC_GOOGLE_CLIENT_ID } from '$env/static/public'
|
|
|
4
4
|
import { googleInitialized, loginSession } from '../stores'
|
|
5
5
|
|
|
6
6
|
export function renderGoogleButton() {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
const btn = document.getElementById('googleButton')
|
|
8
|
+
if (btn) {
|
|
9
|
+
google.accounts.id.renderButton(btn, {
|
|
10
|
+
type: 'standard',
|
|
11
|
+
theme: 'filled_blue',
|
|
12
|
+
size: 'large',
|
|
13
|
+
width: 367
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function initializeGoogleAccounts() {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
initialized = value
|
|
19
|
+
let initialized = false
|
|
20
|
+
const unsubscribe = googleInitialized.subscribe((value) => {
|
|
21
|
+
initialized = value
|
|
22
22
|
})
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
24
|
+
if (!initialized) {
|
|
25
|
+
google.accounts.id.initialize({
|
|
26
|
+
client_id: PUBLIC_GOOGLE_CLIENT_ID,
|
|
27
|
+
callback: googleCallback
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
googleInitialized.set(true)
|
|
31
|
+
}
|
|
32
|
+
unsubscribe()
|
|
33
|
+
|
|
34
|
+
async function googleCallback(response: google.accounts.id.CredentialResponse) {
|
|
35
|
+
const res = await fetch('/auth/google', {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: {
|
|
38
|
+
'Content-Type': 'application/json'
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify({ token: response.credential })
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
if (res.ok) {
|
|
44
|
+
const fromEndpoint = await res.json()
|
|
45
|
+
loginSession.set(fromEndpoint.user) // update loginSession store
|
|
46
|
+
const { role } = fromEndpoint.user
|
|
47
|
+
|
|
48
|
+
let referrer
|
|
49
|
+
const unsubscribe = page.subscribe((p) => {
|
|
50
|
+
referrer = p.url.searchParams.get('referrer')
|
|
51
|
+
})
|
|
52
|
+
unsubscribe()
|
|
53
|
+
|
|
54
|
+
if (referrer) return goto(referrer)
|
|
55
|
+
if (role === 'teacher') return goto('/teachers')
|
|
56
|
+
if (role === 'admin') return goto('/admin')
|
|
57
|
+
if (location.pathname === '/login') goto('/') // logged in so go home
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
60
|
}
|
|
61
|
-
|
package/src/lib/server/db.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import type { QueryResult} from 'pg'
|
|
2
|
+
import type { QueryResult } from 'pg'
|
|
3
3
|
import pg from 'pg'
|
|
4
4
|
import { DATABASE_URL } from '$env/static/private'
|
|
5
5
|
|
|
6
6
|
const pool = new pg.Pool({
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
max: 10, // default
|
|
8
|
+
connectionString: DATABASE_URL,
|
|
9
|
+
ssl: {
|
|
10
|
+
// If your postgresql.conf does not have `ssl = on`, remove the entire ssl property or you will get an error
|
|
11
|
+
rejectUnauthorized: false
|
|
12
|
+
}
|
|
12
13
|
})
|
|
13
14
|
|
|
14
15
|
type PostgresQueryResult = (sql: string, params?: any[]) => Promise<QueryResult<any>>
|
|
@@ -3,15 +3,15 @@ import sgMail from '@sendgrid/mail'
|
|
|
3
3
|
import { env } from '$env/dynamic/private'
|
|
4
4
|
|
|
5
5
|
export const sendMessage = async (message: Partial<MailDataRequired>) => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
6
|
+
const { SENDGRID_SENDER, SENDGRID_KEY } = env
|
|
7
|
+
try {
|
|
8
|
+
sgMail.setApiKey(SENDGRID_KEY)
|
|
9
|
+
const completeMessage = <MailDataRequired>{
|
|
10
|
+
from: SENDGRID_SENDER, // default sender can be altered
|
|
11
|
+
...message
|
|
12
|
+
}
|
|
13
|
+
await sgMail.send(completeMessage)
|
|
14
|
+
} catch (errSendingMail) {
|
|
15
|
+
console.error(errSendingMail)
|
|
16
|
+
}
|
|
17
17
|
}
|
package/src/routes/+error.svelte
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { LayoutServerLoad } from './$types'
|
|
2
2
|
|
|
3
3
|
export const load: LayoutServerLoad = ({ locals }) => {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
4
|
+
const { user } = locals // locals.user set by hooks.server.ts/handle(), undefined if not logged in
|
|
5
|
+
return {
|
|
6
|
+
user
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
import { onMount } from 'svelte'
|
|
3
|
+
import type { LayoutServerData } from './$types'
|
|
4
|
+
import { goto, beforeNavigate } from '$app/navigation'
|
|
5
|
+
import { loginSession, toast } from '../stores'
|
|
6
|
+
import { initializeGoogleAccounts } from '$lib/google'
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
import 'bootstrap/scss/bootstrap.scss' // preferred way to load Bootstrap SCSS for hot module reloading
|
|
9
9
|
|
|
10
10
|
export let data: LayoutServerData
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
// If returning from different website, runs once (as it's an SPA) to restore user session if session cookie is still valid
|
|
13
|
+
const { user } = data
|
|
14
|
+
$loginSession = user
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
let Toast: any
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
beforeNavigate(() => {
|
|
19
19
|
let expirationDate = $loginSession?.expires ? new Date($loginSession.expires) : undefined
|
|
20
20
|
|
|
21
21
|
if (expirationDate && expirationDate < new Date()) {
|
|
@@ -24,17 +24,17 @@
|
|
|
24
24
|
}
|
|
25
25
|
})
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
onMount(async () => {
|
|
28
|
+
initializeGoogleAccounts()
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
await import('bootstrap/js/dist/collapse') // lots of ways to load Bootstrap but prefer this approach to avoid SSR issues
|
|
31
|
+
await import('bootstrap/js/dist/dropdown')
|
|
32
|
+
Toast = (await import('bootstrap/js/dist/toast')).default
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
if (!$loginSession) google.accounts.id.prompt()
|
|
35
35
|
})
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
async function logout() {
|
|
38
38
|
// Request server delete httpOnly cookie called loginSession
|
|
39
39
|
const url = '/auth/logout'
|
|
40
40
|
const res = await fetch(url, {
|
|
@@ -46,95 +46,128 @@
|
|
|
46
46
|
} else console.error(`Logout not successful: ${res.statusText} (${res.status})`)
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
const openToast = (open: boolean) => {
|
|
50
|
+
if (open) {
|
|
51
|
+
const toastDiv = <HTMLDivElement>document.getElementById('authToast')
|
|
52
|
+
const t = new Toast(toastDiv)
|
|
53
|
+
t.show()
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
$: openToast($toast.isOpen)
|
|
58
58
|
</script>
|
|
59
59
|
|
|
60
60
|
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
61
|
+
<div class="container">
|
|
62
|
+
<a class="navbar-brand" href="/">SvelteKit-Auth-Example</a>
|
|
63
|
+
<button
|
|
64
|
+
class="navbar-toggler"
|
|
65
|
+
type="button"
|
|
66
|
+
data-bs-toggle="collapse"
|
|
67
|
+
data-bs-target="#navbarMain"
|
|
68
|
+
aria-controls="navbarMain"
|
|
69
|
+
aria-expanded="false"
|
|
70
|
+
aria-label="Toggle navigation"
|
|
71
|
+
>
|
|
72
|
+
<span class="navbar-toggler-icon"></span>
|
|
73
|
+
</button>
|
|
74
|
+
<div class="collapse navbar-collapse" id="navbarMain">
|
|
75
|
+
<ul class="navbar-nav me-5">
|
|
76
|
+
<li class="nav-item"><a class="nav-link active" aria-current="page" href="/">Home</a></li>
|
|
77
|
+
<li class="nav-item"><a class="nav-link" href="/info">Info</a></li>
|
|
78
|
+
|
|
79
|
+
{#if $loginSession}
|
|
80
|
+
{#if $loginSession.role == 'admin'}
|
|
81
|
+
<li class="nav-item"><a class="nav-link" href="/admin">Admin</a></li>
|
|
82
|
+
{/if}
|
|
83
|
+
{#if $loginSession.role != 'student'}
|
|
84
|
+
<li class="nav-item"><a class="nav-link" href="/teachers">Teachers</a></li>
|
|
85
|
+
{/if}
|
|
86
|
+
{/if}
|
|
87
|
+
</ul>
|
|
88
|
+
<ul class="navbar-nav">
|
|
89
|
+
{#if $loginSession}
|
|
90
|
+
<li class="nav-item dropdown">
|
|
91
|
+
<a
|
|
92
|
+
class="nav-link dropdown-toggle"
|
|
93
|
+
href={'#'}
|
|
94
|
+
role="button"
|
|
95
|
+
data-bs-toggle="dropdown"
|
|
96
|
+
aria-expanded="false"
|
|
97
|
+
>
|
|
98
|
+
<svg
|
|
99
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
100
|
+
width="16"
|
|
101
|
+
height="16"
|
|
102
|
+
fill="currentColor"
|
|
103
|
+
class="avatar"
|
|
104
|
+
viewBox="0 0 16 16"
|
|
105
|
+
>
|
|
106
|
+
<path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0z" />
|
|
107
|
+
<path
|
|
108
|
+
fill-rule="evenodd"
|
|
109
|
+
d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1z"
|
|
110
|
+
/>
|
|
111
|
+
</svg>
|
|
112
|
+
{$loginSession.firstName}
|
|
113
|
+
</a>
|
|
114
|
+
<ul class="dropdown-menu">
|
|
115
|
+
<li>
|
|
116
|
+
<a class="dropdown-item" href="/profile">Profile</a>
|
|
117
|
+
</li>
|
|
118
|
+
<li>
|
|
119
|
+
<a
|
|
120
|
+
on:click|preventDefault={logout}
|
|
121
|
+
class="dropdown-item"
|
|
122
|
+
class:d-none={!$loginSession || $loginSession.id === 0}
|
|
123
|
+
href={'#'}>Logout</a
|
|
124
|
+
>
|
|
125
|
+
</li>
|
|
126
|
+
</ul>
|
|
127
|
+
</li>
|
|
128
|
+
{:else}
|
|
129
|
+
<li class="nav-item">
|
|
130
|
+
<a class="nav-link" href="/login">Login</a>
|
|
131
|
+
</li>
|
|
132
|
+
{/if}
|
|
133
|
+
</ul>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
108
136
|
</nav>
|
|
109
137
|
|
|
110
138
|
<main class="container">
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
139
|
+
<slot />
|
|
140
|
+
|
|
141
|
+
<div
|
|
142
|
+
id="authToast"
|
|
143
|
+
class="toast position-fixed top-0 end-0 m-3"
|
|
144
|
+
role="alert"
|
|
145
|
+
aria-live="assertive"
|
|
146
|
+
aria-atomic="true"
|
|
147
|
+
>
|
|
148
|
+
<div class="toast-header bg-primary text-white">
|
|
149
|
+
<strong class="me-auto">{$toast.title}</strong>
|
|
150
|
+
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
|
151
|
+
</div>
|
|
152
|
+
<div class="toast-body">
|
|
153
|
+
{$toast.body}
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
123
156
|
</main>
|
|
124
157
|
|
|
125
158
|
<style lang="scss" global>
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
</style>
|
|
159
|
+
// Make Retina displays crisper
|
|
160
|
+
* {
|
|
161
|
+
-webkit-font-smoothing: antialiased;
|
|
162
|
+
-moz-osx-font-smoothing: grayscale;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.toast {
|
|
166
|
+
z-index: 9999;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.avatar {
|
|
170
|
+
position: relative;
|
|
171
|
+
top: -1.5px;
|
|
172
|
+
}
|
|
173
|
+
</style>
|
|
@@ -5,7 +5,7 @@ export const load: PageServerLoad = async ({ locals }) => {
|
|
|
5
5
|
const { user } = locals
|
|
6
6
|
const authorized = ['admin']
|
|
7
7
|
if (!user || !authorized.includes(user.role)) {
|
|
8
|
-
redirect(302, '/login?referrer=/admin')
|
|
8
|
+
redirect(302, '/login?referrer=/admin')
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
return { message: 'Admin-only content from server.' }
|