@protobase/server 0.46.2 → 0.46.4
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/better-auth/auth-mail/auth-emails.d.ts +1 -0
- package/dist/better-auth/auth-mail/auth-emails.js +2 -1
- package/dist/better-auth/auth-mail/change-email-code-email.d.ts +1 -0
- package/dist/better-auth/auth-mail/change-email-code-email.js +32 -0
- package/dist/better-auth/create-auth.d.ts +39 -0
- package/dist/better-auth/create-auth.js +6 -0
- package/dist/better-auth/set-password.js +27 -0
- package/dist/better-auth/sign-in-enforcement.js +2 -1
- package/dist/better-auth/sign-in-mail.js +6 -6
- package/dist/better-auth/staff-checks.js +3 -0
- package/dist/better-auth/status-route.js +2 -1
- package/dist/create-admin.js +10 -5
- package/dist/deps.d.ts +4 -0
- package/dist/list-photos.js +2 -2
- package/dist/meta.d.ts +2 -0
- package/dist/meta.js +2 -0
- package/dist/package.js +1 -1
- package/dist/related-labels.js +2 -2
- package/dist/request-access.js +35 -2
- package/package.json +4 -4
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { accountExistsEmail } from "./account-exists-email.js";
|
|
2
|
+
import { changeEmailCodeEmail } from "./change-email-code-email.js";
|
|
2
3
|
import { invitationEmail } from "./invitation-email.js";
|
|
3
4
|
import { noAccountEmail } from "./no-account-email.js";
|
|
4
5
|
import { resetPasswordEmail } from "./reset-password-email.js";
|
|
@@ -6,4 +7,4 @@ import { signInCodeEmail } from "./sign-in-code-email.js";
|
|
|
6
7
|
import { signUpCodeEmail } from "./sign-up-code-email.js";
|
|
7
8
|
import { staffSignInEmail } from "./staff-sign-in-email.js";
|
|
8
9
|
import { twoFactorCodeEmail } from "./two-factor-code-email.js";
|
|
9
|
-
export { accountExistsEmail, invitationEmail, noAccountEmail, resetPasswordEmail, signInCodeEmail, signUpCodeEmail, staffSignInEmail, twoFactorCodeEmail };
|
|
10
|
+
export { accountExistsEmail, changeEmailCodeEmail, invitationEmail, noAccountEmail, resetPasswordEmail, signInCodeEmail, signUpCodeEmail, staffSignInEmail, twoFactorCodeEmail };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "../../index.js";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { defineEmail } from "../../mail/define-email.js";
|
|
2
|
+
import { EmailCode, EmailLayout, EmailNote } from "../../mail/email-layout.js";
|
|
3
|
+
import { Text } from "react-email";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/better-auth/auth-mail/change-email-code-email.tsx
|
|
6
|
+
const ChangeEmailCodeEmail = ({ app, code, lifetime }) => /* @__PURE__ */ jsxs(EmailLayout, {
|
|
7
|
+
app,
|
|
8
|
+
preview: `Your code to sign in to ${app.name} with this address`,
|
|
9
|
+
heading: "Confirm your new address",
|
|
10
|
+
children: [
|
|
11
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
12
|
+
"Use this code to sign in to ",
|
|
13
|
+
app.name,
|
|
14
|
+
" with this address from now on:"
|
|
15
|
+
] }),
|
|
16
|
+
/* @__PURE__ */ jsx(EmailCode, { code }),
|
|
17
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
18
|
+
"It works for ",
|
|
19
|
+
lifetime,
|
|
20
|
+
", once."
|
|
21
|
+
] }),
|
|
22
|
+
/* @__PURE__ */ jsx(EmailNote, { children: "If you did not ask for this, ignore this email: nobody can move an account to your address without the code." })
|
|
23
|
+
]
|
|
24
|
+
});
|
|
25
|
+
/** The email to the new address with the code that moves the account to it. */
|
|
26
|
+
const changeEmailCodeEmail = defineEmail({
|
|
27
|
+
name: "change-email-code",
|
|
28
|
+
subject: ({ app }) => `Confirm your new ${app.name} address`,
|
|
29
|
+
component: ChangeEmailCodeEmail
|
|
30
|
+
});
|
|
31
|
+
//#endregion
|
|
32
|
+
export { changeEmailCodeEmail };
|
|
@@ -4476,6 +4476,41 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
4476
4476
|
handler: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<void>>;
|
|
4477
4477
|
}[];
|
|
4478
4478
|
};
|
|
4479
|
+
} | {
|
|
4480
|
+
id: "protobase-set-password";
|
|
4481
|
+
endpoints: {
|
|
4482
|
+
setFirstPassword: import("better-call").StrictEndpoint<"/account/set-password", {
|
|
4483
|
+
method: "POST";
|
|
4484
|
+
use: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<{
|
|
4485
|
+
session: {
|
|
4486
|
+
session: Record<string, any> & {
|
|
4487
|
+
id: string;
|
|
4488
|
+
createdAt: Date;
|
|
4489
|
+
updatedAt: Date;
|
|
4490
|
+
userId: string;
|
|
4491
|
+
expiresAt: Date;
|
|
4492
|
+
token: string;
|
|
4493
|
+
ipAddress?: string | null | undefined;
|
|
4494
|
+
userAgent?: string | null | undefined;
|
|
4495
|
+
};
|
|
4496
|
+
user: Record<string, any> & {
|
|
4497
|
+
id: string;
|
|
4498
|
+
createdAt: Date;
|
|
4499
|
+
updatedAt: Date;
|
|
4500
|
+
email: string;
|
|
4501
|
+
emailVerified: boolean;
|
|
4502
|
+
name: string;
|
|
4503
|
+
image?: string | null | undefined;
|
|
4504
|
+
};
|
|
4505
|
+
};
|
|
4506
|
+
}>>[];
|
|
4507
|
+
body: import("zod").ZodObject<{
|
|
4508
|
+
newPassword: import("zod").ZodString;
|
|
4509
|
+
}, import("zod/v4/core").$strip>;
|
|
4510
|
+
}, {
|
|
4511
|
+
status: boolean;
|
|
4512
|
+
}>;
|
|
4513
|
+
};
|
|
4479
4514
|
} | {
|
|
4480
4515
|
id: "protobase-gotrue-sessions";
|
|
4481
4516
|
endpoints: {
|
|
@@ -8669,6 +8704,10 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
|
|
|
8669
8704
|
window: number;
|
|
8670
8705
|
max: number;
|
|
8671
8706
|
};
|
|
8707
|
+
"/account/set-password": {
|
|
8708
|
+
window: number;
|
|
8709
|
+
max: number;
|
|
8710
|
+
};
|
|
8672
8711
|
'/invitation/sign-up': {
|
|
8673
8712
|
window: number;
|
|
8674
8713
|
max: number;
|
|
@@ -25,6 +25,7 @@ import { resolveOrganizations } from "./organizations/options.js";
|
|
|
25
25
|
import { passkeyHosts, readPasskeyDomain, resolvePasskeyDomain } from "./passkey-domain.js";
|
|
26
26
|
import { passkeyPlugin } from "./passkey-plugin.js";
|
|
27
27
|
import { passwordResetOptions } from "./password-reset.js";
|
|
28
|
+
import { setPasswordPath, setPasswordPlugin } from "./set-password.js";
|
|
28
29
|
import { rolesPlugin } from "./roles/roles-plugin.js";
|
|
29
30
|
import { platformSignInFor, platformSignInProvider, readPlatformSignIn, resolvePlatformSignIn } from "./platform-sign-in.js";
|
|
30
31
|
import { appSignInPolicy } from "./app-sign-in-policy.js";
|
|
@@ -180,6 +181,7 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
|
|
|
180
181
|
...signIn ? [genericOAuth({ config: [platformSignInProvider(signIn)] })] : [],
|
|
181
182
|
...saml.length > 0 ? [samlPlugin(saml, options.baseURL), samlGuardPlugin(samlIds)] : [],
|
|
182
183
|
signInPolicyPlugin(policy),
|
|
184
|
+
setPasswordPlugin(),
|
|
183
185
|
goTrueSessionsPlugin({
|
|
184
186
|
issuer: goTrueIssuer(options.baseURL),
|
|
185
187
|
jwt: jwtOptions,
|
|
@@ -267,6 +269,10 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
|
|
|
267
269
|
window: 60,
|
|
268
270
|
max: 5
|
|
269
271
|
},
|
|
272
|
+
[setPasswordPath]: {
|
|
273
|
+
window: 60,
|
|
274
|
+
max: 5
|
|
275
|
+
},
|
|
270
276
|
"/invitation/sign-up": {
|
|
271
277
|
window: 60,
|
|
272
278
|
max: options.signInPerMinute ?? 5
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as z$1 from "zod";
|
|
2
|
+
import { createAuthEndpoint, sessionMiddleware, setPassword } from "better-auth/api";
|
|
3
|
+
//#region src/better-auth/set-password.ts
|
|
4
|
+
const setPasswordPath = "/account/set-password";
|
|
5
|
+
/**
|
|
6
|
+
* `POST /account/set-password { newPassword }`: gives the signed-in account its first password, such as one made by
|
|
7
|
+
* signing up with emailed codes or a provider. It is Better Auth's server-only `setPassword`, with its length rules;
|
|
8
|
+
* an account that has a password already answers `400 PASSWORD_ALREADY_SET` and changes it with `/change-password`.
|
|
9
|
+
* Every session, the one asking included, and every MCP grant stays: nothing was known to anyone else before. The
|
|
10
|
+
* sign-in policy refuses it while passwords are off, and the staff plugin refuses it for a staff session.
|
|
11
|
+
*/
|
|
12
|
+
const setPasswordPlugin = () => ({
|
|
13
|
+
id: "protobase-set-password",
|
|
14
|
+
endpoints: { setFirstPassword: createAuthEndpoint(setPasswordPath, {
|
|
15
|
+
method: "POST",
|
|
16
|
+
use: [sessionMiddleware],
|
|
17
|
+
body: z$1.object({ newPassword: z$1.string() })
|
|
18
|
+
}, async (ctx) => {
|
|
19
|
+
await setPassword({
|
|
20
|
+
...ctx,
|
|
21
|
+
body: { newPassword: ctx.body.newPassword }
|
|
22
|
+
});
|
|
23
|
+
return ctx.json({ status: true });
|
|
24
|
+
}) }
|
|
25
|
+
});
|
|
26
|
+
//#endregion
|
|
27
|
+
export { setPasswordPath, setPasswordPlugin };
|
|
@@ -3,7 +3,8 @@ import { signInPolicyMethods } from "./sign-in-policy.js";
|
|
|
3
3
|
const passwordPaths = [
|
|
4
4
|
"/sign-in/email",
|
|
5
5
|
"/request-password-reset",
|
|
6
|
-
"/reset-password"
|
|
6
|
+
"/reset-password",
|
|
7
|
+
"/account/set-password"
|
|
7
8
|
];
|
|
8
9
|
const emailCodePaths = ["/sign-in/email-otp"];
|
|
9
10
|
const passkeyPaths = [
|
|
@@ -2,14 +2,16 @@ import { sendAuthEmail } from "./auth-mail/send-auth-email.js";
|
|
|
2
2
|
const codeLifetimeMinutes = 5;
|
|
3
3
|
const codeLifetime = "5 minutes";
|
|
4
4
|
/**
|
|
5
|
-
* Better Auth's email OTP plugin
|
|
6
|
-
*
|
|
5
|
+
* Better Auth's email OTP plugin for signing in and changing the address: never a sign-up, codes stored hashed, sent
|
|
6
|
+
* with `mail`. A new address gets its own email, with the code that moves the account to it. The plugin's other uses
|
|
7
|
+
* (email verification, its own password reset) are not offered.
|
|
7
8
|
*/
|
|
8
9
|
const emailCodeOptions = (mail) => ({
|
|
9
10
|
disableSignUp: true,
|
|
10
11
|
expiresIn: 300,
|
|
11
12
|
storeOTP: "hashed",
|
|
12
|
-
|
|
13
|
+
changeEmail: { enabled: true },
|
|
14
|
+
sendVerificationOTP: ({ email, otp, type }, ctx) => sendAuthEmail(mail, type === "change-email" ? "changeEmailCodeEmail" : "signInCodeEmail", email, {
|
|
13
15
|
code: otp,
|
|
14
16
|
lifetime: codeLifetime
|
|
15
17
|
}, ctx?.context.baseURL)
|
|
@@ -20,9 +22,7 @@ const unusedEmailCodePaths = [
|
|
|
20
22
|
"/email-otp/verify-email",
|
|
21
23
|
"/email-otp/request-password-reset",
|
|
22
24
|
"/forget-password/email-otp",
|
|
23
|
-
"/email-otp/reset-password"
|
|
24
|
-
"/email-otp/request-email-change",
|
|
25
|
-
"/email-otp/change-email"
|
|
25
|
+
"/email-otp/reset-password"
|
|
26
26
|
];
|
|
27
27
|
/** The two-factor plugin's emailed code, sent with `mail`; stored hashed. */
|
|
28
28
|
const twoFactorCodeOptions = (mail) => ({
|
|
@@ -39,7 +39,10 @@ const staffLabel = (claims) => typeof claims.email === "string" && claims.email
|
|
|
39
39
|
const accountChanges = [
|
|
40
40
|
"/change-password",
|
|
41
41
|
"/set-password",
|
|
42
|
+
"/account/set-password",
|
|
42
43
|
"/change-email",
|
|
44
|
+
"/email-otp/request-email-change",
|
|
45
|
+
"/email-otp/change-email",
|
|
43
46
|
"/delete-user",
|
|
44
47
|
"/link-social",
|
|
45
48
|
"/unlink-account",
|
|
@@ -9,7 +9,7 @@ import { Hono } from "hono";
|
|
|
9
9
|
* `GET {authBase}/status`: `needsAdmin` while the admin store has no user, so a login page can say how to create one;
|
|
10
10
|
* `signInMethods`, the ways to sign in the sign-in policy leaves on (`password`, `emailCode`, `passkey`), so it offers
|
|
11
11
|
* only those; `passwordReset` when reset mail can be sent and passwords are on, so it offers "Forgot password?" only
|
|
12
|
-
* then; `signUp` when the app has open sign-up, so it offers "Create an account"; the ids of the `socialProviders` people can sign in with, so it offers "Continue with GitHub" when `github` is
|
|
12
|
+
* then; `changeEmail` when mail can be sent, so the account page offers to change the address with an emailed code; `signUp` when the app has open sign-up, so it offers "Create an account"; the ids of the `socialProviders` people can sign in with, so it offers "Continue with GitHub" when `github` is
|
|
13
13
|
* one; `platformSignIn`, the id and name of the platform's provider when it is one of them; `samlProviders`, the id and name of each SAML provider, when the app has any; `staffSignIn`, the
|
|
14
14
|
* operator provider's name, when staff can sign in as people; `passkeyDomains`, when people have passkeys for more than
|
|
15
15
|
* one of the app's hosts, the RP IDs a passkey sign-in can ask for, the passkey domain first; with organizations, who may create them and the roles
|
|
@@ -37,6 +37,7 @@ const statusRoute = (auth, { anonymous = false, plugins = [], locale } = {}) =>
|
|
|
37
37
|
needsAdmin: !await hasUsers(auth),
|
|
38
38
|
signInMethods,
|
|
39
39
|
passwordReset: auth.passwordReset && policy.password === "allowed",
|
|
40
|
+
...auth.mail && { changeEmail: true },
|
|
40
41
|
...auth.signUp && { signUp: true },
|
|
41
42
|
socialProviders,
|
|
42
43
|
...platformSignIn && { platformSignIn },
|
package/dist/create-admin.js
CHANGED
|
@@ -96,7 +96,7 @@ import { recordRoutes } from "./routes/record.js";
|
|
|
96
96
|
import { createScanGuard } from "./scan-guard.js";
|
|
97
97
|
import { failedJobs, failedJobsView } from "./jobs/failed-jobs.js";
|
|
98
98
|
import { Hono } from "hono";
|
|
99
|
-
import { checkPages } from "@protobase/layout";
|
|
99
|
+
import { accountPage, accountPageName, checkPages, settingsResources, withSettingsFields } from "@protobase/layout";
|
|
100
100
|
//#region src/create-admin.ts
|
|
101
101
|
const declaredRoles = (auth, roles) => {
|
|
102
102
|
const names = [...auth?.roles ?? [], ...Object.keys(roles?.definitions ?? {})];
|
|
@@ -130,7 +130,9 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
130
130
|
const apiFunctions = Object.fromEntries(Object.entries(functions).map(([name, source]) => [name, toApiFunction(name, source)]));
|
|
131
131
|
const viewModels = views.map((view) => view.toModel());
|
|
132
132
|
const userMenuModel = userMenu?.toUserMenuModel();
|
|
133
|
-
const
|
|
133
|
+
const allPages = pages.map((entry) => entry.toModel());
|
|
134
|
+
const pageModels = allPages.filter((entry) => entry.name !== accountPageName);
|
|
135
|
+
const accountModel = allPages.find((entry) => entry.name === accountPageName) ?? accountPage.toModel();
|
|
134
136
|
checkShell(registry, viewModels, userMenuModel, pageModels.map((entry) => entry.name));
|
|
135
137
|
const bottomBarModels = bottomBars.map((bar) => bar.toBottomBarModel());
|
|
136
138
|
checkBottomBars(registry, bottomBarModels, pageModels.map((entry) => entry.name), declaredRoles(auth, options.roles));
|
|
@@ -141,8 +143,9 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
141
143
|
if (options.mcp) checkMcpWrites(registry, declaredRoles(auth, options.roles), options.mcp.writes);
|
|
142
144
|
checkPositionAfter(registry);
|
|
143
145
|
checkParents(registry);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
+
const resourceModels = Object.fromEntries(registry.entries.map((entry) => [entry.name, entry.model]));
|
|
147
|
+
checkPages([...pageModels, accountModel], {
|
|
148
|
+
models: resourceModels,
|
|
146
149
|
actions: (resource) => new Set(viewModels.filter((view) => view.resource === resource).flatMap((view) => view.actions.map((action) => action.name))),
|
|
147
150
|
createPages: new Set(viewModels.filter((view) => view.create).map((view) => view.resource))
|
|
148
151
|
});
|
|
@@ -188,7 +191,8 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
188
191
|
db,
|
|
189
192
|
registry,
|
|
190
193
|
views: viewModels,
|
|
191
|
-
pages: pageModels,
|
|
194
|
+
pages: pageModels.map((entry) => withSettingsFields(entry, resourceModels)),
|
|
195
|
+
account: withSettingsFields(accountModel, resourceModels),
|
|
192
196
|
...userMenuModel && { userMenu: userMenuModel },
|
|
193
197
|
bottomBars: bottomBarModels,
|
|
194
198
|
basePath,
|
|
@@ -207,6 +211,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
207
211
|
...options.roles && { roles: options.roles },
|
|
208
212
|
...options.defaultAccess && { defaultAccess: options.defaultAccess }
|
|
209
213
|
},
|
|
214
|
+
settingsResources: settingsResources([...pageModels, accountModel]),
|
|
210
215
|
rowPermissions: (resource) => typeof options.rowPermissions === "object" ? !options.rowPermissions.except.includes(resource) : options.rowPermissions !== false,
|
|
211
216
|
...assistant && { assistant: assistant.kind === "external" ? assistant.url : `${systemPath}/assistant` },
|
|
212
217
|
...runtime && { runtime },
|
package/dist/deps.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export type Deps = {
|
|
|
24
24
|
/** The bottom bars, in config order. */
|
|
25
25
|
bottomBars: BottomBarModel[];
|
|
26
26
|
pages: PageModel[];
|
|
27
|
+
/** The signed-in person's account page, at `/-/account`: the app's page named `account`, or the standard one. */
|
|
28
|
+
account?: PageModel;
|
|
27
29
|
basePath: string;
|
|
28
30
|
/** Where the system endpoints live (`/meta`, `/openapi.json`, `/docs`): the parent of `basePath`, `/api` by default. */
|
|
29
31
|
systemPath: string;
|
|
@@ -33,6 +35,8 @@ export type Deps = {
|
|
|
33
35
|
/** Run on a create's or update's body before its transaction opens (see `WriteCheck`). */
|
|
34
36
|
writeChecks: readonly WriteCheck[];
|
|
35
37
|
accessOptions: ResolveOptions;
|
|
38
|
+
/** The resources the pages' `Settings` blocks show: every caller, an admin too, reads and writes only their own records. */
|
|
39
|
+
settingsResources: ReadonlySet<string>;
|
|
36
40
|
/** Where reveals of sensitive fields are published. */
|
|
37
41
|
audit: AuditQueue;
|
|
38
42
|
/** Gets the errors of work that runs after a request's response is decided, such as a committed write's file cleanup. */
|
package/dist/list-photos.js
CHANGED
|
@@ -9,7 +9,7 @@ import { cardPhotoLimit } from "@protobase/schema/internal";
|
|
|
9
9
|
import { checkFilter, createWhere } from "@protobase/schema";
|
|
10
10
|
import { relatedRows } from "@protobase/query";
|
|
11
11
|
//#region src/list-photos.ts
|
|
12
|
-
const where = createWhere();
|
|
12
|
+
const where$1 = createWhere();
|
|
13
13
|
const invalid = (parameter, message) => badRequest("invalid-parameter", `Invalid ${parameter}: ${message}`, { errors: [{
|
|
14
14
|
parameter,
|
|
15
15
|
message
|
|
@@ -64,7 +64,7 @@ const recordPhotos = async (deps, request, model, rows) => {
|
|
|
64
64
|
if (!request.access || keys.length === 0) return { photos: {} };
|
|
65
65
|
const { photos, access } = request;
|
|
66
66
|
const sort = checkedSort(access.model, photos.sort);
|
|
67
|
-
const filter = checkFilter(access.model, where.in(photos.field, keys));
|
|
67
|
+
const filter = checkFilter(access.model, where$1.in(photos.field, keys));
|
|
68
68
|
if (!filter.ok) throw new Error(`Keys of "${model.name}" do not make a filter of "${photos.resource}": ${filter.errors[0].message}`);
|
|
69
69
|
const scope = readScope(access);
|
|
70
70
|
const warning = await deps.guard(access.entry, {
|
package/dist/meta.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export type CallerMeta = {
|
|
|
27
27
|
views: ViewModel[];
|
|
28
28
|
/** The composed pages the caller may open, without the elements that name what they cannot see or do. */
|
|
29
29
|
pages: PageModel[];
|
|
30
|
+
/** The caller's account page, at `/-/account`, when they are signed in, pruned as the pages are. */
|
|
31
|
+
account?: PageModel;
|
|
30
32
|
permissions: Record<string, ResourcePermissions>;
|
|
31
33
|
/** The user menu with the resources the caller cannot see left out. */
|
|
32
34
|
userMenu?: UserMenuModel;
|
package/dist/meta.js
CHANGED
|
@@ -57,6 +57,7 @@ const createMeta = (deps, appName, appIcon) => {
|
|
|
57
57
|
actions: (resource) => new Set(views.find((view) => view.resource === resource)?.actions.map((action) => action.name) ?? [])
|
|
58
58
|
};
|
|
59
59
|
const pages = deps.pages.flatMap((page) => prunePage(page, session.user.roles, callerAccess) ?? []);
|
|
60
|
+
const account = deps.account && !session.anonymous ? prunePage(deps.account, session.user.roles, callerAccess) : void 0;
|
|
60
61
|
const shown = (item) => item.kind === "link" || (item.kind === "resource" ? visible.some((access) => access.entry.name === item.resource) || prompted.has(item.resource) : pages.some((page) => page.name === item.page));
|
|
61
62
|
const bar = bottomBarFor(deps.bottomBars, activeRole(session));
|
|
62
63
|
const barItems = bar?.items.filter(shown) ?? [];
|
|
@@ -69,6 +70,7 @@ const createMeta = (deps, appName, appIcon) => {
|
|
|
69
70
|
resources: visible.map(metaModel),
|
|
70
71
|
views,
|
|
71
72
|
pages,
|
|
73
|
+
...account && { account },
|
|
72
74
|
permissions: Object.fromEntries(visible.map(({ entry, resolved, full }) => [entry.name, {
|
|
73
75
|
read: resolved.operations.read || resolved.operations.list,
|
|
74
76
|
create: resolved.operations.create,
|
package/dist/package.js
CHANGED
package/dist/related-labels.js
CHANGED
|
@@ -4,7 +4,7 @@ import { exposeRow } from "./rows.js";
|
|
|
4
4
|
import { checkFilter, createWhere } from "@protobase/schema";
|
|
5
5
|
import { listQuery } from "@protobase/query";
|
|
6
6
|
//#region src/related-labels.ts
|
|
7
|
-
const where = createWhere();
|
|
7
|
+
const where$1 = createWhere();
|
|
8
8
|
const invalid = (entry, message) => badRequest("invalid-parameter", `Invalid labels entry "${entry}": ${message}`, { errors: [{
|
|
9
9
|
parameter: "labels",
|
|
10
10
|
message
|
|
@@ -55,7 +55,7 @@ const namesOf = async (trx, { field, access }, keys) => {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
};
|
|
58
|
-
const filter = checkFilter(model, where.in(key, keys));
|
|
58
|
+
const filter = checkFilter(model, where$1.in(key, keys));
|
|
59
59
|
if (!filter.ok) throw new Error(`Keys of "${access.entry.name}" do not make a filter: ${filter.errors[0].message}`);
|
|
60
60
|
const named = (await listQuery(trx, model, {
|
|
61
61
|
filter: filter.filter,
|
package/dist/request-access.js
CHANGED
|
@@ -3,7 +3,7 @@ import { forbidden, notFound, unauthorized } from "./problem.js";
|
|
|
3
3
|
import { checkedFilter } from "./filter-input.js";
|
|
4
4
|
import { checkOwnerId } from "./owner-id.js";
|
|
5
5
|
import { tenantScope } from "./tenant.js";
|
|
6
|
-
import { checkFilter, resolveAccess, restrictModel } from "@protobase/schema";
|
|
6
|
+
import { checkFilter, resolveAccess, restrictModel, where } from "@protobase/schema";
|
|
7
7
|
//#region src/request-access.ts
|
|
8
8
|
const organizationOf = (session) => session.organization ? { organization: session.organization } : {};
|
|
9
9
|
const withoutSensitive = (model) => ({
|
|
@@ -32,6 +32,38 @@ const throughForm = (resolved, form) => {
|
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
34
|
};
|
|
35
|
+
const noAccess = {
|
|
36
|
+
operations: {
|
|
37
|
+
read: false,
|
|
38
|
+
list: false,
|
|
39
|
+
create: false,
|
|
40
|
+
update: false,
|
|
41
|
+
delete: false
|
|
42
|
+
},
|
|
43
|
+
recordChecks: [],
|
|
44
|
+
readableFields: [],
|
|
45
|
+
writableFields: {
|
|
46
|
+
create: [],
|
|
47
|
+
update: []
|
|
48
|
+
},
|
|
49
|
+
rowFilter: {}
|
|
50
|
+
};
|
|
51
|
+
const ownOnly = (resolved, entry, session) => {
|
|
52
|
+
if (session.anonymous) return noAccess;
|
|
53
|
+
const own = checkFilter(trusted(entry.model), where.eq(entry.model.owner, session.user.id));
|
|
54
|
+
if (!own.ok) throw new Error(`The owner filter of "${entry.name}" is invalid: ${own.errors[0].message}`);
|
|
55
|
+
const { rowFilter } = resolved;
|
|
56
|
+
const narrowed = (operation) => andFilters([rowFilter[operation], own.filter]);
|
|
57
|
+
return {
|
|
58
|
+
...resolved,
|
|
59
|
+
rowFilter: {
|
|
60
|
+
read: narrowed("read"),
|
|
61
|
+
create: narrowed("create"),
|
|
62
|
+
update: narrowed("update"),
|
|
63
|
+
delete: narrowed("delete")
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
};
|
|
35
67
|
/** `form` is the public form a visitor sends, the only way a caller without a token creates. */
|
|
36
68
|
const requestAccess = async (deps, entry, session, form) => {
|
|
37
69
|
const all = await resolveAccess(entry.source, {
|
|
@@ -41,7 +73,8 @@ const requestAccess = async (deps, entry, session, form) => {
|
|
|
41
73
|
},
|
|
42
74
|
...organizationOf(session)
|
|
43
75
|
}, deps.accessOptions);
|
|
44
|
-
const
|
|
76
|
+
const allowed = form ? throughForm(all, form) : withoutAnonymousCreate(all, session);
|
|
77
|
+
const resolved = deps.settingsResources.has(entry.name) ? ownOnly(allowed, entry, session) : allowed;
|
|
45
78
|
checkOwnerId(entry, session.user, resolved);
|
|
46
79
|
const readable = restrictModel(entry.model, resolved);
|
|
47
80
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.46.
|
|
3
|
+
"version": "0.46.4",
|
|
4
4
|
"description": "The Protobase REST API as a Hono app, with Better Auth sign-in.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"@better-auth/passkey": "^1.7.12",
|
|
37
37
|
"@better-auth/sso": "^1.7.12",
|
|
38
38
|
"@modelcontextprotocol/server": "^2.3.1",
|
|
39
|
-
"@protobase/layout": "0.46.
|
|
40
|
-
"@protobase/query": "0.46.
|
|
41
|
-
"@protobase/schema": "0.46.
|
|
39
|
+
"@protobase/layout": "0.46.4",
|
|
40
|
+
"@protobase/query": "0.46.4",
|
|
41
|
+
"@protobase/schema": "0.46.4",
|
|
42
42
|
"@react-email/render": "^2.1.0",
|
|
43
43
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
44
44
|
"@sentry/core": "^11.7.0",
|