sveltekit-auth-example 5.1.2 → 5.5.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.
package/CHANGELOG.md CHANGED
@@ -2,13 +2,41 @@
2
2
 
3
3
  - Add password complexity checking on /register and /profile pages (only checks for length currently despite what the pages say)
4
4
 
5
+ # 5.5.0
6
+
7
+ - Extract email templates into dedicated files under `src/lib/server/email/` (`password-reset.ts`, `verify-email.ts`)
8
+ - `src/lib/server/email/index.ts` re-exports all templates for clean imports
9
+
10
+ # 5.1.3
11
+
12
+ - Session timeout: automatically redirect to /login on session expiry via fetch interceptor (`src/lib/fetch-interceptor.ts`)
13
+ - Add database-level session cleanup on expiry
14
+ - Strengthen Content Security Policy (CSP) in svelte.config.js and vite.config.ts
15
+ - Harden auth endpoints (forgot password, Google OAuth, register, password reset)
16
+
5
17
  # 5.1.2
6
18
 
7
19
  - Dark mode
20
+ - Rate limiting on auth endpoints
21
+ - Email verification for new registrations
22
+ - Refactor state management: replace `stores.ts` with Svelte 5 `$state`-based `AppState` class (`src/lib/app-state.svelte.ts`)
23
+ - Split single `[slug]` auth handler into dedicated endpoints: login, register, google, logout, reset
24
+ - Add `auth-redirect.ts` helper for consistent post-auth redirects
25
+ - Password complexity enforcement on register and profile pages
26
+ - Add `/api/v1/user` profile update endpoint
27
+ - Service worker improvements
28
+ - Performance improvements in session lookup and DB queries
29
+ - Rename `.env.sample` to `.env.example`
8
30
 
9
31
  # 5.1.0
10
32
 
11
- - Convert to Tailwind CSS
33
+ - Convert to Tailwind CSS 4
34
+ - Remove Bootstrap dependency
35
+ - Modernize PostgreSQL schema and stored functions
36
+ - Add Playwright end-to-end test configuration
37
+ - Add MCP configuration (`.vscode/mcp.json`) for Svelte MCP server
38
+ - Add `AGENTS.md` coding agent instructions
39
+ - Bump dependencies
12
40
 
13
41
  # 5.0.4
14
42
 
package/README.md CHANGED
@@ -1,68 +1,61 @@
1
1
  # SvelteKit Authentication and Authorization Example
2
2
 
3
- **Updated for Svelte 5 and SvelteKit 2.19**
3
+ [![License](https://img.shields.io/github/license/nstuyvesant/sveltekit-auth-example)](https://github.com/nstuyvesant/sveltekit-auth-example/blob/master/LICENSE)
4
+ [![Node](https://img.shields.io/node/v/sveltekit-auth-example)](https://nodejs.org)
5
+ [![Svelte](https://img.shields.io/badge/Svelte-5-orange)](https://svelte.dev)
4
6
 
5
- This is an example of how to register, authenticate, and update users and limit their access to areas of the website by role (admin, teacher, student). It includes profile management and password resets via SendGrid.
7
+ A complete, production-ready authentication and authorization starter for **Svelte 5** and **SvelteKit 2**. Skip the boilerplate get secure local accounts, Google OAuth, MFA, email verification, role-based access control, and OWASP-compliant password hashing out of the box.
6
8
 
7
- It's a Single Page App (SPA) built with SvelteKit and a PostgreSQL database back-end. Code is TypeScript and the website is styled using Tailwind CSS. PostgreSQL functions handle password hashing and UUID generation for the session ID. Unlike most authentication examples, this SPA does not use callbacks that redirect back to the site (causing the website to be reloaded with a visual flash).
9
+ ## Features
8
10
 
9
- The project includes a Content Security Policy (CSP) in svelte.config.js.
11
+ | | |
12
+ |---|---|
13
+ | ✅ Local accounts (email + password) | ✅ Sign in with Google / Google One Tap |
14
+ | ✅ Multi-factor authentication (MFA) | ✅ Email verification |
15
+ | ✅ Forgot password / email reset (SendGrid) | ✅ User profile management |
16
+ | ✅ Session management + timeout | ✅ Rate limiting |
17
+ | ✅ Role-based access control | ✅ Password complexity enforcement |
18
+ | ✅ Content Security Policy (CSP) | ✅ OWASP-compliant password hashing |
10
19
 
11
- The website supports two types of authentication:
20
+ ## Stack
12
21
 
13
- 1. **Local accounts** via username (email) and password
14
- - The login form (/src/routes/login/+page.svelte) sends the login info as JSON to endpoint /auth/login
15
- - The endpoint passes the JSON to PostgreSQL function authenticate(json) which hashes the password and compares it to the stored hashed password in the users table. The function returns JSON containing a session ID (v4 UUID) and user object (sans password).
16
- - The endpoint sends this session ID as an httpOnly SameSite cookie and the user object in the body of the response.
17
- - The client stores the user object in the loginSession store.
18
- - Further requests to the server include the session cookie. The hooks.ts handle() method extracts the session cookie, looks up the user and attaches it to RequestEvent.locals so server-side code can check locals.user.role to see if the request is authorized and return an HTTP 401 status if not.
19
- 2. **Sign in with Google**
20
- - **Sign in with Google** is initialized in /src/routes/+layout.svelte.
21
- - **Google One Tap** prompt is displayed on the initially loaded page unless [Intelligent Tracking Prevention is enabled in the browser](https://developers.google.com/identity/gsi/web/guides/features#upgraded_ux_on_itp_browsers).
22
- - **Sign in with Google** button is on the login page (/src/routes/login/+page.svelte) and register page (/src/routes/register/+page.svelte).
23
- - Clicking either button opens a new window asking the user to authorize this website. If the user OKs it, a JSON Web Token (JWT) is sent to a callback function.
24
- - The callback function (in /src/lib/auth.ts) sends the JWT to an endpoint on this server /auth/google.
25
- - The endpoint decodes and validates the user information then calls the PostgreSQL function start_gmail_user_session to upsert the user to the database returing a session id in an httpOnly SameSite cookie and user in the body of the response.
26
- - The client stores the user object in the loginSession store.
27
- - Further requests to the server work identically to local accounts above.
28
-
29
- > There is some overhead to checking the user session in a database each time versus using a JWT; however, validating each request avoids problems discussed in [this article](https://redis.com/blog/json-web-tokens-jwt-are-dangerous-for-user-sessions/) and [this one](https://scotch.io/bar-talk/why-jwts-suck-as-session-tokens). For a high-volume website, I would use Redis or the equivalent.
22
+ - **[SvelteKit](https://kit.svelte.dev)** Single Page Application
23
+ - **[PostgreSQL 16+](https://www.postgresql.org)** database with server-side hashing
24
+ - **[Tailwind CSS 4](https://tailwindcss.com)** styling
25
+ - **TypeScript** end-to-end type safety
30
26
 
31
- The forgot password / password reset functionality uses a JWT and [**SendGrid**](https://www.sendgrid.com) to send the email. You would need to have a **SendGrid** account and set two 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 **SendGrid** (used their API in another project).
27
+ ## Requirements
32
28
 
33
- ## Prerequisites
34
-
35
- - PostgreSQL 14.10 or higher
36
- - Node.js 18.19.1 or higher
37
- - Google API client
38
- - Twilio SendGrid account (only used for emailing password reset link - the sample can run without it but forgot password will not work)
29
+ - Node.js 24.14.0 or later
30
+ - PostgreSQL 16 or later
31
+ - A [SendGrid](https://sendgrid.com) account (for password reset emails)
32
+ - A [Google API client ID](https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid) (for Sign in with Google)
39
33
 
40
34
  ## Setting up the project
41
35
 
42
- Here are the steps:
43
-
44
- 1. Get the project and setup the database
36
+ 1. Get the project and set up the database
45
37
 
46
38
  ```bash
47
39
  # Clone the repo to your current directory
48
40
  git clone https://github.com/nstuyvesant/sveltekit-auth-example.git
49
41
 
50
42
  # Install the dependencies
51
- cd /sveltekit-auth-example
43
+ cd sveltekit-auth-example
52
44
  yarn install
53
45
 
54
- # Create PostgreSQL database (only works if you installed PostgreSQL)
46
+ # Create PostgreSQL database (only works if you have PostgreSQL installed)
55
47
  psql -d postgres -f db_create.sql
56
48
  ```
57
49
 
58
- 2. Create a **Google API client ID** per [these instructions](https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid). Make sure you include `http://localhost:3000`, `http://localhost` in the Authorized JavaScript origins and `http://localhost:3000/auth/google/callback` in the Authorized redirect URIs for your Client ID for Web application. ** Do not access the site using http://127.0.0.1:3000 ** - use `http://localhost:3000` or it will not work.
50
+ 2. Create a **Google API client ID** per [these instructions](https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid). Make sure you include `http://localhost:3000` and `http://localhost` in the Authorized JavaScript origins, and `http://localhost:3000/auth/google/callback` in the Authorized redirect URIs for your Client ID for Web application. **Do not access the site using http://127.0.0.1:3000** use `http://localhost:3000` or it will not work.
59
51
 
60
52
  3. [Create a free Twilio SendGrid account](https://signup.sendgrid.com) and generate an API Key following [this documentation](https://docs.sendgrid.com/ui/account-and-settings/api-keys) and add a sender as documented [here](https://docs.sendgrid.com/ui/sending-email/senders).
61
53
 
62
- 4. Create an **.env** file at the top level of the project with the following values (substituting your own id and PostgreSQL username and password):
54
+ 4. Create a **.env** file at the top level of the project with the following values (substituting your own id and PostgreSQL username and password):
63
55
 
64
56
  ```bash
65
57
  DATABASE_URL=postgres://user:password@localhost:5432/auth
58
+ DATABASE_SSL=false
66
59
  DOMAIN=http://localhost:3000
67
60
  JWT_SECRET=replace_with_your_own
68
61
  SENDGRID_KEY=replace_with_your_own
@@ -73,14 +66,50 @@ PUBLIC_GOOGLE_CLIENT_ID=replace_with_your_own
73
66
  ## Run locally
74
67
 
75
68
  ```bash
76
- # Start the server and open the app in a new browser tab
69
+ # Start the dev server and open the app in a new browser tab
77
70
  yarn dev -- --open
78
71
  ```
79
72
 
73
+ ## Build and preview
74
+
75
+ ```bash
76
+ # Build for production
77
+ yarn build
78
+
79
+ # Preview the production build
80
+ yarn preview
81
+ ```
82
+
80
83
  ## Valid logins
81
84
 
82
85
  The db_create.sql script adds three users to the database with obvious roles:
83
86
 
84
- - admin@example.com password admin123
85
- - teacher@example.com password teacher123
86
- - student@example.com password student123
87
+ | Email | Password | Role |
88
+ |---|---|---|
89
+ | admin@example.com | admin123 | admin |
90
+ | teacher@example.com | teacher123 | teacher |
91
+ | student@example.com | student123 | student |
92
+
93
+ ## How it works
94
+
95
+ The website supports two types of authentication:
96
+
97
+ 1. **Local accounts** via username (email) and password
98
+ - The login form (/src/routes/login/+page.svelte) sends the login info as JSON to endpoint /auth/login
99
+ - The endpoint passes the JSON to PostgreSQL function authenticate(json) which hashes the password and compares it to the stored hashed password in the users table. The function returns JSON containing a session ID (v4 UUID) and user object (sans password).
100
+ - The endpoint sends this session ID as an httpOnly SameSite cookie and the user object in the body of the response.
101
+ - The client stores the user object in `appState` (see /src/lib/app-state.svelte.ts).
102
+ - Further requests to the server include the session cookie. The hooks.ts handle() method extracts the session cookie, looks up the user and attaches it to RequestEvent.locals so server-side code can check locals.user.role to see if the request is authorized and return an HTTP 401 status if not.
103
+ 2. **Sign in with Google**
104
+ - **Sign in with Google** is initialized in /src/routes/+layout.svelte.
105
+ - **Google One Tap** prompt is displayed on the initially loaded page unless [Intelligent Tracking Prevention is enabled in the browser](https://developers.google.com/identity/gsi/web/guides/features#upgraded_ux_on_itp_browsers).
106
+ - **Sign in with Google** button is on the login page (/src/routes/login/+page.svelte) and register page (/src/routes/register/+page.svelte).
107
+ - Clicking either button opens a new window asking the user to authorize this website. If the user OKs it, a JSON Web Token (JWT) is sent to a callback function.
108
+ - The callback function (in /src/lib/google.ts) sends the JWT to an endpoint on this server /auth/google.
109
+ - The endpoint decodes and validates the user information then calls the PostgreSQL function start_gmail_user_session to upsert the user to the database returning a session id in an httpOnly SameSite cookie and user in the body of the response.
110
+ - The client stores the user object in `appState` (see /src/lib/app-state.svelte.ts).
111
+ - Further requests to the server work identically to local accounts above.
112
+
113
+ > There is some overhead to checking the user session in a database each time versus using a JWT; however, validating each request avoids problems discussed in [this article](https://redis.com/blog/json-web-tokens-jwt-are-dangerous-for-user-sessions/). For a high-volume website, I would use Redis or the equivalent.
114
+
115
+ The forgot password / password reset functionality uses a JWT and [**SendGrid**](https://www.sendgrid.com) to send the email. You would need to have a **SendGrid** account and set two environmental variables. Email sending is in /src/routes/auth/forgot/+server.ts. This code could easily be replaced by nodemailer or something similar. Note: I have no affiliation with **SendGrid** (used their API in another project).
package/db_create.sql CHANGED
@@ -189,6 +189,43 @@ $BODY$;
189
189
 
190
190
  ALTER FUNCTION public.get_session(uuid) OWNER TO auth;
191
191
 
192
+ -- Like get_session but also bumps the expiry (sliding sessions).
193
+ -- Returns NULL if the session is expired or does not exist.
194
+ CREATE OR REPLACE FUNCTION public.get_and_update_session(input_session_id uuid)
195
+ RETURNS json
196
+ LANGUAGE 'plpgsql'
197
+ AS $BODY$
198
+ DECLARE
199
+ result json;
200
+ BEGIN
201
+ UPDATE sessions
202
+ SET expires = CURRENT_TIMESTAMP + INTERVAL '2 hours'
203
+ WHERE id = input_session_id AND expires > CURRENT_TIMESTAMP;
204
+
205
+ IF NOT FOUND THEN
206
+ RETURN NULL;
207
+ END IF;
208
+
209
+ SELECT json_build_object(
210
+ 'id', sessions.user_id,
211
+ 'role', users.role,
212
+ 'email', users.email,
213
+ 'firstName', users.first_name,
214
+ 'lastName', users.last_name,
215
+ 'phone', users.phone,
216
+ 'optOut', users.opt_out,
217
+ 'expires', sessions.expires
218
+ ) INTO result
219
+ FROM sessions
220
+ INNER JOIN users ON sessions.user_id = users.id
221
+ WHERE sessions.id = input_session_id;
222
+
223
+ RETURN result;
224
+ END;
225
+ $BODY$;
226
+
227
+ ALTER FUNCTION public.get_and_update_session(uuid) OWNER TO auth;
228
+
192
229
  CREATE OR REPLACE FUNCTION public.verify_email_and_create_session(input_id integer)
193
230
  RETURNS uuid
194
231
  LANGUAGE 'plpgsql'
@@ -224,7 +261,7 @@ BEGIN
224
261
  PERFORM id FROM users WHERE email = input_email;
225
262
  IF NOT FOUND THEN
226
263
  INSERT INTO users(role, password, email, first_name, last_name, phone)
227
- VALUES('student', crypt(input_password, gen_salt('bf', 8)), input_email, input_first_name, input_last_name, input_phone)
264
+ VALUES('student', crypt(input_password, gen_salt('bf', 12)), input_email, input_first_name, input_last_name, input_phone)
228
265
  RETURNING
229
266
  json_build_object(
230
267
  'sessionId', create_session(users.id),
@@ -286,7 +323,7 @@ CREATE OR REPLACE PROCEDURE public.reset_password(IN input_id integer, IN input_
286
323
  LANGUAGE plpgsql
287
324
  AS $procedure$
288
325
  BEGIN
289
- UPDATE users SET password = crypt(input_password, gen_salt('bf', 8)) WHERE id = input_id;
326
+ UPDATE users SET password = crypt(input_password, gen_salt('bf', 12)) WHERE id = input_id;
290
327
  END;
291
328
  $procedure$
292
329
  ;
@@ -308,7 +345,7 @@ BEGIN
308
345
  IF input_id = 0 THEN
309
346
  INSERT INTO users (role, email, password, first_name, last_name, phone, email_verified)
310
347
  VALUES (
311
- input_role, input_email, crypt(input_password, gen_salt('bf', 8)),
348
+ input_role, input_email, crypt(input_password, gen_salt('bf', 12)),
312
349
  input_first_name, input_last_name, input_phone, true);
313
350
  ELSE
314
351
  UPDATE users SET
@@ -317,7 +354,7 @@ BEGIN
317
354
  email_verified = true,
318
355
  password = CASE WHEN input_password = ''
319
356
  THEN password -- leave as is (we are updating fields other than the password)
320
- ELSE crypt(input_password, gen_salt('bf', 8))
357
+ ELSE crypt(input_password, gen_salt('bf', 12))
321
358
  END,
322
359
  first_name = input_first_name,
323
360
  last_name = input_last_name,
@@ -343,7 +380,7 @@ BEGIN
343
380
  email = input_email,
344
381
  password = CASE WHEN input_password = ''
345
382
  THEN password -- leave as is (we are updating fields other than the password)
346
- ELSE crypt(input_password, gen_salt('bf', 8))
383
+ ELSE crypt(input_password, gen_salt('bf', 12))
347
384
  END,
348
385
  first_name = input_first_name,
349
386
  last_name = input_last_name,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sveltekit-auth-example",
3
3
  "description": "SvelteKit Authentication Example",
4
- "version": "5.1.2",
4
+ "version": "5.5.0",
5
5
  "author": "Nate Stuyvesant",
6
6
  "license": "https://github.com/nstuyvesant/sveltekit-auth-example/blob/master/LICENSE",
7
7
  "repository": {
@@ -37,6 +37,7 @@
37
37
  "dependencies": {
38
38
  "@sendgrid/mail": "^8.1.6",
39
39
  "google-auth-library": "^10.6.1",
40
+ "jsonwebtoken": "^9.0.3",
40
41
  "pg": "^8.20.0"
41
42
  },
42
43
  "devDependencies": {
@@ -55,7 +56,6 @@
55
56
  "eslint-config-prettier": "^10.1.8",
56
57
  "eslint-plugin-svelte": "^3.15.2",
57
58
  "globals": "^17.4.0",
58
- "jsonwebtoken": "^9.0.3",
59
59
  "playwright": "^1.58.2",
60
60
  "prettier": "^3.8.1",
61
61
  "prettier-plugin-sql": "^0.19.2",
@@ -31,8 +31,12 @@ setInterval(() => {
31
31
 
32
32
  // Attach authorization to each server request (role may have changed)
33
33
  async function attachUserToRequestEvent(sessionId: string, event: RequestEvent) {
34
- const result = await query('SELECT * FROM get_session($1::uuid)', [sessionId], 'get-session')
35
- event.locals.user = result.rows[0]?.get_session // undefined if not found
34
+ const result = await query(
35
+ 'SELECT get_and_update_session($1::uuid)',
36
+ [sessionId],
37
+ 'get-and-update-session'
38
+ )
39
+ event.locals.user = result.rows[0]?.get_and_update_session // undefined if not found
36
40
  }
37
41
 
38
42
  // Invoked for each endpoint called and initially for SSR router
@@ -8,7 +8,7 @@ import { page } from '$app/state'
8
8
  export function redirectAfterLogin(user: User): void {
9
9
  if (!user) return
10
10
  const referrer = page.url.searchParams.get('referrer')
11
- if (referrer) {
11
+ if (referrer && referrer.startsWith('/') && !referrer.startsWith('//')) {
12
12
  goto(referrer)
13
13
  return
14
14
  }
@@ -0,0 +1,23 @@
1
+ import { goto } from '$app/navigation'
2
+ import { page } from '$app/state'
3
+ import { appState } from '$lib/app-state.svelte'
4
+
5
+ /**
6
+ * Monkey-patches window.fetch to intercept 401 responses.
7
+ * When a 401 is received while a user is logged in, the session has expired:
8
+ * clear app state and redirect to /login with the current path as the referrer.
9
+ *
10
+ * Call once from +layout.svelte's onMount.
11
+ */
12
+ export function setupFetchInterceptor() {
13
+ const originalFetch = window.fetch
14
+ window.fetch = async (...args) => {
15
+ const response = await originalFetch(...args)
16
+ if (response.status === 401 && appState.user) {
17
+ appState.user = undefined
18
+ const referrer = encodeURIComponent(page.url.pathname + page.url.search)
19
+ goto(`/login?referrer=${referrer}`)
20
+ }
21
+ return response
22
+ }
23
+ }
@@ -0,0 +1,2 @@
1
+ export { sendPasswordResetEmail } from './password-reset'
2
+ export { sendVerificationEmail } from './verify-email'
@@ -0,0 +1,15 @@
1
+ import { DOMAIN, SENDGRID_SENDER } from '$env/static/private'
2
+ import { sendMessage } from '$lib/server/sendgrid'
3
+
4
+ export const sendPasswordResetEmail = async (toEmail: string, token: string) => {
5
+ await sendMessage({
6
+ to: { email: toEmail },
7
+ from: SENDGRID_SENDER,
8
+ subject: 'Password reset',
9
+ categories: ['account'],
10
+ html: `
11
+ <p><a href="${DOMAIN}/auth/reset/${token}">Reset my password</a>. Your browser will open and ask you to
12
+ provide a new password with a confirmation then redirect you to your login page.</p>
13
+ `
14
+ })
15
+ }
@@ -0,0 +1,16 @@
1
+ import { DOMAIN, SENDGRID_SENDER } from '$env/static/private'
2
+ import { sendMessage } from '$lib/server/sendgrid'
3
+
4
+ export const sendVerificationEmail = async (toEmail: string, token: string) => {
5
+ await sendMessage({
6
+ to: { email: toEmail },
7
+ from: SENDGRID_SENDER,
8
+ subject: 'Verify your email address',
9
+ categories: ['account'],
10
+ html: `
11
+ <p>Thanks for registering! Please verify your email address by clicking the link below:</p>
12
+ <p><a href="${DOMAIN}/auth/verify/${token}">Verify my email address</a></p>
13
+ <p>This link expires in 24 hours. If you did not register, you can safely ignore this email.</p>
14
+ `
15
+ })
16
+ }
@@ -4,6 +4,7 @@
4
4
  import { goto, beforeNavigate } from '$app/navigation'
5
5
  import { appState } from '$lib/app-state.svelte'
6
6
  import { initializeGoogleAccounts } from '$lib/google'
7
+ import { setupFetchInterceptor } from '$lib/fetch-interceptor'
7
8
 
8
9
  import './layout.css'
9
10
 
@@ -39,6 +40,7 @@
39
40
  })
40
41
 
41
42
  onMount(() => {
43
+ setupFetchInterceptor()
42
44
  initializeGoogleAccounts()
43
45
  if (!appState.user) google.accounts.id.prompt()
44
46
  })
@@ -1,10 +1,9 @@
1
1
  import type { Secret } from 'jsonwebtoken'
2
- import type { MailDataRequired } from '@sendgrid/mail'
3
2
  import type { RequestHandler } from './$types'
4
3
  import jwt from 'jsonwebtoken'
5
- import { JWT_SECRET, DOMAIN, SENDGRID_SENDER } from '$env/static/private'
4
+ import { JWT_SECRET } from '$env/static/private'
6
5
  import { query } from '$lib/server/db'
7
- import { sendMessage } from '$lib/server/sendgrid'
6
+ import { sendPasswordResetEmail } from '$lib/server/email'
8
7
 
9
8
  export const POST: RequestHandler = async event => {
10
9
  const body = await event.request.json()
@@ -12,26 +11,13 @@ export const POST: RequestHandler = async event => {
12
11
  const { rows } = await query(sql, [body.email])
13
12
 
14
13
  if (rows.length > 0) {
15
- const { userId } = rows[0]
16
- // Create JWT with userId expiring in 30 mins
17
- const secret = JWT_SECRET
18
- const token = jwt.sign({ subject: userId }, <Secret>secret, {
19
- expiresIn: '30m'
20
- })
21
-
22
- // Email URL with token to user
23
- const message: MailDataRequired = {
24
- to: { email: body.email },
25
- from: SENDGRID_SENDER,
26
- subject: 'Password reset',
27
- categories: ['account'],
28
- html: `
29
- <a href="${DOMAIN}/auth/reset/${token}">Reset my password</a>. Your browser will open and ask you to provide a
30
- new password with a confirmation then redirect you to your login page.
31
- `
32
- }
14
+ const token = jwt.sign(
15
+ { subject: rows[0].userId, purpose: 'reset-password' },
16
+ JWT_SECRET as Secret,
17
+ { expiresIn: '30m' }
18
+ )
33
19
  try {
34
- await sendMessage(message)
20
+ await sendPasswordResetEmail(body.email, token)
35
21
  } catch (err) {
36
22
  console.error('Failed to send password reset email:', err)
37
23
  // Still return 204 to avoid leaking whether the email exists in our system
@@ -54,9 +54,7 @@ export const POST: RequestHandler = async event => {
54
54
 
55
55
  cookies.set('session', userSession.id, { httpOnly: true, sameSite: 'lax', secure: true, path: '/' })
56
56
  return json({ message: 'Successful Google Sign-In.', user: userSession.user })
57
- } catch (err) {
58
- let message = ''
59
- if (err instanceof Error) message = err.message
60
- error(401, message)
57
+ } catch {
58
+ error(401, 'Google authentication failed.')
61
59
  }
62
60
  }
@@ -1,10 +1,9 @@
1
1
  import { error, json } from '@sveltejs/kit'
2
2
  import type { RequestHandler } from './$types'
3
- import type { MailDataRequired } from '@sendgrid/mail'
4
3
  import jwt from 'jsonwebtoken'
5
- import { JWT_SECRET, DOMAIN, SENDGRID_SENDER } from '$env/static/private'
4
+ import { JWT_SECRET } from '$env/static/private'
6
5
  import { query } from '$lib/server/db'
7
- import { sendMessage } from '$lib/server/sendgrid'
6
+ import { sendVerificationEmail } from '$lib/server/email'
8
7
 
9
8
  export const POST: RequestHandler = async event => {
10
9
  let body: { email?: string; password?: string; firstName?: string; lastName?: string }
@@ -17,6 +16,10 @@ export const POST: RequestHandler = async event => {
17
16
  if (!body.email || !body.password || !body.firstName || !body.lastName)
18
17
  error(400, 'Please supply all required fields: email, password, first and last name.')
19
18
 
19
+ const passwordPattern = /(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9]).{8,}/
20
+ if (!passwordPattern.test(body.password))
21
+ error(400, 'Password must be at least 8 characters and include an uppercase letter, a number, and a special character.')
22
+
20
23
  let result
21
24
  try {
22
25
  const sql = `SELECT register($1) AS "authenticationResult";`
@@ -43,19 +46,7 @@ export const POST: RequestHandler = async event => {
43
46
  { expiresIn: '24h' }
44
47
  )
45
48
 
46
- const message: MailDataRequired = {
47
- to: { email: body.email },
48
- from: SENDGRID_SENDER,
49
- subject: 'Verify your email address',
50
- categories: ['account'],
51
- html: `
52
- <p>Thanks for registering! Please verify your email address by clicking the link below:</p>
53
- <p><a href="${DOMAIN}/auth/verify/${token}">Verify my email address</a></p>
54
- <p>This link expires in 24 hours. If you did not register, you can safely ignore this email.</p>
55
- `
56
- }
57
-
58
- await sendMessage(message)
49
+ await sendVerificationEmail(body.email, token)
59
50
 
60
51
  return json({
61
52
  message: 'Registration successful. Please check your email to verify your account.',
@@ -12,6 +12,7 @@ export const PUT: RequestHandler = async event => {
12
12
  // Check the validity of the token and extract userId
13
13
  try {
14
14
  const decoded = <JwtPayload>jwt.verify(token, <jwt.Secret>JWT_SECRET)
15
+ if (decoded.purpose !== 'reset-password') throw new Error('Invalid token purpose.')
15
16
  const userId = decoded.subject
16
17
 
17
18
  // Update the database with the new password
package/svelte.config.js CHANGED
@@ -37,7 +37,7 @@ const config = {
37
37
  mode: 'auto',
38
38
  directives: {
39
39
  'default-src': [...baseCsp],
40
- 'script-src': ['unsafe-inline', ...baseCsp],
40
+ 'script-src': [...baseCsp],
41
41
  'img-src': ['data:', 'blob:', ...baseCsp],
42
42
  'style-src': ['unsafe-inline', ...baseCsp],
43
43
  'object-src': ['none'],
package/vite.config.ts CHANGED
@@ -4,7 +4,7 @@ import tailwindcss from '@tailwindcss/vite'
4
4
 
5
5
  export default defineConfig({
6
6
  build: {
7
- sourcemap: true
7
+ sourcemap: process.env.NODE_ENV !== 'production'
8
8
  },
9
9
  plugins: [sveltekit(), tailwindcss()],
10
10
  test: {