sveltekit-auth-example 5.0.3 → 5.1.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/.editorconfig +9 -3
- package/.env.sample +7 -0
- package/.prettierignore +9 -0
- package/.vscode/mcp.json +13 -0
- package/.vscode/settings.json +7 -5
- package/.yarn/releases/yarn-4.13.0.cjs +940 -0
- package/.yarnrc.yml +2 -0
- package/AGENTS.md +23 -0
- package/CHANGELOG.md +11 -0
- package/README.md +2 -3
- package/db_create.sql +53 -24
- package/eslint.config.mjs +48 -0
- package/package.json +37 -34
- package/playwright.config.ts +24 -0
- package/prettier.config.mjs +14 -5
- package/src/app.html +1 -1
- package/src/hooks.server.ts +8 -4
- package/src/lib/server/db.ts +62 -10
- package/src/routes/+error.svelte +3 -3
- package/src/routes/+layout.svelte +76 -125
- package/src/routes/auth/reset/[token]/+page.svelte +69 -61
- package/src/routes/forgot/+page.svelte +40 -38
- package/src/routes/layout.css +17 -0
- package/src/routes/login/+page.svelte +63 -98
- package/src/routes/profile/+page.svelte +123 -111
- package/src/routes/register/+page.svelte +124 -113
- package/svelte.config.js +13 -1
- package/tsconfig.json +3 -1
- package/vite.config.ts +8 -1
- package/.eslintignore +0 -14
- package/.git-blame-ignore-revs +0 -2
- package/eslint.config.js +0 -16
package/.yarnrc.yml
CHANGED
package/AGENTS.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively:
|
|
2
|
+
|
|
3
|
+
## Available MCP Tools:
|
|
4
|
+
|
|
5
|
+
### 1. list-sections
|
|
6
|
+
|
|
7
|
+
Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths.
|
|
8
|
+
When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections.
|
|
9
|
+
|
|
10
|
+
### 2. get-documentation
|
|
11
|
+
|
|
12
|
+
Retrieves full documentation content for specific sections. Accepts single or multiple sections.
|
|
13
|
+
After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task.
|
|
14
|
+
|
|
15
|
+
### 3. svelte-autofixer
|
|
16
|
+
|
|
17
|
+
Analyzes Svelte code and returns issues and suggestions.
|
|
18
|
+
You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned.
|
|
19
|
+
|
|
20
|
+
### 4. playground-link
|
|
21
|
+
|
|
22
|
+
Generates a Svelte Playground link with the provided code.
|
|
23
|
+
After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,13 +2,24 @@
|
|
|
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.1.0
|
|
6
|
+
|
|
7
|
+
- Convert to Tailwind CSS
|
|
8
|
+
|
|
9
|
+
# 5.0.4
|
|
10
|
+
|
|
11
|
+
- Bump dependencies
|
|
12
|
+
|
|
5
13
|
# 5.0.3
|
|
14
|
+
|
|
6
15
|
- Bump dependencies
|
|
7
16
|
|
|
8
17
|
# 5.0.2
|
|
18
|
+
|
|
9
19
|
- Bump dependencies
|
|
10
20
|
|
|
11
21
|
# 5.0.1
|
|
22
|
+
|
|
12
23
|
- Migrate to Svelte 5 runes mode
|
|
13
24
|
- Format with prettier
|
|
14
25
|
- Bump dependencies
|
package/README.md
CHANGED
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
**Updated for Svelte 5 and SvelteKit 2.19**
|
|
4
4
|
|
|
5
|
-
This is an example of how to register, authenticate, and update users and limit their access to
|
|
6
|
-
areas of the website by role (admin, teacher, student). It includes profile management and password resets via SendGrid.
|
|
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
6
|
|
|
8
|
-
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
|
|
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
8
|
|
|
10
9
|
The project includes a Content Security Policy (CSP) in svelte.config.js.
|
|
11
10
|
|
package/db_create.sql
CHANGED
|
@@ -26,7 +26,6 @@ CREATE DATABASE auth
|
|
|
26
26
|
WITH
|
|
27
27
|
OWNER = auth
|
|
28
28
|
ENCODING = 'UTF8'
|
|
29
|
-
TABLESPACE = pg_default
|
|
30
29
|
CONNECTION LIMIT = -1;
|
|
31
30
|
|
|
32
31
|
COMMENT ON DATABASE auth IS 'SvelteKit Auth Example';
|
|
@@ -40,20 +39,44 @@ CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
|
40
39
|
-- Required to generate UUIDs for sessions
|
|
41
40
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
42
41
|
|
|
42
|
+
-- Required for case-insensitive text (email_address domain)
|
|
43
|
+
CREATE EXTENSION IF NOT EXISTS citext;
|
|
44
|
+
|
|
43
45
|
-- Using hard-coded roles (often this would be a table)
|
|
44
46
|
CREATE TYPE public.roles AS ENUM
|
|
45
47
|
('student', 'teacher', 'admin');
|
|
46
48
|
|
|
47
49
|
ALTER TYPE public.roles OWNER TO auth;
|
|
48
50
|
|
|
51
|
+
-- Domains
|
|
52
|
+
CREATE DOMAIN public.email_address AS citext CHECK (
|
|
53
|
+
length(VALUE) <= 254
|
|
54
|
+
AND VALUE = btrim(VALUE)
|
|
55
|
+
AND VALUE ~* '^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$'
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
COMMENT ON DOMAIN public.email_address IS 'RFC-compliant email address (case-insensitive, max 254 chars)';
|
|
59
|
+
|
|
60
|
+
CREATE DOMAIN public.persons_name AS text CHECK (length(VALUE) <= 20) NOT NULL;
|
|
61
|
+
|
|
62
|
+
COMMENT ON DOMAIN public.persons_name IS 'Person first or last name (max 20 characters)';
|
|
63
|
+
|
|
64
|
+
CREATE DOMAIN public.phone_number AS text CHECK (
|
|
65
|
+
VALUE IS NULL
|
|
66
|
+
OR length(VALUE) <= 50
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
COMMENT ON DOMAIN public.phone_number IS 'Phone number (max 50 characters)';
|
|
70
|
+
|
|
49
71
|
CREATE TABLE IF NOT EXISTS public.users (
|
|
50
72
|
id integer NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ),
|
|
51
73
|
role roles NOT NULL DEFAULT 'student'::roles,
|
|
52
|
-
email
|
|
74
|
+
email email_address NOT NULL,
|
|
53
75
|
password character varying(80) COLLATE pg_catalog."default",
|
|
54
|
-
first_name
|
|
55
|
-
last_name
|
|
56
|
-
|
|
76
|
+
first_name persons_name,
|
|
77
|
+
last_name persons_name,
|
|
78
|
+
opt_out boolean NOT NULL DEFAULT false,
|
|
79
|
+
phone phone_number,
|
|
57
80
|
CONSTRAINT users_pkey PRIMARY KEY (id),
|
|
58
81
|
CONSTRAINT users_email_unique UNIQUE (email)
|
|
59
82
|
) TABLESPACE pg_default;
|
|
@@ -78,13 +101,13 @@ CREATE INDEX users_password
|
|
|
78
101
|
CREATE TABLE IF NOT EXISTS public.sessions (
|
|
79
102
|
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
|
80
103
|
user_id integer NOT NULL,
|
|
81
|
-
expires
|
|
104
|
+
expires timestamptz NOT NULL DEFAULT (CURRENT_TIMESTAMP + INTERVAL '2 hours'),
|
|
82
105
|
CONSTRAINT sessions_pkey PRIMARY KEY (id),
|
|
83
106
|
CONSTRAINT sessions_user_fkey FOREIGN KEY (user_id)
|
|
84
107
|
REFERENCES public.users (id) MATCH SIMPLE
|
|
85
|
-
ON UPDATE
|
|
86
|
-
ON DELETE CASCADE
|
|
87
|
-
|
|
108
|
+
ON UPDATE CASCADE
|
|
109
|
+
ON DELETE CASCADE,
|
|
110
|
+
CONSTRAINT sessions_one_per_user UNIQUE (user_id)
|
|
88
111
|
) TABLESPACE pg_default;
|
|
89
112
|
|
|
90
113
|
ALTER TABLE public.sessions OWNER to auth;
|
|
@@ -98,8 +121,8 @@ CREATE OR REPLACE FUNCTION public.authenticate(
|
|
|
98
121
|
VOLATILE PARALLEL UNSAFE
|
|
99
122
|
AS $BODY$
|
|
100
123
|
DECLARE
|
|
101
|
-
input_email
|
|
102
|
-
input_password
|
|
124
|
+
input_email text := trim(input->>'email');
|
|
125
|
+
input_password text := input->>'password';
|
|
103
126
|
BEGIN
|
|
104
127
|
IF input_email IS NULL OR input_password IS NULL THEN
|
|
105
128
|
response := json_build_object('statusCode', 400, 'status', 'Please provide an email address and password to authenticate.', 'user', NULL);
|
|
@@ -107,24 +130,25 @@ BEGIN
|
|
|
107
130
|
END IF;
|
|
108
131
|
|
|
109
132
|
WITH user_authenticated AS (
|
|
110
|
-
SELECT users.id, role, first_name, last_name, phone
|
|
133
|
+
SELECT users.id, role, first_name, last_name, phone, opt_out
|
|
111
134
|
FROM users
|
|
112
135
|
WHERE email = input_email AND password = crypt(input_password, password) LIMIT 1
|
|
113
136
|
)
|
|
114
137
|
SELECT json_build_object(
|
|
115
|
-
'statusCode', CASE WHEN (SELECT
|
|
116
|
-
'status', CASE WHEN (SELECT
|
|
138
|
+
'statusCode', CASE WHEN EXISTS (SELECT 1 FROM user_authenticated) THEN 200 ELSE 401 END,
|
|
139
|
+
'status', CASE WHEN EXISTS (SELECT 1 FROM user_authenticated)
|
|
117
140
|
THEN 'Login successful.'
|
|
118
141
|
ELSE 'Invalid username/password combination.'
|
|
119
142
|
END,
|
|
120
|
-
'user', CASE WHEN (SELECT
|
|
143
|
+
'user', CASE WHEN EXISTS (SELECT 1 FROM user_authenticated)
|
|
121
144
|
THEN (SELECT json_build_object(
|
|
122
145
|
'id', user_authenticated.id,
|
|
123
146
|
'role', user_authenticated.role,
|
|
124
147
|
'email', input_email,
|
|
125
148
|
'firstName', user_authenticated.first_name,
|
|
126
149
|
'lastName', user_authenticated.last_name,
|
|
127
|
-
'phone', user_authenticated.phone
|
|
150
|
+
'phone', user_authenticated.phone,
|
|
151
|
+
'optOut', user_authenticated.opt_out)
|
|
128
152
|
FROM user_authenticated)
|
|
129
153
|
ELSE NULL
|
|
130
154
|
END,
|
|
@@ -142,8 +166,12 @@ CREATE OR REPLACE FUNCTION public.create_session(
|
|
|
142
166
|
COST 100
|
|
143
167
|
VOLATILE PARALLEL UNSAFE
|
|
144
168
|
AS $BODY$
|
|
145
|
-
|
|
146
|
-
|
|
169
|
+
-- Remove expired sessions (index-friendly cleanup)
|
|
170
|
+
DELETE FROM sessions WHERE expires < CURRENT_TIMESTAMP;
|
|
171
|
+
-- Remove any existing session(s) for this user
|
|
172
|
+
DELETE FROM sessions WHERE user_id = input_user_id;
|
|
173
|
+
-- Create the new session
|
|
174
|
+
INSERT INTO sessions(user_id) VALUES (input_user_id) RETURNING sessions.id;
|
|
147
175
|
$BODY$;
|
|
148
176
|
|
|
149
177
|
ALTER FUNCTION public.create_session(integer) OWNER TO auth;
|
|
@@ -159,6 +187,7 @@ SELECT json_build_object(
|
|
|
159
187
|
'firstName', users.first_name,
|
|
160
188
|
'lastName', users.last_name,
|
|
161
189
|
'phone', users.phone,
|
|
190
|
+
'optOut', users.opt_out,
|
|
162
191
|
'expires', sessions.expires
|
|
163
192
|
) AS user
|
|
164
193
|
FROM sessions
|
|
@@ -177,11 +206,11 @@ CREATE OR REPLACE FUNCTION public.register(
|
|
|
177
206
|
VOLATILE PARALLEL UNSAFE
|
|
178
207
|
AS $BODY$
|
|
179
208
|
DECLARE
|
|
180
|
-
input_email
|
|
181
|
-
input_first_name
|
|
182
|
-
input_last_name
|
|
183
|
-
input_phone
|
|
184
|
-
input_password
|
|
209
|
+
input_email text := trim(input->>'email');
|
|
210
|
+
input_first_name text := trim(input->>'firstName');
|
|
211
|
+
input_last_name text := trim(input->>'lastName');
|
|
212
|
+
input_phone text := trim(input->>'phone');
|
|
213
|
+
input_password text := input->>'password';
|
|
185
214
|
BEGIN
|
|
186
215
|
PERFORM id FROM users WHERE email = input_email;
|
|
187
216
|
IF NOT FOUND THEN
|
|
@@ -190,7 +219,7 @@ BEGIN
|
|
|
190
219
|
RETURNING
|
|
191
220
|
json_build_object(
|
|
192
221
|
'sessionId', create_session(users.id),
|
|
193
|
-
'user', json_build_object('id', users.id, 'role', 'student', 'email', input_email, 'firstName', input_first_name, 'lastName', input_last_name, 'phone', input_phone, 'optOut',
|
|
222
|
+
'user', json_build_object('id', users.id, 'role', 'student', 'email', input_email, 'firstName', input_first_name, 'lastName', input_last_name, 'phone', input_phone, 'optOut', users.opt_out)
|
|
194
223
|
) INTO user_session;
|
|
195
224
|
ELSE -- user is registering account that already exists so set sessionId and user to null so client can let them know
|
|
196
225
|
SELECT authenticate(input) INTO user_session;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import prettier from 'eslint-config-prettier'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { includeIgnoreFile } from '@eslint/compat'
|
|
4
|
+
import js from '@eslint/js'
|
|
5
|
+
import svelte from 'eslint-plugin-svelte'
|
|
6
|
+
import { defineConfig } from 'eslint/config'
|
|
7
|
+
import globals from 'globals'
|
|
8
|
+
import ts from 'typescript-eslint'
|
|
9
|
+
import svelteConfig from './svelte.config.js'
|
|
10
|
+
|
|
11
|
+
const prettierIgnorePath = fileURLToPath(new URL('./.prettierignore', import.meta.url))
|
|
12
|
+
|
|
13
|
+
export default defineConfig(
|
|
14
|
+
includeIgnoreFile(prettierIgnorePath),
|
|
15
|
+
js.configs.recommended,
|
|
16
|
+
...ts.configs.recommended,
|
|
17
|
+
...svelte.configs.recommended,
|
|
18
|
+
prettier,
|
|
19
|
+
...svelte.configs.prettier,
|
|
20
|
+
{
|
|
21
|
+
languageOptions: {
|
|
22
|
+
globals: {
|
|
23
|
+
...globals.browser,
|
|
24
|
+
...globals.node
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
rules: {
|
|
28
|
+
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
|
|
29
|
+
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
|
|
30
|
+
'no-undef': 'off'
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
files: ['**/*.svelte', '**/*.svelte.ts'],
|
|
35
|
+
languageOptions: {
|
|
36
|
+
parserOptions: {
|
|
37
|
+
projectService: true,
|
|
38
|
+
extraFileExtensions: ['.svelte'],
|
|
39
|
+
parser: ts.parser,
|
|
40
|
+
svelteConfig
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
rules: {
|
|
44
|
+
'svelte/no-at-html-tags': 'warn',
|
|
45
|
+
'svelte/require-each-key': 'warn'
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
)
|
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.0
|
|
4
|
+
"version": "5.1.0",
|
|
5
5
|
"author": "Nate Stuyvesant",
|
|
6
6
|
"license": "https://github.com/nstuyvesant/sveltekit-auth-example/blob/master/LICENSE",
|
|
7
7
|
"repository": {
|
|
@@ -20,52 +20,55 @@
|
|
|
20
20
|
"postgresql"
|
|
21
21
|
],
|
|
22
22
|
"scripts": {
|
|
23
|
-
"start": "node build",
|
|
24
23
|
"dev": "vite dev",
|
|
24
|
+
"prepare": "svelte-kit sync || echo ''",
|
|
25
25
|
"build": "vite build",
|
|
26
26
|
"package": "svelte-kit package",
|
|
27
|
-
"preview": "vite preview",
|
|
27
|
+
"preview": "vite preview --port 3000 --open",
|
|
28
28
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
29
29
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
30
|
-
"lint": "prettier --check . && eslint .
|
|
31
|
-
"format": "prettier --write .
|
|
30
|
+
"lint": "prettier --check . && eslint .",
|
|
31
|
+
"format": "prettier --write ."
|
|
32
32
|
},
|
|
33
33
|
"engines": {
|
|
34
|
-
"node": "^
|
|
34
|
+
"node": "^24.14.0"
|
|
35
35
|
},
|
|
36
36
|
"type": "module",
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@sendgrid/mail": "^8.1.
|
|
39
|
-
"
|
|
38
|
+
"@sendgrid/mail": "^8.1.6",
|
|
39
|
+
"google-auth-library": "^10.6.1",
|
|
40
|
+
"pg": "^8.20.0"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
|
-
"@eslint/js": "^
|
|
43
|
-
"@
|
|
44
|
-
"@sveltejs/
|
|
45
|
-
"@sveltejs/
|
|
43
|
+
"@eslint/js": "^10.0.1",
|
|
44
|
+
"@playwright/test": "^1.58.2",
|
|
45
|
+
"@sveltejs/adapter-node": "^5.5.4",
|
|
46
|
+
"@sveltejs/kit": "^2.54.0",
|
|
47
|
+
"@sveltejs/mcp": "^0.1.21",
|
|
48
|
+
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
49
|
+
"@tailwindcss/vite": "^4.2.1",
|
|
46
50
|
"@types/bootstrap": "5.2.10",
|
|
47
|
-
"@types/google.accounts": "^0.0.
|
|
48
|
-
"@types/jsonwebtoken": "^9.0.
|
|
49
|
-
"@types/pg": "^8.
|
|
50
|
-
"
|
|
51
|
-
"eslint": "^
|
|
52
|
-
"eslint-
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"prettier": "^3.5.
|
|
59
|
-
"prettier-plugin-
|
|
60
|
-
"
|
|
61
|
-
"svelte": "^
|
|
62
|
-
"svelte-check": "^4.1.5",
|
|
51
|
+
"@types/google.accounts": "^0.0.18",
|
|
52
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
53
|
+
"@types/pg": "^8.18.0",
|
|
54
|
+
"eslint": "^10.0.3",
|
|
55
|
+
"eslint-config-prettier": "^10.1.8",
|
|
56
|
+
"eslint-plugin-svelte": "^3.15.2",
|
|
57
|
+
"globals": "^17.4.0",
|
|
58
|
+
"jsonwebtoken": "^9.0.3",
|
|
59
|
+
"playwright": "^1.58.2",
|
|
60
|
+
"prettier": "^3.8.1",
|
|
61
|
+
"prettier-plugin-sql": "^0.19.2",
|
|
62
|
+
"prettier-plugin-svelte": "^3.5.1",
|
|
63
|
+
"prettier-plugin-tailwindcss": "^0.7.2",
|
|
64
|
+
"svelte": "^5.53.10",
|
|
65
|
+
"svelte-check": "^4.4.5",
|
|
63
66
|
"tslib": "^2.8.1",
|
|
64
|
-
"typescript": "^5.
|
|
65
|
-
"typescript-eslint": "^8.
|
|
66
|
-
"vite": "^
|
|
67
|
-
"vitest": "^
|
|
67
|
+
"typescript": "^5.9.3",
|
|
68
|
+
"typescript-eslint": "^8.57.0",
|
|
69
|
+
"vite": "^7.3.1",
|
|
70
|
+
"vitest": "^4.0.18",
|
|
71
|
+
"vitest-browser-svelte": "^2.0.2"
|
|
68
72
|
},
|
|
69
|
-
"packageManager": "yarn@4.
|
|
70
|
-
"prettier": "./prettier.config.mjs"
|
|
73
|
+
"packageManager": "yarn@4.13.0"
|
|
71
74
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineConfig } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
testDir: './tests',
|
|
5
|
+
testMatch: '**/*.e2e.test.ts',
|
|
6
|
+
use: {
|
|
7
|
+
baseURL: 'http://localhost:3000'
|
|
8
|
+
},
|
|
9
|
+
webServer: {
|
|
10
|
+
// In local dev, use the same dev server you run manually.
|
|
11
|
+
// In CI, fall back to a production-like preview build.
|
|
12
|
+
command: process.env.CI
|
|
13
|
+
? 'yarn build && yarn preview -- --port 3000'
|
|
14
|
+
: 'yarn dev -- --port 3000',
|
|
15
|
+
env: {
|
|
16
|
+
...process.env,
|
|
17
|
+
E2E_BYPASS_RECAPTCHA: 'true',
|
|
18
|
+
E2E_BRAINTREE_UNIQUE_ORDER_ID: 'true'
|
|
19
|
+
},
|
|
20
|
+
url: 'http://localhost:3000',
|
|
21
|
+
reuseExistingServer: !process.env.CI,
|
|
22
|
+
timeout: 120_000
|
|
23
|
+
}
|
|
24
|
+
})
|
package/prettier.config.mjs
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
export default {
|
|
2
2
|
$schema: 'https://json.schemastore.org/prettierrc',
|
|
3
3
|
useTabs: true,
|
|
4
|
-
tabWidth: 2,
|
|
5
|
-
semi: false,
|
|
6
|
-
arrowParens: 'avoid',
|
|
7
4
|
singleQuote: true,
|
|
8
5
|
trailingComma: 'none',
|
|
9
6
|
printWidth: 100,
|
|
10
|
-
|
|
7
|
+
tabWidth: 2,
|
|
8
|
+
semi: false,
|
|
9
|
+
arrowParens: 'avoid',
|
|
10
|
+
|
|
11
|
+
plugins: ['prettier-plugin-svelte', 'prettier-plugin-sql', 'prettier-plugin-tailwindcss'],
|
|
11
12
|
overrides: [
|
|
12
13
|
{
|
|
13
14
|
files: '*.svelte',
|
|
14
15
|
options: { parser: 'svelte' }
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
files: '*.sql',
|
|
19
|
+
options: {
|
|
20
|
+
parser: 'sql',
|
|
21
|
+
language: 'postgresql'
|
|
22
|
+
}
|
|
15
23
|
}
|
|
16
|
-
]
|
|
24
|
+
],
|
|
25
|
+
tailwindStylesheet: './src/routes/layout.css'
|
|
17
26
|
}
|
package/src/app.html
CHANGED
package/src/hooks.server.ts
CHANGED
|
@@ -3,8 +3,7 @@ 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) {
|
|
6
|
-
const
|
|
7
|
-
const { rows } = await query(sql, [sessionId])
|
|
6
|
+
const { rows } = await query('SELECT * FROM get_session($1::uuid)', [sessionId], 'get-session')
|
|
8
7
|
if (rows?.length > 0) {
|
|
9
8
|
event.locals.user = <User>rows[0].get_session
|
|
10
9
|
}
|
|
@@ -12,10 +11,15 @@ async function attachUserToRequestEvent(sessionId: string, event: RequestEvent)
|
|
|
12
11
|
|
|
13
12
|
// Invoked for each endpoint called and initially for SSR router
|
|
14
13
|
export const handle: Handle = async ({ event, resolve }) => {
|
|
15
|
-
const { cookies } = event
|
|
16
|
-
|
|
14
|
+
const { cookies, url } = event
|
|
15
|
+
|
|
16
|
+
// Skip auth overhead for static asset requests
|
|
17
|
+
if (url.pathname.startsWith('/_app/')) {
|
|
18
|
+
return resolve(event)
|
|
19
|
+
}
|
|
17
20
|
|
|
18
21
|
// before endpoint or page is called
|
|
22
|
+
const sessionId = cookies.get('session')
|
|
19
23
|
if (sessionId) {
|
|
20
24
|
await attachUserToRequestEvent(sessionId, event)
|
|
21
25
|
}
|
package/src/lib/server/db.ts
CHANGED
|
@@ -1,16 +1,68 @@
|
|
|
1
|
-
|
|
2
|
-
import type { QueryResult } from 'pg'
|
|
1
|
+
import type { QueryResult, QueryResultRow } from 'pg'
|
|
3
2
|
import pg from 'pg'
|
|
4
|
-
import {
|
|
3
|
+
import { env } from '$env/dynamic/private'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generic function signature for executing a typed SQL query.
|
|
7
|
+
*
|
|
8
|
+
* @template T The row type returned from the database, extending QueryResultRow.
|
|
9
|
+
* @param sql The parameterized SQL query string.
|
|
10
|
+
* @param params Optional positional parameters to bind to the query.
|
|
11
|
+
* @returns A promise resolving to the typed QueryResult.
|
|
12
|
+
*/
|
|
13
|
+
type QueryFunction = <T extends QueryResultRow>(
|
|
14
|
+
sql: string,
|
|
15
|
+
params?: (string | number | boolean | object | null)[],
|
|
16
|
+
name?: string
|
|
17
|
+
) => Promise<QueryResult<T>>
|
|
18
|
+
|
|
19
|
+
let queryFn: QueryFunction
|
|
5
20
|
|
|
6
21
|
const pool = new pg.Pool({
|
|
7
22
|
max: 10, // default
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
23
|
+
idleTimeoutMillis: 10000,
|
|
24
|
+
connectionTimeoutMillis: 5000,
|
|
25
|
+
connectionString: env.DATABASE_URL,
|
|
26
|
+
ssl: env.DATABASE_SSL === 'true' ? { rejectUnauthorized: false } : false
|
|
13
27
|
})
|
|
14
28
|
|
|
15
|
-
|
|
16
|
-
|
|
29
|
+
pool.on('error', (err: Error) => {
|
|
30
|
+
console.error('Unexpected error on idle client', err)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
queryFn = <T extends QueryResultRow>(
|
|
34
|
+
sql: string,
|
|
35
|
+
params?: (string | number | boolean | object | null)[],
|
|
36
|
+
name?: string
|
|
37
|
+
) => pool.query<T>(name ? { name, text: sql, values: params } : { text: sql, values: params })
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Executes a parameterized SQL query against the PostgreSQL database.
|
|
41
|
+
*
|
|
42
|
+
* @template T - The expected shape of rows returned by the query. Must extend QueryResultRow.
|
|
43
|
+
* @param sql - The SQL query string. Use $1, $2, etc. for parameterized queries.
|
|
44
|
+
* @param params - Optional array of parameter values to bind to the query placeholders.
|
|
45
|
+
* @returns A Promise resolving to a PostgreSQL QueryResult containing typed rows and metadata.
|
|
46
|
+
* @throws {Error} If the database connection fails or the query is invalid.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* // Simple query without parameters
|
|
51
|
+
* const result = await query('SELECT * FROM users');
|
|
52
|
+
* console.log(result.rows);
|
|
53
|
+
*
|
|
54
|
+
* // Parameterized query with type safety
|
|
55
|
+
* const result = await query<User>('SELECT * FROM users WHERE id = $1', [userId]);
|
|
56
|
+
* console.log(result.rows[0].name); // TypeScript knows this is a User
|
|
57
|
+
*
|
|
58
|
+
* // Insert with multiple parameters
|
|
59
|
+
* await query(
|
|
60
|
+
* 'INSERT INTO products (name, price) VALUES ($1, $2)',
|
|
61
|
+
* ['Widget', 29.99]
|
|
62
|
+
* );
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export const query = <T extends QueryResultRow = any>(
|
|
66
|
+
sql: string,
|
|
67
|
+
params?: (string | number | boolean | object | null)[]
|
|
68
|
+
): Promise<QueryResult<T>> => queryFn<T>(sql, params)
|
package/src/routes/+error.svelte
CHANGED