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