@crossauth/sveltekit 1.1.0 → 1.1.2

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 (46) hide show
  1. package/dist/index.d.ts +5 -5
  2. package/dist/index.js +16 -6181
  3. package/dist/sveltekitadminclientendpoints.d.ts +31 -30
  4. package/dist/sveltekitadminclientendpoints.js +187 -0
  5. package/dist/sveltekitadminendpoints.d.ts +106 -77
  6. package/dist/sveltekitadminendpoints.js +776 -0
  7. package/dist/sveltekitapikey.d.ts +4 -4
  8. package/dist/sveltekitapikey.js +81 -0
  9. package/dist/sveltekitoauthclient.d.ts +6 -5
  10. package/dist/sveltekitoauthclient.js +2309 -0
  11. package/dist/sveltekitoauthserver.d.ts +11 -11
  12. package/dist/sveltekitoauthserver.js +1350 -0
  13. package/dist/sveltekitresserver.d.ts +6 -5
  14. package/dist/sveltekitresserver.js +286 -0
  15. package/dist/sveltekitserver.d.ts +11 -10
  16. package/dist/sveltekitserver.js +393 -0
  17. package/dist/sveltekitsession.d.ts +5 -5
  18. package/dist/sveltekitsession.js +1112 -0
  19. package/dist/sveltekitsessionadapter.d.ts +2 -3
  20. package/dist/sveltekitsessionadapter.js +2 -0
  21. package/dist/sveltekitsharedclientendpoints.d.ts +38 -30
  22. package/dist/sveltekitsharedclientendpoints.js +635 -0
  23. package/dist/sveltekituserclientendpoints.d.ts +31 -30
  24. package/dist/sveltekituserclientendpoints.js +270 -0
  25. package/dist/sveltekituserendpoints.d.ts +220 -113
  26. package/dist/sveltekituserendpoints.js +1836 -0
  27. package/dist/tests/sveltekitadminclientendpoints.test.js +330 -0
  28. package/dist/tests/sveltekitadminendpoints.test.js +242 -0
  29. package/dist/tests/sveltekitapikeyserver.test.js +44 -0
  30. package/dist/tests/sveltekitoauthclient.test.d.ts +5 -5
  31. package/dist/tests/sveltekitoauthclient.test.js +1016 -0
  32. package/dist/tests/sveltekitoauthresserver.test.d.ts +4 -4
  33. package/dist/tests/sveltekitoauthresserver.test.js +185 -0
  34. package/dist/tests/sveltekitoauthserver.test.js +673 -0
  35. package/dist/tests/sveltekituserclientendpoints.test.js +244 -0
  36. package/dist/tests/sveltekituserendpoints.test.js +152 -0
  37. package/dist/tests/sveltemock.test.js +36 -0
  38. package/dist/tests/sveltemocks.d.ts +2 -3
  39. package/dist/tests/sveltemocks.js +114 -0
  40. package/dist/tests/sveltesessionhooks.test.js +224 -0
  41. package/dist/tests/testshared.d.ts +8 -8
  42. package/dist/tests/testshared.js +344 -0
  43. package/dist/utils.d.ts +1 -2
  44. package/dist/utils.js +123 -0
  45. package/package.json +6 -4
  46. package/dist/index.cjs +0 -1
@@ -0,0 +1,776 @@
1
+ // Copyright (c) 2026 Matthew Baker. All rights reserved. Licenced under the Apache Licence 2.0. See LICENSE file
2
+ import { SvelteKitServer } from './sveltekitserver';
3
+ import { SvelteKitSessionServer } from './sveltekitsession';
4
+ import { TokenEmailer } from '@crossauth/backend';
5
+ import { CrossauthError, CrossauthLogger, j, ErrorCode, UserState } from '@crossauth/common';
6
+ import { JsonOrFormData } from './utils';
7
+ ;
8
+ ;
9
+ ;
10
+ ;
11
+ ;
12
+ ;
13
+ ;
14
+ ;
15
+ ;
16
+ ;
17
+ /**
18
+ * Default function for searching users.
19
+ *
20
+ * Returns the user who's username exactly matches the search term
21
+ *
22
+ * @param searchTerm the term to search for
23
+ * @param userStorage where users are stored
24
+ * @param skip skip this many matches from the start
25
+ * @param _take take this many results (ignored as always returns 0 or 1 matches)
26
+ * @returns matching users as an array
27
+ */
28
+ async function defaultUserSearchFn(searchTerm, userStorage, skip = 0, _take = 10) {
29
+ let users = [];
30
+ if (skip > 0)
31
+ return [];
32
+ try {
33
+ const { user } = await userStorage.getUserByUsername(searchTerm);
34
+ users.push(user);
35
+ }
36
+ catch (e1) {
37
+ const ce1 = CrossauthError.asCrossauthError(e1);
38
+ if (ce1.code != ErrorCode.UserNotExist) {
39
+ CrossauthLogger.logger.debug(j({ err: ce1 }));
40
+ throw ce1;
41
+ }
42
+ try {
43
+ const { user } = await userStorage.getUserByEmail(searchTerm);
44
+ users.push(user);
45
+ }
46
+ catch (e2) {
47
+ const ce2 = CrossauthError.asCrossauthError(e2);
48
+ if (ce2.code != ErrorCode.UserNotExist) {
49
+ CrossauthLogger.logger.debug(j({ err: ce2 }));
50
+ throw ce1;
51
+ }
52
+ }
53
+ }
54
+ return users;
55
+ }
56
+ /**
57
+ * Provides endpoints for users to login, logout and maintain their
58
+ * own account.
59
+ *
60
+ * This is created automatically when {@link SvelteKitServer} is instantiated.
61
+ * The endpoints are available through `SvelteKitServer.sessionServer.adminEndpoints`.
62
+ *
63
+ * The methods in this class are designed to be used in
64
+ * `+*_server.ts` files in the `load` and `actions` exports. You can
65
+ * either use the low-level functions such as {@link updateUser} or use
66
+ * the `action` and `load` members of the endpoint objects.
67
+ * For example, for {@link updateUserEndpoint}
68
+ *
69
+ * ```
70
+ * export const load = crossauth.sessionServer?.adminEndpoints.updateUserEndpoint.load ?? crossauth.dummyLoad;
71
+ * export const actions = crossauth.sessionServer?.adminEndpoints.updateUserEndpoint.actions ?? crossauth.dummyActions;
72
+ * ```
73
+ * The `?? crossauth.dummyLoad` and `?? crossauth.dummyActions` is to stop
74
+ * typescript complaining as the `sessionServer` member of the
75
+ * {@link SvelteKitServer} object may be undefined, because
76
+ * some application do not have a session server.
77
+ *
78
+ * **Endpoints**
79
+ *
80
+ * These endpoints can only be called if an admin user is logged in, as defined
81
+ * by the {@link SvelteKitServer.isAdminFn}. If the user does not
82
+ * have this permission, a 401 error is raised.
83
+ *
84
+ * | Name | Description | PageData (returned by load) | ActionData (return by actions) | Form fields expected by actions | URL param |
85
+ * | -------------------------- | ---------------------------------------------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------- | --------- |
86
+ * | baseEndpoint | This PageData is returned by all endpoints' load function. | - `user` logged in {@link @crossauth/common!User} | *Not provided* | | |
87
+ * | | | - `csrfToken` CSRF token if enabled | | | | | loginPage |
88
+ * | -------------------------- | ---------------------------------------------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------- | --------- |
89
+ * | searchUsersEndpoint | Returns a paginated set of users or those matchign search | See return of {@link SvelteKitAdminEndpoints.searchUsers} | *Not provided* | | |
90
+ * | -------------------------- | ---------------------------------------------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------- | --------- |
91
+ * | updateUserEndpoint | Update a user's details | - `allowedFactor2` see {@link SvelteKitAdminEndpoints}.`signupEndpoint` | `default`: | `default`: | `id` |
92
+ * | | | - `editUser` the {@link @crossauth/common!User} being edited | - see {@link SvelteKitAdminEndpoints.updateUser} return | - see {@link SvelteKitAdminEndpoints.updateUser} event | |
93
+ * | -------------------------- | ---------------------------------------------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------- | --------- |
94
+ * | changePasswordEndpoint | Update a user's password | - `next` page to load on szccess | `default`: | `default`: | `id` |
95
+ * | | | - `editUser` the {@link @crossauth/common!User} being edited | - see {@link SvelteKitAdminEndpoints.changePassword} return | - see {@link SvelteKitAdminEndpoints.changePassword} event | |
96
+ * | -------------------------- | ---------------------------------------------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------- | --------- |
97
+ * | createUserEndpoint | Creates a new user | - `allowedFactor2` see {@link SvelteKitAdminEndpoints}.`signupEndpoint` | `default`: | `default`: | `id` |
98
+ * | | | | - see {@link SvelteKitAdminEndpoints.createUser} return | - see {@link SvelteKitAdminEndpoints.createUser} event | |
99
+ * | -------------------------- | ---------------------------------------------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------- | --------- |
100
+ * | deleteUser | Deletes a user | - `error` error message if user ID doesn't exist | `default`: | `default`: | `id` |
101
+ * | | | | - see {@link SvelteKitAdminEndpoints.deleteUser} return | - see {@link SvelteKitAdminEndpoints.deleteUser} event | |
102
+ */
103
+ export class SvelteKitAdminEndpoints {
104
+ sessionServer;
105
+ userSearchFn = defaultUserSearchFn;
106
+ constructor(sessionServer, options) {
107
+ this.sessionServer = sessionServer;
108
+ if (options.userSearchFn)
109
+ this.userSearchFn = options.userSearchFn;
110
+ }
111
+ /** Returns whether there is a user logged in with a cookie-based session
112
+ */
113
+ isSessionUser(event) {
114
+ return event.locals.user != undefined && event.locals.authType == "cookie";
115
+ }
116
+ /**
117
+ * Returns either a list of all users or users matching a search term.
118
+ *
119
+ * The returned list is pagenaed using the `skip` and `take` parameters.
120
+ *
121
+ * The searching is done with `userSearchFn` that was passed in the
122
+ * options (see {@link SvelteKitSessionServerOptions }). THe default
123
+ * is an exact username match.
124
+ *
125
+ * By default, the searh and pagination parameters are taken from
126
+ * the query parameters in the request but can be overridden.
127
+ *
128
+ * @param event the Sveltekit request event. The following query parameters
129
+ * are read:
130
+ * - `search` the search term which is ignored if it is undefined, null
131
+ * or the empty string.
132
+ * - `skip` the number to start returning from. 0 if not defined
133
+ * - `take` the maximum number to return. 10 if not defined.
134
+ * @param searchTerm overrides the search term from the query.
135
+ * @param skip overrides the skip term from the query
136
+ * @param take overrides the take term from the query
137
+ *
138
+ * @return an object with the following members:
139
+ * - `ok` true or false depending on whether there was an error
140
+ * - `users` the matching array of users
141
+ * - `error` error message if `ok` is false
142
+ * - `exception` a {@link @crossauth/common!CrossauthError} if there was
143
+ * an error.
144
+ * - `search` the search term that was used
145
+ * - `skip` the skip term that was used
146
+ * - `take` the take term that was used
147
+ * - `hasNext` whether there are still more results after the ones that
148
+ * were returned
149
+ * - `hasPrevious` whether there are more results before the ones that
150
+ * were returned.
151
+ */
152
+ async searchUsers(event, searchTerm, skip, take) {
153
+ try {
154
+ if (!this.sessionServer.userStorage)
155
+ throw new CrossauthError(ErrorCode.Configuration, "Must provide user storage to use this function");
156
+ // can only call this if logged in as admin
157
+ if (!event.locals.user || !SvelteKitServer.isAdminFn(event.locals.user)) {
158
+ this.sessionServer.error(401);
159
+ }
160
+ let users = [];
161
+ let prevUsers = [];
162
+ let nextUsers = [];
163
+ if (!skip) {
164
+ try {
165
+ const str = event.url.searchParams.get("skip");
166
+ if (str)
167
+ skip = parseInt(str);
168
+ }
169
+ catch (e) {
170
+ CrossauthLogger.logger.warn(j({ cerr: e, msg: "skip parameter is not an integer" }));
171
+ }
172
+ }
173
+ if (!skip)
174
+ skip = 0;
175
+ if (!take) {
176
+ try {
177
+ const str = event.url.searchParams.get("take");
178
+ if (str)
179
+ take = parseInt(str);
180
+ }
181
+ catch (e) {
182
+ CrossauthLogger.logger.warn(j({ cerr: e, msg: "take parameter is not an integer" }));
183
+ }
184
+ }
185
+ if (!take)
186
+ take = 10;
187
+ const searchFromUrl = event.url.searchParams.get("search");
188
+ if (!searchTerm && searchFromUrl != null && searchFromUrl != "")
189
+ searchTerm = searchFromUrl;
190
+ if (!searchTerm)
191
+ searchTerm = "";
192
+ if (searchTerm.length == 0)
193
+ searchTerm = undefined;
194
+ if (searchTerm) {
195
+ users = await this.userSearchFn(searchTerm, this.sessionServer.userStorage, skip, take);
196
+ if (skip > 0) {
197
+ prevUsers = await this.userSearchFn(searchTerm, this.sessionServer.userStorage, skip - 1, 1);
198
+ }
199
+ }
200
+ else {
201
+ users =
202
+ await this.sessionServer.userStorage.getUsers(skip, take);
203
+ if (users.length == take) {
204
+ nextUsers =
205
+ await this.sessionServer.userStorage.getUsers(skip + take, 1);
206
+ }
207
+ }
208
+ return {
209
+ ok: true,
210
+ users,
211
+ skip,
212
+ take,
213
+ hasPrevious: prevUsers.length > 0,
214
+ hasNext: nextUsers.length > 0,
215
+ search: searchTerm
216
+ };
217
+ }
218
+ catch (e) {
219
+ const ce = CrossauthError.asCrossauthError(e);
220
+ CrossauthLogger.logger.debug(j({ err: ce }));
221
+ CrossauthLogger.logger.error(j({ cerr: ce }));
222
+ return {
223
+ ok: false,
224
+ error: ce.message,
225
+ errorCode: ce.code,
226
+ errorCodeName: ce.codeName,
227
+ hasPrevious: false,
228
+ hasNext: false,
229
+ skip: skip ?? 0,
230
+ take: take ?? 10,
231
+ search: searchTerm,
232
+ };
233
+ }
234
+ }
235
+ /**
236
+ * Call this to update a user's details.
237
+ *
238
+ * If you try updating factor2, the user will be asked to reset factor2
239
+ * upon next login.
240
+ *
241
+ * If you do not set a password, user will be sent a password reset
242
+ * token to set a new one.
243
+ *
244
+ * @param event the Sveltekit event. The form fields used are
245
+ * - `username` the desired username
246
+ * - `factor2` the desiredf second factor
247
+ * - `state` the desired state, which will be overridden if the
248
+ * user has to reset password and/or factor2
249
+ * - `user_*` anything prefixed with `user` that is also in
250
+ * the `userEditableFields` or `adminEditableFields` options
251
+ * passed when constructing the
252
+ * user storage object will be added to the {@link @crossauth/common!User}
253
+ * object (with `user_` removed).
254
+ *
255
+ * @returns object with:
256
+ *
257
+ * - `ok` true if creation and login were successful,
258
+ * false otherwise.
259
+ * even if factor2 authentication is required, this will still
260
+ * be true if there was no error.
261
+ * - `formData` the form fields extracted from the request
262
+ * - `error` an error message or undefined
263
+ * - `exception` a {@link @crossauth/common!CrossauthError} if an
264
+ * exception was raised
265
+ */
266
+ async updateUser(user, event) {
267
+ let formData = undefined;
268
+ try {
269
+ if (!this.sessionServer.userStorage)
270
+ throw new CrossauthError(ErrorCode.Configuration, "Must provide user storage to use this function");
271
+ // get form data
272
+ var data = new JsonOrFormData();
273
+ await data.loadData(event);
274
+ formData = data.toObject();
275
+ // can only call this if logged in as admin
276
+ if (!event.locals.user || !SvelteKitServer.isAdminFn(event.locals.user)) {
277
+ this.sessionServer.error(401);
278
+ }
279
+ // CSRF token must be valid if we are using them
280
+ if (this.isSessionUser(event) && this.sessionServer.enableCsrfProtection && !event.locals.csrfToken)
281
+ throw new CrossauthError(ErrorCode.InvalidCsrf);
282
+ const oldFactor2 = user.factor2;
283
+ const oldState = user.state;
284
+ user.state = formData.state ?? "active";
285
+ user = this.sessionServer.updateUserFn(user, event, formData, { ...this.sessionServer.userStorage.userEditableFields,
286
+ ...this.sessionServer.userStorage.adminEditableFields });
287
+ const factor2ResetNeeded = user.factor2 && user.factor2 != "none" && user.factor2 != oldFactor2;
288
+ if (factor2ResetNeeded && !(user.state == oldState || user.state == "factor2ResetNeeded")) {
289
+ throw new CrossauthError(ErrorCode.BadRequest, "Cannot change both factor2 and state at the same time");
290
+ }
291
+ if (factor2ResetNeeded) {
292
+ user.state = UserState.factor2ResetNeeded;
293
+ CrossauthLogger.logger.warn(j({ msg: `Setting state for user to ${UserState.factor2ResetNeeded}`,
294
+ username: user.username }));
295
+ }
296
+ // validate the new user using the implementor-provided function
297
+ let errors = this.sessionServer.validateUserFn(user);
298
+ if (errors.length > 0) {
299
+ throw new CrossauthError(ErrorCode.FormEntry, errors);
300
+ }
301
+ // update the user
302
+ const resp = await this.sessionServer.sessionManager.updateUser(user, user, true, true);
303
+ let info = undefined;
304
+ if (resp.emailVerificationTokenSent)
305
+ info = "An email verification token has been sent to the user";
306
+ else if (resp.passwordResetTokenSent)
307
+ info = "A password reset token has been sent to the user";
308
+ return {
309
+ ok: true,
310
+ formData: formData,
311
+ info
312
+ };
313
+ }
314
+ catch (e) {
315
+ // let Sveltekit redirect and 401 error through
316
+ if (SvelteKitServer.isSvelteKitRedirect(e))
317
+ throw e;
318
+ if (SvelteKitServer.isSvelteKitError(e, 401))
319
+ throw e;
320
+ let ce = CrossauthError.asCrossauthError(e, "Couldn't log in");
321
+ CrossauthLogger.logger.debug(j({ err: ce }));
322
+ CrossauthLogger.logger.error(j({ cerr: ce }));
323
+ return {
324
+ error: ce.message,
325
+ errorCode: ce.code,
326
+ errorCodeName: ce.codeName,
327
+ ok: false,
328
+ formData,
329
+ };
330
+ }
331
+ }
332
+ /**
333
+ * Call this with POST data to change the logged-in user's password
334
+ *
335
+ * @param user the user to edit
336
+ * @param event the Sveltekit event. This should contain
337
+ * - `old_`*secrets` (eg `old_password`) the existing secret.
338
+ * - `new_`*secrets` (eg `new_password`) the new secret.
339
+ * - `repeat_`*secrets` (eg `repeat_password`) repeat of the new secret.
340
+
341
+ * @returns object with:
342
+ *
343
+ * - `ok` true if creation and login were successful,
344
+ * false otherwise.
345
+ * - `user` the user if successful
346
+ * - `error` an error message or undefined
347
+ * - `exception` a {@link @crossauth/common!CrossauthError} if an
348
+ * exception was raised
349
+ * - `formData` the form fields extracted from the request
350
+ */
351
+ async changePassword(user, event) {
352
+ CrossauthLogger.logger.debug(j({ msg: "changePassword" }));
353
+ let formData = undefined;
354
+ try {
355
+ // get form data
356
+ var data = new JsonOrFormData();
357
+ await data.loadData(event);
358
+ formData = data.toObject();
359
+ // can only call this if logged in as admin
360
+ if (!event.locals.user || !SvelteKitServer.isAdminFn(event.locals.user)) {
361
+ this.sessionServer.error(401);
362
+ }
363
+ //this.validateCsrfToken(request)
364
+ if (this.isSessionUser(event) &&
365
+ this.sessionServer.enableCsrfProtection && !event.locals.csrfToken) {
366
+ throw new CrossauthError(ErrorCode.InvalidCsrf);
367
+ }
368
+ // get the authenticator for factor1 (passwords on factor2 are not supported)
369
+ const authenticator = this.sessionServer.authenticators[user.factor1];
370
+ // the form should contain old_{secret}, new_{secret} and repeat_{secret}
371
+ // extract them, making sure the secret is a valid one
372
+ const secretNames = authenticator.secretNames();
373
+ let oldSecrets = {};
374
+ let newSecrets = {};
375
+ let repeatSecrets = {};
376
+ for (let field in formData) {
377
+ if (field.startsWith("new_")) {
378
+ const name = field.replace(/^new_/, "");
379
+ if (secretNames.includes(name))
380
+ newSecrets[name] = formData[field];
381
+ }
382
+ else if (field.startsWith("old_")) {
383
+ const name = field.replace(/^old_/, "");
384
+ if (secretNames.includes(name))
385
+ oldSecrets[name] = formData[field];
386
+ }
387
+ else if (field.startsWith("repeat_")) {
388
+ const name = field.replace(/^repeat_/, "");
389
+ if (secretNames.includes(name))
390
+ repeatSecrets[name] = formData[field];
391
+ }
392
+ }
393
+ if (Object.keys(repeatSecrets).length === 0)
394
+ repeatSecrets = undefined;
395
+ if (Object.keys(oldSecrets).length === 0)
396
+ oldSecrets = undefined;
397
+ // validate the new secret - this is through an implementor-supplied function
398
+ let errors = authenticator.validateSecrets(newSecrets);
399
+ if (errors.length > 0) {
400
+ throw new CrossauthError(ErrorCode.PasswordFormat);
401
+ }
402
+ // validate the old secrets, check the new and repeat ones match and
403
+ // update if valid
404
+ try {
405
+ await this.sessionServer.sessionManager.changeSecrets(user.username, 1, newSecrets, repeatSecrets, oldSecrets);
406
+ }
407
+ catch (e) {
408
+ const ce = CrossauthError.asCrossauthError(e);
409
+ CrossauthLogger.logger.debug(j({ err: e }));
410
+ throw ce;
411
+ }
412
+ return {
413
+ ok: true,
414
+ formData: formData,
415
+ };
416
+ }
417
+ catch (e) {
418
+ // let Sveltekit redirect and 401 error through
419
+ if (SvelteKitServer.isSvelteKitRedirect(e))
420
+ throw e;
421
+ if (SvelteKitServer.isSvelteKitError(e, 401))
422
+ throw e;
423
+ let ce = CrossauthError.asCrossauthError(e, "Couldn't change password");
424
+ CrossauthLogger.logger.debug(j({ err: ce }));
425
+ CrossauthLogger.logger.error(j({ cerr: ce }));
426
+ return {
427
+ error: ce.message,
428
+ errorCode: ce.code,
429
+ errorCodeName: ce.codeName,
430
+ ok: false,
431
+ formData,
432
+ };
433
+ }
434
+ }
435
+ /**
436
+ * Creates an account.
437
+ *
438
+ * Form data is returned unless there was an error extrafting it.
439
+ *
440
+ * Initiates user login if creation was successful.
441
+ *
442
+ * If login was successful, no factor2 is needed
443
+ * and no email verification is needed, the user is returned.
444
+ *
445
+ * If email verification is needed, `emailVerificationRequired` is
446
+ * returned as `true`.
447
+ *
448
+ * If factor2 configuration is required, `factor2Required` is returned
449
+ * as `true`.
450
+ *
451
+ * @param event the Sveltekit event. The form fields used are
452
+ * - `username` the desired username
453
+ * - `factor2` which must be in the `allowedFactor2` option passed
454
+ * to the constructor.
455
+ * - *secrets* (eg `password`) which are factor1 authenticator specific
456
+ * - `repeat_`*secrets* (eg `repeat_password`)
457
+ * - `user_*` anything prefixed with `user` that is also in
458
+ * - the `userEditableFields` option passed when constructing the
459
+ * user storage object will be added to the {@link @crossauth/common!User}
460
+ * object (with `user_` removed).
461
+ *
462
+ * @returns object with:
463
+ *
464
+ * - `ok` true if creation and login were successful,
465
+ * false otherwise.
466
+ * even if factor2 authentication is required, this will still
467
+ * be true if there was no error.
468
+ * - `user` the user if login was successful
469
+ * - `formData` the form fields extracted from the request
470
+ * - `error` an error message or undefined
471
+ * - `exception` a {@link @crossauth/common!CrossauthError} if an
472
+ * exception was raised
473
+ * - `factor2Required` if true, second factor authentication is needed
474
+ * to complete login
475
+ * - `factor2Data` contains data that needs to be passed to the user's
476
+ * chosen factor2 authenticator
477
+ * - `emailVerificationRequired` if true, the user needs to click on
478
+ * the link emailed to them to complete signup.
479
+ */
480
+ async createUser(event) {
481
+ let formData = undefined;
482
+ try {
483
+ if (!this.sessionServer.userStorage)
484
+ throw new CrossauthError(ErrorCode.Configuration, "Must provide user storage to use this function");
485
+ // get form data
486
+ var data = new JsonOrFormData();
487
+ await data.loadData(event);
488
+ formData = data.toObject();
489
+ const username = data.get('username') ?? "";
490
+ let user;
491
+ // can only call this if logged in as admin
492
+ if (!event.locals.user || !SvelteKitServer.isAdminFn(event.locals.user)) {
493
+ this.sessionServer.error(401);
494
+ }
495
+ // throw an error if the CSRF token is invalid
496
+ if (this.isSessionUser(event) && this.sessionServer.enableCsrfProtection && !event.locals.csrfToken)
497
+ throw new CrossauthError(ErrorCode.InvalidCsrf);
498
+ if (username == "")
499
+ throw new CrossauthError(ErrorCode.InvalidUsername, "Username field may not be empty");
500
+ // get factor2 from user input
501
+ if (!formData.factor2) {
502
+ formData.factor2 = this.sessionServer.allowedFactor2Names[0];
503
+ }
504
+ if (formData.factor2 &&
505
+ !(this.sessionServer.allowedFactor2Names.includes(formData.factor2 ?? "none"))) {
506
+ throw new CrossauthError(ErrorCode.Forbidden, "Illegal second factor " + formData.factor2 + " requested");
507
+ }
508
+ if (formData.factor2 == "none" || formData.factor2 == "") {
509
+ formData.factor2 = undefined;
510
+ }
511
+ // call implementor-provided function to create the user object (or our default)
512
+ user =
513
+ this.sessionServer.createUserFn(event, formData, { ...this.sessionServer.userStorage.userEditableFields,
514
+ ...this.sessionServer.userStorage.adminEditableFields }, this.sessionServer.adminAllowedFactor1);
515
+ const secretNames = this.sessionServer.authenticators[user.factor1].secretNames();
516
+ let hasSecrets = true;
517
+ for (let secret of secretNames) {
518
+ if (!formData[secret] && !formData["repeat_" + secret])
519
+ hasSecrets = false;
520
+ }
521
+ // ask the authenticator to validate the user-provided secret
522
+ let passwordErrors = [];
523
+ let repeatSecrets = {};
524
+ if (hasSecrets) {
525
+ passwordErrors = this.sessionServer.authenticators[user.factor1].validateSecrets(formData);
526
+ // get the repeat secrets (secret names prefixed with repeat_)
527
+ for (let field in formData) {
528
+ if (field.startsWith("repeat_")) {
529
+ const name = field.replace(/^repeat_/, "");
530
+ // @ts-ignore as it complains about request.body[field]
531
+ if (secretNames.includes(name))
532
+ repeatSecrets[name] =
533
+ formData[field];
534
+ }
535
+ }
536
+ if (Object.keys(repeatSecrets).length === 0)
537
+ repeatSecrets = undefined;
538
+ }
539
+ // If a password wasn't given, force the user to do a passowrd
540
+ // reset on login
541
+ if (!hasSecrets) {
542
+ if (formData.factor2 == undefined)
543
+ user.state = UserState.passwordResetNeeded;
544
+ else
545
+ user.state = UserState.passwordAndFactor2ResetNeeded;
546
+ }
547
+ else if (formData.factor2 != undefined)
548
+ user.state = UserState.factor2ResetNeeded;
549
+ // call the implementor-provided hook to validate the user fields
550
+ let userErrors = this.sessionServer.validateUserFn(user);
551
+ // report any errors
552
+ let errors = [...userErrors, ...passwordErrors];
553
+ if (errors.length > 0) {
554
+ throw new CrossauthError(ErrorCode.FormEntry, errors);
555
+ }
556
+ const newUser = await this.sessionServer.sessionManager.createUser(user, formData, repeatSecrets, true, !hasSecrets);
557
+ if (!hasSecrets) {
558
+ let email = formData.username;
559
+ if ("user_email" in formData)
560
+ email = formData.user_email;
561
+ TokenEmailer.validateEmail(email);
562
+ if (!email)
563
+ throw new CrossauthError(ErrorCode.FormEntry, "No password given but no email address found either");
564
+ await this.sessionServer.sessionManager.requestPasswordReset(email);
565
+ }
566
+ return { ok: true, user: newUser, formData };
567
+ }
568
+ catch (e) {
569
+ let ce = CrossauthError.asCrossauthError(e, "Couldn't create user");
570
+ CrossauthLogger.logger.debug(j({ err: ce }));
571
+ CrossauthLogger.logger.error(j({ cerr: ce }));
572
+ return {
573
+ error: ce.message,
574
+ errorCode: ce.code,
575
+ errorCodeName: ce.codeName,
576
+ ok: false,
577
+ formData,
578
+ };
579
+ }
580
+ }
581
+ /**
582
+ * Call this to delete the logged-in user
583
+ *
584
+ * @param event the Sveltekit event.
585
+
586
+ * @returns object with:
587
+ *
588
+ * - `ok` true if creation and login were successful,
589
+ * false otherwise.
590
+ * - `error` an error message or undefined
591
+ * - `exception` a {@link @crossauth/common!CrossauthError} if an
592
+ * exception was raised
593
+ */
594
+ async deleteUser(event) {
595
+ CrossauthLogger.logger.debug(j({ msg: "deleteUser" }));
596
+ if (!this.sessionServer.userStorage)
597
+ throw new CrossauthError(ErrorCode.Configuration, "Must provide user storage to use this function");
598
+ try {
599
+ const userid = event.params.id;
600
+ if (!userid)
601
+ throw new CrossauthError(ErrorCode.BadRequest, "User ID is undefined");
602
+ // throw an error if the CSRF token is invalid
603
+ if (this.sessionServer.enableCsrfProtection && !event.locals.csrfToken) {
604
+ throw new CrossauthError(ErrorCode.InvalidCsrf);
605
+ }
606
+ // can only call this if logged in as admin
607
+ if (!event.locals.user || !SvelteKitServer.isAdminFn(event.locals.user)) {
608
+ this.sessionServer.error(401);
609
+ }
610
+ await this.sessionServer.userStorage.deleteUserById(userid);
611
+ return {
612
+ ok: true,
613
+ };
614
+ }
615
+ catch (e) {
616
+ let ce = CrossauthError.asCrossauthError(e, "Couldn't delete account");
617
+ CrossauthLogger.logger.debug(j({ err: ce }));
618
+ CrossauthLogger.logger.error(j({ cerr: ce }));
619
+ return {
620
+ error: ce.message,
621
+ errorCode: ce.code,
622
+ errorCodeName: ce.codeName,
623
+ ok: false,
624
+ };
625
+ }
626
+ }
627
+ ///////////////////////////////////////////////////////////////////
628
+ // endpoints
629
+ baseEndpoint(event) {
630
+ return {
631
+ user: event.locals.user,
632
+ csrfToken: event.locals.csrfToken,
633
+ };
634
+ }
635
+ searchUsersEndpoint = {
636
+ load: async (event) => {
637
+ if (!event.locals.user || !SvelteKitServer.isAdminFn(event.locals.user))
638
+ this.sessionServer.error(event, 401);
639
+ const resp = await this.searchUsers(event);
640
+ return {
641
+ ...this.baseEndpoint(event),
642
+ ...resp,
643
+ };
644
+ },
645
+ };
646
+ async getUserFromParam(event, paramName = "id") {
647
+ let userid = event.params[paramName];
648
+ if (!userid) {
649
+ return { exception: new CrossauthError(ErrorCode.BadRequest, "Must give user id") };
650
+ }
651
+ try {
652
+ if (!this.sessionServer.userStorage)
653
+ throw new CrossauthError(ErrorCode.Configuration, "Must provide user storage to use this function");
654
+ const resp = await this.sessionServer.userStorage.getUserById(userid, { skipEmailVerifiedCheck: true, skipActiveCheck: true });
655
+ return { user: resp.user };
656
+ }
657
+ catch (e) {
658
+ return { exception: CrossauthError.asCrossauthError(e) };
659
+ }
660
+ }
661
+ updateUserEndpoint = {
662
+ actions: {
663
+ default: async (event) => {
664
+ const getUserResp = await this.getUserFromParam(event);
665
+ if (getUserResp.exception || !getUserResp.user) {
666
+ return {
667
+ ok: false,
668
+ error: getUserResp.exception?.message ?? "Couldn't get user",
669
+ errorCode: getUserResp.exception?.code,
670
+ errorCodeName: getUserResp.exception?.codeName,
671
+ };
672
+ }
673
+ const resp = await this.updateUser(getUserResp.user, event);
674
+ return resp;
675
+ }
676
+ },
677
+ load: async (event) => {
678
+ if (!event.locals.user || !SvelteKitServer.isAdminFn(event.locals.user))
679
+ this.sessionServer.error(event, 401);
680
+ let allowedFactor2 = this.sessionServer.allowedFactor2 ??
681
+ [{ name: "none", friendlyName: "None" }];
682
+ const getUserResp = await this.getUserFromParam(event);
683
+ if (getUserResp.exception || !getUserResp.user) {
684
+ return {
685
+ allowedFactor2,
686
+ editUser: getUserResp.user,
687
+ ...this.baseEndpoint(event),
688
+ };
689
+ }
690
+ //this.sessionServer?.refreshLocals(event);
691
+ return {
692
+ allowedFactor2,
693
+ editUser: getUserResp.user,
694
+ ...this.baseEndpoint(event),
695
+ };
696
+ }
697
+ };
698
+ changePasswordEndpoint = {
699
+ actions: {
700
+ default: async (event) => {
701
+ const getUserResp = await this.getUserFromParam(event);
702
+ if (getUserResp.exception || !getUserResp.user) {
703
+ return {
704
+ ok: false,
705
+ error: getUserResp.exception?.message ?? "Couldn't get user",
706
+ errorCode: getUserResp.exception?.code,
707
+ errorCodeName: getUserResp.exception?.codeName,
708
+ };
709
+ }
710
+ const resp = await this.changePassword(getUserResp.user, event);
711
+ return resp;
712
+ }
713
+ },
714
+ load: async (event) => {
715
+ if (!event.locals.user || !SvelteKitServer.isAdminFn(event.locals.user))
716
+ this.sessionServer.error(event, 401);
717
+ const getUserResp = await this.getUserFromParam(event);
718
+ if (getUserResp.exception || !getUserResp.user) {
719
+ return {
720
+ editUser: getUserResp.user,
721
+ ...this.baseEndpoint(event),
722
+ };
723
+ }
724
+ let data = {};
725
+ let next = event.url.searchParams.get("next");
726
+ if (next)
727
+ data.next = next;
728
+ return {
729
+ ...data,
730
+ editUser: getUserResp.user,
731
+ ...this.baseEndpoint(event),
732
+ };
733
+ },
734
+ };
735
+ createUserEndpoint = {
736
+ load: async (event) => {
737
+ if (!event.locals.user || !SvelteKitServer.isAdminFn(event.locals.user))
738
+ this.sessionServer.error(event, 401);
739
+ let allowedFactor2 = this.sessionServer?.allowedFactor2 ??
740
+ [{ name: "none", friendlyName: "None" }];
741
+ return {
742
+ allowedFactor2,
743
+ ...this.baseEndpoint(event),
744
+ };
745
+ },
746
+ actions: {
747
+ default: async (event) => {
748
+ const resp = await this.createUser(event);
749
+ return resp;
750
+ }
751
+ }
752
+ };
753
+ deleteUserEndpoint = {
754
+ actions: {
755
+ default: async (event) => {
756
+ const resp = await this.deleteUser(event);
757
+ return resp;
758
+ }
759
+ },
760
+ load: async (event) => {
761
+ const getUserResp = await this.getUserFromParam(event);
762
+ if (getUserResp.exception || !getUserResp.user) {
763
+ return {
764
+ error: "User doesn't exist",
765
+ errorCode: getUserResp.exception?.code,
766
+ errorCodeName: getUserResp.exception?.codeName,
767
+ ...this.baseEndpoint(event),
768
+ };
769
+ }
770
+ return {
771
+ username: getUserResp.user?.username,
772
+ ...this.baseEndpoint(event),
773
+ };
774
+ },
775
+ };
776
+ }