@madauth/server 0.2.0-beta.1 → 0.3.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/dist/bin.js +98 -31
- package/dist/{chunk-CNL562LZ.js → chunk-56MOVFO4.js} +24 -2
- package/dist/{chunk-XNBWZZHT.js → chunk-CD7Y4T66.js} +11 -9
- package/dist/{chunk-7CTAB3QV.js → chunk-DCDTO5LG.js} +2 -2
- package/dist/{chunk-6EJDVZCD.js → chunk-NPNM26J3.js} +9 -2
- package/dist/{chunk-5CKEMC6M.js → chunk-ODITZLMM.js} +218 -86
- package/dist/{chunk-YALS7OMV.js → chunk-ZSBKNV5W.js} +217 -122
- package/dist/{config-BkxADgtp.d.ts → config-DEBYi8hb.d.ts} +7 -6
- package/dist/dynamodb.js +1 -1
- package/dist/entry/azure.d.ts +3 -3
- package/dist/entry/azure.js +4 -4
- package/dist/entry/lambda.d.ts +3 -3
- package/dist/entry/lambda.js +4 -4
- package/dist/entry/node.d.ts +3 -3
- package/dist/entry/node.js +4 -4
- package/dist/index.d.ts +30 -7
- package/dist/index.js +15 -7
- package/dist/{schema-BSgYVRpd.d.ts → schema-3yhCc2jH.d.ts} +17 -9
- package/dist/sqlite.js +2 -2
- package/dist/standalone/azure.mjs +799 -564
- package/dist/standalone/lambda.mjs +799 -564
- package/dist/standalone/main.mjs +563 -328
- 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 +2 -0
- package/dist/user-BMWEWREH.d.ts +12 -0
- package/dist/verify.d.ts +1 -1
- package/dist/verify.js +1 -1
- package/dist/{webhook-ltwulgcB.d.ts → webhook-DCDP6SaE.d.ts} +1 -1
- package/dist/webhook.d.ts +1 -1
- package/dist/webhook.js +1 -1
- package/package.json +1 -1
- package/dist/user-C1A3BfOo.d.ts +0 -12
package/dist/standalone/main.mjs
CHANGED
|
@@ -38,8 +38,9 @@ var init_schema = __esm({
|
|
|
38
38
|
"src/store/schema.ts"() {
|
|
39
39
|
"use strict";
|
|
40
40
|
madauthSchema = {
|
|
41
|
-
version:
|
|
41
|
+
version: 4,
|
|
42
42
|
models: {
|
|
43
|
+
/** Someone who signed in, whichever way: the e-mail address is the identity shared by the sign-in methods. */
|
|
43
44
|
user: {
|
|
44
45
|
fields: {
|
|
45
46
|
id: { type: "string", primaryKey: true },
|
|
@@ -51,10 +52,12 @@ var init_schema = __esm({
|
|
|
51
52
|
lastMailAt: { type: "number" },
|
|
52
53
|
/** Wrong e-mail codes in a row, counted across e-mails (since version 2). */
|
|
53
54
|
wrongCodes: { type: "number" },
|
|
55
|
+
/** What admins attached to the user, as JSON, e.g. `{"roles":["admin"]}` (since version 4). */
|
|
56
|
+
claims: { type: "string", nullable: true },
|
|
54
57
|
createdAt: { type: "number" }
|
|
55
58
|
}
|
|
56
59
|
},
|
|
57
|
-
/** How a user signs in. `key` is
|
|
60
|
+
/** How a user signs in. `key` is `password:<userId>` or `google:<sub>`; later e.g. `totp:<userId>`. */
|
|
58
61
|
account: {
|
|
59
62
|
fields: {
|
|
60
63
|
id: { type: "string", primaryKey: true },
|
|
@@ -63,6 +66,8 @@ var init_schema = __esm({
|
|
|
63
66
|
secret: { type: "string", nullable: true },
|
|
64
67
|
failedAttempts: { type: "number" },
|
|
65
68
|
lockedUntil: { type: "number" },
|
|
69
|
+
/** The address the provider reported at the last sign-in, e.g. Google's current primary address (since version 4). */
|
|
70
|
+
email: { type: "string", nullable: true },
|
|
66
71
|
createdAt: { type: "number" }
|
|
67
72
|
}
|
|
68
73
|
},
|
|
@@ -77,16 +82,13 @@ var init_schema = __esm({
|
|
|
77
82
|
expiresAt: { type: "number" }
|
|
78
83
|
}
|
|
79
84
|
},
|
|
80
|
-
/**
|
|
81
|
-
|
|
82
|
-
* apply whichever way its owner signs in. `roles` holds the names separated by spaces.
|
|
83
|
-
*/
|
|
84
|
-
role: {
|
|
85
|
+
/** A setting admins change at runtime, as JSON (since version 4), e.g. `methods`: which sign-in methods are on. */
|
|
86
|
+
setting: {
|
|
85
87
|
fields: {
|
|
86
88
|
id: { type: "string", primaryKey: true },
|
|
87
|
-
|
|
89
|
+
value: { type: "string" },
|
|
88
90
|
updatedAt: { type: "number" },
|
|
89
|
-
/** The address of the admin who set
|
|
91
|
+
/** The address of the admin who set it; empty when set from the command line. */
|
|
90
92
|
updatedBy: { type: "string", nullable: true }
|
|
91
93
|
}
|
|
92
94
|
}
|
|
@@ -141,8 +143,30 @@ var init_sql = __esm({
|
|
|
141
143
|
sql: (dialect) => `ALTER TABLE ${tableName("user")} ADD COLUMN ${columnName("wrongCodes")} ${columnTypes[dialect].number} NOT NULL DEFAULT 0;`
|
|
142
144
|
},
|
|
143
145
|
{
|
|
146
|
+
// Roles were per e-mail address in their own table; since version 4 they are one of the user's claims.
|
|
144
147
|
version: 3,
|
|
145
|
-
sql: (dialect) => createTablesSql(dialect, {
|
|
148
|
+
sql: (dialect) => createTablesSql(dialect, {
|
|
149
|
+
version: 3,
|
|
150
|
+
models: {
|
|
151
|
+
role: {
|
|
152
|
+
fields: {
|
|
153
|
+
id: { type: "string", primaryKey: true },
|
|
154
|
+
roles: { type: "string" },
|
|
155
|
+
updatedAt: { type: "number" },
|
|
156
|
+
updatedBy: { type: "string", nullable: true }
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
version: 4,
|
|
164
|
+
sql: (dialect) => [
|
|
165
|
+
`ALTER TABLE ${tableName("user")} ADD COLUMN ${columnName("claims")} ${columnTypes[dialect].string};`,
|
|
166
|
+
`ALTER TABLE ${tableName("account")} ADD COLUMN ${columnName("email")} ${columnTypes[dialect].string};`,
|
|
167
|
+
`DROP TABLE ${tableName("role")};`,
|
|
168
|
+
createTablesSql(dialect, { version: 4, models: { setting: madauthSchema.models.setting } })
|
|
169
|
+
].join("\n")
|
|
146
170
|
}
|
|
147
171
|
];
|
|
148
172
|
}
|
|
@@ -29999,11 +30023,16 @@ var WEBHOOK_TYPES = [
|
|
|
29999
30023
|
"signup.before",
|
|
30000
30024
|
"user.created",
|
|
30001
30025
|
"email.verified",
|
|
30002
|
-
"
|
|
30026
|
+
"email.password_reset",
|
|
30003
30027
|
"user.signed_in",
|
|
30004
30028
|
"user.deleted",
|
|
30005
|
-
"
|
|
30029
|
+
"user.claims_changed"
|
|
30006
30030
|
];
|
|
30031
|
+
var WEBHOOK_TIMEOUT_MS = 1e4;
|
|
30032
|
+
var MAX_LOCALE_LENGTH = 35;
|
|
30033
|
+
function localeOf(value) {
|
|
30034
|
+
return typeof value === "string" && value.length <= MAX_LOCALE_LENGTH ? value : void 0;
|
|
30035
|
+
}
|
|
30007
30036
|
var REQUIRED_EMAIL_TYPES = ["email.verify", "email.reset"];
|
|
30008
30037
|
var SECRET_PREFIX = "whsec_";
|
|
30009
30038
|
var DEV_WEBHOOK_RECEIVER_URL = "https://github.com/inouiw/madAuth-webhooks/tree/main/dev-receiver";
|
|
@@ -30124,19 +30153,18 @@ MADAUTH_SIGNING_KEY='${JSON.stringify(key)}'
|
|
|
30124
30153
|
if (clientId && !clientId.endsWith(".apps.googleusercontent.com")) {
|
|
30125
30154
|
throw new ConfigError(`GOOGLE_CLIENT_ID must end with ".apps.googleusercontent.com" but is "${clientId}".`);
|
|
30126
30155
|
}
|
|
30127
|
-
const
|
|
30128
|
-
const
|
|
30129
|
-
|
|
30156
|
+
const webhook = webhookFromEnv(read("WEBHOOK_URL"), read("WEBHOOK_SECRET"), read("WEBHOOK_EVENTS"));
|
|
30157
|
+
const sendsEmails = !!webhook && REQUIRED_EMAIL_TYPES.every((type) => webhook.events.has(type));
|
|
30158
|
+
const password = sendsEmails ? { minLength: passwordMinLength(read("PASSWORD_MIN_LENGTH")) } : void 0;
|
|
30159
|
+
if (!clientId && !password) {
|
|
30130
30160
|
throw new ConfigError(
|
|
30131
|
-
`
|
|
30161
|
+
`No sign-in method is configured. Set GOOGLE_CLIENT_ID for Google sign-in, and/or a webhook that sends the e-mails for e-mail & password sign-in: for development, run the receiver at ${DEV_WEBHOOK_RECEIVER_URL} and set WEBHOOK_URL=http://localhost:8790/webhook and WEBHOOK_EVENTS=${REQUIRED_EMAIL_TYPES.join(",")},email.already_registered. See docs/server.md.`
|
|
30132
30162
|
);
|
|
30133
30163
|
}
|
|
30134
30164
|
const store = overrides.store ?? await storeFromDatabaseUrl(read("DATABASE_URL"));
|
|
30135
|
-
|
|
30136
|
-
if (store) password = { minLength: passwordMinLength(read("PASSWORD_MIN_LENGTH")), store };
|
|
30137
|
-
if (!clientId && !password) {
|
|
30165
|
+
if (!store) {
|
|
30138
30166
|
throw new ConfigError(
|
|
30139
|
-
"
|
|
30167
|
+
"DATABASE_URL is not set. madAuth stores every user, whichever way they sign in: set e.g. DATABASE_URL=sqlite:./madauth.db or DATABASE_URL=dynamodb:<table>. See docs/server.md."
|
|
30140
30168
|
);
|
|
30141
30169
|
}
|
|
30142
30170
|
const ttlValue = read("SESSION_TTL");
|
|
@@ -30159,6 +30187,7 @@ MADAUTH_SIGNING_KEY='${JSON.stringify(key)}'
|
|
|
30159
30187
|
renewalTtlSeconds,
|
|
30160
30188
|
cookieDomain: read("COOKIE_DOMAIN"),
|
|
30161
30189
|
google: clientId ? { clientId, clientSecret: read("GOOGLE_CLIENT_SECRET") } : void 0,
|
|
30190
|
+
store,
|
|
30162
30191
|
password,
|
|
30163
30192
|
webhook
|
|
30164
30193
|
};
|
|
@@ -30204,8 +30233,9 @@ async function dynamoDbStore(tableName2) {
|
|
|
30204
30233
|
const { createDynamoDbAdapter: createDynamoDbAdapter2 } = await Promise.resolve().then(() => (init_dynamodb(), dynamodb_exports));
|
|
30205
30234
|
return createDynamoDbAdapter2({ tableName: tableName2 });
|
|
30206
30235
|
}
|
|
30236
|
+
var RENAMED_TYPES = { "password.reset": "email.password_reset", "roles.changed": "user.claims_changed" };
|
|
30207
30237
|
var LOCAL_HOSTS = ["localhost", "127.0.0.1", "[::1]", "host.docker.internal"];
|
|
30208
|
-
function webhookFromEnv(url, secret, events
|
|
30238
|
+
function webhookFromEnv(url, secret, events) {
|
|
30209
30239
|
if (!url) {
|
|
30210
30240
|
if (secret || events) throw new ConfigError("WEBHOOK_SECRET and WEBHOOK_EVENTS need WEBHOOK_URL.");
|
|
30211
30241
|
return void 0;
|
|
@@ -30235,24 +30265,22 @@ WEBHOOK_SECRET=${generateWebhookSecret()}
|
|
|
30235
30265
|
const allTypes = `All types: ${WEBHOOK_TYPES.join(", ")}. See "Webhooks" in docs/server.md.`;
|
|
30236
30266
|
if (!selected.size) {
|
|
30237
30267
|
throw new ConfigError(
|
|
30238
|
-
|
|
30268
|
+
`WEBHOOK_EVENTS is not set. List the types your webhook receiver handles; only these are sent. E-mail & password sign-in is on when they include ${REQUIRED_EMAIL_TYPES.join(" and ")}. For a receiver that sends the e-mails:
|
|
30239
30269
|
|
|
30240
30270
|
WEBHOOK_EVENTS=email.verify,email.reset,email.already_registered
|
|
30241
30271
|
|
|
30242
|
-
|
|
30272
|
+
For one that only wants to know who signed in:
|
|
30273
|
+
|
|
30274
|
+
WEBHOOK_EVENTS=user.signed_in
|
|
30275
|
+
|
|
30276
|
+
` + allTypes
|
|
30243
30277
|
);
|
|
30244
30278
|
}
|
|
30245
30279
|
const unknown = [...selected].filter((e6) => !WEBHOOK_TYPES.includes(e6));
|
|
30246
30280
|
if (unknown.length) {
|
|
30247
|
-
|
|
30248
|
-
}
|
|
30249
|
-
const missing = sendsEmails ? REQUIRED_EMAIL_TYPES.filter((type) => !selected.has(type)) : [];
|
|
30250
|
-
if (missing.length) {
|
|
30281
|
+
const renamed = unknown.filter((e6) => e6 in RENAMED_TYPES).map((e6) => `${e6} is now ${RENAMED_TYPES[e6]}`);
|
|
30251
30282
|
throw new ConfigError(
|
|
30252
|
-
`WEBHOOK_EVENTS
|
|
30253
|
-
|
|
30254
|
-
WEBHOOK_EVENTS=${[...selected, ...missing].join(",")}
|
|
30255
|
-
`
|
|
30283
|
+
`WEBHOOK_EVENTS: unknown type ${unknown.join(", ")}. ${renamed.length ? `${renamed.join("; ")}. ` : ""}Known types: ${WEBHOOK_TYPES.join(", ")}.`
|
|
30256
30284
|
);
|
|
30257
30285
|
}
|
|
30258
30286
|
return { url: parsed.href, secret, events: selected };
|
|
@@ -33551,6 +33579,63 @@ var cors = (options) => {
|
|
|
33551
33579
|
};
|
|
33552
33580
|
};
|
|
33553
33581
|
|
|
33582
|
+
// src/claims.ts
|
|
33583
|
+
var ADMIN_ROLE = "admin";
|
|
33584
|
+
var MAX_ROLES = 20;
|
|
33585
|
+
var MAX_CLAIMS_LENGTH = 2048;
|
|
33586
|
+
var ROLE_NAME = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
33587
|
+
var CLAIM_NAME = /^[a-zA-Z][a-zA-Z0-9_]{0,63}$/;
|
|
33588
|
+
function parseRoles(input) {
|
|
33589
|
+
if (!Array.isArray(input)) return null;
|
|
33590
|
+
if (!input.every((role) => typeof role === "string" && ROLE_NAME.test(role))) return null;
|
|
33591
|
+
const roles = [...new Set(input)].sort();
|
|
33592
|
+
return roles.length <= MAX_ROLES ? roles : null;
|
|
33593
|
+
}
|
|
33594
|
+
function parseClaims(input) {
|
|
33595
|
+
if (!isJsonObject(input)) return null;
|
|
33596
|
+
const claims = {};
|
|
33597
|
+
for (const key of Object.keys(input).sort()) {
|
|
33598
|
+
const value = input[key];
|
|
33599
|
+
if (!CLAIM_NAME.test(key) || !isJsonValue(value)) return null;
|
|
33600
|
+
if (key === "roles") {
|
|
33601
|
+
const roles = parseRoles(value);
|
|
33602
|
+
if (!roles) return null;
|
|
33603
|
+
claims[key] = roles;
|
|
33604
|
+
} else {
|
|
33605
|
+
claims[key] = value;
|
|
33606
|
+
}
|
|
33607
|
+
}
|
|
33608
|
+
return JSON.stringify(claims).length <= MAX_CLAIMS_LENGTH ? claims : null;
|
|
33609
|
+
}
|
|
33610
|
+
function isJsonObject(value) {
|
|
33611
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
33612
|
+
}
|
|
33613
|
+
function isJsonValue(value) {
|
|
33614
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
33615
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
33616
|
+
if (Array.isArray(value)) return value.every(isJsonValue);
|
|
33617
|
+
return isJsonObject(value) && Object.values(value).every(isJsonValue);
|
|
33618
|
+
}
|
|
33619
|
+
function sameClaims(a6, b6) {
|
|
33620
|
+
return JSON.stringify(a6 ?? {}) === JSON.stringify(b6 ?? {});
|
|
33621
|
+
}
|
|
33622
|
+
function claimsFromJson(json2) {
|
|
33623
|
+
if (!json2) return {};
|
|
33624
|
+
try {
|
|
33625
|
+
const parsed = JSON.parse(json2);
|
|
33626
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
33627
|
+
} catch {
|
|
33628
|
+
return {};
|
|
33629
|
+
}
|
|
33630
|
+
}
|
|
33631
|
+
function rolesOf(user) {
|
|
33632
|
+
const roles = user.claims?.roles;
|
|
33633
|
+
return Array.isArray(roles) ? roles.filter((role) => typeof role === "string") : [];
|
|
33634
|
+
}
|
|
33635
|
+
function isAdmin(user) {
|
|
33636
|
+
return rolesOf(user).includes(ADMIN_ROLE);
|
|
33637
|
+
}
|
|
33638
|
+
|
|
33554
33639
|
// src/password.ts
|
|
33555
33640
|
import { createHash as createHash4, createHmac as createHmac3, hkdfSync, randomBytes as randomBytes2, randomInt, scrypt, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
33556
33641
|
var HASH_PARAMS = { logN: 15, r: 8, p: 3 };
|
|
@@ -33626,45 +33711,6 @@ function safeEqual(a6, b6) {
|
|
|
33626
33711
|
return x2.length === y2.length && timingSafeEqual2(x2, y2);
|
|
33627
33712
|
}
|
|
33628
33713
|
|
|
33629
|
-
// src/roles.ts
|
|
33630
|
-
var ADMIN_ROLE = "admin";
|
|
33631
|
-
var MAX_ROLES = 20;
|
|
33632
|
-
var ROLE_NAME = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
33633
|
-
function parseRoles(input) {
|
|
33634
|
-
if (!Array.isArray(input)) return null;
|
|
33635
|
-
if (!input.every((role) => typeof role === "string" && ROLE_NAME.test(role))) return null;
|
|
33636
|
-
const roles = [...new Set(input)].sort();
|
|
33637
|
-
return roles.length <= MAX_ROLES ? roles : null;
|
|
33638
|
-
}
|
|
33639
|
-
function sameRoles(a6, b6) {
|
|
33640
|
-
return a6.length === b6.length && a6.every((role, i6) => role === b6[i6]);
|
|
33641
|
-
}
|
|
33642
|
-
var Roles = class {
|
|
33643
|
-
constructor(store) {
|
|
33644
|
-
this.store = store;
|
|
33645
|
-
}
|
|
33646
|
-
/** The address's roles, sorted; empty if it has none. */
|
|
33647
|
-
async get(emailNormalized) {
|
|
33648
|
-
const record = await this.store.findOne("role", { id: emailNormalized });
|
|
33649
|
-
return typeof record?.roles === "string" && record.roles ? record.roles.split(" ").filter(Boolean) : [];
|
|
33650
|
-
}
|
|
33651
|
-
/** Replaces the address's roles. `updatedBy` is the address of the admin, or null for the command line. */
|
|
33652
|
-
async set(emailNormalized, roles, updatedBy) {
|
|
33653
|
-
if (!roles.length) {
|
|
33654
|
-
await this.remove(emailNormalized);
|
|
33655
|
-
return;
|
|
33656
|
-
}
|
|
33657
|
-
const data = { roles: roles.join(" "), updatedAt: Date.now(), updatedBy };
|
|
33658
|
-
if (await this.store.update("role", { id: emailNormalized }, data) === 1) return;
|
|
33659
|
-
if (!await this.store.create("role", { id: emailNormalized, ...data })) {
|
|
33660
|
-
await this.store.update("role", { id: emailNormalized }, data);
|
|
33661
|
-
}
|
|
33662
|
-
}
|
|
33663
|
-
async remove(emailNormalized) {
|
|
33664
|
-
await this.store.delete("role", { id: emailNormalized });
|
|
33665
|
-
}
|
|
33666
|
-
};
|
|
33667
|
-
|
|
33668
33714
|
// src/google.ts
|
|
33669
33715
|
var GOOGLE_ISSUERS = ["https://accounts.google.com", "accounts.google.com"];
|
|
33670
33716
|
var googleJwks;
|
|
@@ -33689,14 +33735,13 @@ async function verifyGoogleIdToken(idToken, opts) {
|
|
|
33689
33735
|
if (payload.nonce !== opts.nonce) {
|
|
33690
33736
|
return { ok: false, error: "verification_failed", reason: "nonce mismatch" };
|
|
33691
33737
|
}
|
|
33692
|
-
if (payload.email_verified !== true) {
|
|
33738
|
+
if (typeof payload.email !== "string" || !payload.email || payload.email_verified !== true) {
|
|
33693
33739
|
return { ok: false, error: "email_unverified", reason: "e-mail address not verified" };
|
|
33694
33740
|
}
|
|
33695
|
-
const
|
|
33696
|
-
if (typeof payload.
|
|
33697
|
-
if (typeof payload.
|
|
33698
|
-
|
|
33699
|
-
return { ok: true, user };
|
|
33741
|
+
const profile = { sub: payload.sub, email: payload.email };
|
|
33742
|
+
if (typeof payload.name === "string") profile.name = payload.name;
|
|
33743
|
+
if (typeof payload.picture === "string") profile.picture = payload.picture;
|
|
33744
|
+
return { ok: true, profile };
|
|
33700
33745
|
}
|
|
33701
33746
|
|
|
33702
33747
|
// src/user.ts
|
|
@@ -33730,7 +33775,7 @@ function signSession(keys, issuer, ttlSeconds, user, amr, extra = {}) {
|
|
|
33730
33775
|
email_verified: user.email ? true : void 0,
|
|
33731
33776
|
name: user.name,
|
|
33732
33777
|
picture: user.picture,
|
|
33733
|
-
|
|
33778
|
+
claims: user.claims && Object.keys(user.claims).length ? user.claims : void 0,
|
|
33734
33779
|
amr
|
|
33735
33780
|
};
|
|
33736
33781
|
return signToken(keys, issuer, SESSION_TYP, claims, ttlSeconds);
|
|
@@ -33752,10 +33797,203 @@ function userFromClaims(claims) {
|
|
|
33752
33797
|
if (claims.email) user.email = claims.email;
|
|
33753
33798
|
if (claims.name) user.name = claims.name;
|
|
33754
33799
|
if (claims.picture) user.picture = claims.picture;
|
|
33755
|
-
if (
|
|
33800
|
+
if (claims.claims && typeof claims.claims === "object" && Object.keys(claims.claims).length) user.claims = claims.claims;
|
|
33756
33801
|
return user;
|
|
33757
33802
|
}
|
|
33758
33803
|
|
|
33804
|
+
// src/users.ts
|
|
33805
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
33806
|
+
var MAX_CODE_ATTEMPTS = 5;
|
|
33807
|
+
var MAX_WRONG_CODES = 10;
|
|
33808
|
+
var newId = (prefix) => `${prefix}_${randomBytes3(16).toString("base64url")}`;
|
|
33809
|
+
function passwordAccountKey(userId) {
|
|
33810
|
+
return `password:${userId}`;
|
|
33811
|
+
}
|
|
33812
|
+
function googleAccountKey(sub) {
|
|
33813
|
+
return `google:${sub}`;
|
|
33814
|
+
}
|
|
33815
|
+
function toMadauthUser(user, profile = {}) {
|
|
33816
|
+
const result = { id: user.id, email: user.email };
|
|
33817
|
+
const name = user.name ?? profile.name;
|
|
33818
|
+
if (name) result.name = name;
|
|
33819
|
+
if (profile.picture) result.picture = profile.picture;
|
|
33820
|
+
const claims = claimsFromJson(user.claims);
|
|
33821
|
+
if (Object.keys(claims).length) result.claims = claims;
|
|
33822
|
+
return result;
|
|
33823
|
+
}
|
|
33824
|
+
var Users = class {
|
|
33825
|
+
/** `codeKey` keys the e-mail codes (see deriveCodeKey); only needed to issue and check them. */
|
|
33826
|
+
constructor(store, codeKey) {
|
|
33827
|
+
this.store = store;
|
|
33828
|
+
this.codeKey = codeKey;
|
|
33829
|
+
}
|
|
33830
|
+
get key() {
|
|
33831
|
+
if (!this.codeKey) throw new Error("Users needs a code key to issue or check e-mail codes.");
|
|
33832
|
+
return this.codeKey;
|
|
33833
|
+
}
|
|
33834
|
+
async findByEmail(emailNormalized) {
|
|
33835
|
+
return await this.store.findOne("user", { emailNormalized });
|
|
33836
|
+
}
|
|
33837
|
+
async findById(id) {
|
|
33838
|
+
return await this.store.findOne("user", { id });
|
|
33839
|
+
}
|
|
33840
|
+
async passwordAccount(userId) {
|
|
33841
|
+
return this.findAccountByKey(passwordAccountKey(userId));
|
|
33842
|
+
}
|
|
33843
|
+
async findAccountByKey(key) {
|
|
33844
|
+
return await this.store.findOne("account", { key });
|
|
33845
|
+
}
|
|
33846
|
+
/**
|
|
33847
|
+
* Creates a user with their first account. Resolves to null if the e-mail address or the account key is
|
|
33848
|
+
* taken. `account.key` is `password:<userId>` for a password (its hash as `secret`), or e.g. `google:<sub>`.
|
|
33849
|
+
*/
|
|
33850
|
+
async createUser(data) {
|
|
33851
|
+
const user = {
|
|
33852
|
+
id: newId("usr"),
|
|
33853
|
+
email: data.email,
|
|
33854
|
+
emailNormalized: data.emailNormalized,
|
|
33855
|
+
emailVerified: data.emailVerified,
|
|
33856
|
+
name: data.name,
|
|
33857
|
+
sessionVersion: 0,
|
|
33858
|
+
lastMailAt: 0,
|
|
33859
|
+
wrongCodes: 0,
|
|
33860
|
+
claims: null,
|
|
33861
|
+
createdAt: Date.now()
|
|
33862
|
+
};
|
|
33863
|
+
if (!await this.store.create("user", user)) return null;
|
|
33864
|
+
const account = typeof data.account === "function" ? data.account(user.id) : data.account;
|
|
33865
|
+
if (!await this.linkAccount(user.id, account)) {
|
|
33866
|
+
await this.store.delete("user", { id: user.id });
|
|
33867
|
+
return null;
|
|
33868
|
+
}
|
|
33869
|
+
return user;
|
|
33870
|
+
}
|
|
33871
|
+
/**
|
|
33872
|
+
* A provider verified the user's address: an unconfirmed password sign-up with it, whose password nobody
|
|
33873
|
+
* has proven, is dropped (the account and its pending links and codes), and the user counts as verified.
|
|
33874
|
+
*/
|
|
33875
|
+
async verifyByProvider(user) {
|
|
33876
|
+
if (user.emailVerified) return;
|
|
33877
|
+
await this.store.delete("account", { key: passwordAccountKey(user.id) });
|
|
33878
|
+
await this.clearVerifications(user.id);
|
|
33879
|
+
await this.updateUser(user.id, { emailVerified: true, wrongCodes: 0 });
|
|
33880
|
+
}
|
|
33881
|
+
/** Removes an account record, e.g. one left behind by a deletion that did not finish. */
|
|
33882
|
+
async deleteAccount(id) {
|
|
33883
|
+
await this.store.delete("account", { id });
|
|
33884
|
+
}
|
|
33885
|
+
/** Adds a sign-in method to a user. Resolves to null if the account key is taken. */
|
|
33886
|
+
async linkAccount(userId, account) {
|
|
33887
|
+
const record = {
|
|
33888
|
+
id: newId("acc"),
|
|
33889
|
+
userId,
|
|
33890
|
+
key: account.key,
|
|
33891
|
+
secret: account.secret ?? null,
|
|
33892
|
+
failedAttempts: 0,
|
|
33893
|
+
lockedUntil: 0,
|
|
33894
|
+
email: account.email ?? null,
|
|
33895
|
+
createdAt: Date.now()
|
|
33896
|
+
};
|
|
33897
|
+
return await this.store.create("account", record) ? record : null;
|
|
33898
|
+
}
|
|
33899
|
+
/**
|
|
33900
|
+
* Deletes the user and everything stored for them. Resolves to false if there is no such user (any more).
|
|
33901
|
+
*/
|
|
33902
|
+
async deleteById(id) {
|
|
33903
|
+
const user = await this.findById(id);
|
|
33904
|
+
if (!user) return false;
|
|
33905
|
+
if (await this.store.delete("user", { id: user.id }) !== 1) return false;
|
|
33906
|
+
try {
|
|
33907
|
+
await this.store.delete("account", { userId: user.id });
|
|
33908
|
+
await this.store.delete("verification", { userId: user.id });
|
|
33909
|
+
} catch (e6) {
|
|
33910
|
+
console.error(`[madauth] Deleted user ${user.id}, but not all of their records: ${e6.message}`);
|
|
33911
|
+
}
|
|
33912
|
+
return true;
|
|
33913
|
+
}
|
|
33914
|
+
async updateUser(id, patch) {
|
|
33915
|
+
await this.store.update("user", { id }, patch);
|
|
33916
|
+
}
|
|
33917
|
+
async updateAccount(id, patch) {
|
|
33918
|
+
await this.store.update("account", { id }, patch);
|
|
33919
|
+
}
|
|
33920
|
+
/**
|
|
33921
|
+
* Counts a sign-in attempt: writes `patch` only if the account's `failedAttempts` is still the value that
|
|
33922
|
+
* was read. Resolves to false if another request counted an attempt meanwhile.
|
|
33923
|
+
*/
|
|
33924
|
+
async countAttempt(account, patch) {
|
|
33925
|
+
const changed = await this.store.update("account", { id: account.id, failedAttempts: account.failedAttempts }, patch);
|
|
33926
|
+
return changed === 1;
|
|
33927
|
+
}
|
|
33928
|
+
/** Deletes the user's pending verifications and password resets; their links and codes stop working. */
|
|
33929
|
+
async clearVerifications(userId) {
|
|
33930
|
+
await this.store.delete("verification", { userId });
|
|
33931
|
+
}
|
|
33932
|
+
/**
|
|
33933
|
+
* Starts a verification or password reset: replaces the user's earlier one for the same purpose and
|
|
33934
|
+
* returns the link token and the code for the e-mail. Only their hashes are stored.
|
|
33935
|
+
*/
|
|
33936
|
+
async issueVerification(userId, purpose, ttlMs) {
|
|
33937
|
+
await this.store.delete("verification", { userId, purpose });
|
|
33938
|
+
const token = generateLinkToken();
|
|
33939
|
+
const code = generateCode();
|
|
33940
|
+
const record = {
|
|
33941
|
+
id: hashLinkToken(token),
|
|
33942
|
+
userId,
|
|
33943
|
+
purpose,
|
|
33944
|
+
codeHash: hashCode(this.key, code),
|
|
33945
|
+
attempts: 0,
|
|
33946
|
+
expiresAt: Date.now() + ttlMs
|
|
33947
|
+
};
|
|
33948
|
+
await this.store.create("verification", record);
|
|
33949
|
+
return { token, code };
|
|
33950
|
+
}
|
|
33951
|
+
/** Uses up the verification a link token belongs to. Resolves to its user ID, or null if invalid, expired or used. */
|
|
33952
|
+
async consumeLinkToken(token, purpose) {
|
|
33953
|
+
const id = hashLinkToken(token);
|
|
33954
|
+
const record = await this.store.findOne("verification", { id, purpose });
|
|
33955
|
+
return record ? this.consume(record) : null;
|
|
33956
|
+
}
|
|
33957
|
+
/**
|
|
33958
|
+
* Uses up the user's verification if `code` matches. Too many wrong codes delete it, and after
|
|
33959
|
+
* MAX_WRONG_CODES wrong codes in a row the user's codes stop working until an e-mail link is used.
|
|
33960
|
+
*/
|
|
33961
|
+
async consumeCode(user, purpose, code) {
|
|
33962
|
+
if ((user.wrongCodes ?? 0) >= MAX_WRONG_CODES) return { userId: null, locked: true };
|
|
33963
|
+
const record = await this.store.findOne("verification", { userId: user.id, purpose });
|
|
33964
|
+
if (!record) return { userId: null, locked: false };
|
|
33965
|
+
const attempts = record.attempts + 1;
|
|
33966
|
+
if (await this.store.update("verification", { id: record.id, attempts: record.attempts }, { attempts }) !== 1) {
|
|
33967
|
+
return { userId: null, locked: false };
|
|
33968
|
+
}
|
|
33969
|
+
const wrongCodes = await this.countWrongCode(user);
|
|
33970
|
+
if (wrongCodes === null) return { userId: null, locked: false };
|
|
33971
|
+
if (wrongCodes > MAX_WRONG_CODES) return { userId: null, locked: true };
|
|
33972
|
+
if (!safeEqual(hashCode(this.key, code), record.codeHash)) {
|
|
33973
|
+
if (attempts >= MAX_CODE_ATTEMPTS) await this.store.delete("verification", { id: record.id });
|
|
33974
|
+
return { userId: null, locked: wrongCodes >= MAX_WRONG_CODES };
|
|
33975
|
+
}
|
|
33976
|
+
return { userId: await this.consume(record), locked: false };
|
|
33977
|
+
}
|
|
33978
|
+
/** Counts a code for the user and resolves to the new count; null if other requests kept changing it. */
|
|
33979
|
+
async countWrongCode(user) {
|
|
33980
|
+
let current = user;
|
|
33981
|
+
for (let attempt = 0; attempt < 3 && current; attempt++) {
|
|
33982
|
+
const wrongCodes = (current.wrongCodes ?? 0) + 1;
|
|
33983
|
+
if (await this.store.update("user", { id: user.id, wrongCodes: current.wrongCodes ?? null }, { wrongCodes }) === 1) {
|
|
33984
|
+
return wrongCodes;
|
|
33985
|
+
}
|
|
33986
|
+
current = await this.findById(user.id);
|
|
33987
|
+
}
|
|
33988
|
+
return null;
|
|
33989
|
+
}
|
|
33990
|
+
async consume(record) {
|
|
33991
|
+
const deleted = await this.store.delete("verification", { id: record.id });
|
|
33992
|
+
if (deleted !== 1 || record.expiresAt <= Date.now()) return null;
|
|
33993
|
+
return record.userId;
|
|
33994
|
+
}
|
|
33995
|
+
};
|
|
33996
|
+
|
|
33759
33997
|
// src/routes/google.ts
|
|
33760
33998
|
var NONCE_COOKIE = "madauth_nonce";
|
|
33761
33999
|
var NONCE_TYP = "madauth-nonce+jwt";
|
|
@@ -33771,10 +34009,57 @@ async function pkceChallenge(verifier) {
|
|
|
33771
34009
|
return base64url_exports.encode(new Uint8Array(digest2));
|
|
33772
34010
|
}
|
|
33773
34011
|
function googleRoutes(app, ctx, google) {
|
|
33774
|
-
const { config, keys, secure } = ctx;
|
|
34012
|
+
const { config, keys, secure, users } = ctx;
|
|
33775
34013
|
const { issuer } = config;
|
|
33776
34014
|
const callbackUrl = `${issuer}/auth/google/callback`;
|
|
34015
|
+
const resolveUser = async (profile, locale) => {
|
|
34016
|
+
const key = googleAccountKey(profile.sub);
|
|
34017
|
+
const emailNormalized = normalizeEmail(profile.email);
|
|
34018
|
+
const signedIn = (user) => ({ ok: true, user: toMadauthUser(user, profile), sv: user.sessionVersion });
|
|
34019
|
+
const account = await users.findAccountByKey(key);
|
|
34020
|
+
if (account) {
|
|
34021
|
+
const user = await users.findById(account.userId);
|
|
34022
|
+
if (user) {
|
|
34023
|
+
if (account.email !== profile.email) await users.updateAccount(account.id, { email: profile.email });
|
|
34024
|
+
return signedIn(user);
|
|
34025
|
+
}
|
|
34026
|
+
await users.deleteAccount(account.id);
|
|
34027
|
+
}
|
|
34028
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
34029
|
+
const existing = await users.findByEmail(emailNormalized);
|
|
34030
|
+
if (existing) {
|
|
34031
|
+
await users.verifyByProvider(existing);
|
|
34032
|
+
if (!await users.linkAccount(existing.id, { key, email: profile.email })) {
|
|
34033
|
+
const linked = await users.findAccountByKey(key);
|
|
34034
|
+
const owner = linked && await users.findById(linked.userId);
|
|
34035
|
+
if (!owner) return { ok: false, error: "temporarily_unavailable", message: "Please try again." };
|
|
34036
|
+
return signedIn(owner);
|
|
34037
|
+
}
|
|
34038
|
+
return signedIn(existing);
|
|
34039
|
+
}
|
|
34040
|
+
if (attempt === 0) {
|
|
34041
|
+
const check2 = await ctx.checkSignup({ email: profile.email, name: profile.name, locale, method: "google" });
|
|
34042
|
+
if (!check2.ok) {
|
|
34043
|
+
return check2.reason === "rejected" ? { ok: false, error: "signup_rejected", message: check2.message } : { ok: false, error: "temporarily_unavailable", message: "Signing up is not possible right now. Please try again later." };
|
|
34044
|
+
}
|
|
34045
|
+
}
|
|
34046
|
+
const user = await users.createUser({
|
|
34047
|
+
email: profile.email,
|
|
34048
|
+
emailNormalized,
|
|
34049
|
+
name: profile.name ?? null,
|
|
34050
|
+
emailVerified: true,
|
|
34051
|
+
account: { key, email: profile.email }
|
|
34052
|
+
});
|
|
34053
|
+
if (user) {
|
|
34054
|
+
await ctx.emit("user.created", { user: toMadauthUser(user), method: "google" });
|
|
34055
|
+
return signedIn(user);
|
|
34056
|
+
}
|
|
34057
|
+
}
|
|
34058
|
+
return { ok: false, error: "temporarily_unavailable", message: "Please try again." };
|
|
34059
|
+
};
|
|
34060
|
+
const disabled = (c6) => c6.json({ error: "method_disabled", message: "Google sign-in is switched off." }, 403);
|
|
33777
34061
|
app.post("/auth/google/nonce", async (c6) => {
|
|
34062
|
+
if (!await ctx.enabled("google")) return disabled(c6);
|
|
33778
34063
|
const nonce = randomString();
|
|
33779
34064
|
const token = await signToken(await keys, issuer, NONCE_TYP, { nonce }, NONCE_TTL);
|
|
33780
34065
|
setCookie(c6, NONCE_COOKIE, token, {
|
|
@@ -33787,6 +34072,7 @@ function googleRoutes(app, ctx, google) {
|
|
|
33787
34072
|
return c6.json({ nonce });
|
|
33788
34073
|
});
|
|
33789
34074
|
app.post("/auth/google/verify", async (c6) => {
|
|
34075
|
+
if (!await ctx.enabled("google")) return disabled(c6);
|
|
33790
34076
|
const body = await c6.req.json().catch(() => ({}));
|
|
33791
34077
|
if (typeof body.credential !== "string" || !body.credential) {
|
|
33792
34078
|
return c6.json({ error: "verification_failed", message: "credential is missing" }, 400);
|
|
@@ -33804,12 +34090,17 @@ function googleRoutes(app, ctx, google) {
|
|
|
33804
34090
|
return c6.json({ error: result.error, message: result.reason }, result.error === "email_unverified" ? 403 : 401);
|
|
33805
34091
|
}
|
|
33806
34092
|
deleteCookie(c6, NONCE_COOKIE, { path: "/auth/google", secure });
|
|
33807
|
-
const
|
|
34093
|
+
const resolved = await resolveUser(result.profile, localeOf(body.locale));
|
|
34094
|
+
if (!resolved.ok) {
|
|
34095
|
+
return c6.json({ error: resolved.error, message: resolved.message }, resolved.error === "signup_rejected" ? 403 : 503);
|
|
34096
|
+
}
|
|
34097
|
+
const user = await ctx.startSession(c6, resolved.user, ["google"], { sv: resolved.sv });
|
|
33808
34098
|
await ctx.emit("user.signed_in", { user, method: "google" });
|
|
33809
34099
|
return c6.json({ user });
|
|
33810
34100
|
});
|
|
33811
34101
|
const requireCodeFlow = async (c6, next) => {
|
|
33812
34102
|
if (!google.clientSecret) return c6.json({ error: "not_found" }, 404);
|
|
34103
|
+
if (!await ctx.enabled("google")) return disabled(c6);
|
|
33813
34104
|
await next();
|
|
33814
34105
|
};
|
|
33815
34106
|
app.get("/auth/google/start", requireCodeFlow, async (c6) => {
|
|
@@ -33819,7 +34110,8 @@ function googleRoutes(app, ctx, google) {
|
|
|
33819
34110
|
state: randomString(),
|
|
33820
34111
|
nonce: randomString(),
|
|
33821
34112
|
verifier: randomString(),
|
|
33822
|
-
returnTo: returnUrl.href
|
|
34113
|
+
returnTo: returnUrl.href,
|
|
34114
|
+
locale: localeOf(c6.req.query("locale"))
|
|
33823
34115
|
};
|
|
33824
34116
|
const token = await signToken(await keys, issuer, OAUTH_TYP, { ...oauth }, OAUTH_TTL);
|
|
33825
34117
|
setCookie(c6, OAUTH_COOKIE, token, {
|
|
@@ -33880,7 +34172,9 @@ function googleRoutes(app, ctx, google) {
|
|
|
33880
34172
|
keys: config.jwksResolver
|
|
33881
34173
|
});
|
|
33882
34174
|
if (!result.ok) return back(result.error);
|
|
33883
|
-
const
|
|
34175
|
+
const resolved = await resolveUser(result.profile, typeof oauth.locale === "string" ? oauth.locale : void 0);
|
|
34176
|
+
if (!resolved.ok) return back(resolved.error);
|
|
34177
|
+
const user = await ctx.startSession(c6, resolved.user, ["google"], { sv: resolved.sv });
|
|
33884
34178
|
await ctx.emit("user.signed_in", { user, method: "google" });
|
|
33885
34179
|
return back();
|
|
33886
34180
|
});
|
|
@@ -33892,18 +34186,16 @@ var MAX_LOCK_MS = 15 * 60 * 1e3;
|
|
|
33892
34186
|
var MAIL_INTERVAL_MS = 60 * 1e3;
|
|
33893
34187
|
var VERIFY_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
33894
34188
|
var RESET_TTL_MS = 30 * 60 * 1e3;
|
|
33895
|
-
var WEBHOOK_TIMEOUT_MS = 1e4;
|
|
33896
34189
|
var VERIFY_LINK_PARAM = "madauth_verify";
|
|
33897
34190
|
var RESET_LINK_PARAM = "madauth_reset";
|
|
33898
34191
|
var MAX_NAME_LENGTH = 100;
|
|
33899
|
-
var MAX_LOCALE_LENGTH = 35;
|
|
33900
|
-
function toMadauthUser(user) {
|
|
33901
|
-
const result = { id: user.id, email: user.email };
|
|
33902
|
-
if (user.name) result.name = user.name;
|
|
33903
|
-
return result;
|
|
33904
|
-
}
|
|
33905
34192
|
function passwordRoutes(app, ctx, password, users, webhook) {
|
|
33906
34193
|
const { minLength } = password;
|
|
34194
|
+
const disabled = (c6) => c6.json({ error: "method_disabled", message: "E-mail & password sign-in is switched off." }, 403);
|
|
34195
|
+
app.use("/auth/password/*", async (c6, next) => {
|
|
34196
|
+
if (!await ctx.enabled("password")) return disabled(c6);
|
|
34197
|
+
await next();
|
|
34198
|
+
});
|
|
33907
34199
|
const body = async (c6) => {
|
|
33908
34200
|
const data = await c6.req.json().catch(() => null);
|
|
33909
34201
|
return data && typeof data === "object" ? data : {};
|
|
@@ -33939,7 +34231,6 @@ function passwordRoutes(app, ctx, password, users, webhook) {
|
|
|
33939
34231
|
locale
|
|
33940
34232
|
});
|
|
33941
34233
|
};
|
|
33942
|
-
const localeOf = (data) => typeof data.locale === "string" && data.locale.length <= MAX_LOCALE_LENGTH ? data.locale : void 0;
|
|
33943
34234
|
const consume = async (data, purpose) => {
|
|
33944
34235
|
if (typeof data.token === "string" && data.token) {
|
|
33945
34236
|
return { userId: await users.consumeLinkToken(data.token, purpose), via: "link" };
|
|
@@ -34001,19 +34292,9 @@ function passwordRoutes(app, ctx, password, users, webhook) {
|
|
|
34001
34292
|
if (policy) return error(c6, 400, "weak_password", policy);
|
|
34002
34293
|
const name = typeof data.name === "string" && data.name.trim() ? data.name.trim().slice(0, MAX_NAME_LENGTH) : null;
|
|
34003
34294
|
const email = data.email.trim();
|
|
34004
|
-
const locale = localeOf(data);
|
|
34005
|
-
|
|
34006
|
-
|
|
34007
|
-
if (!check2.ok) {
|
|
34008
|
-
console.error(`[madauth] Webhook "signup.before" failed: ${check2.reason}`);
|
|
34009
|
-
return unavailable(c6);
|
|
34010
|
-
}
|
|
34011
|
-
const answer = check2.body;
|
|
34012
|
-
if (answer?.allow === false) {
|
|
34013
|
-
const message2 = typeof answer.message === "string" && answer.message ? answer.message : "Sign-up is not possible with this e-mail address.";
|
|
34014
|
-
return error(c6, 403, "signup_rejected", message2);
|
|
34015
|
-
}
|
|
34016
|
-
}
|
|
34295
|
+
const locale = localeOf(data.locale);
|
|
34296
|
+
const check2 = await ctx.checkSignup({ email, name: name ?? void 0, locale, method: "password" });
|
|
34297
|
+
if (!check2.ok) return check2.reason === "rejected" ? error(c6, 403, "signup_rejected", check2.message) : unavailable(c6);
|
|
34017
34298
|
const passwordHash = await hashPassword(data.password);
|
|
34018
34299
|
const existing = await users.findByEmail(normalizeEmail(email));
|
|
34019
34300
|
if (existing?.emailVerified) {
|
|
@@ -34025,18 +34306,19 @@ function passwordRoutes(app, ctx, password, users, webhook) {
|
|
|
34025
34306
|
if (Date.now() - existing.lastMailAt < MAIL_INTERVAL_MS) return accepted(c6);
|
|
34026
34307
|
const account = await users.passwordAccount(existing.id);
|
|
34027
34308
|
if (account) await users.updateAccount(account.id, { secret: passwordHash, failedAttempts: 0, lockedUntil: 0 });
|
|
34309
|
+
else await users.linkAccount(existing.id, { key: passwordAccountKey(existing.id), secret: passwordHash });
|
|
34028
34310
|
await users.updateUser(existing.id, { name });
|
|
34029
34311
|
return await sendLinkEmail({ ...existing, name }, "verify", target, locale) ? accepted(c6) : unavailable(c6);
|
|
34030
34312
|
}
|
|
34031
|
-
const user = await users.
|
|
34313
|
+
const user = await users.createUser({
|
|
34032
34314
|
email,
|
|
34033
34315
|
emailNormalized: normalizeEmail(email),
|
|
34034
34316
|
name,
|
|
34035
|
-
|
|
34036
|
-
|
|
34317
|
+
emailVerified: false,
|
|
34318
|
+
account: (userId) => ({ key: passwordAccountKey(userId), secret: passwordHash })
|
|
34037
34319
|
});
|
|
34038
34320
|
if (!user) return accepted(c6);
|
|
34039
|
-
await ctx.emit("user.created", { user: toMadauthUser(user) });
|
|
34321
|
+
await ctx.emit("user.created", { user: toMadauthUser(user), method: "password" });
|
|
34040
34322
|
return await sendLinkEmail(user, "verify", target, locale) ? accepted(c6) : unavailable(c6);
|
|
34041
34323
|
});
|
|
34042
34324
|
app.post("/auth/password/send-verification", async (c6) => {
|
|
@@ -34046,7 +34328,7 @@ function passwordRoutes(app, ctx, password, users, webhook) {
|
|
|
34046
34328
|
if (!isValidEmail(data.email)) return error(c6, 400, "invalid_email", "This is not a valid e-mail address.");
|
|
34047
34329
|
const user = await users.findByEmail(normalizeEmail(data.email));
|
|
34048
34330
|
if (user && !user.emailVerified && await users.passwordAccount(user.id)) {
|
|
34049
|
-
if (!await sendLinkEmail(user, "verify", target, localeOf(data))) return unavailable(c6);
|
|
34331
|
+
if (!await sendLinkEmail(user, "verify", target, localeOf(data.locale))) return unavailable(c6);
|
|
34050
34332
|
}
|
|
34051
34333
|
return accepted(c6);
|
|
34052
34334
|
});
|
|
@@ -34067,8 +34349,8 @@ function passwordRoutes(app, ctx, password, users, webhook) {
|
|
|
34067
34349
|
if (target instanceof Response) return target;
|
|
34068
34350
|
if (!isValidEmail(data.email)) return error(c6, 400, "invalid_email", "This is not a valid e-mail address.");
|
|
34069
34351
|
const user = await users.findByEmail(normalizeEmail(data.email));
|
|
34070
|
-
if (user && await users.passwordAccount(user.id)) {
|
|
34071
|
-
if (!await sendLinkEmail(user, "reset", target, localeOf(data))) return unavailable(c6);
|
|
34352
|
+
if (user && (user.emailVerified || await users.passwordAccount(user.id))) {
|
|
34353
|
+
if (!await sendLinkEmail(user, "reset", target, localeOf(data.locale))) return unavailable(c6);
|
|
34072
34354
|
}
|
|
34073
34355
|
return accepted(c6);
|
|
34074
34356
|
});
|
|
@@ -34078,174 +34360,88 @@ function passwordRoutes(app, ctx, password, users, webhook) {
|
|
|
34078
34360
|
if (policy) return error(c6, 400, "weak_password", policy);
|
|
34079
34361
|
const consumed = await consume(data, "reset");
|
|
34080
34362
|
const user = consumed.userId ? await users.findById(consumed.userId) : null;
|
|
34081
|
-
|
|
34082
|
-
|
|
34083
|
-
|
|
34363
|
+
if (!user) return invalidVerification(c6, consumed);
|
|
34364
|
+
const secret = await hashPassword(data.password);
|
|
34365
|
+
const account = await users.passwordAccount(user.id);
|
|
34366
|
+
if (account) await users.updateAccount(account.id, { secret, failedAttempts: 0, lockedUntil: 0 });
|
|
34367
|
+
else await users.linkAccount(user.id, { key: passwordAccountKey(user.id), secret });
|
|
34084
34368
|
const sessionVersion = user.sessionVersion + 1;
|
|
34085
34369
|
await users.updateUser(user.id, { emailVerified: true, sessionVersion, wrongCodes: 0 });
|
|
34086
34370
|
await users.clearVerifications(user.id);
|
|
34087
34371
|
const result = await ctx.startSession(c6, toMadauthUser(user), ["pwd"], { sv: sessionVersion });
|
|
34088
|
-
await ctx.emit("
|
|
34372
|
+
await ctx.emit("email.password_reset", { user: result });
|
|
34089
34373
|
await ctx.emit("user.signed_in", { user: result, method: "password" });
|
|
34090
34374
|
return c6.json({ user: result });
|
|
34091
34375
|
});
|
|
34092
34376
|
}
|
|
34093
34377
|
|
|
34094
|
-
// src/
|
|
34095
|
-
|
|
34096
|
-
var
|
|
34097
|
-
|
|
34098
|
-
|
|
34099
|
-
|
|
34100
|
-
|
|
34378
|
+
// src/settings.ts
|
|
34379
|
+
var SIGN_IN_METHODS = ["google", "password"];
|
|
34380
|
+
var METHODS_SETTING = "methods";
|
|
34381
|
+
function parseMethodSettings(input) {
|
|
34382
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return null;
|
|
34383
|
+
const settings = {};
|
|
34384
|
+
for (const [key, value] of Object.entries(input)) {
|
|
34385
|
+
if (!SIGN_IN_METHODS.includes(key) || typeof value !== "boolean") return null;
|
|
34386
|
+
settings[key] = value;
|
|
34387
|
+
}
|
|
34388
|
+
return settings;
|
|
34101
34389
|
}
|
|
34102
|
-
var
|
|
34103
|
-
|
|
34104
|
-
constructor(store
|
|
34390
|
+
var MAX_TRIES = 3;
|
|
34391
|
+
var Settings = class {
|
|
34392
|
+
constructor(store) {
|
|
34105
34393
|
this.store = store;
|
|
34106
|
-
this.codeKey = codeKey;
|
|
34107
|
-
}
|
|
34108
|
-
get key() {
|
|
34109
|
-
if (!this.codeKey) throw new Error("Users needs a code key to issue or check e-mail codes.");
|
|
34110
|
-
return this.codeKey;
|
|
34111
|
-
}
|
|
34112
|
-
async findByEmail(emailNormalized) {
|
|
34113
|
-
return await this.store.findOne("user", { emailNormalized });
|
|
34114
|
-
}
|
|
34115
|
-
async findById(id) {
|
|
34116
|
-
return await this.store.findOne("user", { id });
|
|
34117
|
-
}
|
|
34118
|
-
async passwordAccount(userId) {
|
|
34119
|
-
return await this.store.findOne("account", { key: passwordAccountKey(userId) });
|
|
34120
34394
|
}
|
|
34121
|
-
/**
|
|
34122
|
-
async
|
|
34123
|
-
|
|
34124
|
-
const user = {
|
|
34125
|
-
id: newId("usr"),
|
|
34126
|
-
email: data.email,
|
|
34127
|
-
emailNormalized: data.emailNormalized,
|
|
34128
|
-
emailVerified: data.emailVerified,
|
|
34129
|
-
name: data.name,
|
|
34130
|
-
sessionVersion: 0,
|
|
34131
|
-
lastMailAt: 0,
|
|
34132
|
-
wrongCodes: 0,
|
|
34133
|
-
createdAt: now
|
|
34134
|
-
};
|
|
34135
|
-
if (!await this.store.create("user", user)) return null;
|
|
34136
|
-
const account = {
|
|
34137
|
-
id: newId("acc"),
|
|
34138
|
-
userId: user.id,
|
|
34139
|
-
key: passwordAccountKey(user.id),
|
|
34140
|
-
secret: data.passwordHash,
|
|
34141
|
-
failedAttempts: 0,
|
|
34142
|
-
lockedUntil: 0,
|
|
34143
|
-
createdAt: now
|
|
34144
|
-
};
|
|
34145
|
-
if (!await this.store.create("account", account)) {
|
|
34146
|
-
await this.store.delete("user", { id: user.id });
|
|
34147
|
-
return null;
|
|
34148
|
-
}
|
|
34149
|
-
return user;
|
|
34395
|
+
/** The setting's value, or undefined if it was never set. */
|
|
34396
|
+
async get(id) {
|
|
34397
|
+
return (await this.read(id)).value;
|
|
34150
34398
|
}
|
|
34151
|
-
/**
|
|
34152
|
-
|
|
34153
|
-
|
|
34154
|
-
|
|
34155
|
-
async deleteByEmail(emailNormalized) {
|
|
34156
|
-
const user = await this.findByEmail(emailNormalized);
|
|
34157
|
-
if (!user) return null;
|
|
34158
|
-
if (await this.store.delete("user", { id: user.id }) !== 1) return null;
|
|
34399
|
+
/** The stored JSON and what it holds; both undefined if the setting was never set. */
|
|
34400
|
+
async read(id) {
|
|
34401
|
+
const record = await this.store.findOne("setting", { id });
|
|
34402
|
+
if (typeof record?.value !== "string") return {};
|
|
34159
34403
|
try {
|
|
34160
|
-
|
|
34161
|
-
|
|
34162
|
-
|
|
34163
|
-
console.error(`[madauth] Deleted user ${user.id}, but not all of their records: ${e6.message}`);
|
|
34404
|
+
return { json: record.value, value: JSON.parse(record.value) };
|
|
34405
|
+
} catch {
|
|
34406
|
+
return { json: record.value };
|
|
34164
34407
|
}
|
|
34165
|
-
return user.id;
|
|
34166
|
-
}
|
|
34167
|
-
async updateUser(id, patch) {
|
|
34168
|
-
await this.store.update("user", { id }, patch);
|
|
34169
|
-
}
|
|
34170
|
-
async updateAccount(id, patch) {
|
|
34171
|
-
await this.store.update("account", { id }, patch);
|
|
34172
34408
|
}
|
|
34173
34409
|
/**
|
|
34174
|
-
*
|
|
34175
|
-
*
|
|
34176
|
-
|
|
34177
|
-
|
|
34178
|
-
const changed = await this.store.update("account", { id: account.id, failedAttempts: account.failedAttempts }, patch);
|
|
34179
|
-
return changed === 1;
|
|
34180
|
-
}
|
|
34181
|
-
/** Deletes the user's pending verifications and password resets; their links and codes stop working. */
|
|
34182
|
-
async clearVerifications(userId) {
|
|
34183
|
-
await this.store.delete("verification", { userId });
|
|
34184
|
-
}
|
|
34185
|
-
/**
|
|
34186
|
-
* Starts a verification or password reset: replaces the user's earlier one for the same purpose and
|
|
34187
|
-
* returns the link token and the code for the e-mail. Only their hashes are stored.
|
|
34410
|
+
* Changes the setting: `apply` gets the current value and returns the new one, or null to refuse the
|
|
34411
|
+
* change. The write only lands if nobody changed the setting since `apply` read it; otherwise `apply` runs
|
|
34412
|
+
* again with the newer value. Resolves to the new value, or null if `apply` refused.
|
|
34413
|
+
* `updatedBy` is the address of the admin, or null for the command line.
|
|
34188
34414
|
*/
|
|
34189
|
-
async
|
|
34190
|
-
|
|
34191
|
-
|
|
34192
|
-
|
|
34193
|
-
|
|
34194
|
-
|
|
34195
|
-
|
|
34196
|
-
|
|
34197
|
-
codeHash: hashCode(this.key, code),
|
|
34198
|
-
attempts: 0,
|
|
34199
|
-
expiresAt: Date.now() + ttlMs
|
|
34200
|
-
};
|
|
34201
|
-
await this.store.create("verification", record);
|
|
34202
|
-
return { token, code };
|
|
34203
|
-
}
|
|
34204
|
-
/** Uses up the verification a link token belongs to. Resolves to its user ID, or null if invalid, expired or used. */
|
|
34205
|
-
async consumeLinkToken(token, purpose) {
|
|
34206
|
-
const id = hashLinkToken(token);
|
|
34207
|
-
const record = await this.store.findOne("verification", { id, purpose });
|
|
34208
|
-
return record ? this.consume(record) : null;
|
|
34209
|
-
}
|
|
34210
|
-
/**
|
|
34211
|
-
* Uses up the user's verification if `code` matches. Too many wrong codes delete it, and after
|
|
34212
|
-
* MAX_WRONG_CODES wrong codes in a row the user's codes stop working until an e-mail link is used.
|
|
34213
|
-
*/
|
|
34214
|
-
async consumeCode(user, purpose, code) {
|
|
34215
|
-
if ((user.wrongCodes ?? 0) >= MAX_WRONG_CODES) return { userId: null, locked: true };
|
|
34216
|
-
const record = await this.store.findOne("verification", { userId: user.id, purpose });
|
|
34217
|
-
if (!record) return { userId: null, locked: false };
|
|
34218
|
-
const attempts = record.attempts + 1;
|
|
34219
|
-
if (await this.store.update("verification", { id: record.id, attempts: record.attempts }, { attempts }) !== 1) {
|
|
34220
|
-
return { userId: null, locked: false };
|
|
34221
|
-
}
|
|
34222
|
-
const wrongCodes = await this.countWrongCode(user);
|
|
34223
|
-
if (wrongCodes === null) return { userId: null, locked: false };
|
|
34224
|
-
if (wrongCodes > MAX_WRONG_CODES) return { userId: null, locked: true };
|
|
34225
|
-
if (!safeEqual(hashCode(this.key, code), record.codeHash)) {
|
|
34226
|
-
if (attempts >= MAX_CODE_ATTEMPTS) await this.store.delete("verification", { id: record.id });
|
|
34227
|
-
return { userId: null, locked: wrongCodes >= MAX_WRONG_CODES };
|
|
34415
|
+
async change(id, apply, updatedBy) {
|
|
34416
|
+
for (let attempt = 0; attempt < MAX_TRIES; attempt++) {
|
|
34417
|
+
const { json: json2, value } = await this.read(id);
|
|
34418
|
+
const next = apply(value);
|
|
34419
|
+
if (next === null) return null;
|
|
34420
|
+
const patch = { value: JSON.stringify(next), updatedAt: Date.now(), updatedBy };
|
|
34421
|
+
const written = json2 === void 0 ? await this.store.create("setting", { id, ...patch }) : await this.store.update("setting", { id, value: json2 }, patch) === 1;
|
|
34422
|
+
if (written) return next;
|
|
34228
34423
|
}
|
|
34229
|
-
|
|
34424
|
+
throw new Error(`Setting "${id}" kept changing; try again.`);
|
|
34230
34425
|
}
|
|
34231
|
-
/**
|
|
34232
|
-
async
|
|
34233
|
-
|
|
34234
|
-
for (let attempt = 0; attempt < 3 && current; attempt++) {
|
|
34235
|
-
const wrongCodes = (current.wrongCodes ?? 0) + 1;
|
|
34236
|
-
if (await this.store.update("user", { id: user.id, wrongCodes: current.wrongCodes ?? null }, { wrongCodes }) === 1) {
|
|
34237
|
-
return wrongCodes;
|
|
34238
|
-
}
|
|
34239
|
-
current = await this.findById(user.id);
|
|
34240
|
-
}
|
|
34241
|
-
return null;
|
|
34426
|
+
/** Replaces the setting. */
|
|
34427
|
+
async set(id, value, updatedBy) {
|
|
34428
|
+
await this.change(id, () => value, updatedBy);
|
|
34242
34429
|
}
|
|
34243
|
-
|
|
34244
|
-
|
|
34245
|
-
|
|
34246
|
-
|
|
34430
|
+
/** Which sign-in methods are on; a method that was never switched off is on. */
|
|
34431
|
+
async methods() {
|
|
34432
|
+
return parseMethodSettings(await this.get(METHODS_SETTING)) ?? {};
|
|
34433
|
+
}
|
|
34434
|
+
/** Changes which methods are on, see {@link change}; `apply` gets the methods as they are now. */
|
|
34435
|
+
async changeMethods(apply, updatedBy) {
|
|
34436
|
+
return this.change(METHODS_SETTING, (current) => apply(parseMethodSettings(current) ?? {}), updatedBy);
|
|
34247
34437
|
}
|
|
34248
34438
|
};
|
|
34439
|
+
function isOn(method, available, methods) {
|
|
34440
|
+
return available(method) && methods[method] !== false;
|
|
34441
|
+
}
|
|
34442
|
+
function anyOn(available, methods) {
|
|
34443
|
+
return SIGN_IN_METHODS.some((method) => isOn(method, available, methods));
|
|
34444
|
+
}
|
|
34249
34445
|
|
|
34250
34446
|
// src/app.ts
|
|
34251
34447
|
var EVENT_TIMEOUT_MS = 5e3;
|
|
@@ -34254,12 +34450,9 @@ function createApp(config) {
|
|
|
34254
34450
|
const keys = importSigningKeys(config.signingKey);
|
|
34255
34451
|
const secure = issuer.startsWith("https:");
|
|
34256
34452
|
const isAllowedOrigin = (origin) => !!origin && allowedOrigins.includes(origin);
|
|
34257
|
-
const
|
|
34258
|
-
const
|
|
34259
|
-
|
|
34260
|
-
const current = roles && user.email ? await roles.get(normalizeEmail(user.email)) : [];
|
|
34261
|
-
return current.length ? { ...rest, roles: current } : rest;
|
|
34262
|
-
};
|
|
34453
|
+
const users = new Users(config.store, deriveCodeKey(config.signingKey.d));
|
|
34454
|
+
const settings = new Settings(config.store);
|
|
34455
|
+
const available = (method) => method === "google" ? !!config.google : !!config.password;
|
|
34263
34456
|
const issueSession = async (c6, user, amr, extra) => {
|
|
34264
34457
|
const signingKeys = await keys;
|
|
34265
34458
|
const [sessionToken, renewalToken] = await Promise.all([
|
|
@@ -34311,12 +34504,29 @@ function createApp(config) {
|
|
|
34311
34504
|
return url;
|
|
34312
34505
|
},
|
|
34313
34506
|
async startSession(c6, user, amr, extra) {
|
|
34314
|
-
|
|
34315
|
-
|
|
34316
|
-
return signedIn;
|
|
34507
|
+
await issueSession(c6, user, amr, extra);
|
|
34508
|
+
return user;
|
|
34317
34509
|
},
|
|
34318
|
-
users
|
|
34510
|
+
users,
|
|
34511
|
+
settings,
|
|
34319
34512
|
webhook: config.webhook ? createWebhookClient(config.webhook, config.webhookFetch) : void 0,
|
|
34513
|
+
async enabled(method) {
|
|
34514
|
+
return isOn(method, available, await settings.methods());
|
|
34515
|
+
},
|
|
34516
|
+
async checkSignup(data) {
|
|
34517
|
+
if (!this.webhook?.wants("signup.before")) return { ok: true };
|
|
34518
|
+
const check2 = await this.webhook.call("signup.before", data, WEBHOOK_TIMEOUT_MS);
|
|
34519
|
+
if (!check2.ok) {
|
|
34520
|
+
console.error(`[madauth] Webhook "signup.before" failed: ${check2.reason}`);
|
|
34521
|
+
return { ok: false, reason: "unavailable" };
|
|
34522
|
+
}
|
|
34523
|
+
const answer = check2.body;
|
|
34524
|
+
if (answer?.allow === false) {
|
|
34525
|
+
const message2 = typeof answer.message === "string" && answer.message ? answer.message : "Sign-up is not possible with this e-mail address.";
|
|
34526
|
+
return { ok: false, reason: "rejected", message: message2 };
|
|
34527
|
+
}
|
|
34528
|
+
return { ok: true };
|
|
34529
|
+
},
|
|
34320
34530
|
async emit(type, data) {
|
|
34321
34531
|
if (!this.webhook?.wants(type)) return;
|
|
34322
34532
|
const result = await this.webhook.call(type, data, EVENT_TIMEOUT_MS);
|
|
@@ -34346,29 +34556,28 @@ function createApp(config) {
|
|
|
34346
34556
|
c6.header("Cache-Control", "public, max-age=3600");
|
|
34347
34557
|
return c6.json({ keys: [publicJwk] });
|
|
34348
34558
|
});
|
|
34349
|
-
app.get(
|
|
34350
|
-
|
|
34351
|
-
(
|
|
34352
|
-
|
|
34353
|
-
|
|
34354
|
-
|
|
34355
|
-
|
|
34559
|
+
app.get("/auth/config", async (c6) => {
|
|
34560
|
+
const methods = await settings.methods();
|
|
34561
|
+
const on = (method) => isOn(method, available, methods);
|
|
34562
|
+
return c6.json({
|
|
34563
|
+
google: on("google") ? { clientId: config.google.clientId, codeFlow: !!config.google.clientSecret } : null,
|
|
34564
|
+
password: on("password") ? { minLength: config.password.minLength } : null
|
|
34565
|
+
});
|
|
34566
|
+
});
|
|
34356
34567
|
if (config.google) googleRoutes(app, ctx, config.google);
|
|
34357
|
-
if (config.password && ctx.
|
|
34568
|
+
if (config.password && ctx.webhook) passwordRoutes(app, ctx, config.password, users, ctx.webhook);
|
|
34358
34569
|
const tokenClaims = async (c6, cookie, typ) => readToken(await keys, issuer, typ, getCookie(c6, cookie));
|
|
34359
|
-
const hasEnded = async (claims) => {
|
|
34360
|
-
if (!claims.sub.startsWith("usr_")) return false;
|
|
34361
|
-
const stored = await ctx.users?.findById(claims.sub);
|
|
34362
|
-
return !stored || stored.sessionVersion !== claims.sv;
|
|
34363
|
-
};
|
|
34364
34570
|
const checked = async (claims) => {
|
|
34365
|
-
const
|
|
34366
|
-
|
|
34571
|
+
const stored = await users.findById(claims.sub);
|
|
34572
|
+
if (!stored || stored.sessionVersion !== claims.sv) return null;
|
|
34573
|
+
const { claims: _, ...user } = userFromClaims(claims);
|
|
34574
|
+
const current = claimsFromJson(stored.claims);
|
|
34575
|
+
return Object.keys(current).length ? { ...user, claims: current } : user;
|
|
34367
34576
|
};
|
|
34368
34577
|
const currentSession = async (c6) => {
|
|
34369
34578
|
const session = await tokenClaims(c6, SESSION_COOKIE, SESSION_TYP);
|
|
34370
34579
|
const user = session ? await checked(session) : null;
|
|
34371
|
-
const upToDate = session && user &&
|
|
34580
|
+
const upToDate = session && user && sameClaims(user.claims, session.claims) && Date.now() / 1e3 - session.iat <= sessionTtlSeconds / 2;
|
|
34372
34581
|
if (upToDate) return user;
|
|
34373
34582
|
const renewal = await tokenClaims(c6, RENEWAL_COOKIE, RENEWAL_TYP);
|
|
34374
34583
|
const sameSignIn = session && user && renewal && renewal.sub === session.sub && renewal.sv === session.sv && renewal.email === session.email;
|
|
@@ -34394,56 +34603,82 @@ function createApp(config) {
|
|
|
34394
34603
|
app.post("/auth/account/delete", async (c6) => {
|
|
34395
34604
|
const user = await currentSession(c6);
|
|
34396
34605
|
if (!user) return c6.json({ error: "no_session" }, 401);
|
|
34397
|
-
|
|
34398
|
-
const passwordUserId = ctx.users && user.email ? await ctx.users.deleteByEmail(normalizeEmail(user.email)) : null;
|
|
34606
|
+
await users.deleteById(user.id);
|
|
34399
34607
|
clearSession(c6);
|
|
34400
|
-
await ctx.emit("user.deleted", { user
|
|
34608
|
+
await ctx.emit("user.deleted", { user });
|
|
34401
34609
|
return c6.body(null, 204);
|
|
34402
34610
|
});
|
|
34403
|
-
|
|
34404
|
-
const
|
|
34405
|
-
|
|
34406
|
-
|
|
34407
|
-
|
|
34408
|
-
|
|
34409
|
-
|
|
34410
|
-
|
|
34411
|
-
|
|
34412
|
-
|
|
34413
|
-
|
|
34414
|
-
|
|
34415
|
-
|
|
34416
|
-
|
|
34417
|
-
|
|
34418
|
-
|
|
34419
|
-
|
|
34420
|
-
|
|
34421
|
-
|
|
34422
|
-
|
|
34423
|
-
|
|
34424
|
-
|
|
34425
|
-
|
|
34426
|
-
|
|
34427
|
-
|
|
34428
|
-
|
|
34429
|
-
|
|
34430
|
-
|
|
34431
|
-
|
|
34432
|
-
|
|
34433
|
-
|
|
34434
|
-
|
|
34435
|
-
|
|
34436
|
-
|
|
34437
|
-
|
|
34438
|
-
|
|
34439
|
-
|
|
34440
|
-
|
|
34441
|
-
|
|
34442
|
-
|
|
34443
|
-
|
|
34444
|
-
|
|
34445
|
-
});
|
|
34446
|
-
|
|
34611
|
+
const admin = async (c6) => {
|
|
34612
|
+
const user = await currentSession(c6);
|
|
34613
|
+
if (!user) return { answer: c6.json({ error: "no_session" }, 401) };
|
|
34614
|
+
const email = user.email ? normalizeEmail(user.email) : void 0;
|
|
34615
|
+
if (!email || !isAdmin(user)) {
|
|
34616
|
+
const message2 = `Only users with the role "${ADMIN_ROLE}" can manage claims and settings.`;
|
|
34617
|
+
return { answer: c6.json({ error: "forbidden", message: message2 }, 403) };
|
|
34618
|
+
}
|
|
34619
|
+
return { email };
|
|
34620
|
+
};
|
|
34621
|
+
const body = async (c6) => {
|
|
34622
|
+
const data = await c6.req.json().catch(() => null);
|
|
34623
|
+
return data && typeof data === "object" ? data : {};
|
|
34624
|
+
};
|
|
34625
|
+
const invalidEmail = (c6) => c6.json({ error: "invalid_email", message: "This is not a valid e-mail address." }, 400);
|
|
34626
|
+
const noUser = (c6) => c6.json({ error: "user_not_found", message: "No user has this e-mail address." }, 404);
|
|
34627
|
+
app.post("/auth/admin/claims/get", async (c6) => {
|
|
34628
|
+
const caller = await admin(c6);
|
|
34629
|
+
if ("answer" in caller) return caller.answer;
|
|
34630
|
+
const data = await body(c6);
|
|
34631
|
+
if (!isValidEmail(data.email)) return invalidEmail(c6);
|
|
34632
|
+
const user = await users.findByEmail(normalizeEmail(data.email));
|
|
34633
|
+
if (!user) return noUser(c6);
|
|
34634
|
+
return c6.json({ email: user.emailNormalized, userId: user.id, claims: claimsFromJson(user.claims) });
|
|
34635
|
+
});
|
|
34636
|
+
app.post("/auth/admin/claims/set", async (c6) => {
|
|
34637
|
+
const caller = await admin(c6);
|
|
34638
|
+
if ("answer" in caller) return caller.answer;
|
|
34639
|
+
const data = await body(c6);
|
|
34640
|
+
if (!isValidEmail(data.email)) return invalidEmail(c6);
|
|
34641
|
+
const claims = parseClaims(data.claims);
|
|
34642
|
+
if (!claims) {
|
|
34643
|
+
return c6.json(
|
|
34644
|
+
{
|
|
34645
|
+
error: "invalid_claims",
|
|
34646
|
+
message: 'claims must be a JSON object of at most 2048 characters whose keys are names (letters, digits and "_", starting with a letter). "roles" must be a list of names of lower-case letters, digits, "-" and "_".'
|
|
34647
|
+
},
|
|
34648
|
+
400
|
|
34649
|
+
);
|
|
34650
|
+
}
|
|
34651
|
+
const user = await users.findByEmail(normalizeEmail(data.email));
|
|
34652
|
+
if (!user) return noUser(c6);
|
|
34653
|
+
await users.updateUser(user.id, { claims: Object.keys(claims).length ? JSON.stringify(claims) : null });
|
|
34654
|
+
const email = user.emailNormalized;
|
|
34655
|
+
await ctx.emit("user.claims_changed", { userId: user.id, email, claims, by: caller.email });
|
|
34656
|
+
return c6.json({ email, userId: user.id, claims });
|
|
34657
|
+
});
|
|
34658
|
+
const settingsAnswer = (methods) => ({
|
|
34659
|
+
methods: Object.fromEntries(
|
|
34660
|
+
SIGN_IN_METHODS.map((method) => [method, { available: available(method), enabled: isOn(method, available, methods) }])
|
|
34661
|
+
)
|
|
34662
|
+
});
|
|
34663
|
+
app.post("/auth/admin/settings/get", async (c6) => {
|
|
34664
|
+
const caller = await admin(c6);
|
|
34665
|
+
if ("answer" in caller) return caller.answer;
|
|
34666
|
+
return c6.json(settingsAnswer(await settings.methods()));
|
|
34667
|
+
});
|
|
34668
|
+
app.post("/auth/admin/settings/set", async (c6) => {
|
|
34669
|
+
const caller = await admin(c6);
|
|
34670
|
+
if ("answer" in caller) return caller.answer;
|
|
34671
|
+
const data = await body(c6);
|
|
34672
|
+
const invalid2 = (message2) => c6.json({ error: "invalid_settings", message: message2 }, 400);
|
|
34673
|
+
const changes = parseMethodSettings(data.methods);
|
|
34674
|
+
if (!changes) return invalid2(`methods must be an object with true or false for ${SIGN_IN_METHODS.join(" and/or ")}.`);
|
|
34675
|
+
const methods = await settings.changeMethods((current) => {
|
|
34676
|
+
const next = { ...current, ...changes };
|
|
34677
|
+
return anyOn(available, next) ? next : null;
|
|
34678
|
+
}, caller.email);
|
|
34679
|
+
if (!methods) return invalid2("At least one sign-in method must stay on.");
|
|
34680
|
+
return c6.json(settingsAnswer(methods));
|
|
34681
|
+
});
|
|
34447
34682
|
return app;
|
|
34448
34683
|
}
|
|
34449
34684
|
|