@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.
- package/dist/index.d.ts +1 -1
- package/dist/index.js +16 -6181
- package/dist/sveltekitadminclientendpoints.d.ts +13 -12
- package/dist/sveltekitadminclientendpoints.js +187 -0
- package/dist/sveltekitadminendpoints.d.ts +5 -4
- package/dist/sveltekitadminendpoints.js +766 -0
- package/dist/sveltekitapikey.d.ts +4 -4
- package/dist/sveltekitapikey.js +81 -0
- package/dist/sveltekitoauthclient.d.ts +6 -5
- package/dist/sveltekitoauthclient.js +2309 -0
- package/dist/sveltekitoauthserver.d.ts +4 -4
- package/dist/sveltekitoauthserver.js +1350 -0
- package/dist/sveltekitresserver.d.ts +6 -5
- package/dist/sveltekitresserver.js +286 -0
- package/dist/sveltekitserver.d.ts +11 -10
- package/dist/sveltekitserver.js +393 -0
- package/dist/sveltekitsession.d.ts +5 -5
- package/dist/sveltekitsession.js +1112 -0
- package/dist/sveltekitsessionadapter.d.ts +2 -3
- package/dist/sveltekitsessionadapter.js +2 -0
- package/dist/sveltekitsharedclientendpoints.d.ts +7 -6
- package/dist/sveltekitsharedclientendpoints.js +630 -0
- package/dist/sveltekituserclientendpoints.d.ts +13 -12
- package/dist/sveltekituserclientendpoints.js +270 -0
- package/dist/sveltekituserendpoints.d.ts +6 -5
- package/dist/sveltekituserendpoints.js +1813 -0
- package/dist/tests/sveltekitadminclientendpoints.test.js +330 -0
- package/dist/tests/sveltekitadminendpoints.test.js +242 -0
- package/dist/tests/sveltekitapikeyserver.test.js +44 -0
- package/dist/tests/sveltekitoauthclient.test.d.ts +5 -5
- package/dist/tests/sveltekitoauthclient.test.js +1016 -0
- package/dist/tests/sveltekitoauthresserver.test.d.ts +4 -4
- package/dist/tests/sveltekitoauthresserver.test.js +185 -0
- package/dist/tests/sveltekitoauthserver.test.js +673 -0
- package/dist/tests/sveltekituserclientendpoints.test.js +244 -0
- package/dist/tests/sveltekituserendpoints.test.js +152 -0
- package/dist/tests/sveltemock.test.js +36 -0
- package/dist/tests/sveltemocks.d.ts +2 -3
- package/dist/tests/sveltemocks.js +114 -0
- package/dist/tests/sveltesessionhooks.test.js +224 -0
- package/dist/tests/testshared.d.ts +8 -8
- package/dist/tests/testshared.js +344 -0
- package/dist/utils.d.ts +1 -2
- package/dist/utils.js +123 -0
- package/package.json +6 -4
- 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
|
+
}
|