sveltekit-auth-example 5.0.3 → 5.1.0

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.
@@ -5,7 +5,7 @@
5
5
  import { loginSession, toast } from '../stores'
6
6
  import { initializeGoogleAccounts } from '$lib/google'
7
7
 
8
- import 'bootstrap/scss/bootstrap.scss' // preferred way to load Bootstrap SCSS for hot module reloading
8
+ import './layout.css'
9
9
 
10
10
  interface Props {
11
11
  data: LayoutServerData
@@ -14,167 +14,118 @@
14
14
 
15
15
  let { data, children }: Props = $props()
16
16
 
17
- // If returning from different website, runs once (as it's an SPA) to restore user session if session cookie is still valid
18
- const { user } = data
19
- $loginSession = user
17
+ $effect(() => {
18
+ $loginSession = data.user
19
+ })
20
20
 
21
- let Toast: any
21
+ let navOpen = $state(false)
22
+ let dropdownOpen = $state(false)
22
23
 
23
24
  beforeNavigate(() => {
24
- let expirationDate = $loginSession?.expires ? new Date($loginSession.expires) : undefined
25
-
25
+ navOpen = false
26
+ dropdownOpen = false
27
+ const expirationDate = $loginSession?.expires ? new Date($loginSession.expires) : undefined
26
28
  if (expirationDate && expirationDate < new Date()) {
27
29
  console.log('Login session expired.')
28
30
  $loginSession = null
29
31
  }
30
32
  })
31
33
 
32
- onMount(async () => {
34
+ onMount(() => {
33
35
  initializeGoogleAccounts()
34
-
35
- await import('bootstrap/js/dist/collapse') // lots of ways to load Bootstrap but prefer this approach to avoid SSR issues
36
- await import('bootstrap/js/dist/dropdown')
37
- Toast = (await import('bootstrap/js/dist/toast')).default
38
-
39
36
  if (!$loginSession) google.accounts.id.prompt()
40
37
  })
41
38
 
42
39
  async function logout(event: MouseEvent) {
43
40
  event.preventDefault()
44
- // Request server delete httpOnly cookie called loginSession
45
- const url = '/auth/logout'
46
- const res = await fetch(url, {
47
- method: 'POST'
48
- })
41
+ const res = await fetch('/auth/logout', { method: 'POST' })
49
42
  if (res.ok) {
50
- loginSession.set(undefined) // delete loginSession.user from
43
+ loginSession.set(undefined)
51
44
  goto('/login')
52
45
  } else console.error(`Logout not successful: ${res.statusText} (${res.status})`)
53
46
  }
54
-
55
- const openToast = (open: boolean) => {
56
- if (open) {
57
- const toastDiv = document.getElementById('authToast') as HTMLDivElement
58
- const t = new Toast(toastDiv)
59
- t.show()
60
- }
61
- }
62
-
63
- $effect(() => {
64
- openToast($toast.isOpen)
65
- })
66
47
  </script>
67
48
 
68
- <nav class="navbar navbar-expand-lg navbar-light bg-light">
69
- <div class="container">
70
- <a class="navbar-brand" href="/">SvelteKit-Auth-Example</a>
49
+ <nav class="tw:bg-gray-100 tw:border-b tw:border-gray-200">
50
+ <div class="tw:mx-auto tw:max-w-5xl tw:px-4 tw:flex tw:items-center tw:justify-between tw:h-14">
51
+ <a class="tw:font-semibold tw:text-gray-800 tw:no-underline" href="/">SvelteKit-Auth-Example</a>
52
+
53
+ <!-- Mobile toggle -->
71
54
  <button
72
- class="navbar-toggler"
73
- type="button"
74
- data-bs-toggle="collapse"
75
- data-bs-target="#navbarMain"
76
- aria-controls="navbarMain"
77
- aria-expanded="false"
55
+ class="tw:sm:hidden tw:p-2 tw:rounded tw:text-gray-600 hover:tw:bg-gray-200"
78
56
  aria-label="Toggle navigation"
57
+ onclick={() => (navOpen = !navOpen)}
79
58
  >
80
- <span class="navbar-toggler-icon"></span>
59
+ <svg xmlns="http://www.w3.org/2000/svg" class="tw:h-5 tw:w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
60
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
61
+ </svg>
81
62
  </button>
82
- <div class="collapse navbar-collapse" id="navbarMain">
83
- <ul class="navbar-nav me-5">
84
- <li class="nav-item"><a class="nav-link active" aria-current="page" href="/">Home</a></li>
85
- <li class="nav-item"><a class="nav-link" href="/info">Info</a></li>
86
63
 
87
- {#if $loginSession}
88
- {#if $loginSession.role == 'admin'}
89
- <li class="nav-item"><a class="nav-link" href="/admin">Admin</a></li>
90
- {/if}
91
- {#if $loginSession.role != 'student'}
92
- <li class="nav-item"><a class="nav-link" href="/teachers">Teachers</a></li>
93
- {/if}
94
- {/if}
95
- </ul>
96
- <ul class="navbar-nav">
97
- {#if $loginSession}
98
- <li class="nav-item dropdown">
99
- <a
100
- class="nav-link dropdown-toggle"
101
- href={'#'}
102
- role="button"
103
- data-bs-toggle="dropdown"
104
- aria-expanded="false"
105
- >
106
- <svg
107
- xmlns="http://www.w3.org/2000/svg"
108
- width="16"
109
- height="16"
110
- fill="currentColor"
111
- class="avatar"
112
- viewBox="0 0 16 16"
113
- >
114
- <path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0z" />
115
- <path
116
- fill-rule="evenodd"
117
- 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"
118
- />
119
- </svg>
120
- {$loginSession.firstName}
121
- </a>
122
- <ul class="dropdown-menu">
123
- <li>
124
- <a class="dropdown-item" href="/profile">Profile</a>
125
- </li>
126
- <li>
127
- <a
128
- onclick={logout}
129
- class="dropdown-item"
130
- class:d-none={!$loginSession || $loginSession.id === 0}
131
- href={'#'}>Logout</a
132
- >
133
- </li>
64
+ <!-- Nav links -->
65
+ <div class="tw:hidden tw:sm:flex tw:items-center tw:gap-6 {navOpen ? '!tw:flex tw:flex-col tw:absolute tw:top-14 tw:left-0 tw:right-0 tw:bg-gray-100 tw:p-4 tw:border-b tw:border-gray-200' : ''}">
66
+ <a class="tw:text-sm tw:text-gray-700 tw:no-underline hover:tw:text-gray-900" href="/">Home</a>
67
+ <a class="tw:text-sm tw:text-gray-700 tw:no-underline hover:tw:text-gray-900" href="/info">Info</a>
68
+
69
+ {#if $loginSession?.role === 'admin'}
70
+ <a class="tw:text-sm tw:text-gray-700 tw:no-underline hover:tw:text-gray-900" href="/admin">Admin</a>
71
+ {/if}
72
+ {#if $loginSession && $loginSession.role !== 'student'}
73
+ <a class="tw:text-sm tw:text-gray-700 tw:no-underline hover:tw:text-gray-900" href="/teachers">Teachers</a>
74
+ {/if}
75
+
76
+ {#if $loginSession}
77
+ <!-- User dropdown -->
78
+ <div class="tw:relative">
79
+ <button
80
+ class="tw:flex tw:items-center tw:gap-1 tw:text-sm tw:text-gray-700 hover:tw:text-gray-900 tw:bg-transparent tw:border-0 tw:cursor-pointer"
81
+ onclick={() => (dropdownOpen = !dropdownOpen)}
82
+ aria-expanded={dropdownOpen}
83
+ >
84
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" class="tw:relative tw:top-[-1.5px]">
85
+ <path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0z" />
86
+ <path fill-rule="evenodd" 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" />
87
+ </svg>
88
+ {$loginSession.firstName}
89
+ <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" viewBox="0 0 16 16">
90
+ <path d="M7.247 11.14 2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z"/>
91
+ </svg>
92
+ </button>
93
+ {#if dropdownOpen}
94
+ <ul class="tw:absolute tw:right-0 tw:mt-1 tw:w-36 tw:rounded tw:border tw:border-gray-200 tw:bg-white tw:shadow-md tw:py-1 tw:z-50 tw:list-none tw:p-0">
95
+ <li><a class="tw:block tw:px-4 tw:py-2 tw:text-sm tw:text-gray-700 tw:no-underline hover:tw:bg-gray-100" href="/profile">Profile</a></li>
96
+ {#if $loginSession.id !== 0}
97
+ <li><a onclick={logout} class="tw:block tw:px-4 tw:py-2 tw:text-sm tw:text-gray-700 tw:no-underline hover:tw:bg-gray-100" href="#">Logout</a></li>
98
+ {/if}
134
99
  </ul>
135
- </li>
136
- {:else}
137
- <li class="nav-item">
138
- <a class="nav-link" href="/login">Login</a>
139
- </li>
140
- {/if}
141
- </ul>
100
+ {/if}
101
+ </div>
102
+ {:else}
103
+ <a class="tw:text-sm tw:text-gray-700 tw:no-underline hover:tw:text-gray-900" href="/login">Login</a>
104
+ {/if}
142
105
  </div>
143
106
  </div>
144
107
  </nav>
145
108
 
146
- <main class="container">
109
+ <main class="tw:mx-auto tw:max-w-5xl tw:px-4 tw:py-6">
147
110
  {@render children?.()}
111
+ </main>
148
112
 
113
+ <!-- Toast notification -->
114
+ {#if $toast.isOpen}
149
115
  <div
150
- id="authToast"
151
- class="toast position-fixed top-0 end-0 m-3"
152
116
  role="alert"
153
117
  aria-live="assertive"
154
118
  aria-atomic="true"
119
+ class="tw:fixed tw:top-4 tw:right-4 tw:z-50 tw:min-w-64 tw:rounded tw:shadow-lg tw:border tw:border-gray-200 tw:bg-white tw:overflow-hidden"
155
120
  >
156
- <div class="toast-header bg-primary text-white">
157
- <strong class="me-auto">{$toast.title}</strong>
158
- <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
159
- </div>
160
- <div class="toast-body">
161
- {$toast.body}
121
+ <div class="tw:flex tw:items-center tw:justify-between tw:bg-blue-600 tw:px-4 tw:py-2">
122
+ <strong class="tw:text-white tw:text-sm">{$toast.title}</strong>
123
+ <button
124
+ type="button"
125
+ aria-label="Close"
126
+ class="tw:text-white tw:bg-transparent tw:border-0 tw:cursor-pointer tw:text-lg tw:leading-none"
127
+ onclick={() => ($toast = { ...$toast, isOpen: false })}>&times;</button>
162
128
  </div>
129
+ <div class="tw:px-4 tw:py-3 tw:text-sm">{$toast.body}</div>
163
130
  </div>
164
- </main>
165
-
166
- <style global>
167
- * {
168
- -webkit-font-smoothing: antialiased;
169
- -moz-osx-font-smoothing: grayscale;
170
- }
171
-
172
- .toast {
173
- z-index: 9999;
174
- }
175
-
176
- .avatar {
177
- position: relative;
178
- top: -1.5px;
179
- }
180
- </style>
131
+ {/if}
@@ -15,6 +15,8 @@
15
15
  let password = $state('')
16
16
  let confirmPassword: HTMLInputElement | undefined = $state()
17
17
  let message = $state('')
18
+ let submitted = $state(false)
19
+ let passwordMismatch = $state(false)
18
20
 
19
21
  onMount(() => {
20
22
  focusedField?.focus()
@@ -27,10 +29,13 @@
27
29
 
28
30
  const resetPassword = async () => {
29
31
  message = ''
32
+ submitted = false
33
+ passwordMismatch = false
30
34
  const form = document.getElementById('reset') as HTMLFormElement
31
35
 
32
36
  if (!passwordMatch()) {
33
- confirmPassword?.classList.add('is-invalid')
37
+ passwordMismatch = true
38
+ return
34
39
  }
35
40
 
36
41
  if (form.checkValidity()) {
@@ -60,7 +65,7 @@
60
65
  message = body.message
61
66
  }
62
67
  } else {
63
- form.classList.add('was-validated')
68
+ submitted = true
64
69
  focusOnFirstError(form)
65
70
  }
66
71
  }
@@ -70,62 +75,65 @@
70
75
  <title>New Password</title>
71
76
  </svelte:head>
72
77
 
73
- <div class="d-flex justify-content-center mt-5">
74
- <div class="card login">
75
- <div class="card-body">
76
- <form id="reset" autocomplete="on" novalidate>
77
- <h4><strong>New Password</strong></h4>
78
- <p>Please provide a new password.</p>
79
- <div class="mb-3">
80
- <label class="form-label" for="password">Password</label>
81
- <input
82
- class="form-control"
83
- id="password"
84
- type="password"
85
- bind:value={password}
86
- bind:this={focusedField}
87
- minlength="8"
88
- maxlength="80"
89
- placeholder="Password"
90
- autocomplete="new-password"
91
- />
92
- <div class="invalid-feedback">Password with 8 chars or more required</div>
93
- <div class="form-text">
94
- Password minimum length 8, must have one capital letter, 1 number, and one unique
95
- character.
96
- </div>
97
- </div>
98
- <div class="mb-3">
99
- <label class="form-label" for="passwordConfirm">Password (retype)</label>
100
- <input
101
- class="form-control"
102
- id="passwordConfirm"
103
- type="password"
104
- required={!!password}
105
- bind:this={confirmPassword}
106
- minlength="8"
107
- maxlength="80"
108
- placeholder="Password (again)"
109
- autocomplete="new-password"
110
- />
111
- <div class="invalid-feedback">Passwords must match</div>
112
- </div>
113
-
114
- {#if message}
115
- <p class="text-danger">{message}</p>
116
- {/if}
117
- <div class="d-grid gap-2">
118
- <button onclick={resetPassword} type="button" class="btn btn-primary btn-lg"
119
- >Send Email</button
120
- >
121
- </div>
122
- </form>
123
- </div>
124
- </div>
125
- </div>
126
-
127
- <style>
128
- .card-body {
129
- width: 25rem;
130
- }
131
- </style>
78
+ <form
79
+ id="reset"
80
+ autocomplete="on"
81
+ novalidate
82
+ class="tw:mx-auto tw:mt-20 tw:max-w-sm tw:space-y-4"
83
+ class:submitted
84
+ >
85
+ <h4><strong>New Password</strong></h4>
86
+ <p>Please provide a new password.</p>
87
+
88
+ <label class="tw:block tw:text-sm tw:font-medium" for="password">
89
+ Password
90
+ <input
91
+ class="tw:peer tw:mt-1 tw:block tw:w-full tw:rounded tw:border tw:border-gray-300 tw:px-3 tw:py-1.5 tw:text-sm focus:tw:outline-none focus:tw:ring-2 focus:tw:ring-blue-500 tw:[.submitted_&]:invalid:border-red-500"
92
+ id="password"
93
+ type="password"
94
+ bind:value={password}
95
+ bind:this={focusedField}
96
+ required
97
+ minlength="8"
98
+ maxlength="80"
99
+ placeholder="Password"
100
+ autocomplete="new-password"
101
+ />
102
+ <span class="tw:hidden tw:text-xs tw:text-red-600 tw:mt-0.5 tw:[.submitted_&]:peer-invalid:block">Password with 8 chars or more required</span>
103
+ <span class="tw:text-xs tw:text-gray-500">
104
+ Minimum 8 characters, one capital letter, one number, one special character.
105
+ </span>
106
+ </label>
107
+
108
+ <label class="tw:block tw:text-sm tw:font-medium" for="passwordConfirm">
109
+ Password (retype)
110
+ <input
111
+ class="tw:mt-1 tw:block tw:w-full tw:rounded tw:border tw:border-gray-300 tw:px-3 tw:py-1.5 tw:text-sm focus:tw:outline-none focus:tw:ring-2 focus:tw:ring-blue-500"
112
+ class:tw:border-red-500={passwordMismatch}
113
+ id="passwordConfirm"
114
+ type="password"
115
+ required={!!password}
116
+ bind:this={confirmPassword}
117
+ minlength="8"
118
+ maxlength="80"
119
+ placeholder="Password (again)"
120
+ autocomplete="new-password"
121
+ />
122
+ {#if passwordMismatch}
123
+ <span class="tw:text-xs tw:text-red-600 tw:mt-0.5">Passwords must match</span>
124
+ {/if}
125
+ </label>
126
+
127
+ {#if message}
128
+ <p class="tw:text-red-600">{message}</p>
129
+ {/if}
130
+
131
+ <button
132
+ onclick={resetPassword}
133
+ type="button"
134
+ class="tw:w-full tw:rounded tw:bg-blue-600 tw:px-4 tw:py-2 tw:font-semibold tw:text-white hover:tw:bg-blue-700"
135
+ >
136
+ Reset Password
137
+ </button>
138
+ </form>
139
+
@@ -7,6 +7,7 @@
7
7
  let focusedField: HTMLInputElement | undefined = $state()
8
8
  let email: string = $state('')
9
9
  let message: string = $state('')
10
+ let submitted = $state(false)
10
11
 
11
12
  onMount(() => {
12
13
  focusedField?.focus()
@@ -38,7 +39,7 @@
38
39
  return goto('/')
39
40
  }
40
41
  } else {
41
- form.classList.add('was-validated')
42
+ submitted = true
42
43
  focusOnFirstError(form)
43
44
  }
44
45
  }
@@ -48,41 +49,42 @@
48
49
  <title>Forgot Password</title>
49
50
  </svelte:head>
50
51
 
51
- <div class="d-flex justify-content-center mt-5">
52
- <div class="card">
53
- <div class="card-body">
54
- <form id="forgot" autocomplete="on" novalidate>
55
- <h4><strong>Forgot password</strong></h4>
56
- <p>Hey, you're human. We get it.</p>
57
- <div class="mb-3">
58
- <label class="form-label" for="email">Email</label>
59
- <input
60
- bind:this={focusedField}
61
- bind:value={email}
62
- type="email"
63
- id="email"
64
- class="form-control"
65
- required
66
- placeholder="Email"
67
- autocomplete="email"
68
- />
69
- <div class="invalid-feedback">Email address required</div>
70
- </div>
71
- {#if message}
72
- <p class="text-danger">{message}</p>
73
- {/if}
74
- <div class="d-grid gap-2">
75
- <button onclick={sendPasswordReset} type="button" class="btn btn-primary btn-lg"
76
- >Send Email</button
77
- >
78
- </div>
79
- </form>
80
- </div>
81
- </div>
82
- </div>
52
+ <form
53
+ id="forgot"
54
+ autocomplete="on"
55
+ novalidate
56
+ class="tw:mx-auto tw:mt-20 tw:max-w-sm tw:space-y-4"
57
+ class:submitted
58
+ >
59
+ <h4><strong>Forgot password</strong></h4>
60
+ <p>Hey, you're human. We get it.</p>
61
+
62
+ <label class="tw:block tw:text-sm tw:font-medium" for="email">
63
+ Email
64
+ <input
65
+ bind:this={focusedField}
66
+ bind:value={email}
67
+ type="email"
68
+ id="email"
69
+ class="tw:peer tw:mt-1 tw:block tw:w-full tw:rounded tw:border tw:border-gray-300 tw:px-3 tw:py-1.5 tw:text-sm focus:tw:outline-none focus:tw:ring-2 focus:tw:ring-blue-500 tw:[.submitted_&]:invalid:border-red-500"
70
+ required
71
+ placeholder="Email"
72
+ autocomplete="email"
73
+ />
74
+ <span class="tw:hidden tw:text-xs tw:text-red-600 tw:mt-0.5 tw:[.submitted_&]:peer-invalid:block">Email address required</span>
75
+ </label>
76
+
77
+ {#if message}
78
+ <p class="tw:text-red-600">{message}</p>
79
+ {/if}
80
+
81
+ <button
82
+ onclick={sendPasswordReset}
83
+ type="button"
84
+ class="tw:w-full tw:rounded tw:bg-blue-600 tw:px-4 tw:py-2 tw:font-semibold tw:text-white hover:tw:bg-blue-700"
85
+ >
86
+ Send Email
87
+ </button>
88
+ </form>
89
+
83
90
 
84
- <style>
85
- .card-body {
86
- width: 25rem;
87
- }
88
- </style>
@@ -0,0 +1,17 @@
1
+ @import 'tailwindcss' prefix(tw);
2
+
3
+ @theme {
4
+ --font-sans: 'Avenir Next', 'Avenir', 'Myriad Pro', 'Helvetica Neue', sans-serif;
5
+ }
6
+
7
+ @layer base {
8
+ * {
9
+ -webkit-font-smoothing: antialiased;
10
+ -moz-osx-font-smoothing: grayscale;
11
+ }
12
+
13
+ html,
14
+ :host {
15
+ @apply tw:font-sans tw:text-gray-800;
16
+ }
17
+ }