@fonderie/workspaces 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/index.cjs +1339 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +98 -0
- package/dist/index.d.ts +98 -0
- package/dist/index.js +1303 -0
- package/dist/index.js.map +1 -0
- package/dist/middlewares/index.cjs +141 -0
- package/dist/middlewares/index.cjs.map +1 -0
- package/dist/middlewares/index.d.cts +9 -0
- package/dist/middlewares/index.d.ts +9 -0
- package/dist/middlewares/index.js +113 -0
- package/dist/middlewares/index.js.map +1 -0
- package/dist/migrations/index.js +7 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/migrations/sql/001_workspaces.sql +85 -0
- package/dist/migrations/sql/002_personal_workspace.sql +8 -0
- package/dist/migrations/sql/003_organization_profile.sql +5 -0
- package/dist/types.cjs +19 -0
- package/dist/types.cjs.map +1 -0
- package/dist/types.d.cts +69 -0
- package/dist/types.d.ts +69 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/package.json +92 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1303 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
var MESSAGE_KEYS = {
|
|
3
|
+
workspaceInvitation: "workspace-invitation"
|
|
4
|
+
};
|
|
5
|
+
var EVENT_KEYS = {
|
|
6
|
+
personalWorkspaceCreated: "fonderie.workspace.personal.created"
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// src/routes.ts
|
|
10
|
+
import { requireAuth } from "@fonderie/core/middlewares";
|
|
11
|
+
|
|
12
|
+
// src/middlewares/workspace-context.ts
|
|
13
|
+
import { setApiResponse, HTTP } from "@fonderie/core";
|
|
14
|
+
|
|
15
|
+
// src/services/members.ts
|
|
16
|
+
var SELECT_MEMBER = `
|
|
17
|
+
ruw.user_id AS "userId",
|
|
18
|
+
ruw.workspace_id AS "workspaceId",
|
|
19
|
+
ruw.role_id AS "roleId",
|
|
20
|
+
r.name AS "roleName",
|
|
21
|
+
ruw.confirmed AS "confirmed",
|
|
22
|
+
ruw.created_at AS "createdAt",
|
|
23
|
+
u.first_name AS "firstName",
|
|
24
|
+
u.last_name AS "lastName",
|
|
25
|
+
u.email AS "email",
|
|
26
|
+
u.profile_image_url AS "profileImageUrl"
|
|
27
|
+
`;
|
|
28
|
+
async function getMember(userId, workspaceId, store) {
|
|
29
|
+
const [row] = await store.query(
|
|
30
|
+
`SELECT ${SELECT_MEMBER}
|
|
31
|
+
FROM fonderie_role_user_workspaces ruw
|
|
32
|
+
LEFT JOIN fonderie_roles r ON r.id = ruw.role_id
|
|
33
|
+
LEFT JOIN fonderie_users u ON u.id = ruw.user_id
|
|
34
|
+
WHERE ruw.user_id = $1
|
|
35
|
+
AND ruw.workspace_id = $2
|
|
36
|
+
AND ruw.removed = false
|
|
37
|
+
AND ruw.suspended = false
|
|
38
|
+
LIMIT 1`,
|
|
39
|
+
[userId, workspaceId]
|
|
40
|
+
);
|
|
41
|
+
return row ?? null;
|
|
42
|
+
}
|
|
43
|
+
async function listMembers(workspaceId, store) {
|
|
44
|
+
return store.query(
|
|
45
|
+
`SELECT ${SELECT_MEMBER}
|
|
46
|
+
FROM fonderie_role_user_workspaces ruw
|
|
47
|
+
LEFT JOIN fonderie_roles r ON r.id = ruw.role_id
|
|
48
|
+
LEFT JOIN fonderie_users u ON u.id = ruw.user_id
|
|
49
|
+
WHERE ruw.workspace_id = $1
|
|
50
|
+
AND ruw.removed = false
|
|
51
|
+
AND ruw.suspended = false
|
|
52
|
+
ORDER BY ruw.created_at ASC`,
|
|
53
|
+
[workspaceId]
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
async function addMember(opts, store) {
|
|
57
|
+
await store.query(
|
|
58
|
+
`INSERT INTO fonderie_role_user_workspaces (user_id, workspace_id, role_id, confirmed)
|
|
59
|
+
VALUES ($1, $2, $3, $4)
|
|
60
|
+
ON CONFLICT (user_id, workspace_id, role_id) DO UPDATE
|
|
61
|
+
SET confirmed = $4, removed = false, suspended = false`,
|
|
62
|
+
[opts.userId, opts.workspaceId, opts.roleId, opts.confirmed ?? true]
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
async function removeMember(userId, workspaceId, store) {
|
|
66
|
+
await store.query(
|
|
67
|
+
`UPDATE fonderie_role_user_workspaces
|
|
68
|
+
SET removed = true
|
|
69
|
+
WHERE user_id = $1
|
|
70
|
+
AND workspace_id = $2`,
|
|
71
|
+
[userId, workspaceId]
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
async function getUserRoles(userId, workspaceId, store) {
|
|
75
|
+
return store.query(
|
|
76
|
+
`SELECT
|
|
77
|
+
r.id,
|
|
78
|
+
r.name,
|
|
79
|
+
r.is_system AS "isSystem",
|
|
80
|
+
r.active,
|
|
81
|
+
r.description,
|
|
82
|
+
r.workspace_id AS "workspaceId"
|
|
83
|
+
FROM fonderie_role_user_workspaces ruw
|
|
84
|
+
JOIN fonderie_roles r ON r.id = ruw.role_id
|
|
85
|
+
WHERE ruw.user_id = $1
|
|
86
|
+
AND ruw.workspace_id = $2
|
|
87
|
+
AND ruw.removed = false
|
|
88
|
+
AND ruw.suspended = false`,
|
|
89
|
+
[userId, workspaceId]
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
async function addRoleToMember(userId, workspaceId, roleId, store) {
|
|
93
|
+
await store.query(
|
|
94
|
+
`INSERT INTO fonderie_role_user_workspaces (user_id, workspace_id, role_id, confirmed)
|
|
95
|
+
VALUES ($1, $2, $3, true)
|
|
96
|
+
ON CONFLICT (user_id, workspace_id, role_id) DO UPDATE
|
|
97
|
+
SET confirmed = true, removed = false, suspended = false`,
|
|
98
|
+
[userId, workspaceId, roleId]
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
async function removeRoleFromMember(userId, workspaceId, roleId, store) {
|
|
102
|
+
const remaining = await store.query(
|
|
103
|
+
`SELECT COUNT(*) AS count
|
|
104
|
+
FROM fonderie_role_user_workspaces
|
|
105
|
+
WHERE user_id = $1
|
|
106
|
+
AND workspace_id = $2
|
|
107
|
+
AND removed = false`,
|
|
108
|
+
[userId, workspaceId]
|
|
109
|
+
);
|
|
110
|
+
const count = parseInt(remaining[0]?.count ?? "0", 10);
|
|
111
|
+
if (count <= 1) throw new Error("Cannot remove last role from member");
|
|
112
|
+
await store.query(
|
|
113
|
+
`DELETE FROM fonderie_role_user_workspaces
|
|
114
|
+
WHERE user_id = $1
|
|
115
|
+
AND workspace_id = $2
|
|
116
|
+
AND role_id = $3`,
|
|
117
|
+
[userId, workspaceId, roleId]
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/services/workspaces.ts
|
|
122
|
+
var SELECT_WS = `
|
|
123
|
+
id,
|
|
124
|
+
name,
|
|
125
|
+
slug,
|
|
126
|
+
type,
|
|
127
|
+
description,
|
|
128
|
+
motto,
|
|
129
|
+
phone,
|
|
130
|
+
business_type AS "businessType",
|
|
131
|
+
address,
|
|
132
|
+
plan,
|
|
133
|
+
owner_id AS "ownerId",
|
|
134
|
+
is_personal AS "isPersonal",
|
|
135
|
+
archived_at AS "archivedAt",
|
|
136
|
+
archived_by AS "archivedBy",
|
|
137
|
+
created_at AS "createdAt",
|
|
138
|
+
updated_at AS "updatedAt"
|
|
139
|
+
`;
|
|
140
|
+
var SELECT_WS_W = `
|
|
141
|
+
w.id,
|
|
142
|
+
w.name,
|
|
143
|
+
w.slug,
|
|
144
|
+
w.type,
|
|
145
|
+
w.description,
|
|
146
|
+
w.motto,
|
|
147
|
+
w.phone,
|
|
148
|
+
w.business_type AS "businessType",
|
|
149
|
+
w.address,
|
|
150
|
+
w.plan,
|
|
151
|
+
w.owner_id AS "ownerId",
|
|
152
|
+
w.is_personal AS "isPersonal",
|
|
153
|
+
w.archived_at AS "archivedAt",
|
|
154
|
+
w.archived_by AS "archivedBy",
|
|
155
|
+
w.created_at AS "createdAt",
|
|
156
|
+
w.updated_at AS "updatedAt"
|
|
157
|
+
`;
|
|
158
|
+
async function findWorkspaceById(id, store) {
|
|
159
|
+
const [row] = await store.query(
|
|
160
|
+
`SELECT ${SELECT_WS} FROM fonderie_workspaces WHERE id = $1`,
|
|
161
|
+
[id]
|
|
162
|
+
);
|
|
163
|
+
return row ?? null;
|
|
164
|
+
}
|
|
165
|
+
async function findWorkspacesByUserId(userId, store) {
|
|
166
|
+
return store.query(
|
|
167
|
+
`SELECT ${SELECT_WS_W}
|
|
168
|
+
FROM fonderie_workspaces w
|
|
169
|
+
JOIN fonderie_role_user_workspaces ruw ON ruw.workspace_id = w.id
|
|
170
|
+
WHERE ruw.user_id = $1
|
|
171
|
+
AND ruw.removed = false
|
|
172
|
+
AND ruw.suspended = false
|
|
173
|
+
GROUP BY w.id
|
|
174
|
+
ORDER BY w.created_at ASC`,
|
|
175
|
+
[userId]
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
async function createWorkspace(opts, store) {
|
|
179
|
+
const [workspace] = await store.query(
|
|
180
|
+
`INSERT INTO fonderie_workspaces (name, slug, owner_id, type, description, plan)
|
|
181
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
|
182
|
+
RETURNING ${SELECT_WS}`,
|
|
183
|
+
[
|
|
184
|
+
opts.name,
|
|
185
|
+
opts.slug,
|
|
186
|
+
opts.ownerId,
|
|
187
|
+
opts.type ?? "ORGANIZATION",
|
|
188
|
+
opts.description ?? null,
|
|
189
|
+
opts.plan ?? "free"
|
|
190
|
+
]
|
|
191
|
+
);
|
|
192
|
+
if (!workspace) throw new Error("Failed to create workspace");
|
|
193
|
+
return workspace;
|
|
194
|
+
}
|
|
195
|
+
async function createPersonalWorkspace(opts, store) {
|
|
196
|
+
const [workspace] = await store.query(
|
|
197
|
+
`INSERT INTO fonderie_workspaces (name, slug, owner_id, type, is_personal)
|
|
198
|
+
VALUES ($1, $2, $3, 'PERSONAL', true)
|
|
199
|
+
ON CONFLICT (owner_id) WHERE is_personal = true DO NOTHING
|
|
200
|
+
RETURNING ${SELECT_WS}`,
|
|
201
|
+
[opts.name, opts.slug, opts.ownerId]
|
|
202
|
+
);
|
|
203
|
+
return workspace ?? null;
|
|
204
|
+
}
|
|
205
|
+
async function findPersonalWorkspace(userId, store) {
|
|
206
|
+
const [row] = await store.query(
|
|
207
|
+
`SELECT ${SELECT_WS} FROM fonderie_workspaces
|
|
208
|
+
WHERE owner_id = $1 AND is_personal = true
|
|
209
|
+
LIMIT 1`,
|
|
210
|
+
[userId]
|
|
211
|
+
);
|
|
212
|
+
return row ?? null;
|
|
213
|
+
}
|
|
214
|
+
async function updateWorkspace(id, opts, store) {
|
|
215
|
+
const sets = ["updated_at = now()"];
|
|
216
|
+
const params = [id];
|
|
217
|
+
if (opts.name !== void 0) {
|
|
218
|
+
params.push(opts.name);
|
|
219
|
+
sets.push(`name = $${params.length}`);
|
|
220
|
+
}
|
|
221
|
+
if (opts.description !== void 0) {
|
|
222
|
+
params.push(opts.description);
|
|
223
|
+
sets.push(`description = $${params.length}`);
|
|
224
|
+
}
|
|
225
|
+
if (opts.slug !== void 0) {
|
|
226
|
+
params.push(opts.slug);
|
|
227
|
+
sets.push(`slug = $${params.length}`);
|
|
228
|
+
}
|
|
229
|
+
if (opts.motto !== void 0) {
|
|
230
|
+
params.push(opts.motto);
|
|
231
|
+
sets.push(`motto = $${params.length}`);
|
|
232
|
+
}
|
|
233
|
+
if (opts.phone !== void 0) {
|
|
234
|
+
params.push(opts.phone);
|
|
235
|
+
sets.push(`phone = $${params.length}`);
|
|
236
|
+
}
|
|
237
|
+
if (opts.businessType !== void 0) {
|
|
238
|
+
params.push(opts.businessType);
|
|
239
|
+
sets.push(`business_type = $${params.length}`);
|
|
240
|
+
}
|
|
241
|
+
if (opts.address !== void 0) {
|
|
242
|
+
params.push(JSON.stringify(opts.address ?? {}));
|
|
243
|
+
sets.push(`address = $${params.length}::jsonb`);
|
|
244
|
+
}
|
|
245
|
+
const [row] = await store.query(
|
|
246
|
+
`UPDATE fonderie_workspaces
|
|
247
|
+
SET ${sets.join(", ")}
|
|
248
|
+
WHERE id = $1
|
|
249
|
+
RETURNING ${SELECT_WS}`,
|
|
250
|
+
params
|
|
251
|
+
);
|
|
252
|
+
return row ?? null;
|
|
253
|
+
}
|
|
254
|
+
async function archiveWorkspace(id, byUser, store) {
|
|
255
|
+
await store.query(
|
|
256
|
+
`UPDATE fonderie_workspaces
|
|
257
|
+
SET archived_at = now(), archived_by = $2, updated_at = now()
|
|
258
|
+
WHERE id = $1 AND archived_at IS NULL`,
|
|
259
|
+
[id, byUser]
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
async function restoreWorkspace(id, store) {
|
|
263
|
+
await store.query(
|
|
264
|
+
`UPDATE fonderie_workspaces
|
|
265
|
+
SET archived_at = NULL, archived_by = NULL, updated_at = now()
|
|
266
|
+
WHERE id = $1`,
|
|
267
|
+
[id]
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
var SETTINGS_DEFAULTS = {
|
|
271
|
+
locale: "en-US",
|
|
272
|
+
timezone: "UTC",
|
|
273
|
+
currency: "USD",
|
|
274
|
+
dateFormat: "MM/DD/YYYY",
|
|
275
|
+
timeFormat: "hh:mm A"
|
|
276
|
+
};
|
|
277
|
+
async function getWorkspaceSettings(id, store) {
|
|
278
|
+
const [row] = await store.query(
|
|
279
|
+
`SELECT settings FROM fonderie_workspaces WHERE id = $1`,
|
|
280
|
+
[id]
|
|
281
|
+
);
|
|
282
|
+
const raw = row?.settings ?? {};
|
|
283
|
+
const s = raw["settings"] ?? raw;
|
|
284
|
+
return {
|
|
285
|
+
locale: typeof s["locale"] === "string" ? s["locale"] : SETTINGS_DEFAULTS.locale,
|
|
286
|
+
timezone: typeof s["timezone"] === "string" ? s["timezone"] : SETTINGS_DEFAULTS.timezone,
|
|
287
|
+
currency: typeof s["currency"] === "string" ? s["currency"] : SETTINGS_DEFAULTS.currency,
|
|
288
|
+
dateFormat: typeof s["dateFormat"] === "string" ? s["dateFormat"] : SETTINGS_DEFAULTS.dateFormat,
|
|
289
|
+
timeFormat: typeof s["timeFormat"] === "string" ? s["timeFormat"] : SETTINGS_DEFAULTS.timeFormat
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
async function updateWorkspaceSettings(id, settings, store) {
|
|
293
|
+
await store.query(
|
|
294
|
+
`UPDATE fonderie_workspaces
|
|
295
|
+
SET settings = settings || jsonb_build_object('settings', $2::jsonb),
|
|
296
|
+
updated_at = now()
|
|
297
|
+
WHERE id = $1`,
|
|
298
|
+
[id, JSON.stringify(settings)]
|
|
299
|
+
);
|
|
300
|
+
return getWorkspaceSettings(id, store);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/middlewares/workspace-context.ts
|
|
304
|
+
function makeHandler(store) {
|
|
305
|
+
return async (ctx, next) => {
|
|
306
|
+
const params = ctx.meta["params"];
|
|
307
|
+
const workspaceId = params?.["workspaceId"] ?? params?.["id"] ?? ctx.request.headers.get("x-workspace-id") ?? void 0;
|
|
308
|
+
if (!workspaceId) {
|
|
309
|
+
if (ctx.user) {
|
|
310
|
+
const personal = await findPersonalWorkspace(ctx.user.id, store);
|
|
311
|
+
if (personal) Object.assign(ctx, { workspace: personal });
|
|
312
|
+
}
|
|
313
|
+
return next();
|
|
314
|
+
}
|
|
315
|
+
const workspace = await findWorkspaceById(workspaceId, store);
|
|
316
|
+
if (!workspace) {
|
|
317
|
+
return setApiResponse(HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
|
|
318
|
+
}
|
|
319
|
+
if (ctx.user) {
|
|
320
|
+
const member = await getMember(ctx.user.id, workspaceId, store);
|
|
321
|
+
if (!member) {
|
|
322
|
+
return setApiResponse(HTTP.FORBIDDEN, "FORBIDDEN", "Not a member of this workspace");
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
Object.assign(ctx, { workspace });
|
|
326
|
+
return next();
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function withWorkspace(store, ctx, next) {
|
|
330
|
+
const handler = makeHandler(store);
|
|
331
|
+
if (ctx !== void 0 && next !== void 0) return handler(ctx, next);
|
|
332
|
+
return handler;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/controllers/workspace.controller.ts
|
|
336
|
+
import { setApiResponse as setApiResponse2, HTTP as HTTP2 } from "@fonderie/core";
|
|
337
|
+
|
|
338
|
+
// src/models/workspace.model.ts
|
|
339
|
+
var WorkspaceModel = class {
|
|
340
|
+
constructor(store) {
|
|
341
|
+
this.store = store;
|
|
342
|
+
}
|
|
343
|
+
store;
|
|
344
|
+
findById(id) {
|
|
345
|
+
return findWorkspaceById(id, this.store);
|
|
346
|
+
}
|
|
347
|
+
findByUserId(userId) {
|
|
348
|
+
return findWorkspacesByUserId(userId, this.store);
|
|
349
|
+
}
|
|
350
|
+
create(opts) {
|
|
351
|
+
return createWorkspace(opts, this.store);
|
|
352
|
+
}
|
|
353
|
+
createPersonal(opts) {
|
|
354
|
+
return createPersonalWorkspace(opts, this.store);
|
|
355
|
+
}
|
|
356
|
+
findPersonal(userId) {
|
|
357
|
+
return findPersonalWorkspace(userId, this.store);
|
|
358
|
+
}
|
|
359
|
+
update(id, opts) {
|
|
360
|
+
return updateWorkspace(id, opts, this.store);
|
|
361
|
+
}
|
|
362
|
+
archive(id, byUser) {
|
|
363
|
+
return archiveWorkspace(id, byUser, this.store);
|
|
364
|
+
}
|
|
365
|
+
restore(id) {
|
|
366
|
+
return restoreWorkspace(id, this.store);
|
|
367
|
+
}
|
|
368
|
+
getSettings(id) {
|
|
369
|
+
return getWorkspaceSettings(id, this.store);
|
|
370
|
+
}
|
|
371
|
+
updateSettings(id, settings) {
|
|
372
|
+
return updateWorkspaceSettings(id, settings, this.store);
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
// src/models/member.model.ts
|
|
377
|
+
var MemberModel = class {
|
|
378
|
+
constructor(store) {
|
|
379
|
+
this.store = store;
|
|
380
|
+
}
|
|
381
|
+
store;
|
|
382
|
+
get(userId, workspaceId) {
|
|
383
|
+
return getMember(userId, workspaceId, this.store);
|
|
384
|
+
}
|
|
385
|
+
list(workspaceId) {
|
|
386
|
+
return listMembers(workspaceId, this.store);
|
|
387
|
+
}
|
|
388
|
+
add(opts) {
|
|
389
|
+
return addMember(opts, this.store);
|
|
390
|
+
}
|
|
391
|
+
remove(userId, workspaceId) {
|
|
392
|
+
return removeMember(userId, workspaceId, this.store);
|
|
393
|
+
}
|
|
394
|
+
getUserRoles(userId, workspaceId) {
|
|
395
|
+
return getUserRoles(userId, workspaceId, this.store);
|
|
396
|
+
}
|
|
397
|
+
addRole(userId, workspaceId, roleId) {
|
|
398
|
+
return addRoleToMember(userId, workspaceId, roleId, this.store);
|
|
399
|
+
}
|
|
400
|
+
removeRole(userId, workspaceId, roleId) {
|
|
401
|
+
return removeRoleFromMember(userId, workspaceId, roleId, this.store);
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
// src/services/roles.ts
|
|
406
|
+
var SELECT_ROLE = `
|
|
407
|
+
id,
|
|
408
|
+
name,
|
|
409
|
+
is_system AS "isSystem",
|
|
410
|
+
active,
|
|
411
|
+
description,
|
|
412
|
+
workspace_id AS "workspaceId"
|
|
413
|
+
`;
|
|
414
|
+
async function createRole(opts, store) {
|
|
415
|
+
const [role] = await store.query(
|
|
416
|
+
`INSERT INTO fonderie_roles (name, workspace_id, description)
|
|
417
|
+
VALUES ($1, $2, $3)
|
|
418
|
+
RETURNING ${SELECT_ROLE}`,
|
|
419
|
+
[opts.name, opts.workspaceId, opts.description ?? null]
|
|
420
|
+
);
|
|
421
|
+
if (!role) throw new Error("Failed to create role");
|
|
422
|
+
return role;
|
|
423
|
+
}
|
|
424
|
+
async function findSystemRole(name, store) {
|
|
425
|
+
const [row] = await store.query(
|
|
426
|
+
`SELECT ${SELECT_ROLE} FROM fonderie_roles WHERE name = $1 AND is_system = true LIMIT 1`,
|
|
427
|
+
[name]
|
|
428
|
+
);
|
|
429
|
+
return row ?? null;
|
|
430
|
+
}
|
|
431
|
+
async function getRoleById(id, store) {
|
|
432
|
+
const [row] = await store.query(
|
|
433
|
+
`SELECT ${SELECT_ROLE} FROM fonderie_roles WHERE id = $1`,
|
|
434
|
+
[id]
|
|
435
|
+
);
|
|
436
|
+
return row ?? null;
|
|
437
|
+
}
|
|
438
|
+
async function listWorkspaceRoles(workspaceId, store) {
|
|
439
|
+
return store.query(
|
|
440
|
+
`SELECT ${SELECT_ROLE}
|
|
441
|
+
FROM fonderie_roles
|
|
442
|
+
WHERE workspace_id = $1 OR is_system = true
|
|
443
|
+
ORDER BY is_system DESC, name ASC`,
|
|
444
|
+
[workspaceId]
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
async function updateRole(id, opts, store) {
|
|
448
|
+
const sets = [];
|
|
449
|
+
const params = [id];
|
|
450
|
+
if (opts.name !== void 0) {
|
|
451
|
+
params.push(opts.name);
|
|
452
|
+
sets.push(`name = $${params.length}`);
|
|
453
|
+
}
|
|
454
|
+
if (opts.description !== void 0) {
|
|
455
|
+
params.push(opts.description);
|
|
456
|
+
sets.push(`description = $${params.length}`);
|
|
457
|
+
}
|
|
458
|
+
if (opts.active !== void 0) {
|
|
459
|
+
params.push(opts.active);
|
|
460
|
+
sets.push(`active = $${params.length}`);
|
|
461
|
+
}
|
|
462
|
+
if (sets.length === 0) return getRoleById(id, store);
|
|
463
|
+
const [row] = await store.query(
|
|
464
|
+
`UPDATE fonderie_roles
|
|
465
|
+
SET ${sets.join(", ")}
|
|
466
|
+
WHERE id = $1 AND is_system = false
|
|
467
|
+
RETURNING ${SELECT_ROLE}`,
|
|
468
|
+
params
|
|
469
|
+
);
|
|
470
|
+
return row ?? null;
|
|
471
|
+
}
|
|
472
|
+
async function deleteRole(id, workspaceId, store) {
|
|
473
|
+
await store.query(
|
|
474
|
+
`DELETE FROM fonderie_roles
|
|
475
|
+
WHERE id = $1 AND workspace_id = $2 AND is_system = false`,
|
|
476
|
+
[id, workspaceId]
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
async function setRolePermissions(roleId, workspaceId, permissions, store) {
|
|
480
|
+
if (permissions.length === 0) {
|
|
481
|
+
await store.query(
|
|
482
|
+
`DELETE FROM fonderie_role_permissions WHERE role_id = $1 AND workspace_id = $2`,
|
|
483
|
+
[roleId, workspaceId]
|
|
484
|
+
);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
await store.transaction(async (tx) => {
|
|
488
|
+
await tx.query(
|
|
489
|
+
`DELETE FROM fonderie_role_permissions WHERE role_id = $1 AND workspace_id = $2`,
|
|
490
|
+
[roleId, workspaceId]
|
|
491
|
+
);
|
|
492
|
+
for (const p of permissions) {
|
|
493
|
+
await tx.query(
|
|
494
|
+
`INSERT INTO fonderie_role_permissions
|
|
495
|
+
(role_id, workspace_id, permission_key, can_create, can_read, can_update, can_delete)
|
|
496
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
497
|
+
ON CONFLICT (role_id, permission_key)
|
|
498
|
+
DO UPDATE SET
|
|
499
|
+
can_create = $4, can_read = $5,
|
|
500
|
+
can_update = $6, can_delete = $7`,
|
|
501
|
+
[roleId, workspaceId, p.permissionKey, p.canCreate, p.canRead, p.canUpdate, p.canDelete]
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/models/role.model.ts
|
|
508
|
+
var RoleModel = class {
|
|
509
|
+
constructor(store) {
|
|
510
|
+
this.store = store;
|
|
511
|
+
}
|
|
512
|
+
store;
|
|
513
|
+
create(opts) {
|
|
514
|
+
return createRole(opts, this.store);
|
|
515
|
+
}
|
|
516
|
+
findSystem(name) {
|
|
517
|
+
return findSystemRole(name, this.store);
|
|
518
|
+
}
|
|
519
|
+
findById(id) {
|
|
520
|
+
return getRoleById(id, this.store);
|
|
521
|
+
}
|
|
522
|
+
list(workspaceId) {
|
|
523
|
+
return listWorkspaceRoles(workspaceId, this.store);
|
|
524
|
+
}
|
|
525
|
+
update(id, opts) {
|
|
526
|
+
return updateRole(id, opts, this.store);
|
|
527
|
+
}
|
|
528
|
+
delete(id, workspaceId) {
|
|
529
|
+
return deleteRole(id, workspaceId, this.store);
|
|
530
|
+
}
|
|
531
|
+
setPermissions(roleId, workspaceId, permissions) {
|
|
532
|
+
return setRolePermissions(roleId, workspaceId, permissions, this.store);
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
// src/dtos/workspace.ts
|
|
537
|
+
import { stringOrEmpty, booleanOrFalse } from "@fonderie/core/parser";
|
|
538
|
+
function toWorkspaceDTO(ws) {
|
|
539
|
+
const addr = ws.address ?? {};
|
|
540
|
+
return {
|
|
541
|
+
id: stringOrEmpty(ws.id),
|
|
542
|
+
name: stringOrEmpty(ws.name),
|
|
543
|
+
slug: stringOrEmpty(ws.slug),
|
|
544
|
+
type: stringOrEmpty(ws.type),
|
|
545
|
+
description: stringOrEmpty(ws.description),
|
|
546
|
+
motto: stringOrEmpty(ws.motto),
|
|
547
|
+
phone: stringOrEmpty(ws.phone),
|
|
548
|
+
businessType: stringOrEmpty(ws.businessType),
|
|
549
|
+
address: {
|
|
550
|
+
line1: stringOrEmpty(addr.line1),
|
|
551
|
+
line2: stringOrEmpty(addr.line2),
|
|
552
|
+
city: stringOrEmpty(addr.city),
|
|
553
|
+
state: stringOrEmpty(addr.state),
|
|
554
|
+
zip: stringOrEmpty(addr.zip),
|
|
555
|
+
country: stringOrEmpty(addr.country)
|
|
556
|
+
},
|
|
557
|
+
plan: stringOrEmpty(ws.plan),
|
|
558
|
+
ownerId: stringOrEmpty(ws.ownerId),
|
|
559
|
+
isPersonal: booleanOrFalse(ws.isPersonal),
|
|
560
|
+
isArchived: ws.archivedAt !== null,
|
|
561
|
+
archivedAt: stringOrEmpty(ws.archivedAt),
|
|
562
|
+
createdAt: stringOrEmpty(ws.createdAt),
|
|
563
|
+
updatedAt: stringOrEmpty(ws.updatedAt)
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
function toRoleDTO(role) {
|
|
567
|
+
return {
|
|
568
|
+
id: stringOrEmpty(role.id),
|
|
569
|
+
name: stringOrEmpty(role.name),
|
|
570
|
+
isSystem: booleanOrFalse(role.isSystem),
|
|
571
|
+
active: role.active !== false,
|
|
572
|
+
description: stringOrEmpty(role.description),
|
|
573
|
+
workspaceId: stringOrEmpty(role.workspaceId)
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
function toMemberDTO(m) {
|
|
577
|
+
return {
|
|
578
|
+
userId: stringOrEmpty(m.userId),
|
|
579
|
+
workspaceId: stringOrEmpty(m.workspaceId),
|
|
580
|
+
roleId: stringOrEmpty(m.roleId),
|
|
581
|
+
roleName: stringOrEmpty(m.roleName),
|
|
582
|
+
confirmed: booleanOrFalse(m.confirmed),
|
|
583
|
+
createdAt: stringOrEmpty(m.createdAt)
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
function toInvitationDTO(inv) {
|
|
587
|
+
return {
|
|
588
|
+
id: stringOrEmpty(inv.id),
|
|
589
|
+
workspaceId: stringOrEmpty(inv.workspaceId),
|
|
590
|
+
email: stringOrEmpty(inv.email),
|
|
591
|
+
roleId: stringOrEmpty(inv.roleId),
|
|
592
|
+
token: stringOrEmpty(inv.token),
|
|
593
|
+
status: stringOrEmpty(inv.status),
|
|
594
|
+
expiresAt: stringOrEmpty(inv.expiresAt),
|
|
595
|
+
createdAt: stringOrEmpty(inv.createdAt)
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
function toSettingsDTO(s) {
|
|
599
|
+
return {
|
|
600
|
+
locale: stringOrEmpty(s.locale),
|
|
601
|
+
timezone: stringOrEmpty(s.timezone),
|
|
602
|
+
currency: stringOrEmpty(s.currency),
|
|
603
|
+
dateFormat: stringOrEmpty(s.dateFormat),
|
|
604
|
+
timeFormat: stringOrEmpty(s.timeFormat)
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// src/controllers/workspace.controller.ts
|
|
609
|
+
function workspaceController(store, config) {
|
|
610
|
+
const workspaces = new WorkspaceModel(store);
|
|
611
|
+
const members = new MemberModel(store);
|
|
612
|
+
const roles = new RoleModel(store);
|
|
613
|
+
return {
|
|
614
|
+
async list(ctx) {
|
|
615
|
+
if (!ctx.user)
|
|
616
|
+
return setApiResponse2(HTTP2.UNAUTHORIZED, "UNAUTHORIZED", "Authentication required");
|
|
617
|
+
const list = await workspaces.findByUserId(ctx.user.id);
|
|
618
|
+
return setApiResponse2(HTTP2.OK, "WORKSPACES_FETCHED", "Workspaces retrieved successfully.", {
|
|
619
|
+
workspaces: list.map(toWorkspaceDTO)
|
|
620
|
+
});
|
|
621
|
+
},
|
|
622
|
+
async create(ctx) {
|
|
623
|
+
const body = ctx.meta["body"];
|
|
624
|
+
const name = body?.["name"];
|
|
625
|
+
const description = body?.["description"];
|
|
626
|
+
const type = body?.["type"];
|
|
627
|
+
if (typeof name !== "string" || name.trim().length === 0) {
|
|
628
|
+
return setApiResponse2(HTTP2.UNPROCESSABLE, "INVALID_PARAMETER", "name is required");
|
|
629
|
+
}
|
|
630
|
+
if (type === "PERSONAL") {
|
|
631
|
+
return setApiResponse2(
|
|
632
|
+
HTTP2.UNPROCESSABLE,
|
|
633
|
+
"INVALID_PARAMETER",
|
|
634
|
+
"Personal workspaces are created automatically"
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
638
|
+
const workspace = await store.transaction(async (tx) => {
|
|
639
|
+
const wsOpts = {
|
|
640
|
+
name: name.trim(),
|
|
641
|
+
slug,
|
|
642
|
+
ownerId: ctx.user.id,
|
|
643
|
+
type: typeof type === "string" ? type : "ORGANIZATION"
|
|
644
|
+
};
|
|
645
|
+
if (typeof description === "string") wsOpts.description = description;
|
|
646
|
+
const wsModel = new WorkspaceModel(tx);
|
|
647
|
+
const roleModel = new RoleModel(tx);
|
|
648
|
+
const memModel = new MemberModel(tx);
|
|
649
|
+
const ws = await wsModel.create(wsOpts);
|
|
650
|
+
const adminRole = await roleModel.findSystem("ADMIN");
|
|
651
|
+
if (!adminRole) throw new Error("System ADMIN role not found");
|
|
652
|
+
await memModel.add({ userId: ctx.user.id, workspaceId: ws.id, roleId: adminRole.id });
|
|
653
|
+
return ws;
|
|
654
|
+
});
|
|
655
|
+
return setApiResponse2(HTTP2.CREATED, "WORKSPACE_CREATED", "Workspace created successfully.", {
|
|
656
|
+
workspace: toWorkspaceDTO(workspace)
|
|
657
|
+
});
|
|
658
|
+
},
|
|
659
|
+
async get(ctx) {
|
|
660
|
+
if (!ctx.workspace) return setApiResponse2(HTTP2.NOT_FOUND, "NOT_FOUND", "Workspace not found");
|
|
661
|
+
return setApiResponse2(HTTP2.OK, "WORKSPACE_FETCHED", "Workspace retrieved successfully.", {
|
|
662
|
+
workspace: toWorkspaceDTO(ctx.workspace)
|
|
663
|
+
});
|
|
664
|
+
},
|
|
665
|
+
async update(ctx) {
|
|
666
|
+
if (!ctx.workspace) return setApiResponse2(HTTP2.NOT_FOUND, "NOT_FOUND", "Workspace not found");
|
|
667
|
+
const body = ctx.meta["body"];
|
|
668
|
+
const opts = {};
|
|
669
|
+
if (typeof body?.["name"] === "string") opts.name = body["name"].trim();
|
|
670
|
+
if (body?.["description"] !== void 0)
|
|
671
|
+
opts.description = typeof body["description"] === "string" ? body["description"] : null;
|
|
672
|
+
if (body?.["motto"] !== void 0)
|
|
673
|
+
opts.motto = typeof body["motto"] === "string" ? body["motto"] : null;
|
|
674
|
+
if (body?.["phone"] !== void 0)
|
|
675
|
+
opts.phone = typeof body["phone"] === "string" ? body["phone"].trim() : null;
|
|
676
|
+
if (body?.["businessType"] !== void 0)
|
|
677
|
+
opts.businessType = typeof body["businessType"] === "string" ? body["businessType"] : null;
|
|
678
|
+
if (body?.["address"] !== void 0 && typeof body["address"] === "object")
|
|
679
|
+
opts.address = body["address"];
|
|
680
|
+
const workspace = await workspaces.update(ctx.workspace.id, opts);
|
|
681
|
+
if (!workspace) return setApiResponse2(HTTP2.NOT_FOUND, "NOT_FOUND", "Workspace not found");
|
|
682
|
+
return setApiResponse2(HTTP2.OK, "WORKSPACE_UPDATED", "Workspace updated successfully.", {
|
|
683
|
+
workspace: toWorkspaceDTO(workspace)
|
|
684
|
+
});
|
|
685
|
+
},
|
|
686
|
+
async archive(ctx) {
|
|
687
|
+
if (!ctx.workspace) return setApiResponse2(HTTP2.NOT_FOUND, "NOT_FOUND", "Workspace not found");
|
|
688
|
+
if (ctx.workspace.isPersonal) {
|
|
689
|
+
return setApiResponse2(
|
|
690
|
+
HTTP2.FORBIDDEN,
|
|
691
|
+
"FORBIDDEN",
|
|
692
|
+
"Personal workspaces cannot be archived"
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
await workspaces.archive(ctx.workspace.id, ctx.user.id);
|
|
696
|
+
return setApiResponse2(HTTP2.OK, "WORKSPACE_ARCHIVED", "Workspace archived successfully.");
|
|
697
|
+
},
|
|
698
|
+
async restore(ctx) {
|
|
699
|
+
if (!ctx.workspace) return setApiResponse2(HTTP2.NOT_FOUND, "NOT_FOUND", "Workspace not found");
|
|
700
|
+
await workspaces.restore(ctx.workspace.id);
|
|
701
|
+
return setApiResponse2(HTTP2.OK, "WORKSPACE_RESTORED", "Workspace restored successfully.");
|
|
702
|
+
},
|
|
703
|
+
async getSettings(ctx) {
|
|
704
|
+
if (!ctx.workspace) return setApiResponse2(HTTP2.NOT_FOUND, "NOT_FOUND", "Workspace not found");
|
|
705
|
+
const settings = await workspaces.getSettings(ctx.workspace.id);
|
|
706
|
+
return setApiResponse2(
|
|
707
|
+
HTTP2.OK,
|
|
708
|
+
"SETTINGS_FETCHED",
|
|
709
|
+
"Workspace settings retrieved successfully.",
|
|
710
|
+
{
|
|
711
|
+
settings: toSettingsDTO(settings)
|
|
712
|
+
}
|
|
713
|
+
);
|
|
714
|
+
},
|
|
715
|
+
async updateSettings(ctx) {
|
|
716
|
+
if (!ctx.workspace) return setApiResponse2(HTTP2.NOT_FOUND, "NOT_FOUND", "Workspace not found");
|
|
717
|
+
const body = ctx.meta["body"];
|
|
718
|
+
if (!body || Object.keys(body).length === 0) {
|
|
719
|
+
return setApiResponse2(HTTP2.UNPROCESSABLE, "INVALID_PARAMETER", "No settings provided");
|
|
720
|
+
}
|
|
721
|
+
const patch = {};
|
|
722
|
+
for (const key of ["locale", "timezone", "currency", "dateFormat", "timeFormat"]) {
|
|
723
|
+
if (typeof body[key] === "string") patch[key] = body[key];
|
|
724
|
+
}
|
|
725
|
+
const settings = await workspaces.updateSettings(ctx.workspace.id, patch);
|
|
726
|
+
return setApiResponse2(
|
|
727
|
+
HTTP2.OK,
|
|
728
|
+
"SETTINGS_UPDATED",
|
|
729
|
+
"Workspace settings updated successfully.",
|
|
730
|
+
{
|
|
731
|
+
settings: toSettingsDTO(settings)
|
|
732
|
+
}
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// src/controllers/member.controller.ts
|
|
739
|
+
import { setApiResponse as setApiResponse3, HTTP as HTTP3 } from "@fonderie/core";
|
|
740
|
+
function memberController(store) {
|
|
741
|
+
const members = new MemberModel(store);
|
|
742
|
+
return {
|
|
743
|
+
async list(ctx) {
|
|
744
|
+
if (!ctx.workspace) return setApiResponse3(HTTP3.NOT_FOUND, "NOT_FOUND", "Workspace not found");
|
|
745
|
+
const list = await members.list(ctx.workspace.id);
|
|
746
|
+
return setApiResponse3(HTTP3.OK, "MEMBERS_FETCHED", "Members retrieved successfully.", {
|
|
747
|
+
members: list.map(toMemberDTO)
|
|
748
|
+
});
|
|
749
|
+
},
|
|
750
|
+
async remove(ctx) {
|
|
751
|
+
if (!ctx.workspace) return setApiResponse3(HTTP3.NOT_FOUND, "NOT_FOUND", "Workspace not found");
|
|
752
|
+
if (ctx.workspace.isPersonal) {
|
|
753
|
+
return setApiResponse3(
|
|
754
|
+
HTTP3.FORBIDDEN,
|
|
755
|
+
"FORBIDDEN",
|
|
756
|
+
"Personal workspaces do not support member management"
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
const params = ctx.meta["params"];
|
|
760
|
+
const userId = params?.["userId"];
|
|
761
|
+
if (!userId)
|
|
762
|
+
return setApiResponse3(HTTP3.UNPROCESSABLE, "INVALID_PARAMETER", "userId is required");
|
|
763
|
+
if (userId === ctx.user?.id) {
|
|
764
|
+
return setApiResponse3(HTTP3.BAD_REQUEST, "INVALID_OPERATION", "Cannot remove yourself");
|
|
765
|
+
}
|
|
766
|
+
await members.remove(userId, ctx.workspace.id);
|
|
767
|
+
return setApiResponse3(HTTP3.OK, "MEMBER_REMOVED", "Member removed successfully.");
|
|
768
|
+
},
|
|
769
|
+
async getUserRoles(ctx) {
|
|
770
|
+
if (!ctx.workspace) return setApiResponse3(HTTP3.NOT_FOUND, "NOT_FOUND", "Workspace not found");
|
|
771
|
+
const params = ctx.meta["params"];
|
|
772
|
+
const userId = params?.["userId"];
|
|
773
|
+
if (!userId)
|
|
774
|
+
return setApiResponse3(HTTP3.UNPROCESSABLE, "INVALID_PARAMETER", "userId is required");
|
|
775
|
+
const roles = await members.getUserRoles(userId, ctx.workspace.id);
|
|
776
|
+
return setApiResponse3(HTTP3.OK, "ROLES_FETCHED", "Member roles retrieved successfully.", {
|
|
777
|
+
roles: roles.map(toRoleDTO)
|
|
778
|
+
});
|
|
779
|
+
},
|
|
780
|
+
async addRole(ctx) {
|
|
781
|
+
if (!ctx.workspace) return setApiResponse3(HTTP3.NOT_FOUND, "NOT_FOUND", "Workspace not found");
|
|
782
|
+
const params = ctx.meta["params"];
|
|
783
|
+
const body = ctx.meta["body"];
|
|
784
|
+
const userId = params?.["userId"];
|
|
785
|
+
const roleId = body?.["roleId"] ?? params?.["roleId"];
|
|
786
|
+
if (!userId)
|
|
787
|
+
return setApiResponse3(HTTP3.UNPROCESSABLE, "INVALID_PARAMETER", "userId is required");
|
|
788
|
+
if (!roleId)
|
|
789
|
+
return setApiResponse3(HTTP3.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
|
|
790
|
+
await members.addRole(userId, ctx.workspace.id, roleId);
|
|
791
|
+
return setApiResponse3(HTTP3.OK, "ROLE_ASSIGNED", "Role assigned successfully.");
|
|
792
|
+
},
|
|
793
|
+
async removeRole(ctx) {
|
|
794
|
+
if (!ctx.workspace) return setApiResponse3(HTTP3.NOT_FOUND, "NOT_FOUND", "Workspace not found");
|
|
795
|
+
const params = ctx.meta["params"];
|
|
796
|
+
const userId = params?.["userId"];
|
|
797
|
+
const roleId = params?.["roleId"];
|
|
798
|
+
if (!userId)
|
|
799
|
+
return setApiResponse3(HTTP3.UNPROCESSABLE, "INVALID_PARAMETER", "userId is required");
|
|
800
|
+
if (!roleId)
|
|
801
|
+
return setApiResponse3(HTTP3.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
|
|
802
|
+
try {
|
|
803
|
+
await members.removeRole(userId, ctx.workspace.id, roleId);
|
|
804
|
+
return setApiResponse3(HTTP3.OK, "ROLE_REMOVED", "Role removed successfully.");
|
|
805
|
+
} catch (err) {
|
|
806
|
+
const message = err instanceof Error ? err.message : "Failed";
|
|
807
|
+
return setApiResponse3(HTTP3.BAD_REQUEST, "OPERATION_FAILED", message);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// src/controllers/role.controller.ts
|
|
814
|
+
import { setApiResponse as setApiResponse4, HTTP as HTTP4 } from "@fonderie/core";
|
|
815
|
+
function roleController(store) {
|
|
816
|
+
const roles = new RoleModel(store);
|
|
817
|
+
return {
|
|
818
|
+
async create(ctx) {
|
|
819
|
+
if (!ctx.workspace) {
|
|
820
|
+
return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "Workspace not found");
|
|
821
|
+
}
|
|
822
|
+
const body = ctx.meta["body"];
|
|
823
|
+
const name = body?.["name"];
|
|
824
|
+
const description = body?.["description"];
|
|
825
|
+
if (typeof name !== "string" || name.trim().length === 0) {
|
|
826
|
+
return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "name is required");
|
|
827
|
+
}
|
|
828
|
+
try {
|
|
829
|
+
const opts = {
|
|
830
|
+
name: name.trim(),
|
|
831
|
+
workspaceId: ctx.workspace.id
|
|
832
|
+
};
|
|
833
|
+
if (typeof description === "string") {
|
|
834
|
+
opts.description = description;
|
|
835
|
+
}
|
|
836
|
+
const role = await roles.create(opts);
|
|
837
|
+
return setApiResponse4(HTTP4.CREATED, "ROLE_CREATED", "Role created successfully.", {
|
|
838
|
+
role: toRoleDTO(role)
|
|
839
|
+
});
|
|
840
|
+
} catch (err) {
|
|
841
|
+
const message = err instanceof Error ? err.message : "Failed to create role";
|
|
842
|
+
return setApiResponse4(HTTP4.BAD_REQUEST, "OPERATION_FAILED", message);
|
|
843
|
+
}
|
|
844
|
+
},
|
|
845
|
+
async list(ctx) {
|
|
846
|
+
if (!ctx.workspace) {
|
|
847
|
+
return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "Workspace not found");
|
|
848
|
+
}
|
|
849
|
+
const list = await roles.list(ctx.workspace.id);
|
|
850
|
+
return setApiResponse4(HTTP4.OK, "ROLES_FETCHED", "Roles retrieved successfully.", {
|
|
851
|
+
roles: list.map(toRoleDTO)
|
|
852
|
+
});
|
|
853
|
+
},
|
|
854
|
+
async get(ctx) {
|
|
855
|
+
if (!ctx.workspace) {
|
|
856
|
+
return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "Workspace not found");
|
|
857
|
+
}
|
|
858
|
+
const params = ctx.meta["params"];
|
|
859
|
+
const roleId = params?.["roleId"];
|
|
860
|
+
if (!roleId) {
|
|
861
|
+
return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
|
|
862
|
+
}
|
|
863
|
+
const role = await roles.findById(roleId);
|
|
864
|
+
if (!role) {
|
|
865
|
+
return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "Role not found");
|
|
866
|
+
}
|
|
867
|
+
return setApiResponse4(HTTP4.OK, "ROLE_FETCHED", "Role retrieved successfully.", {
|
|
868
|
+
role: toRoleDTO(role)
|
|
869
|
+
});
|
|
870
|
+
},
|
|
871
|
+
async update(ctx) {
|
|
872
|
+
if (!ctx.workspace) {
|
|
873
|
+
return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "Workspace not found");
|
|
874
|
+
}
|
|
875
|
+
const params = ctx.meta["params"];
|
|
876
|
+
const body = ctx.meta["body"];
|
|
877
|
+
const roleId = params?.["roleId"];
|
|
878
|
+
if (!roleId) {
|
|
879
|
+
return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
|
|
880
|
+
}
|
|
881
|
+
const opts = {};
|
|
882
|
+
if (typeof body?.["name"] === "string") {
|
|
883
|
+
opts.name = body["name"];
|
|
884
|
+
}
|
|
885
|
+
if (typeof body?.["description"] === "string") {
|
|
886
|
+
opts.description = body["description"];
|
|
887
|
+
}
|
|
888
|
+
if (body?.["description"] === null) {
|
|
889
|
+
opts.description = null;
|
|
890
|
+
}
|
|
891
|
+
if (typeof body?.["active"] === "boolean") {
|
|
892
|
+
opts.active = body["active"];
|
|
893
|
+
}
|
|
894
|
+
const role = await roles.update(roleId, opts);
|
|
895
|
+
if (!role) {
|
|
896
|
+
return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "Role not found or is a system role");
|
|
897
|
+
}
|
|
898
|
+
return setApiResponse4(HTTP4.OK, "ROLE_UPDATED", "Role updated successfully.", {
|
|
899
|
+
role: toRoleDTO(role)
|
|
900
|
+
});
|
|
901
|
+
},
|
|
902
|
+
async remove(ctx) {
|
|
903
|
+
if (!ctx.workspace) {
|
|
904
|
+
return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "Workspace not found");
|
|
905
|
+
}
|
|
906
|
+
const params = ctx.meta["params"];
|
|
907
|
+
const roleId = params?.["roleId"];
|
|
908
|
+
if (!roleId) {
|
|
909
|
+
return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
|
|
910
|
+
}
|
|
911
|
+
await roles.delete(roleId, ctx.workspace.id);
|
|
912
|
+
return setApiResponse4(HTTP4.OK, "ROLE_DELETED", "Role deleted successfully.");
|
|
913
|
+
},
|
|
914
|
+
async setPermissions(ctx) {
|
|
915
|
+
if (!ctx.workspace) {
|
|
916
|
+
return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "Workspace not found");
|
|
917
|
+
}
|
|
918
|
+
const params = ctx.meta["params"];
|
|
919
|
+
const body = ctx.meta["body"];
|
|
920
|
+
const roleId = params?.["roleId"];
|
|
921
|
+
if (!roleId) {
|
|
922
|
+
return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "roleId is required");
|
|
923
|
+
}
|
|
924
|
+
const perms = body?.["permissions"];
|
|
925
|
+
if (!Array.isArray(perms)) {
|
|
926
|
+
return setApiResponse4(
|
|
927
|
+
HTTP4.UNPROCESSABLE,
|
|
928
|
+
"INVALID_PARAMETER",
|
|
929
|
+
"permissions array is required"
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
const normalized = perms.map((p) => {
|
|
933
|
+
const perm = p;
|
|
934
|
+
return {
|
|
935
|
+
permissionKey: String(perm["permissionKey"] ?? ""),
|
|
936
|
+
canCreate: Boolean(perm["canCreate"]),
|
|
937
|
+
canRead: Boolean(perm["canRead"]),
|
|
938
|
+
canUpdate: Boolean(perm["canUpdate"]),
|
|
939
|
+
canDelete: Boolean(perm["canDelete"])
|
|
940
|
+
};
|
|
941
|
+
}).filter((p) => p.permissionKey.length > 0);
|
|
942
|
+
await roles.setPermissions(roleId, ctx.workspace.id, normalized);
|
|
943
|
+
return setApiResponse4(HTTP4.OK, "PERMISSIONS_SET", "Role permissions updated successfully.");
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// src/controllers/invitation.controller.ts
|
|
949
|
+
import { setApiResponse as setApiResponse5, HTTP as HTTP5 } from "@fonderie/core";
|
|
950
|
+
import { NOTIFICATION_EVENT } from "@fonderie/events";
|
|
951
|
+
import { getPlanLimit } from "@fonderie/billing";
|
|
952
|
+
|
|
953
|
+
// src/services/invitations.ts
|
|
954
|
+
import { randomBytes } from "crypto";
|
|
955
|
+
function generateToken() {
|
|
956
|
+
return randomBytes(32).toString("hex");
|
|
957
|
+
}
|
|
958
|
+
function generatePin() {
|
|
959
|
+
return Math.floor(1e5 + Math.random() * 9e5).toString();
|
|
960
|
+
}
|
|
961
|
+
function parseTtl(ttl) {
|
|
962
|
+
const units = {
|
|
963
|
+
s: 1e3,
|
|
964
|
+
m: 6e4,
|
|
965
|
+
h: 36e5,
|
|
966
|
+
d: 864e5
|
|
967
|
+
};
|
|
968
|
+
const match = ttl.match(/^(\d+)([smhd])$/);
|
|
969
|
+
if (!match) return 7 * 864e5;
|
|
970
|
+
const [, n, unit] = match;
|
|
971
|
+
return parseInt(n, 10) * (units[unit] ?? 0);
|
|
972
|
+
}
|
|
973
|
+
var SELECT_INV = `
|
|
974
|
+
id,
|
|
975
|
+
workspace_id AS "workspaceId",
|
|
976
|
+
email,
|
|
977
|
+
role_id AS "roleId",
|
|
978
|
+
token,
|
|
979
|
+
pin,
|
|
980
|
+
status,
|
|
981
|
+
expires_at AS "expiresAt",
|
|
982
|
+
created_at AS "createdAt"
|
|
983
|
+
`;
|
|
984
|
+
async function createInvitation(opts, store) {
|
|
985
|
+
const token = generateToken();
|
|
986
|
+
const pin = generatePin();
|
|
987
|
+
const expiresAt = new Date(Date.now() + parseTtl(opts.ttl ?? "7d"));
|
|
988
|
+
const [invitation] = await store.query(
|
|
989
|
+
`INSERT INTO fonderie_workspace_invitations
|
|
990
|
+
(workspace_id, email, role_id, token, pin, expires_at)
|
|
991
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
|
992
|
+
ON CONFLICT DO NOTHING
|
|
993
|
+
RETURNING ${SELECT_INV}`,
|
|
994
|
+
[opts.workspaceId, opts.email, opts.roleId, token, pin, expiresAt]
|
|
995
|
+
);
|
|
996
|
+
if (!invitation) {
|
|
997
|
+
const [updated] = await store.query(
|
|
998
|
+
`UPDATE fonderie_workspace_invitations
|
|
999
|
+
SET token = $4, pin = $5, expires_at = $6, role_id = $3, status = 'PENDING'
|
|
1000
|
+
WHERE workspace_id = $1 AND email = $2 AND status = 'PENDING'
|
|
1001
|
+
RETURNING ${SELECT_INV}`,
|
|
1002
|
+
[opts.workspaceId, opts.email, opts.roleId, token, pin, expiresAt]
|
|
1003
|
+
);
|
|
1004
|
+
if (!updated) throw new Error("Failed to create invitation");
|
|
1005
|
+
return updated;
|
|
1006
|
+
}
|
|
1007
|
+
return invitation;
|
|
1008
|
+
}
|
|
1009
|
+
async function listInvitations(workspaceId, store) {
|
|
1010
|
+
return store.query(
|
|
1011
|
+
`SELECT ${SELECT_INV}
|
|
1012
|
+
FROM fonderie_workspace_invitations
|
|
1013
|
+
WHERE workspace_id = $1 AND status = 'PENDING'
|
|
1014
|
+
ORDER BY created_at DESC`,
|
|
1015
|
+
[workspaceId]
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
async function cancelInvitation(invitationId, workspaceId, store) {
|
|
1019
|
+
await store.query(
|
|
1020
|
+
`UPDATE fonderie_workspace_invitations
|
|
1021
|
+
SET status = 'CANCELLED'
|
|
1022
|
+
WHERE id = $1 AND workspace_id = $2 AND status = 'PENDING'`,
|
|
1023
|
+
[invitationId, workspaceId]
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
async function acceptInvitationByPin(opts, store) {
|
|
1027
|
+
const [inv] = await store.query(
|
|
1028
|
+
`SELECT id, workspace_id AS "workspaceId", role_id AS "roleId", expires_at AS "expiresAt"
|
|
1029
|
+
FROM fonderie_workspace_invitations
|
|
1030
|
+
WHERE pin = $1 AND status = 'PENDING'`,
|
|
1031
|
+
[opts.pin]
|
|
1032
|
+
);
|
|
1033
|
+
if (!inv) throw new Error("Invalid PIN");
|
|
1034
|
+
if (/* @__PURE__ */ new Date() > new Date(inv.expiresAt)) throw new Error("Invitation expired");
|
|
1035
|
+
await store.transaction(async (tx) => {
|
|
1036
|
+
await Promise.all([
|
|
1037
|
+
tx.query(
|
|
1038
|
+
`INSERT INTO fonderie_role_user_workspaces (user_id, workspace_id, role_id, confirmed)
|
|
1039
|
+
VALUES ($1, $2, $3, true)
|
|
1040
|
+
ON CONFLICT (user_id, workspace_id, role_id) DO UPDATE
|
|
1041
|
+
SET confirmed = true, removed = false`,
|
|
1042
|
+
[opts.userId, inv.workspaceId, inv.roleId]
|
|
1043
|
+
),
|
|
1044
|
+
tx.query(`UPDATE fonderie_workspace_invitations SET status = 'ACCEPTED' WHERE id = $1`, [
|
|
1045
|
+
inv.id
|
|
1046
|
+
])
|
|
1047
|
+
]);
|
|
1048
|
+
});
|
|
1049
|
+
return { workspaceId: inv.workspaceId, roleId: inv.roleId };
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// src/models/invitation.model.ts
|
|
1053
|
+
var InvitationModel = class {
|
|
1054
|
+
constructor(store) {
|
|
1055
|
+
this.store = store;
|
|
1056
|
+
}
|
|
1057
|
+
store;
|
|
1058
|
+
create(opts) {
|
|
1059
|
+
return createInvitation(opts, this.store);
|
|
1060
|
+
}
|
|
1061
|
+
list(workspaceId) {
|
|
1062
|
+
return listInvitations(workspaceId, this.store);
|
|
1063
|
+
}
|
|
1064
|
+
cancel(invitationId, workspaceId) {
|
|
1065
|
+
return cancelInvitation(invitationId, workspaceId, this.store);
|
|
1066
|
+
}
|
|
1067
|
+
acceptByPin(opts) {
|
|
1068
|
+
return acceptInvitationByPin(opts, this.store);
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
|
|
1072
|
+
// src/controllers/invitation.controller.ts
|
|
1073
|
+
function invitationController(store, ttl, bus) {
|
|
1074
|
+
const invitations = new InvitationModel(store);
|
|
1075
|
+
return {
|
|
1076
|
+
async list(ctx) {
|
|
1077
|
+
if (!ctx.workspace) return setApiResponse5(HTTP5.NOT_FOUND, "NOT_FOUND", "Workspace not found");
|
|
1078
|
+
const list = await invitations.list(ctx.workspace.id);
|
|
1079
|
+
return setApiResponse5(HTTP5.OK, "INVITATIONS_FETCHED", "Invitations retrieved successfully.", {
|
|
1080
|
+
invitations: list.map(toInvitationDTO)
|
|
1081
|
+
});
|
|
1082
|
+
},
|
|
1083
|
+
async invite(ctx) {
|
|
1084
|
+
if (!ctx.workspace) return setApiResponse5(HTTP5.NOT_FOUND, "NOT_FOUND", "Workspace not found");
|
|
1085
|
+
if (ctx.workspace.isPersonal) {
|
|
1086
|
+
return setApiResponse5(
|
|
1087
|
+
HTTP5.FORBIDDEN,
|
|
1088
|
+
"FORBIDDEN",
|
|
1089
|
+
"Personal workspaces do not support invitations"
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
const body = ctx.meta["body"];
|
|
1093
|
+
const entries = Array.isArray(body) ? body : [body];
|
|
1094
|
+
if (!entries.length) {
|
|
1095
|
+
return setApiResponse5(HTTP5.UNPROCESSABLE, "INVALID_PARAMETER", "at least one invite is required");
|
|
1096
|
+
}
|
|
1097
|
+
for (const entry of entries) {
|
|
1098
|
+
if (typeof entry?.["email"] !== "string") {
|
|
1099
|
+
return setApiResponse5(HTTP5.UNPROCESSABLE, "INVALID_PARAMETER", "email is required for every invite");
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
const seatLimit = getPlanLimit(ctx, "seats");
|
|
1103
|
+
if (seatLimit !== null) {
|
|
1104
|
+
const [countRow] = await store.query(
|
|
1105
|
+
`SELECT COUNT(*) AS count FROM fonderie_role_user_workspaces
|
|
1106
|
+
WHERE workspace_id = $1 AND removed = false AND suspended = false`,
|
|
1107
|
+
[ctx.workspace.id]
|
|
1108
|
+
);
|
|
1109
|
+
const total = parseInt(countRow.count, 10);
|
|
1110
|
+
const occupied = ctx.workspace.isPersonal ? total : Math.max(0, total - 1);
|
|
1111
|
+
if (occupied + entries.length > seatLimit) {
|
|
1112
|
+
return setApiResponse5(
|
|
1113
|
+
HTTP5.PAYMENT_REQUIRED,
|
|
1114
|
+
"SEAT_LIMIT_REACHED",
|
|
1115
|
+
`Your plan allows ${seatLimit} seat${seatLimit === 1 ? "" : "s"}. Upgrade to invite more members.`,
|
|
1116
|
+
{ limit: seatLimit }
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
let defaultRoleId;
|
|
1121
|
+
const needsDefault = entries.some((e) => !e["roleId"]);
|
|
1122
|
+
if (needsDefault) {
|
|
1123
|
+
const [row] = await store.query(
|
|
1124
|
+
`SELECT id FROM fonderie_roles
|
|
1125
|
+
WHERE name = 'ADMIN' AND workspace_id = $1 LIMIT 1`,
|
|
1126
|
+
[ctx.workspace.id]
|
|
1127
|
+
);
|
|
1128
|
+
defaultRoleId = row?.id;
|
|
1129
|
+
if (!defaultRoleId) {
|
|
1130
|
+
return setApiResponse5(HTTP5.SERVER_ERROR, "SERVER_ERROR", "Default role not found");
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
const results = await Promise.all(
|
|
1134
|
+
entries.map(async (entry) => {
|
|
1135
|
+
const email = entry["email"];
|
|
1136
|
+
const resolvedRoleId = entry["roleId"] ?? defaultRoleId;
|
|
1137
|
+
const invitation = await invitations.create({
|
|
1138
|
+
workspaceId: ctx.workspace.id,
|
|
1139
|
+
email,
|
|
1140
|
+
roleId: resolvedRoleId,
|
|
1141
|
+
ttl
|
|
1142
|
+
});
|
|
1143
|
+
bus?.emit(NOTIFICATION_EVENT, {
|
|
1144
|
+
type: MESSAGE_KEYS.workspaceInvitation,
|
|
1145
|
+
recipient: { email, phone: null, deviceToken: null },
|
|
1146
|
+
data: { token: invitation.token, pin: invitation.pin }
|
|
1147
|
+
}).catch(() => {
|
|
1148
|
+
});
|
|
1149
|
+
return { invitationId: invitation.id, email };
|
|
1150
|
+
})
|
|
1151
|
+
);
|
|
1152
|
+
return setApiResponse5(HTTP5.CREATED, "INVITATIONS_SENT", "Invitations sent successfully.", {
|
|
1153
|
+
invitations: results
|
|
1154
|
+
});
|
|
1155
|
+
},
|
|
1156
|
+
async cancel(ctx) {
|
|
1157
|
+
if (!ctx.workspace) return setApiResponse5(HTTP5.NOT_FOUND, "NOT_FOUND", "Workspace not found");
|
|
1158
|
+
const params = ctx.meta["params"];
|
|
1159
|
+
const invitationId = params?.["inviteId"];
|
|
1160
|
+
if (!invitationId)
|
|
1161
|
+
return setApiResponse5(HTTP5.UNPROCESSABLE, "INVALID_PARAMETER", "inviteId is required");
|
|
1162
|
+
await invitations.cancel(invitationId, ctx.workspace.id);
|
|
1163
|
+
return setApiResponse5(HTTP5.OK, "INVITATION_CANCELLED", "Invitation cancelled successfully.");
|
|
1164
|
+
},
|
|
1165
|
+
async accept(ctx) {
|
|
1166
|
+
const body = ctx.meta["body"];
|
|
1167
|
+
const pin = body?.["pin"];
|
|
1168
|
+
if (typeof pin !== "string") {
|
|
1169
|
+
return setApiResponse5(HTTP5.UNPROCESSABLE, "INVALID_PARAMETER", "pin is required");
|
|
1170
|
+
}
|
|
1171
|
+
try {
|
|
1172
|
+
const { workspaceId } = await invitations.acceptByPin({ pin, userId: ctx.user.id });
|
|
1173
|
+
return setApiResponse5(HTTP5.OK, "INVITATION_ACCEPTED", "Invitation accepted successfully.", {
|
|
1174
|
+
workspaceId
|
|
1175
|
+
});
|
|
1176
|
+
} catch (err) {
|
|
1177
|
+
const message = err instanceof Error ? err.message : "Invalid invitation";
|
|
1178
|
+
return setApiResponse5(HTTP5.BAD_REQUEST, "INVITATION_FAILED", message);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// src/routes.ts
|
|
1185
|
+
function buildWorkspaceRoutes(store, config, bus) {
|
|
1186
|
+
const ttl = config.invitationTtl ?? "7d";
|
|
1187
|
+
const wsCtx = withWorkspace(store);
|
|
1188
|
+
const workspace = workspaceController(store, config);
|
|
1189
|
+
const member = memberController(store);
|
|
1190
|
+
const role = roleController(store);
|
|
1191
|
+
const invitation = invitationController(store, ttl, bus);
|
|
1192
|
+
return [
|
|
1193
|
+
// ── Workspace creation + listing (no workspace context required)
|
|
1194
|
+
["POST", "/workspaces", requireAuth, workspace.create],
|
|
1195
|
+
["GET", "/workspaces", requireAuth, workspace.list],
|
|
1196
|
+
// ── Members (workspace resolved from X-Workspace-ID header)
|
|
1197
|
+
["GET", "/workspaces/members", requireAuth, wsCtx, member.list],
|
|
1198
|
+
["DELETE", "/workspaces/members/:userId", requireAuth, wsCtx, member.remove],
|
|
1199
|
+
["GET", "/workspaces/members/:userId/roles", requireAuth, wsCtx, member.getUserRoles],
|
|
1200
|
+
["POST", "/workspaces/members/:userId/roles", requireAuth, wsCtx, member.addRole],
|
|
1201
|
+
["DELETE", "/workspaces/members/:userId/roles/:roleId", requireAuth, wsCtx, member.removeRole],
|
|
1202
|
+
// ── Invitations
|
|
1203
|
+
["GET", "/workspaces/invitations", requireAuth, wsCtx, invitation.list],
|
|
1204
|
+
["POST", "/workspaces/invitations", requireAuth, wsCtx, invitation.invite],
|
|
1205
|
+
["DELETE", "/workspaces/invitations/:inviteId", requireAuth, wsCtx, invitation.cancel],
|
|
1206
|
+
["POST", "/workspaces/invitations/accept", requireAuth, invitation.accept],
|
|
1207
|
+
// ── Roles
|
|
1208
|
+
["POST", "/workspaces/roles", requireAuth, wsCtx, role.create],
|
|
1209
|
+
["GET", "/workspaces/roles", requireAuth, wsCtx, role.list],
|
|
1210
|
+
["GET", "/workspaces/roles/:roleId", requireAuth, wsCtx, role.get],
|
|
1211
|
+
["PUT", "/workspaces/roles/:roleId", requireAuth, wsCtx, role.update],
|
|
1212
|
+
["DELETE", "/workspaces/roles/:roleId", requireAuth, wsCtx, role.remove],
|
|
1213
|
+
["POST", "/workspaces/roles/:roleId/permissions", requireAuth, wsCtx, role.setPermissions],
|
|
1214
|
+
// ── Workspace lifecycle
|
|
1215
|
+
["POST", "/workspaces/archive", requireAuth, wsCtx, workspace.archive],
|
|
1216
|
+
["POST", "/workspaces/restore", requireAuth, wsCtx, workspace.restore],
|
|
1217
|
+
["GET", "/workspaces/settings", requireAuth, wsCtx, workspace.getSettings],
|
|
1218
|
+
["PUT", "/workspaces/settings", requireAuth, wsCtx, workspace.updateSettings],
|
|
1219
|
+
// ── Path-based lookup by ID (admin / cross-workspace use)
|
|
1220
|
+
["GET", "/workspaces/:id", requireAuth, wsCtx, workspace.get],
|
|
1221
|
+
// ── Update current workspace — ID resolved from X-Workspace-ID header
|
|
1222
|
+
// (or personal workspace fallback when header is absent)
|
|
1223
|
+
["PUT", "/workspaces", requireAuth, wsCtx, workspace.update]
|
|
1224
|
+
];
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// src/module.ts
|
|
1228
|
+
var AUTH_USER_REGISTERED = "fonderie.user.registered";
|
|
1229
|
+
var WorkspacesModule = class {
|
|
1230
|
+
constructor(store, config = {}, bus) {
|
|
1231
|
+
this.store = store;
|
|
1232
|
+
this.config = config;
|
|
1233
|
+
this.bus = bus;
|
|
1234
|
+
}
|
|
1235
|
+
store;
|
|
1236
|
+
config;
|
|
1237
|
+
bus;
|
|
1238
|
+
name = "@fonderie/workspaces";
|
|
1239
|
+
deps = ["@fonderie/auth", "@fonderie/billing"];
|
|
1240
|
+
install(app) {
|
|
1241
|
+
if (this.bus && this.config.personalWorkspace !== false) {
|
|
1242
|
+
this.bus.on(
|
|
1243
|
+
AUTH_USER_REGISTERED,
|
|
1244
|
+
(payload) => this.provisionPersonalWorkspace(payload),
|
|
1245
|
+
"workspaces"
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
const routes = buildWorkspaceRoutes(this.store, this.config, this.bus);
|
|
1249
|
+
for (const [method, path, ...handlers] of routes) {
|
|
1250
|
+
app.addRoute(method, path, ...handlers);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
async provisionPersonalWorkspace(payload) {
|
|
1254
|
+
const name = "My Workspace";
|
|
1255
|
+
const slug = `${payload.userId}-personal`;
|
|
1256
|
+
let workspaceId;
|
|
1257
|
+
await this.store.transaction(async (tx) => {
|
|
1258
|
+
const wsModel = new WorkspaceModel(tx);
|
|
1259
|
+
const roleModel = new RoleModel(tx);
|
|
1260
|
+
const memModel = new MemberModel(tx);
|
|
1261
|
+
const ws = await wsModel.createPersonal({ name, slug, ownerId: payload.userId });
|
|
1262
|
+
if (!ws) return;
|
|
1263
|
+
workspaceId = ws.id;
|
|
1264
|
+
const adminRole = await roleModel.findSystem("ADMIN");
|
|
1265
|
+
if (!adminRole) throw new Error("System ADMIN role not found");
|
|
1266
|
+
await memModel.add({
|
|
1267
|
+
userId: payload.userId,
|
|
1268
|
+
workspaceId: ws.id,
|
|
1269
|
+
roleId: adminRole.id,
|
|
1270
|
+
confirmed: true
|
|
1271
|
+
});
|
|
1272
|
+
});
|
|
1273
|
+
if (workspaceId) {
|
|
1274
|
+
this.bus?.emit(EVENT_KEYS.personalWorkspaceCreated, {
|
|
1275
|
+
workspaceId,
|
|
1276
|
+
userId: payload.userId
|
|
1277
|
+
}).catch(() => {
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
|
|
1283
|
+
// src/middlewares/require-workspace.ts
|
|
1284
|
+
import { setApiResponse as setApiResponse6, HTTP as HTTP6 } from "@fonderie/core";
|
|
1285
|
+
var requireWorkspace = async (ctx, next) => {
|
|
1286
|
+
if (!ctx.workspace) {
|
|
1287
|
+
return setApiResponse6(HTTP6.BAD_REQUEST, "WORKSPACE_REQUIRED", "Workspace context required");
|
|
1288
|
+
}
|
|
1289
|
+
return next();
|
|
1290
|
+
};
|
|
1291
|
+
export {
|
|
1292
|
+
EVENT_KEYS,
|
|
1293
|
+
MESSAGE_KEYS,
|
|
1294
|
+
WorkspacesModule,
|
|
1295
|
+
requireWorkspace,
|
|
1296
|
+
toInvitationDTO,
|
|
1297
|
+
toMemberDTO,
|
|
1298
|
+
toRoleDTO,
|
|
1299
|
+
toSettingsDTO,
|
|
1300
|
+
toWorkspaceDTO,
|
|
1301
|
+
withWorkspace
|
|
1302
|
+
};
|
|
1303
|
+
//# sourceMappingURL=index.js.map
|