sveltekit-auth-example 1.0.3

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.
@@ -0,0 +1,116 @@
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
+ <script lang="ts">
14
+ import { onMount } from 'svelte'
15
+ import { goto } from '$app/navigation'
16
+ import { toast } from '../../../stores'
17
+ import { focusOnFirstError } from '$lib/focus'
18
+
19
+ export let token: string
20
+
21
+ let focusedField: HTMLInputElement
22
+ let password: string
23
+ let confirmPassword: HTMLInputElement
24
+ let message: string
25
+
26
+ onMount(() => {
27
+ focusedField.focus()
28
+ })
29
+
30
+ const passwordMatch = () => {
31
+ if (!password) password = ''
32
+ return password == confirmPassword.value
33
+ }
34
+
35
+ const resetPassword = async () => {
36
+ message = ''
37
+ const form = document.forms['reset']
38
+
39
+ if (!passwordMatch()) {
40
+ confirmPassword.classList.add('is-invalid')
41
+ }
42
+
43
+ if (form.checkValidity()) {
44
+
45
+ const url = `/auth/reset`
46
+ const res = await fetch(url, {
47
+ method: 'PUT',
48
+ headers: {
49
+ 'Content-Type': 'application/json'
50
+ },
51
+ body: JSON.stringify({
52
+ token,
53
+ password
54
+ })
55
+ })
56
+
57
+ if (res.ok) {
58
+ $toast = {
59
+ title: 'Password Reset Succesful',
60
+ body: 'Your password was reset. Please login.',
61
+ isOpen: true
62
+ }
63
+
64
+ goto('/login')
65
+ } else {
66
+ const body = await res.json()
67
+ console.log('Failed reset', body)
68
+ message = body.message
69
+ }
70
+
71
+ } else {
72
+ form.classList.add('was-validated')
73
+ focusOnFirstError(form)
74
+ }
75
+
76
+ }
77
+ </script>
78
+
79
+ <svelte:head>
80
+ <title>New Password</title>
81
+ </svelte:head>
82
+
83
+ <div class="d-flex justify-content-center mt-5">
84
+ <div class="card login">
85
+ <div class="card-body">
86
+ <form id="reset" autocomplete="on" novalidate>
87
+ <h4><strong>New Password</strong></h4>
88
+ <p>Please provide a new password.</p>
89
+ <div class="mb-3">
90
+ <label class="form-label" for="password">Password</label>
91
+ <input class="form-control" id="password" type="password" bind:value={password} bind:this={focusedField} minlength="8" maxlength="80" placeholder="Password" autocomplete="new-password"/>
92
+ <div class="invalid-feedback">Password with 8 chars or more required</div>
93
+ <div class="form-text">Password minimum length 8, must have one capital letter, 1 number, and one unique character.</div>
94
+ </div>
95
+ <div class="mb-3">
96
+ <label class="form-label" for="passwordConfirm">Password (retype)</label>
97
+ <input class="form-control" id="passwordConfirm" type="password" required={!!password} bind:this={confirmPassword} minlength="8" maxlength="80" placeholder="Password (again)" autocomplete="new-password"/>
98
+ <div class="invalid-feedback">Passwords must match</div>
99
+ </div>
100
+
101
+ {#if message}
102
+ <p class="text-danger">{message}</p>
103
+ {/if}
104
+ <div class="d-grid gap-2">
105
+ <button on:click|preventDefault={resetPassword} class="btn btn-primary btn-lg">Send Email</button>
106
+ </div>
107
+ </form>
108
+ </div>
109
+ </div>
110
+ </div>
111
+
112
+ <style lang="scss">
113
+ .card-body {
114
+ width: 25rem;
115
+ }
116
+ </style>
@@ -0,0 +1,39 @@
1
+ import dotenv from 'dotenv'
2
+ import type { RequestHandler } from '@sveltejs/kit'
3
+ import type { JwtPayload } from 'jsonwebtoken'
4
+ import jwt from 'jsonwebtoken'
5
+ import { query } from '../../_db'
6
+
7
+ dotenv.config()
8
+
9
+ const JWT_SECRET = process.env['JWT_SECRET']
10
+
11
+ export const put: RequestHandler = async event => {
12
+ const body = await event.request.json()
13
+ const { token, password } = body
14
+
15
+ // Check the validity of the token and extract userId
16
+ try {
17
+ const decoded = <JwtPayload> jwt.verify(token, JWT_SECRET)
18
+ const userId = decoded.subject
19
+
20
+ // Update the database with the new password
21
+ const sql = `CALL reset_password($1, $2);`
22
+ await query(sql, [userId, password])
23
+
24
+ return {
25
+ status: 200,
26
+ body: {
27
+ message: 'Password successfully reset.'
28
+ }
29
+ }
30
+ } catch (error) {
31
+ // 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
+ }
38
+ }
39
+ }
@@ -0,0 +1,77 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte'
3
+ import { goto } from '$app/navigation'
4
+ import { toast } from '../stores'
5
+ import { focusOnFirstError } from '$lib/focus'
6
+
7
+ let focusedField: HTMLInputElement
8
+ let email: string
9
+ let message: string
10
+
11
+ onMount(() => {
12
+ focusedField.focus()
13
+ })
14
+
15
+ const sendPasswordReset = async () => {
16
+ message = ''
17
+ const form = document.forms['forgot']
18
+
19
+ if (form.checkValidity()) {
20
+ if (email.toLowerCase().includes('gmail.com')) {
21
+ return message = 'GMail passwords must be reset on Manage Your Google Account.'
22
+ }
23
+ const url = `/auth/forgot`
24
+ const res = await fetch(url, {
25
+ method: 'POST',
26
+ headers: {
27
+ 'Content-Type': 'application/json'
28
+ },
29
+ body: JSON.stringify({ email })
30
+ })
31
+
32
+ if (res.ok) {
33
+ $toast = {
34
+ title: 'Password Reset',
35
+ body: 'Please check your inbox for a password reset email (junk mail, too).',
36
+ isOpen: true
37
+ }
38
+ return goto('/')
39
+ }
40
+ } else {
41
+ form.classList.add('was-validated')
42
+ focusOnFirstError(form)
43
+ }
44
+ }
45
+ </script>
46
+
47
+ <svelte:head>
48
+ <title>Forgot Password</title>
49
+ </svelte:head>
50
+
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 bind:this={focusedField} bind:value={email} type="email" id="email" class="form-control" required placeholder="Email" autocomplete="email"/>
60
+ <div class="invalid-feedback">Email address required</div>
61
+ </div>
62
+ {#if message}
63
+ <p class="text-danger">{message}</p>
64
+ {/if}
65
+ <div class="d-grid gap-2">
66
+ <button on:click|preventDefault={sendPasswordReset} class="btn btn-primary btn-lg">Send Email</button>
67
+ </div>
68
+ </form>
69
+ </div>
70
+ </div>
71
+ </div>
72
+
73
+ <style lang="scss">
74
+ .card-body {
75
+ width: 25rem;
76
+ }
77
+ </style>
@@ -0,0 +1,2 @@
1
+ <h1>Home</h1>
2
+ <h4>Public</h4>
@@ -0,0 +1,6 @@
1
+ <svelte:head>
2
+ <title>Info Page</title>
3
+ </svelte:head>
4
+
5
+ <h1>Info</h1>
6
+ <h4>Public</h4>
@@ -0,0 +1,127 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte'
3
+ import { goto } from '$app/navigation'
4
+ import { page, session } from '$app/stores'
5
+ import useAuth from '$lib/auth'
6
+ import { focusOnFirstError } from '$lib/focus'
7
+
8
+ const { loadScript, initializeSignInWithGoogle, loginLocal } = useAuth(page, session, goto)
9
+
10
+ let focusedField: HTMLInputElement
11
+ let message: string
12
+ const credentials: Credentials = {
13
+ email: '',
14
+ password: ''
15
+ }
16
+
17
+ async function login() {
18
+ message = ''
19
+ const form = document.forms['signIn']
20
+
21
+ if (form.checkValidity()) {
22
+ try {
23
+ await loginLocal(credentials)
24
+ } catch (err) {
25
+ console.error('Login error', err.message)
26
+ message = err.message
27
+ }
28
+ } else {
29
+ form.classList.add('was-validated')
30
+ focusOnFirstError(form)
31
+ }
32
+ }
33
+
34
+ onMount(async() => {
35
+ await loadScript() // probably cached
36
+ initializeSignInWithGoogle('googleButton')
37
+ focusedField.focus()
38
+ })
39
+ </script>
40
+
41
+ <svelte:head>
42
+ <title>Login Form</title>
43
+ <meta name='robots' content='noindex, nofollow'/>
44
+ </svelte:head>
45
+
46
+ <div class="d-flex justify-content-center mt-5">
47
+ <div class="card">
48
+ <div class="card-body">
49
+ <form id="signIn" autocomplete="on" novalidate>
50
+ <h4><strong>Sign In</strong></h4>
51
+ <p>Welcome back.</p>
52
+ <div>
53
+ <div class="mb-1">
54
+ <div id="googleButton"></div>
55
+ </div>
56
+ <div class="text-centered">
57
+ <div class="strike">
58
+ <span>or</span>
59
+ </div>
60
+ </div>
61
+ <div class="mb-3">
62
+ <label class="form-label" for="email">Email</label>
63
+ <input type="email" class="form-control" bind:this={focusedField} bind:value={credentials.email} required placeholder="Email" autocomplete="email"/>
64
+ <div class="invalid-feedback">Email address required</div>
65
+ </div>
66
+ <div class="mb-3">
67
+ <label class="form-label" for="password">Password</label>
68
+ <input class="form-control" type="password" bind:value={credentials.password} required minlength="8" maxlength="80" placeholder="Password" autocomplete="current-password"/>
69
+ <div class="invalid-feedback">Password with 8 chars or more required</div>
70
+ <div class="form-text">Password minimum length 8, must have one capital letter, 1 number, and one unique character.</div>
71
+ </div>
72
+ </div>
73
+ <div>
74
+ <a href="/forgot" class="text-black-50">Forgot Password?</a><br/>
75
+ <br/>
76
+ </div>
77
+ {#if message}
78
+ <p class="text-danger">{message}</p>
79
+ {/if}
80
+ <div class="d-grid gap-2">
81
+ <button on:click|preventDefault={login} class="btn btn-primary btn-lg">Sign In</button>
82
+ </div>
83
+ </form>
84
+ </div>
85
+ <div class="card-footer text-center bg-white">
86
+ <a href="/register" class="text-black-50">Don't have an account?</a>
87
+ </div>
88
+ </div>
89
+ </div>
90
+
91
+ <style lang="scss">
92
+ .card-body {
93
+ width: 25rem;
94
+ }
95
+
96
+ .strike {
97
+ display: block;
98
+ text-align: center;
99
+ overflow: hidden;
100
+ white-space: nowrap;
101
+ }
102
+
103
+ .strike > span {
104
+ position: relative;
105
+ display: inline-block;
106
+ }
107
+
108
+ .strike > span:before,
109
+ .strike > span:after {
110
+ content: "";
111
+ position: absolute;
112
+ top: 50%;
113
+ width: 9999px;
114
+ height: 1px;
115
+ background: darkgray;
116
+ }
117
+
118
+ .strike > span:before {
119
+ right: 100%;
120
+ margin-right: 10px;
121
+ }
122
+
123
+ .strike > span:after {
124
+ left: 100%;
125
+ margin-left: 10px;
126
+ }
127
+ </style>
@@ -0,0 +1,122 @@
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 (!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
+ <script lang="ts">
23
+ import { session } from '$app/stores'
24
+ import { focusOnFirstError } from '$lib/focus';
25
+
26
+ let focusedField: HTMLInputElement
27
+ let message: string
28
+ let confirmPassword: HTMLInputElement
29
+ export let user: User
30
+
31
+ async function update() {
32
+ message = ''
33
+ const form = document.forms['profile']
34
+
35
+ if (!passwordMatch()) {
36
+ confirmPassword.classList.add('is-invalid')
37
+ return
38
+ }
39
+
40
+ if (form.checkValidity()) {
41
+ $session.user = JSON.parse(JSON.stringify(user)) // deep clone
42
+ const url = '/api/v1/user'
43
+ const res = await fetch(url, {
44
+ method: 'PUT',
45
+ headers: {
46
+ 'Content-Type': 'application/json'
47
+ },
48
+ body: JSON.stringify(user)
49
+ })
50
+ const reply = await res.json()
51
+ message = reply.message
52
+ } else {
53
+ form.classList.add('was-validated')
54
+ focusOnFirstError(form)
55
+ }
56
+
57
+ }
58
+
59
+ const passwordMatch = () => {
60
+ if (!user.password) user.password = ''
61
+ return user.password == confirmPassword.value
62
+ }
63
+ </script>
64
+
65
+ <svelte:head>
66
+ <title>Profile</title>
67
+ </svelte:head>
68
+
69
+ <div class="d-flex justify-content-center my-3">
70
+ <div class="card login">
71
+ <div class="card-body">
72
+ <h4><strong>Profile</strong></h4>
73
+ <p>Update your information.</p>
74
+ <form id="profile" autocomplete="on" novalidate class="mt-3">
75
+ {#if !user.email.includes('gmail.com')}
76
+ <div class="mb-3">
77
+ <label class="form-label" for="email">Email</label>
78
+ <input bind:this={focusedField} type="email" class="form-control" bind:value={user.email} required placeholder="Email" id="email" autocomplete="email"/>
79
+ <div class="invalid-feedback">Email address required</div>
80
+ </div>
81
+ <div class="mb-3">
82
+ <label class="form-label" for="password">Password</label>
83
+ <input type="password" id="password" class="form-control" bind:value={user.password} minlength="8" maxlength="80" placeholder="Password"/>
84
+ <div class="invalid-feedback">Password with 8 chars or more required</div>
85
+ <div class="form-text">Password minimum length 8, must have one capital letter, 1 number, and one unique character.</div>
86
+ </div>
87
+ <div class="mb-3">
88
+ <label class="form-label" for="password">Confirm password</label>
89
+ <input type="password" id="password" class="form-control" bind:this={confirmPassword} required={!!user.password} minlength="8" maxlength="80" placeholder="Password (again)" autocomplete="new-password"/>
90
+ <div class="form-text">Password minimum length 8, must have one capital letter, 1 number, and one unique character.</div>
91
+ </div>
92
+ {/if}
93
+ <div class="mb-3">
94
+ <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"/>
96
+ <div class="invalid-feedback">First name required</div>
97
+ </div>
98
+ <div class="mb-3">
99
+ <label class="form-label" for="lastName">Last name</label>
100
+ <input bind:value={user.lastName} class="form-control" id="lastName" required placeholder="Last name" autocomplete="family-name"/>
101
+ <div class="invalid-feedback">Last name required</div>
102
+ </div>
103
+ <div class="mb-3">
104
+ <label class="form-label" for="phone">Phone</label>
105
+ <input type="tel" bind:value={user.phone} id="phone" class="form-control" placeholder="Phone" autocomplete="tel-local"/>
106
+ </div>
107
+
108
+ {#if message}
109
+ <p>{message}</p>
110
+ {/if}
111
+
112
+ <button type="button" on:click={update} class="btn btn-primary btn-lg">Update</button>
113
+ </form>
114
+ </div>
115
+ </div>
116
+ </div>
117
+
118
+ <style lang="scss">
119
+ .card-body {
120
+ width: 25rem;
121
+ }
122
+ </style>
@@ -0,0 +1,133 @@
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
+ <script lang="ts">
16
+ import { onMount } from 'svelte'
17
+ import { goto } from '$app/navigation'
18
+ import { page, session } from '$app/stores'
19
+ import useAuth from '$lib/auth'
20
+ import { focusOnFirstError } from '$lib/focus'
21
+
22
+ const { initializeSignInWithGoogle, registerLocal } = useAuth(page, session, goto)
23
+
24
+ let focusedField: HTMLInputElement
25
+
26
+ let user = {
27
+ firstName: '',
28
+ lastName: '',
29
+ password: '',
30
+ email: '',
31
+ phone: ''
32
+ }
33
+ let confirmPassword: HTMLInputElement
34
+ let message: string
35
+
36
+ async function register() {
37
+ const form = document.forms['register']
38
+ message = ''
39
+
40
+ if (!passwordMatch()) {
41
+ confirmPassword.classList.add('is-invalid')
42
+ return
43
+ }
44
+
45
+ if (form.checkValidity()) {
46
+ try {
47
+ await registerLocal(user)
48
+ } catch (err) {
49
+ console.error('Login error', err.message)
50
+ message = err.message
51
+ }
52
+ } else {
53
+ form.classList.add('was-validated')
54
+ focusOnFirstError(form)
55
+ }
56
+
57
+ }
58
+
59
+ onMount(() => {
60
+ focusedField.focus()
61
+ initializeSignInWithGoogle()
62
+ google.accounts.id.renderButton(
63
+ document.getElementById('googleButton'),
64
+ { theme: 'filled_blue', size: 'large', width: '367' } // customization attributes
65
+ )
66
+ })
67
+
68
+
69
+ const passwordMatch = () => {
70
+ if (!user.password) user.password = ''
71
+ return user.password == confirmPassword.value
72
+ }
73
+ </script>
74
+
75
+ <svelte:head>
76
+ <title>Register</title>
77
+ </svelte:head>
78
+
79
+ <div class="d-flex justify-content-center my-3">
80
+ <div class="card login">
81
+ <div class="card-body">
82
+ <h4><strong>Register</strong></h4>
83
+ <p>Welcome to our community.</p>
84
+ <form id="register" autocomplete="on" novalidate class="mt-3">
85
+ <div class="mb-3">
86
+ <div id="googleButton"></div>
87
+ </div>
88
+ <div class="mb-3">
89
+ <label class="form-label" for="email">Email</label>
90
+ <input bind:this={focusedField} type="email" class="form-control" bind:value={user.email} required placeholder="Email" id="email" autocomplete="email"/>
91
+ <div class="invalid-feedback">Email address required</div>
92
+ </div>
93
+ <div class="mb-3">
94
+ <label class="form-label" for="password">Password</label>
95
+ <input type="password" id="password" class="form-control" bind:value={user.password} required minlength="8" maxlength="80" placeholder="Password" autocomplete="new-password"/>
96
+ <div class="invalid-feedback">Password with 8 chars or more required</div>
97
+ <div class="form-text">Password minimum length 8, must have one capital letter, 1 number, and one unique character.</div>
98
+ </div>
99
+ <div class="mb-3">
100
+ <label class="form-label" for="password">Confirm password</label>
101
+ <input type="password" id="password" class="form-control" bind:this={confirmPassword} required minlength="8" maxlength="80" placeholder="Password (again)" autocomplete="new-password"/>
102
+ <div class="form-text">Password minimum length 8, must have one capital letter, 1 number, and one unique character.</div>
103
+ </div>
104
+ <div class="mb-3">
105
+ <label class="form-label" for="firstName">First name</label>
106
+ <input bind:value={user.firstName} class="form-control" id="firstName" placeholder="First name" required autocomplete="given-name"/>
107
+ <div class="invalid-feedback">First name required</div>
108
+ </div>
109
+ <div class="mb-3">
110
+ <label class="form-label" for="lastName">Last name</label>
111
+ <input bind:value={user.lastName} class="form-control" id="lastName" placeholder="Last name" required autocomplete="family-name"/>
112
+ <div class="invalid-feedback">Last name required</div>
113
+ </div>
114
+ <div class="mb-3">
115
+ <label class="form-label" for="phone">Phone</label>
116
+ <input type="tel" bind:value={user.phone} id="phone" class="form-control" placeholder="Phone" autocomplete="tel-local"/>
117
+ </div>
118
+
119
+ {#if message}
120
+ <p>{message}</p>
121
+ {/if}
122
+
123
+ <button type="button" on:click={register} class="btn btn-primary btn-lg">Register</button>
124
+ </form>
125
+ </div>
126
+ </div>
127
+ </div>
128
+
129
+ <style lang="scss">
130
+ .card-body {
131
+ width: 25rem;
132
+ }
133
+ </style>
@@ -0,0 +1,39 @@
1
+ <script context="module" lang="ts">
2
+ import type { Load } from '@sveltejs/kit'
3
+
4
+ export const load: Load = async ({ session }) => {
5
+ const authorized = ['admin', 'teacher']
6
+ if (!authorized.includes(session.user?.role)) {
7
+ return {
8
+ status: 302,
9
+ redirect: '/login?referrer=/teachers'
10
+ }
11
+ }
12
+
13
+ const url = '/api/v1/teacher'
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
+ </script>
28
+
29
+ <script lang="ts">
30
+ export let message
31
+ </script>
32
+
33
+ <svelte:head>
34
+ <title>Teachers</title>
35
+ </svelte:head>
36
+
37
+ <h1>Teachers</h1>
38
+ <h4>Teacher Or Admin Role</h4>
39
+ <p>{message}</p>
package/src/stores.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { writable } from 'svelte/store'
2
+
3
+ export const toast = writable({
4
+ title: '',
5
+ body: '',
6
+ isOpen: false
7
+ })
Binary file