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 +29 -1
- package/README.md +69 -40
- package/db_create.sql +42 -5
- package/package.json +2 -2
- package/src/hooks.server.ts +6 -2
- package/src/lib/auth-redirect.ts +1 -1
- package/src/lib/fetch-interceptor.ts +23 -0
- package/src/lib/server/email/index.ts +2 -0
- package/src/lib/server/email/password-reset.ts +15 -0
- package/src/lib/server/email/verify-email.ts +16 -0
- package/src/routes/+layout.svelte +2 -0
- package/src/routes/auth/forgot/+server.ts +8 -22
- package/src/routes/auth/google/+server.ts +2 -4
- package/src/routes/auth/register/+server.ts +7 -16
- package/src/routes/auth/reset/+server.ts +1 -0
- package/svelte.config.js +1 -1
- package/vite.config.ts +1 -1
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
|
-
|
|
3
|
+
[](https://github.com/nstuyvesant/sveltekit-auth-example/blob/master/LICENSE)
|
|
4
|
+
[](https://nodejs.org)
|
|
5
|
+
[](https://svelte.dev)
|
|
4
6
|
|
|
5
|
-
|
|
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
|
-
|
|
9
|
+
## Features
|
|
8
10
|
|
|
9
|
-
|
|
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
|
-
|
|
20
|
+
## Stack
|
|
12
21
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
27
|
+
## Requirements
|
|
32
28
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
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
|
-
|
|
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
|
|
43
|
+
cd sveltekit-auth-example
|
|
52
44
|
yarn install
|
|
53
45
|
|
|
54
|
-
# Create PostgreSQL database (only works if you
|
|
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
|
|
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
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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.
|
|
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",
|
package/src/hooks.server.ts
CHANGED
|
@@ -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(
|
|
35
|
-
|
|
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
|
package/src/lib/auth-redirect.ts
CHANGED
|
@@ -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,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
|
|
4
|
+
import { JWT_SECRET } from '$env/static/private'
|
|
6
5
|
import { query } from '$lib/server/db'
|
|
7
|
-
import {
|
|
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
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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
|
|
58
|
-
|
|
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
|
|
4
|
+
import { JWT_SECRET } from '$env/static/private'
|
|
6
5
|
import { query } from '$lib/server/db'
|
|
7
|
-
import {
|
|
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
|
-
|
|
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': [
|
|
40
|
+
'script-src': [...baseCsp],
|
|
41
41
|
'img-src': ['data:', 'blob:', ...baseCsp],
|
|
42
42
|
'style-src': ['unsafe-inline', ...baseCsp],
|
|
43
43
|
'object-src': ['none'],
|