@selvajs/server 1.1.0 → 1.3.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/dist/access/guards.d.ts +213 -0
- package/dist/access/guards.d.ts.map +1 -0
- package/dist/access/guards.js +417 -0
- package/dist/access/guards.js.map +1 -0
- package/dist/access/index.d.ts +2 -0
- package/dist/access/index.d.ts.map +1 -1
- package/dist/access/index.js +6 -0
- package/dist/access/index.js.map +1 -1
- package/dist/access/permissions-scope.d.ts +15 -0
- package/dist/access/permissions-scope.d.ts.map +1 -0
- package/dist/access/permissions-scope.js +23 -0
- package/dist/access/permissions-scope.js.map +1 -0
- package/dist/api/bodies.d.ts +104 -0
- package/dist/api/bodies.d.ts.map +1 -0
- package/dist/api/bodies.js +87 -0
- package/dist/api/bodies.js.map +1 -0
- package/dist/api/deps.d.ts +135 -0
- package/dist/api/deps.d.ts.map +1 -0
- package/dist/api/deps.js +55 -0
- package/dist/api/deps.js.map +1 -0
- package/dist/api/errors.d.ts +35 -0
- package/dist/api/errors.d.ts.map +1 -0
- package/dist/api/errors.js +63 -0
- package/dist/api/errors.js.map +1 -0
- package/dist/api/index.d.ts +12 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +15 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/map-core-error.d.ts +19 -0
- package/dist/api/map-core-error.d.ts.map +1 -0
- package/dist/api/map-core-error.js +49 -0
- package/dist/api/map-core-error.js.map +1 -0
- package/dist/api/pagination.d.ts +19 -0
- package/dist/api/pagination.d.ts.map +1 -0
- package/dist/api/pagination.js +40 -0
- package/dist/api/pagination.js.map +1 -0
- package/dist/api/request.d.ts +93 -0
- package/dist/api/request.d.ts.map +1 -0
- package/dist/api/request.js +150 -0
- package/dist/api/request.js.map +1 -0
- package/dist/api/respond.d.ts +29 -0
- package/dist/api/respond.d.ts.map +1 -0
- package/dist/api/respond.js +59 -0
- package/dist/api/respond.js.map +1 -0
- package/dist/api/responses-schema.d.ts +158 -0
- package/dist/api/responses-schema.d.ts.map +1 -0
- package/dist/api/responses-schema.js +104 -0
- package/dist/api/responses-schema.js.map +1 -0
- package/dist/api/responses.d.ts +19 -0
- package/dist/api/responses.d.ts.map +1 -0
- package/dist/api/responses.js +21 -0
- package/dist/api/responses.js.map +1 -0
- package/dist/api/shaped.d.ts +23 -0
- package/dist/api/shaped.d.ts.map +1 -0
- package/dist/api/shaped.js +29 -0
- package/dist/api/shaped.js.map +1 -0
- package/dist/api/types.d.ts +41 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +12 -0
- package/dist/api/types.js.map +1 -0
- package/dist/compute/errors.d.ts +18 -0
- package/dist/compute/errors.d.ts.map +1 -0
- package/dist/compute/errors.js +21 -0
- package/dist/compute/errors.js.map +1 -0
- package/dist/compute/evict-changed-servers.d.ts +22 -0
- package/dist/compute/evict-changed-servers.d.ts.map +1 -0
- package/dist/compute/evict-changed-servers.js +31 -0
- package/dist/compute/evict-changed-servers.js.map +1 -0
- package/dist/compute/idempotency-http.d.ts +47 -0
- package/dist/compute/idempotency-http.d.ts.map +1 -0
- package/dist/compute/idempotency-http.js +51 -0
- package/dist/compute/idempotency-http.js.map +1 -0
- package/dist/compute/index.d.ts +5 -0
- package/dist/compute/index.d.ts.map +1 -1
- package/dist/compute/index.js +5 -0
- package/dist/compute/index.js.map +1 -1
- package/dist/compute/resolve-server.d.ts +25 -0
- package/dist/compute/resolve-server.d.ts.map +1 -0
- package/dist/compute/resolve-server.js +35 -0
- package/dist/compute/resolve-server.js.map +1 -0
- package/dist/compute/server-config-write.d.ts +52 -0
- package/dist/compute/server-config-write.d.ts.map +1 -0
- package/dist/compute/server-config-write.js +100 -0
- package/dist/compute/server-config-write.js.map +1 -0
- package/dist/definitions/index.d.ts +1 -0
- package/dist/definitions/index.d.ts.map +1 -1
- package/dist/definitions/index.js +1 -0
- package/dist/definitions/index.js.map +1 -1
- package/dist/definitions/visibility.d.ts +74 -0
- package/dist/definitions/visibility.d.ts.map +1 -0
- package/dist/definitions/visibility.js +117 -0
- package/dist/definitions/visibility.js.map +1 -0
- package/dist/handlers/definitionVersions.d.ts +30 -0
- package/dist/handlers/definitionVersions.d.ts.map +1 -0
- package/dist/handlers/definitionVersions.js +93 -0
- package/dist/handlers/definitionVersions.js.map +1 -0
- package/dist/handlers/definitions.d.ts +35 -0
- package/dist/handlers/definitions.d.ts.map +1 -0
- package/dist/handlers/definitions.js +207 -0
- package/dist/handlers/definitions.js.map +1 -0
- package/dist/handlers/index.d.ts +27 -0
- package/dist/handlers/index.d.ts.map +1 -0
- package/dist/handlers/index.js +29 -0
- package/dist/handlers/index.js.map +1 -0
- package/dist/handlers/invite-lookup.d.ts +11 -0
- package/dist/handlers/invite-lookup.d.ts.map +1 -0
- package/dist/handlers/invite-lookup.js +20 -0
- package/dist/handlers/invite-lookup.js.map +1 -0
- package/dist/handlers/invites.d.ts +31 -0
- package/dist/handlers/invites.d.ts.map +1 -0
- package/dist/handlers/invites.js +148 -0
- package/dist/handlers/invites.js.map +1 -0
- package/dist/handlers/me.d.ts +8 -0
- package/dist/handlers/me.d.ts.map +1 -0
- package/dist/handlers/me.js +21 -0
- package/dist/handlers/me.js.map +1 -0
- package/dist/handlers/me.starred.d.ts +11 -0
- package/dist/handlers/me.starred.d.ts.map +1 -0
- package/dist/handlers/me.starred.js +21 -0
- package/dist/handlers/me.starred.js.map +1 -0
- package/dist/handlers/orgAssets.d.ts +13 -0
- package/dist/handlers/orgAssets.d.ts.map +1 -0
- package/dist/handlers/orgAssets.js +54 -0
- package/dist/handlers/orgAssets.js.map +1 -0
- package/dist/handlers/orgCompute.d.ts +16 -0
- package/dist/handlers/orgCompute.d.ts.map +1 -0
- package/dist/handlers/orgCompute.js +96 -0
- package/dist/handlers/orgCompute.js.map +1 -0
- package/dist/handlers/orgMembers.d.ts +29 -0
- package/dist/handlers/orgMembers.d.ts.map +1 -0
- package/dist/handlers/orgMembers.js +200 -0
- package/dist/handlers/orgMembers.js.map +1 -0
- package/dist/handlers/orgs.d.ts +16 -0
- package/dist/handlers/orgs.d.ts.map +1 -0
- package/dist/handlers/orgs.js +26 -0
- package/dist/handlers/orgs.js.map +1 -0
- package/dist/handlers/projectMembers.d.ts +15 -0
- package/dist/handlers/projectMembers.d.ts.map +1 -0
- package/dist/handlers/projectMembers.js +95 -0
- package/dist/handlers/projectMembers.js.map +1 -0
- package/dist/handlers/projects.d.ts +21 -0
- package/dist/handlers/projects.d.ts.map +1 -0
- package/dist/handlers/projects.js +164 -0
- package/dist/handlers/projects.js.map +1 -0
- package/dist/handlers/reclaim.d.ts +10 -0
- package/dist/handlers/reclaim.d.ts.map +1 -0
- package/dist/handlers/reclaim.js +38 -0
- package/dist/handlers/reclaim.js.map +1 -0
- package/dist/handlers/services.d.ts +22 -0
- package/dist/handlers/services.d.ts.map +1 -0
- package/dist/handlers/services.js +37 -0
- package/dist/handlers/services.js.map +1 -0
- package/dist/handlers/shareLinks.d.ts +23 -0
- package/dist/handlers/shareLinks.d.ts.map +1 -0
- package/dist/handlers/shareLinks.js +85 -0
- package/dist/handlers/shareLinks.js.map +1 -0
- package/dist/http/index.d.ts +1 -0
- package/dist/http/index.d.ts.map +1 -1
- package/dist/http/index.js +1 -0
- package/dist/http/index.js.map +1 -1
- package/dist/http/login-rate-limit.d.ts +72 -0
- package/dist/http/login-rate-limit.d.ts.map +1 -0
- package/dist/http/login-rate-limit.js +84 -0
- package/dist/http/login-rate-limit.js.map +1 -0
- package/dist/notifications/deliver-invite.d.ts +27 -0
- package/dist/notifications/deliver-invite.d.ts.map +1 -0
- package/dist/notifications/deliver-invite.js +32 -0
- package/dist/notifications/deliver-invite.js.map +1 -0
- package/dist/notifications/index.d.ts +4 -0
- package/dist/notifications/index.d.ts.map +1 -0
- package/dist/notifications/index.js +4 -0
- package/dist/notifications/index.js.map +1 -0
- package/dist/notifications/smtp-config.d.ts +37 -0
- package/dist/notifications/smtp-config.d.ts.map +1 -0
- package/dist/notifications/smtp-config.js +41 -0
- package/dist/notifications/smtp-config.js.map +1 -0
- package/dist/notifications/smtp-provider.d.ts +18 -0
- package/dist/notifications/smtp-provider.d.ts.map +1 -0
- package/dist/notifications/smtp-provider.js +71 -0
- package/dist/notifications/smtp-provider.js.map +1 -0
- package/dist/ops/deploymentConfig.d.ts +73 -0
- package/dist/ops/deploymentConfig.d.ts.map +1 -0
- package/dist/ops/deploymentConfig.js +154 -0
- package/dist/ops/deploymentConfig.js.map +1 -0
- package/dist/ops/index.d.ts +1 -0
- package/dist/ops/index.d.ts.map +1 -1
- package/dist/ops/index.js +3 -2
- package/dist/ops/index.js.map +1 -1
- package/dist/organizations/index.d.ts +3 -0
- package/dist/organizations/index.d.ts.map +1 -0
- package/dist/organizations/index.js +5 -0
- package/dist/organizations/index.js.map +1 -0
- package/dist/organizations/list-all-members.d.ts +10 -0
- package/dist/organizations/list-all-members.d.ts.map +1 -0
- package/dist/organizations/list-all-members.js +36 -0
- package/dist/organizations/list-all-members.js.map +1 -0
- package/dist/organizations/org-asset-service.d.ts +39 -0
- package/dist/organizations/org-asset-service.d.ts.map +1 -0
- package/dist/organizations/org-asset-service.js +71 -0
- package/dist/organizations/org-asset-service.js.map +1 -0
- package/dist/projects/create-project.d.ts +30 -0
- package/dist/projects/create-project.d.ts.map +1 -0
- package/dist/projects/create-project.js +56 -0
- package/dist/projects/create-project.js.map +1 -0
- package/dist/projects/index.d.ts +2 -0
- package/dist/projects/index.d.ts.map +1 -0
- package/dist/projects/index.js +3 -0
- package/dist/projects/index.js.map +1 -0
- package/dist/providers/auth-lookup.d.ts +14 -0
- package/dist/providers/auth-lookup.d.ts.map +1 -0
- package/dist/providers/auth-lookup.js +34 -0
- package/dist/providers/auth-lookup.js.map +1 -0
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +1 -0
- package/dist/providers/index.js.map +1 -1
- package/dist/testing/harness.d.ts +168 -0
- package/dist/testing/harness.d.ts.map +1 -0
- package/dist/testing/harness.js +314 -0
- package/dist/testing/harness.js.map +1 -0
- package/dist/testing/index.d.ts +18 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +15 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/scenarios.d.ts +67 -0
- package/dist/testing/scenarios.d.ts.map +1 -0
- package/dist/testing/scenarios.js +104 -0
- package/dist/testing/scenarios.js.map +1 -0
- package/dist/testing/seed-adapter.d.ts +31 -0
- package/dist/testing/seed-adapter.d.ts.map +1 -0
- package/dist/testing/seed-adapter.js +19 -0
- package/dist/testing/seed-adapter.js.map +1 -0
- package/package.json +42 -3
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Org member role, permissions, and removal.
|
|
3
|
+
*
|
|
4
|
+
* Two gates run on every request: `requireManageOrgMembers` (owner+admin) and
|
|
5
|
+
* `requireActingOrg` (the URL `orgId` must equal `ctx.actingOrgId`, so
|
|
6
|
+
* cross-org member mutation is blocked even for an actor with permissions in
|
|
7
|
+
* another org).
|
|
8
|
+
*
|
|
9
|
+
* The sole-owner invariant applies to demotion and removal alike — an org with
|
|
10
|
+
* no owner is unadministrable, and removing an owner ends their role exactly as
|
|
11
|
+
* demoting them does.
|
|
12
|
+
*/
|
|
13
|
+
import { apiError, ApiErrorCode, noContent, parseBody, requireParams, UpdateOrgMemberBodySchema } from '../api/index.js';
|
|
14
|
+
import { MEMBER_ASSIGNABLE_PERMISSIONS, ALL_ORG_PERMISSIONS, actorFrom, canChangeOrgRole } from '@selvajs/platform';
|
|
15
|
+
import { requireManageOrgMembers, requireActingOrg } from '../access/index.js';
|
|
16
|
+
const ROSTER_PAGE_LIMIT = 200;
|
|
17
|
+
// Runaway guard against an adapter returning a non-advancing cursor. Matches
|
|
18
|
+
// `listAllOrgMembers`; 100 pages is 20k members, past any real org here.
|
|
19
|
+
const MAX_ROSTER_PAGES = 100;
|
|
20
|
+
/**
|
|
21
|
+
* Whether anyone other than `exceptUserId` still owns the org.
|
|
22
|
+
*
|
|
23
|
+
* Both branches below need this and both used to scan the roster themselves —
|
|
24
|
+
* the sole-owner invariant is one rule, so it reads from one place.
|
|
25
|
+
*
|
|
26
|
+
* Pages rather than reading one 200-row window: a second owner sitting past the
|
|
27
|
+
* first page read as "no other owner", so the invariant refused a demotion or
|
|
28
|
+
* removal that was in fact safe. That failed closed, but it made the org
|
|
29
|
+
* unadministrable exactly when it had grown enough to need administering.
|
|
30
|
+
* Returns on the first owner found, so the common case is still one round-trip.
|
|
31
|
+
*/
|
|
32
|
+
async function hasAnotherOwner(req, ctx, orgId, exceptUserId) {
|
|
33
|
+
let cursor;
|
|
34
|
+
for (let page = 0; page < MAX_ROSTER_PAGES; page++) {
|
|
35
|
+
const result = await req.deps.orgs.listOrgMembers(ctx, orgId, {
|
|
36
|
+
limit: ROSTER_PAGE_LIMIT,
|
|
37
|
+
cursor
|
|
38
|
+
});
|
|
39
|
+
if (result.items.some((m) => m.role === 'owner' && m.userId !== exceptUserId && !m.deletedAt)) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
cursor = result.nextCursor;
|
|
43
|
+
if (!cursor)
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
// Cap hit without finding one: report "no other owner" so the caller refuses.
|
|
47
|
+
// Failing closed on an incomplete read is the safe direction for an invariant
|
|
48
|
+
// whose whole job is preventing an unadministrable org.
|
|
49
|
+
req.log.warn('hasAnotherOwner hit the page cap — treating as sole owner', {
|
|
50
|
+
orgId,
|
|
51
|
+
pages: MAX_ROSTER_PAGES
|
|
52
|
+
});
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Projects in `orgId` where `userId` is the only live owner — the ones that
|
|
57
|
+
* will have no owner once `removeOrgMember` cascades their membership rows.
|
|
58
|
+
*
|
|
59
|
+
* **Report, do not block.** Blocking puts an unbounded chore in front of
|
|
60
|
+
* offboarding — the cost scales with how many projects the departing person
|
|
61
|
+
* owned, which is backwards, because the most prolific people are the ones
|
|
62
|
+
* whose departure most needs to be clean. An offboarding that fails halfway is
|
|
63
|
+
* worse than an ownerless project: the person stays in the org while someone
|
|
64
|
+
* works through the backlog. Reclaim already exists to adopt an ownerless
|
|
65
|
+
* project, so the recovery path is built; what was missing was any signal that
|
|
66
|
+
* recovery was needed. Hence the event.
|
|
67
|
+
*
|
|
68
|
+
* MUST be called before the removal — afterwards the rows it reads are gone.
|
|
69
|
+
*/
|
|
70
|
+
async function findProjectsLosingSoleOwner(req, ctx, orgId, userId) {
|
|
71
|
+
const projects = req.deps.projects;
|
|
72
|
+
const orgProjects = await projects.listProjects(ctx, orgId, { limit: ROSTER_PAGE_LIMIT });
|
|
73
|
+
const checks = await Promise.all(orgProjects.items.map(async (project) => {
|
|
74
|
+
const members = await projects.listProjectMembers(ctx, project.id, {
|
|
75
|
+
limit: ROSTER_PAGE_LIMIT
|
|
76
|
+
});
|
|
77
|
+
const owners = members.items.filter((m) => m.role === 'owner' && !m.deletedAt);
|
|
78
|
+
return owners.length === 1 && owners[0].userId === userId ? project : null;
|
|
79
|
+
}));
|
|
80
|
+
return checks.filter((p) => p !== null);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Body accepts either or both:
|
|
84
|
+
* - `{ role: OrgRole }` — change the member's role. Owner-only.
|
|
85
|
+
* - `{ permissions: OrgPermission[] }` — replace the member's grantable
|
|
86
|
+
* permissions. Owner/admin via `manage_org_members`. For `member`-role
|
|
87
|
+
* targets, only `MEMBER_ASSIGNABLE_PERMISSIONS` are accepted; promoting a
|
|
88
|
+
* member's permissions does NOT change their role.
|
|
89
|
+
*/
|
|
90
|
+
export const updateOrgMember = async (req) => {
|
|
91
|
+
const { userId } = requireParams(req.params, 'orgId', 'userId');
|
|
92
|
+
// Owner+admin gate, covering permission edits. The role-change branch adds
|
|
93
|
+
// an owner-only gate below.
|
|
94
|
+
requireManageOrgMembers(req);
|
|
95
|
+
const { ctx, orgId } = requireActingOrg(req, req.params.orgId);
|
|
96
|
+
const patch = await parseBody(req.request, UpdateOrgMemberBodySchema);
|
|
97
|
+
const orgs = req.deps.orgs;
|
|
98
|
+
const [actorMember, target] = await Promise.all([
|
|
99
|
+
orgs.getOrgMember(ctx, orgId, ctx.userId),
|
|
100
|
+
orgs.getOrgMember(ctx, orgId, userId)
|
|
101
|
+
]);
|
|
102
|
+
if (!target)
|
|
103
|
+
apiError(404, ApiErrorCode.NOT_FOUND, 'Member not found in this organization.');
|
|
104
|
+
if (!actorMember) {
|
|
105
|
+
apiError(403, ApiErrorCode.FORBIDDEN, 'You are not a member of this organization.');
|
|
106
|
+
}
|
|
107
|
+
if (patch.role !== undefined && patch.role !== target.role) {
|
|
108
|
+
// Gated on both the role being granted and the one being taken away —
|
|
109
|
+
// demoting an owner is an owner-only act even though `member` is not.
|
|
110
|
+
if (!canChangeOrgRole({ actorMember, role: patch.role }) ||
|
|
111
|
+
!canChangeOrgRole({ actorMember, role: target.role })) {
|
|
112
|
+
apiError(403, ApiErrorCode.FORBIDDEN, 'Only the org owner can change roles.');
|
|
113
|
+
}
|
|
114
|
+
if (target.role === 'owner' &&
|
|
115
|
+
patch.role !== 'owner' &&
|
|
116
|
+
!(await hasAnotherOwner(req, ctx, orgId, target.userId))) {
|
|
117
|
+
apiError(409, ApiErrorCode.CONFLICT, 'Cannot demote the sole owner of this organization. Promote another member to owner first.');
|
|
118
|
+
}
|
|
119
|
+
await orgs.updateOrgMemberRole(ctx, orgId, userId, patch.role);
|
|
120
|
+
}
|
|
121
|
+
if (patch.permissions !== undefined) {
|
|
122
|
+
// Validate against the role the member will have, not the one they had.
|
|
123
|
+
const effectiveRole = patch.role ?? target.role;
|
|
124
|
+
const valid = effectiveRole === 'member'
|
|
125
|
+
? new Set(MEMBER_ASSIGNABLE_PERMISSIONS)
|
|
126
|
+
: new Set(ALL_ORG_PERMISSIONS);
|
|
127
|
+
const invalid = patch.permissions.filter((p) => !valid.has(p));
|
|
128
|
+
if (invalid.length > 0) {
|
|
129
|
+
apiError(400, ApiErrorCode.VALIDATION_FAILED, `Permissions [${invalid.join(', ')}] cannot be assigned to a ${effectiveRole}.`);
|
|
130
|
+
}
|
|
131
|
+
await orgs.updateOrgMemberPermissions(ctx, orgId, userId, patch.permissions);
|
|
132
|
+
}
|
|
133
|
+
return noContent();
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Remove a member from the org. Owner/admin via `manage_org_members`, same
|
|
137
|
+
* tenancy check as PATCH, and the same sole-owner invariant — removing the last
|
|
138
|
+
* owner would leave the org unadministrable.
|
|
139
|
+
*/
|
|
140
|
+
export const removeOrgMember = async (req) => {
|
|
141
|
+
const { userId } = requireParams(req.params, 'orgId', 'userId');
|
|
142
|
+
requireManageOrgMembers(req);
|
|
143
|
+
const { ctx, orgId } = requireActingOrg(req, req.params.orgId);
|
|
144
|
+
const orgs = req.deps.orgs;
|
|
145
|
+
const [actorMember, target] = await Promise.all([
|
|
146
|
+
orgs.getOrgMember(ctx, orgId, ctx.userId),
|
|
147
|
+
orgs.getOrgMember(ctx, orgId, userId)
|
|
148
|
+
]);
|
|
149
|
+
if (!target)
|
|
150
|
+
apiError(404, ApiErrorCode.NOT_FOUND, 'Member not found in this organization.');
|
|
151
|
+
// Same owner-only gate PATCH applies to demotion. Removing an owner ends
|
|
152
|
+
// their role just as demoting them does, so an admin got 403 on the demote
|
|
153
|
+
// and 204 on the remove — the harder-to-reverse of the two.
|
|
154
|
+
if (!canChangeOrgRole({ actorMember, role: target.role })) {
|
|
155
|
+
apiError(403, ApiErrorCode.FORBIDDEN, 'Only the org owner can remove another owner.');
|
|
156
|
+
}
|
|
157
|
+
if (target.role === 'owner' && !(await hasAnotherOwner(req, ctx, orgId, target.userId))) {
|
|
158
|
+
apiError(409, ApiErrorCode.CONFLICT, 'Cannot remove the sole owner of this organization. Promote another member to owner first.');
|
|
159
|
+
}
|
|
160
|
+
// Must run before the removal: the cascade soft-deletes the very rows this
|
|
161
|
+
// reads. Reported, not blocked — see the docblock on `findProjectsLosingSoleOwner`.
|
|
162
|
+
const orphaned = await findProjectsLosingSoleOwner(req, ctx, orgId, userId);
|
|
163
|
+
await orgs.removeOrgMember(ctx, orgId, userId);
|
|
164
|
+
if (orphaned.length > 0) {
|
|
165
|
+
await req.deps.events.emit({
|
|
166
|
+
type: 'org_member.removed_orphaning_projects',
|
|
167
|
+
orgId,
|
|
168
|
+
userId,
|
|
169
|
+
projectIds: orphaned.map((p) => p.id),
|
|
170
|
+
actorId: actorFrom(ctx)
|
|
171
|
+
});
|
|
172
|
+
req.log.warn('Org member removal left projects without an owner', {
|
|
173
|
+
orgId,
|
|
174
|
+
userId,
|
|
175
|
+
actorId: ctx.userId,
|
|
176
|
+
projectCount: orphaned.length
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
// `removeOrgMember` cascades `project_members` but not invites, so a
|
|
180
|
+
// dormant invite would let the removed user walk straight back in at
|
|
181
|
+
// their original role. Best-effort: the removal itself has committed, and
|
|
182
|
+
// failing the request now would report an offboarding that did happen as
|
|
183
|
+
// one that didn't. A failure here leaves a live re-entry path, so it is
|
|
184
|
+
// logged rather than swallowed.
|
|
185
|
+
const email = (await req.deps.auth.getUser(userId))?.email;
|
|
186
|
+
if (email) {
|
|
187
|
+
try {
|
|
188
|
+
await req.deps.invites.revokePendingByEmail(ctx, orgId, email);
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
req.log.error('Removed org member but failed to revoke their pending invites', {
|
|
192
|
+
orgId,
|
|
193
|
+
userId,
|
|
194
|
+
error: err instanceof Error ? err.message : String(err)
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return noContent();
|
|
199
|
+
};
|
|
200
|
+
//# sourceMappingURL=orgMembers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orgMembers.js","sourceRoot":"","sources":["../../src/handlers/orgMembers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The acting org's record and roster.
|
|
3
|
+
*
|
|
4
|
+
* `requireActingOrg` is the tenancy gate on both: the URL `orgId` must equal
|
|
5
|
+
* `ctx.actingOrgId`, so the URL alone never decides which tenant is read.
|
|
6
|
+
*/
|
|
7
|
+
import { type ApiHandler } from '../api/index.js';
|
|
8
|
+
/** The acting org's record — name, slug, and branding asset URLs. */
|
|
9
|
+
export declare const getOrg: ApiHandler;
|
|
10
|
+
/**
|
|
11
|
+
* List the org's members. Any member of the acting org may read this — the
|
|
12
|
+
* roster is what the team page renders, and `requireActingOrg` already confines
|
|
13
|
+
* the read to the caller's own tenant.
|
|
14
|
+
*/
|
|
15
|
+
export declare const listOrgMembers: ApiHandler;
|
|
16
|
+
//# sourceMappingURL=orgs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orgs.d.ts","sourceRoot":"","sources":["../../src/handlers/orgs.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAKN,KAAK,UAAU,EACf,MAAM,iBAAiB,CAAC;AAGzB,qEAAqE;AACrE,eAAO,MAAM,MAAM,EAAE,UAMpB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,cAAc,EAAE,UAI5B,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The acting org's record and roster.
|
|
3
|
+
*
|
|
4
|
+
* `requireActingOrg` is the tenancy gate on both: the URL `orgId` must equal
|
|
5
|
+
* `ctx.actingOrgId`, so the URL alone never decides which tenant is read.
|
|
6
|
+
*/
|
|
7
|
+
import { apiError, ApiErrorCode, collection, parseListOptions } from '../api/index.js';
|
|
8
|
+
import { requireActingOrg } from '../access/index.js';
|
|
9
|
+
/** The acting org's record — name, slug, and branding asset URLs. */
|
|
10
|
+
export const getOrg = async (req) => {
|
|
11
|
+
const { ctx, orgId } = requireActingOrg(req, req.params.orgId);
|
|
12
|
+
const org = await req.deps.orgs.getOrg(ctx, orgId);
|
|
13
|
+
if (!org)
|
|
14
|
+
apiError(404, ApiErrorCode.NOT_FOUND, 'Organization not found');
|
|
15
|
+
return { body: org };
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* List the org's members. Any member of the acting org may read this — the
|
|
19
|
+
* roster is what the team page renders, and `requireActingOrg` already confines
|
|
20
|
+
* the read to the caller's own tenant.
|
|
21
|
+
*/
|
|
22
|
+
export const listOrgMembers = async (req) => {
|
|
23
|
+
const { ctx, orgId } = requireActingOrg(req, req.params.orgId);
|
|
24
|
+
return collection(await req.deps.orgs.listOrgMembers(ctx, orgId, parseListOptions(req.url)));
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=orgs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orgs.js","sourceRoot":"","sources":["../../src/handlers/orgs.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACN,QAAQ,EACR,YAAY,EACZ,UAAU,EACV,gBAAgB,EAEhB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtD,qEAAqE;AACrE,MAAM,CAAC,MAAM,MAAM,GAAe,KAAK,EAAE,GAAG,EAAE,EAAE;IAC/C,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,gBAAgB,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAE/D,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACnD,IAAI,CAAC,GAAG;QAAE,QAAQ,CAAC,GAAG,EAAE,YAAY,CAAC,SAAS,EAAE,wBAAwB,CAAC,CAAC;IAC1E,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC;AACtB,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAe,KAAK,EAAE,GAAG,EAAE,EAAE;IACvD,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,gBAAgB,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAE/D,OAAO,UAAU,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,KAAK,EAAE,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAC9F,CAAC,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project membership: list, add, change role, remove.
|
|
3
|
+
*
|
|
4
|
+
* Every method here gates on `requireCanManage(req, id, 'members')`. The
|
|
5
|
+
* owner-count preconditions on PATCH and DELETE come from `checkOwnerRemoval`,
|
|
6
|
+
* a pure rules function, so demotion and removal cannot drift apart — a sole
|
|
7
|
+
* owner demoting themselves locks the project exactly as removing themselves
|
|
8
|
+
* would.
|
|
9
|
+
*/
|
|
10
|
+
import type { ApiHandler } from '../api/index.js';
|
|
11
|
+
export declare const listProjectMembers: ApiHandler;
|
|
12
|
+
export declare const addProjectMember: ApiHandler;
|
|
13
|
+
export declare const updateProjectMemberRole: ApiHandler;
|
|
14
|
+
export declare const removeProjectMember: ApiHandler;
|
|
15
|
+
//# sourceMappingURL=projectMembers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projectMembers.d.ts","sourceRoot":"","sources":["../../src/handlers/projectMembers.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAeH,OAAO,KAAK,EAAE,UAAU,EAAc,MAAM,iBAAiB,CAAC;AAI9D,eAAO,MAAM,kBAAkB,EAAE,UAMhC,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,UAwB9B,CAAC;AAuCF,eAAO,MAAM,uBAAuB,EAAE,UAmBrC,CAAC;AAEF,eAAO,MAAM,mBAAmB,EAAE,UAejC,CAAC"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project membership: list, add, change role, remove.
|
|
3
|
+
*
|
|
4
|
+
* Every method here gates on `requireCanManage(req, id, 'members')`. The
|
|
5
|
+
* owner-count preconditions on PATCH and DELETE come from `checkOwnerRemoval`,
|
|
6
|
+
* a pure rules function, so demotion and removal cannot drift apart — a sole
|
|
7
|
+
* owner demoting themselves locks the project exactly as removing themselves
|
|
8
|
+
* would.
|
|
9
|
+
*/
|
|
10
|
+
import { AddProjectMemberBodySchema, apiError, ApiErrorCode, collection, created, noContent, parseBody, parseListOptions, requireCaller, requireParams, UpdateProjectMemberBodySchema } from '../api/index.js';
|
|
11
|
+
import { checkOwnerRemoval } from '@selvajs/platform';
|
|
12
|
+
import { requireCanManage, requireTargetIsOrgMember } from '../access/index.js';
|
|
13
|
+
export const listProjectMembers = async (req) => {
|
|
14
|
+
const { id } = requireParams(req.params, 'id');
|
|
15
|
+
const { ctx } = requireCaller(req);
|
|
16
|
+
await requireCanManage(req, id, 'members');
|
|
17
|
+
return collection(await req.deps.projects.listProjectMembers(ctx, id, parseListOptions(req.url)));
|
|
18
|
+
};
|
|
19
|
+
export const addProjectMember = async (req) => {
|
|
20
|
+
const { id } = requireParams(req.params, 'id');
|
|
21
|
+
const { ctx } = requireCaller(req);
|
|
22
|
+
await requireCanManage(req, id, 'members');
|
|
23
|
+
const input = await parseBody(req.request, AddProjectMemberBodySchema);
|
|
24
|
+
const project = await req.deps.projects.getProject(ctx, id);
|
|
25
|
+
if (!project)
|
|
26
|
+
apiError(404, ApiErrorCode.NOT_FOUND, 'Project not found');
|
|
27
|
+
await requireTargetIsOrgMember(req, project.orgId, input.userId);
|
|
28
|
+
const now = new Date().toISOString();
|
|
29
|
+
const member = {
|
|
30
|
+
projectId: id,
|
|
31
|
+
userId: input.userId,
|
|
32
|
+
role: input.role,
|
|
33
|
+
joinedAt: now,
|
|
34
|
+
updatedAt: now,
|
|
35
|
+
updatedBy: ctx.userId || input.userId,
|
|
36
|
+
deletedAt: null
|
|
37
|
+
};
|
|
38
|
+
await req.deps.projects.addProjectMember(ctx, member);
|
|
39
|
+
return created(member);
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Guard the owner count before a demotion or removal goes through.
|
|
43
|
+
*
|
|
44
|
+
* `?confirm=true` is what distinguishes "I meant to do this" from a misclick;
|
|
45
|
+
* the sole-owner case has no confirmation, because there is no state the
|
|
46
|
+
* caller could confirm into that leaves the project manageable.
|
|
47
|
+
*/
|
|
48
|
+
async function assertOwnerRemovable(req, projectId, target, action) {
|
|
49
|
+
const { ctx } = requireCaller(req);
|
|
50
|
+
const page = await req.deps.projects.listProjectMembers(ctx, projectId, { limit: 200 });
|
|
51
|
+
const decision = checkOwnerRemoval({
|
|
52
|
+
target: { role: target.role },
|
|
53
|
+
allMembers: page.items.map((m) => ({ role: m.role })),
|
|
54
|
+
confirmed: req.url.searchParams.get('confirm') === 'true'
|
|
55
|
+
});
|
|
56
|
+
if (decision === 'sole_owner') {
|
|
57
|
+
apiError(409, ApiErrorCode.CONFLICT, `Cannot ${action} the sole owner of a project. Assign another owner first, or use reclaim to add a co-owner.`);
|
|
58
|
+
}
|
|
59
|
+
if (decision === 'needs_confirm') {
|
|
60
|
+
apiError(409, ApiErrorCode.CONFLICT, `${action === 'demote' ? 'Demoting' : 'Removing'} another project owner requires explicit confirmation. Retry with ?confirm=true.`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export const updateProjectMemberRole = async (req) => {
|
|
64
|
+
const { id, userId } = requireParams(req.params, 'id', 'userId');
|
|
65
|
+
const { ctx } = requireCaller(req);
|
|
66
|
+
await requireCanManage(req, id, 'members');
|
|
67
|
+
const { role } = await parseBody(req.request, UpdateProjectMemberBodySchema);
|
|
68
|
+
// Demoting an owner reduces the owner count exactly like removing one, so
|
|
69
|
+
// it runs the same guard DELETE does. Without this a sole owner can PATCH
|
|
70
|
+
// themselves to `viewer` and lock the project — `canManage`,
|
|
71
|
+
// `canEditProjectSettings` and `canEdit` all go false at once.
|
|
72
|
+
if (role !== 'owner') {
|
|
73
|
+
const target = await req.deps.projects.getProjectMember(ctx, id, userId);
|
|
74
|
+
if (target?.role === 'owner') {
|
|
75
|
+
await assertOwnerRemovable(req, id, target, 'demote');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
await req.deps.projects.updateProjectMemberRole(ctx, id, userId, role);
|
|
79
|
+
return noContent();
|
|
80
|
+
};
|
|
81
|
+
export const removeProjectMember = async (req) => {
|
|
82
|
+
const { id, userId } = requireParams(req.params, 'id', 'userId');
|
|
83
|
+
const { ctx } = requireCaller(req);
|
|
84
|
+
await requireCanManage(req, id, 'members');
|
|
85
|
+
const target = await req.deps.projects.getProjectMember(ctx, id, userId);
|
|
86
|
+
// Idempotent: already gone, or never there.
|
|
87
|
+
if (!target)
|
|
88
|
+
return noContent();
|
|
89
|
+
if (target.role === 'owner') {
|
|
90
|
+
await assertOwnerRemovable(req, id, target, 'remove');
|
|
91
|
+
}
|
|
92
|
+
await req.deps.projects.removeProjectMember(ctx, id, userId);
|
|
93
|
+
return noContent();
|
|
94
|
+
};
|
|
95
|
+
//# sourceMappingURL=projectMembers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projectMembers.js","sourceRoot":"","sources":["../../src/handlers/projectMembers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project handlers: the collection, and a single project's detail and settings.
|
|
3
|
+
*
|
|
4
|
+
* `listProjects` was authored against `resolveAccessibleProjects`, not lifted
|
|
5
|
+
* from the pre-v1 handler: that one listed every project in the acting org with
|
|
6
|
+
* no `canView` filter and had no UI caller, so it carried no evidence of being
|
|
7
|
+
* correct. The rest are lifted, logic intact, with reads moved onto `req.deps`.
|
|
8
|
+
*/
|
|
9
|
+
import { type ApiHandler } from '../api/index.js';
|
|
10
|
+
export declare const listProjects: ApiHandler;
|
|
11
|
+
export declare const createProject: ApiHandler;
|
|
12
|
+
/**
|
|
13
|
+
* Project detail plus the caller's effective capabilities on it.
|
|
14
|
+
*
|
|
15
|
+
* A project the caller cannot view returns 404, not 403 — `403` would confirm
|
|
16
|
+
* the id exists to anyone probing.
|
|
17
|
+
*/
|
|
18
|
+
export declare const getProject: ApiHandler;
|
|
19
|
+
export declare const updateProject: ApiHandler;
|
|
20
|
+
export declare const deleteProject: ApiHandler;
|
|
21
|
+
//# sourceMappingURL=projects.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projects.d.ts","sourceRoot":"","sources":["../../src/handlers/projects.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAYN,KAAK,UAAU,EACf,MAAM,iBAAiB,CAAC;AAsBzB,eAAO,MAAM,YAAY,EAAE,UAkB1B,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,UAiD3B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,UAAU,EAAE,UAiCxB,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,UAuE3B,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,UAO3B,CAAC"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project handlers: the collection, and a single project's detail and settings.
|
|
3
|
+
*
|
|
4
|
+
* `listProjects` was authored against `resolveAccessibleProjects`, not lifted
|
|
5
|
+
* from the pre-v1 handler: that one listed every project in the acting org with
|
|
6
|
+
* no `canView` filter and had no UI caller, so it carried no evidence of being
|
|
7
|
+
* correct. The rest are lifted, logic intact, with reads moved onto `req.deps`.
|
|
8
|
+
*/
|
|
9
|
+
import { randomUUID } from 'node:crypto';
|
|
10
|
+
import { apiError, ApiErrorCode, collection, created, noContent, parseBody, parseListOptions, requireCaller, requireParams, CreateProjectBodySchema, UpdateProjectBodySchema } from '../api/index.js';
|
|
11
|
+
import { canChangeVisibilityToPublic, canEdit, canSolve, canView, hasPermission, slugify, SYSTEM_CONTEXT, validateProjectFlags, withAdminBypass } from '@selvajs/platform';
|
|
12
|
+
import { resolveAccessibleProjects } from '../definitions/index.js';
|
|
13
|
+
import { projectAccessInputFromRowsWith, requireCanCreateProject, requireCanEditProjectSettings, requireCanManage } from '../access/index.js';
|
|
14
|
+
import { createProjectWithUniqueSlug } from '../projects/index.js';
|
|
15
|
+
export const listProjects = async (req) => {
|
|
16
|
+
const { ctx } = requireCaller(req);
|
|
17
|
+
const { projects } = await resolveAccessibleProjects(ctx, req.deps);
|
|
18
|
+
const { limit, cursor } = parseListOptions(req.url);
|
|
19
|
+
// The accessible set is resolved in-process rather than by the store, so the
|
|
20
|
+
// cursor is an index into it. Opaque to callers either way.
|
|
21
|
+
const start = cursor ? Number(cursor) : 0;
|
|
22
|
+
if (!Number.isInteger(start) || start < 0) {
|
|
23
|
+
apiError(400, ApiErrorCode.VALIDATION_FAILED, 'Invalid cursor');
|
|
24
|
+
}
|
|
25
|
+
const items = projects.slice(start, start + (limit ?? projects.length));
|
|
26
|
+
const nextIndex = start + items.length;
|
|
27
|
+
return collection({
|
|
28
|
+
items,
|
|
29
|
+
nextCursor: nextIndex < projects.length ? String(nextIndex) : undefined
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
export const createProject = async (req) => {
|
|
33
|
+
const { ctx, user } = requireCaller(req);
|
|
34
|
+
if (!ctx.actingOrgId)
|
|
35
|
+
apiError(400, ApiErrorCode.VALIDATION_FAILED, 'No active organization');
|
|
36
|
+
// Throws SvelteKit's `error()`; the host binding folds that into the
|
|
37
|
+
// envelope. Guards stay shared with the page loads that need them.
|
|
38
|
+
await requireCanCreateProject(req, ctx.actingOrgId);
|
|
39
|
+
const input = await parseBody(req.request, CreateProjectBodySchema);
|
|
40
|
+
// Same gate as PATCH: a `platform` project belongs to no org, so creating
|
|
41
|
+
// one here would put it beyond its own org's reach — and beyond anyone's
|
|
42
|
+
// with ENABLE_PLATFORM_PROJECTS off. Platform projects are created
|
|
43
|
+
// through /api/admin/projects.
|
|
44
|
+
if (input.visibility === 'platform' && !hasPermission(ctx, 'instance_admin')) {
|
|
45
|
+
apiError(403, ApiErrorCode.FORBIDDEN, 'Only a platform admin can create a project with platform visibility.');
|
|
46
|
+
}
|
|
47
|
+
const autoJoinOnUpload = input.autoJoinOnUpload ?? false;
|
|
48
|
+
// Same call PATCH makes, rather than a second hand-rolled copy of the
|
|
49
|
+
// flag/visibility invariant.
|
|
50
|
+
const flagIssues = validateProjectFlags({ visibility: input.visibility, autoJoinOnUpload });
|
|
51
|
+
if (flagIssues.length > 0) {
|
|
52
|
+
apiError(400, ApiErrorCode.VALIDATION_FAILED, flagIssues.map((i) => `${i.path}: ${i.message}`).join('; '));
|
|
53
|
+
}
|
|
54
|
+
const project = await createProjectWithUniqueSlug(req.deps.projects, {
|
|
55
|
+
id: randomUUID(),
|
|
56
|
+
orgId: ctx.actingOrgId,
|
|
57
|
+
name: input.name,
|
|
58
|
+
description: input.description,
|
|
59
|
+
visibility: input.visibility,
|
|
60
|
+
ownerId: user.id,
|
|
61
|
+
createdBy: user.id,
|
|
62
|
+
updatedBy: user.id,
|
|
63
|
+
autoJoinOnUpload
|
|
64
|
+
}, { writeCtx: ctx, fallbackSlug: 'project', conflictScope: 'this organization' });
|
|
65
|
+
return created(project);
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Project detail plus the caller's effective capabilities on it.
|
|
69
|
+
*
|
|
70
|
+
* A project the caller cannot view returns 404, not 403 — `403` would confirm
|
|
71
|
+
* the id exists to anyone probing.
|
|
72
|
+
*/
|
|
73
|
+
export const getProject = async (req) => {
|
|
74
|
+
const { id } = requireParams(req.params, 'id');
|
|
75
|
+
const { ctx } = requireCaller(req);
|
|
76
|
+
// Read as SYSTEM to decide visibility here rather than let the store decide:
|
|
77
|
+
// the 404-not-403 rule above needs the row before it can hide it.
|
|
78
|
+
const project = await req.deps.projects.getProject(SYSTEM_CONTEXT, id);
|
|
79
|
+
if (!project)
|
|
80
|
+
apiError(404, ApiErrorCode.NOT_FOUND, 'Project not found');
|
|
81
|
+
const [orgMembers, projectMembers, grants] = await Promise.all([
|
|
82
|
+
req.deps.orgs.getOrgMembersFor(SYSTEM_CONTEXT, [project.orgId], ctx.userId),
|
|
83
|
+
req.deps.projects.getProjectMembersFor(SYSTEM_CONTEXT, [project.id], ctx.userId),
|
|
84
|
+
project.visibility === 'platform'
|
|
85
|
+
? req.deps.platformProjectGrants.listByProject(SYSTEM_CONTEXT, project.id)
|
|
86
|
+
: Promise.resolve([])
|
|
87
|
+
]);
|
|
88
|
+
const member = projectMembers.get(project.id) ?? null;
|
|
89
|
+
const input = projectAccessInputFromRowsWith(req, ctx, project, {
|
|
90
|
+
member,
|
|
91
|
+
orgMember: orgMembers.get(project.orgId) ?? null,
|
|
92
|
+
platformGrants: grants
|
|
93
|
+
});
|
|
94
|
+
if (!canView(input))
|
|
95
|
+
apiError(404, ApiErrorCode.NOT_FOUND, 'Project not found');
|
|
96
|
+
return {
|
|
97
|
+
body: {
|
|
98
|
+
...project,
|
|
99
|
+
role: member?.role ?? null,
|
|
100
|
+
canEdit: canEdit(input),
|
|
101
|
+
canSolve: canSolve(input)
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
export const updateProject = async (req) => {
|
|
106
|
+
const { id } = requireParams(req.params, 'id');
|
|
107
|
+
// `requireCanEditProjectSettings` is owner-only at the project level — that
|
|
108
|
+
// IS the gate for editing settings. The platform-scope `manage_projects`
|
|
109
|
+
// check that used to live here was redundant for org owners/admins (they
|
|
110
|
+
// always carry it via DEFAULT_ORG_PERMISSIONS) and a regression for plain
|
|
111
|
+
// org members who happened to own a project — they'd hit 403 on their own
|
|
112
|
+
// project's settings.
|
|
113
|
+
const { ctx, project: existing } = await requireCanEditProjectSettings(req, id);
|
|
114
|
+
const input = await parseBody(req.request, UpdateProjectBodySchema);
|
|
115
|
+
// `platform` takes a project out of its org entirely: no org member can
|
|
116
|
+
// see it, `canReclaim` returns false forever, and with
|
|
117
|
+
// ENABLE_PLATFORM_PROJECTS off (the default) every rule denies everyone,
|
|
118
|
+
// including admins. Escalating into it must be instance-admin only.
|
|
119
|
+
if (input.visibility === 'platform' && existing.visibility !== 'platform') {
|
|
120
|
+
if (!hasPermission(ctx, 'instance_admin')) {
|
|
121
|
+
apiError(403, ApiErrorCode.FORBIDDEN, 'Only a platform admin can give a project platform visibility.');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Flipping *to* public is a disclosure action — a stricter gate than a
|
|
125
|
+
// normal edit.
|
|
126
|
+
if (input.visibility === 'public' && existing.visibility !== 'public') {
|
|
127
|
+
const orgMember = await req.deps.orgs.getOrgMember(ctx, existing.orgId, ctx.userId);
|
|
128
|
+
const passes = withAdminBypass(ctx.platformPermissions, () => canChangeVisibilityToPublic({ orgMember }));
|
|
129
|
+
if (!passes) {
|
|
130
|
+
apiError(403, ApiErrorCode.FORBIDDEN, 'Only org owners or admins can make a project public.');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// The flag/visibility invariant runs on the merged post-patch shape, so a
|
|
134
|
+
// partial update is validated against what the project will become.
|
|
135
|
+
const flagIssues = validateProjectFlags({
|
|
136
|
+
visibility: input.visibility ?? existing.visibility,
|
|
137
|
+
autoJoinOnUpload: input.autoJoinOnUpload ?? existing.autoJoinOnUpload
|
|
138
|
+
});
|
|
139
|
+
if (flagIssues.length > 0) {
|
|
140
|
+
apiError(400, ApiErrorCode.VALIDATION_FAILED, flagIssues.map((i) => `${i.path}: ${i.message}`).join('; '));
|
|
141
|
+
}
|
|
142
|
+
const patch = {};
|
|
143
|
+
// A renamed project gets a re-derived slug; the two must not drift apart.
|
|
144
|
+
if (input.name !== undefined) {
|
|
145
|
+
patch.name = input.name;
|
|
146
|
+
patch.slug = slugify(input.name);
|
|
147
|
+
}
|
|
148
|
+
if (input.description !== undefined)
|
|
149
|
+
patch.description = input.description ?? undefined;
|
|
150
|
+
if (input.visibility !== undefined)
|
|
151
|
+
patch.visibility = input.visibility;
|
|
152
|
+
if (input.autoJoinOnUpload !== undefined)
|
|
153
|
+
patch.autoJoinOnUpload = input.autoJoinOnUpload;
|
|
154
|
+
await req.deps.projects.updateProject(ctx, id, patch);
|
|
155
|
+
return noContent();
|
|
156
|
+
};
|
|
157
|
+
export const deleteProject = async (req) => {
|
|
158
|
+
const { id } = requireParams(req.params, 'id');
|
|
159
|
+
const { ctx } = requireCaller(req);
|
|
160
|
+
await requireCanManage(req, id);
|
|
161
|
+
await req.deps.projects.deleteProject(ctx, id);
|
|
162
|
+
return noContent();
|
|
163
|
+
};
|
|
164
|
+
//# sourceMappingURL=projects.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projects.js","sourceRoot":"","sources":["../../src/handlers/projects.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Org owner/admin escape hatch for a project they cannot otherwise reach.
|
|
3
|
+
*
|
|
4
|
+
* Adds the actor as a co-owner and does NOT demote the existing owner.
|
|
5
|
+
* Idempotent: reclaiming twice is a no-op, because `addProjectMember`
|
|
6
|
+
* reactivates a soft-deleted row in place.
|
|
7
|
+
*/
|
|
8
|
+
import { type ApiHandler } from '../api/index.js';
|
|
9
|
+
export declare const reclaimProject: ApiHandler;
|
|
10
|
+
//# sourceMappingURL=reclaim.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reclaim.d.ts","sourceRoot":"","sources":["../../src/handlers/reclaim.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAA0B,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAI1E,eAAO,MAAM,cAAc,EAAE,UA8B5B,CAAC"}
|