better-auth-organization-member 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/README.md +245 -0
- package/dist/client-B1z87oFG.d.mts +26 -0
- package/dist/client-B1z87oFG.d.mts.map +1 -0
- package/dist/client-PBBSie1a.mjs +11 -0
- package/dist/client-PBBSie1a.mjs.map +1 -0
- package/dist/client.d.mts +3 -0
- package/dist/client.d.ts +21 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +7 -0
- package/dist/client.js.map +1 -0
- package/dist/client.mjs +3 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +4 -0
- package/dist/server-Big_p9zG.mjs +1017 -0
- package/dist/server-Big_p9zG.mjs.map +1 -0
- package/dist/server-uBIvbfMk.d.mts +119 -0
- package/dist/server-uBIvbfMk.d.mts.map +1 -0
- package/dist/server.d.mts +2 -0
- package/dist/server.d.ts +115 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +649 -0
- package/dist/server.js.map +1 -0
- package/dist/server.mjs +3 -0
- package/package.json +54 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
/** @format */
|
|
2
|
+
import { BASE_ERROR_CODES, defineErrorCodes, } from "better-auth";
|
|
3
|
+
import { createAuthEndpoint, createAuthMiddleware, sessionMiddleware, } from "better-auth/api";
|
|
4
|
+
import { clientSideHasPermission } from "better-auth/client/plugins";
|
|
5
|
+
import { toZodSchema } from "better-auth/db";
|
|
6
|
+
import { getOrgAdapter, } from "better-auth/plugins";
|
|
7
|
+
import { APIError } from "better-call";
|
|
8
|
+
import z from "zod";
|
|
9
|
+
const ORGANIZATION_ERROR_CODES = defineErrorCodes({
|
|
10
|
+
NO_ACTIVE_ORGANIZATION: "No active organization",
|
|
11
|
+
MEMBER_NOT_FOUND: "Member not found",
|
|
12
|
+
ORGANIZATION_NOT_FOUND: "Organization not found",
|
|
13
|
+
YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER: "You are not allowed to update this member",
|
|
14
|
+
YOU_CANNOT_LEAVE_THE_ORGANIZATION_WITHOUT_AN_OWNER: "You cannot leave the organization without an owner",
|
|
15
|
+
INVITATION_NOT_FOUND: "Invitation not found",
|
|
16
|
+
YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_INVITATION: "You are not allowed to update this invitation",
|
|
17
|
+
ONLY_PENDING_INVITATIONS_CAN_BE_UPDATED: "Only pending invitations can be updated",
|
|
18
|
+
YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION: "You are not a member of this organization",
|
|
19
|
+
});
|
|
20
|
+
const getOrganizationPlugin = (ctx) => {
|
|
21
|
+
return ctx.options.plugins?.find((plugin) => plugin.id === "organization");
|
|
22
|
+
};
|
|
23
|
+
export const orgMiddleware = createAuthMiddleware(async () => {
|
|
24
|
+
return {};
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* The middleware forces the endpoint to require a valid session by utilizing the `sessionMiddleware`.
|
|
28
|
+
* It also appends additional types to the session type regarding organizations.
|
|
29
|
+
*/
|
|
30
|
+
export const orgSessionMiddleware = createAuthMiddleware({
|
|
31
|
+
use: [sessionMiddleware],
|
|
32
|
+
}, async (ctx) => {
|
|
33
|
+
const session = ctx.context.session;
|
|
34
|
+
return {
|
|
35
|
+
session,
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
39
|
+
* Organization Member Plugin
|
|
40
|
+
*
|
|
41
|
+
* Extends the organization plugin with:
|
|
42
|
+
* - updateMember endpoint to update member fields (not just role)
|
|
43
|
+
* - Automatically adds afterAcceptInvitation hook to transfer invitation data to member
|
|
44
|
+
*
|
|
45
|
+
* @requires organization plugin
|
|
46
|
+
*/
|
|
47
|
+
const createUpdateMemberEndpoint = (options) => {
|
|
48
|
+
// from /better-auth/packages/better-auth/src/plugins/organization/routes/crud-members.ts
|
|
49
|
+
const baseMemberSchema = z.object({
|
|
50
|
+
role: z.union([z.string(), z.array(z.string())]).meta({
|
|
51
|
+
description: 'The new role to be applied. This can be a string or array of strings representing the roles. Eg: ["admin", "sale"]',
|
|
52
|
+
}),
|
|
53
|
+
memberId: z.string().meta({
|
|
54
|
+
description: 'The member id to apply the role update to. Eg: "member-id"',
|
|
55
|
+
}),
|
|
56
|
+
organizationId: z
|
|
57
|
+
.string()
|
|
58
|
+
.meta({
|
|
59
|
+
description: 'An optional organization ID which the member is a part of to apply the role update. If not provided, you must provide session headers to get the active organization. Eg: "organization-id"',
|
|
60
|
+
})
|
|
61
|
+
.optional(),
|
|
62
|
+
// Manually add default member fields
|
|
63
|
+
firstName: z.string().optional().meta({
|
|
64
|
+
description: "First name of the member",
|
|
65
|
+
}),
|
|
66
|
+
lastName: z.string().optional().meta({
|
|
67
|
+
description: "Last name of the member",
|
|
68
|
+
}),
|
|
69
|
+
avatar: z.string().optional().meta({
|
|
70
|
+
description: "Avatar URL of the member",
|
|
71
|
+
}),
|
|
72
|
+
status: z.enum(["active", "inactive"]).optional(),
|
|
73
|
+
});
|
|
74
|
+
const additionalFieldsSchema = toZodSchema({
|
|
75
|
+
fields: options?.schema?.member?.additionalFields || {},
|
|
76
|
+
isClientSide: true,
|
|
77
|
+
});
|
|
78
|
+
// Remove status field from additionalFieldsSchema to prevent overriding the strict enum in baseMemberSchema
|
|
79
|
+
const { status: _status, ...additionalFieldsWithoutStatus } = additionalFieldsSchema.shape;
|
|
80
|
+
return createAuthEndpoint("/organization/update-member", {
|
|
81
|
+
method: "POST",
|
|
82
|
+
body: z.object({
|
|
83
|
+
...baseMemberSchema.shape,
|
|
84
|
+
...additionalFieldsWithoutStatus.shape,
|
|
85
|
+
}),
|
|
86
|
+
use: [orgMiddleware, orgSessionMiddleware],
|
|
87
|
+
requireHeaders: true,
|
|
88
|
+
metadata: {
|
|
89
|
+
$Infer: {
|
|
90
|
+
body: {},
|
|
91
|
+
},
|
|
92
|
+
openapi: {
|
|
93
|
+
operationId: "updateOrganizationMember",
|
|
94
|
+
description: "Update a member in an organization",
|
|
95
|
+
responses: {
|
|
96
|
+
"200": {
|
|
97
|
+
description: "Success",
|
|
98
|
+
content: {
|
|
99
|
+
"application/json": {
|
|
100
|
+
schema: {
|
|
101
|
+
type: "object",
|
|
102
|
+
properties: {
|
|
103
|
+
id: {
|
|
104
|
+
type: "string",
|
|
105
|
+
},
|
|
106
|
+
userId: {
|
|
107
|
+
type: "string",
|
|
108
|
+
},
|
|
109
|
+
organizationId: {
|
|
110
|
+
type: "string",
|
|
111
|
+
},
|
|
112
|
+
firstName: {
|
|
113
|
+
type: "string",
|
|
114
|
+
},
|
|
115
|
+
lastName: {
|
|
116
|
+
type: "string",
|
|
117
|
+
},
|
|
118
|
+
avatar: {
|
|
119
|
+
type: "string",
|
|
120
|
+
},
|
|
121
|
+
status: {
|
|
122
|
+
type: "string",
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
required: ["id", "userId", "organizationId", "role"],
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
}, async (ctx) => {
|
|
134
|
+
const session = ctx.context.session;
|
|
135
|
+
const organizationPlugin = getOrganizationPlugin(ctx.context);
|
|
136
|
+
if (!organizationPlugin) {
|
|
137
|
+
throw new Error("organization-member plugin requires the organization plugin");
|
|
138
|
+
}
|
|
139
|
+
if (!ctx.body.role) {
|
|
140
|
+
throw new APIError("BAD_REQUEST");
|
|
141
|
+
}
|
|
142
|
+
const organizationId = ctx.body.organizationId || session.session.activeOrganizationId;
|
|
143
|
+
if (!organizationId) {
|
|
144
|
+
throw new APIError("BAD_REQUEST", {
|
|
145
|
+
message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const adapter = getOrgAdapter(ctx.context, organizationPlugin.options);
|
|
149
|
+
const roleToSet = Array.isArray(ctx.body.role)
|
|
150
|
+
? ctx.body.role
|
|
151
|
+
: ctx.body.role
|
|
152
|
+
? [ctx.body.role]
|
|
153
|
+
: [];
|
|
154
|
+
const member = await adapter.findMemberByOrgId({
|
|
155
|
+
userId: session.user.id,
|
|
156
|
+
organizationId: organizationId,
|
|
157
|
+
});
|
|
158
|
+
if (!member) {
|
|
159
|
+
throw new APIError("BAD_REQUEST", {
|
|
160
|
+
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
const toBeUpdatedMember = member.id !== ctx.body.memberId
|
|
164
|
+
? await adapter.findMemberById(ctx.body.memberId)
|
|
165
|
+
: member;
|
|
166
|
+
if (!toBeUpdatedMember) {
|
|
167
|
+
throw new APIError("BAD_REQUEST", {
|
|
168
|
+
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const memberBelongsToOrganization = toBeUpdatedMember.organizationId === organizationId;
|
|
172
|
+
if (!memberBelongsToOrganization) {
|
|
173
|
+
throw new APIError("FORBIDDEN", {
|
|
174
|
+
message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
const creatorRole = organizationPlugin.options?.creatorRole || "owner";
|
|
178
|
+
const updatingMemberRoles = member.role.split(",");
|
|
179
|
+
const toBeUpdatedMemberRoles = toBeUpdatedMember.role.split(",");
|
|
180
|
+
const isUpdatingCreator = toBeUpdatedMemberRoles.includes(creatorRole);
|
|
181
|
+
const updaterIsCreator = updatingMemberRoles.includes(creatorRole);
|
|
182
|
+
const isSettingCreatorRole = roleToSet.includes(creatorRole);
|
|
183
|
+
const memberIsUpdatingThemselves = member.id === toBeUpdatedMember.id;
|
|
184
|
+
if ((isUpdatingCreator && !updaterIsCreator) ||
|
|
185
|
+
(isSettingCreatorRole && !updaterIsCreator)) {
|
|
186
|
+
throw new APIError("FORBIDDEN", {
|
|
187
|
+
message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
if (updaterIsCreator && memberIsUpdatingThemselves) {
|
|
191
|
+
const members = await ctx.context.adapter.findMany({
|
|
192
|
+
model: "member",
|
|
193
|
+
where: [
|
|
194
|
+
{
|
|
195
|
+
field: "organizationId",
|
|
196
|
+
value: organizationId,
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
});
|
|
200
|
+
const owners = members.filter((member) => {
|
|
201
|
+
const roles = member.role.split(",");
|
|
202
|
+
return roles.includes(creatorRole);
|
|
203
|
+
});
|
|
204
|
+
if (owners.length <= 1 && !isSettingCreatorRole) {
|
|
205
|
+
throw new APIError("BAD_REQUEST", {
|
|
206
|
+
message: ORGANIZATION_ERROR_CODES.YOU_CANNOT_LEAVE_THE_ORGANIZATION_WITHOUT_AN_OWNER,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// currently use clientSideHasPermission instead of hasPermission
|
|
211
|
+
// this does not support dynamic access control yet
|
|
212
|
+
// TODO: improve me
|
|
213
|
+
const canUpdateMember = clientSideHasPermission({
|
|
214
|
+
role: member.role,
|
|
215
|
+
options: organizationPlugin.options,
|
|
216
|
+
permissions: {
|
|
217
|
+
member: ["update"],
|
|
218
|
+
},
|
|
219
|
+
allowCreatorAllPermissions: true,
|
|
220
|
+
});
|
|
221
|
+
if (!canUpdateMember) {
|
|
222
|
+
throw new APIError("FORBIDDEN", {
|
|
223
|
+
message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_MEMBER,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
// Get organization
|
|
227
|
+
const organization = await adapter.findOrganizationById(organizationId);
|
|
228
|
+
if (!organization) {
|
|
229
|
+
throw new APIError("BAD_REQUEST", {
|
|
230
|
+
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
const userBeingUpdated = await ctx.context.internalAdapter.findUserById(toBeUpdatedMember.userId);
|
|
234
|
+
if (!userBeingUpdated) {
|
|
235
|
+
throw new APIError("BAD_REQUEST", {
|
|
236
|
+
message: BASE_ERROR_CODES.USER_NOT_FOUND,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
// Extract updates (exclude memberId and organizationId from body)
|
|
240
|
+
const { memberId: _, organizationId: __, ...updates } = ctx.body;
|
|
241
|
+
let finalUpdates = updates;
|
|
242
|
+
// Run beforeUpdateMember hook (if provided via options parameter)
|
|
243
|
+
if (options?.organizationMemberHooks?.beforeUpdateMember) {
|
|
244
|
+
const response = await options.organizationMemberHooks.beforeUpdateMember({
|
|
245
|
+
member: toBeUpdatedMember,
|
|
246
|
+
updates,
|
|
247
|
+
user: userBeingUpdated,
|
|
248
|
+
organization: organization,
|
|
249
|
+
});
|
|
250
|
+
if (response && typeof response === "object" && "data" in response) {
|
|
251
|
+
finalUpdates = response.data;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Update the member - NOTE: this is different from updateMemberRole
|
|
255
|
+
// which only updates the role field. We update any additional fields.
|
|
256
|
+
const updatedMember = await ctx.context.adapter.update({
|
|
257
|
+
model: "member",
|
|
258
|
+
where: [{ field: "id", value: ctx.body.memberId }],
|
|
259
|
+
update: finalUpdates,
|
|
260
|
+
});
|
|
261
|
+
if (!updatedMember) {
|
|
262
|
+
throw new APIError("BAD_REQUEST", {
|
|
263
|
+
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
// Run afterUpdateMember hook (if provided via options parameter)
|
|
267
|
+
if (options?.organizationMemberHooks?.afterUpdateMember) {
|
|
268
|
+
await options.organizationMemberHooks.afterUpdateMember({
|
|
269
|
+
member: updatedMember,
|
|
270
|
+
user: userBeingUpdated,
|
|
271
|
+
organization: organization,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return ctx.json(updatedMember);
|
|
275
|
+
});
|
|
276
|
+
};
|
|
277
|
+
const createGetMemberEndpoint = () => {
|
|
278
|
+
return createAuthEndpoint("/organization/get-member", {
|
|
279
|
+
method: "GET",
|
|
280
|
+
query: z.object({
|
|
281
|
+
userId: z.string().meta({
|
|
282
|
+
description: 'The user id of the member to retrieve. Eg: "user-id"',
|
|
283
|
+
}).optional(),
|
|
284
|
+
organizationId: z
|
|
285
|
+
.string()
|
|
286
|
+
.meta({
|
|
287
|
+
description: 'The organization ID which the member belongs to. If not provided, will default to the user\'s active organization. Eg: "organization-id"',
|
|
288
|
+
})
|
|
289
|
+
.optional(),
|
|
290
|
+
organizationSlug: z
|
|
291
|
+
.string()
|
|
292
|
+
.meta({
|
|
293
|
+
description: 'The organization slug to get member for. If not provided, will default to the user\'s active organization. Eg: "organization-slug"',
|
|
294
|
+
})
|
|
295
|
+
.optional(),
|
|
296
|
+
}),
|
|
297
|
+
use: [orgMiddleware, orgSessionMiddleware],
|
|
298
|
+
requireHeaders: true,
|
|
299
|
+
metadata: {
|
|
300
|
+
$Infer: {
|
|
301
|
+
query: {},
|
|
302
|
+
},
|
|
303
|
+
openapi: {
|
|
304
|
+
operationId: "getOrganizationMember",
|
|
305
|
+
description: "Get a member from an organization by user ID",
|
|
306
|
+
responses: {
|
|
307
|
+
"200": {
|
|
308
|
+
description: "Success",
|
|
309
|
+
content: {
|
|
310
|
+
"application/json": {
|
|
311
|
+
schema: {
|
|
312
|
+
type: "object",
|
|
313
|
+
properties: {
|
|
314
|
+
id: {
|
|
315
|
+
type: "string",
|
|
316
|
+
},
|
|
317
|
+
userId: {
|
|
318
|
+
type: "string",
|
|
319
|
+
},
|
|
320
|
+
organizationId: {
|
|
321
|
+
type: "string",
|
|
322
|
+
},
|
|
323
|
+
role: {
|
|
324
|
+
type: "string",
|
|
325
|
+
},
|
|
326
|
+
firstName: {
|
|
327
|
+
type: "string",
|
|
328
|
+
},
|
|
329
|
+
lastName: {
|
|
330
|
+
type: "string",
|
|
331
|
+
},
|
|
332
|
+
avatar: {
|
|
333
|
+
type: "string",
|
|
334
|
+
},
|
|
335
|
+
status: {
|
|
336
|
+
type: "string",
|
|
337
|
+
},
|
|
338
|
+
createdAt: {
|
|
339
|
+
type: "string",
|
|
340
|
+
format: "date-time",
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
required: ["id", "userId", "organizationId", "role"],
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
}, async (ctx) => {
|
|
352
|
+
const session = ctx.context.session;
|
|
353
|
+
const organizationPlugin = getOrganizationPlugin(ctx.context);
|
|
354
|
+
if (!organizationPlugin) {
|
|
355
|
+
throw new Error("organization-member plugin requires the organization plugin");
|
|
356
|
+
}
|
|
357
|
+
const adapter = getOrgAdapter(ctx.context, organizationPlugin.options);
|
|
358
|
+
// Resolve organizationId from slug if provided
|
|
359
|
+
let organizationId = ctx.query?.organizationId || session.session.activeOrganizationId;
|
|
360
|
+
const userId = ctx.query.userId || session.user.id;
|
|
361
|
+
if (!userId) {
|
|
362
|
+
throw new APIError("BAD_REQUEST", {
|
|
363
|
+
message: BASE_ERROR_CODES.USER_NOT_FOUND,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
if (ctx.query?.organizationSlug) {
|
|
367
|
+
const organization = await adapter.findOrganizationBySlug(ctx.query.organizationSlug);
|
|
368
|
+
if (!organization) {
|
|
369
|
+
throw new APIError("BAD_REQUEST", {
|
|
370
|
+
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
organizationId = organization.id;
|
|
374
|
+
}
|
|
375
|
+
if (!organizationId) {
|
|
376
|
+
throw new APIError("BAD_REQUEST", {
|
|
377
|
+
message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
// Verify the current user is a member of the organization
|
|
381
|
+
const currentUserMember = await adapter.findMemberByOrgId({
|
|
382
|
+
userId: session.user.id,
|
|
383
|
+
organizationId: organizationId,
|
|
384
|
+
});
|
|
385
|
+
if (!currentUserMember) {
|
|
386
|
+
throw new APIError("FORBIDDEN", {
|
|
387
|
+
message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
// Return current user member if the query is empty
|
|
391
|
+
if (!ctx.query.userId && (ctx.query.organizationSlug || ctx.query.organizationId)) {
|
|
392
|
+
return ctx.json(currentUserMember);
|
|
393
|
+
}
|
|
394
|
+
// Get the requested member by userId and organizationId
|
|
395
|
+
const requestedMember = await adapter.findMemberByOrgId({
|
|
396
|
+
userId: userId,
|
|
397
|
+
organizationId: organizationId,
|
|
398
|
+
});
|
|
399
|
+
if (!requestedMember) {
|
|
400
|
+
throw new APIError("BAD_REQUEST", {
|
|
401
|
+
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
return ctx.json(requestedMember);
|
|
405
|
+
});
|
|
406
|
+
};
|
|
407
|
+
const createUpdateInvitationEndpoint = (options) => {
|
|
408
|
+
const baseInvitationSchema = z.object({
|
|
409
|
+
invitationId: z.string().meta({
|
|
410
|
+
description: 'The invitation id to update. Eg: "invitation-id"',
|
|
411
|
+
}),
|
|
412
|
+
organizationId: z
|
|
413
|
+
.string()
|
|
414
|
+
.meta({
|
|
415
|
+
description: 'An optional organization ID which the invitation belongs to. If not provided, you must provide session headers to get the active organization. Eg: "organization-id"',
|
|
416
|
+
})
|
|
417
|
+
.optional(),
|
|
418
|
+
});
|
|
419
|
+
const additionalFieldsSchema = toZodSchema({
|
|
420
|
+
fields: options?.schema?.invitation?.additionalFields || {},
|
|
421
|
+
isClientSide: true,
|
|
422
|
+
});
|
|
423
|
+
return createAuthEndpoint("/organization/update-invitation", {
|
|
424
|
+
method: "POST",
|
|
425
|
+
body: z.object({
|
|
426
|
+
...baseInvitationSchema.shape,
|
|
427
|
+
...additionalFieldsSchema.shape,
|
|
428
|
+
role: z
|
|
429
|
+
.union([z.string(), z.array(z.string())])
|
|
430
|
+
.optional()
|
|
431
|
+
.meta({
|
|
432
|
+
description: 'The new role to be applied. This can be a string or array of strings representing the roles. Eg: ["admin", "sale"]',
|
|
433
|
+
}),
|
|
434
|
+
}),
|
|
435
|
+
use: [orgMiddleware, orgSessionMiddleware],
|
|
436
|
+
requireHeaders: true,
|
|
437
|
+
metadata: {
|
|
438
|
+
$Infer: {
|
|
439
|
+
body: {},
|
|
440
|
+
},
|
|
441
|
+
openapi: {
|
|
442
|
+
operationId: "updateOrganizationInvitation",
|
|
443
|
+
description: "Update a pending invitation in an organization",
|
|
444
|
+
responses: {
|
|
445
|
+
"200": {
|
|
446
|
+
description: "Success",
|
|
447
|
+
content: {
|
|
448
|
+
"application/json": {
|
|
449
|
+
schema: {
|
|
450
|
+
type: "object",
|
|
451
|
+
properties: {
|
|
452
|
+
id: {
|
|
453
|
+
type: "string",
|
|
454
|
+
},
|
|
455
|
+
organizationId: {
|
|
456
|
+
type: "string",
|
|
457
|
+
},
|
|
458
|
+
email: {
|
|
459
|
+
type: "string",
|
|
460
|
+
},
|
|
461
|
+
role: {
|
|
462
|
+
type: "string",
|
|
463
|
+
},
|
|
464
|
+
status: {
|
|
465
|
+
type: "string",
|
|
466
|
+
},
|
|
467
|
+
createdAt: {
|
|
468
|
+
type: "string",
|
|
469
|
+
format: "date-time",
|
|
470
|
+
description: "Timestamp when the team was created",
|
|
471
|
+
},
|
|
472
|
+
updatedAt: {
|
|
473
|
+
type: "string",
|
|
474
|
+
format: "date-time",
|
|
475
|
+
description: "Timestamp when the team was last updated",
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
required: [
|
|
479
|
+
"id",
|
|
480
|
+
"organizationId",
|
|
481
|
+
"email",
|
|
482
|
+
"role",
|
|
483
|
+
"status",
|
|
484
|
+
],
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
}, async (ctx) => {
|
|
493
|
+
const session = ctx.context.session;
|
|
494
|
+
const organizationPlugin = getOrganizationPlugin(ctx.context);
|
|
495
|
+
if (!organizationPlugin) {
|
|
496
|
+
throw new Error("organization-member plugin requires the organization plugin");
|
|
497
|
+
}
|
|
498
|
+
const organizationId = ctx.body.organizationId || session.session.activeOrganizationId;
|
|
499
|
+
if (!organizationId) {
|
|
500
|
+
throw new APIError("BAD_REQUEST", {
|
|
501
|
+
message: ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
const adapter = getOrgAdapter(ctx.context, organizationPlugin.options);
|
|
505
|
+
// Get the current user's member record
|
|
506
|
+
const member = await adapter.findMemberByOrgId({
|
|
507
|
+
userId: session.user.id,
|
|
508
|
+
organizationId: organizationId,
|
|
509
|
+
});
|
|
510
|
+
if (!member) {
|
|
511
|
+
throw new APIError("BAD_REQUEST", {
|
|
512
|
+
message: ORGANIZATION_ERROR_CODES.MEMBER_NOT_FOUND,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
// Check permissions - user must have permission to manage invitations
|
|
516
|
+
const canManageInvitations = clientSideHasPermission({
|
|
517
|
+
role: member.role,
|
|
518
|
+
options: organizationPlugin.options,
|
|
519
|
+
permissions: {
|
|
520
|
+
invitation: ["create"],
|
|
521
|
+
},
|
|
522
|
+
allowCreatorAllPermissions: true,
|
|
523
|
+
});
|
|
524
|
+
if (!canManageInvitations) {
|
|
525
|
+
throw new APIError("FORBIDDEN", {
|
|
526
|
+
message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_INVITATION,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
// Get the invitation
|
|
530
|
+
const invitation = await adapter.findInvitationById(ctx.body.invitationId);
|
|
531
|
+
if (!invitation) {
|
|
532
|
+
throw new APIError("BAD_REQUEST", {
|
|
533
|
+
message: ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
// Verify invitation belongs to the organization
|
|
537
|
+
if (invitation.organizationId !== organizationId) {
|
|
538
|
+
throw new APIError("FORBIDDEN", {
|
|
539
|
+
message: ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_ALLOWED_TO_UPDATE_THIS_INVITATION,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
// Only allow updating pending invitations
|
|
543
|
+
if (invitation.status !== "pending") {
|
|
544
|
+
throw new APIError("BAD_REQUEST", {
|
|
545
|
+
message: ORGANIZATION_ERROR_CODES.ONLY_PENDING_INVITATIONS_CAN_BE_UPDATED,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
// Get organization
|
|
549
|
+
const organization = await adapter.findOrganizationById(organizationId);
|
|
550
|
+
if (!organization) {
|
|
551
|
+
throw new APIError("BAD_REQUEST", {
|
|
552
|
+
message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
// Extract updates (exclude invitationId and organizationId from body)
|
|
556
|
+
const { invitationId: _, organizationId: __, ...updates } = ctx.body;
|
|
557
|
+
let finalUpdates = updates;
|
|
558
|
+
// Run beforeUpdateInvitation hook (if provided via options parameter)
|
|
559
|
+
if (options?.organizationMemberHooks?.beforeUpdateInvitation) {
|
|
560
|
+
const response = await options.organizationMemberHooks.beforeUpdateInvitation({
|
|
561
|
+
invitation,
|
|
562
|
+
updates,
|
|
563
|
+
inviter: session.user,
|
|
564
|
+
organization,
|
|
565
|
+
});
|
|
566
|
+
if (response && typeof response === "object" && "data" in response) {
|
|
567
|
+
finalUpdates = response.data;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
// Update the invitation
|
|
571
|
+
const updatedInvitation = await ctx.context.adapter.update({
|
|
572
|
+
model: "invitation",
|
|
573
|
+
where: [{ field: "id", value: ctx.body.invitationId }],
|
|
574
|
+
update: finalUpdates,
|
|
575
|
+
});
|
|
576
|
+
if (!updatedInvitation) {
|
|
577
|
+
throw new APIError("BAD_REQUEST", {
|
|
578
|
+
message: ORGANIZATION_ERROR_CODES.INVITATION_NOT_FOUND,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
// Run afterUpdateInvitation hook (if provided via options parameter)
|
|
582
|
+
if (options?.organizationMemberHooks?.afterUpdateInvitation) {
|
|
583
|
+
await options.organizationMemberHooks.afterUpdateInvitation({
|
|
584
|
+
invitation: updatedInvitation,
|
|
585
|
+
inviter: session.user,
|
|
586
|
+
organization,
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
return ctx.json(updatedInvitation);
|
|
590
|
+
});
|
|
591
|
+
};
|
|
592
|
+
export const organizationMember = (userConfig) => {
|
|
593
|
+
const config = {
|
|
594
|
+
...userConfig,
|
|
595
|
+
};
|
|
596
|
+
return {
|
|
597
|
+
id: "organization-member",
|
|
598
|
+
init(ctx) {
|
|
599
|
+
const organizationPlugin = getOrganizationPlugin(ctx);
|
|
600
|
+
if (!organizationPlugin) {
|
|
601
|
+
throw new Error("organization-member plugin requires the organization plugin");
|
|
602
|
+
}
|
|
603
|
+
// Get or create organizationHooks
|
|
604
|
+
if (!organizationPlugin.options.organizationHooks) {
|
|
605
|
+
organizationPlugin.options.organizationHooks = {};
|
|
606
|
+
}
|
|
607
|
+
const existingAfterAcceptInvitation = organizationPlugin.options.organizationHooks?.afterAcceptInvitation;
|
|
608
|
+
organizationPlugin.options.organizationHooks.afterAcceptInvitation =
|
|
609
|
+
async (data) => {
|
|
610
|
+
await existingAfterAcceptInvitation?.(data);
|
|
611
|
+
try {
|
|
612
|
+
const { invitation, member } = data;
|
|
613
|
+
const updateData = {};
|
|
614
|
+
// TODO improve me
|
|
615
|
+
for (const field of Object.keys(organizationPlugin.options?.schema?.invitation
|
|
616
|
+
?.additionalFields ?? {})) {
|
|
617
|
+
const fieldName = organizationPlugin.options?.schema?.invitation
|
|
618
|
+
?.additionalFields?.[field]?.fieldName || field;
|
|
619
|
+
updateData[fieldName] = invitation[fieldName];
|
|
620
|
+
}
|
|
621
|
+
// Only update if there are fields to transfer
|
|
622
|
+
if (Object.keys(updateData).length > 0) {
|
|
623
|
+
await ctx.adapter.update({
|
|
624
|
+
model: "member",
|
|
625
|
+
where: [{ field: "id", value: member.id }],
|
|
626
|
+
update: updateData,
|
|
627
|
+
});
|
|
628
|
+
ctx.logger?.info("Member fields updated from invitation:", {
|
|
629
|
+
memberId: member.id,
|
|
630
|
+
fields: Object.keys(updateData),
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
catch (error) {
|
|
635
|
+
ctx.logger?.error("Failed to update member fields from invitation:", error);
|
|
636
|
+
// Don't throw - let the invitation acceptance succeed
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
},
|
|
640
|
+
endpoints: {
|
|
641
|
+
getMember: createGetMemberEndpoint(),
|
|
642
|
+
updateMember: createUpdateMemberEndpoint(userConfig),
|
|
643
|
+
updateInvitation: createUpdateInvitationEndpoint(userConfig),
|
|
644
|
+
},
|
|
645
|
+
options: userConfig,
|
|
646
|
+
$ERROR_CODES: ORGANIZATION_ERROR_CODES,
|
|
647
|
+
};
|
|
648
|
+
};
|
|
649
|
+
//# sourceMappingURL=server.js.map
|