sveltekit-auth-example 5.5.0 → 5.6.1

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/README.md +12 -12
  3. package/db_create.sh +13 -0
  4. package/db_create.sql +15 -376
  5. package/db_schema.sql +369 -0
  6. package/package.json +5 -2
  7. package/src/app.d.ts +22 -1
  8. package/src/hooks.server.ts +47 -12
  9. package/src/lib/app-state.svelte.ts +8 -0
  10. package/src/lib/auth-redirect.ts +4 -0
  11. package/src/lib/focus.ts +8 -0
  12. package/src/lib/google.ts +17 -0
  13. package/src/lib/server/db.ts +9 -0
  14. package/src/lib/server/email/index.ts +1 -0
  15. package/src/lib/server/email/mfa-code.ts +22 -0
  16. package/src/lib/server/email/password-reset.ts +6 -0
  17. package/src/lib/server/email/verify-email.ts +6 -0
  18. package/src/lib/server/sendgrid.ts +9 -0
  19. package/src/routes/+layout.server.ts +10 -1
  20. package/src/routes/+layout.svelte +103 -28
  21. package/src/routes/admin/+page.server.ts +8 -0
  22. package/src/routes/api/v1/user/+server.ts +20 -0
  23. package/src/routes/auth/[slug]/+server.ts +9 -2
  24. package/src/routes/auth/forgot/+server.ts +10 -0
  25. package/src/routes/auth/google/+server.ts +35 -4
  26. package/src/routes/auth/login/+server.ts +67 -10
  27. package/src/routes/auth/logout/+server.ts +10 -0
  28. package/src/routes/auth/mfa/+server.ts +75 -0
  29. package/src/routes/auth/register/+server.ts +21 -1
  30. package/src/routes/auth/reset/+server.ts +15 -0
  31. package/src/routes/auth/reset/[token]/+page.svelte +16 -8
  32. package/src/routes/auth/reset/[token]/+page.ts +8 -0
  33. package/src/routes/auth/verify/[token]/+server.ts +12 -1
  34. package/src/routes/forgot/+page.svelte +13 -8
  35. package/src/routes/layout.css +3 -3
  36. package/src/routes/login/+page.server.ts +8 -0
  37. package/src/routes/login/+page.svelte +222 -77
  38. package/src/routes/profile/+page.server.ts +9 -0
  39. package/src/routes/profile/+page.svelte +32 -12
  40. package/src/routes/register/+page.server.ts +8 -1
  41. package/src/routes/register/+page.svelte +160 -122
  42. package/src/routes/teachers/+page.server.ts +9 -0
  43. package/src/service-worker.ts +17 -1
package/CHANGELOG.md CHANGED
@@ -2,9 +2,27 @@
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.1
6
+
7
+ - Add JSDoc comments throughout the codebase (`app.d.ts`, server hooks, route handlers, lib utilities, Svelte components)
8
+
9
+ # 5.6.0
10
+
11
+ - Split `db_create.sql` into `db_create.sql` (role + database creation) and `db_schema.sql` (schema, functions, seed data)
12
+ - Add `db_create.sh` shell wrapper to run both files in sequence
13
+ - `db_schema.sql` is pure SQL and fully prettier-formattable
14
+ - README updated to use `bash db_create.sh`
15
+
5
16
  # 5.5.0
6
17
 
7
- - Extract email templates into dedicated files under `src/lib/server/email/` (`password-reset.ts`, `verify-email.ts`)
18
+ - Multi-factor authentication (MFA) via email for local accounts (Google Sign In is exempt)
19
+ - 6-digit OTP code sent via SendGrid, expires in 10 minutes
20
+ - 30-day trusted-device cookie (signed JWT) suppresses MFA on subsequent logins from the same device
21
+ - New `mfa_codes` table in PostgreSQL with `create_mfa_code()` and `verify_mfa_code()` functions
22
+ - New `/auth/mfa` endpoint verifies the code, creates the session, and issues the trusted-device cookie
23
+ - Login page shows an inline MFA step when a code is required
24
+ - `/auth/mfa` added to rate-limited paths
25
+ - Extract email templates into dedicated files under `src/lib/server/email/` (`password-reset.ts`, `verify-email.ts`, `mfa-code.ts`)
8
26
  - `src/lib/server/email/index.ts` re-exports all templates for clean imports
9
27
 
10
28
  # 5.1.3
package/README.md CHANGED
@@ -8,14 +8,14 @@ A complete, production-ready authentication and authorization starter for **Svel
8
8
 
9
9
  ## Features
10
10
 
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 |
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 |
19
19
 
20
20
  ## Stack
21
21
 
@@ -44,7 +44,7 @@ cd sveltekit-auth-example
44
44
  yarn install
45
45
 
46
46
  # Create PostgreSQL database (only works if you have PostgreSQL installed)
47
- psql -d postgres -f db_create.sql
47
+ bash db_create.sh
48
48
  ```
49
49
 
50
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.
@@ -84,9 +84,9 @@ yarn preview
84
84
 
85
85
  The db_create.sql script adds three users to the database with obvious roles:
86
86
 
87
- | Email | Password | Role |
88
- |---|---|---|
89
- | admin@example.com | admin123 | admin |
87
+ | Email | Password | Role |
88
+ | ------------------- | ---------- | ------- |
89
+ | admin@example.com | admin123 | admin |
90
90
  | teacher@example.com | teacher123 | teacher |
91
91
  | student@example.com | student123 | student |
92
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,8 @@
1
- -- Invoke this script via
2
- -- $ psql -d postgres -f db_create.sql
1
+ -- Run via db_create.sh or:
2
+ -- $ psql -d postgres -f db_create.sql && psql -d auth -f db_schema.sql
3
3
 
4
4
  -- Create role if not already there
5
- DO
6
- $do$
5
+ DO $do$
7
6
  BEGIN
8
7
  IF NOT EXISTS (
9
8
  SELECT -- SELECT list can stay empty for this
@@ -16,381 +15,21 @@ END
16
15
  $do$;
17
16
 
18
17
  -- Forcefully disconnect anyone
19
- SELECT pid, pg_terminate_backend(pid)
20
- FROM pg_stat_activity
21
- WHERE datname = 'auth' AND pid <> pg_backend_pid();
18
+ SELECT
19
+ pid,
20
+ pg_terminate_backend(pid)
21
+ FROM
22
+ pg_stat_activity
23
+ WHERE
24
+ datname = 'auth'
25
+ AND pid <> pg_backend_pid();
22
26
 
23
27
  DROP DATABASE IF EXISTS auth;
24
28
 
25
29
  CREATE DATABASE auth
26
- WITH
27
- OWNER = auth
28
- ENCODING = 'UTF8'
29
- CONNECTION LIMIT = -1;
30
+ WITH
31
+ OWNER = auth ENCODING = 'UTF8' CONNECTION
32
+ LIMIT
33
+ = -1;
30
34
 
31
35
  COMMENT ON DATABASE auth IS 'SvelteKit Auth Example';
32
-
33
- -- Connect to auth database
34
- \connect auth
35
-
36
- -- Required for password hashing
37
- CREATE EXTENSION IF NOT EXISTS pgcrypto;
38
-
39
- -- Required to generate UUIDs for sessions
40
- CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
41
-
42
- -- Required for case-insensitive text (email_address domain)
43
- CREATE EXTENSION IF NOT EXISTS citext;
44
-
45
- -- Using hard-coded roles (often this would be a table)
46
- CREATE TYPE public.roles AS ENUM
47
- ('student', 'teacher', 'admin');
48
-
49
- ALTER TYPE public.roles OWNER TO auth;
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
-
71
- CREATE TABLE IF NOT EXISTS public.users (
72
- id integer NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ),
73
- role roles NOT NULL DEFAULT 'student'::roles,
74
- email email_address NOT NULL,
75
- password character varying(80) COLLATE pg_catalog."default",
76
- first_name persons_name,
77
- last_name persons_name,
78
- opt_out boolean NOT NULL DEFAULT false,
79
- email_verified boolean NOT NULL DEFAULT false,
80
- phone phone_number,
81
- CONSTRAINT users_pkey PRIMARY KEY (id),
82
- CONSTRAINT users_email_unique UNIQUE (email)
83
- ) TABLESPACE pg_default;
84
-
85
- ALTER TABLE public.users OWNER to auth;
86
-
87
- CREATE INDEX users_first_name_index
88
- ON public.users USING btree
89
- (first_name COLLATE pg_catalog."default" ASC NULLS LAST)
90
- TABLESPACE pg_default;
91
-
92
- CREATE INDEX users_last_name_index
93
- ON public.users USING btree
94
- (last_name COLLATE pg_catalog."default" ASC NULLS LAST)
95
- TABLESPACE pg_default;
96
-
97
- CREATE INDEX users_password
98
- ON public.users USING btree
99
- (password COLLATE pg_catalog."default" ASC NULLS LAST)
100
- TABLESPACE pg_default;
101
-
102
- CREATE TABLE IF NOT EXISTS public.sessions (
103
- id uuid NOT NULL DEFAULT uuid_generate_v4(),
104
- user_id integer NOT NULL,
105
- expires timestamptz NOT NULL DEFAULT (CURRENT_TIMESTAMP + INTERVAL '2 hours'),
106
- CONSTRAINT sessions_pkey PRIMARY KEY (id),
107
- CONSTRAINT sessions_user_fkey FOREIGN KEY (user_id)
108
- REFERENCES public.users (id) MATCH SIMPLE
109
- ON UPDATE CASCADE
110
- ON DELETE CASCADE,
111
- CONSTRAINT sessions_one_per_user UNIQUE (user_id)
112
- ) TABLESPACE pg_default;
113
-
114
- ALTER TABLE public.sessions OWNER to auth;
115
-
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$
124
- DECLARE
125
- input_email text := trim(input->>'email');
126
- input_password text := input->>'password';
127
- v_user users%ROWTYPE;
128
- BEGIN
129
- IF input_email IS NULL OR input_password IS NULL THEN
130
- response := json_build_object('statusCode', 400, 'status', 'Please provide an email address and password to authenticate.', 'user', NULL, 'sessionId', NULL);
131
- RETURN;
132
- END IF;
133
-
134
- SELECT * INTO v_user FROM users
135
- WHERE email = input_email AND password = crypt(input_password, password) LIMIT 1;
136
-
137
- IF NOT FOUND THEN
138
- response := json_build_object('statusCode', 401, 'status', 'Invalid username/password combination.', 'user', NULL, 'sessionId', NULL);
139
- ELSIF NOT v_user.email_verified THEN
140
- response := json_build_object('statusCode', 403, 'status', 'Please verify your email address before logging in.', 'user', NULL, 'sessionId', NULL);
141
- ELSE
142
- response := json_build_object(
143
- 'statusCode', 200,
144
- 'status', 'Login successful.',
145
- 'user', json_build_object('id', v_user.id, 'role', v_user.role, 'email', input_email, 'firstName', v_user.first_name, 'lastName', v_user.last_name, 'phone', v_user.phone, 'optOut', v_user.opt_out),
146
- 'sessionId', create_session(v_user.id)
147
- );
148
- END IF;
149
- END;
150
- $BODY$;
151
-
152
- ALTER FUNCTION public.authenticate(json) OWNER TO auth;
153
-
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$
161
- -- Remove expired sessions (index-friendly cleanup)
162
- DELETE FROM sessions WHERE expires < CURRENT_TIMESTAMP;
163
- -- Remove any existing session(s) for this user
164
- DELETE FROM sessions WHERE user_id = input_user_id;
165
- -- Create the new session
166
- INSERT INTO sessions(user_id) VALUES (input_user_id) RETURNING sessions.id;
167
- $BODY$;
168
-
169
- ALTER FUNCTION public.create_session(integer) OWNER TO auth;
170
-
171
- CREATE OR REPLACE FUNCTION public.get_session(input_session_id uuid)
172
- RETURNS json
173
- LANGUAGE 'sql'
174
- AS $BODY$
175
- SELECT json_build_object(
176
- 'id', sessions.user_id,
177
- 'role', users.role,
178
- 'email', users.email,
179
- 'firstName', users.first_name,
180
- 'lastName', users.last_name,
181
- 'phone', users.phone,
182
- 'optOut', users.opt_out,
183
- 'expires', sessions.expires
184
- ) AS user
185
- FROM sessions
186
- INNER JOIN users ON sessions.user_id = users.id
187
- WHERE sessions.id = input_session_id AND expires > CURRENT_TIMESTAMP LIMIT 1;
188
- $BODY$;
189
-
190
- ALTER FUNCTION public.get_session(uuid) OWNER TO auth;
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
-
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$
235
- DECLARE
236
- session_id uuid;
237
- BEGIN
238
- UPDATE users SET email_verified = true WHERE id = input_id;
239
- SELECT create_session(input_id) INTO session_id;
240
- RETURN session_id;
241
- END;
242
- $BODY$;
243
-
244
- ALTER FUNCTION public.verify_email_and_create_session(integer) OWNER TO auth;
245
-
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$
254
- DECLARE
255
- input_email text := trim(input->>'email');
256
- input_first_name text := trim(input->>'firstName');
257
- input_last_name text := trim(input->>'lastName');
258
- input_phone text := trim(input->>'phone');
259
- input_password text := input->>'password';
260
- BEGIN
261
- PERFORM id FROM users WHERE email = input_email;
262
- IF NOT FOUND THEN
263
- INSERT INTO users(role, password, email, first_name, last_name, phone)
264
- VALUES('student', crypt(input_password, gen_salt('bf', 12)), input_email, input_first_name, input_last_name, input_phone)
265
- RETURNING
266
- json_build_object(
267
- 'sessionId', create_session(users.id),
268
- '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)
269
- ) INTO user_session;
270
- ELSE -- user is registering account that already exists so set sessionId and user to null so client can let them know
271
- SELECT authenticate(input) INTO user_session;
272
- END IF;
273
- END;
274
- $BODY$;
275
-
276
- ALTER FUNCTION public.register(json) OWNER TO auth;
277
-
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$
286
- DECLARE
287
- input_email varchar(80) := LOWER(TRIM((input->>'email')::varchar));
288
- input_first_name varchar(20) := TRIM((input->>'firstName')::varchar);
289
- input_last_name varchar(20) := TRIM((input->>'lastName')::varchar);
290
- BEGIN
291
- -- Google verifies email ownership; mark user as verified on every sign-in
292
- UPDATE users SET email_verified = true WHERE email = input_email;
293
- 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;
294
- IF NOT FOUND THEN
295
- INSERT INTO users(role, email, first_name, last_name, email_verified)
296
- VALUES('student', input_email, input_first_name, input_last_name, true)
297
- RETURNING
298
- json_build_object(
299
- 'id', create_session(users.id),
300
- 'user', json_build_object('id', users.id, 'role', 'student', 'email', input_email, 'firstName', input_first_name, 'lastName', input_last_name, 'phone', null)
301
- ) INTO user_session;
302
- END IF;
303
- END;
304
- $BODY$;
305
-
306
- ALTER FUNCTION public.start_gmail_user_session(json) OWNER TO auth;
307
-
308
- CREATE PROCEDURE public.delete_session(input_id integer)
309
- LANGUAGE sql
310
- AS $$
311
- DELETE FROM sessions WHERE user_id = input_id;
312
- $$;
313
-
314
- CREATE OR REPLACE PROCEDURE public.delete_user(input_id integer)
315
- LANGUAGE sql
316
- AS $$
317
- DELETE FROM users WHERE id = input_id;
318
- $$;
319
-
320
- ALTER PROCEDURE public.delete_user(integer) OWNER TO auth;
321
-
322
- CREATE OR REPLACE PROCEDURE public.reset_password(IN input_id integer, IN input_password text)
323
- LANGUAGE plpgsql
324
- AS $procedure$
325
- BEGIN
326
- UPDATE users SET password = crypt(input_password, gen_salt('bf', 12)) WHERE id = input_id;
327
- END;
328
- $procedure$
329
- ;
330
-
331
- ALTER PROCEDURE public.reset_password(integer, text) OWNER TO auth;
332
-
333
- CREATE OR REPLACE PROCEDURE public.upsert_user(input json)
334
- LANGUAGE plpgsql
335
- AS $BODY$
336
- DECLARE
337
- input_id integer := COALESCE((input->>'id')::integer,0);
338
- input_role roles := COALESCE((input->>'role')::roles, 'student');
339
- input_email varchar(80) := LOWER(TRIM((input->>'email')::varchar));
340
- input_password varchar(80) := COALESCE((input->>'password')::varchar, '');
341
- input_first_name varchar(20) := TRIM((input->>'firstName')::varchar);
342
- input_last_name varchar(20) := TRIM((input->>'lastName')::varchar);
343
- input_phone varchar(23) := TRIM((input->>'phone')::varchar);
344
- BEGIN
345
- IF input_id = 0 THEN
346
- INSERT INTO users (role, email, password, first_name, last_name, phone, email_verified)
347
- VALUES (
348
- input_role, input_email, crypt(input_password, gen_salt('bf', 12)),
349
- input_first_name, input_last_name, input_phone, true);
350
- ELSE
351
- UPDATE users SET
352
- role = input_role,
353
- email = input_email,
354
- email_verified = true,
355
- password = CASE WHEN input_password = ''
356
- THEN password -- leave as is (we are updating fields other than the password)
357
- ELSE crypt(input_password, gen_salt('bf', 12))
358
- END,
359
- first_name = input_first_name,
360
- last_name = input_last_name,
361
- phone = input_phone
362
- WHERE id = input_id;
363
- END IF;
364
- END;
365
- $BODY$;
366
-
367
- ALTER PROCEDURE public.upsert_user(json) OWNER TO auth;
368
-
369
- CREATE OR REPLACE PROCEDURE public.update_user(input_id integer, input json)
370
- LANGUAGE plpgsql
371
- AS $BODY$
372
- DECLARE
373
- input_email varchar(80) := LOWER(TRIM((input->>'email')::varchar));
374
- input_password varchar(80) := COALESCE((input->>'password')::varchar, '');
375
- input_first_name varchar(20) := TRIM((input->>'firstName')::varchar);
376
- input_last_name varchar(20) := TRIM((input->>'lastName')::varchar);
377
- input_phone varchar(23) := TRIM((input->>'phone')::varchar);
378
- BEGIN
379
- UPDATE users SET
380
- email = input_email,
381
- password = CASE WHEN input_password = ''
382
- THEN password -- leave as is (we are updating fields other than the password)
383
- ELSE crypt(input_password, gen_salt('bf', 12))
384
- END,
385
- first_name = input_first_name,
386
- last_name = input_last_name,
387
- phone = input_phone
388
- WHERE id = input_id;
389
- END;
390
- $BODY$;
391
-
392
- ALTER PROCEDURE public.update_user(integer, json) OWNER TO auth;
393
-
394
- CALL public.upsert_user('{"id":0, "role":"admin", "email":"admin@example.com", "password":"admin123", "firstName":"Jane", "lastName":"Doe", "phone":"412-555-1212"}'::json);
395
- CALL public.upsert_user('{"id":0, "role":"teacher", "email":"teacher@example.com", "password":"teacher123", "firstName":"John", "lastName":"Doe", "phone":"724-555-1212"}'::json);
396
- CALL public.upsert_user('{"id":0, "role":"student", "email":"student@example.com", "password":"student123", "firstName":"Justin", "lastName":"Case", "phone":"814-555-1212"}'::json);