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.
- package/.eslintrc.cjs +20 -0
- package/.prettierrc +6 -0
- package/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/db_create.sql +307 -0
- package/package.json +59 -0
- package/src/app.html +12 -0
- package/src/global.d.ts +71 -0
- package/src/hooks.ts +43 -0
- package/src/lib/auth.ts +152 -0
- package/src/lib/config.ts +4 -0
- package/src/lib/focus.ts +10 -0
- package/src/routes/__error.svelte +16 -0
- package/src/routes/__layout.svelte +84 -0
- package/src/routes/_db.ts +17 -0
- package/src/routes/_send-in-blue.ts +29 -0
- package/src/routes/admin.svelte +40 -0
- package/src/routes/api/v1/admin.ts +21 -0
- package/src/routes/api/v1/teacher.ts +21 -0
- package/src/routes/api/v1/user.ts +36 -0
- package/src/routes/auth/[slug].ts +82 -0
- package/src/routes/auth/forgot.ts +42 -0
- package/src/routes/auth/google.ts +65 -0
- package/src/routes/auth/reset/[token].svelte +116 -0
- package/src/routes/auth/reset/index.ts +39 -0
- package/src/routes/forgot.svelte +77 -0
- package/src/routes/index.svelte +2 -0
- package/src/routes/info.svelte +6 -0
- package/src/routes/login.svelte +127 -0
- package/src/routes/profile.svelte +122 -0
- package/src/routes/register.svelte +133 -0
- package/src/routes/teachers.svelte +39 -0
- package/src/stores.ts +7 -0
- package/static/favicon.png +0 -0
- package/svelte.config.js +15 -0
- package/tsconfig.json +32 -0
|
@@ -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,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
|
Binary file
|