@intx/hub-api 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -0
- package/package.json +28 -0
- package/src/app.test.ts +225 -0
- package/src/app.ts +382 -0
- package/src/auth.ts +21 -0
- package/src/context.ts +38 -0
- package/src/format.ts +9 -0
- package/src/git-http/advertise-refs.test.ts +459 -0
- package/src/git-http/advertise-refs.ts +226 -0
- package/src/git-http/pkt-line.test.ts +220 -0
- package/src/git-http/pkt-line.ts +235 -0
- package/src/git-http/receive-pack.test.ts +397 -0
- package/src/git-http/receive-pack.ts +261 -0
- package/src/git-http/side-band-64k.test.ts +181 -0
- package/src/git-http/side-band-64k.ts +134 -0
- package/src/git-http/upload-pack.test.ts +545 -0
- package/src/git-http/upload-pack.ts +396 -0
- package/src/index.ts +23 -0
- package/src/middleware/git-token-auth.test.ts +587 -0
- package/src/middleware/git-token-auth.ts +315 -0
- package/src/middleware/grant.ts +106 -0
- package/src/middleware/session.ts +13 -0
- package/src/middleware/tenant.test.ts +192 -0
- package/src/middleware/tenant.ts +101 -0
- package/src/openapi.ts +66 -0
- package/src/pagination.ts +117 -0
- package/src/routes/agent-data.ts +179 -0
- package/src/routes/agent-state-git.ts +562 -0
- package/src/routes/agents.test.ts +337 -0
- package/src/routes/agents.ts +704 -0
- package/src/routes/approvals.ts +130 -0
- package/src/routes/assets.test.ts +567 -0
- package/src/routes/assets.ts +592 -0
- package/src/routes/credentials.ts +435 -0
- package/src/routes/git-tokens.test.ts +709 -0
- package/src/routes/git-tokens.ts +771 -0
- package/src/routes/grants.ts +509 -0
- package/src/routes/instances.test.ts +1103 -0
- package/src/routes/instances.ts +1797 -0
- package/src/routes/me.ts +405 -0
- package/src/routes/oauth-clients.ts +349 -0
- package/src/routes/observability.ts +146 -0
- package/src/routes/offerings.ts +382 -0
- package/src/routes/principals.ts +515 -0
- package/src/routes/providers.ts +351 -0
- package/src/routes/roles.ts +452 -0
- package/src/routes/sidecars.ts +221 -0
- package/src/routes/tenant-federation.ts +225 -0
- package/src/routes/tenants.ts +369 -0
- package/src/routes/wallets.ts +370 -0
- package/src/session.ts +44 -0
- package/src/timeline-reconstruction.test.ts +786 -0
- package/src/timeline-reconstruction.ts +383 -0
- package/tsconfig.json +4 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
import { eq, ne, and } from "drizzle-orm";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { describeRoute, resolver, validator } from "hono-openapi";
|
|
4
|
+
|
|
5
|
+
import { principal, principalRole, role, user } from "@intx/db/schema";
|
|
6
|
+
import type { DB } from "@intx/db";
|
|
7
|
+
import {
|
|
8
|
+
PrincipalResponse,
|
|
9
|
+
UpdatePrincipal,
|
|
10
|
+
InviteMember,
|
|
11
|
+
ErrorResponse,
|
|
12
|
+
paginatedSchema,
|
|
13
|
+
} from "@intx/types";
|
|
14
|
+
|
|
15
|
+
import type { TenantEnv } from "../context";
|
|
16
|
+
import { first, ts } from "../format";
|
|
17
|
+
import { generateId } from "@intx/hub-common";
|
|
18
|
+
import { idResource } from "../middleware/grant";
|
|
19
|
+
import type { RequireGrant } from "../middleware/grant";
|
|
20
|
+
import {
|
|
21
|
+
parsePageParams,
|
|
22
|
+
cursorCondition,
|
|
23
|
+
pageOrder,
|
|
24
|
+
paginatedResponse,
|
|
25
|
+
pageParameters,
|
|
26
|
+
} from "../pagination";
|
|
27
|
+
|
|
28
|
+
type ResolvedIdentity = { displayName: string; email?: string };
|
|
29
|
+
|
|
30
|
+
function formatPrincipal(
|
|
31
|
+
row: typeof principal.$inferSelect,
|
|
32
|
+
roles: { id: string; name: string }[],
|
|
33
|
+
identity?: ResolvedIdentity,
|
|
34
|
+
) {
|
|
35
|
+
return {
|
|
36
|
+
id: row.id,
|
|
37
|
+
tenantId: row.tenantId,
|
|
38
|
+
kind: row.kind as "user" | "agent",
|
|
39
|
+
refId: row.refId,
|
|
40
|
+
displayName: identity?.displayName ?? row.refId,
|
|
41
|
+
...(identity?.email ? { email: identity.email } : {}),
|
|
42
|
+
status: row.status as "active" | "suspended" | "invited" | "deactivated",
|
|
43
|
+
roles,
|
|
44
|
+
createdAt: ts(row.createdAt),
|
|
45
|
+
updatedAt: ts(row.updatedAt),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function resolveIdentities(
|
|
50
|
+
db: DB["db"],
|
|
51
|
+
principals: (typeof principal.$inferSelect)[],
|
|
52
|
+
): Promise<Map<string, ResolvedIdentity>> {
|
|
53
|
+
const identities = new Map<string, ResolvedIdentity>();
|
|
54
|
+
|
|
55
|
+
const userRefIds = principals
|
|
56
|
+
.filter((p) => p.kind === "user")
|
|
57
|
+
.map((p) => p.refId);
|
|
58
|
+
const agentRefIds = principals
|
|
59
|
+
.filter((p) => p.kind === "agent")
|
|
60
|
+
.map((p) => p.refId);
|
|
61
|
+
|
|
62
|
+
if (userRefIds.length > 0) {
|
|
63
|
+
const users = await db.query.user.findMany({
|
|
64
|
+
where: (u, { inArray }) => inArray(u.id, userRefIds),
|
|
65
|
+
});
|
|
66
|
+
for (const u of users) {
|
|
67
|
+
identities.set(u.id, { displayName: u.name, email: u.email });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (agentRefIds.length > 0) {
|
|
72
|
+
// First pass: resolve definition principals (refId = agent.id)
|
|
73
|
+
const agents = await db.query.agent.findMany({
|
|
74
|
+
where: (a, { inArray }) => inArray(a.id, agentRefIds),
|
|
75
|
+
});
|
|
76
|
+
for (const a of agents) {
|
|
77
|
+
identities.set(a.id, { displayName: a.name });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Second pass: resolve instance principals (refId = agentInstance.id)
|
|
81
|
+
const unresolvedRefIds = agentRefIds.filter((id) => !identities.has(id));
|
|
82
|
+
if (unresolvedRefIds.length > 0) {
|
|
83
|
+
const instances = await db.query.agentInstance.findMany({
|
|
84
|
+
where: (i, { inArray }) => inArray(i.id, unresolvedRefIds),
|
|
85
|
+
});
|
|
86
|
+
const definitionIds = [...new Set(instances.map((i) => i.agentId))];
|
|
87
|
+
const definitions =
|
|
88
|
+
definitionIds.length > 0
|
|
89
|
+
? await db.query.agent.findMany({
|
|
90
|
+
where: (a, { inArray }) => inArray(a.id, definitionIds),
|
|
91
|
+
})
|
|
92
|
+
: [];
|
|
93
|
+
const defNames = new Map(definitions.map((d) => [d.id, d.name]));
|
|
94
|
+
for (const inst of instances) {
|
|
95
|
+
const name = defNames.get(inst.agentId);
|
|
96
|
+
if (name) {
|
|
97
|
+
identities.set(inst.id, { displayName: `${name} (instance)` });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return identities;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function loadRolesForPrincipal(db: DB["db"], principalId: string) {
|
|
107
|
+
const assignments = await db.query.principalRole.findMany({
|
|
108
|
+
where: eq(principalRole.principalId, principalId),
|
|
109
|
+
});
|
|
110
|
+
if (assignments.length === 0) return [];
|
|
111
|
+
|
|
112
|
+
const roleIds = assignments.map((a) => a.roleId);
|
|
113
|
+
const roles = await db.query.role.findMany({
|
|
114
|
+
where: (r, { inArray }) => inArray(r.id, roleIds),
|
|
115
|
+
});
|
|
116
|
+
return roles.map((r) => ({ id: r.id, name: r.name }));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export type CreatePrincipalRoutesDeps = {
|
|
120
|
+
db: DB["db"];
|
|
121
|
+
requireGrant: RequireGrant;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export function createPrincipalRoutes({
|
|
125
|
+
db,
|
|
126
|
+
requireGrant,
|
|
127
|
+
}: CreatePrincipalRoutesDeps): Hono<TenantEnv> {
|
|
128
|
+
const app = new Hono<TenantEnv>();
|
|
129
|
+
|
|
130
|
+
app.get(
|
|
131
|
+
"/",
|
|
132
|
+
requireGrant("principal:*", "read"),
|
|
133
|
+
describeRoute({
|
|
134
|
+
tags: ["Principals"],
|
|
135
|
+
summary: "List principals in the tenant",
|
|
136
|
+
description:
|
|
137
|
+
"Lists all principals (users and agents) in the tenant. Filterable by kind and status.",
|
|
138
|
+
parameters: [
|
|
139
|
+
{
|
|
140
|
+
name: "kind",
|
|
141
|
+
in: "query",
|
|
142
|
+
schema: { type: "string", enum: ["user", "agent"] },
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: "status",
|
|
146
|
+
in: "query",
|
|
147
|
+
schema: {
|
|
148
|
+
type: "string",
|
|
149
|
+
enum: ["active", "suspended", "invited", "deactivated"],
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
...pageParameters,
|
|
153
|
+
],
|
|
154
|
+
responses: {
|
|
155
|
+
200: {
|
|
156
|
+
description: "List of principals",
|
|
157
|
+
content: {
|
|
158
|
+
"application/json": {
|
|
159
|
+
schema: resolver(paginatedSchema(PrincipalResponse)),
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
}),
|
|
165
|
+
async (c) => {
|
|
166
|
+
const tenantCtx = c.get("tenant");
|
|
167
|
+
const kind = c.req.query("kind");
|
|
168
|
+
const status = c.req.query("status");
|
|
169
|
+
const { limit, cursor } = parsePageParams({
|
|
170
|
+
cursor: c.req.query("cursor"),
|
|
171
|
+
limit: c.req.query("limit"),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const conditions = [eq(principal.tenantId, tenantCtx.id)];
|
|
175
|
+
if (kind === "user" || kind === "agent") {
|
|
176
|
+
conditions.push(eq(principal.kind, kind));
|
|
177
|
+
}
|
|
178
|
+
if (
|
|
179
|
+
status === "active" ||
|
|
180
|
+
status === "suspended" ||
|
|
181
|
+
status === "invited" ||
|
|
182
|
+
status === "deactivated"
|
|
183
|
+
) {
|
|
184
|
+
conditions.push(eq(principal.status, status));
|
|
185
|
+
} else {
|
|
186
|
+
// Exclude deactivated principals by default
|
|
187
|
+
conditions.push(ne(principal.status, "deactivated"));
|
|
188
|
+
}
|
|
189
|
+
if (cursor) {
|
|
190
|
+
conditions.push(
|
|
191
|
+
cursorCondition(principal.createdAt, principal.id, cursor),
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const rows = await db.query.principal.findMany({
|
|
196
|
+
where: and(...conditions),
|
|
197
|
+
orderBy: pageOrder(principal.createdAt, principal.id),
|
|
198
|
+
limit,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const allAssignments =
|
|
202
|
+
rows.length > 0
|
|
203
|
+
? await db.query.principalRole.findMany({
|
|
204
|
+
where: (pr, { inArray }) =>
|
|
205
|
+
inArray(
|
|
206
|
+
pr.principalId,
|
|
207
|
+
rows.map((p) => p.id),
|
|
208
|
+
),
|
|
209
|
+
})
|
|
210
|
+
: [];
|
|
211
|
+
|
|
212
|
+
const roleIds = [...new Set(allAssignments.map((a) => a.roleId))];
|
|
213
|
+
const roles =
|
|
214
|
+
roleIds.length > 0
|
|
215
|
+
? await db.query.role.findMany({
|
|
216
|
+
where: (r, { inArray }) => inArray(r.id, roleIds),
|
|
217
|
+
})
|
|
218
|
+
: [];
|
|
219
|
+
const roleMap = new Map(roles.map((r) => [r.id, r]));
|
|
220
|
+
|
|
221
|
+
const rolesByPrincipal = new Map<
|
|
222
|
+
string,
|
|
223
|
+
{ id: string; name: string }[]
|
|
224
|
+
>();
|
|
225
|
+
for (const a of allAssignments) {
|
|
226
|
+
const r = roleMap.get(a.roleId);
|
|
227
|
+
if (!r) continue;
|
|
228
|
+
const list = rolesByPrincipal.get(a.principalId) ?? [];
|
|
229
|
+
list.push({ id: r.id, name: r.name });
|
|
230
|
+
rolesByPrincipal.set(a.principalId, list);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const identities = await resolveIdentities(db, rows);
|
|
234
|
+
|
|
235
|
+
const items = rows.map((p) =>
|
|
236
|
+
formatPrincipal(
|
|
237
|
+
p,
|
|
238
|
+
rolesByPrincipal.get(p.id) ?? [],
|
|
239
|
+
identities.get(p.refId),
|
|
240
|
+
),
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
return c.json(paginatedResponse(items, rows, limit));
|
|
244
|
+
},
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
app.get(
|
|
248
|
+
"/:principalId",
|
|
249
|
+
requireGrant(idResource("principal", "principalId"), "read"),
|
|
250
|
+
describeRoute({
|
|
251
|
+
tags: ["Principals"],
|
|
252
|
+
summary: "Get principal details",
|
|
253
|
+
description:
|
|
254
|
+
"Returns principal details including kind, status, assigned roles, and effective grants.",
|
|
255
|
+
responses: {
|
|
256
|
+
200: {
|
|
257
|
+
description: "Principal details",
|
|
258
|
+
content: {
|
|
259
|
+
"application/json": { schema: resolver(PrincipalResponse) },
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
404: {
|
|
263
|
+
description: "Principal not found",
|
|
264
|
+
content: {
|
|
265
|
+
"application/json": { schema: resolver(ErrorResponse) },
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
}),
|
|
270
|
+
async (c) => {
|
|
271
|
+
const tenantCtx = c.get("tenant");
|
|
272
|
+
const principalId = c.req.param("principalId");
|
|
273
|
+
|
|
274
|
+
const row = await db.query.principal.findFirst({
|
|
275
|
+
where: and(
|
|
276
|
+
eq(principal.id, principalId),
|
|
277
|
+
eq(principal.tenantId, tenantCtx.id),
|
|
278
|
+
),
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
if (!row) {
|
|
282
|
+
return c.json(
|
|
283
|
+
{ error: { code: "not_found", message: "Principal not found" } },
|
|
284
|
+
404,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const roles = await loadRolesForPrincipal(db, principalId);
|
|
289
|
+
const identities = await resolveIdentities(db, [row]);
|
|
290
|
+
return c.json(formatPrincipal(row, roles, identities.get(row.refId)));
|
|
291
|
+
},
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
app.patch(
|
|
295
|
+
"/:principalId",
|
|
296
|
+
requireGrant(idResource("principal", "principalId"), "manage"),
|
|
297
|
+
describeRoute({
|
|
298
|
+
tags: ["Principals"],
|
|
299
|
+
summary: "Update principal status",
|
|
300
|
+
description: "Activate, suspend, or deactivate a principal.",
|
|
301
|
+
responses: {
|
|
302
|
+
200: {
|
|
303
|
+
description: "Principal updated",
|
|
304
|
+
content: {
|
|
305
|
+
"application/json": { schema: resolver(PrincipalResponse) },
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
403: {
|
|
309
|
+
description: "Insufficient grants",
|
|
310
|
+
content: {
|
|
311
|
+
"application/json": { schema: resolver(ErrorResponse) },
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
}),
|
|
316
|
+
validator("json", UpdatePrincipal),
|
|
317
|
+
async (c) => {
|
|
318
|
+
const tenantCtx = c.get("tenant");
|
|
319
|
+
const principalId = c.req.param("principalId");
|
|
320
|
+
const body = c.req.valid("json");
|
|
321
|
+
|
|
322
|
+
const [updated] = await db
|
|
323
|
+
.update(principal)
|
|
324
|
+
.set({ status: body.status, updatedAt: new Date() })
|
|
325
|
+
.where(
|
|
326
|
+
and(
|
|
327
|
+
eq(principal.id, principalId),
|
|
328
|
+
eq(principal.tenantId, tenantCtx.id),
|
|
329
|
+
),
|
|
330
|
+
)
|
|
331
|
+
.returning();
|
|
332
|
+
|
|
333
|
+
if (!updated) {
|
|
334
|
+
return c.json(
|
|
335
|
+
{ error: { code: "not_found", message: "Principal not found" } },
|
|
336
|
+
404,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const roles = await loadRolesForPrincipal(db, principalId);
|
|
341
|
+
const identities = await resolveIdentities(db, [updated]);
|
|
342
|
+
return c.json(
|
|
343
|
+
formatPrincipal(updated, roles, identities.get(updated.refId)),
|
|
344
|
+
);
|
|
345
|
+
},
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
app.delete(
|
|
349
|
+
"/:principalId",
|
|
350
|
+
requireGrant(idResource("principal", "principalId"), "manage"),
|
|
351
|
+
describeRoute({
|
|
352
|
+
tags: ["Principals"],
|
|
353
|
+
summary: "Remove principal from tenant",
|
|
354
|
+
description:
|
|
355
|
+
"Removes a user or agent principal from the tenant. For agents, use agent deletion instead.",
|
|
356
|
+
responses: {
|
|
357
|
+
204: {
|
|
358
|
+
description: "Principal removed",
|
|
359
|
+
},
|
|
360
|
+
403: {
|
|
361
|
+
description: "Insufficient grants",
|
|
362
|
+
content: {
|
|
363
|
+
"application/json": { schema: resolver(ErrorResponse) },
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
}),
|
|
368
|
+
async (c) => {
|
|
369
|
+
const tenantCtx = c.get("tenant");
|
|
370
|
+
const principalId = c.req.param("principalId");
|
|
371
|
+
|
|
372
|
+
const deleted = await db
|
|
373
|
+
.delete(principal)
|
|
374
|
+
.where(
|
|
375
|
+
and(
|
|
376
|
+
eq(principal.id, principalId),
|
|
377
|
+
eq(principal.tenantId, tenantCtx.id),
|
|
378
|
+
),
|
|
379
|
+
)
|
|
380
|
+
.returning();
|
|
381
|
+
|
|
382
|
+
if (deleted.length === 0) {
|
|
383
|
+
return c.json(
|
|
384
|
+
{ error: { code: "not_found", message: "Principal not found" } },
|
|
385
|
+
404,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return c.body(null, 204);
|
|
390
|
+
},
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
return app;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Invite is mounted separately at ../members/invite in app.ts
|
|
397
|
+
export type CreateInviteRoutesDeps = {
|
|
398
|
+
db: DB["db"];
|
|
399
|
+
requireGrant: RequireGrant;
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
export function createInviteRoutes({
|
|
403
|
+
db,
|
|
404
|
+
requireGrant,
|
|
405
|
+
}: CreateInviteRoutesDeps): Hono<TenantEnv> {
|
|
406
|
+
const inviteApp = new Hono<TenantEnv>();
|
|
407
|
+
|
|
408
|
+
inviteApp.post(
|
|
409
|
+
"/",
|
|
410
|
+
requireGrant("principal:*", "create"),
|
|
411
|
+
describeRoute({
|
|
412
|
+
tags: ["Principals"],
|
|
413
|
+
summary: "Invite a user to the tenant",
|
|
414
|
+
description:
|
|
415
|
+
"Invites a user by email. Creates a principal with invited status and optionally assigns a role.",
|
|
416
|
+
responses: {
|
|
417
|
+
201: {
|
|
418
|
+
description: "Invitation sent",
|
|
419
|
+
content: {
|
|
420
|
+
"application/json": { schema: resolver(PrincipalResponse) },
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
400: {
|
|
424
|
+
description: "Validation error",
|
|
425
|
+
content: {
|
|
426
|
+
"application/json": { schema: resolver(ErrorResponse) },
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
}),
|
|
431
|
+
validator("json", InviteMember),
|
|
432
|
+
async (c) => {
|
|
433
|
+
const tenantCtx = c.get("tenant");
|
|
434
|
+
const body = c.req.valid("json");
|
|
435
|
+
|
|
436
|
+
const invitedUser = await db.query.user.findFirst({
|
|
437
|
+
where: eq(user.email, body.email),
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
if (!invitedUser) {
|
|
441
|
+
return c.json(
|
|
442
|
+
{
|
|
443
|
+
error: {
|
|
444
|
+
code: "not_found",
|
|
445
|
+
message: "No user found with that email",
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
404,
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const existing = await db.query.principal.findFirst({
|
|
453
|
+
where: and(
|
|
454
|
+
eq(principal.tenantId, tenantCtx.id),
|
|
455
|
+
eq(principal.kind, "user"),
|
|
456
|
+
eq(principal.refId, invitedUser.id),
|
|
457
|
+
),
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
if (existing) {
|
|
461
|
+
return c.json(
|
|
462
|
+
{
|
|
463
|
+
error: {
|
|
464
|
+
code: "conflict",
|
|
465
|
+
message: "User is already a member of this tenant",
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
409,
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const now = new Date();
|
|
473
|
+
const principalId = generateId("principal");
|
|
474
|
+
|
|
475
|
+
const row = first(
|
|
476
|
+
await db
|
|
477
|
+
.insert(principal)
|
|
478
|
+
.values({
|
|
479
|
+
id: principalId,
|
|
480
|
+
tenantId: tenantCtx.id,
|
|
481
|
+
kind: "user",
|
|
482
|
+
refId: invitedUser.id,
|
|
483
|
+
status: "invited",
|
|
484
|
+
createdAt: now,
|
|
485
|
+
updatedAt: now,
|
|
486
|
+
})
|
|
487
|
+
.returning(),
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
let roles: { id: string; name: string }[] = [];
|
|
491
|
+
|
|
492
|
+
if (body.roleId) {
|
|
493
|
+
const roleRow = await db.query.role.findFirst({
|
|
494
|
+
where: and(eq(role.id, body.roleId), eq(role.tenantId, tenantCtx.id)),
|
|
495
|
+
});
|
|
496
|
+
if (roleRow) {
|
|
497
|
+
await db.insert(principalRole).values({
|
|
498
|
+
principalId,
|
|
499
|
+
roleId: roleRow.id,
|
|
500
|
+
createdAt: now,
|
|
501
|
+
});
|
|
502
|
+
roles = [{ id: roleRow.id, name: roleRow.name }];
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const identity: ResolvedIdentity = {
|
|
507
|
+
displayName: invitedUser.name,
|
|
508
|
+
email: invitedUser.email,
|
|
509
|
+
};
|
|
510
|
+
return c.json(formatPrincipal(row, roles, identity), 201);
|
|
511
|
+
},
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
return inviteApp;
|
|
515
|
+
}
|