@protobase/server 0.7.0 → 0.8.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/assistant/conversations.d.ts +1 -1
- package/dist/assistant/tools.d.ts +1 -1
- package/dist/assistant/turn.d.ts +2 -2
- package/dist/audit/origin.js +12 -0
- package/dist/auth/jwt.d.ts +2 -0
- package/dist/auth/jwt.js +5 -3
- package/dist/batch/reorder.js +1 -1
- package/dist/better-auth/authenticator.d.ts +7 -3
- package/dist/better-auth/authenticator.js +24 -3
- package/dist/better-auth/create-auth.d.ts +619 -81
- package/dist/better-auth/create-auth.js +64 -27
- package/dist/better-auth/organizations/accept-invitation.js +80 -0
- package/dist/better-auth/organizations/active-organization.js +12 -0
- package/dist/better-auth/organizations/admin-routes.js +83 -0
- package/dist/better-auth/organizations/app-roles.js +43 -0
- package/dist/better-auth/organizations/host.d.ts +53 -0
- package/dist/better-auth/organizations/host.js +251 -0
- package/dist/better-auth/organizations/invitation-endpoints.js +133 -0
- package/dist/better-auth/organizations/invitation-mail.js +17 -0
- package/dist/better-auth/organizations/invitation-token.js +27 -0
- package/dist/better-auth/organizations/member-endpoints.js +182 -0
- package/dist/better-auth/organizations/options.d.ts +48 -0
- package/dist/better-auth/organizations/options.js +23 -0
- package/dist/better-auth/organizations/organization-plugin.js +128 -0
- package/dist/better-auth/organizations/protobase-plugin.js +21 -0
- package/dist/better-auth/organizations/role-definitions.d.ts +15 -0
- package/dist/better-auth/organizations/role-definitions.js +56 -0
- package/dist/better-auth/organizations/session-organization.js +68 -0
- package/dist/better-auth/organizations/store.d.ts +19 -0
- package/dist/better-auth/organizations/store.js +73 -0
- package/dist/better-auth/staff-checks.js +5 -1
- package/dist/better-auth/staff-sign-in-endpoints.js +1 -1
- package/dist/better-auth/status-route.js +14 -3
- package/dist/better-auth/users.d.ts +2 -1
- package/dist/better-auth/users.js +4 -1
- package/dist/create-admin.d.ts +8 -2
- package/dist/create-admin.js +31 -2
- package/dist/deps.d.ts +1 -0
- package/dist/files/attach.js +80 -0
- package/dist/files/cleanup-runner.d.ts +8 -0
- package/dist/files/cleanup-runner.js +58 -0
- package/dist/files/cleanup-schedule.d.ts +28 -0
- package/dist/files/cleanup-schedule.js +48 -0
- package/dist/files/detect.js +51 -0
- package/dist/files/download-url.js +43 -0
- package/dist/files/download.js +90 -0
- package/dist/files/duration.js +35 -0
- package/dist/files/file-cleanup.d.ts +18 -0
- package/dist/files/file-cleanup.js +17 -0
- package/dist/files/local-files.d.ts +17 -0
- package/dist/files/local-files.js +101 -0
- package/dist/files/media-types.js +99 -0
- package/dist/files/options.d.ts +15 -0
- package/dist/files/present.d.ts +15 -0
- package/dist/files/present.js +59 -0
- package/dist/files/processors.d.ts +15 -0
- package/dist/files/processors.js +39 -0
- package/dist/files/providers.js +70 -0
- package/dist/files/routes.js +26 -0
- package/dist/files/runtime.d.ts +3 -0
- package/dist/files/runtime.js +37 -0
- package/dist/files/signer.js +32 -0
- package/dist/files/store.d.ts +37 -0
- package/dist/files/ticket.js +87 -0
- package/dist/files/upload-body.js +85 -0
- package/dist/files/upload.js +124 -0
- package/dist/files/uri.js +47 -0
- package/dist/functions/define-function.d.ts +71 -0
- package/dist/functions/define-function.js +36 -0
- package/dist/functions/function-app.d.ts +17 -0
- package/dist/functions/function-app.js +19 -0
- package/dist/functions/read-json.d.ts +35 -0
- package/dist/functions/read-json.js +24 -0
- package/dist/functions/records.d.ts +43 -0
- package/dist/functions/records.js +31 -0
- package/dist/functions/require-env.d.ts +7 -0
- package/dist/functions/require-env.js +12 -0
- package/dist/functions/routes.js +60 -0
- package/dist/functions/verify-signature.d.ts +32 -0
- package/dist/functions/verify-signature.js +49 -0
- package/dist/index.d.ts +21 -3
- package/dist/index.js +13 -3
- package/dist/list-service.js +4 -2
- package/dist/openapi/resource-paths.js +78 -1
- package/dist/openapi/schemas.js +28 -3
- package/dist/project-config.d.ts +7 -1
- package/dist/project-config.js +37 -1
- package/dist/read-service.js +5 -1
- package/dist/request-access.d.ts +5 -0
- package/dist/request-access.js +9 -4
- package/dist/resource-source.d.ts +4 -1
- package/dist/routes/batch.js +1 -1
- package/dist/routes/collection.js +3 -1
- package/dist/routes/record.js +2 -5
- package/dist/types.d.ts +25 -5
- package/dist/write-pipeline.js +2 -1
- package/dist/write-shared.js +6 -3
- package/dist/write-update.js +2 -1
- package/package.json +6 -4
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { readMailSettings } from "../mail/mail-settings.js";
|
|
2
2
|
import { smtpMailer } from "../mail/smtp-mailer.js";
|
|
3
3
|
import { readOperatorSettings, resolveOperator } from "./operator-provider.js";
|
|
4
|
+
import { roleDefinitions } from "./organizations/role-definitions.js";
|
|
5
|
+
import { lastOrganizationField, organizationClaims, sessionOrganizationHooks } from "./organizations/session-organization.js";
|
|
6
|
+
import { organizationPlugin } from "./organizations/organization-plugin.js";
|
|
7
|
+
import { protobaseOrganizationPlugin } from "./organizations/protobase-plugin.js";
|
|
8
|
+
import { resolveOrganizations } from "./organizations/options.js";
|
|
4
9
|
import { passwordResetOptions } from "./password-reset.js";
|
|
5
10
|
import { platformSignInFor, platformSignInProvider, readPlatformSignIn, resolvePlatformSignIn } from "./platform-sign-in.js";
|
|
6
11
|
import { signInPolicyPlugin } from "./policy-plugin.js";
|
|
@@ -28,19 +33,16 @@ const authBasePath = "/api/auth";
|
|
|
28
33
|
* `createUser`, or by an admin through the admin plugin), roles from the admin plugin's `role` column, 15 minute JWTs
|
|
29
34
|
* at `/api/auth/token` with the keys at `/api/auth/jwks`.
|
|
30
35
|
*/
|
|
31
|
-
const roleName = /^[a-z][a-z0-9_-]*$/;
|
|
32
36
|
/** The configured roles with `admin` first; `admin` is added when missing. */
|
|
33
|
-
const normalizeRoles = (roles
|
|
34
|
-
const bad = roles.find((role) => !roleName.test(role));
|
|
35
|
-
if (bad !== void 0) throw new Error(`Invalid role "${bad}": use lowercase letters, digits, "_" or "-"`);
|
|
36
|
-
return ["admin", ...new Set(roles.filter((role) => role !== "admin"))];
|
|
37
|
-
};
|
|
37
|
+
const normalizeRoles = (roles) => roleDefinitions(roles).names;
|
|
38
38
|
const adminImpersonationPaths = ["/admin/impersonate-user", "/admin/stop-impersonating"];
|
|
39
|
+
const noContext = () => Promise.reject(/* @__PURE__ */ new Error("The auth context is read only once createAuth has made the instance"));
|
|
40
|
+
const organizationPaths = ["/organization/set-active", "/organization/accept-invitation"];
|
|
39
41
|
/**
|
|
40
42
|
* The options `createAuth` gives Better Auth, with the roles, default role, mailer, operator provider and platform
|
|
41
43
|
* sign-in provider already resolved.
|
|
42
44
|
*/
|
|
43
|
-
const betterAuthOptions = (options, { roles, defaultRole, mailer, operator, platformSignIn }) => {
|
|
45
|
+
const betterAuthOptions = (options, { roles, defaultRole, mailer, operator, platformSignIn, organizations, context = noContext }) => {
|
|
44
46
|
const secure = options.baseURL.startsWith("https://");
|
|
45
47
|
const signIn = platformSignInFor(platformSignIn, Object.keys(options.socialProviders ?? {}));
|
|
46
48
|
return {
|
|
@@ -61,7 +63,10 @@ const betterAuthOptions = (options, { roles, defaultRole, mailer, operator, plat
|
|
|
61
63
|
...mailer && passwordResetOptions(mailer)
|
|
62
64
|
},
|
|
63
65
|
...options.socialProviders && { socialProviders: options.socialProviders },
|
|
64
|
-
user:
|
|
66
|
+
user: organizations ? {
|
|
67
|
+
...coreNames.user,
|
|
68
|
+
additionalFields: lastOrganizationField
|
|
69
|
+
} : coreNames.user,
|
|
65
70
|
session: coreNames.session,
|
|
66
71
|
verification: coreNames.verification,
|
|
67
72
|
account: {
|
|
@@ -81,9 +86,10 @@ const betterAuthOptions = (options, { roles, defaultRole, mailer, operator, plat
|
|
|
81
86
|
expirationTime: tokenLifetime,
|
|
82
87
|
issuer: options.baseURL,
|
|
83
88
|
audience: options.baseURL,
|
|
84
|
-
definePayload: ({ user }) => ({
|
|
89
|
+
definePayload: async ({ user, session }) => ({
|
|
85
90
|
email: user.email,
|
|
86
|
-
role: user.role
|
|
91
|
+
role: user.role,
|
|
92
|
+
...organizations && await organizationClaims((await context()).adapter, user, session.activeOrganizationId)
|
|
87
93
|
})
|
|
88
94
|
},
|
|
89
95
|
disableSettingJwtHeader: true
|
|
@@ -108,21 +114,37 @@ const betterAuthOptions = (options, { roles, defaultRole, mailer, operator, plat
|
|
|
108
114
|
staffSignInPlugin({
|
|
109
115
|
...operator && { operator },
|
|
110
116
|
...mailer && { mailer }
|
|
111
|
-
})
|
|
117
|
+
}),
|
|
118
|
+
...organizations ? [organizationPlugin({
|
|
119
|
+
resolved: organizations,
|
|
120
|
+
secret: options.secret,
|
|
121
|
+
...mailer && { mailer },
|
|
122
|
+
context
|
|
123
|
+
}), protobaseOrganizationPlugin({
|
|
124
|
+
resolved: organizations,
|
|
125
|
+
mail: Boolean(mailer)
|
|
126
|
+
})] : []
|
|
112
127
|
],
|
|
113
|
-
disabledPaths: [
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
128
|
+
disabledPaths: [
|
|
129
|
+
...unusedEmailCodePaths,
|
|
130
|
+
...adminImpersonationPaths,
|
|
131
|
+
...organizations ? organizationPaths : []
|
|
132
|
+
],
|
|
133
|
+
databaseHooks: {
|
|
134
|
+
user: { create: {
|
|
135
|
+
before: async (user, context) => {
|
|
136
|
+
const given = String(user.role ?? "").split(",").map((name) => name.trim()).filter(Boolean);
|
|
137
|
+
const unknown = given.find((name) => !roles.includes(name));
|
|
138
|
+
if (given.length === 0 || unknown !== void 0) throw new APIError("BAD_REQUEST", { message: `Choose a role for the new user: ${roles.join(", ")}` });
|
|
139
|
+
return { data: context?.path === "/admin/create-user" ? {
|
|
140
|
+
...user,
|
|
141
|
+
emailVerified: true
|
|
142
|
+
} : user };
|
|
143
|
+
},
|
|
144
|
+
...options.onUserCreated && { after: options.onUserCreated }
|
|
145
|
+
} },
|
|
146
|
+
...organizations && { session: sessionOrganizationHooks(context) }
|
|
147
|
+
},
|
|
126
148
|
rateLimit: {
|
|
127
149
|
enabled: true,
|
|
128
150
|
window: 60,
|
|
@@ -135,6 +157,10 @@ const betterAuthOptions = (options, { roles, defaultRole, mailer, operator, plat
|
|
|
135
157
|
"/staff/sign-in": {
|
|
136
158
|
window: 60,
|
|
137
159
|
max: 5
|
|
160
|
+
},
|
|
161
|
+
"/invitation/sign-up": {
|
|
162
|
+
window: 60,
|
|
163
|
+
max: options.signInPerMinute ?? 5
|
|
138
164
|
}
|
|
139
165
|
}
|
|
140
166
|
},
|
|
@@ -149,25 +175,36 @@ const betterAuthOptions = (options, { roles, defaultRole, mailer, operator, plat
|
|
|
149
175
|
};
|
|
150
176
|
};
|
|
151
177
|
const createAuth = (options) => {
|
|
152
|
-
const
|
|
178
|
+
const definitions = roleDefinitions(options.roles, { organizations: options.organizations !== void 0 });
|
|
179
|
+
const roles = definitions.names;
|
|
153
180
|
if (options.defaultRole !== void 0 && !roles.includes(options.defaultRole)) throw new Error(`defaultRole "${options.defaultRole}" is not one of the roles: ${roles.join(", ")}`);
|
|
154
181
|
const others = roles.filter((role) => role !== "admin");
|
|
155
|
-
const defaultRole = options.defaultRole ?? (others.length === 1 ? others[0] : void 0);
|
|
182
|
+
const defaultRole = options.defaultRole ?? (options.organizations ? "user" : others.length === 1 ? others[0] : void 0);
|
|
156
183
|
const mailer = options.mailer === void 0 ? platformMailer() : options.mailer || void 0;
|
|
157
184
|
const provider = options.operator === void 0 ? platformOperator() : options.operator || void 0;
|
|
158
185
|
const operator = provider && resolveOperator(provider);
|
|
159
186
|
const signInSettings = options.signInProvider === void 0 ? platformSignInSettings() : options.signInProvider || void 0;
|
|
160
187
|
const socialProviders = Object.keys(options.socialProviders ?? {});
|
|
161
188
|
const platformSignIn = platformSignInFor(signInSettings && resolvePlatformSignIn(signInSettings), socialProviders);
|
|
189
|
+
const organizations = options.organizations && resolveOrganizations(options.organizations, definitions, options.baseURL);
|
|
190
|
+
let instance;
|
|
191
|
+
const context = () => instance?.$context ?? Promise.reject(/* @__PURE__ */ new Error("The auth instance is not made yet"));
|
|
162
192
|
const auth = betterAuth(betterAuthOptions(options, {
|
|
163
193
|
roles,
|
|
164
194
|
defaultRole,
|
|
165
195
|
mailer,
|
|
166
196
|
operator,
|
|
167
|
-
platformSignIn
|
|
197
|
+
platformSignIn,
|
|
198
|
+
...organizations && {
|
|
199
|
+
organizations,
|
|
200
|
+
context
|
|
201
|
+
}
|
|
168
202
|
}));
|
|
203
|
+
instance = auth;
|
|
169
204
|
return Object.assign(auth, {
|
|
170
205
|
roles,
|
|
206
|
+
roleDefinitions: definitions,
|
|
207
|
+
organizations,
|
|
171
208
|
defaultRole,
|
|
172
209
|
mail: Boolean(mailer),
|
|
173
210
|
passwordReset: Boolean(mailer),
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { parseAppRoles } from "./app-roles.js";
|
|
2
|
+
import { findMember, membersOf } from "./store.js";
|
|
3
|
+
import { invitationIdOf, verifyInvitationToken } from "./invitation-token.js";
|
|
4
|
+
import { APIError } from "better-auth/api";
|
|
5
|
+
//#region src/better-auth/organizations/accept-invitation.ts
|
|
6
|
+
const invalid = {
|
|
7
|
+
code: "INVITATION_INVALID",
|
|
8
|
+
message: "This invitation link is not valid any more: it was used, revoked, replaced by a newer one, or it expired."
|
|
9
|
+
};
|
|
10
|
+
/** The pending invitation an emailed link's token names, or a 400 `INVITATION_INVALID`. */
|
|
11
|
+
const invitationOfToken = async (context, token) => {
|
|
12
|
+
const id = invitationIdOf(token);
|
|
13
|
+
const row = id && await context.adapter.findOne({
|
|
14
|
+
model: "invitation",
|
|
15
|
+
where: [{
|
|
16
|
+
field: "id",
|
|
17
|
+
value: id
|
|
18
|
+
}]
|
|
19
|
+
});
|
|
20
|
+
if (!row || !await verifyInvitationToken(context.secret, token, row)) throw new APIError("BAD_REQUEST", invalid);
|
|
21
|
+
if (row.status !== "pending" || new Date(row.expiresAt).getTime() < Date.now()) throw new APIError("BAD_REQUEST", invalid);
|
|
22
|
+
return {
|
|
23
|
+
...row,
|
|
24
|
+
appRoles: parseAppRoles(row.appRoles)
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Makes `user` a member as the invitation says, with its organization role and app roles, in one transaction with the
|
|
29
|
+
* invitation's acceptance, so a member never exists without the app roles they were invited with. Better Auth's own
|
|
30
|
+
* accept leaves the app roles out; it is turned off with organizations.
|
|
31
|
+
*/
|
|
32
|
+
const acceptInvitation = async (context, invitation, user, membershipLimit) => {
|
|
33
|
+
if (invitation.email.toLowerCase() !== user.email.toLowerCase()) throw new APIError("FORBIDDEN", {
|
|
34
|
+
code: "NOT_THE_INVITED_ADDRESS",
|
|
35
|
+
message: `This invitation is for ${invitation.email}; sign in with that address to accept it.`
|
|
36
|
+
});
|
|
37
|
+
if (!user.emailVerified) throw new APIError("FORBIDDEN", {
|
|
38
|
+
code: "EMAIL_NOT_VERIFIED",
|
|
39
|
+
message: "Verify your email address before accepting an invitation."
|
|
40
|
+
});
|
|
41
|
+
if (await findMember(context.adapter, {
|
|
42
|
+
organizationId: invitation.organizationId,
|
|
43
|
+
userId: user.id
|
|
44
|
+
})) throw new APIError("BAD_REQUEST", {
|
|
45
|
+
code: "ALREADY_A_MEMBER",
|
|
46
|
+
message: "You are a member of this organization already."
|
|
47
|
+
});
|
|
48
|
+
if ((await membersOf(context.adapter, invitation.organizationId)).length >= membershipLimit) throw new APIError("FORBIDDEN", {
|
|
49
|
+
code: "MEMBERSHIP_LIMIT_REACHED",
|
|
50
|
+
message: "This organization has as many members as it can have."
|
|
51
|
+
});
|
|
52
|
+
return context.adapter.transaction(async (trx) => {
|
|
53
|
+
if (!await trx.update({
|
|
54
|
+
model: "invitation",
|
|
55
|
+
where: [{
|
|
56
|
+
field: "id",
|
|
57
|
+
value: invitation.id
|
|
58
|
+
}, {
|
|
59
|
+
field: "status",
|
|
60
|
+
value: "pending"
|
|
61
|
+
}],
|
|
62
|
+
update: { status: "accepted" }
|
|
63
|
+
})) throw new APIError("BAD_REQUEST", invalid);
|
|
64
|
+
return {
|
|
65
|
+
...await trx.create({
|
|
66
|
+
model: "member",
|
|
67
|
+
data: {
|
|
68
|
+
organizationId: invitation.organizationId,
|
|
69
|
+
userId: user.id,
|
|
70
|
+
role: invitation.role,
|
|
71
|
+
appRoles: invitation.appRoles,
|
|
72
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
73
|
+
}
|
|
74
|
+
}),
|
|
75
|
+
appRoles: invitation.appRoles
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
//#endregion
|
|
80
|
+
export { acceptInvitation, invitationOfToken };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/better-auth/organizations/active-organization.ts
|
|
2
|
+
/**
|
|
3
|
+
* The organization a new session starts in: the last one the person worked in while they may still enter it (a member
|
|
4
|
+
* there, or holding a global role, which enters any that still exists), else their oldest membership, else none.
|
|
5
|
+
*/
|
|
6
|
+
const initialOrganization = (input) => {
|
|
7
|
+
const { memberships, last } = input;
|
|
8
|
+
if (last && (memberships.some((membership) => membership.organizationId === last) || input.entersAny && input.lastExists)) return last;
|
|
9
|
+
return [...memberships].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())[0]?.organizationId;
|
|
10
|
+
};
|
|
11
|
+
//#endregion
|
|
12
|
+
export { initialOrganization };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { globalRoles } from "./app-roles.js";
|
|
2
|
+
import { findMember, findOrganization } from "./store.js";
|
|
3
|
+
import { auditOrigin } from "../../audit/origin.js";
|
|
4
|
+
import { Hono } from "hono";
|
|
5
|
+
import { exactCount } from "@protobase/query";
|
|
6
|
+
//#region src/better-auth/organizations/admin-routes.ts
|
|
7
|
+
/** The resources with rows in an organization, soft-deleted ones too: they would point at nothing once it is gone. */
|
|
8
|
+
const resourcesWithRecords = async (registry, db, organizationId) => {
|
|
9
|
+
const names = [];
|
|
10
|
+
for (const entry of registry.entries) {
|
|
11
|
+
if (!entry.model.tenant) continue;
|
|
12
|
+
const { softDelete: _, ...model } = entry.model;
|
|
13
|
+
if (await exactCount(db, model, void 0, { tenantValue: organizationId }) > 0) names.push(entry.name);
|
|
14
|
+
}
|
|
15
|
+
return names;
|
|
16
|
+
};
|
|
17
|
+
const refusal = (status, code, message) => ({
|
|
18
|
+
status,
|
|
19
|
+
code,
|
|
20
|
+
message
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* The organization routes that need the admin's own parts, mounted before Better Auth's handler: switching the
|
|
24
|
+
* organization one works in, which writes an audit event when someone enters one they are not a member of, and, while
|
|
25
|
+
* `deleteRecords` is `'refuse'`, deleting only an organization without records.
|
|
26
|
+
*/
|
|
27
|
+
const organizationRoutes = ({ auth, audit, registry, db }) => {
|
|
28
|
+
const app = new Hono();
|
|
29
|
+
const contextOf = async () => await auth.$context;
|
|
30
|
+
const fail = (c, problem) => c.json({
|
|
31
|
+
code: problem.code,
|
|
32
|
+
message: problem.message
|
|
33
|
+
}, problem.status);
|
|
34
|
+
/**
|
|
35
|
+
* `POST /organization/switch` with `{ organizationId }`: the session works in that organization from its next token
|
|
36
|
+
* on, for a member, or for someone with a global role, who may enter any; `null` leaves it.
|
|
37
|
+
*/
|
|
38
|
+
app.post("/organization/switch", async (c) => {
|
|
39
|
+
const session = await auth.api.getSession({ headers: c.req.raw.headers });
|
|
40
|
+
if (!session) return fail(c, refusal(401, "UNAUTHORIZED", "Sign in first."));
|
|
41
|
+
const organizationId = (await c.req.json().catch(() => void 0))?.organizationId;
|
|
42
|
+
if (organizationId !== null && typeof organizationId !== "string") return fail(c, refusal(400, "ORGANIZATION_REQUIRED", "Name the organization to work in."));
|
|
43
|
+
const { adapter, internalAdapter } = await contextOf();
|
|
44
|
+
if (organizationId !== null) {
|
|
45
|
+
const member = await findMember(adapter, {
|
|
46
|
+
organizationId,
|
|
47
|
+
userId: session.user.id
|
|
48
|
+
});
|
|
49
|
+
const global = globalRoles(session.user.role);
|
|
50
|
+
if (!member) {
|
|
51
|
+
if (global.length === 0 || !await findOrganization(adapter, organizationId)) return fail(c, refusal(403, "NOT_A_MEMBER", "You are not a member of this organization."));
|
|
52
|
+
await audit.publish({
|
|
53
|
+
type: "organization.entered",
|
|
54
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
55
|
+
actor: {
|
|
56
|
+
id: session.user.id,
|
|
57
|
+
roles: global
|
|
58
|
+
},
|
|
59
|
+
organization: organizationId,
|
|
60
|
+
origin: auditOrigin(c.req.raw.headers)
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
await internalAdapter.updateSession(session.session.token, { activeOrganizationId: organizationId });
|
|
65
|
+
return c.json({ organizationId });
|
|
66
|
+
});
|
|
67
|
+
if (auth.organizations?.deleteRecords !== "cascade") app.post("/organization/delete", async (c, next) => {
|
|
68
|
+
const body = await c.req.raw.clone().json().catch(() => void 0);
|
|
69
|
+
const session = await auth.api.getSession({ headers: c.req.raw.headers });
|
|
70
|
+
if (!session || typeof body?.organizationId !== "string") return next();
|
|
71
|
+
const member = await findMember((await contextOf()).adapter, {
|
|
72
|
+
organizationId: body.organizationId,
|
|
73
|
+
userId: session.user.id
|
|
74
|
+
});
|
|
75
|
+
if (!member || !member.role.split(",").includes("owner")) return next();
|
|
76
|
+
const names = await resourcesWithRecords(registry, db, body.organizationId);
|
|
77
|
+
if (names.length > 0) return fail(c, refusal(409, "ORGANIZATION_HAS_RECORDS", `Delete the organization's records first: ${names.join(", ")}.`));
|
|
78
|
+
return next();
|
|
79
|
+
});
|
|
80
|
+
return app;
|
|
81
|
+
};
|
|
82
|
+
//#endregion
|
|
83
|
+
export { organizationRoutes, resourcesWithRecords };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { parseRoles } from "../parse-roles.js";
|
|
2
|
+
import { membershipRoles, noGlobalRole } from "./role-definitions.js";
|
|
3
|
+
//#region src/better-auth/organizations/app-roles.ts
|
|
4
|
+
/** A user's global roles, from the admin plugin's `role` column; the placeholder `user` is none. */
|
|
5
|
+
const globalRoles = (role) => parseRoles(role).filter((name) => name !== noGlobalRole);
|
|
6
|
+
/** App roles stored on a membership or an invitation: a JSON array, as the store returns it or as text. */
|
|
7
|
+
const parseAppRoles = (value) => {
|
|
8
|
+
const list = typeof value === "string" ? JSON.parse(value) : value;
|
|
9
|
+
return Array.isArray(list) ? list.filter((name) => typeof name === "string") : [];
|
|
10
|
+
};
|
|
11
|
+
/** The roles a request has: the global ones, then the membership's in the active organization, each once. */
|
|
12
|
+
const effectiveRoles = (global, membership = []) => [.../* @__PURE__ */ new Set([...global, ...membership])];
|
|
13
|
+
/**
|
|
14
|
+
* The roles someone may give or take away: those they hold, globally or in the organization, and those a held role
|
|
15
|
+
* lists in its `grants`. Grants do not chain. A superuser may give every role.
|
|
16
|
+
*/
|
|
17
|
+
const grantableRoles = (held, definitions) => {
|
|
18
|
+
if (held.includes("admin")) return new Set(definitions.names);
|
|
19
|
+
return /* @__PURE__ */ new Set([...held, ...held.flatMap((name) => definitions.grants[name] ?? [])]);
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Whether someone holding `held` may change a membership's app roles from `before` to `after`: every role added or
|
|
23
|
+
* removed must be grantable by them, and every role in `after` one a member can hold. `undefined` when allowed.
|
|
24
|
+
*/
|
|
25
|
+
const checkAppRoleChange = (input) => {
|
|
26
|
+
const allowed = membershipRoles(input.definitions);
|
|
27
|
+
const unknown = input.after.filter((name) => !allowed.includes(name));
|
|
28
|
+
if (unknown.length > 0) return {
|
|
29
|
+
status: "BAD_REQUEST",
|
|
30
|
+
code: "UNKNOWN_APP_ROLE",
|
|
31
|
+
message: `A member cannot hold ${unknown.join(", ")}; choose from ${allowed.join(", ")}`
|
|
32
|
+
};
|
|
33
|
+
const grantable = grantableRoles(input.held, input.definitions);
|
|
34
|
+
const refused = [...input.after.filter((name) => !input.before.includes(name)), ...input.before.filter((name) => !input.after.includes(name))].filter((name) => !grantable.has(name));
|
|
35
|
+
if (refused.length === 0) return void 0;
|
|
36
|
+
return {
|
|
37
|
+
status: "FORBIDDEN",
|
|
38
|
+
code: "APP_ROLE_NOT_GRANTABLE",
|
|
39
|
+
message: `You cannot give or take away ${refused.map((name) => input.definitions.labels[name] ?? name).join(", ")}: hold the role, or one that grants it`
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
//#endregion
|
|
43
|
+
export { checkAppRoleChange, effectiveRoles, globalRoles, grantableRoles, parseAppRoles };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { OrganizationRole, StoredMember, StoredOrganization } from "./store.js";
|
|
2
|
+
import { AdminAuth } from "../create-auth.js";
|
|
3
|
+
//#region src/better-auth/organizations/host.d.ts
|
|
4
|
+
export type NewOrganization = {
|
|
5
|
+
name: string;
|
|
6
|
+
slug: string;
|
|
7
|
+
owner: string;
|
|
8
|
+
id?: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Creates an organization with `owner` (an email) as its owner, holding the creator's app roles. `id` keeps an existing
|
|
12
|
+
* tenant value, for an app that had one organization before; otherwise the configured `generateId` makes one.
|
|
13
|
+
*/
|
|
14
|
+
export declare const createOrganization: (auth: AdminAuth, input: NewOrganization) => Promise<{
|
|
15
|
+
organization: StoredOrganization;
|
|
16
|
+
member: {
|
|
17
|
+
appRoles: string[];
|
|
18
|
+
id: string;
|
|
19
|
+
organizationId: string;
|
|
20
|
+
userId: string;
|
|
21
|
+
role: string;
|
|
22
|
+
createdAt: Date;
|
|
23
|
+
};
|
|
24
|
+
}>;
|
|
25
|
+
export type MemberInput = {
|
|
26
|
+
organization: string;
|
|
27
|
+
email: string;
|
|
28
|
+
role?: string;
|
|
29
|
+
appRoles?: string[];
|
|
30
|
+
};
|
|
31
|
+
/** Adds someone with an account as a member: organization role `member` unless given, and the app roles given. */
|
|
32
|
+
export declare const addMember: (auth: AdminAuth, input: MemberInput) => Promise<StoredMember>;
|
|
33
|
+
/** Changes a member's organization role, app roles, or both. */
|
|
34
|
+
export declare const setMemberRoles: (auth: AdminAuth, input: MemberInput) => Promise<StoredMember | undefined>;
|
|
35
|
+
/** Removes a member; refuses the last owner. */
|
|
36
|
+
export declare const removeMember: (auth: AdminAuth, input: {
|
|
37
|
+
organization: string;
|
|
38
|
+
email: string;
|
|
39
|
+
}) => Promise<void>;
|
|
40
|
+
/** Every organization, oldest first, with its members' emails and roles. */
|
|
41
|
+
export declare const listOrganizations: (auth: AdminAuth) => Promise<{
|
|
42
|
+
id: string;
|
|
43
|
+
name: string;
|
|
44
|
+
slug: string;
|
|
45
|
+
members: {
|
|
46
|
+
email: string;
|
|
47
|
+
role: OrganizationRole;
|
|
48
|
+
appRoles: string[];
|
|
49
|
+
}[];
|
|
50
|
+
}[]>;
|
|
51
|
+
/** Gives an app role to every owner of every organization, for a role the project adds once organizations exist. */
|
|
52
|
+
export declare const addAppRoleToOwners: (auth: AdminAuth, appRole: string) => Promise<number>;
|
|
53
|
+
//#endregion
|