@madauth/server 0.4.0-beta.1 → 0.5.0-beta.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/README.md +1 -1
- package/dist/bin.js +134 -44
- package/dist/{chunk-3PACOYTG.js → chunk-7DHKDRGY.js} +897 -376
- package/dist/{chunk-GA4TF7EI.js → chunk-NP75Y3IV.js} +27 -4
- package/dist/{chunk-ISJGOCF3.js → chunk-NYIEFI4B.js} +268 -55
- package/dist/{chunk-OMIX3ERC.js → chunk-O2URPWA6.js} +27 -3
- package/dist/{chunk-Y7ZSUROX.js → chunk-SISM7YUZ.js} +14 -12
- package/dist/{config-DkE_2D3-.d.ts → config-CcGlHVAN.d.ts} +16 -8
- package/dist/dynamodb.js +1 -1
- package/dist/entry/azure.d.ts +3 -3
- package/dist/entry/azure.js +3 -3
- package/dist/entry/lambda.d.ts +3 -3
- package/dist/entry/lambda.js +3 -3
- package/dist/entry/node.d.ts +3 -3
- package/dist/entry/node.js +3 -3
- package/dist/index.d.ts +22 -15
- package/dist/index.js +7 -5
- package/dist/{schema-DTeiSuQB.d.ts → schema-BTgDj1Xn.d.ts} +43 -4
- package/dist/sqlite.js +2 -2
- package/dist/standalone/azure.mjs +4373 -424
- package/dist/standalone/lambda.mjs +4372 -423
- package/dist/standalone/main.mjs +4310 -361
- package/dist/store/dynamodb.d.ts +1 -1
- package/dist/store/sqlite.d.ts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +19 -0
- package/dist/{user-BZs84sui.d.ts → user-yoGjrw4k.d.ts} +2 -2
- package/dist/verify.d.ts +1 -1
- package/dist/{webhook-B6P50ocX.d.ts → webhook-DlmfG_kS.d.ts} +5 -2
- package/dist/webhook.d.ts +1 -1
- package/dist/webhook.js +1 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @madauth/server
|
|
2
2
|
|
|
3
|
-
The server of [madAuth](https://github.com/inouiw/madAuth), a self-hostable alternative to Cognito / Auth0: Google, e-mail & password and authenticator-app (TOTP) sign-in with madAuth sessions. It runs with Node, in Docker, on AWS Lambda, on Azure Functions or inside your own Node server.
|
|
3
|
+
The server of [madAuth](https://github.com/inouiw/madAuth), a self-hostable alternative to Cognito / Auth0: Google, GitHub, e-mail & password, e-mail link and authenticator-app (TOTP) sign-in with madAuth sessions. It runs with Node, in Docker, on AWS Lambda, on Azure Functions or inside your own Node server.
|
|
4
4
|
|
|
5
5
|
The browser side is [`@madauth/web`](https://www.npmjs.com/package/@madauth/web).
|
|
6
6
|
|
package/dist/bin.js
CHANGED
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
import {
|
|
3
3
|
createTablesSql,
|
|
4
4
|
upgradeTablesSql
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-O2URPWA6.js";
|
|
6
6
|
import {
|
|
7
7
|
madauthSchema
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-NP75Y3IV.js";
|
|
9
9
|
import {
|
|
10
|
+
DEFAULT_PASSWORD_RULES,
|
|
11
|
+
MAX_MIN_LENGTH,
|
|
12
|
+
METHODS,
|
|
10
13
|
SECOND_FACTOR_POLICIES,
|
|
11
14
|
SIGN_IN_METHODS,
|
|
12
15
|
Settings,
|
|
@@ -15,28 +18,29 @@ import {
|
|
|
15
18
|
canSignUp,
|
|
16
19
|
checkPasswordPolicy,
|
|
17
20
|
claimsFromJson,
|
|
21
|
+
hasSecondFactor,
|
|
18
22
|
hashPassword,
|
|
19
23
|
initialMethods,
|
|
20
24
|
isConfigured,
|
|
21
25
|
isOn,
|
|
26
|
+
isPasswordScore,
|
|
22
27
|
isValidEmail,
|
|
23
28
|
loadConfig,
|
|
24
29
|
loadStore,
|
|
25
|
-
loadUserStoreConfig,
|
|
26
30
|
methodConfig,
|
|
27
31
|
normalizeEmail,
|
|
28
32
|
parseClaims,
|
|
29
33
|
parseRoles,
|
|
30
34
|
passwordAccountKey,
|
|
31
35
|
policyOf
|
|
32
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-NYIEFI4B.js";
|
|
33
37
|
import {
|
|
34
38
|
generateSigningKey
|
|
35
39
|
} from "./chunk-JDQLSL36.js";
|
|
36
40
|
import {
|
|
37
41
|
DEV_WEBHOOK_RECEIVER_URL,
|
|
38
42
|
generateWebhookSecret
|
|
39
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-SISM7YUZ.js";
|
|
40
44
|
|
|
41
45
|
// src/cli.ts
|
|
42
46
|
import { existsSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
@@ -48,15 +52,18 @@ var usage = `Usage: npx @madauth/server <command> [--env-file <path>]
|
|
|
48
52
|
Commands:
|
|
49
53
|
init Ask a few questions, write the configuration to .env and print the next steps.
|
|
50
54
|
An answer given as an option is not asked for: --app-url <url>,
|
|
51
|
-
--google-client-id <id>, --google-client-secret <secret>,
|
|
55
|
+
--google-client-id <id>, --google-client-secret <secret>,
|
|
56
|
+
--github-client-id <id>, --github-client-secret <secret>, --password or
|
|
52
57
|
--no-password, --totp or --no-totp (sign-up with the authenticator app),
|
|
58
|
+
--email-link or --no-email-link (passwordless sign-in by e-mail),
|
|
59
|
+
--sms or --no-sms (SMS codes as a second factor),
|
|
53
60
|
--database <url>, --webhook-url <url>. --yes takes the default for the
|
|
54
61
|
rest. --out <path> writes another file, --force overwrites.
|
|
55
62
|
start Start the server on PORT (default ${PORT})
|
|
56
63
|
generate-key Print a new private ES256 key to use as MADAUTH_SIGNING_KEY
|
|
57
64
|
generate-webhook-secret Print a new secret to use as WEBHOOK_SECRET (madAuth and your receiver)
|
|
58
|
-
create-user <email> Create a user with a password (asks for it), already verified.
|
|
59
|
-
Uses DATABASE_URL
|
|
65
|
+
create-user <email> Create a user with a password (asks for it), already verified. The password
|
|
66
|
+
must follow the password rules. Uses DATABASE_URL from the environment.
|
|
60
67
|
set-roles <email> [role...] Set the roles of a user (the claim "roles"), e.g. to make the first admin:
|
|
61
68
|
set-roles you@example.com admin. Without roles, removes them all.
|
|
62
69
|
The user must exist: signed in once, or created with create-user.
|
|
@@ -65,15 +72,24 @@ Commands:
|
|
|
65
72
|
get-claims <email> Print the claims of a user as JSON.
|
|
66
73
|
set-methods [method[=policy]...]
|
|
67
74
|
Switch sign-in methods on and off while the server runs: the listed ones
|
|
68
|
-
are on, the others off. Methods: google, password, totp (the
|
|
69
|
-
app on its own)
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
are on, the others off. Methods: google, github, password, totp (the
|
|
76
|
+
authenticator app on its own), email-link (passwordless sign-in by e-mail),
|
|
77
|
+
and sms (SMS codes as a second factor, next to the app). google, github,
|
|
78
|
+
password and email-link take a policy for the second factor: none (default),
|
|
79
|
+
optional or required, e.g. set-methods google password=required totp email-link=optional sms
|
|
72
80
|
get-methods Print which sign-in methods are switched on, with their policies.
|
|
81
|
+
set-password-rules [min-length=<n>] [min-score=<0-4>]
|
|
82
|
+
Set the rules for new passwords: the minimum length (default 8, at most 128)
|
|
83
|
+
and how hard to guess they must be, 0 (anything) to 4 (default 2: no common
|
|
84
|
+
passwords or simple patterns). What is left out takes its default, e.g.
|
|
85
|
+
set-password-rules min-length=10 min-score=3. Uses DATABASE_URL.
|
|
86
|
+
get-password-rules Print the rules for new passwords.
|
|
73
87
|
remove-totp <email> Remove a user's authenticator app and recovery codes: the last resort when
|
|
74
88
|
both are lost. With a required policy, the user sets it up again next time.
|
|
75
|
-
|
|
76
|
-
|
|
89
|
+
remove-sms <email> Remove a user's phone number for SMS codes, when they lost the phone.
|
|
90
|
+
The claims commands, remove-totp and remove-sms use DATABASE_URL from the
|
|
91
|
+
environment; the methods commands need the server's whole configuration
|
|
92
|
+
(e.g. --env-file .env).
|
|
77
93
|
schema [--dialect <name>] [--from <version>]
|
|
78
94
|
Print the SQL that creates madAuth's tables for a custom store adapter.
|
|
79
95
|
Dialects: postgres (default), mysql, sqlite. With --from, print only the
|
|
@@ -169,8 +185,12 @@ function parseCliArgs(args) {
|
|
|
169
185
|
"app-url": { type: "string" },
|
|
170
186
|
"google-client-id": { type: "string" },
|
|
171
187
|
"google-client-secret": { type: "string" },
|
|
188
|
+
"github-client-id": { type: "string" },
|
|
189
|
+
"github-client-secret": { type: "string" },
|
|
172
190
|
password: { type: "boolean" },
|
|
173
191
|
totp: { type: "boolean" },
|
|
192
|
+
"email-link": { type: "boolean" },
|
|
193
|
+
sms: { type: "boolean" },
|
|
174
194
|
database: { type: "string" },
|
|
175
195
|
"webhook-url": { type: "string" },
|
|
176
196
|
yes: { type: "boolean" },
|
|
@@ -196,11 +216,13 @@ ${usage}`, exitCode: 1 };
|
|
|
196
216
|
const argumentLimits = {
|
|
197
217
|
"set-roles": Infinity,
|
|
198
218
|
"set-methods": Infinity,
|
|
219
|
+
"set-password-rules": Infinity,
|
|
199
220
|
"set-claims": 2,
|
|
200
221
|
"create-user": 1,
|
|
201
222
|
"get-roles": 1,
|
|
202
223
|
"get-claims": 1,
|
|
203
|
-
"remove-totp": 1
|
|
224
|
+
"remove-totp": 1,
|
|
225
|
+
"remove-sms": 1
|
|
204
226
|
};
|
|
205
227
|
const maxArguments = command ? argumentLimits[command] ?? 0 : 0;
|
|
206
228
|
if (rest.length > maxArguments) {
|
|
@@ -245,9 +267,15 @@ ${usage}`, exitCode: 1 };
|
|
|
245
267
|
if (command === "set-methods" || command === "get-methods") {
|
|
246
268
|
return await methods(command, rest, env);
|
|
247
269
|
}
|
|
270
|
+
if (command === "set-password-rules" || command === "get-password-rules") {
|
|
271
|
+
return await passwordRules(command, rest, env);
|
|
272
|
+
}
|
|
248
273
|
if (command === "remove-totp") {
|
|
249
274
|
return await removeTotp(rest[0], env);
|
|
250
275
|
}
|
|
276
|
+
if (command === "remove-sms") {
|
|
277
|
+
return await removeSms(rest[0], env);
|
|
278
|
+
}
|
|
251
279
|
} catch (e) {
|
|
252
280
|
return { output: e instanceof Error ? e.message : String(e), exitCode: 1 };
|
|
253
281
|
}
|
|
@@ -280,21 +308,37 @@ async function init(options, io) {
|
|
|
280
308
|
return fail("--google-client-secret needs a Google client ID.");
|
|
281
309
|
}
|
|
282
310
|
const clientSecret = clientId ? (options["google-client-secret"] ?? await askClientSecret()).trim() : "";
|
|
311
|
+
const githubClientId = await answer(options["github-client-id"], "GitHub client ID (empty: no GitHub sign-in)");
|
|
312
|
+
if (!githubClientId && options["github-client-secret"] !== void 0) {
|
|
313
|
+
return fail("--github-client-secret needs a GitHub client ID.");
|
|
314
|
+
}
|
|
315
|
+
const githubClientSecret = githubClientId ? (options["github-client-secret"] ?? (options.yes ? "" : await io.askSecret("GitHub client secret: "))).trim() : "";
|
|
316
|
+
if (githubClientId && !githubClientSecret) {
|
|
317
|
+
return fail("GitHub sign-in needs the client secret of the OAuth App too (--github-client-secret).");
|
|
318
|
+
}
|
|
283
319
|
const password = options.password ?? /^y/i.test(await answer(void 0, "E-mail & password sign-in? (yes/no)", "yes"));
|
|
284
320
|
const totp = options.totp ?? /^y/i.test(await answer(void 0, "Sign-up with the authenticator app alone, without a password? (yes/no)", "no"));
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
321
|
+
const emailLink = options["email-link"] ?? /^y/i.test(await answer(void 0, "Passwordless sign-in with an e-mail link? (yes/no)", "no"));
|
|
322
|
+
const sms = options.sms ?? /^y/i.test(await answer(void 0, "SMS codes as a second factor, sent by your webhook receiver? (yes/no)", "no"));
|
|
323
|
+
const emails = password || totp || emailLink;
|
|
324
|
+
const webhook = emails || sms;
|
|
325
|
+
if (!webhook && options["webhook-url"] !== void 0) {
|
|
326
|
+
return fail("--webhook-url is for the e-mails of e-mail & password sign-in, of the e-mail link and of sign-up with the authenticator app, and for SMS codes, which are turned off.");
|
|
288
327
|
}
|
|
289
|
-
if (!clientId && !emails) {
|
|
290
|
-
return fail("No sign-in method is chosen. Give a Google client ID, turn on e-mail & password sign-in or sign-up with the authenticator app.");
|
|
328
|
+
if (!clientId && !githubClientId && !emails) {
|
|
329
|
+
return fail("No sign-in method is chosen. Give a Google or GitHub client ID, turn on e-mail & password sign-in, the e-mail link or sign-up with the authenticator app.");
|
|
291
330
|
}
|
|
292
331
|
const database = await answer(options.database, "Database for the users", "sqlite:./madauth.db");
|
|
293
332
|
if (!/^(sqlite|dynamodb):./.test(database)) {
|
|
294
333
|
return fail(`The database must be sqlite:<path> or dynamodb:<table> but is "${database}".`);
|
|
295
334
|
}
|
|
296
|
-
const webhookUrl =
|
|
297
|
-
const events =
|
|
335
|
+
const webhookUrl = webhook ? await answer(options["webhook-url"], `Webhook URL, where madAuth hands over its ${emails ? sms ? "e-mails and SMS" : "e-mails" : "SMS"}`, "http://localhost:8790/webhook") : "";
|
|
336
|
+
const events = [
|
|
337
|
+
...password ? ["email.send_verification", "email.send_password_reset", "email.send_already_registered", "email.send_password_reset_no_password"] : [],
|
|
338
|
+
...totp ? ["email.send_verification", "email.send_already_registered"] : [],
|
|
339
|
+
...emailLink ? ["email.send_signin_link", "email.send_already_registered"] : [],
|
|
340
|
+
...sms ? ["sms.send_code", "user.second_factor_enabled", "user.second_factor_disabled"] : []
|
|
341
|
+
].filter((type, i, all) => all.indexOf(type) === i).join(",");
|
|
298
342
|
const variable = (name, value, comment) => `# ${comment}
|
|
299
343
|
${name}=${value}
|
|
300
344
|
`;
|
|
@@ -316,10 +360,12 @@ ${name}=${value}
|
|
|
316
360
|
variable("ALLOWED_ORIGINS", origin, "Origins of your apps, comma-separated. Only these may call the server."),
|
|
317
361
|
clientId && variable("GOOGLE_CLIENT_ID", clientId, "Google sign-in: your OAuth client ID, of the type Web application."),
|
|
318
362
|
clientSecret && variable("GOOGLE_CLIENT_SECRET", clientSecret, "Client secret of the same client. It enables the redirect flow."),
|
|
363
|
+
githubClientId && variable("GITHUB_CLIENT_ID", githubClientId, "GitHub sign-in: the client ID of your OAuth App."),
|
|
364
|
+
githubClientSecret && variable("GITHUB_CLIENT_SECRET", githubClientSecret, "Client secret of the same OAuth App."),
|
|
319
365
|
variable("DATABASE_URL", database, "Where the users are stored, whichever way they sign in."),
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
366
|
+
webhook && variable("WEBHOOK_URL", webhookUrl, `Your webhook receiver. madAuth hands its ${sms ? "e-mails and SMS" : "e-mails"} to it.`),
|
|
367
|
+
webhook && variable("WEBHOOK_SECRET", generateWebhookSecret(), "Signs every webhook call. Your receiver needs the same one."),
|
|
368
|
+
webhook && variable("WEBHOOK_EVENTS", events, "The types your webhook receiver handles; only these are sent. Add e.g. user.signing_up or user.signed_up."),
|
|
323
369
|
variable("PORT", String(PORT), "Port the server listens on.")
|
|
324
370
|
].filter(Boolean).join("");
|
|
325
371
|
await loadConfig({ ...parseEnv(content), DATABASE_URL: void 0 }, { store: {} });
|
|
@@ -330,23 +376,23 @@ ${name}=${value}
|
|
|
330
376
|
|
|
331
377
|
Next steps:
|
|
332
378
|
|
|
333
|
-
${nextSteps(file, origin, google, password, totp)}`, exitCode: 0 };
|
|
379
|
+
${nextSteps(file, origin, google, !!githubClientId, password, totp, emailLink, sms)}`, exitCode: 0 };
|
|
334
380
|
}
|
|
335
|
-
function nextSteps(file, origin, google, password, totp = false) {
|
|
381
|
+
function nextSteps(file, origin, google, github, password, totp = false, emailLink = false, sms = false) {
|
|
336
382
|
const server = `http://localhost:${PORT}`;
|
|
337
383
|
const envFile = /[^\w./-]/.test(file) ? `'${file.replaceAll("'", `'\\''`)}'` : file;
|
|
338
|
-
const providers = [google, password && "Password", totp && "Totp"].filter(Boolean);
|
|
339
|
-
const methodList = [google && "google", password && "password", totp && "totp"].filter(Boolean).join(" ");
|
|
384
|
+
const providers = [google, github && "GitHub", password && "Password", totp && "Totp", emailLink && "EmailLink"].filter(Boolean);
|
|
385
|
+
const methodList = [google && "google", github && "github", password && "password", totp && "totp", emailLink && "email-link"].filter(Boolean).join(" ");
|
|
340
386
|
const googleOrigins = /* @__PURE__ */ new Set([origin, ...new URL(origin).hostname === "localhost" ? ["http://localhost"] : []]);
|
|
341
387
|
const steps = [
|
|
342
388
|
["Start the madAuth server:", "", ` npx @madauth/server start --env-file ${envFile}`],
|
|
343
|
-
(password || totp) && [
|
|
344
|
-
|
|
389
|
+
(password || totp || emailLink || sms) && [
|
|
390
|
+
`madAuth hands its ${sms ? "e-mails and SMS" : "e-mails"} to your webhook receiver. For development, start the development receiver,`,
|
|
345
391
|
`which prints them, with the same WEBHOOK_URL and WEBHOOK_SECRET as in ${file}:`,
|
|
346
392
|
"",
|
|
347
393
|
` ${DEV_WEBHOOK_RECEIVER_URL}`
|
|
348
394
|
],
|
|
349
|
-
totp && (google || password) && [
|
|
395
|
+
totp && (google || github || password) && [
|
|
350
396
|
"Signing in with the authenticator app alone is off until an admin lists it. Once the server runs:",
|
|
351
397
|
"",
|
|
352
398
|
` npx @madauth/server set-methods ${methodList} --env-file ${envFile}`
|
|
@@ -377,6 +423,11 @@ function nextSteps(file, origin, google, password, totp = false) {
|
|
|
377
423
|
` Authorized JavaScript origins: ${[...googleOrigins].join(" and ")}`,
|
|
378
424
|
...google === "GoogleRedirect" ? [` Authorized redirect URIs: ${origin}/auth/google/callback`] : []
|
|
379
425
|
],
|
|
426
|
+
github && [
|
|
427
|
+
"In your GitHub OAuth App (https://github.com/settings/developers), set:",
|
|
428
|
+
"",
|
|
429
|
+
` Authorization callback URL: ${origin}/auth/github/callback`
|
|
430
|
+
],
|
|
380
431
|
[
|
|
381
432
|
`Never commit ${file}: it holds the signing key and your secrets. Production needs an https app URL`,
|
|
382
433
|
"and a signing key of its own: run init again for it."
|
|
@@ -441,17 +492,17 @@ async function methods(command, args, env) {
|
|
|
441
492
|
const next = {};
|
|
442
493
|
for (const arg of args) {
|
|
443
494
|
const [name, policy, ...more] = arg.split("=");
|
|
444
|
-
if (!
|
|
445
|
-
return { output: `Unknown sign-in method "${arg}". Methods: ${
|
|
495
|
+
if (!METHODS.includes(name) || more.length) {
|
|
496
|
+
return { output: `Unknown sign-in method "${arg}". Methods: ${METHODS.join(", ")}, e.g. set-methods google password=required totp sms.`, exitCode: 1 };
|
|
446
497
|
}
|
|
447
498
|
const method = name;
|
|
448
|
-
if (policy !== void 0 && (method
|
|
499
|
+
if (policy !== void 0 && (!hasSecondFactor(method) || !SECOND_FACTOR_POLICIES.includes(policy))) {
|
|
449
500
|
return {
|
|
450
|
-
output: `"${arg}": google
|
|
501
|
+
output: `"${arg}": google, github, password and email-link take a policy for the second factor: ${SECOND_FACTOR_POLICIES.join(", ")}.`,
|
|
451
502
|
exitCode: 1
|
|
452
503
|
};
|
|
453
504
|
}
|
|
454
|
-
next[method] = method
|
|
505
|
+
next[method] = hasSecondFactor(method) ? { secondFactor: policy ?? "none" } : {};
|
|
455
506
|
}
|
|
456
507
|
if (!anyOn(signsUp, next)) {
|
|
457
508
|
const usable = SIGN_IN_METHODS.filter(signsUp);
|
|
@@ -464,13 +515,38 @@ async function methods(command, args, env) {
|
|
|
464
515
|
}
|
|
465
516
|
const current = await settings.methods();
|
|
466
517
|
const describe = (method) => {
|
|
467
|
-
const policy = policyOf(method, current);
|
|
468
|
-
return
|
|
518
|
+
const policy = hasSecondFactor(method) ? policyOf(method, current) : "none";
|
|
519
|
+
return policy !== "none" ? `${method} (second factor ${policy})` : method;
|
|
469
520
|
};
|
|
470
|
-
const on =
|
|
471
|
-
const off =
|
|
521
|
+
const on = METHODS.filter((method) => isOn(method, configured, current)).map(describe);
|
|
522
|
+
const off = METHODS.filter((method) => configured(method) && !isOn(method, configured, current));
|
|
472
523
|
return { output: `Switched on: ${on.join(", ")}.${off.length ? ` Switched off: ${off.join(", ")}.` : ""}`, exitCode: 0 };
|
|
473
524
|
}
|
|
525
|
+
function describeRules(rules) {
|
|
526
|
+
return `New passwords: at least ${rules.minLength} characters, and a strength score of at least ${rules.minScore} (0 to 4).`;
|
|
527
|
+
}
|
|
528
|
+
async function passwordRules(command, args, env) {
|
|
529
|
+
const settings = new Settings(await loadStore(env));
|
|
530
|
+
if (command === "set-password-rules") {
|
|
531
|
+
const fail = (output2) => ({ output: output2, exitCode: 1 });
|
|
532
|
+
const rules = { ...DEFAULT_PASSWORD_RULES };
|
|
533
|
+
for (const arg of args) {
|
|
534
|
+
const [name, value, ...more] = arg.split("=");
|
|
535
|
+
const number = value !== void 0 && /^\d+$/.test(value) && !more.length ? Number(value) : NaN;
|
|
536
|
+
if (name === "min-length") {
|
|
537
|
+
if (!(number >= 1 && number <= MAX_MIN_LENGTH)) return fail(`min-length must be a whole number from 1 to ${MAX_MIN_LENGTH} but is "${value ?? ""}".`);
|
|
538
|
+
rules.minLength = number;
|
|
539
|
+
} else if (name === "min-score") {
|
|
540
|
+
if (!isPasswordScore(number)) return fail(`min-score must be 0, 1, 2, 3 or 4 but is "${value ?? ""}".`);
|
|
541
|
+
rules.minScore = number;
|
|
542
|
+
} else {
|
|
543
|
+
return fail(`Unknown password rule "${arg}". Rules: min-length=<n>, min-score=<0-4>, e.g. set-password-rules min-length=10 min-score=3.`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
await settings.setPasswordRules(rules, null);
|
|
547
|
+
}
|
|
548
|
+
return { output: describeRules(await settings.passwordRules()), exitCode: 0 };
|
|
549
|
+
}
|
|
474
550
|
async function removeTotp(email, env) {
|
|
475
551
|
if (!isValidEmail(email)) return { output: `"${email ?? ""}" is not an e-mail address.
|
|
476
552
|
|
|
@@ -485,14 +561,28 @@ ${usage}`, exitCode: 1 };
|
|
|
485
561
|
exitCode: 0
|
|
486
562
|
};
|
|
487
563
|
}
|
|
564
|
+
async function removeSms(email, env) {
|
|
565
|
+
if (!isValidEmail(email)) return { output: `"${email ?? ""}" is not an e-mail address.
|
|
566
|
+
|
|
567
|
+
${usage}`, exitCode: 1 };
|
|
568
|
+
const users = new Users(await loadStore(env));
|
|
569
|
+
const address = normalizeEmail(email);
|
|
570
|
+
const user = await users.findByEmail(address);
|
|
571
|
+
if (!user) return { output: `No user has the e-mail address ${address}.`, exitCode: 1 };
|
|
572
|
+
const removed = await users.removeSms(user.id);
|
|
573
|
+
return {
|
|
574
|
+
output: removed ? `Removed the phone number of ${address}.` : `${address} has no phone number for SMS codes.`,
|
|
575
|
+
exitCode: 0
|
|
576
|
+
};
|
|
577
|
+
}
|
|
488
578
|
async function createUser(email, io, env) {
|
|
489
579
|
if (!isValidEmail(email)) return { output: `"${email ?? ""}" is not an e-mail address.
|
|
490
580
|
|
|
491
581
|
${usage}`, exitCode: 1 };
|
|
492
|
-
const
|
|
582
|
+
const store = await loadStore(env);
|
|
493
583
|
const password = await io.askSecret(`Password for ${email}: `);
|
|
494
|
-
const
|
|
495
|
-
if (
|
|
584
|
+
const problem = await checkPasswordPolicy(password, await new Settings(store).passwordRules(), [email]);
|
|
585
|
+
if (problem) return { output: problem.message, exitCode: 1 };
|
|
496
586
|
const users = new Users(store);
|
|
497
587
|
const passwordHash = await hashPassword(password);
|
|
498
588
|
const user = await users.createUser({
|