sveltekit-auth-example 5.1.3 → 5.6.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 +19 -0
- package/README.md +12 -13
- package/db_create.sh +13 -0
- package/db_create.sql +135 -133
- package/db_schema.sql +369 -0
- package/package.json +4 -2
- package/src/hooks.server.ts +10 -7
- package/src/lib/server/email/index.ts +3 -0
- package/src/lib/server/email/mfa-code.ts +16 -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 +89 -28
- package/src/routes/auth/forgot/+server.ts +8 -22
- package/src/routes/auth/google/+server.ts +6 -1
- package/src/routes/auth/login/+server.ts +41 -8
- package/src/routes/auth/mfa/+server.ts +60 -0
- package/src/routes/auth/register/+server.ts +7 -17
- package/src/routes/auth/reset/[token]/+page.svelte +6 -8
- package/src/routes/forgot/+page.svelte +5 -8
- package/src/routes/layout.css +3 -3
- package/src/routes/login/+page.svelte +203 -77
- package/src/routes/profile/+page.svelte +16 -12
- package/src/routes/register/+page.svelte +145 -122
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
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.6.0
|
|
6
|
+
|
|
7
|
+
- Split `db_create.sql` into `db_create.sql` (role + database creation) and `db_schema.sql` (schema, functions, seed data)
|
|
8
|
+
- Add `db_create.sh` shell wrapper to run both files in sequence
|
|
9
|
+
- `db_schema.sql` is pure SQL and fully prettier-formattable
|
|
10
|
+
- README updated to use `bash db_create.sh`
|
|
11
|
+
|
|
12
|
+
# 5.5.0
|
|
13
|
+
|
|
14
|
+
- Multi-factor authentication (MFA) via email for local accounts (Google Sign In is exempt)
|
|
15
|
+
- 6-digit OTP code sent via SendGrid, expires in 10 minutes
|
|
16
|
+
- 30-day trusted-device cookie (signed JWT) suppresses MFA on subsequent logins from the same device
|
|
17
|
+
- New `mfa_codes` table in PostgreSQL with `create_mfa_code()` and `verify_mfa_code()` functions
|
|
18
|
+
- New `/auth/mfa` endpoint verifies the code, creates the session, and issues the trusted-device cookie
|
|
19
|
+
- Login page shows an inline MFA step when a code is required
|
|
20
|
+
- `/auth/mfa` added to rate-limited paths
|
|
21
|
+
- Extract email templates into dedicated files under `src/lib/server/email/` (`password-reset.ts`, `verify-email.ts`, `mfa-code.ts`)
|
|
22
|
+
- `src/lib/server/email/index.ts` re-exports all templates for clean imports
|
|
23
|
+
|
|
5
24
|
# 5.1.3
|
|
6
25
|
|
|
7
26
|
- Session timeout: automatically redirect to /login on session expiry via fetch interceptor (`src/lib/fetch-interceptor.ts`)
|
package/README.md
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# SvelteKit Authentication and Authorization Example
|
|
2
2
|
|
|
3
|
-
[](https://github.com/nstuyvesant/sveltekit-auth-example/releases)
|
|
4
3
|
[](https://github.com/nstuyvesant/sveltekit-auth-example/blob/master/LICENSE)
|
|
5
4
|
[](https://nodejs.org)
|
|
6
5
|
[](https://svelte.dev)
|
|
@@ -9,14 +8,14 @@ A complete, production-ready authentication and authorization starter for **Svel
|
|
|
9
8
|
|
|
10
9
|
## Features
|
|
11
10
|
|
|
12
|
-
|
|
|
13
|
-
|
|
14
|
-
| ✅ Local accounts (email + password)
|
|
15
|
-
| ✅ Multi-factor authentication (MFA) | ✅ Email verification
|
|
16
|
-
| ✅ Forgot password / email reset (SendGrid)
|
|
17
|
-
| ✅ Session management + timeout
|
|
18
|
-
| ✅ Role-based access control
|
|
19
|
-
| ✅ Content Security Policy (CSP)
|
|
11
|
+
| | |
|
|
12
|
+
| ---------------------------------------------- | --------------------------------------- |
|
|
13
|
+
| ✅ Local accounts (email + password) | ✅ Sign in with Google / Google One Tap |
|
|
14
|
+
| ✅ Multi-factor authentication (MFA via email) | ✅ 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 |
|
|
20
19
|
|
|
21
20
|
## Stack
|
|
22
21
|
|
|
@@ -45,7 +44,7 @@ cd sveltekit-auth-example
|
|
|
45
44
|
yarn install
|
|
46
45
|
|
|
47
46
|
# Create PostgreSQL database (only works if you have PostgreSQL installed)
|
|
48
|
-
|
|
47
|
+
bash db_create.sh
|
|
49
48
|
```
|
|
50
49
|
|
|
51
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.
|
|
@@ -85,9 +84,9 @@ yarn preview
|
|
|
85
84
|
|
|
86
85
|
The db_create.sql script adds three users to the database with obvious roles:
|
|
87
86
|
|
|
88
|
-
| Email
|
|
89
|
-
|
|
90
|
-
| admin@example.com
|
|
87
|
+
| Email | Password | Role |
|
|
88
|
+
| ------------------- | ---------- | ------- |
|
|
89
|
+
| admin@example.com | admin123 | admin |
|
|
91
90
|
| teacher@example.com | teacher123 | teacher |
|
|
92
91
|
| student@example.com | student123 | student |
|
|
93
92
|
|
package/db_create.sh
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Creates the auth role and database, then applies the schema.
|
|
5
|
+
# Usage: bash db_create.sh
|
|
6
|
+
#
|
|
7
|
+
# Optionally override the superuser with PGUSER env var:
|
|
8
|
+
# PGUSER=myuser bash db_create.sh
|
|
9
|
+
|
|
10
|
+
psql -d postgres -f db_create.sql
|
|
11
|
+
psql -d auth -f db_schema.sql
|
|
12
|
+
|
|
13
|
+
echo "Database created successfully."
|
package/db_create.sql
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
--
|
|
2
|
-
-- $ psql -d postgres -f db_create.sql
|
|
3
|
-
|
|
1
|
+
-- Run via db_create.sh or:
|
|
2
|
+
-- $ psql -d postgres -f db_create.sql && psql -d auth -f db_schema.sql
|
|
4
3
|
-- Create role if not already there
|
|
5
|
-
DO
|
|
6
|
-
$do$
|
|
4
|
+
DO $do$
|
|
7
5
|
BEGIN
|
|
8
6
|
IF NOT EXISTS (
|
|
9
7
|
SELECT -- SELECT list can stay empty for this
|
|
@@ -16,26 +14,25 @@ END
|
|
|
16
14
|
$do$;
|
|
17
15
|
|
|
18
16
|
-- Forcefully disconnect anyone
|
|
19
|
-
SELECT
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
SELECT
|
|
18
|
+
pid,
|
|
19
|
+
pg_terminate_backend(pid)
|
|
20
|
+
FROM
|
|
21
|
+
pg_stat_activity
|
|
22
|
+
WHERE
|
|
23
|
+
datname = 'auth'
|
|
24
|
+
AND pid <> pg_backend_pid();
|
|
22
25
|
|
|
23
26
|
DROP DATABASE IF EXISTS auth;
|
|
24
27
|
|
|
25
28
|
CREATE DATABASE auth
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
WITH
|
|
30
|
+
OWNER = auth ENCODING = 'UTF8' CONNECTION
|
|
31
|
+
LIMIT
|
|
32
|
+
= -1;
|
|
30
33
|
|
|
31
34
|
COMMENT ON DATABASE auth IS 'SvelteKit Auth Example';
|
|
32
35
|
|
|
33
|
-
-- Connect to auth database
|
|
34
|
-
\connect auth
|
|
35
|
-
|
|
36
|
-
-- Required for password hashing
|
|
37
|
-
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
38
|
-
|
|
39
36
|
-- Required to generate UUIDs for sessions
|
|
40
37
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
41
38
|
|
|
@@ -43,16 +40,15 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
|
43
40
|
CREATE EXTENSION IF NOT EXISTS citext;
|
|
44
41
|
|
|
45
42
|
-- Using hard-coded roles (often this would be a table)
|
|
46
|
-
CREATE TYPE public.roles AS ENUM
|
|
47
|
-
('student', 'teacher', 'admin');
|
|
43
|
+
CREATE TYPE public.roles AS ENUM('student', 'teacher', 'admin');
|
|
48
44
|
|
|
49
45
|
ALTER TYPE public.roles OWNER TO auth;
|
|
50
46
|
|
|
51
47
|
-- Domains
|
|
52
48
|
CREATE DOMAIN public.email_address AS citext CHECK (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
length(VALUE) <= 254
|
|
50
|
+
AND VALUE = btrim(VALUE)
|
|
51
|
+
AND VALUE ~* '^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$'
|
|
56
52
|
);
|
|
57
53
|
|
|
58
54
|
COMMENT ON DOMAIN public.email_address IS 'RFC-compliant email address (case-insensitive, max 254 chars)';
|
|
@@ -62,65 +58,54 @@ CREATE DOMAIN public.persons_name AS text CHECK (length(VALUE) <= 20) NOT NULL;
|
|
|
62
58
|
COMMENT ON DOMAIN public.persons_name IS 'Person first or last name (max 20 characters)';
|
|
63
59
|
|
|
64
60
|
CREATE DOMAIN public.phone_number AS text CHECK (
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
VALUE IS NULL
|
|
62
|
+
OR length(VALUE) <= 50
|
|
67
63
|
);
|
|
68
64
|
|
|
69
65
|
COMMENT ON DOMAIN public.phone_number IS 'Phone number (max 50 characters)';
|
|
70
66
|
|
|
71
67
|
CREATE TABLE IF NOT EXISTS public.users (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
68
|
+
id integer NOT NULL GENERATED ALWAYS AS IDENTITY (
|
|
69
|
+
INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1
|
|
70
|
+
),
|
|
71
|
+
role roles NOT NULL DEFAULT 'student'::roles,
|
|
72
|
+
email email_address NOT NULL,
|
|
73
|
+
password character varying(80) COLLATE pg_catalog."default",
|
|
74
|
+
first_name persons_name,
|
|
75
|
+
last_name persons_name,
|
|
76
|
+
opt_out boolean NOT NULL DEFAULT false,
|
|
77
|
+
email_verified boolean NOT NULL DEFAULT false,
|
|
78
|
+
phone phone_number,
|
|
79
|
+
CONSTRAINT users_pkey PRIMARY KEY (id),
|
|
80
|
+
CONSTRAINT users_email_unique UNIQUE (email)
|
|
83
81
|
) TABLESPACE pg_default;
|
|
84
82
|
|
|
85
83
|
ALTER TABLE public.users OWNER to auth;
|
|
86
84
|
|
|
87
|
-
CREATE INDEX users_first_name_index
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
TABLESPACE pg_default;
|
|
85
|
+
CREATE INDEX users_first_name_index ON public.users USING btree (
|
|
86
|
+
first_name COLLATE pg_catalog."default" ASC NULLS LAST
|
|
87
|
+
) TABLESPACE pg_default;
|
|
91
88
|
|
|
92
|
-
CREATE INDEX users_last_name_index
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
TABLESPACE pg_default;
|
|
89
|
+
CREATE INDEX users_last_name_index ON public.users USING btree (
|
|
90
|
+
last_name COLLATE pg_catalog."default" ASC NULLS LAST
|
|
91
|
+
) TABLESPACE pg_default;
|
|
96
92
|
|
|
97
|
-
CREATE INDEX users_password
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
TABLESPACE pg_default;
|
|
93
|
+
CREATE INDEX users_password ON public.users USING btree (
|
|
94
|
+
password COLLATE pg_catalog."default" ASC NULLS LAST
|
|
95
|
+
) TABLESPACE pg_default;
|
|
101
96
|
|
|
102
97
|
CREATE TABLE IF NOT EXISTS public.sessions (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
ON UPDATE CASCADE
|
|
110
|
-
ON DELETE CASCADE,
|
|
111
|
-
CONSTRAINT sessions_one_per_user UNIQUE (user_id)
|
|
98
|
+
id uuid NOT NULL DEFAULT uuid_generate_v4 (),
|
|
99
|
+
user_id integer NOT NULL,
|
|
100
|
+
expires timestamptz NOT NULL DEFAULT (CURRENT_TIMESTAMP + INTERVAL '2 hours'),
|
|
101
|
+
CONSTRAINT sessions_pkey PRIMARY KEY (id),
|
|
102
|
+
CONSTRAINT sessions_user_fkey FOREIGN KEY (user_id) REFERENCES public.users (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE,
|
|
103
|
+
CONSTRAINT sessions_one_per_user UNIQUE (user_id)
|
|
112
104
|
) TABLESPACE pg_default;
|
|
113
105
|
|
|
114
106
|
ALTER TABLE public.sessions OWNER to auth;
|
|
115
107
|
|
|
116
|
-
CREATE OR REPLACE FUNCTION public.authenticate(
|
|
117
|
-
input json,
|
|
118
|
-
OUT response json)
|
|
119
|
-
RETURNS json
|
|
120
|
-
LANGUAGE 'plpgsql'
|
|
121
|
-
COST 100
|
|
122
|
-
VOLATILE PARALLEL UNSAFE
|
|
123
|
-
AS $BODY$
|
|
108
|
+
CREATE OR REPLACE FUNCTION public.authenticate (input json, OUT response json) RETURNS json LANGUAGE 'plpgsql' COST 100 VOLATILE PARALLEL UNSAFE AS $BODY$
|
|
124
109
|
DECLARE
|
|
125
110
|
input_email text := trim(input->>'email');
|
|
126
111
|
input_password text := input->>'password';
|
|
@@ -149,15 +134,9 @@ BEGIN
|
|
|
149
134
|
END;
|
|
150
135
|
$BODY$;
|
|
151
136
|
|
|
152
|
-
ALTER FUNCTION public.authenticate(json) OWNER TO auth;
|
|
137
|
+
ALTER FUNCTION public.authenticate (json) OWNER TO auth;
|
|
153
138
|
|
|
154
|
-
CREATE OR REPLACE FUNCTION public.create_session(
|
|
155
|
-
input_user_id integer)
|
|
156
|
-
RETURNS uuid
|
|
157
|
-
LANGUAGE 'sql'
|
|
158
|
-
COST 100
|
|
159
|
-
VOLATILE PARALLEL UNSAFE
|
|
160
|
-
AS $BODY$
|
|
139
|
+
CREATE OR REPLACE FUNCTION public.create_session (input_user_id integer) RETURNS uuid LANGUAGE 'sql' COST 100 VOLATILE PARALLEL UNSAFE AS $BODY$
|
|
161
140
|
-- Remove expired sessions (index-friendly cleanup)
|
|
162
141
|
DELETE FROM sessions WHERE expires < CURRENT_TIMESTAMP;
|
|
163
142
|
-- Remove any existing session(s) for this user
|
|
@@ -166,12 +145,9 @@ AS $BODY$
|
|
|
166
145
|
INSERT INTO sessions(user_id) VALUES (input_user_id) RETURNING sessions.id;
|
|
167
146
|
$BODY$;
|
|
168
147
|
|
|
169
|
-
ALTER FUNCTION public.create_session(integer) OWNER TO auth;
|
|
148
|
+
ALTER FUNCTION public.create_session (integer) OWNER TO auth;
|
|
170
149
|
|
|
171
|
-
CREATE OR REPLACE FUNCTION public.get_session(input_session_id uuid)
|
|
172
|
-
RETURNS json
|
|
173
|
-
LANGUAGE 'sql'
|
|
174
|
-
AS $BODY$
|
|
150
|
+
CREATE OR REPLACE FUNCTION public.get_session (input_session_id uuid) RETURNS json LANGUAGE 'sql' AS $BODY$
|
|
175
151
|
SELECT json_build_object(
|
|
176
152
|
'id', sessions.user_id,
|
|
177
153
|
'role', users.role,
|
|
@@ -187,14 +163,11 @@ FROM sessions
|
|
|
187
163
|
WHERE sessions.id = input_session_id AND expires > CURRENT_TIMESTAMP LIMIT 1;
|
|
188
164
|
$BODY$;
|
|
189
165
|
|
|
190
|
-
ALTER FUNCTION public.get_session(uuid) OWNER TO auth;
|
|
166
|
+
ALTER FUNCTION public.get_session (uuid) OWNER TO auth;
|
|
191
167
|
|
|
192
168
|
-- Like get_session but also bumps the expiry (sliding sessions).
|
|
193
169
|
-- 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$
|
|
170
|
+
CREATE OR REPLACE FUNCTION public.get_and_update_session (input_session_id uuid) RETURNS json LANGUAGE 'plpgsql' AS $BODY$
|
|
198
171
|
DECLARE
|
|
199
172
|
result json;
|
|
200
173
|
BEGIN
|
|
@@ -224,14 +197,9 @@ BEGIN
|
|
|
224
197
|
END;
|
|
225
198
|
$BODY$;
|
|
226
199
|
|
|
227
|
-
ALTER FUNCTION public.get_and_update_session(uuid) OWNER TO auth;
|
|
200
|
+
ALTER FUNCTION public.get_and_update_session (uuid) OWNER TO auth;
|
|
228
201
|
|
|
229
|
-
CREATE OR REPLACE FUNCTION public.verify_email_and_create_session(input_id integer)
|
|
230
|
-
RETURNS uuid
|
|
231
|
-
LANGUAGE 'plpgsql'
|
|
232
|
-
COST 100
|
|
233
|
-
VOLATILE PARALLEL UNSAFE
|
|
234
|
-
AS $BODY$
|
|
202
|
+
CREATE OR REPLACE FUNCTION public.verify_email_and_create_session (input_id integer) RETURNS uuid LANGUAGE 'plpgsql' COST 100 VOLATILE PARALLEL UNSAFE AS $BODY$
|
|
235
203
|
DECLARE
|
|
236
204
|
session_id uuid;
|
|
237
205
|
BEGIN
|
|
@@ -241,16 +209,9 @@ BEGIN
|
|
|
241
209
|
END;
|
|
242
210
|
$BODY$;
|
|
243
211
|
|
|
244
|
-
ALTER FUNCTION public.verify_email_and_create_session(integer) OWNER TO auth;
|
|
212
|
+
ALTER FUNCTION public.verify_email_and_create_session (integer) OWNER TO auth;
|
|
245
213
|
|
|
246
|
-
CREATE OR REPLACE FUNCTION public.register(
|
|
247
|
-
input json,
|
|
248
|
-
OUT user_session json)
|
|
249
|
-
RETURNS json
|
|
250
|
-
LANGUAGE 'plpgsql'
|
|
251
|
-
COST 100
|
|
252
|
-
VOLATILE PARALLEL UNSAFE
|
|
253
|
-
AS $BODY$
|
|
214
|
+
CREATE OR REPLACE FUNCTION public.register (input json, OUT user_session json) RETURNS json LANGUAGE 'plpgsql' COST 100 VOLATILE PARALLEL UNSAFE AS $BODY$
|
|
254
215
|
DECLARE
|
|
255
216
|
input_email text := trim(input->>'email');
|
|
256
217
|
input_first_name text := trim(input->>'firstName');
|
|
@@ -273,16 +234,9 @@ BEGIN
|
|
|
273
234
|
END;
|
|
274
235
|
$BODY$;
|
|
275
236
|
|
|
276
|
-
ALTER FUNCTION public.register(json) OWNER TO auth;
|
|
237
|
+
ALTER FUNCTION public.register (json) OWNER TO auth;
|
|
277
238
|
|
|
278
|
-
CREATE OR REPLACE FUNCTION public.start_gmail_user_session(
|
|
279
|
-
input json,
|
|
280
|
-
OUT user_session json)
|
|
281
|
-
RETURNS json
|
|
282
|
-
LANGUAGE 'plpgsql'
|
|
283
|
-
COST 100
|
|
284
|
-
VOLATILE PARALLEL UNSAFE
|
|
285
|
-
AS $BODY$
|
|
239
|
+
CREATE OR REPLACE FUNCTION public.start_gmail_user_session (input json, OUT user_session json) RETURNS json LANGUAGE 'plpgsql' COST 100 VOLATILE PARALLEL UNSAFE AS $BODY$
|
|
286
240
|
DECLARE
|
|
287
241
|
input_email varchar(80) := LOWER(TRIM((input->>'email')::varchar));
|
|
288
242
|
input_first_name varchar(20) := TRIM((input->>'firstName')::varchar);
|
|
@@ -303,36 +257,27 @@ BEGIN
|
|
|
303
257
|
END;
|
|
304
258
|
$BODY$;
|
|
305
259
|
|
|
306
|
-
ALTER FUNCTION public.start_gmail_user_session(json) OWNER TO auth;
|
|
260
|
+
ALTER FUNCTION public.start_gmail_user_session (json) OWNER TO auth;
|
|
307
261
|
|
|
308
|
-
CREATE PROCEDURE public.delete_session(input_id integer)
|
|
309
|
-
LANGUAGE sql
|
|
310
|
-
AS $$
|
|
262
|
+
CREATE PROCEDURE public.delete_session (input_id integer) LANGUAGE sql AS $$
|
|
311
263
|
DELETE FROM sessions WHERE user_id = input_id;
|
|
312
264
|
$$;
|
|
313
265
|
|
|
314
|
-
CREATE OR REPLACE PROCEDURE public.delete_user(input_id integer)
|
|
315
|
-
LANGUAGE sql
|
|
316
|
-
AS $$
|
|
266
|
+
CREATE OR REPLACE PROCEDURE public.delete_user (input_id integer) LANGUAGE sql AS $$
|
|
317
267
|
DELETE FROM users WHERE id = input_id;
|
|
318
268
|
$$;
|
|
319
269
|
|
|
320
|
-
ALTER PROCEDURE public.delete_user(integer) OWNER TO auth;
|
|
270
|
+
ALTER PROCEDURE public.delete_user (integer) OWNER TO auth;
|
|
321
271
|
|
|
322
|
-
CREATE OR REPLACE PROCEDURE public.reset_password(IN input_id integer, IN input_password text)
|
|
323
|
-
LANGUAGE plpgsql
|
|
324
|
-
AS $procedure$
|
|
272
|
+
CREATE OR REPLACE PROCEDURE public.reset_password (IN input_id integer, IN input_password text) LANGUAGE plpgsql AS $procedure$
|
|
325
273
|
BEGIN
|
|
326
274
|
UPDATE users SET password = crypt(input_password, gen_salt('bf', 12)) WHERE id = input_id;
|
|
327
275
|
END;
|
|
328
|
-
$procedure
|
|
329
|
-
;
|
|
276
|
+
$procedure$;
|
|
330
277
|
|
|
331
|
-
ALTER PROCEDURE public.reset_password(integer, text) OWNER TO auth;
|
|
278
|
+
ALTER PROCEDURE public.reset_password (integer, text) OWNER TO auth;
|
|
332
279
|
|
|
333
|
-
CREATE OR REPLACE PROCEDURE public.upsert_user(input json)
|
|
334
|
-
LANGUAGE plpgsql
|
|
335
|
-
AS $BODY$
|
|
280
|
+
CREATE OR REPLACE PROCEDURE public.upsert_user (input json) LANGUAGE plpgsql AS $BODY$
|
|
336
281
|
DECLARE
|
|
337
282
|
input_id integer := COALESCE((input->>'id')::integer,0);
|
|
338
283
|
input_role roles := COALESCE((input->>'role')::roles, 'student');
|
|
@@ -364,11 +309,9 @@ BEGIN
|
|
|
364
309
|
END;
|
|
365
310
|
$BODY$;
|
|
366
311
|
|
|
367
|
-
ALTER PROCEDURE public.upsert_user(json) OWNER TO auth;
|
|
312
|
+
ALTER PROCEDURE public.upsert_user (json) OWNER TO auth;
|
|
368
313
|
|
|
369
|
-
CREATE OR REPLACE PROCEDURE public.update_user(input_id integer, input json)
|
|
370
|
-
LANGUAGE plpgsql
|
|
371
|
-
AS $BODY$
|
|
314
|
+
CREATE OR REPLACE PROCEDURE public.update_user (input_id integer, input json) LANGUAGE plpgsql AS $BODY$
|
|
372
315
|
DECLARE
|
|
373
316
|
input_email varchar(80) := LOWER(TRIM((input->>'email')::varchar));
|
|
374
317
|
input_password varchar(80) := COALESCE((input->>'password')::varchar, '');
|
|
@@ -389,8 +332,67 @@ BEGIN
|
|
|
389
332
|
END;
|
|
390
333
|
$BODY$;
|
|
391
334
|
|
|
392
|
-
ALTER PROCEDURE public.update_user(integer, json) OWNER TO auth;
|
|
335
|
+
ALTER PROCEDURE public.update_user (integer, json) OWNER TO auth;
|
|
336
|
+
|
|
337
|
+
-- MFA codes table (one pending code per user, replaced on each new login attempt)
|
|
338
|
+
CREATE TABLE IF NOT EXISTS public.mfa_codes (
|
|
339
|
+
user_id integer NOT NULL,
|
|
340
|
+
code varchar(6) NOT NULL,
|
|
341
|
+
expires timestamptz NOT NULL DEFAULT (CURRENT_TIMESTAMP + INTERVAL '10 minutes'),
|
|
342
|
+
CONSTRAINT mfa_codes_pkey PRIMARY KEY (user_id),
|
|
343
|
+
CONSTRAINT mfa_codes_user_fkey FOREIGN KEY (user_id) REFERENCES public.users (id) ON DELETE CASCADE
|
|
344
|
+
) TABLESPACE pg_default;
|
|
345
|
+
|
|
346
|
+
ALTER TABLE public.mfa_codes OWNER TO auth;
|
|
347
|
+
|
|
348
|
+
-- Generate and store a fresh 6-digit MFA code for a user, returning the code
|
|
349
|
+
CREATE OR REPLACE FUNCTION public.create_mfa_code (input_user_id integer) RETURNS varchar LANGUAGE plpgsql AS $BODY$
|
|
350
|
+
DECLARE
|
|
351
|
+
v_code varchar(6) := lpad(floor(random() * 1000000)::integer::text, 6, '0');
|
|
352
|
+
BEGIN
|
|
353
|
+
DELETE FROM mfa_codes WHERE expires < CURRENT_TIMESTAMP;
|
|
354
|
+
INSERT INTO mfa_codes(user_id, code)
|
|
355
|
+
VALUES (input_user_id, v_code)
|
|
356
|
+
ON CONFLICT (user_id) DO UPDATE
|
|
357
|
+
SET code = EXCLUDED.code,
|
|
358
|
+
expires = CURRENT_TIMESTAMP + INTERVAL '10 minutes';
|
|
359
|
+
RETURN v_code;
|
|
360
|
+
END;
|
|
361
|
+
$BODY$;
|
|
362
|
+
|
|
363
|
+
ALTER FUNCTION public.create_mfa_code (integer) OWNER TO auth;
|
|
364
|
+
|
|
365
|
+
-- Verify an MFA code for a given email address.
|
|
366
|
+
-- Returns the user_id on success (and deletes the code), or NULL on failure.
|
|
367
|
+
CREATE OR REPLACE FUNCTION public.verify_mfa_code (input_email text, input_code text) RETURNS integer LANGUAGE plpgsql AS $BODY$
|
|
368
|
+
DECLARE
|
|
369
|
+
v_user_id integer;
|
|
370
|
+
BEGIN
|
|
371
|
+
SELECT mfa_codes.user_id INTO v_user_id
|
|
372
|
+
FROM mfa_codes
|
|
373
|
+
INNER JOIN users ON mfa_codes.user_id = users.id
|
|
374
|
+
WHERE users.email = input_email
|
|
375
|
+
AND mfa_codes.code = input_code
|
|
376
|
+
AND mfa_codes.expires > CURRENT_TIMESTAMP;
|
|
377
|
+
|
|
378
|
+
IF FOUND THEN
|
|
379
|
+
DELETE FROM mfa_codes WHERE user_id = v_user_id;
|
|
380
|
+
END IF;
|
|
381
|
+
|
|
382
|
+
RETURN v_user_id;
|
|
383
|
+
END;
|
|
384
|
+
$BODY$;
|
|
385
|
+
|
|
386
|
+
ALTER FUNCTION public.verify_mfa_code (text, text) OWNER TO auth;
|
|
387
|
+
|
|
388
|
+
CALL public.upsert_user (
|
|
389
|
+
'{"id":0, "role":"admin", "email":"admin@example.com", "password":"admin123", "firstName":"Jane", "lastName":"Doe", "phone":"412-555-1212"}'::json
|
|
390
|
+
);
|
|
393
391
|
|
|
394
|
-
CALL public.upsert_user(
|
|
395
|
-
|
|
396
|
-
|
|
392
|
+
CALL public.upsert_user (
|
|
393
|
+
'{"id":0, "role":"teacher", "email":"teacher@example.com", "password":"teacher123", "firstName":"John", "lastName":"Doe", "phone":"724-555-1212"}'::json
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
CALL public.upsert_user (
|
|
397
|
+
'{"id":0, "role":"student", "email":"student@example.com", "password":"student123", "firstName":"Justin", "lastName":"Case", "phone":"814-555-1212"}'::json
|
|
398
|
+
);
|