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 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.7.0 or higher
33
- - npm 8.18.0 or higher
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
- VITE_GOOGLE_CLIENT_ID=replace_with_your_own
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.20",
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.36.1",
55
- "@typescript-eslint/parser": "^5.36.1",
49
+ "@typescript-eslint/eslint-plugin": "^5.37.0",
50
+ "@typescript-eslint/parser": "^5.37.0",
56
51
  "bootstrap": "^5.2.1",
57
- "bootstrap-icons": "^1.9.1",
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
- // interface PrivateEnv {} // $env/dynamic/private
17
-
18
- // interface PublicEnv {} // $env/dynamic/public
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
@@ -1,5 +1,5 @@
1
1
  import type { Handle, RequestEvent } from '@sveltejs/kit'
2
- import { query } from './routes/_db'
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 { config } from '$lib/config'
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: config.googleClientId, callback: googleCallback })
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 needs to ignore user.role and always set it to 'student'
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: process.env.DATABASE_URL,
8
+ connectionString: DATABASE_URL,
11
9
  ssl: {
12
10
  rejectUnauthorized: false
13
11
  }
@@ -1,11 +1,7 @@
1
- import dotenv from 'dotenv'
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
- dotenv.config()
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 = (event) => {
4
- const locals = event.locals
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
  }
@@ -9,6 +9,6 @@ export const load: PageServerLoad = async ({locals})=> {
9
9
  }
10
10
 
11
11
  return {
12
- message: 'Admin-only content from endpoint.'
12
+ message: 'Admin-only content from server.'
13
13
  }
14
14
  }
@@ -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 '../../../_db'
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 '../../_db'
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 just set
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 dotenv from 'dotenv'
5
- import { query } from '../../_db'
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 '../../_db';
5
- import { config } from '$lib/config'
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 clientId = config.googleClientId
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: clientId
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 as json$1 } from '@sveltejs/kit';
2
- import dotenv from 'dotenv'
1
+ import { json } from '@sveltejs/kit'
3
2
  import type { RequestHandler } from './$types'
4
- import type { JwtPayload } from 'jsonwebtoken'
3
+ import type { JwtPayload } from 'jsonwebtoken'
5
4
  import jwt from 'jsonwebtoken'
6
- import { query } from '../../_db'
5
+ import { JWT_SECRET } from '$env/static/private'
6
+ import { query } from '$lib/server/db'
7
7
 
8
- dotenv.config()
8
+ export const PUT: RequestHandler = async (event) => {
9
+ const body = await event.request.json()
10
+ const { token, password } = body
9
11
 
10
- const JWT_SECRET = <jwt.Secret> process.env.JWT_SECRET
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
- export const PUT: RequestHandler = async event => {
13
- const body = await event.request.json()
14
- const { token, password } = body
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
- // Check the validity of the token and extract userId
17
- try {
18
- const decoded = <JwtPayload> jwt.verify(token, JWT_SECRET)
19
- const userId = decoded.subject
20
-
21
- // Update the database with the new password
22
- const sql = `CALL reset_password($1, $2);`
23
- await query(sql, [userId, password])
24
-
25
- return json$1({
26
- message: 'Password successfully reset.'
27
- })
28
- } catch (error) {
29
- // Technically, I should check error.message to make sure it's not a DB issue
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
  }
@@ -52,7 +52,6 @@
52
52
  initializeSignInWithGoogle('googleButton')
53
53
  })
54
54
 
55
-
56
55
  const passwordMatch = () => {
57
56
  if (!user) return false // placate TypeScript
58
57
  if (!user.password) user.password = ''
@@ -9,6 +9,6 @@ export const load: PageServerLoad = async ({locals}) => {
9
9
  }
10
10
 
11
11
  return {
12
- message: 'Teachers or Admin-only content from endpoint.'
12
+ message: 'Teachers or Admin-only content from server.'
13
13
  }
14
14
  }
package/src/lib/config.ts DELETED
@@ -1,4 +0,0 @@
1
- // Only include data that is not sensitive
2
- export const config = {
3
- googleClientId: import.meta.env.VITE_GOOGLE_CLIENT_ID
4
- }