sveltekit-auth-example 1.0.3

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/.eslintrc.cjs ADDED
@@ -0,0 +1,20 @@
1
+ module.exports = {
2
+ root: true,
3
+ parser: '@typescript-eslint/parser',
4
+ extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
5
+ plugins: ['svelte3', '@typescript-eslint'],
6
+ ignorePatterns: ['*.cjs'],
7
+ overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
8
+ settings: {
9
+ 'svelte3/typescript': () => require('typescript')
10
+ },
11
+ parserOptions: {
12
+ sourceType: 'module',
13
+ ecmaVersion: 2019
14
+ },
15
+ env: {
16
+ browser: true,
17
+ es2017: true,
18
+ node: true
19
+ }
20
+ };
package/.prettierrc ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "useTabs": true,
3
+ "singleQuote": true,
4
+ "trailingComma": "none",
5
+ "printWidth": 100
6
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # 1.0.3
2
+ * [Fix] user created or updated when password mismatches (@lxy-yz)
3
+ * Updated project dependencies
4
+ * Replaced Sveltestrap's Toast with native Bootstrap 5 JavaScript to avoid error with @popperjs import (lacks type=module)
5
+ * Added declarations for Session and Locals for type safety
6
+
7
+ # 1.0.2
8
+
9
+ * [Fix] Updated endpoints and hooks to conform to SvelteKit's API changes.
10
+ * Updated project dependencies
11
+
12
+ # 1.0.1
13
+
14
+ * Switched to dotenv vs. VITE_ env values for better security
15
+ * Load Sign in with Google via code instead of static template
16
+ * Fix logout (didn't work if session expired)
17
+ * Fix login button rendering if that's the starting page
18
+
19
+ # Backlog
20
+
21
+ * [Low] Add password complexity check
22
+ * [Low] Add Google reCaptcha 3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Nate Stuyvesant
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # SvelteKit Authentication and Authorization Example
2
+
3
+ This is an example of how to register, authenticate, and update users and limit their access to
4
+ areas of the website by role (admin, teacher, student).
5
+
6
+ It's an SPA built with SvelteKit and a PostgreSQL database back-end. Code is TypeScript and the website is styled using Bootstrap. PostgreSQL functions handle password hashing and UUID generation for the session ID.
7
+
8
+ 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). Because of that, **getSession()** in hooks.ts is not useful. Instead, the client makes REST calls, gets the user in the body of the response (if successful) and adds the user to the client-side session store.
9
+
10
+ The website supports two types of authentication:
11
+ 1. **Local accounts** via username (email) and password
12
+ - The login form (/src/routes/login.svelte) sends the login info as JSON to endpoint /auth/login
13
+ - 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).
14
+ - The endpoint sends session ID as an httpOnly SameSite cookie and the user object in the body of the response.
15
+ - The client stores the user object in SvelteKit's session store.
16
+ 2. **Sign in with Google**
17
+ - **Sign in with Google** is initialized in /src/routes/__layout.svelte.
18
+ - **One Tap** "dialog" is displayed on the Home page (/src/routes/index.svelte) while the **Sign in with Google** button is on the login page (/src/routes/login.svelte).
19
+ - Clicking either button opens a new window asking the user to authorize this website. If they OK it, a JWT is sent to a callback function.
20
+ - The callback function (in /src/lib/auth.ts) sends the JWT to an endpoint on this server /auth/google.
21
+ - 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.
22
+ - The client stores the user object in SvelteKit's session store.
23
+
24
+ As the client calls endpoints, each request to the server includes the session ID in the httpOnly cookie. The handle() function in hooks.ts checks the database for this session ID. If it exists and is not expired, it attaches the user (including role) to request.locals. Each endpoint can then examine request.locals.user.role to determine whether to respond or return a 401.
25
+
26
+ > There is some overhead to checking the user session in a database each time versus using a JSON web token; 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.
27
+
28
+ Pages use the session.user.role to determine whether they are authorized. While a malicious user could alter the client-side session store to see pages they should not, the dynamic data in those restricted pages is served via endpoints which check request.locals.user to determine whether to grant access.
29
+
30
+ The forgot password functionality uses SendInBlue 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.
31
+
32
+ ## Prerequisites
33
+ - PostgreSQL 13 or higher
34
+ - Node.js 16.13.0 or higher
35
+ - npm 8.1.0 or higher
36
+ - Google API client
37
+ - SendInBlue account (only used for emailing password reset link - the sample can run without it but forgot password will not work)
38
+
39
+ ## Setting up the project
40
+
41
+ Here are the steps using a macOS, Linux or UNIX command-line:
42
+
43
+ 1. Get the project and setup the database
44
+ ```bash
45
+ # Clone the repo to your current directory
46
+ git clone https://github.com/nstuyvesant/sveltekit-auth-example.git
47
+
48
+ # Install the dependencies
49
+ cd /sveltekit-auth-example
50
+ npm install
51
+
52
+ # Create PostgreSQL database
53
+ psql -d postgres -f db_create.sql
54
+ ```
55
+
56
+ 2. Create a **Google API client ID** per [these instructions](https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid).
57
+
58
+ 3. Create an **.env** file at the top level of the project with the following values (substituting your own id and PostgreSQL username and password):
59
+ ```bash
60
+ DATABASE_URL=postgres://user:password@localhost:5432/auth
61
+ DOMAIN=http://localhost:3000
62
+ JWT_SECRET=replace_with_your_own
63
+ SEND_IN_BLUE_URL=https://api.sendinblue.com
64
+ SEND_IN_BLUE_KEY=replace_with_your_own
65
+ SEND_IN_BLUE_FROM='{ "email":"jdoe@example.com", "name":"John Doe" }'
66
+ SEND_IN_BLUE_ADMINS='{ "email":"jdoe@example.com", "name":"John Doe" }'
67
+ VITE_GOOGLE_CLIENT_ID=replace_with_your_own
68
+ ```
69
+
70
+ ## Run locally
71
+
72
+ ```bash
73
+ # Start the server and open the app in a new browser tab
74
+ npm run dev -- --open
75
+ ```
76
+
77
+ ## Valid logins
78
+
79
+ The db_create.sql script adds three users to the database with obvious roles:
80
+ - admin@example.com password admin123
81
+ - teacher@example.com password teacher123
82
+ - student@example.com password student123
83
+
84
+ ## My ask of you
85
+
86
+ Please report any issues or areas where the code can be optimized. I am still learning Svelte and SvelteKit. All feedback is appreciated.
package/db_create.sql ADDED
@@ -0,0 +1,307 @@
1
+ -- Invoke this script via
2
+ -- $ psql -d postgres -f db_create.sql
3
+
4
+ -- Create role if not already there
5
+ DO
6
+ $do$
7
+ BEGIN
8
+ IF NOT EXISTS (
9
+ SELECT -- SELECT list can stay empty for this
10
+ FROM pg_catalog.pg_roles
11
+ WHERE rolname = 'auth') THEN
12
+
13
+ CREATE ROLE auth;
14
+ END IF;
15
+ END
16
+ $do$;
17
+
18
+ -- Forcefully disconnect anyone
19
+ SELECT pid, pg_terminate_backend(pid)
20
+ FROM pg_stat_activity
21
+ WHERE datname = 'auth' AND pid <> pg_backend_pid();
22
+
23
+ DROP DATABASE IF EXISTS auth;
24
+
25
+ CREATE DATABASE auth
26
+ WITH
27
+ OWNER = auth
28
+ ENCODING = 'UTF8'
29
+ TABLESPACE = pg_default
30
+ CONNECTION LIMIT = -1;
31
+
32
+ COMMENT ON DATABASE auth IS 'SvelteKit Auth Example';
33
+
34
+ -- Connect to auth database
35
+ \connect auth
36
+
37
+ -- Required for password hashing
38
+ CREATE EXTENSION IF NOT EXISTS pgcrypto;
39
+
40
+ -- Required to generate UUIDs for sessions
41
+ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
42
+
43
+ -- Using hard-coded roles (often this would be a table)
44
+ CREATE TYPE public.roles AS ENUM
45
+ ('student', 'teacher', 'admin');
46
+
47
+ ALTER TYPE public.roles OWNER TO auth;
48
+
49
+ CREATE TABLE IF NOT EXISTS public.users (
50
+ id integer NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ),
51
+ role roles NOT NULL DEFAULT 'student'::roles,
52
+ email character varying(80) COLLATE pg_catalog."default" NOT NULL,
53
+ password character varying(80) COLLATE pg_catalog."default",
54
+ first_name character varying(20) COLLATE pg_catalog."default" NOT NULL,
55
+ last_name character varying(20) COLLATE pg_catalog."default" NOT NULL,
56
+ phone character varying(23) COLLATE pg_catalog."default",
57
+ CONSTRAINT users_pkey PRIMARY KEY (id),
58
+ CONSTRAINT users_email_unique UNIQUE (email)
59
+ ) TABLESPACE pg_default;
60
+
61
+ ALTER TABLE public.users OWNER to auth;
62
+
63
+ CREATE INDEX users_first_name_index
64
+ ON public.users USING btree
65
+ (first_name COLLATE pg_catalog."default" ASC NULLS LAST)
66
+ TABLESPACE pg_default;
67
+
68
+ CREATE INDEX users_last_name_index
69
+ ON public.users USING btree
70
+ (last_name COLLATE pg_catalog."default" ASC NULLS LAST)
71
+ TABLESPACE pg_default;
72
+
73
+ CREATE INDEX users_password
74
+ ON public.users USING btree
75
+ (password COLLATE pg_catalog."default" ASC NULLS LAST)
76
+ TABLESPACE pg_default;
77
+
78
+ CREATE TABLE IF NOT EXISTS public.sessions (
79
+ id uuid NOT NULL DEFAULT uuid_generate_v4(),
80
+ user_id integer NOT NULL,
81
+ expires timestamp with time zone DEFAULT (CURRENT_TIMESTAMP + '02:00:00'::interval),
82
+ CONSTRAINT sessions_pkey PRIMARY KEY (id),
83
+ CONSTRAINT sessions_user_fkey FOREIGN KEY (user_id)
84
+ REFERENCES public.users (id) MATCH SIMPLE
85
+ ON UPDATE NO ACTION
86
+ ON DELETE CASCADE
87
+ NOT VALID
88
+ ) TABLESPACE pg_default;
89
+
90
+ ALTER TABLE public.sessions OWNER to auth;
91
+
92
+ CREATE OR REPLACE FUNCTION public.authenticate(
93
+ input json,
94
+ OUT response json)
95
+ RETURNS json
96
+ LANGUAGE 'plpgsql'
97
+ COST 100
98
+ VOLATILE PARALLEL UNSAFE
99
+ AS $BODY$
100
+ DECLARE
101
+ input_email varchar(80) := LOWER(TRIM((input->>'email')::varchar));
102
+ input_password varchar(80) := (input->>'password')::varchar;
103
+ BEGIN
104
+ IF input_email IS NULL OR input_password IS NULL THEN
105
+ response := json_build_object('statusCode', 400, 'status', 'Please provide an email address and password to authenticate.', 'user', NULL);
106
+ RETURN;
107
+ END IF;
108
+
109
+ WITH user_authenticated AS (
110
+ SELECT users.id, role, first_name, last_name, phone
111
+ FROM users
112
+ WHERE email = input_email AND password = crypt(input_password, password) LIMIT 1
113
+ )
114
+ SELECT json_build_object(
115
+ 'statusCode', CASE WHEN (SELECT COUNT(*) FROM user_authenticated) > 0 THEN 200 ELSE 401 END,
116
+ 'status', CASE WHEN (SELECT COUNT(*) FROM user_authenticated) > 0
117
+ THEN 'Login successful.'
118
+ ELSE 'Invalid username/password combination.'
119
+ END,
120
+ 'user', CASE WHEN (SELECT COUNT(*) FROM user_authenticated) > 0
121
+ THEN (SELECT json_build_object(
122
+ 'id', user_authenticated.id,
123
+ 'role', user_authenticated.role,
124
+ 'email', input_email,
125
+ 'firstName', user_authenticated.first_name,
126
+ 'lastName', user_authenticated.last_name,
127
+ 'phone', user_authenticated.phone)
128
+ FROM user_authenticated)
129
+ ELSE NULL
130
+ END,
131
+ 'sessionId', (SELECT create_session(user_authenticated.id) FROM user_authenticated)
132
+ ) INTO response;
133
+ END;
134
+ $BODY$;
135
+
136
+ ALTER FUNCTION public.authenticate(json) OWNER TO auth;
137
+
138
+ CREATE OR REPLACE FUNCTION public.create_session(
139
+ input_user_id integer)
140
+ RETURNS uuid
141
+ LANGUAGE 'sql'
142
+ COST 100
143
+ VOLATILE PARALLEL UNSAFE
144
+ AS $BODY$
145
+ DELETE FROM sessions WHERE user_id = input_user_id;
146
+ INSERT INTO sessions(user_id) VALUES (input_user_id) RETURNING sessions.id;
147
+ $BODY$;
148
+
149
+ ALTER FUNCTION public.create_session(integer) OWNER TO auth;
150
+
151
+ CREATE OR REPLACE FUNCTION public.get_session(input_session_id uuid)
152
+ RETURNS json
153
+ LANGUAGE 'sql'
154
+ AS $BODY$
155
+ SELECT json_build_object(
156
+ 'id', sessions.user_id,
157
+ 'role', users.role,
158
+ 'email', users.email,
159
+ 'firstName', users.first_name,
160
+ 'lastName', users.last_name,
161
+ 'phone', users.phone
162
+ ) AS user
163
+ FROM sessions
164
+ INNER JOIN users ON sessions.user_id = users.id
165
+ WHERE sessions.id = input_session_id AND expires > CURRENT_TIMESTAMP LIMIT 1;
166
+ $BODY$;
167
+
168
+ ALTER FUNCTION public.get_session(uuid) OWNER TO auth;
169
+
170
+ CREATE OR REPLACE FUNCTION public.register(
171
+ input json,
172
+ OUT user_session json)
173
+ RETURNS json
174
+ LANGUAGE 'plpgsql'
175
+ COST 100
176
+ VOLATILE PARALLEL UNSAFE
177
+ AS $BODY$
178
+ DECLARE
179
+ input_email varchar(80) := LOWER(TRIM((input->>'email')::varchar));
180
+ input_first_name varchar(20) := TRIM((input->>'firstName')::varchar);
181
+ input_last_name varchar(20) := TRIM((input->>'lastName')::varchar);
182
+ input_phone varchar(23) := TRIM((input->>'phone')::varchar);
183
+ input_password varchar(80) := (input->>'password')::varchar;
184
+ BEGIN
185
+ SELECT json_build_object('id', create_session(users.id), 'user', json_build_object('id', users.id, 'role', users.role, 'email', input_email, 'firstName', users.first_name, 'lastName', users.last_name, 'phone', users.phone)) INTO user_session FROM users WHERE email = input_email;
186
+ IF NOT FOUND THEN
187
+ INSERT INTO users(role, password, email, first_name, last_name, phone)
188
+ VALUES('student', crypt(input_password, input_password), input_email, input_first_name, input_last_name, input_phone)
189
+ RETURNING
190
+ json_build_object(
191
+ 'sessionId', create_session(users.id),
192
+ 'user', json_build_object('id', users.id, 'role', 'student', 'email', input_email, 'firstName', input_first_name, 'lastName', input_last_name, 'phone', input_phone)
193
+ ) INTO user_session;
194
+ END IF;
195
+ END;
196
+ $BODY$;
197
+
198
+ ALTER FUNCTION public.register(json) OWNER TO auth;
199
+
200
+ CREATE OR REPLACE FUNCTION public.start_gmail_user_session(
201
+ input json,
202
+ OUT user_session json)
203
+ RETURNS json
204
+ LANGUAGE 'plpgsql'
205
+ COST 100
206
+ VOLATILE PARALLEL UNSAFE
207
+ AS $BODY$
208
+ DECLARE
209
+ input_email varchar(80) := LOWER(TRIM((input->>'email')::varchar));
210
+ input_first_name varchar(20) := TRIM((input->>'firstName')::varchar);
211
+ input_last_name varchar(20) := TRIM((input->>'lastName')::varchar);
212
+ BEGIN
213
+ SELECT json_build_object('id', create_session(users.id), 'user', json_build_object('id', users.id, 'role', users.role, 'email', input_email, 'firstName', users.first_name, 'lastName', users.last_name, 'phone', users.phone)) INTO user_session FROM users WHERE email = input_email;
214
+ IF NOT FOUND THEN
215
+ INSERT INTO users(role, email, first_name, last_name)
216
+ VALUES('student', input_email, input_first_name, input_last_name)
217
+ RETURNING
218
+ json_build_object(
219
+ 'id', create_session(users.id),
220
+ 'user', json_build_object('id', users.id, 'role', 'student', 'email', input_email, 'firstName', input_first_name, 'lastName', input_last_name, 'phone', null)
221
+ ) INTO user_session;
222
+ END IF;
223
+ END;
224
+ $BODY$;
225
+
226
+ ALTER FUNCTION public.start_gmail_user_session(json) OWNER TO auth;
227
+
228
+ CREATE PROCEDURE public.delete_session(input_id integer)
229
+ LANGUAGE sql
230
+ AS $$
231
+ DELETE FROM sessions WHERE user_id = input_id;
232
+ $$;
233
+
234
+ CREATE OR REPLACE PROCEDURE public.reset_password(
235
+ input_id integer,
236
+ input_password text)
237
+ LANGUAGE 'sql'
238
+ AS $BODY$
239
+ UPDATE users SET password = crypt(input_password, gen_salt('bf', 8)) WHERE id = input_id;
240
+ END;
241
+ $BODY$;
242
+
243
+ ALTER PROCEDURE public.reset_password(integer, text) OWNER TO auth;
244
+
245
+ CREATE OR REPLACE PROCEDURE public.upsert_user(input json)
246
+ LANGUAGE plpgsql
247
+ AS $BODY$
248
+ DECLARE
249
+ input_id integer := COALESCE((input->>'id')::integer,0);
250
+ input_role roles := COALESCE((input->>'role')::roles, 'student');
251
+ input_email varchar(80) := LOWER(TRIM((input->>'email')::varchar));
252
+ input_password varchar(80) := COALESCE((input->>'password')::varchar, '');
253
+ input_first_name varchar(20) := TRIM((input->>'firstName')::varchar);
254
+ input_last_name varchar(20) := TRIM((input->>'lastName')::varchar);
255
+ input_phone varchar(23) := TRIM((input->>'phone')::varchar);
256
+ BEGIN
257
+ IF input_id = 0 THEN
258
+ INSERT INTO users (role, email, password, first_name, last_name, phone)
259
+ VALUES (
260
+ input_role, input_email, crypt(input_password, gen_salt('bf', 8)),
261
+ input_first_name, input_last_name, input_phone);
262
+ ELSE
263
+ UPDATE users SET
264
+ role = input_role,
265
+ email = input_email,
266
+ password = CASE WHEN input_password = ''
267
+ THEN password -- leave as is (we are updating fields other than the password)
268
+ ELSE crypt(input_password, gen_salt('bf', 8))
269
+ END,
270
+ first_name = input_first_name,
271
+ last_name = input_last_name,
272
+ phone = input_phone
273
+ WHERE id = input_id;
274
+ END IF;
275
+ END;
276
+ $BODY$;
277
+
278
+ ALTER PROCEDURE public.upsert_user(json) OWNER TO auth;
279
+
280
+ CREATE OR REPLACE PROCEDURE public.update_user(input_id integer, input json)
281
+ LANGUAGE plpgsql
282
+ AS $BODY$
283
+ DECLARE
284
+ input_email varchar(80) := LOWER(TRIM((input->>'email')::varchar));
285
+ input_password varchar(80) := COALESCE((input->>'password')::varchar, '');
286
+ input_first_name varchar(20) := TRIM((input->>'firstName')::varchar);
287
+ input_last_name varchar(20) := TRIM((input->>'lastName')::varchar);
288
+ input_phone varchar(23) := TRIM((input->>'phone')::varchar);
289
+ BEGIN
290
+ UPDATE users SET
291
+ email = input_email,
292
+ password = CASE WHEN input_password = ''
293
+ THEN password -- leave as is (we are updating fields other than the password)
294
+ ELSE crypt(input_password, gen_salt('bf', 8))
295
+ END,
296
+ first_name = input_first_name,
297
+ last_name = input_last_name,
298
+ phone = input_phone
299
+ WHERE id = input_id;
300
+ END;
301
+ $BODY$;
302
+
303
+ ALTER PROCEDURE public.update_user(integer, json) OWNER TO auth;
304
+
305
+ CALL public.upsert_user('{"id":0, "role":"admin", "email":"admin@example.com", "password":"admin123", "firstName":"Jane", "lastName":"Doe", "phone":"412-555-1212"}'::json);
306
+ CALL public.upsert_user('{"id":0, "role":"teacher", "email":"teacher@example.com", "password":"teacher123", "firstName":"John", "lastName":"Doe", "phone":"724-555-1212"}'::json);
307
+ CALL public.upsert_user('{"id":0, "role":"student", "email":"student@example.com", "password":"student123", "firstName":"Justin", "lastName":"Case", "phone":"814-555-1212"}'::json);
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "sveltekit-auth-example",
3
+ "description": "SvelteKit Authentication Example",
4
+ "version": "1.0.3",
5
+ "private": false,
6
+ "author": "Nate Stuyvesant",
7
+ "license": "https://github.com/nstuyvesant/sveltekit-auth-example/blob/master/LICENSE",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/nstuyvesant/sveltekit-auth-example.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/nstuyvesant/sveltekit-auth-example/issues"
14
+ },
15
+ "scripts": {
16
+ "dev": "svelte-kit dev",
17
+ "serve": "npm run dev -- --open",
18
+ "build": "svelte-kit build",
19
+ "preview": "svelte-kit preview",
20
+ "check": "svelte-check --tsconfig ./tsconfig.json",
21
+ "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
22
+ "lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
23
+ "format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ."
24
+ },
25
+ "engines": {
26
+ "node": "~16.14.2",
27
+ "npm": "^8.6.0"
28
+ },
29
+ "type": "module",
30
+ "dependencies": {
31
+ "cookie": "^0.4.2",
32
+ "dotenv": "^16.0.0",
33
+ "google-auth-library": "^7.11.1",
34
+ "jsonwebtoken": "^8.5.1",
35
+ "pg": "^8.7.3",
36
+ "pg-native": "^3.0.0"
37
+ },
38
+ "devDependencies": {
39
+ "@sveltejs/adapter-node": "next",
40
+ "@sveltejs/kit": "next",
41
+ "@types/jsonwebtoken": "^8.5.8",
42
+ "@types/pg": "^8.6.5",
43
+ "@typescript-eslint/eslint-plugin": "^5.18.0",
44
+ "@typescript-eslint/parser": "^5.18.0",
45
+ "bootstrap": "^5.1.3",
46
+ "bootstrap-icons": "^1.8.1",
47
+ "eslint": "^8.12.0",
48
+ "eslint-config-prettier": "^8.5.0",
49
+ "eslint-plugin-svelte3": "^3.4.1",
50
+ "prettier": "^2.6.2",
51
+ "prettier-plugin-svelte": "^2.6.0",
52
+ "sass": "^1.49.11",
53
+ "svelte": "^3.46.6",
54
+ "svelte-check": "^2.4.6",
55
+ "svelte-preprocess": "^4.10.5",
56
+ "tslib": "^2.3.1",
57
+ "typescript": "^4.6.3"
58
+ }
59
+ }
package/src/app.html ADDED
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <link rel="icon" href="/favicon.png" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ %svelte.head%
8
+ </head>
9
+ <body>
10
+ <div id="svelte">%svelte.body%</div>
11
+ </body>
12
+ </html>
@@ -0,0 +1,71 @@
1
+ /// <reference types="@sveltejs/kit" />
2
+
3
+ /* eslint-disable @typescript-eslint/no-explicit-any */
4
+
5
+ // See https://kit.svelte.dev/docs/typescript
6
+ // for information about these interfaces
7
+ declare namespace App {
8
+ interface Locals {
9
+ user: User
10
+ }
11
+
12
+ // interface Platform {}
13
+
14
+ interface Session {
15
+ reservationDate: Date
16
+ scheduledClass?: ScheduledClass
17
+ user?: User
18
+ }
19
+
20
+ // interface Stuff {}
21
+ }
22
+
23
+ interface ImportMetaEnv {
24
+ VITE_GOOGLE_CLIENT_ID: string
25
+ }
26
+
27
+ type AuthenticationResult = {
28
+ statusCode: number
29
+ status: string
30
+ user: User
31
+ sessionId: string
32
+ }
33
+
34
+ type Credentials = {
35
+ email: string
36
+ password: string
37
+ }
38
+
39
+ type MessageAddressee = {
40
+ email: string
41
+ name?: string
42
+ }
43
+
44
+ type Message = {
45
+ sender?: MessageAddressee[]
46
+ to: MessageAddressee[]
47
+ subject: string
48
+ htmlContent?: string
49
+ textContent?: string
50
+ tags?: string[]
51
+ contact?: Person
52
+ }
53
+
54
+ type User = {
55
+ id?: number
56
+ role?: 'student' | 'teacher' | 'admin'
57
+ password?: string
58
+ firstName?: string
59
+ lastName?: string
60
+ email?: string
61
+ phone?: string
62
+ }
63
+
64
+ type UserSession = {
65
+ id: string,
66
+ user: User
67
+ }
68
+
69
+ interface Window {
70
+ google?: any
71
+ }
package/src/hooks.ts ADDED
@@ -0,0 +1,43 @@
1
+ import * as cookie from 'cookie'
2
+ import type { Handle, GetSession, RequestEvent } from '@sveltejs/kit'
3
+ import { query } from './routes/_db'
4
+
5
+ // Attach authorization to each server request (role may have changed)
6
+ async function attachUserToRequest(sessionId: string, event: RequestEvent) {
7
+ const sql = `
8
+ SELECT * FROM get_session($1);`
9
+ const { rows } = await query(sql, [sessionId])
10
+ if (rows?.length > 0) {
11
+ event.locals.user = rows[0].get_session
12
+ }
13
+ }
14
+
15
+ function deleteCookieIfNoUser(event: RequestEvent, response: Response) {
16
+ if (!event.locals.user) {
17
+ response.headers['Set-Cookie'] = `session=; Path=/; HttpOnly; SameSite=Lax; Expires=${new Date().toUTCString()}`
18
+ }
19
+ }
20
+
21
+ // Invoked for each endpoint called and initially for SSR router
22
+ export const handle: Handle = async ({ event, resolve }) => {
23
+
24
+ // before endpoint or page is called
25
+ const cookies = cookie.parse(event.request.headers.get('Cookie') || '')
26
+ if (cookies.session) {
27
+ await attachUserToRequest(cookies.session, event)
28
+ }
29
+
30
+ const response = await resolve(event)
31
+
32
+ // after endpoint or page is called
33
+ deleteCookieIfNoUser(event, response)
34
+ return response
35
+ }
36
+
37
+ // Only useful for authentication schemes that redirect back to the website - not
38
+ // an SPA with client-side routing that handles authentication seamlessly
39
+ export const getSession: GetSession = event => {
40
+ return event.locals.user ?
41
+ { user: event.locals.user }
42
+ : {}
43
+ }