sveltekit-auth-example 1.0.20 → 1.0.23
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/.env-sample +8 -0
- package/CHANGELOG.md +11 -1
- package/README.md +4 -4
- package/package.json +7 -11
- package/src/app.d.ts +13 -7
- package/src/hooks.server.ts +1 -1
- package/src/lib/auth.ts +3 -3
- package/src/{routes/_db.ts → lib/server/db.ts} +2 -4
- package/src/{routes/_send-in-blue.ts → lib/server/send-in-blue.ts} +3 -10
- package/src/routes/+layout.server.ts +2 -3
- package/src/routes/admin/+page.server.ts +1 -1
- package/src/routes/api/v1/user/+server.ts +2 -2
- package/src/routes/auth/[slug]/+server.ts +3 -4
- package/src/routes/auth/forgot/+server.ts +3 -8
- package/src/routes/auth/google/+server.ts +5 -6
- package/src/routes/auth/reset/+server.ts +28 -29
- package/src/routes/register/+page.svelte +0 -1
- package/src/routes/teachers/+page.server.ts +1 -1
- package/src/lib/config.ts +0 -4
package/.env-sample
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
DATABASE_URL=postgres://REPLACE_WITH_USER:REPLACE_WITH_PASSWORD@localhost:5432/auth
|
|
2
|
+
DOMAIN=http://localhost:3000
|
|
3
|
+
JWT_SECRET=replace_with_your_own
|
|
4
|
+
SEND_IN_BLUE_URL=https://api.sendinblue.com
|
|
5
|
+
SEND_IN_BLUE_KEY=REPLACE_WITH_YOUR_OWN
|
|
6
|
+
SEND_IN_BLUE_FROM='{ "email":"sender@example.com", "name":"First Last" }'
|
|
7
|
+
SEND_IN_BLUE_ADMINS='{ "email":"admin@example.com", "name":"First Last" }'
|
|
8
|
+
PUBLIC_GOOGLE_CLIENT_ID=REPLACE_WITH_YOUR_OWN
|
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
# Backlog
|
|
2
|
-
* Refactor $env/dynamic/private and public
|
|
3
2
|
* Add password complexity checking on /register and /profile pages (only checks for length currently despite what the pages say)
|
|
4
3
|
|
|
4
|
+
# 1.0.23
|
|
5
|
+
* Restructured server-side libraries to $lib/server based on https://github.com/sveltejs/kit/pull/6623
|
|
6
|
+
* General cleanup
|
|
7
|
+
|
|
8
|
+
# 1.0.22
|
|
9
|
+
* Move google-auth-library and jsonwebtoken to devDependencies from dependencies and other cleanup to package.json
|
|
10
|
+
|
|
11
|
+
# 1.0.21
|
|
12
|
+
* Refactor to use $env/static/private and public, dropping dotenv dependency
|
|
13
|
+
* Remove @types/cookie and bootstrap-icons dependencies
|
|
14
|
+
|
|
5
15
|
# 1.0.20
|
|
6
16
|
* Bump dependencies
|
|
7
17
|
* Add service-worker
|
package/README.md
CHANGED
|
@@ -28,9 +28,9 @@ The website supports two types of authentication:
|
|
|
28
28
|
The forgot password functionality uses [**SendInBlue**](https://www.sendinblue.com) to send the email. You would need to have a **SendInBlue** account and set three environmental variables. Email sending is in /src/routes/auth/forgot.ts. This code could easily be replaced by nodemailer or something similar. Note: I have no affliation with **SendInBlue** (just happen to be familiar with their API because of another project).
|
|
29
29
|
|
|
30
30
|
## Prerequisites
|
|
31
|
-
- PostgreSQL 14 or higher
|
|
32
|
-
- Node.js 18.
|
|
33
|
-
- npm 8.
|
|
31
|
+
- PostgreSQL 14.5 or higher
|
|
32
|
+
- Node.js 18.9.0 or higher
|
|
33
|
+
- npm 8.19.1 or higher
|
|
34
34
|
- Google API client
|
|
35
35
|
- SendInBlue account (only used for emailing password reset link - the sample can run without it but forgot password will not work)
|
|
36
36
|
|
|
@@ -62,7 +62,7 @@ SEND_IN_BLUE_URL=https://api.sendinblue.com
|
|
|
62
62
|
SEND_IN_BLUE_KEY=replace_with_your_own
|
|
63
63
|
SEND_IN_BLUE_FROM='{ "email":"jdoe@example.com", "name":"John Doe" }'
|
|
64
64
|
SEND_IN_BLUE_ADMINS='{ "email":"jdoe@example.com", "name":"John Doe" }'
|
|
65
|
-
|
|
65
|
+
PUBLIC_GOOGLE_CLIENT_ID=replace_with_your_own
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
## Run locally
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sveltekit-auth-example",
|
|
3
3
|
"description": "SvelteKit Authentication Example",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.23",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "Nate Stuyvesant",
|
|
7
7
|
"license": "https://github.com/nstuyvesant/sveltekit-auth-example/blob/master/LICENSE",
|
|
@@ -18,8 +18,7 @@
|
|
|
18
18
|
"authentication",
|
|
19
19
|
"example",
|
|
20
20
|
"google",
|
|
21
|
-
"postgresql"
|
|
22
|
-
"example"
|
|
21
|
+
"postgresql"
|
|
23
22
|
],
|
|
24
23
|
"scripts": {
|
|
25
24
|
"start": "node build",
|
|
@@ -38,26 +37,23 @@
|
|
|
38
37
|
},
|
|
39
38
|
"type": "module",
|
|
40
39
|
"dependencies": {
|
|
41
|
-
"dotenv": "^16.0.2",
|
|
42
|
-
"google-auth-library": "^8.5.1",
|
|
43
|
-
"jsonwebtoken": "^8.5.1",
|
|
44
40
|
"pg": "^8.8.0"
|
|
45
41
|
},
|
|
46
42
|
"devDependencies": {
|
|
47
43
|
"@sveltejs/adapter-node": "latest",
|
|
48
44
|
"@sveltejs/kit": "latest",
|
|
49
45
|
"@types/bootstrap": "5.2.4",
|
|
50
|
-
"@types/cookie": "^0.5.1",
|
|
51
46
|
"@types/google.accounts": "0.0.2",
|
|
52
47
|
"@types/jsonwebtoken": "^8.5.9",
|
|
53
48
|
"@types/pg": "^8.6.5",
|
|
54
|
-
"@typescript-eslint/eslint-plugin": "^5.
|
|
55
|
-
"@typescript-eslint/parser": "^5.
|
|
49
|
+
"@typescript-eslint/eslint-plugin": "^5.37.0",
|
|
50
|
+
"@typescript-eslint/parser": "^5.37.0",
|
|
56
51
|
"bootstrap": "^5.2.1",
|
|
57
|
-
"
|
|
58
|
-
"eslint": "^8.23.0",
|
|
52
|
+
"eslint": "^8.23.1",
|
|
59
53
|
"eslint-config-prettier": "^8.5.0",
|
|
60
54
|
"eslint-plugin-svelte3": "^4.0.0",
|
|
55
|
+
"google-auth-library": "^8.5.1",
|
|
56
|
+
"jsonwebtoken": "^8.5.1",
|
|
61
57
|
"prettier": "^2.7.1",
|
|
62
58
|
"prettier-plugin-svelte": "^2.7.0",
|
|
63
59
|
"sass": "^1.54.9",
|
package/src/app.d.ts
CHANGED
|
@@ -13,9 +13,19 @@ declare namespace App {
|
|
|
13
13
|
|
|
14
14
|
// interface Platform {}
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
interface PrivateEnv { // $env/static/private
|
|
17
|
+
DATABASE_URL: string
|
|
18
|
+
DOMAIN: string
|
|
19
|
+
JWT_SECRET: string
|
|
20
|
+
SEND_IN_BLUE_URL: string
|
|
21
|
+
SEND_IN_BLUE_KEY: string
|
|
22
|
+
SEND_IN_BLUE_FROM: string
|
|
23
|
+
SEND_IN_BLUE_ADMINS: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface PublicEnv { // $env/static/public
|
|
27
|
+
PUBLIC_GOOGLE_CLIENT_ID: string
|
|
28
|
+
}
|
|
19
29
|
}
|
|
20
30
|
|
|
21
31
|
interface AuthenticationResult {
|
|
@@ -43,10 +53,6 @@ interface GoogleCredentialResponse {
|
|
|
43
53
|
| 'btn_confirm_add_session'
|
|
44
54
|
}
|
|
45
55
|
|
|
46
|
-
interface ImportMetaEnv {
|
|
47
|
-
VITE_GOOGLE_CLIENT_ID: string
|
|
48
|
-
}
|
|
49
|
-
|
|
50
56
|
interface MessageAddressee {
|
|
51
57
|
email: string
|
|
52
58
|
name?: string
|
package/src/hooks.server.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Handle, RequestEvent } from '@sveltejs/kit'
|
|
2
|
-
import { query } from '
|
|
2
|
+
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) {
|
package/src/lib/auth.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import type { Page } from '@sveltejs/kit'
|
|
4
4
|
import type { Readable, Writable } from 'svelte/store'
|
|
5
|
-
import {
|
|
5
|
+
import { PUBLIC_GOOGLE_CLIENT_ID } from '$env/static/public'
|
|
6
6
|
|
|
7
7
|
export default function useAuth(
|
|
8
8
|
page: Readable<Page>,
|
|
@@ -44,7 +44,7 @@ export default function useAuth(
|
|
|
44
44
|
|
|
45
45
|
function initializeSignInWithGoogle(htmlId?: string) {
|
|
46
46
|
const { id } = window.google.accounts // assumes <script src="https://accounts.google.com/gsi/client" async defer></script> is in app.html
|
|
47
|
-
id.initialize({ client_id:
|
|
47
|
+
id.initialize({ client_id: PUBLIC_GOOGLE_CLIENT_ID, callback: googleCallback })
|
|
48
48
|
|
|
49
49
|
if (htmlId) {
|
|
50
50
|
// render button instead of prompt
|
|
@@ -62,7 +62,7 @@ export default function useAuth(
|
|
|
62
62
|
try {
|
|
63
63
|
const res = await fetch('/auth/register', {
|
|
64
64
|
method: 'POST',
|
|
65
|
-
body: JSON.stringify(user), // server
|
|
65
|
+
body: JSON.stringify(user), // server ignores user.role - always set it to 'student' (lowest priv)
|
|
66
66
|
headers: {
|
|
67
67
|
'Content-Type': 'application/json'
|
|
68
68
|
}
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import dotenv from 'dotenv'
|
|
3
2
|
import type { QueryResult} from 'pg'
|
|
4
3
|
import pg from 'pg'
|
|
5
|
-
|
|
6
|
-
dotenv.config()
|
|
4
|
+
import { DATABASE_URL } from '$env/static/private'
|
|
7
5
|
|
|
8
6
|
const pool = new pg.Pool({
|
|
9
7
|
max: 10, // default
|
|
10
|
-
connectionString:
|
|
8
|
+
connectionString: DATABASE_URL,
|
|
11
9
|
ssl: {
|
|
12
10
|
rejectUnauthorized: false
|
|
13
11
|
}
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { SEND_IN_BLUE_KEY, SEND_IN_BLUE_URL, SEND_IN_BLUE_FROM, SEND_IN_BLUE_ADMINS } from '$env/static/private'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const SEND_IN_BLUE_KEY = process.env.SEND_IN_BLUE_KEY
|
|
6
|
-
const SEND_IN_BLUE_URL = process.env.SEND_IN_BLUE_URL
|
|
7
|
-
const SEND_IN_BLUE_FROM = <MessageAddressee> JSON.parse(process.env.SEND_IN_BLUE_FROM || '')
|
|
8
|
-
const SEND_IN_BLUE_ADMINS = <MessageAddressee> JSON.parse(process.env.SEND_IN_BLUE_ADMINS || '')
|
|
3
|
+
const sender = <MessageAddressee> JSON.parse(SEND_IN_BLUE_FROM || '')
|
|
4
|
+
const to = <MessageAddressee> JSON.parse(SEND_IN_BLUE_ADMINS || '')
|
|
9
5
|
|
|
10
6
|
// POST or PUT submission to SendInBlue
|
|
11
7
|
const submit = async (method: string, url: string, data: Partial<SendInBlueContact> | SendInBlueMessage) => {
|
|
@@ -23,7 +19,4 @@ const submit = async (method: string, url: string, data: Partial<SendInBlueConta
|
|
|
23
19
|
}
|
|
24
20
|
}
|
|
25
21
|
|
|
26
|
-
const sender = SEND_IN_BLUE_FROM
|
|
27
|
-
const to = SEND_IN_BLUE_ADMINS
|
|
28
|
-
|
|
29
22
|
export const sendMessage = async (message: Message) => submit('POST', '/v3/smtp/email', { sender, to: [to], ...message })
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { LayoutServerLoad } from './$types'
|
|
2
2
|
|
|
3
|
-
export const load: LayoutServerLoad = (
|
|
4
|
-
const
|
|
5
|
-
const { user }: { user: User } = locals // locals.user set by hooks.ts/handle(), undefined if not logged in
|
|
3
|
+
export const load: LayoutServerLoad = ({ locals }) => {
|
|
4
|
+
const { user } = locals // locals.user set by hooks.server.ts/handle(), undefined if not logged in
|
|
6
5
|
return {
|
|
7
6
|
user
|
|
8
7
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { error, json} from '@sveltejs/kit'
|
|
1
|
+
import { error, json } from '@sveltejs/kit'
|
|
2
2
|
import type { RequestHandler } from './$types'
|
|
3
|
-
import { query } from '
|
|
3
|
+
import { query } from '$lib/server/db'
|
|
4
4
|
|
|
5
5
|
export const PUT: RequestHandler = async event => {
|
|
6
6
|
const { user } = event.locals
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { error, json } from '@sveltejs/kit'
|
|
2
2
|
import type { RequestHandler } from './$types'
|
|
3
|
-
import { query } from '
|
|
3
|
+
import { query } from '$lib/server/db'
|
|
4
4
|
|
|
5
5
|
export const POST: RequestHandler = async (event) => {
|
|
6
6
|
const { slug } = event.params
|
|
@@ -11,8 +11,7 @@ export const POST: RequestHandler = async (event) => {
|
|
|
11
11
|
try {
|
|
12
12
|
switch (slug) {
|
|
13
13
|
case 'logout':
|
|
14
|
-
if (event.locals.user) {
|
|
15
|
-
// if user is null, they are logged out anyway (session might have ended)
|
|
14
|
+
if (event.locals.user) { // else they are logged out / session ended
|
|
16
15
|
sql = `CALL delete_session($1);`
|
|
17
16
|
result = await query(sql, [event.locals.user.id])
|
|
18
17
|
}
|
|
@@ -50,7 +49,7 @@ export const POST: RequestHandler = async (event) => {
|
|
|
50
49
|
// includes when a user tries to register an existing email account with wrong password
|
|
51
50
|
throw error(authenticationResult.statusCode, authenticationResult.status)
|
|
52
51
|
|
|
53
|
-
// Ensures hooks.ts:handle() will not delete cookie
|
|
52
|
+
// Ensures hooks.server.ts:handle() will not delete session cookie
|
|
54
53
|
event.locals.user = authenticationResult.user
|
|
55
54
|
|
|
56
55
|
return json(
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import type { RequestHandler } from './$types'
|
|
2
|
+
import { JWT_SECRET, DOMAIN } from '$env/static/private'
|
|
2
3
|
import type { Secret } from 'jsonwebtoken'
|
|
3
4
|
import jwt from 'jsonwebtoken'
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import { sendMessage } from '../../_send-in-blue'
|
|
7
|
-
|
|
8
|
-
dotenv.config()
|
|
9
|
-
const DOMAIN = process.env.DOMAIN
|
|
10
|
-
const JWT_SECRET = process.env.JWT_SECRET
|
|
5
|
+
import { query } from '$lib/server/db'
|
|
6
|
+
import { sendMessage } from '$lib/server/send-in-blue'
|
|
11
7
|
|
|
12
8
|
export const POST: RequestHandler = async event => {
|
|
13
9
|
const body = await event.request.json()
|
|
@@ -24,7 +20,6 @@ export const POST: RequestHandler = async event => {
|
|
|
24
20
|
|
|
25
21
|
// Email URL with token to user
|
|
26
22
|
const message: Message = {
|
|
27
|
-
// sender: JSON.parse(<string> VITE_EMAIL_FROM),
|
|
28
23
|
to: [{ email: body.email }],
|
|
29
24
|
subject: 'Password reset',
|
|
30
25
|
tags: ['account'],
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { error, json } from '@sveltejs/kit'
|
|
2
2
|
import type { RequestHandler } from './$types'
|
|
3
3
|
import { OAuth2Client } from 'google-auth-library'
|
|
4
|
-
import { query } from '
|
|
5
|
-
import {
|
|
4
|
+
import { query } from '$lib/server/db'
|
|
5
|
+
import { PUBLIC_GOOGLE_CLIENT_ID } from '$env/static/public'
|
|
6
6
|
|
|
7
7
|
// Verify JWT per https://developers.google.com/identity/gsi/web/guides/verify-google-id-token
|
|
8
8
|
async function getGoogleUserFromJWT(token: string): Promise<Partial<User>> {
|
|
9
9
|
try {
|
|
10
|
-
const
|
|
11
|
-
const client = new OAuth2Client(clientId)
|
|
10
|
+
const client = new OAuth2Client(PUBLIC_GOOGLE_CLIENT_ID)
|
|
12
11
|
const ticket = await client.verifyIdToken({
|
|
13
12
|
idToken: token,
|
|
14
|
-
audience:
|
|
13
|
+
audience: PUBLIC_GOOGLE_CLIENT_ID
|
|
15
14
|
});
|
|
16
15
|
const payload = ticket.getPayload()
|
|
17
16
|
if (!payload) throw error(500, 'Google authentication did not get the expected payload')
|
|
@@ -48,7 +47,7 @@ export const POST: RequestHandler = async event => {
|
|
|
48
47
|
const user = await getGoogleUserFromJWT(token)
|
|
49
48
|
const userSession = await upsertGoogleUser(user)
|
|
50
49
|
|
|
51
|
-
// Prevent hooks.ts's handler() from deleting cookie thinking no one has authenticated
|
|
50
|
+
// Prevent hooks.server.ts's handler() from deleting cookie thinking no one has authenticated
|
|
52
51
|
event.locals.user = userSession.user
|
|
53
52
|
|
|
54
53
|
return json({
|
|
@@ -1,36 +1,35 @@
|
|
|
1
|
-
import { json
|
|
2
|
-
import dotenv from 'dotenv'
|
|
1
|
+
import { json } from '@sveltejs/kit'
|
|
3
2
|
import type { RequestHandler } from './$types'
|
|
4
|
-
import type
|
|
3
|
+
import type { JwtPayload } from 'jsonwebtoken'
|
|
5
4
|
import jwt from 'jsonwebtoken'
|
|
6
|
-
import {
|
|
5
|
+
import { JWT_SECRET } from '$env/static/private'
|
|
6
|
+
import { query } from '$lib/server/db'
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
export const PUT: RequestHandler = async (event) => {
|
|
9
|
+
const body = await event.request.json()
|
|
10
|
+
const { token, password } = body
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
// Check the validity of the token and extract userId
|
|
13
|
+
try {
|
|
14
|
+
const decoded = <JwtPayload> jwt.verify(token, <jwt.Secret> JWT_SECRET)
|
|
15
|
+
const userId = decoded.subject
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
// Update the database with the new password
|
|
18
|
+
const sql = `CALL reset_password($1, $2);`
|
|
19
|
+
await query(sql, [userId, password])
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return json$1({
|
|
31
|
-
message: 'Password reset token expired.'
|
|
32
|
-
}, {
|
|
33
|
-
status: 403
|
|
34
|
-
})
|
|
35
|
-
}
|
|
21
|
+
return json({
|
|
22
|
+
message: 'Password successfully reset.'
|
|
23
|
+
})
|
|
24
|
+
} catch (error) {
|
|
25
|
+
// Technically, I should check error.message to make sure it's not a DB issue
|
|
26
|
+
return json(
|
|
27
|
+
{
|
|
28
|
+
message: 'Password reset token expired.'
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
status: 403
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
}
|
|
36
35
|
}
|
package/src/lib/config.ts
DELETED