@maudecode/kanvil 0.1.0 → 0.2.0
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/apps/server/dist/{auth-BscXb1NQ.mjs → auth-I1XEr0d4.mjs} +272 -7
- package/apps/server/dist/{config-Qxr_7oqZ.mjs → config-CbNqtJEj.mjs} +8 -2
- package/apps/server/dist/index.mjs +22 -186
- package/apps/server/dist/migrate.mjs +1 -1
- package/apps/server/dist/recover.mjs +21 -6
- package/apps/server/drizzle/0013_authentication_policy.sql +2 -0
- package/apps/server/drizzle/meta/0013_snapshot.json +4240 -0
- package/apps/server/drizzle/meta/_journal.json +7 -0
- package/apps/web/dist/assets/{CompositeItem-D9KvUNiq.js → CompositeItem-BWPJ5Mol.js} +1 -1
- package/apps/web/dist/assets/admin-settings-DZqkC6ju.js +1 -0
- package/apps/web/dist/assets/{date-picker-lhXxKIlN.js → date-picker-6H_iwP6t.js} +1 -1
- package/apps/web/dist/assets/{ellipsis-CN74O_EP.js → ellipsis-BT172Uvu.js} +1 -1
- package/apps/web/dist/assets/{forms-456UbfIC.js → forms-B4Vg3CPz.js} +23 -23
- package/apps/web/dist/assets/image-edit-trigger-DGmVtWLC.js +1 -0
- package/apps/web/dist/assets/index-C_a21taR.js +50 -0
- package/apps/web/dist/assets/index-DM-D_3Sv.css +2 -0
- package/apps/web/dist/assets/kbd-tTQGZxaK.js +1 -0
- package/apps/web/dist/assets/link-DoIP9KJ6.js +2 -0
- package/apps/web/dist/assets/{list-order-1UoOT5nR.js → list-order-Bzsrihyr.js} +1 -1
- package/apps/web/dist/assets/member-directory-BiCE8DRq.js +1 -0
- package/apps/web/dist/assets/popover-BhzZ0U1d.js +1 -0
- package/apps/web/dist/assets/project-label-provider-9L7Uts3_.js +1 -0
- package/apps/web/dist/assets/project-settings-Q7Q9Reec.js +1 -0
- package/apps/web/dist/assets/{rotate-ccw-BLeWpuu1.js → rotate-ccw-sjyLwLLy.js} +1 -1
- package/apps/web/dist/assets/settings-B6qD6q1J.js +1 -0
- package/apps/web/dist/assets/settings-layout-B-AwUIMu.js +1 -0
- package/apps/web/dist/assets/switch-D0nFHj6l.js +1 -0
- package/apps/web/dist/assets/{table-BQbZG4BK.js → table-CYZiwo9g.js} +1 -1
- package/apps/web/dist/assets/ticket-detail-B-3vbHSB.js +2 -0
- package/apps/web/dist/assets/ticket-editor-C3d8TZIT.js +1 -0
- package/apps/web/dist/assets/ticket-list-LtGGOnwN.js +5 -0
- package/apps/web/dist/assets/ticket-tags-2rdmkcJV.js +1 -0
- package/apps/web/dist/assets/{upload-5Wg5ou_Z.js → upload-DXPRzAaa.js} +1 -1
- package/apps/web/dist/assets/useNavigate-DFohfUD3.js +1 -0
- package/apps/web/dist/assets/workspace-settings-D3tCm1vH.js +1 -0
- package/apps/web/dist/index.html +8 -4
- package/docs/api.md +4 -0
- package/docs/mobile.md +0 -2
- package/docs/releasing.md +3 -1
- package/docs/self-hosting.md +9 -1
- package/package.json +2 -1
- package/apps/web/dist/assets/admin-settings-B0u2-aAK.js +0 -1
- package/apps/web/dist/assets/camera-Cv5wKkKn.js +0 -1
- package/apps/web/dist/assets/index-CVzq9BMC.css +0 -2
- package/apps/web/dist/assets/index-Dt8wYIVA.js +0 -51
- package/apps/web/dist/assets/instance-BCNvOvYa.js +0 -1
- package/apps/web/dist/assets/member-directory-C81ugis6.js +0 -1
- package/apps/web/dist/assets/popover-BWk65ByY.js +0 -1
- package/apps/web/dist/assets/project-settings-DnpVfVf5.js +0 -1
- package/apps/web/dist/assets/settings-DUXuOgWn.js +0 -1
- package/apps/web/dist/assets/settings-layout-xR4hrfte.js +0 -1
- package/apps/web/dist/assets/ticket-detail-B9M_eaVu.js +0 -2
- package/apps/web/dist/assets/ticket-editor-Chg1lDuU.js +0 -1
- package/apps/web/dist/assets/ticket-list-C8500tXj.js +0 -5
- package/apps/web/dist/assets/ticket-tags-cbwQ93Gt.js +0 -1
- package/apps/web/dist/assets/workspace-settings-Dte44Kkx.js +0 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as instanceUpdateSchema, c as s3SettingsSchema, d as createDatabase, f as account, g as user, h as session, i as brokerSettingsSchema, n as adminInstanceSchema, o as photoInputSchema, p as apikey, s as photoResultSchema, t as readConfig, u as searchSettingsSchema } from "./config-CbNqtJEj.mjs";
|
|
2
2
|
import { t as migrateDatabase } from "./migrate-C9f4ozB4.mjs";
|
|
3
|
-
import { A as
|
|
3
|
+
import { A as pushDevices, B as memberSchema, D as profilePhotos, E as notifications, F as workspaces, G as pushDeviceInput, H as notificationPreferencesSchema, I as ApiError, J as matchesOIDCBinding, K as roles, L as errorSchema, M as subscriptions, N as tickets, O as projects, P as workspaceMembers, R as hasAdminRole, S as events, T as notificationPreferences, U as okSchema, V as notificationPreferencesInput, W as operations, _ as attachmentContent, a as fail, b as deliveryCheckpoints, c as requireRow, d as createTicketIndex, f as createBlobStorage, g as createPollingJob, h as createObjectStore, i as createDomain, j as storageBackends, k as pushDeliveries, l as projectRoleSQL, m as imageResponse, n as createInstance, o as json, p as decodeImage, q as scopes, r as assertVersion, s as one, t as createAuth, u as createTicketSearch, v as attachments, w as members, x as eventClock, y as blobObjects, z as isOperation } from "./auth-I1XEr0d4.mjs";
|
|
4
4
|
import { serve } from "@hono/node-server";
|
|
5
5
|
import pg from "pg";
|
|
6
6
|
import { and, asc, count, desc, eq, gt, ilike, inArray, isNull, lte, or, sql } from "drizzle-orm";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import {
|
|
9
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
10
10
|
import { isDeepStrictEqual } from "node:util";
|
|
11
11
|
import { EventEmitter } from "node:events";
|
|
12
12
|
import { connect } from "amqplib";
|
|
@@ -88,7 +88,7 @@ function authPath(request) {
|
|
|
88
88
|
return fail(400, "invalid_path", "The authentication path is invalid");
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
-
function createAccountAdministration(database, auth) {
|
|
91
|
+
function createAccountAdministration(database, auth, config) {
|
|
92
92
|
async function revokeAfterPasswordChange(headers, userId) {
|
|
93
93
|
try {
|
|
94
94
|
await auth.api.revokeUserSessions({
|
|
@@ -99,16 +99,23 @@ function createAccountAdministration(database, auth) {
|
|
|
99
99
|
fail(503, "session_revocation_failed", "The password changed, but existing sessions could not be revoked. Revoke this user’s sessions before continuing.");
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
|
-
async function mutate(request, run) {
|
|
102
|
+
async function mutate(request, run, administratorOnly = true) {
|
|
103
103
|
const login = await auth.api.getSession({ headers: request.headers });
|
|
104
104
|
if (!login) fail(401, "unauthorized", "Sign in to continue");
|
|
105
|
-
if (!hasAdminRole(login.user.role) || login.user.banned) fail(403, "forbidden", "Instance administrator access is required");
|
|
105
|
+
if (administratorOnly && !hasAdminRole(login.user.role) || login.user.banned) fail(403, "forbidden", "Instance administrator access is required");
|
|
106
106
|
const lock = await database.pool.connect();
|
|
107
107
|
let locked = false;
|
|
108
108
|
try {
|
|
109
109
|
await lock.query("SELECT pg_advisory_lock(7361083)");
|
|
110
110
|
locked = true;
|
|
111
111
|
const path = authPath(request);
|
|
112
|
+
if (path === "/api/auth/unlink-account") {
|
|
113
|
+
if (!one((await lock.query("SELECT email_password_enabled AS enabled FROM instance_settings WHERE id=1")).rows).enabled) {
|
|
114
|
+
const accountId = z.string().parse(mutationBody.parse(await request.clone().json()).accountId);
|
|
115
|
+
const issuer = (await auth.$context).socialProviders.find((provider) => provider.id === "oidc")?.issuer;
|
|
116
|
+
if (!(await lock.query(`SELECT account_id AS "accountId", id_token AS "idToken" FROM account WHERE user_id=$1 AND provider_id='oidc' AND id<>$2`, [login.user.id, accountId])).rows.some((binding) => matchesOIDCBinding(binding, issuer, config.OIDC_CLIENT_ID))) fail(409, "oidc_binding_required", "Enable password sign-in before unlinking your last account for the configured OIDC provider");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
112
119
|
let passwordUserId;
|
|
113
120
|
if (path.endsWith("/admin/set-user-password")) passwordUserId = mutationBody.parse(await request.clone().json()).userId;
|
|
114
121
|
if ([
|
|
@@ -152,7 +159,8 @@ function createAccountAdministration(database, auth) {
|
|
|
152
159
|
"remove-user",
|
|
153
160
|
"set-user-password"
|
|
154
161
|
].some((name) => path === `/api/auth/admin/${name}`);
|
|
155
|
-
|
|
162
|
+
const unlink = path === "/api/auth/unlink-account";
|
|
163
|
+
return request.method === "POST" && (guarded || unlink) ? mutate(request, () => auth.handler(request), !unlink) : auth.handler(request);
|
|
156
164
|
},
|
|
157
165
|
register(app) {
|
|
158
166
|
app.openAPIRegistry.registerPath({
|
|
@@ -241,181 +249,6 @@ function createAccountAdministration(database, auth) {
|
|
|
241
249
|
};
|
|
242
250
|
}
|
|
243
251
|
//#endregion
|
|
244
|
-
//#region src/instance.ts
|
|
245
|
-
function storageIdentity(settings) {
|
|
246
|
-
return createHash("sha256").update(JSON.stringify([
|
|
247
|
-
settings.endpoint,
|
|
248
|
-
settings.region,
|
|
249
|
-
settings.bucket,
|
|
250
|
-
settings.prefix,
|
|
251
|
-
settings.forcePathStyle
|
|
252
|
-
])).digest("hex");
|
|
253
|
-
}
|
|
254
|
-
function createInstance(database, config) {
|
|
255
|
-
const key = createHash("sha256").update("kanvil:instance-config:v1:").update(config.BETTER_AUTH_SECRET).digest();
|
|
256
|
-
function seal(value) {
|
|
257
|
-
const iv = randomBytes(12), cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
258
|
-
const bytes = Buffer.concat([cipher.update(JSON.stringify(value)), cipher.final()]);
|
|
259
|
-
return Buffer.concat([
|
|
260
|
-
iv,
|
|
261
|
-
cipher.getAuthTag(),
|
|
262
|
-
bytes
|
|
263
|
-
]).toString("base64");
|
|
264
|
-
}
|
|
265
|
-
function unseal(value) {
|
|
266
|
-
try {
|
|
267
|
-
const bytes = Buffer.from(value, "base64"), decipher = createDecipheriv("aes-256-gcm", key, bytes.subarray(0, 12));
|
|
268
|
-
decipher.setAuthTag(bytes.subarray(12, 28));
|
|
269
|
-
return JSON.parse(Buffer.concat([decipher.update(bytes.subarray(28)), decipher.final()]).toString());
|
|
270
|
-
} catch {
|
|
271
|
-
return fail(503, "configuration_key_changed", "Saved integration credentials cannot be decrypted. Restore the instance secret from your backup.");
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
async function read(db = database.db) {
|
|
275
|
-
await db.insert(instanceSettings).values({ id: 1 }).onConflictDoNothing();
|
|
276
|
-
return one(await db.select().from(instanceSettings).where(eq(instanceSettings.id, 1)));
|
|
277
|
-
}
|
|
278
|
-
async function backend(id, db = database.db) {
|
|
279
|
-
const row = one(await db.select().from(storageBackends).where(eq(storageBackends.id, id)));
|
|
280
|
-
const settings = s3SettingsSchema.parse(unseal(row.config));
|
|
281
|
-
return {
|
|
282
|
-
...row,
|
|
283
|
-
settings,
|
|
284
|
-
spec: {
|
|
285
|
-
provider: "s3",
|
|
286
|
-
config: settings
|
|
287
|
-
}
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
async function resolveS3(settings, db = database.db) {
|
|
291
|
-
settings = s3SettingsSchema.strip().parse(settings);
|
|
292
|
-
const identity = storageIdentity(settings);
|
|
293
|
-
const previous = (await db.select().from(storageBackends).where(eq(storageBackends.identity, identity)))[0];
|
|
294
|
-
const old = previous ? s3SettingsSchema.parse(unseal(previous.config)) : void 0;
|
|
295
|
-
const { secretAccessKey: suppliedSecret, ...publicSettings } = settings, secret = suppliedSecret || (old?.accessKeyId === settings.accessKeyId ? old.secretAccessKey : void 0);
|
|
296
|
-
if (settings.accessKeyId && !secret) fail(400, "missing_secret", "Provide the S3 secret access key");
|
|
297
|
-
return {
|
|
298
|
-
...publicSettings,
|
|
299
|
-
...secret ? { secretAccessKey: secret } : {}
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
async function resolveSearch(settings, db = database.db) {
|
|
303
|
-
settings = searchSettingsSchema.strip().parse(settings);
|
|
304
|
-
const row = await read(db), old = row.searchConfig ? searchSettingsSchema.parse(unseal(row.searchConfig)) : null, same = old?.url === settings.url && old.index === settings.index;
|
|
305
|
-
const { apiKey: suppliedKey, password: suppliedPassword, auth: requestedAuth, ...publicSettings } = settings, auth = requestedAuth ?? (suppliedKey ? "apiKey" : suppliedPassword && settings.username ? "basic" : same && old ? searchAuthentication(old) : searchAuthentication(settings)), apiKey = suppliedKey || (same ? old?.apiKey : void 0), password = suppliedPassword || (same && old?.username === settings.username ? old?.password : void 0);
|
|
306
|
-
if (auth === "apiKey" && !apiKey) fail(400, "missing_secret", "Provide the search API key");
|
|
307
|
-
if (auth === "basic" && (!settings.username || !password)) fail(400, "missing_secret", "Provide the search username and password");
|
|
308
|
-
return {
|
|
309
|
-
...publicSettings,
|
|
310
|
-
auth,
|
|
311
|
-
...auth === "apiKey" && apiKey ? { apiKey } : {},
|
|
312
|
-
...auth === "basic" && password ? { password } : {}
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
async function saveBackend(settings, db = database.db) {
|
|
316
|
-
const complete = await resolveS3(settings, db), identity = storageIdentity(settings);
|
|
317
|
-
return one(await db.insert(storageBackends).values({
|
|
318
|
-
identity,
|
|
319
|
-
config: seal(complete)
|
|
320
|
-
}).onConflictDoUpdate({
|
|
321
|
-
target: storageBackends.identity,
|
|
322
|
-
set: { config: seal(complete) }
|
|
323
|
-
}).returning());
|
|
324
|
-
}
|
|
325
|
-
async function storage() {
|
|
326
|
-
if (config.KANVIL_STORAGE_PROVIDER === "local") return null;
|
|
327
|
-
if (config.KANVIL_STORAGE_PROVIDER === "s3") return saveBackend(s3SettingsSchema.parse({
|
|
328
|
-
endpoint: config.S3_ENDPOINT ?? "",
|
|
329
|
-
region: config.S3_REGION,
|
|
330
|
-
bucket: config.S3_BUCKET,
|
|
331
|
-
prefix: config.S3_PREFIX,
|
|
332
|
-
forcePathStyle: config.S3_FORCE_PATH_STYLE,
|
|
333
|
-
accessKeyId: config.S3_ACCESS_KEY_ID ?? "",
|
|
334
|
-
secretAccessKey: config.S3_SECRET_ACCESS_KEY
|
|
335
|
-
}));
|
|
336
|
-
const row = await read();
|
|
337
|
-
return row.storageBackendId ? backend(row.storageBackendId) : null;
|
|
338
|
-
}
|
|
339
|
-
async function search() {
|
|
340
|
-
const row = await read();
|
|
341
|
-
if (config.KANVIL_SEARCH_PROVIDER === "postgres") return null;
|
|
342
|
-
if (config.KANVIL_SEARCH_PROVIDER === "elasticsearch") {
|
|
343
|
-
const settings = searchSettingsSchema.parse({
|
|
344
|
-
url: config.ELASTICSEARCH_URL,
|
|
345
|
-
index: config.ELASTICSEARCH_INDEX ?? `kanvil-${row.identity}`,
|
|
346
|
-
auth: config.ELASTICSEARCH_AUTH,
|
|
347
|
-
apiKey: config.ELASTICSEARCH_API_KEY,
|
|
348
|
-
username: config.ELASTICSEARCH_USERNAME,
|
|
349
|
-
password: config.ELASTICSEARCH_PASSWORD
|
|
350
|
-
});
|
|
351
|
-
return {
|
|
352
|
-
...settings,
|
|
353
|
-
auth: searchAuthentication(settings)
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
const settings = row.searchConfig ? searchSettingsSchema.parse(unseal(row.searchConfig)) : null;
|
|
357
|
-
return settings ? {
|
|
358
|
-
...settings,
|
|
359
|
-
auth: searchAuthentication(settings)
|
|
360
|
-
} : null;
|
|
361
|
-
}
|
|
362
|
-
async function broker() {
|
|
363
|
-
if (config.KANVIL_BROKER_PROVIDER === "postgres") return null;
|
|
364
|
-
if (config.KANVIL_BROKER_PROVIDER === "rabbitmq") return brokerSettingsSchema.parse({
|
|
365
|
-
url: config.RABBITMQ_URL,
|
|
366
|
-
exchange: config.RABBITMQ_EXCHANGE
|
|
367
|
-
});
|
|
368
|
-
const row = await read();
|
|
369
|
-
return row.brokerConfig ? brokerSettingsSchema.parse(unseal(row.brokerConfig)) : null;
|
|
370
|
-
}
|
|
371
|
-
async function resolveBroker(settings, db = database.db) {
|
|
372
|
-
settings = brokerSettingsSchema.strip().parse(settings);
|
|
373
|
-
const row = await read(db), old = row.brokerConfig ? brokerSettingsSchema.parse(unseal(row.brokerConfig)) : null;
|
|
374
|
-
const url = settings.url || old?.url;
|
|
375
|
-
if (!url) return fail(400, "missing_secret", "Provide the RabbitMQ connection URL");
|
|
376
|
-
return {
|
|
377
|
-
...settings,
|
|
378
|
-
url
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
async function update(value) {
|
|
382
|
-
const input = instanceUpdateSchema.parse(value);
|
|
383
|
-
return database.db.transaction(async (tx) => {
|
|
384
|
-
await tx.execute(sql`SELECT pg_advisory_xact_lock(7361083)`);
|
|
385
|
-
const current = await read(tx);
|
|
386
|
-
assertVersion(current, input.expectedVersion, "instance settings", "saving");
|
|
387
|
-
let storageId = current.storageBackendId, searchConfig = current.searchConfig, brokerConfig = current.brokerConfig;
|
|
388
|
-
if (!config.KANVIL_STORAGE_PROVIDER) storageId = input.storage.provider === "s3" ? (await saveBackend(input.storage, tx)).id : null;
|
|
389
|
-
if (!config.KANVIL_SEARCH_PROVIDER) {
|
|
390
|
-
if (input.search.provider === "postgres") searchConfig = null;
|
|
391
|
-
else searchConfig = seal(await resolveSearch(input.search, tx));
|
|
392
|
-
}
|
|
393
|
-
if (!config.KANVIL_BROKER_PROVIDER && input.broker) brokerConfig = input.broker.provider === "postgres" ? null : seal(await resolveBroker(input.broker, tx));
|
|
394
|
-
await tx.update(instanceSettings).set({
|
|
395
|
-
name: input.name,
|
|
396
|
-
description: input.description,
|
|
397
|
-
storageBackendId: storageId,
|
|
398
|
-
searchConfig,
|
|
399
|
-
brokerConfig,
|
|
400
|
-
version: current.version + 1
|
|
401
|
-
}).where(eq(instanceSettings.id, 1));
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
return {
|
|
405
|
-
read,
|
|
406
|
-
storage,
|
|
407
|
-
search,
|
|
408
|
-
backend,
|
|
409
|
-
saveBackend,
|
|
410
|
-
resolveS3,
|
|
411
|
-
resolveSearch,
|
|
412
|
-
broker,
|
|
413
|
-
resolveBroker,
|
|
414
|
-
update,
|
|
415
|
-
config
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
//#endregion
|
|
419
252
|
//#region src/access.ts
|
|
420
253
|
const credentialPermissions = z.object({ kanvil: z.array(z.enum(scopes)) });
|
|
421
254
|
const credentialMetadata = z.object({ projectIds: z.array(z.uuid()).max(1e3) });
|
|
@@ -815,7 +648,7 @@ async function createEventDelivery(config, instance, database, factory = createW
|
|
|
815
648
|
}
|
|
816
649
|
//#endregion
|
|
817
650
|
//#region src/instance-routes.ts
|
|
818
|
-
function registerInstanceRoutes(app, database, instance, blobs, search, delivery) {
|
|
651
|
+
function registerInstanceRoutes(app, database, instance, blobs, search, delivery, auth) {
|
|
819
652
|
app.openAPIRegistry.registerComponent("securitySchemes", "sessionCookie", {
|
|
820
653
|
type: "apiKey",
|
|
821
654
|
in: "cookie",
|
|
@@ -922,6 +755,7 @@ function registerInstanceRoutes(app, database, instance, blobs, search, delivery
|
|
|
922
755
|
return Response.json({
|
|
923
756
|
id: row.identity,
|
|
924
757
|
version: row.version,
|
|
758
|
+
authentication: await instance.authentication(),
|
|
925
759
|
name: row.name,
|
|
926
760
|
description: row.description,
|
|
927
761
|
storage: {
|
|
@@ -995,7 +829,8 @@ function registerInstanceRoutes(app, database, instance, blobs, search, delivery
|
|
|
995
829
|
const candidate = await instance.resolveBroker(input.broker);
|
|
996
830
|
if (!isDeepStrictEqual(candidate, await instance.broker())) await delivery.test(candidate);
|
|
997
831
|
}
|
|
998
|
-
await
|
|
832
|
+
const issuer = input.authentication && !input.authentication.emailPasswordEnabled ? (await auth.$context).socialProviders.find((provider) => provider.id === "oidc")?.issuer : void 0;
|
|
833
|
+
await instance.update(input, c.get("actor").id, issuer);
|
|
999
834
|
return Response.json({ ok: true });
|
|
1000
835
|
});
|
|
1001
836
|
app.post("/api/v1/admin/storage/test", async (c) => {
|
|
@@ -1269,7 +1104,7 @@ async function createApplication(database, config, overrides = {}) {
|
|
|
1269
1104
|
search.sync().catch(() => {});
|
|
1270
1105
|
});
|
|
1271
1106
|
const push = adapters.push(database, config);
|
|
1272
|
-
const accounts = createAccountAdministration(integrationDatabase, auth);
|
|
1107
|
+
const accounts = createAccountAdministration(integrationDatabase, auth, config);
|
|
1273
1108
|
const app = new OpenAPIHono({ defaultHook: (result) => {
|
|
1274
1109
|
if (!result.success) return Response.json({ error: {
|
|
1275
1110
|
code: "validation_error",
|
|
@@ -1338,6 +1173,7 @@ async function createApplication(database, config, overrides = {}) {
|
|
|
1338
1173
|
description: row.description
|
|
1339
1174
|
})),
|
|
1340
1175
|
version: "0.1.0",
|
|
1176
|
+
authentication: await instance.authentication(),
|
|
1341
1177
|
setupRequired: await setupRequired(),
|
|
1342
1178
|
push: config.KANVIL_PUSH_ENABLED,
|
|
1343
1179
|
oidc: config.OIDC_DISCOVERY_URL ? {
|
|
@@ -1390,7 +1226,7 @@ async function createApplication(database, config, overrides = {}) {
|
|
|
1390
1226
|
requireInstanceAdmin(c.get("actor"));
|
|
1391
1227
|
await next();
|
|
1392
1228
|
});
|
|
1393
|
-
registerInstanceRoutes(app, database, instance, blobs, search, realtime);
|
|
1229
|
+
registerInstanceRoutes(app, database, instance, blobs, search, realtime, auth);
|
|
1394
1230
|
accounts.register(app);
|
|
1395
1231
|
app.openAPIRegistry.registerPath({
|
|
1396
1232
|
method: "get",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { d as createDatabase, t as readConfig } from "./config-CbNqtJEj.mjs";
|
|
2
2
|
import { t as migrateDatabase } from "./migrate-C9f4ozB4.mjs";
|
|
3
3
|
//#region src/cli/migrate.ts
|
|
4
4
|
const database = createDatabase(readConfig().DATABASE_URL);
|
|
@@ -1,19 +1,27 @@
|
|
|
1
|
-
import { d as account,
|
|
2
|
-
import {
|
|
3
|
-
import { and, eq } from "drizzle-orm";
|
|
1
|
+
import { d as createDatabase, f as account, g as user, h as session, t as readConfig } from "./config-CbNqtJEj.mjs";
|
|
2
|
+
import { C as instanceSettings, R as hasAdminRole, a as fail, t as createAuth } from "./auth-I1XEr0d4.mjs";
|
|
3
|
+
import { and, eq, sql } from "drizzle-orm";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
6
6
|
import passwordPrompt from "@inquirer/password";
|
|
7
7
|
import { readFileSync } from "node:fs";
|
|
8
8
|
//#region src/recover-account.ts
|
|
9
9
|
const localPasswordSchema = z.string().min(12).max(128);
|
|
10
|
-
async function recoverLocalPassword(database, config, email, password) {
|
|
10
|
+
async function recoverLocalPassword(database, config, email, password, enablePasswordSignIn = false) {
|
|
11
11
|
const normalizedEmail = z.email().parse(email.trim().toLowerCase());
|
|
12
12
|
localPasswordSchema.parse(password);
|
|
13
13
|
const hash = await (await createAuth(database, config).$context).password.hash(password);
|
|
14
14
|
return database.db.transaction(async (tx) => {
|
|
15
|
+
await tx.execute(sql`SELECT pg_advisory_xact_lock(7361083)`);
|
|
15
16
|
const identity = (await tx.select().from(user).where(eq(user.email, normalizedEmail)).for("update"))[0];
|
|
16
17
|
if (!identity) fail(404, "not_found", "No account has this email address");
|
|
18
|
+
if (enablePasswordSignIn) {
|
|
19
|
+
if (!hasAdminRole(identity.role) || identity.banned) fail(403, "forbidden", "Recover an active administrator to enable password sign-in");
|
|
20
|
+
await tx.update(instanceSettings).set({
|
|
21
|
+
emailPasswordEnabled: true,
|
|
22
|
+
version: sql`${instanceSettings.version} + 1`
|
|
23
|
+
}).where(eq(instanceSettings.id, 1));
|
|
24
|
+
}
|
|
17
25
|
const where = and(eq(account.userId, identity.id), eq(account.providerId, "credential"));
|
|
18
26
|
if ((await tx.select().from(account).where(where))[0]) await tx.update(account).set({
|
|
19
27
|
password: hash,
|
|
@@ -33,7 +41,13 @@ async function recoverLocalPassword(database, config, email, password) {
|
|
|
33
41
|
}
|
|
34
42
|
//#endregion
|
|
35
43
|
//#region src/cli/recover.ts
|
|
36
|
-
const { values } = parseArgs({ options: {
|
|
44
|
+
const { values } = parseArgs({ options: {
|
|
45
|
+
email: { type: "string" },
|
|
46
|
+
"enable-password-sign-in": {
|
|
47
|
+
type: "boolean",
|
|
48
|
+
default: false
|
|
49
|
+
}
|
|
50
|
+
} });
|
|
37
51
|
if (!values.email) throw new Error("Pass --email <account-email>. The new password is read privately from the terminal or stdin.");
|
|
38
52
|
const password = process.stdin.isTTY ? await passwordPrompt({
|
|
39
53
|
message: "New local password",
|
|
@@ -43,7 +57,8 @@ const password = process.stdin.isTTY ? await passwordPrompt({
|
|
|
43
57
|
const config = readConfig();
|
|
44
58
|
const database = createDatabase(config.DATABASE_URL);
|
|
45
59
|
try {
|
|
46
|
-
await recoverLocalPassword(database, config, values.email, password);
|
|
60
|
+
await recoverLocalPassword(database, config, values.email, password, values["enable-password-sign-in"]);
|
|
61
|
+
if (values["enable-password-sign-in"]) console.info("Instance password sign-in enabled.");
|
|
47
62
|
console.info("Local password replaced and account sessions revoked. API credentials and linked OIDC accounts are retained.");
|
|
48
63
|
} finally {
|
|
49
64
|
await database.pool.end();
|