@open-mercato/core 0.6.6-develop.5612.1.d382eb2f33 → 0.6.6-develop.5617.1.62538c48ca
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/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/api_key/index.js +2 -0
- package/dist/generated/entities/api_key/index.js.map +2 -2
- package/dist/generated/entities/attachment_partition/index.js +4 -0
- package/dist/generated/entities/attachment_partition/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +3 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/api_keys/data/entities.js +9 -2
- package/dist/modules/api_keys/data/entities.js.map +2 -2
- package/dist/modules/api_keys/migrations/Migration20260523234901.js +17 -0
- package/dist/modules/api_keys/migrations/Migration20260523234901.js.map +7 -0
- package/dist/modules/api_keys/services/apiKeyService.js +26 -0
- package/dist/modules/api_keys/services/apiKeyService.js.map +2 -2
- package/dist/modules/attachments/api/partitions/route.js +54 -12
- package/dist/modules/attachments/api/partitions/route.js.map +2 -2
- package/dist/modules/attachments/data/entities.js +7 -0
- package/dist/modules/attachments/data/entities.js.map +2 -2
- package/dist/modules/attachments/migrations/Migration20260524000000.js +27 -0
- package/dist/modules/attachments/migrations/Migration20260524000000.js.map +7 -0
- package/dist/modules/auth/api/roles/route.js +6 -35
- package/dist/modules/auth/api/roles/route.js.map +2 -2
- package/dist/modules/auth/cli.js +26 -4
- package/dist/modules/auth/cli.js.map +2 -2
- package/dist/modules/auth/commands/roles.js +34 -11
- package/dist/modules/auth/commands/roles.js.map +2 -2
- package/dist/modules/auth/lib/roleTenantGuard.js +41 -13
- package/dist/modules/auth/lib/roleTenantGuard.js.map +3 -3
- package/dist/modules/auth/lib/setup-app.js +88 -26
- package/dist/modules/auth/lib/setup-app.js.map +2 -2
- package/dist/modules/currencies/api/fetch-configs/route.js +4 -2
- package/dist/modules/currencies/api/fetch-configs/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/route.js +5 -1
- package/dist/modules/integrations/api/[id]/route.js.map +2 -2
- package/dist/modules/integrations/api/route.js +5 -1
- package/dist/modules/integrations/api/route.js.map +2 -2
- package/dist/modules/integrations/lib/credentials-service.js +6 -4
- package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
- package/generated/entities/api_key/index.ts +1 -0
- package/generated/entities/attachment_partition/index.ts +2 -0
- package/generated/entity-fields-registry.ts +3 -0
- package/package.json +7 -7
- package/src/modules/api_keys/data/entities.ts +15 -1
- package/src/modules/api_keys/migrations/.snapshot-open-mercato.json +18 -0
- package/src/modules/api_keys/migrations/Migration20260523234901.ts +17 -0
- package/src/modules/api_keys/services/apiKeyService.ts +63 -0
- package/src/modules/attachments/api/partitions/route.ts +58 -11
- package/src/modules/attachments/data/entities.ts +7 -0
- package/src/modules/attachments/migrations/.snapshot-open-mercato.json +28 -0
- package/src/modules/attachments/migrations/Migration20260524000000.ts +25 -0
- package/src/modules/auth/api/roles/route.ts +7 -38
- package/src/modules/auth/cli.ts +27 -4
- package/src/modules/auth/commands/roles.ts +39 -11
- package/src/modules/auth/lib/roleTenantGuard.ts +56 -17
- package/src/modules/auth/lib/setup-app.ts +160 -32
- package/src/modules/currencies/api/fetch-configs/route.ts +4 -2
- package/src/modules/integrations/api/[id]/route.ts +5 -2
- package/src/modules/integrations/api/route.ts +5 -2
- package/src/modules/integrations/lib/credentials-service.ts +15 -5
|
@@ -8,7 +8,7 @@ var __decorateClass = (decorators, target, key, kind) => {
|
|
|
8
8
|
if (kind && result) __defProp(target, key, result);
|
|
9
9
|
return result;
|
|
10
10
|
};
|
|
11
|
-
import { Entity, PrimaryKey, Property, Unique } from "@mikro-orm/decorators/legacy";
|
|
11
|
+
import { Entity, Index, PrimaryKey, Property, Unique } from "@mikro-orm/decorators/legacy";
|
|
12
12
|
let ApiKey = class {
|
|
13
13
|
constructor() {
|
|
14
14
|
this.createdAt = /* @__PURE__ */ new Date();
|
|
@@ -50,6 +50,9 @@ __decorateClass([
|
|
|
50
50
|
__decorateClass([
|
|
51
51
|
Property({ name: "session_secret_encrypted", type: "text", nullable: true })
|
|
52
52
|
], ApiKey.prototype, "sessionSecretEncrypted", 2);
|
|
53
|
+
__decorateClass([
|
|
54
|
+
Property({ name: "opencode_session_id", type: "text", nullable: true })
|
|
55
|
+
], ApiKey.prototype, "opencodeSessionId", 2);
|
|
53
56
|
__decorateClass([
|
|
54
57
|
Property({ name: "last_used_at", type: Date, nullable: true })
|
|
55
58
|
], ApiKey.prototype, "lastUsedAt", 2);
|
|
@@ -67,7 +70,11 @@ __decorateClass([
|
|
|
67
70
|
], ApiKey.prototype, "deletedAt", 2);
|
|
68
71
|
ApiKey = __decorateClass([
|
|
69
72
|
Entity({ tableName: "api_keys" }),
|
|
70
|
-
Unique({ properties: ["keyPrefix"] })
|
|
73
|
+
Unique({ properties: ["keyPrefix"] }),
|
|
74
|
+
Index({
|
|
75
|
+
name: "api_keys_opencode_session_id_uq",
|
|
76
|
+
expression: 'create unique index "api_keys_opencode_session_id_uq" on "api_keys" ("opencode_session_id") where "opencode_session_id" is not null and "deleted_at" is null'
|
|
77
|
+
})
|
|
71
78
|
], ApiKey);
|
|
72
79
|
export {
|
|
73
80
|
ApiKey
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/api_keys/data/entities.ts"],
|
|
4
|
-
"sourcesContent": ["import { Entity, PrimaryKey, Property, Unique } from '@mikro-orm/decorators/legacy'\n\n@Entity({ tableName: 'api_keys' })\n@Unique({ properties: ['keyPrefix'] })\nexport class ApiKey {\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ type: 'text' })\n name!: string\n\n @Property({ name: 'description', type: 'text', nullable: true })\n description?: string | null\n\n @Property({ name: 'tenant_id', type: 'uuid', nullable: true })\n tenantId?: string | null\n\n @Property({ name: 'organization_id', type: 'uuid', nullable: true })\n organizationId?: string | null\n\n @Property({ name: 'key_hash', type: 'text' })\n keyHash!: string\n\n @Property({ name: 'key_prefix', type: 'text' })\n keyPrefix!: string\n\n @Property({ name: 'roles_json', type: 'json', nullable: true })\n rolesJson?: string[] | null\n\n @Property({ name: 'created_by', type: 'uuid', nullable: true })\n createdBy?: string | null\n\n /** Session token for ephemeral session-scoped keys (used by AI chat) */\n @Property({ name: 'session_token', type: 'text', nullable: true })\n sessionToken?: string | null\n\n /** User ID who owns this session (for ephemeral keys) */\n @Property({ name: 'session_user_id', type: 'uuid', nullable: true })\n sessionUserId?: string | null\n\n /** Encrypted API key secret for session keys (recoverable for API calls) */\n @Property({ name: 'session_secret_encrypted', type: 'text', nullable: true })\n sessionSecretEncrypted?: string | null\n\n @Property({ name: 'last_used_at', type: Date, nullable: true })\n lastUsedAt?: Date | null\n\n @Property({ name: 'expires_at', type: Date, nullable: true })\n expiresAt?: Date | null\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date(), nullable: true })\n updatedAt?: Date\n\n @Property({ name: 'deleted_at', type: Date, nullable: true })\n deletedAt?: Date | null\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;AAAA,SAAS,QAAQ,YAAY,UAAU,cAAc;
|
|
4
|
+
"sourcesContent": ["import { Entity, Index, PrimaryKey, Property, Unique } from '@mikro-orm/decorators/legacy'\n\n@Entity({ tableName: 'api_keys' })\n@Unique({ properties: ['keyPrefix'] })\n@Index({\n name: 'api_keys_opencode_session_id_uq',\n expression:\n 'create unique index \"api_keys_opencode_session_id_uq\" on \"api_keys\" (\"opencode_session_id\") where \"opencode_session_id\" is not null and \"deleted_at\" is null',\n})\nexport class ApiKey {\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ type: 'text' })\n name!: string\n\n @Property({ name: 'description', type: 'text', nullable: true })\n description?: string | null\n\n @Property({ name: 'tenant_id', type: 'uuid', nullable: true })\n tenantId?: string | null\n\n @Property({ name: 'organization_id', type: 'uuid', nullable: true })\n organizationId?: string | null\n\n @Property({ name: 'key_hash', type: 'text' })\n keyHash!: string\n\n @Property({ name: 'key_prefix', type: 'text' })\n keyPrefix!: string\n\n @Property({ name: 'roles_json', type: 'json', nullable: true })\n rolesJson?: string[] | null\n\n @Property({ name: 'created_by', type: 'uuid', nullable: true })\n createdBy?: string | null\n\n /** Session token for ephemeral session-scoped keys (used by AI chat) */\n @Property({ name: 'session_token', type: 'text', nullable: true })\n sessionToken?: string | null\n\n /** User ID who owns this session (for ephemeral keys) */\n @Property({ name: 'session_user_id', type: 'uuid', nullable: true })\n sessionUserId?: string | null\n\n /** Encrypted API key secret for session keys (recoverable for API calls) */\n @Property({ name: 'session_secret_encrypted', type: 'text', nullable: true })\n sessionSecretEncrypted?: string | null\n\n /**\n * OpenCode session id bound to this api_key row. Set the first time the\n * chat dispatcher receives a `done` event after minting a session token,\n * then asserted on every subsequent resume to prevent cross-user session\n * continuation (see security fix 2026-05-23).\n */\n @Property({ name: 'opencode_session_id', type: 'text', nullable: true })\n opencodeSessionId?: string | null\n\n @Property({ name: 'last_used_at', type: Date, nullable: true })\n lastUsedAt?: Date | null\n\n @Property({ name: 'expires_at', type: Date, nullable: true })\n expiresAt?: Date | null\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date(), nullable: true })\n updatedAt?: Date\n\n @Property({ name: 'deleted_at', type: Date, nullable: true })\n deletedAt?: Date | null\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;AAAA,SAAS,QAAQ,OAAO,YAAY,UAAU,cAAc;AASrD,IAAM,SAAN,MAAa;AAAA,EAAb;AAwDL,qBAAkB,oBAAI,KAAK;AAAA;AAO7B;AA7DE;AAAA,EADC,WAAW,EAAE,MAAM,QAAQ,YAAY,oBAAoB,CAAC;AAAA,GADlD,OAEX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,OAAO,CAAC;AAAA,GAJf,OAKX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,eAAe,MAAM,QAAQ,UAAU,KAAK,CAAC;AAAA,GAPpD,OAQX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,aAAa,MAAM,QAAQ,UAAU,KAAK,CAAC;AAAA,GAVlD,OAWX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,mBAAmB,MAAM,QAAQ,UAAU,KAAK,CAAC;AAAA,GAbxD,OAcX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,YAAY,MAAM,OAAO,CAAC;AAAA,GAhBjC,OAiBX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,cAAc,MAAM,OAAO,CAAC;AAAA,GAnBnC,OAoBX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,cAAc,MAAM,QAAQ,UAAU,KAAK,CAAC;AAAA,GAtBnD,OAuBX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,cAAc,MAAM,QAAQ,UAAU,KAAK,CAAC;AAAA,GAzBnD,OA0BX;AAIA;AAAA,EADC,SAAS,EAAE,MAAM,iBAAiB,MAAM,QAAQ,UAAU,KAAK,CAAC;AAAA,GA7BtD,OA8BX;AAIA;AAAA,EADC,SAAS,EAAE,MAAM,mBAAmB,MAAM,QAAQ,UAAU,KAAK,CAAC;AAAA,GAjCxD,OAkCX;AAIA;AAAA,EADC,SAAS,EAAE,MAAM,4BAA4B,MAAM,QAAQ,UAAU,KAAK,CAAC;AAAA,GArCjE,OAsCX;AASA;AAAA,EADC,SAAS,EAAE,MAAM,uBAAuB,MAAM,QAAQ,UAAU,KAAK,CAAC;AAAA,GA9C5D,OA+CX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,gBAAgB,MAAM,MAAM,UAAU,KAAK,CAAC;AAAA,GAjDnD,OAkDX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,cAAc,MAAM,MAAM,UAAU,KAAK,CAAC;AAAA,GApDjD,OAqDX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,cAAc,MAAM,MAAM,UAAU,MAAM,oBAAI,KAAK,EAAE,CAAC;AAAA,GAvD7D,OAwDX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,cAAc,MAAM,MAAM,UAAU,MAAM,oBAAI,KAAK,GAAG,UAAU,KAAK,CAAC;AAAA,GA1D7E,OA2DX;AAGA;AAAA,EADC,SAAS,EAAE,MAAM,cAAc,MAAM,MAAM,UAAU,KAAK,CAAC;AAAA,GA7DjD,OA8DX;AA9DW,SAAN;AAAA,EAPN,OAAO,EAAE,WAAW,WAAW,CAAC;AAAA,EAChC,OAAO,EAAE,YAAY,CAAC,WAAW,EAAE,CAAC;AAAA,EACpC,MAAM;AAAA,IACL,MAAM;AAAA,IACN,YACE;AAAA,EACJ,CAAC;AAAA,GACY;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Migration } from "@mikro-orm/migrations";
|
|
2
|
+
class Migration20260523234901 extends Migration {
|
|
3
|
+
async up() {
|
|
4
|
+
this.addSql(`alter table "api_keys" add column "opencode_session_id" text null;`);
|
|
5
|
+
this.addSql(
|
|
6
|
+
`create unique index "api_keys_opencode_session_id_uq" on "api_keys" ("opencode_session_id") where "opencode_session_id" is not null and "deleted_at" is null;`
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
async down() {
|
|
10
|
+
this.addSql(`drop index "api_keys_opencode_session_id_uq";`);
|
|
11
|
+
this.addSql(`alter table "api_keys" drop column "opencode_session_id";`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export {
|
|
15
|
+
Migration20260523234901
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=Migration20260523234901.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/api_keys/migrations/Migration20260523234901.ts"],
|
|
4
|
+
"sourcesContent": ["import { Migration } from '@mikro-orm/migrations';\n\nexport class Migration20260523234901 extends Migration {\n\n override async up(): Promise<void> {\n this.addSql(`alter table \"api_keys\" add column \"opencode_session_id\" text null;`);\n this.addSql(\n `create unique index \"api_keys_opencode_session_id_uq\" on \"api_keys\" (\"opencode_session_id\") where \"opencode_session_id\" is not null and \"deleted_at\" is null;`\n );\n }\n\n override async down(): Promise<void> {\n this.addSql(`drop index \"api_keys_opencode_session_id_uq\";`);\n this.addSql(`alter table \"api_keys\" drop column \"opencode_session_id\";`);\n }\n\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,iBAAiB;AAEnB,MAAM,gCAAgC,UAAU;AAAA,EAErD,MAAe,KAAoB;AACjC,SAAK,OAAO,oEAAoE;AAChF,SAAK;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAe,OAAsB;AACnC,SAAK,OAAO,+CAA+C;AAC3D,SAAK,OAAO,2DAA2D;AAAA,EACzE;AAEF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -4,6 +4,7 @@ import { ApiKey } from "../data/entities.js";
|
|
|
4
4
|
import { createKmsService } from "@open-mercato/shared/lib/encryption/kms";
|
|
5
5
|
import { encryptWithAesGcm, decryptWithAesGcm } from "@open-mercato/shared/lib/encryption/aes";
|
|
6
6
|
import { getSharedApiKeyAuthCache } from "@open-mercato/shared/lib/auth/apiKeyAuthCache";
|
|
7
|
+
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
7
8
|
const BCRYPT_COST = 10;
|
|
8
9
|
async function encryptSessionSecret(secret, tenantId) {
|
|
9
10
|
if (!tenantId) return null;
|
|
@@ -123,6 +124,29 @@ async function findApiKeyBySessionToken(em, sessionToken) {
|
|
|
123
124
|
if (record.expiresAt && record.expiresAt.getTime() < Date.now()) return null;
|
|
124
125
|
return record;
|
|
125
126
|
}
|
|
127
|
+
async function bindOpencodeSessionToApiKey(em, sessionToken, opencodeSessionId) {
|
|
128
|
+
if (!sessionToken) throw new Error("Session token not found or expired");
|
|
129
|
+
if (!opencodeSessionId) throw new Error("OpenCode session id is required");
|
|
130
|
+
const row = await findApiKeyBySessionToken(em, sessionToken);
|
|
131
|
+
if (!row) throw new Error("Session token not found or expired");
|
|
132
|
+
if (row.opencodeSessionId === opencodeSessionId) return;
|
|
133
|
+
if (row.opencodeSessionId && row.opencodeSessionId !== opencodeSessionId) {
|
|
134
|
+
throw new Error("Session token already bound to a different OpenCode session");
|
|
135
|
+
}
|
|
136
|
+
row.opencodeSessionId = opencodeSessionId;
|
|
137
|
+
await em.persist(row).flush();
|
|
138
|
+
}
|
|
139
|
+
async function findApiKeyByOpencodeSessionId(em, opencodeSessionId) {
|
|
140
|
+
if (!opencodeSessionId) return null;
|
|
141
|
+
const record = await findOneWithDecryption(
|
|
142
|
+
em,
|
|
143
|
+
ApiKey,
|
|
144
|
+
{ opencodeSessionId, deletedAt: null }
|
|
145
|
+
);
|
|
146
|
+
if (!record) return null;
|
|
147
|
+
if (record.expiresAt && record.expiresAt.getTime() < Date.now()) return null;
|
|
148
|
+
return record;
|
|
149
|
+
}
|
|
126
150
|
async function findSessionApiKeyWithSecret(em, sessionToken) {
|
|
127
151
|
const record = await findApiKeyBySessionToken(em, sessionToken);
|
|
128
152
|
if (!record) return null;
|
|
@@ -168,10 +192,12 @@ async function withOnetimeApiKey(em, input, fn) {
|
|
|
168
192
|
}
|
|
169
193
|
}
|
|
170
194
|
export {
|
|
195
|
+
bindOpencodeSessionToApiKey,
|
|
171
196
|
createApiKey,
|
|
172
197
|
createSessionApiKey,
|
|
173
198
|
deleteApiKey,
|
|
174
199
|
deleteSessionApiKey,
|
|
200
|
+
findApiKeyByOpencodeSessionId,
|
|
175
201
|
findApiKeyBySecret,
|
|
176
202
|
findApiKeyBySessionToken,
|
|
177
203
|
findSessionApiKeyWithSecret,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/api_keys/services/apiKeyService.ts"],
|
|
4
|
-
"sourcesContent": ["import { randomBytes } from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { hash, compare } from 'bcryptjs'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { Role } from '@open-mercato/core/modules/auth/data/entities'\nimport { ApiKey } from '../data/entities'\nimport { createKmsService } from '@open-mercato/shared/lib/encryption/kms'\nimport { encryptWithAesGcm, decryptWithAesGcm } from '@open-mercato/shared/lib/encryption/aes'\nimport { getSharedApiKeyAuthCache } from '@open-mercato/shared/lib/auth/apiKeyAuthCache'\n\nconst BCRYPT_COST = 10\n\n// =============================================================================\n// Session Secret Encryption Helpers\n// =============================================================================\n\n/**\n * Encrypt an API key secret for storage.\n * Uses tenant-specific DEK if available, otherwise returns null.\n */\nasync function encryptSessionSecret(\n secret: string,\n tenantId: string | null\n): Promise<string | null> {\n if (!tenantId) return null\n\n const kms = createKmsService()\n if (!kms.isHealthy()) return null\n\n const dek = await kms.getTenantDek(tenantId)\n if (!dek) {\n // Try to create a DEK if one doesn't exist\n const created = await kms.createTenantDek(tenantId)\n if (!created) return null\n const encrypted = encryptWithAesGcm(secret, created.key)\n return encrypted.value\n }\n\n const encrypted = encryptWithAesGcm(secret, dek.key)\n return encrypted.value\n}\n\n/**\n * Decrypt an API key secret from storage.\n * Returns null if decryption fails or no DEK available.\n */\nasync function decryptSessionSecret(\n encrypted: string,\n tenantId: string | null\n): Promise<string | null> {\n if (!tenantId || !encrypted) return null\n\n const kms = createKmsService()\n if (!kms.isHealthy()) return null\n\n const dek = await kms.getTenantDek(tenantId)\n if (!dek) return null\n\n return decryptWithAesGcm(encrypted, dek.key)\n}\n\nexport type CreateApiKeyInput = {\n name: string\n description?: string | null\n tenantId?: string | null\n organizationId?: string | null\n roles?: string[]\n expiresAt?: Date | null\n createdBy?: string | null\n}\n\nexport type ApiKeyWithSecret = {\n record: ApiKey\n secret: string\n}\n\nexport function generateApiKeySecret(): { secret: string; prefix: string } {\n const short = randomBytes(4).toString('hex')\n const body = randomBytes(24).toString('hex')\n const secret = `omk_${short}.${body}`\n const prefix = secret.slice(0, 12)\n return { secret, prefix }\n}\n\nexport async function hashApiKey(secret: string): Promise<string> {\n return hash(secret, BCRYPT_COST)\n}\n\nexport async function verifyApiKey(secret: string, keyHash: string): Promise<boolean> {\n return compare(secret, keyHash)\n}\n\nexport async function createApiKey(\n em: EntityManager,\n input: CreateApiKeyInput,\n opts: { rbac?: RbacService } = {},\n): Promise<ApiKeyWithSecret> {\n const { secret, prefix } = generateApiKeySecret()\n const keyHash = await hashApiKey(secret)\n const record = em.create(ApiKey, {\n name: input.name,\n description: input.description ?? null,\n tenantId: input.tenantId ?? null,\n organizationId: input.organizationId ?? null,\n keyHash,\n keyPrefix: prefix,\n rolesJson: Array.isArray(input.roles) ? input.roles : [],\n createdBy: input.createdBy ?? null,\n expiresAt: input.expiresAt ?? null,\n createdAt: new Date(),\n })\n await em.persist(record).flush()\n if (opts.rbac) {\n await opts.rbac.invalidateUserCache(`api_key:${record.id}`)\n }\n return { record, secret }\n}\n\nexport async function deleteApiKey(\n em: EntityManager,\n id: string,\n opts: { rbac?: RbacService } = {},\n): Promise<void> {\n const record = await em.findOne(ApiKey, { id })\n if (!record) return\n record.deletedAt = new Date()\n await em.persist(record).flush()\n getSharedApiKeyAuthCache().invalidateByKeyId(record.id)\n if (opts.rbac) {\n await opts.rbac.invalidateUserCache(`api_key:${record.id}`)\n }\n}\n\nexport async function findApiKeyBySecret(em: EntityManager, secret: string): Promise<ApiKey | null> {\n if (!secret) return null\n // Extract prefix from the secret for fast candidate lookup\n const prefix = secret.slice(0, 12)\n // Find candidates by prefix (fast index lookup)\n const candidates = await em.find(ApiKey, { keyPrefix: prefix, deletedAt: null })\n // Verify each candidate with bcrypt until we find a match\n for (const candidate of candidates) {\n if (candidate.expiresAt && candidate.expiresAt.getTime() < Date.now()) continue\n const isValid = await verifyApiKey(secret, candidate.keyHash)\n if (isValid) return candidate\n }\n return null\n}\n\n// =============================================================================\n// Session-scoped API Keys (for AI Chat ephemeral authorization)\n// =============================================================================\n\nexport type CreateSessionApiKeyInput = {\n sessionToken: string\n userId: string\n userRoles: string[]\n tenantId?: string | null\n organizationId?: string | null\n ttlMinutes?: number\n}\n\n/**\n * Generate a unique session token for ephemeral API keys.\n * Format: sess_{32 hex chars}\n */\nexport function generateSessionToken(): string {\n return `sess_${randomBytes(16).toString('hex')}`\n}\n\n/**\n * Create an ephemeral API key scoped to a chat session.\n * The key inherits the user's roles and expires after ttlMinutes (default 30).\n * The API key secret is encrypted and stored so it can be recovered for API calls.\n */\nexport async function createSessionApiKey(\n em: EntityManager,\n input: CreateSessionApiKeyInput\n): Promise<{ keyId: string; secret: string; sessionToken: string }> {\n const { secret, prefix } = generateApiKeySecret()\n const ttl = input.ttlMinutes ?? 30\n const expiresAt = new Date(Date.now() + ttl * 60 * 1000)\n const keyHash = await hashApiKey(secret)\n\n // Encrypt the secret for later retrieval (used by MCP server for API calls)\n const encryptedSecret = await encryptSessionSecret(secret, input.tenantId ?? null)\n\n const record = em.create(ApiKey, {\n name: `__session_${input.sessionToken}__`,\n description: 'Ephemeral session API key for AI chat',\n tenantId: input.tenantId ?? null,\n organizationId: input.organizationId ?? null,\n keyHash,\n keyPrefix: prefix,\n rolesJson: input.userRoles,\n createdBy: input.userId,\n sessionToken: input.sessionToken,\n sessionUserId: input.userId,\n sessionSecretEncrypted: encryptedSecret,\n expiresAt,\n createdAt: new Date(),\n })\n\n await em.persist(record).flush()\n\n return {\n keyId: record.id,\n secret,\n sessionToken: input.sessionToken,\n }\n}\n\n/**\n * Find an API key by its session token.\n * Returns null if not found, expired, or deleted.\n */\nexport async function findApiKeyBySessionToken(\n em: EntityManager,\n sessionToken: string\n): Promise<ApiKey | null> {\n if (!sessionToken) return null\n\n const record = await em.findOne(ApiKey, {\n sessionToken,\n deletedAt: null,\n })\n\n if (!record) return null\n if (record.expiresAt && record.expiresAt.getTime() < Date.now()) return null\n\n return record\n}\n\n/**\n * Find a session API key with its decrypted secret.\n * Returns null if not found, expired, deleted, or decryption fails.\n * This is used by the MCP server to recover the API key secret for making\n * authenticated API calls on behalf of the user.\n */\nexport async function findSessionApiKeyWithSecret(\n em: EntityManager,\n sessionToken: string\n): Promise<{ key: ApiKey; secret: string } | null> {\n const record = await findApiKeyBySessionToken(em, sessionToken)\n if (!record) return null\n\n // If no encrypted secret stored, cannot recover\n if (!record.sessionSecretEncrypted) {\n console.warn('[ApiKeyService] Session key has no encrypted secret:', sessionToken.slice(0, 12))\n return null\n }\n\n // Decrypt the secret\n const secret = await decryptSessionSecret(record.sessionSecretEncrypted, record.tenantId ?? null)\n if (!secret) {\n console.warn('[ApiKeyService] Failed to decrypt session secret:', sessionToken.slice(0, 12))\n return null\n }\n\n return { key: record, secret }\n}\n\n/**\n * Delete an ephemeral API key by its session token.\n */\nexport async function deleteSessionApiKey(\n em: EntityManager,\n sessionToken: string\n): Promise<void> {\n const record = await em.findOne(ApiKey, { sessionToken, deletedAt: null })\n if (!record) return\n\n record.deletedAt = new Date()\n await em.persist(record).flush()\n getSharedApiKeyAuthCache().invalidateByKeyId(record.id)\n}\n\n/**\n * Execute a function with a one-time API key\n *\n * Creates a temporary API key, executes the function, and deletes the key.\n * Perfect for workflow activities that need authenticated access without\n * storing long-lived credentials.\n *\n * @param em - Entity manager\n * @param input - API key configuration\n * @param fn - Function to execute with the API key secret\n * @returns Result of the function\n */\nconst ONETIME_KEY_MAX_TTL_MS = 5 * 60 * 1000\n\nexport async function withOnetimeApiKey<T>(\n em: EntityManager,\n input: CreateApiKeyInput,\n fn: (secret: string) => Promise<T>\n): Promise<T> {\n const maxExpiresAt = new Date(Date.now() + ONETIME_KEY_MAX_TTL_MS)\n const safeExpiresAt = input.expiresAt && input.expiresAt < maxExpiresAt\n ? input.expiresAt\n : maxExpiresAt\n\n const { record, secret } = await createApiKey(em, {\n ...input,\n name: input.name || '__onetime__',\n description: input.description || 'One-time API key',\n expiresAt: safeExpiresAt,\n })\n\n try {\n const result = await fn(secret)\n return result\n } finally {\n try {\n record.deletedAt = new Date()\n await em.persist(record).flush()\n getSharedApiKeyAuthCache().invalidateByKeyId(record.id)\n } catch (error) {\n console.error('[withOnetimeApiKey] Failed to soft-delete one-time API key:', error)\n }\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,mBAAmB;AAE5B,SAAS,MAAM,eAAe;AAG9B,SAAS,cAAc;AACvB,SAAS,wBAAwB;AACjC,SAAS,mBAAmB,yBAAyB;AACrD,SAAS,gCAAgC;
|
|
4
|
+
"sourcesContent": ["import { randomBytes } from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { hash, compare } from 'bcryptjs'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { Role } from '@open-mercato/core/modules/auth/data/entities'\nimport { ApiKey } from '../data/entities'\nimport { createKmsService } from '@open-mercato/shared/lib/encryption/kms'\nimport { encryptWithAesGcm, decryptWithAesGcm } from '@open-mercato/shared/lib/encryption/aes'\nimport { getSharedApiKeyAuthCache } from '@open-mercato/shared/lib/auth/apiKeyAuthCache'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\nconst BCRYPT_COST = 10\n\n// =============================================================================\n// Session Secret Encryption Helpers\n// =============================================================================\n\n/**\n * Encrypt an API key secret for storage.\n * Uses tenant-specific DEK if available, otherwise returns null.\n */\nasync function encryptSessionSecret(\n secret: string,\n tenantId: string | null\n): Promise<string | null> {\n if (!tenantId) return null\n\n const kms = createKmsService()\n if (!kms.isHealthy()) return null\n\n const dek = await kms.getTenantDek(tenantId)\n if (!dek) {\n // Try to create a DEK if one doesn't exist\n const created = await kms.createTenantDek(tenantId)\n if (!created) return null\n const encrypted = encryptWithAesGcm(secret, created.key)\n return encrypted.value\n }\n\n const encrypted = encryptWithAesGcm(secret, dek.key)\n return encrypted.value\n}\n\n/**\n * Decrypt an API key secret from storage.\n * Returns null if decryption fails or no DEK available.\n */\nasync function decryptSessionSecret(\n encrypted: string,\n tenantId: string | null\n): Promise<string | null> {\n if (!tenantId || !encrypted) return null\n\n const kms = createKmsService()\n if (!kms.isHealthy()) return null\n\n const dek = await kms.getTenantDek(tenantId)\n if (!dek) return null\n\n return decryptWithAesGcm(encrypted, dek.key)\n}\n\nexport type CreateApiKeyInput = {\n name: string\n description?: string | null\n tenantId?: string | null\n organizationId?: string | null\n roles?: string[]\n expiresAt?: Date | null\n createdBy?: string | null\n}\n\nexport type ApiKeyWithSecret = {\n record: ApiKey\n secret: string\n}\n\nexport function generateApiKeySecret(): { secret: string; prefix: string } {\n const short = randomBytes(4).toString('hex')\n const body = randomBytes(24).toString('hex')\n const secret = `omk_${short}.${body}`\n const prefix = secret.slice(0, 12)\n return { secret, prefix }\n}\n\nexport async function hashApiKey(secret: string): Promise<string> {\n return hash(secret, BCRYPT_COST)\n}\n\nexport async function verifyApiKey(secret: string, keyHash: string): Promise<boolean> {\n return compare(secret, keyHash)\n}\n\nexport async function createApiKey(\n em: EntityManager,\n input: CreateApiKeyInput,\n opts: { rbac?: RbacService } = {},\n): Promise<ApiKeyWithSecret> {\n const { secret, prefix } = generateApiKeySecret()\n const keyHash = await hashApiKey(secret)\n const record = em.create(ApiKey, {\n name: input.name,\n description: input.description ?? null,\n tenantId: input.tenantId ?? null,\n organizationId: input.organizationId ?? null,\n keyHash,\n keyPrefix: prefix,\n rolesJson: Array.isArray(input.roles) ? input.roles : [],\n createdBy: input.createdBy ?? null,\n expiresAt: input.expiresAt ?? null,\n createdAt: new Date(),\n })\n await em.persist(record).flush()\n if (opts.rbac) {\n await opts.rbac.invalidateUserCache(`api_key:${record.id}`)\n }\n return { record, secret }\n}\n\nexport async function deleteApiKey(\n em: EntityManager,\n id: string,\n opts: { rbac?: RbacService } = {},\n): Promise<void> {\n const record = await em.findOne(ApiKey, { id })\n if (!record) return\n record.deletedAt = new Date()\n await em.persist(record).flush()\n getSharedApiKeyAuthCache().invalidateByKeyId(record.id)\n if (opts.rbac) {\n await opts.rbac.invalidateUserCache(`api_key:${record.id}`)\n }\n}\n\nexport async function findApiKeyBySecret(em: EntityManager, secret: string): Promise<ApiKey | null> {\n if (!secret) return null\n // Extract prefix from the secret for fast candidate lookup\n const prefix = secret.slice(0, 12)\n // Find candidates by prefix (fast index lookup)\n const candidates = await em.find(ApiKey, { keyPrefix: prefix, deletedAt: null })\n // Verify each candidate with bcrypt until we find a match\n for (const candidate of candidates) {\n if (candidate.expiresAt && candidate.expiresAt.getTime() < Date.now()) continue\n const isValid = await verifyApiKey(secret, candidate.keyHash)\n if (isValid) return candidate\n }\n return null\n}\n\n// =============================================================================\n// Session-scoped API Keys (for AI Chat ephemeral authorization)\n// =============================================================================\n\nexport type CreateSessionApiKeyInput = {\n sessionToken: string\n userId: string\n userRoles: string[]\n tenantId?: string | null\n organizationId?: string | null\n ttlMinutes?: number\n}\n\n/**\n * Generate a unique session token for ephemeral API keys.\n * Format: sess_{32 hex chars}\n */\nexport function generateSessionToken(): string {\n return `sess_${randomBytes(16).toString('hex')}`\n}\n\n/**\n * Create an ephemeral API key scoped to a chat session.\n * The key inherits the user's roles and expires after ttlMinutes (default 30).\n * The API key secret is encrypted and stored so it can be recovered for API calls.\n */\nexport async function createSessionApiKey(\n em: EntityManager,\n input: CreateSessionApiKeyInput\n): Promise<{ keyId: string; secret: string; sessionToken: string }> {\n const { secret, prefix } = generateApiKeySecret()\n const ttl = input.ttlMinutes ?? 30\n const expiresAt = new Date(Date.now() + ttl * 60 * 1000)\n const keyHash = await hashApiKey(secret)\n\n // Encrypt the secret for later retrieval (used by MCP server for API calls)\n const encryptedSecret = await encryptSessionSecret(secret, input.tenantId ?? null)\n\n const record = em.create(ApiKey, {\n name: `__session_${input.sessionToken}__`,\n description: 'Ephemeral session API key for AI chat',\n tenantId: input.tenantId ?? null,\n organizationId: input.organizationId ?? null,\n keyHash,\n keyPrefix: prefix,\n rolesJson: input.userRoles,\n createdBy: input.userId,\n sessionToken: input.sessionToken,\n sessionUserId: input.userId,\n sessionSecretEncrypted: encryptedSecret,\n expiresAt,\n createdAt: new Date(),\n })\n\n await em.persist(record).flush()\n\n return {\n keyId: record.id,\n secret,\n sessionToken: input.sessionToken,\n }\n}\n\n/**\n * Find an API key by its session token.\n * Returns null if not found, expired, or deleted.\n */\nexport async function findApiKeyBySessionToken(\n em: EntityManager,\n sessionToken: string\n): Promise<ApiKey | null> {\n if (!sessionToken) return null\n\n const record = await em.findOne(ApiKey, {\n sessionToken,\n deletedAt: null,\n })\n\n if (!record) return null\n if (record.expiresAt && record.expiresAt.getTime() < Date.now()) return null\n\n return record\n}\n\n/**\n * Bind an OpenCode session id to the api_key row that owns this chat session.\n *\n * Called by the chat dispatcher the first time we see the `done` event for a\n * freshly minted session token. From that point on,\n * `findApiKeyByOpencodeSessionId(em, opencodeSessionId)` returns the same row,\n * which the ai-assistant runtime uses to assert ownership on every resume.\n *\n * Throws when the session token has been deleted/expired, and when the api_key\n * row is already bound to a DIFFERENT OpenCode session (defensive: this should\n * never happen in practice because each chat mints a new session token, but we\n * fail closed instead of silently overwriting).\n *\n * Idempotent when the row is already bound to the same OpenCode session id.\n */\nexport async function bindOpencodeSessionToApiKey(\n em: EntityManager,\n sessionToken: string,\n opencodeSessionId: string\n): Promise<void> {\n if (!sessionToken) throw new Error('Session token not found or expired')\n if (!opencodeSessionId) throw new Error('OpenCode session id is required')\n\n const row = await findApiKeyBySessionToken(em, sessionToken)\n if (!row) throw new Error('Session token not found or expired')\n\n if (row.opencodeSessionId === opencodeSessionId) return\n if (row.opencodeSessionId && row.opencodeSessionId !== opencodeSessionId) {\n throw new Error('Session token already bound to a different OpenCode session')\n }\n\n row.opencodeSessionId = opencodeSessionId\n await em.persist(row).flush()\n}\n\n/**\n * Find an api_key row by its bound OpenCode session id.\n *\n * Returns null if no active row matches, or if the matched row is expired\n * (same contract as `findApiKeyBySessionToken`). Uses\n * `findOneWithDecryption` so encrypted-at-rest fields on the row are decrypted\n * before the ai-assistant runtime inspects `sessionUserId` / `tenantId` /\n * `organizationId` for the ownership check.\n */\nexport async function findApiKeyByOpencodeSessionId(\n em: EntityManager,\n opencodeSessionId: string\n): Promise<ApiKey | null> {\n if (!opencodeSessionId) return null\n\n const record = await findOneWithDecryption(\n em,\n ApiKey,\n { opencodeSessionId, deletedAt: null } as any,\n )\n\n if (!record) return null\n if (record.expiresAt && record.expiresAt.getTime() < Date.now()) return null\n\n return record\n}\n\n/**\n * Find a session API key with its decrypted secret.\n * Returns null if not found, expired, deleted, or decryption fails.\n * This is used by the MCP server to recover the API key secret for making\n * authenticated API calls on behalf of the user.\n */\nexport async function findSessionApiKeyWithSecret(\n em: EntityManager,\n sessionToken: string\n): Promise<{ key: ApiKey; secret: string } | null> {\n const record = await findApiKeyBySessionToken(em, sessionToken)\n if (!record) return null\n\n // If no encrypted secret stored, cannot recover\n if (!record.sessionSecretEncrypted) {\n console.warn('[ApiKeyService] Session key has no encrypted secret:', sessionToken.slice(0, 12))\n return null\n }\n\n // Decrypt the secret\n const secret = await decryptSessionSecret(record.sessionSecretEncrypted, record.tenantId ?? null)\n if (!secret) {\n console.warn('[ApiKeyService] Failed to decrypt session secret:', sessionToken.slice(0, 12))\n return null\n }\n\n return { key: record, secret }\n}\n\n/**\n * Delete an ephemeral API key by its session token.\n */\nexport async function deleteSessionApiKey(\n em: EntityManager,\n sessionToken: string\n): Promise<void> {\n const record = await em.findOne(ApiKey, { sessionToken, deletedAt: null })\n if (!record) return\n\n record.deletedAt = new Date()\n await em.persist(record).flush()\n getSharedApiKeyAuthCache().invalidateByKeyId(record.id)\n}\n\n/**\n * Execute a function with a one-time API key\n *\n * Creates a temporary API key, executes the function, and deletes the key.\n * Perfect for workflow activities that need authenticated access without\n * storing long-lived credentials.\n *\n * @param em - Entity manager\n * @param input - API key configuration\n * @param fn - Function to execute with the API key secret\n * @returns Result of the function\n */\nconst ONETIME_KEY_MAX_TTL_MS = 5 * 60 * 1000\n\nexport async function withOnetimeApiKey<T>(\n em: EntityManager,\n input: CreateApiKeyInput,\n fn: (secret: string) => Promise<T>\n): Promise<T> {\n const maxExpiresAt = new Date(Date.now() + ONETIME_KEY_MAX_TTL_MS)\n const safeExpiresAt = input.expiresAt && input.expiresAt < maxExpiresAt\n ? input.expiresAt\n : maxExpiresAt\n\n const { record, secret } = await createApiKey(em, {\n ...input,\n name: input.name || '__onetime__',\n description: input.description || 'One-time API key',\n expiresAt: safeExpiresAt,\n })\n\n try {\n const result = await fn(secret)\n return result\n } finally {\n try {\n record.deletedAt = new Date()\n await em.persist(record).flush()\n getSharedApiKeyAuthCache().invalidateByKeyId(record.id)\n } catch (error) {\n console.error('[withOnetimeApiKey] Failed to soft-delete one-time API key:', error)\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["encrypted"]
|
|
7
7
|
}
|
|
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
4
4
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
5
|
+
import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
5
6
|
import { Attachment, AttachmentPartition } from "../../data/entities.js";
|
|
6
7
|
import { ensureDefaultPartitions, DEFAULT_ATTACHMENT_PARTITIONS, sanitizePartitionCode, isPartitionSettingsLocked } from "../../lib/partitions.js";
|
|
7
8
|
import { resolvePartitionEnvKey } from "../../lib/partitionEnv.js";
|
|
@@ -18,6 +19,19 @@ const deleteSchema = z.object({
|
|
|
18
19
|
id: z.string().uuid()
|
|
19
20
|
});
|
|
20
21
|
const DEFAULT_CODES = new Set(DEFAULT_ATTACHMENT_PARTITIONS.map((entry) => entry.code));
|
|
22
|
+
function isSuperAdmin(auth) {
|
|
23
|
+
return auth?.isSuperAdmin === true;
|
|
24
|
+
}
|
|
25
|
+
function canManagePartition(auth, entry) {
|
|
26
|
+
if (!auth?.tenantId) return false;
|
|
27
|
+
if (entry.tenantId && entry.tenantId === auth.tenantId) return true;
|
|
28
|
+
if (entry.tenantId == null && isSuperAdmin(auth)) return true;
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
function partitionVisibilityFilter(auth) {
|
|
32
|
+
if (isSuperAdmin(auth)) return {};
|
|
33
|
+
return { $or: [{ tenantId: null }, { tenantId: auth?.tenantId ?? null }] };
|
|
34
|
+
}
|
|
21
35
|
function serializePartition(entry) {
|
|
22
36
|
return {
|
|
23
37
|
id: entry.id,
|
|
@@ -46,12 +60,18 @@ async function resolveEm() {
|
|
|
46
60
|
}
|
|
47
61
|
async function GET(req) {
|
|
48
62
|
const auth = await getAuthFromRequest(req);
|
|
49
|
-
if (!auth?.sub) {
|
|
63
|
+
if (!auth?.sub || !auth.tenantId) {
|
|
50
64
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
51
65
|
}
|
|
52
66
|
const em = await resolveEm();
|
|
53
67
|
await ensureDefaultPartitions(em);
|
|
54
|
-
const rows = await
|
|
68
|
+
const rows = await findWithDecryption(
|
|
69
|
+
em,
|
|
70
|
+
AttachmentPartition,
|
|
71
|
+
partitionVisibilityFilter(auth),
|
|
72
|
+
{ orderBy: { createdAt: "asc" } },
|
|
73
|
+
{ tenantId: auth.tenantId, organizationId: auth.orgId ?? null }
|
|
74
|
+
);
|
|
55
75
|
return NextResponse.json({ items: rows.map((entry) => serializePartition(entry)) });
|
|
56
76
|
}
|
|
57
77
|
async function POST(req) {
|
|
@@ -62,7 +82,7 @@ async function POST(req) {
|
|
|
62
82
|
);
|
|
63
83
|
}
|
|
64
84
|
const auth = await getAuthFromRequest(req);
|
|
65
|
-
if (!auth?.sub) {
|
|
85
|
+
if (!auth?.sub || !auth.tenantId) {
|
|
66
86
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
67
87
|
}
|
|
68
88
|
let json = null;
|
|
@@ -81,7 +101,13 @@ async function POST(req) {
|
|
|
81
101
|
}
|
|
82
102
|
const em = await resolveEm();
|
|
83
103
|
await ensureDefaultPartitions(em);
|
|
84
|
-
const exists = await
|
|
104
|
+
const exists = await findOneWithDecryption(
|
|
105
|
+
em,
|
|
106
|
+
AttachmentPartition,
|
|
107
|
+
{ code },
|
|
108
|
+
void 0,
|
|
109
|
+
{ tenantId: auth.tenantId, organizationId: auth.orgId ?? null }
|
|
110
|
+
);
|
|
85
111
|
if (exists) {
|
|
86
112
|
return NextResponse.json({ error: "Partition code already exists." }, { status: 409 });
|
|
87
113
|
}
|
|
@@ -93,7 +119,9 @@ async function POST(req) {
|
|
|
93
119
|
configJson: parsed.data.configJson ?? null,
|
|
94
120
|
isPublic: parsed.data.isPublic ?? false,
|
|
95
121
|
requiresOcr: typeof parsed.data.requiresOcr === "boolean" ? parsed.data.requiresOcr : resolveDefaultAttachmentOcrEnabled(),
|
|
96
|
-
ocrModel: parsed.data.ocrModel?.trim() || null
|
|
122
|
+
ocrModel: parsed.data.ocrModel?.trim() || null,
|
|
123
|
+
tenantId: auth.tenantId,
|
|
124
|
+
organizationId: auth.orgId ?? null
|
|
97
125
|
});
|
|
98
126
|
await em.persist(entry).flush();
|
|
99
127
|
return NextResponse.json({ item: serializePartition(entry) }, { status: 201 });
|
|
@@ -106,7 +134,7 @@ async function PUT(req) {
|
|
|
106
134
|
);
|
|
107
135
|
}
|
|
108
136
|
const auth = await getAuthFromRequest(req);
|
|
109
|
-
if (!auth?.sub) {
|
|
137
|
+
if (!auth?.sub || !auth.tenantId) {
|
|
110
138
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
111
139
|
}
|
|
112
140
|
let json = null;
|
|
@@ -120,8 +148,14 @@ async function PUT(req) {
|
|
|
120
148
|
return NextResponse.json({ error: "Invalid payload" }, { status: 400 });
|
|
121
149
|
}
|
|
122
150
|
const em = await resolveEm();
|
|
123
|
-
const entry = await
|
|
124
|
-
|
|
151
|
+
const entry = await findOneWithDecryption(
|
|
152
|
+
em,
|
|
153
|
+
AttachmentPartition,
|
|
154
|
+
{ id: parsed.data.id },
|
|
155
|
+
void 0,
|
|
156
|
+
{ tenantId: auth.tenantId, organizationId: auth.orgId ?? null }
|
|
157
|
+
);
|
|
158
|
+
if (!entry || !canManagePartition(auth, entry)) {
|
|
125
159
|
return NextResponse.json({ error: "Partition not found" }, { status: 404 });
|
|
126
160
|
}
|
|
127
161
|
if (sanitizePartitionCode(parsed.data.code) !== entry.code) {
|
|
@@ -153,7 +187,7 @@ async function DELETE(req) {
|
|
|
153
187
|
);
|
|
154
188
|
}
|
|
155
189
|
const auth = await getAuthFromRequest(req);
|
|
156
|
-
if (!auth?.sub) {
|
|
190
|
+
if (!auth?.sub || !auth.tenantId) {
|
|
157
191
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
158
192
|
}
|
|
159
193
|
const url = new URL(req.url);
|
|
@@ -163,14 +197,22 @@ async function DELETE(req) {
|
|
|
163
197
|
return NextResponse.json({ error: "Partition id is required" }, { status: 400 });
|
|
164
198
|
}
|
|
165
199
|
const em = await resolveEm();
|
|
166
|
-
const entry = await
|
|
167
|
-
|
|
200
|
+
const entry = await findOneWithDecryption(
|
|
201
|
+
em,
|
|
202
|
+
AttachmentPartition,
|
|
203
|
+
{ id: parsed.data.id },
|
|
204
|
+
void 0,
|
|
205
|
+
{ tenantId: auth.tenantId, organizationId: auth.orgId ?? null }
|
|
206
|
+
);
|
|
207
|
+
if (!entry || !canManagePartition(auth, entry)) {
|
|
168
208
|
return NextResponse.json({ error: "Partition not found" }, { status: 404 });
|
|
169
209
|
}
|
|
170
210
|
if (DEFAULT_CODES.has(entry.code)) {
|
|
171
211
|
return NextResponse.json({ error: "Default partitions cannot be removed." }, { status: 400 });
|
|
172
212
|
}
|
|
173
|
-
const
|
|
213
|
+
const usageFilter = { partitionCode: entry.code };
|
|
214
|
+
if (entry.tenantId) usageFilter.tenantId = entry.tenantId;
|
|
215
|
+
const usage = await em.count(Attachment, usageFilter);
|
|
174
216
|
if (usage > 0) {
|
|
175
217
|
return NextResponse.json({ error: "Partition is in use and cannot be removed." }, { status: 409 });
|
|
176
218
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/attachments/api/partitions/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { Attachment, AttachmentPartition } from '../../data/entities'\nimport { ensureDefaultPartitions, DEFAULT_ATTACHMENT_PARTITIONS, sanitizePartitionCode, isPartitionSettingsLocked } from '../../lib/partitions'\nimport { resolvePartitionEnvKey } from '../../lib/partitionEnv'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { resolveDefaultAttachmentOcrEnabled } from '../../lib/ocrConfig'\nimport {\n attachmentsTag,\n partitionCreateSchema,\n partitionUpdateSchema,\n partitionResponseSchema,\n partitionListResponseSchema,\n attachmentErrorSchema,\n} from '../openapi'\n\nconst deleteSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst DEFAULT_CODES = new Set(DEFAULT_ATTACHMENT_PARTITIONS.map((entry) => entry.code))\n\nfunction serializePartition(entry: AttachmentPartition) {\n return {\n id: entry.id,\n code: entry.code,\n title: entry.title,\n description: entry.description ?? null,\n isPublic: entry.isPublic ?? false,\n requiresOcr: entry.requiresOcr ?? resolveDefaultAttachmentOcrEnabled(),\n ocrModel: entry.ocrModel ?? null,\n storageDriver: entry.storageDriver ?? 'local',\n configJson: entry.configJson ?? null,\n createdAt: entry.createdAt instanceof Date ? entry.createdAt.toISOString() : null,\n updatedAt: entry.updatedAt instanceof Date ? entry.updatedAt.toISOString() : null,\n envKey: resolvePartitionEnvKey(entry.code),\n }\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n POST: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n} as const\n\nasync function resolveEm() {\n const { resolve } = await createRequestContainer()\n return resolve('em') as EntityManager\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const em = await resolveEm()\n await ensureDefaultPartitions(em)\n const rows = await em.find(AttachmentPartition, {}, { orderBy: { createdAt: 'asc' } })\n return NextResponse.json({ items: rows.map((entry) => serializePartition(entry)) })\n}\n\nexport async function POST(req: Request) {\n if (isPartitionSettingsLocked()) {\n return NextResponse.json(\n { error: 'Attachment partitions are managed by the environment in demo/onboarding mode.' },\n { status: 403 },\n )\n }\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n let json: unknown = null\n try {\n json = await req.json()\n } catch {\n json = null\n }\n const parsed = partitionCreateSchema.safeParse(json)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n const code = sanitizePartitionCode(parsed.data.code)\n if (!code) {\n return NextResponse.json({ error: 'Partition code is required.' }, { status: 400 })\n }\n const em = await resolveEm()\n await ensureDefaultPartitions(em)\n const exists = await em.findOne(AttachmentPartition, { code })\n if (exists) {\n return NextResponse.json({ error: 'Partition code already exists.' }, { status: 409 })\n }\n const entry = em.create(AttachmentPartition, {\n code,\n title: parsed.data.title.trim(),\n description: parsed.data.description?.trim() ?? null,\n storageDriver: parsed.data.storageDriver ?? 'local',\n configJson: parsed.data.configJson ?? null,\n isPublic: parsed.data.isPublic ?? false,\n requiresOcr:\n typeof parsed.data.requiresOcr === 'boolean'\n ? parsed.data.requiresOcr\n : resolveDefaultAttachmentOcrEnabled(),\n ocrModel: parsed.data.ocrModel?.trim() || null,\n })\n await em.persist(entry).flush()\n return NextResponse.json({ item: serializePartition(entry) }, { status: 201 })\n}\n\nexport async function PUT(req: Request) {\n if (isPartitionSettingsLocked()) {\n return NextResponse.json(\n { error: 'Attachment partitions are managed by the environment in demo/onboarding mode.' },\n { status: 403 },\n )\n }\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n let json: unknown = null\n try {\n json = await req.json()\n } catch {\n json = null\n }\n const parsed = partitionUpdateSchema.safeParse(json)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n const em = await resolveEm()\n const entry = await em.findOne(AttachmentPartition, { id: parsed.data.id })\n if (!entry) {\n return NextResponse.json({ error: 'Partition not found' }, { status: 404 })\n }\n if (sanitizePartitionCode(parsed.data.code) !== entry.code) {\n return NextResponse.json({ error: 'Partition code cannot be changed.' }, { status: 400 })\n }\n entry.title = parsed.data.title.trim()\n entry.description = parsed.data.description?.trim() ?? null\n entry.isPublic = parsed.data.isPublic ?? false\n if (typeof parsed.data.requiresOcr === 'boolean') {\n entry.requiresOcr = parsed.data.requiresOcr\n }\n if (parsed.data.ocrModel !== undefined) {\n entry.ocrModel = parsed.data.ocrModel?.trim() || null\n }\n if (parsed.data.storageDriver !== undefined) {\n entry.storageDriver = parsed.data.storageDriver\n }\n if (parsed.data.configJson !== undefined) {\n entry.configJson = parsed.data.configJson ?? null\n }\n await em.persist(entry).flush()\n return NextResponse.json({ item: serializePartition(entry) })\n}\n\nexport async function DELETE(req: Request) {\n if (isPartitionSettingsLocked()) {\n return NextResponse.json(\n { error: 'Attachment partitions are managed by the environment in demo/onboarding mode.' },\n { status: 403 },\n )\n }\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const url = new URL(req.url)\n const id = url.searchParams.get('id')\n const parsed = deleteSchema.safeParse({ id })\n if (!parsed.success) {\n return NextResponse.json({ error: 'Partition id is required' }, { status: 400 })\n }\n const em = await resolveEm()\n const entry = await em.findOne(AttachmentPartition, { id: parsed.data.id })\n if (!entry) {\n return NextResponse.json({ error: 'Partition not found' }, { status: 404 })\n }\n if (DEFAULT_CODES.has(entry.code)) {\n return NextResponse.json({ error: 'Default partitions cannot be removed.' }, { status: 400 })\n }\n const usage = await em.count(Attachment, { partitionCode: entry.code })\n if (usage > 0) {\n return NextResponse.json({ error: 'Partition is in use and cannot be removed.' }, { status: 409 })\n }\n await em.remove(entry).flush()\n return NextResponse.json({ ok: true })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: 'Attachment partition management',\n methods: {\n GET: {\n summary: 'List all attachment partitions',\n description: 'Returns all configured attachment partitions with storage settings, OCR configuration, and access control settings.',\n responses: [\n { status: 200, description: 'List of partitions', schema: partitionListResponseSchema },\n ],\n errors: [\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n ],\n },\n POST: {\n summary: 'Create new partition',\n description: 'Creates a new attachment partition with specified storage and OCR settings. Requires unique partition code.',\n requestBody: {\n contentType: 'application/json',\n schema: partitionCreateSchema,\n },\n responses: [\n { status: 201, description: 'Partition created successfully', schema: partitionResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or partition code', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 403, description: 'Partitions locked in demo mode', schema: attachmentErrorSchema },\n { status: 409, description: 'Partition code already exists', schema: attachmentErrorSchema },\n ],\n },\n PUT: {\n summary: 'Update partition',\n description: 'Updates an existing partition. Partition code cannot be changed. Title, description, OCR settings, and access control can be modified.',\n requestBody: {\n contentType: 'application/json',\n schema: partitionUpdateSchema,\n },\n responses: [\n { status: 200, description: 'Partition updated successfully', schema: partitionResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or code change attempt', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 403, description: 'Partitions locked in demo mode', schema: attachmentErrorSchema },\n { status: 404, description: 'Partition not found', schema: attachmentErrorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete partition',\n description: 'Deletes a partition. Default partitions cannot be deleted. Partitions with existing attachments cannot be deleted.',\n responses: [\n { status: 200, description: 'Partition deleted successfully', schema: z.object({ ok: z.literal(true) }) },\n ],\n errors: [\n { status: 400, description: 'Invalid ID or default partition deletion attempt', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 403, description: 'Partitions locked in demo mode', schema: attachmentErrorSchema },\n { status: 404, description: 'Partition not found', schema: attachmentErrorSchema },\n { status: 409, description: 'Partition in use', schema: attachmentErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,YAAY,2BAA2B;AAChD,SAAS,yBAAyB,+BAA+B,uBAAuB,iCAAiC;AACzH,SAAS,8BAA8B;AAEvC,SAAS,0CAA0C;AACnD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,MAAM,eAAe,EAAE,OAAO;AAAA,EAC5B,IAAI,EAAE,OAAO,EAAE,KAAK;AACtB,CAAC;AAED,MAAM,gBAAgB,IAAI,IAAI,8BAA8B,IAAI,CAAC,UAAU,MAAM,IAAI,CAAC;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { Attachment, AttachmentPartition } from '../../data/entities'\nimport { ensureDefaultPartitions, DEFAULT_ATTACHMENT_PARTITIONS, sanitizePartitionCode, isPartitionSettingsLocked } from '../../lib/partitions'\nimport { resolvePartitionEnvKey } from '../../lib/partitionEnv'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { resolveDefaultAttachmentOcrEnabled } from '../../lib/ocrConfig'\nimport {\n attachmentsTag,\n partitionCreateSchema,\n partitionUpdateSchema,\n partitionResponseSchema,\n partitionListResponseSchema,\n attachmentErrorSchema,\n} from '../openapi'\n\nconst deleteSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst DEFAULT_CODES = new Set(DEFAULT_ATTACHMENT_PARTITIONS.map((entry) => entry.code))\n\ntype AuthLike = { sub?: string; tenantId?: string | null; orgId?: string | null; isSuperAdmin?: boolean } | null | undefined\n\nfunction isSuperAdmin(auth: AuthLike): boolean {\n return auth?.isSuperAdmin === true\n}\n\nfunction canManagePartition(auth: AuthLike, entry: AttachmentPartition): boolean {\n if (!auth?.tenantId) return false\n if (entry.tenantId && entry.tenantId === auth.tenantId) return true\n if (entry.tenantId == null && isSuperAdmin(auth)) return true\n return false\n}\n\nfunction partitionVisibilityFilter(auth: AuthLike) {\n if (isSuperAdmin(auth)) return {}\n return { $or: [{ tenantId: null }, { tenantId: auth?.tenantId ?? null }] }\n}\n\nfunction serializePartition(entry: AttachmentPartition) {\n return {\n id: entry.id,\n code: entry.code,\n title: entry.title,\n description: entry.description ?? null,\n isPublic: entry.isPublic ?? false,\n requiresOcr: entry.requiresOcr ?? resolveDefaultAttachmentOcrEnabled(),\n ocrModel: entry.ocrModel ?? null,\n storageDriver: entry.storageDriver ?? 'local',\n configJson: entry.configJson ?? null,\n createdAt: entry.createdAt instanceof Date ? entry.createdAt.toISOString() : null,\n updatedAt: entry.updatedAt instanceof Date ? entry.updatedAt.toISOString() : null,\n envKey: resolvePartitionEnvKey(entry.code),\n }\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n POST: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n} as const\n\nasync function resolveEm() {\n const { resolve } = await createRequestContainer()\n return resolve('em') as EntityManager\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const em = await resolveEm()\n await ensureDefaultPartitions(em)\n const rows = await findWithDecryption(\n em,\n AttachmentPartition,\n partitionVisibilityFilter(auth),\n { orderBy: { createdAt: 'asc' } },\n { tenantId: auth.tenantId, organizationId: auth.orgId ?? null },\n )\n return NextResponse.json({ items: rows.map((entry) => serializePartition(entry)) })\n}\n\nexport async function POST(req: Request) {\n if (isPartitionSettingsLocked()) {\n return NextResponse.json(\n { error: 'Attachment partitions are managed by the environment in demo/onboarding mode.' },\n { status: 403 },\n )\n }\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n let json: unknown = null\n try {\n json = await req.json()\n } catch {\n json = null\n }\n const parsed = partitionCreateSchema.safeParse(json)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n const code = sanitizePartitionCode(parsed.data.code)\n if (!code) {\n return NextResponse.json({ error: 'Partition code is required.' }, { status: 400 })\n }\n const em = await resolveEm()\n await ensureDefaultPartitions(em)\n const exists = await findOneWithDecryption(\n em,\n AttachmentPartition,\n { code },\n undefined,\n { tenantId: auth.tenantId, organizationId: auth.orgId ?? null },\n )\n if (exists) {\n return NextResponse.json({ error: 'Partition code already exists.' }, { status: 409 })\n }\n const entry = em.create(AttachmentPartition, {\n code,\n title: parsed.data.title.trim(),\n description: parsed.data.description?.trim() ?? null,\n storageDriver: parsed.data.storageDriver ?? 'local',\n configJson: parsed.data.configJson ?? null,\n isPublic: parsed.data.isPublic ?? false,\n requiresOcr:\n typeof parsed.data.requiresOcr === 'boolean'\n ? parsed.data.requiresOcr\n : resolveDefaultAttachmentOcrEnabled(),\n ocrModel: parsed.data.ocrModel?.trim() || null,\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n })\n await em.persist(entry).flush()\n return NextResponse.json({ item: serializePartition(entry) }, { status: 201 })\n}\n\nexport async function PUT(req: Request) {\n if (isPartitionSettingsLocked()) {\n return NextResponse.json(\n { error: 'Attachment partitions are managed by the environment in demo/onboarding mode.' },\n { status: 403 },\n )\n }\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n let json: unknown = null\n try {\n json = await req.json()\n } catch {\n json = null\n }\n const parsed = partitionUpdateSchema.safeParse(json)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n const em = await resolveEm()\n const entry = await findOneWithDecryption(\n em,\n AttachmentPartition,\n { id: parsed.data.id },\n undefined,\n { tenantId: auth.tenantId, organizationId: auth.orgId ?? null },\n )\n if (!entry || !canManagePartition(auth, entry)) {\n return NextResponse.json({ error: 'Partition not found' }, { status: 404 })\n }\n if (sanitizePartitionCode(parsed.data.code) !== entry.code) {\n return NextResponse.json({ error: 'Partition code cannot be changed.' }, { status: 400 })\n }\n entry.title = parsed.data.title.trim()\n entry.description = parsed.data.description?.trim() ?? null\n entry.isPublic = parsed.data.isPublic ?? false\n if (typeof parsed.data.requiresOcr === 'boolean') {\n entry.requiresOcr = parsed.data.requiresOcr\n }\n if (parsed.data.ocrModel !== undefined) {\n entry.ocrModel = parsed.data.ocrModel?.trim() || null\n }\n if (parsed.data.storageDriver !== undefined) {\n entry.storageDriver = parsed.data.storageDriver\n }\n if (parsed.data.configJson !== undefined) {\n entry.configJson = parsed.data.configJson ?? null\n }\n await em.persist(entry).flush()\n return NextResponse.json({ item: serializePartition(entry) })\n}\n\nexport async function DELETE(req: Request) {\n if (isPartitionSettingsLocked()) {\n return NextResponse.json(\n { error: 'Attachment partitions are managed by the environment in demo/onboarding mode.' },\n { status: 403 },\n )\n }\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const url = new URL(req.url)\n const id = url.searchParams.get('id')\n const parsed = deleteSchema.safeParse({ id })\n if (!parsed.success) {\n return NextResponse.json({ error: 'Partition id is required' }, { status: 400 })\n }\n const em = await resolveEm()\n const entry = await findOneWithDecryption(\n em,\n AttachmentPartition,\n { id: parsed.data.id },\n undefined,\n { tenantId: auth.tenantId, organizationId: auth.orgId ?? null },\n )\n if (!entry || !canManagePartition(auth, entry)) {\n return NextResponse.json({ error: 'Partition not found' }, { status: 404 })\n }\n if (DEFAULT_CODES.has(entry.code)) {\n return NextResponse.json({ error: 'Default partitions cannot be removed.' }, { status: 400 })\n }\n const usageFilter: Record<string, unknown> = { partitionCode: entry.code }\n if (entry.tenantId) usageFilter.tenantId = entry.tenantId\n const usage = await em.count(Attachment, usageFilter)\n if (usage > 0) {\n return NextResponse.json({ error: 'Partition is in use and cannot be removed.' }, { status: 409 })\n }\n await em.remove(entry).flush()\n return NextResponse.json({ ok: true })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: 'Attachment partition management',\n methods: {\n GET: {\n summary: 'List all attachment partitions',\n description: 'Returns all configured attachment partitions with storage settings, OCR configuration, and access control settings.',\n responses: [\n { status: 200, description: 'List of partitions', schema: partitionListResponseSchema },\n ],\n errors: [\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n ],\n },\n POST: {\n summary: 'Create new partition',\n description: 'Creates a new attachment partition with specified storage and OCR settings. Requires unique partition code.',\n requestBody: {\n contentType: 'application/json',\n schema: partitionCreateSchema,\n },\n responses: [\n { status: 201, description: 'Partition created successfully', schema: partitionResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or partition code', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 403, description: 'Partitions locked in demo mode', schema: attachmentErrorSchema },\n { status: 409, description: 'Partition code already exists', schema: attachmentErrorSchema },\n ],\n },\n PUT: {\n summary: 'Update partition',\n description: 'Updates an existing partition. Partition code cannot be changed. Title, description, OCR settings, and access control can be modified.',\n requestBody: {\n contentType: 'application/json',\n schema: partitionUpdateSchema,\n },\n responses: [\n { status: 200, description: 'Partition updated successfully', schema: partitionResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or code change attempt', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 403, description: 'Partitions locked in demo mode', schema: attachmentErrorSchema },\n { status: 404, description: 'Partition not found', schema: attachmentErrorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete partition',\n description: 'Deletes a partition. Default partitions cannot be deleted. Partitions with existing attachments cannot be deleted.',\n responses: [\n { status: 200, description: 'Partition deleted successfully', schema: z.object({ ok: z.literal(true) }) },\n ],\n errors: [\n { status: 400, description: 'Invalid ID or default partition deletion attempt', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 403, description: 'Partitions locked in demo mode', schema: attachmentErrorSchema },\n { status: 404, description: 'Partition not found', schema: attachmentErrorSchema },\n { status: 409, description: 'Partition in use', schema: attachmentErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -48,6 +48,13 @@ __decorateClass([
|
|
|
48
48
|
__decorateClass([
|
|
49
49
|
Property({ name: "ocr_model", type: "text", nullable: true })
|
|
50
50
|
], AttachmentPartition.prototype, "ocrModel", 2);
|
|
51
|
+
__decorateClass([
|
|
52
|
+
Property({ name: "organization_id", type: "uuid", nullable: true })
|
|
53
|
+
], AttachmentPartition.prototype, "organizationId", 2);
|
|
54
|
+
__decorateClass([
|
|
55
|
+
Property({ name: "tenant_id", type: "uuid", nullable: true }),
|
|
56
|
+
Index({ name: "attachment_partitions_tenant_idx" })
|
|
57
|
+
], AttachmentPartition.prototype, "tenantId", 2);
|
|
51
58
|
__decorateClass([
|
|
52
59
|
Property({ name: "created_at", type: Date, onCreate: () => /* @__PURE__ */ new Date() })
|
|
53
60
|
], AttachmentPartition.prototype, "createdAt", 2);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/attachments/data/entities.ts"],
|
|
4
|
-
"sourcesContent": ["import { OptionalProps } from '@mikro-orm/core'\nimport { Entity, Index, PrimaryKey, Property, Unique } from '@mikro-orm/decorators/legacy'\nimport { resolveDefaultAttachmentOcrEnabled } from '../lib/ocrConfig'\n\n@Entity({ tableName: 'attachment_partitions' })\n@Unique({ name: 'attachment_partitions_code_unique', properties: ['code'] })\nexport class AttachmentPartition {\n [OptionalProps]?: 'createdAt' | 'updatedAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ type: 'text' })\n code!: string\n\n @Property({ type: 'text' })\n title!: string\n\n @Property({ type: 'text', nullable: true })\n description?: string | null\n\n @Property({ name: 'storage_driver', type: 'text', default: 'local' })\n storageDriver: string = 'local'\n\n @Property({ name: 'config_json', type: 'json', nullable: true })\n configJson?: Record<string, unknown> | null\n\n @Property({ name: 'is_public', type: 'boolean', default: false })\n isPublic: boolean = false\n\n @Property({ name: 'requires_ocr', type: 'boolean', default: resolveDefaultAttachmentOcrEnabled() })\n requiresOcr: boolean = resolveDefaultAttachmentOcrEnabled()\n\n @Property({ name: 'ocr_model', type: 'text', nullable: true })\n ocrModel?: string | null\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n}\n\n@Entity({ tableName: 'attachments' })\nexport class Attachment {\n [OptionalProps]?: 'createdAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'entity_id', type: 'text' })\n entityId!: string\n\n @Property({ name: 'record_id', type: 'text' })\n @Index({ name: 'attachments_entity_record_idx' })\n recordId!: string\n\n @Property({ name: 'organization_id', type: 'uuid', nullable: true })\n organizationId?: string | null\n\n @Property({ name: 'tenant_id', type: 'uuid', nullable: true })\n tenantId?: string | null\n\n @Property({ name: 'partition_code', type: 'text' })\n @Index({ name: 'attachments_partition_code_idx' })\n partitionCode!: string\n\n @Property({ name: 'file_name', type: 'text' })\n fileName!: string\n\n @Property({ name: 'mime_type', type: 'text' })\n mimeType!: string\n\n @Property({ name: 'file_size', type: 'int' })\n fileSize!: number\n\n @Property({ name: 'storage_driver', type: 'text', default: 'local' })\n storageDriver: string = 'local'\n\n @Property({ name: 'storage_path', type: 'text' })\n storagePath!: string\n\n @Property({ name: 'storage_metadata', type: 'jsonb', nullable: true })\n storageMetadata?: Record<string, unknown> | null\n\n @Property({ name: 'url', type: 'text' })\n url!: string\n\n @Property({ name: 'content', type: 'text', nullable: true })\n content: string | null = null\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;AAAA,SAAS,qBAAqB;AAC9B,SAAS,QAAQ,OAAO,YAAY,UAAU,cAAc;AAC5D,SAAS,0CAA0C;AAKhD;AADI,IAAM,sBAAN,MAA0B;AAAA,EAA1B;AAgBL,yBAAwB;AAMxB,oBAAoB;AAGpB,uBAAuB,mCAAmC;
|
|
4
|
+
"sourcesContent": ["import { OptionalProps } from '@mikro-orm/core'\nimport { Entity, Index, PrimaryKey, Property, Unique } from '@mikro-orm/decorators/legacy'\nimport { resolveDefaultAttachmentOcrEnabled } from '../lib/ocrConfig'\n\n@Entity({ tableName: 'attachment_partitions' })\n@Unique({ name: 'attachment_partitions_code_unique', properties: ['code'] })\nexport class AttachmentPartition {\n [OptionalProps]?: 'createdAt' | 'updatedAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ type: 'text' })\n code!: string\n\n @Property({ type: 'text' })\n title!: string\n\n @Property({ type: 'text', nullable: true })\n description?: string | null\n\n @Property({ name: 'storage_driver', type: 'text', default: 'local' })\n storageDriver: string = 'local'\n\n @Property({ name: 'config_json', type: 'json', nullable: true })\n configJson?: Record<string, unknown> | null\n\n @Property({ name: 'is_public', type: 'boolean', default: false })\n isPublic: boolean = false\n\n @Property({ name: 'requires_ocr', type: 'boolean', default: resolveDefaultAttachmentOcrEnabled() })\n requiresOcr: boolean = resolveDefaultAttachmentOcrEnabled()\n\n @Property({ name: 'ocr_model', type: 'text', nullable: true })\n ocrModel?: string | null\n\n @Property({ name: 'organization_id', type: 'uuid', nullable: true })\n organizationId?: string | null\n\n @Property({ name: 'tenant_id', type: 'uuid', nullable: true })\n @Index({ name: 'attachment_partitions_tenant_idx' })\n tenantId?: string | null\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n}\n\n@Entity({ tableName: 'attachments' })\nexport class Attachment {\n [OptionalProps]?: 'createdAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'entity_id', type: 'text' })\n entityId!: string\n\n @Property({ name: 'record_id', type: 'text' })\n @Index({ name: 'attachments_entity_record_idx' })\n recordId!: string\n\n @Property({ name: 'organization_id', type: 'uuid', nullable: true })\n organizationId?: string | null\n\n @Property({ name: 'tenant_id', type: 'uuid', nullable: true })\n tenantId?: string | null\n\n @Property({ name: 'partition_code', type: 'text' })\n @Index({ name: 'attachments_partition_code_idx' })\n partitionCode!: string\n\n @Property({ name: 'file_name', type: 'text' })\n fileName!: string\n\n @Property({ name: 'mime_type', type: 'text' })\n mimeType!: string\n\n @Property({ name: 'file_size', type: 'int' })\n fileSize!: number\n\n @Property({ name: 'storage_driver', type: 'text', default: 'local' })\n storageDriver: string = 'local'\n\n @Property({ name: 'storage_path', type: 'text' })\n storagePath!: string\n\n @Property({ name: 'storage_metadata', type: 'jsonb', nullable: true })\n storageMetadata?: Record<string, unknown> | null\n\n @Property({ name: 'url', type: 'text' })\n url!: string\n\n @Property({ name: 'content', type: 'text', nullable: true })\n content: string | null = null\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Migration } from "@mikro-orm/migrations";
|
|
2
|
+
class Migration20260524000000 extends Migration {
|
|
3
|
+
async up() {
|
|
4
|
+
this.addSql(
|
|
5
|
+
`alter table "attachment_partitions" add column "organization_id" uuid null;`
|
|
6
|
+
);
|
|
7
|
+
this.addSql(
|
|
8
|
+
`alter table "attachment_partitions" add column "tenant_id" uuid null;`
|
|
9
|
+
);
|
|
10
|
+
this.addSql(
|
|
11
|
+
`create index "attachment_partitions_tenant_idx" on "attachment_partitions" ("tenant_id");`
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
async down() {
|
|
15
|
+
this.addSql(`drop index if exists "attachment_partitions_tenant_idx";`);
|
|
16
|
+
this.addSql(
|
|
17
|
+
`alter table "attachment_partitions" drop column "tenant_id";`
|
|
18
|
+
);
|
|
19
|
+
this.addSql(
|
|
20
|
+
`alter table "attachment_partitions" drop column "organization_id";`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export {
|
|
25
|
+
Migration20260524000000
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=Migration20260524000000.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/attachments/migrations/Migration20260524000000.ts"],
|
|
4
|
+
"sourcesContent": ["import { Migration } from '@mikro-orm/migrations'\n\nexport class Migration20260524000000 extends Migration {\n override async up(): Promise<void> {\n this.addSql(\n `alter table \"attachment_partitions\" add column \"organization_id\" uuid null;`,\n )\n this.addSql(\n `alter table \"attachment_partitions\" add column \"tenant_id\" uuid null;`,\n )\n this.addSql(\n `create index \"attachment_partitions_tenant_idx\" on \"attachment_partitions\" (\"tenant_id\");`,\n )\n }\n\n override async down(): Promise<void> {\n this.addSql(`drop index if exists \"attachment_partitions_tenant_idx\";`)\n this.addSql(\n `alter table \"attachment_partitions\" drop column \"tenant_id\";`,\n )\n this.addSql(\n `alter table \"attachment_partitions\" drop column \"organization_id\";`,\n )\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,iBAAiB;AAEnB,MAAM,gCAAgC,UAAU;AAAA,EACrD,MAAe,KAAoB;AACjC,SAAK;AAAA,MACH;AAAA,IACF;AACA,SAAK;AAAA,MACH;AAAA,IACF;AACA,SAAK;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAe,OAAsB;AACnC,SAAK,OAAO,0DAA0D;AACtE,SAAK;AAAA,MACH;AAAA,IACF;AACA,SAAK;AAAA,MACH;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|