mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-07 22:39:55 +00:00
Compare commits
9 Commits
private-re
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbb1e7b274 | ||
|
|
b2d1a66279 | ||
|
|
202466792c | ||
|
|
4dcf684868 | ||
|
|
2b8f50af6f | ||
|
|
a8c255ef71 | ||
|
|
d9608bd408 | ||
|
|
be193731c1 | ||
|
|
0c3edc7560 |
@@ -615,7 +615,8 @@
|
||||
"idpNameInternal": "Internal",
|
||||
"emailInvalid": "Invalid email address",
|
||||
"inviteValidityDuration": "Please select a duration",
|
||||
"accessRoleSelectPlease": "Please select a role",
|
||||
"accessRoleSelectPlease": "A user must belong to at least one role.",
|
||||
"accessRoleRequired": "Role required",
|
||||
"removeOwnAdminRoleConfirmTitle": "Remove your administrator access?",
|
||||
"removeOwnAdminRoleConfirmDescription": "You will no longer have administrator permissions in this organization after saving. Another administrator can restore access if needed.",
|
||||
"removeOwnAdminRoleConfirmButton": "Remove My Administrator Access",
|
||||
@@ -1433,6 +1434,15 @@
|
||||
"actionSetResourcePincode": "Set Resource Pincode",
|
||||
"actionSetResourceEmailWhitelist": "Set Resource Email Whitelist",
|
||||
"actionGetResourceEmailWhitelist": "Get Resource Email Whitelist",
|
||||
"actionGetResourcePolicy": "Get Resource Policy",
|
||||
"actionUpdateResourcePolicy": "Update Resource Policy",
|
||||
"actionSetResourcePolicyUsers": "Set Resource Policy Users",
|
||||
"actionSetResourcePolicyRoles": "Set Resource Policy Roles",
|
||||
"actionSetResourcePolicyPassword": "Set Resource Policy Password",
|
||||
"actionSetResourcePolicyPincode": "Set Resource Policy Pincode",
|
||||
"actionSetResourcePolicyHeaderAuth": "Set Resource Policy Header Authentication",
|
||||
"actionSetResourcePolicyWhitelist": "Set Resource Policy Email Whitelist",
|
||||
"actionSetResourcePolicyRules": "Set Resource Policy Rules",
|
||||
"actionCreateTarget": "Create Target",
|
||||
"actionDeleteTarget": "Delete Target",
|
||||
"actionGetTarget": "Get Target",
|
||||
|
||||
@@ -2,7 +2,7 @@ import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
// This is a placeholder value replaced by the build process
|
||||
export const APP_VERSION = "1.19.0";
|
||||
export const APP_VERSION = "1.20.0";
|
||||
|
||||
export const __FILENAME = fileURLToPath(import.meta.url);
|
||||
export const __DIRNAME = path.dirname(__FILENAME);
|
||||
|
||||
@@ -895,12 +895,6 @@ authenticated.delete(
|
||||
user.removeUserOrg
|
||||
);
|
||||
|
||||
// authenticated.put(
|
||||
// "/newt",
|
||||
// verifyApiKeyHasAction(ActionsEnum.createNewt),
|
||||
// newt.createNewt
|
||||
// );
|
||||
|
||||
authenticated.get(
|
||||
`/org/:orgId/api-keys`,
|
||||
verifyApiKeyIsRoot,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { alias, db } from "@server/db";
|
||||
import { db } from "@server/db";
|
||||
import {
|
||||
exitNodes,
|
||||
labels,
|
||||
launcherViews,
|
||||
resourceLabels,
|
||||
resourcePolicies,
|
||||
resources,
|
||||
rolePolicies,
|
||||
roleResources,
|
||||
@@ -32,7 +31,6 @@ import {
|
||||
inArray,
|
||||
isNull,
|
||||
like,
|
||||
not,
|
||||
or,
|
||||
sql,
|
||||
type SQL
|
||||
@@ -136,48 +134,6 @@ function launcherAccessibleIdsCacheKey(
|
||||
return `launcherAccessibleIds:${orgId}:${userId}:${rolesKey}`;
|
||||
}
|
||||
|
||||
async function fetchOrgNonSsoPublicResourceIds(
|
||||
orgId: string
|
||||
): Promise<number[]> {
|
||||
const sharedPolicy = alias(resourcePolicies, "sharedPolicy");
|
||||
const defaultPolicy = alias(resourcePolicies, "defaultPolicy");
|
||||
|
||||
const effectiveSso = sql<boolean>`
|
||||
COALESCE(
|
||||
CASE
|
||||
WHEN ${sharedPolicy.resourcePolicyId} IS NOT NULL THEN ${sharedPolicy.sso}
|
||||
ELSE ${defaultPolicy.sso}
|
||||
END,
|
||||
false
|
||||
)
|
||||
`;
|
||||
|
||||
const rows = await db
|
||||
.select({ resourceId: resources.resourceId })
|
||||
.from(resources)
|
||||
.leftJoin(
|
||||
sharedPolicy,
|
||||
eq(sharedPolicy.resourcePolicyId, resources.resourcePolicyId)
|
||||
)
|
||||
.leftJoin(
|
||||
defaultPolicy,
|
||||
eq(
|
||||
defaultPolicy.resourcePolicyId,
|
||||
resources.defaultResourcePolicyId
|
||||
)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(resources.orgId, orgId),
|
||||
eq(resources.enabled, true),
|
||||
eq(resources.blockAccess, false),
|
||||
not(effectiveSso)
|
||||
)
|
||||
);
|
||||
|
||||
return rows.map((row) => row.resourceId);
|
||||
}
|
||||
|
||||
async function resolveAccessibleIdsUncached(
|
||||
orgId: string,
|
||||
userId: string,
|
||||
@@ -189,8 +145,7 @@ async function resolveAccessibleIdsUncached(
|
||||
directPolicyResourceResults,
|
||||
rolePolicyResourceResults,
|
||||
directSiteResourceResults,
|
||||
roleSiteResourceResults,
|
||||
orgNonSsoPublicResources
|
||||
roleSiteResourceResults
|
||||
] = await Promise.all([
|
||||
db
|
||||
.select({ resourceId: userResources.resourceId })
|
||||
@@ -259,8 +214,7 @@ async function resolveAccessibleIdsUncached(
|
||||
})
|
||||
.from(roleSiteResources)
|
||||
.where(inArray(roleSiteResources.roleId, userRoleIds))
|
||||
: Promise.resolve([]),
|
||||
fetchOrgNonSsoPublicResourceIds(orgId)
|
||||
: Promise.resolve([])
|
||||
]);
|
||||
|
||||
return {
|
||||
@@ -269,8 +223,7 @@ async function resolveAccessibleIdsUncached(
|
||||
...directResources.map((r) => r.resourceId),
|
||||
...roleResourceResults.map((r) => r.resourceId),
|
||||
...directPolicyResourceResults.map((r) => r.resourceId),
|
||||
...rolePolicyResourceResults.map((r) => r.resourceId),
|
||||
...orgNonSsoPublicResources
|
||||
...rolePolicyResourceResults.map((r) => r.resourceId)
|
||||
])
|
||||
),
|
||||
siteResourceIds: Array.from(
|
||||
|
||||
@@ -161,7 +161,8 @@ const createSiteResourceSchema = z
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
data.destination !== undefined && data.destination.trim() !== ""
|
||||
data.destination !== undefined &&
|
||||
data.destination?.trim() !== ""
|
||||
);
|
||||
},
|
||||
{
|
||||
|
||||
@@ -157,7 +157,8 @@ const updateSiteResourceSchema = z
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
data.destination !== undefined && data.destination.trim() !== ""
|
||||
data.destination !== undefined &&
|
||||
data.destination?.trim() !== ""
|
||||
);
|
||||
},
|
||||
{
|
||||
@@ -616,10 +617,7 @@ export async function updateSiteResource(
|
||||
await trx
|
||||
.delete(roleSiteResources)
|
||||
.where(
|
||||
eq(
|
||||
roleSiteResources.siteResourceId,
|
||||
siteResourceId
|
||||
)
|
||||
eq(roleSiteResources.siteResourceId, siteResourceId)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import m17 from "./scriptsPg/1.18.0";
|
||||
import m18 from "./scriptsPg/1.18.3";
|
||||
import m19 from "./scriptsPg/1.18.4";
|
||||
import m20 from "./scriptsPg/1.19.0";
|
||||
import m21 from "./scriptsPg/1.20.0";
|
||||
|
||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||
@@ -51,7 +52,8 @@ const migrations = [
|
||||
{ version: "1.18.0", run: m17 },
|
||||
{ version: "1.18.3", run: m18 },
|
||||
{ version: "1.18.4", run: m19 },
|
||||
{ version: "1.19.0", run: m20 }
|
||||
{ version: "1.19.0", run: m20 },
|
||||
{ version: "1.20.0", run: m21 }
|
||||
// Add new migrations here as they are created
|
||||
] as {
|
||||
version: string;
|
||||
|
||||
@@ -45,7 +45,7 @@ import m39 from "./scriptsSqlite/1.18.3";
|
||||
import m40 from "./scriptsSqlite/1.18.4";
|
||||
import m41 from "./scriptsSqlite/1.19.0";
|
||||
import m42 from "./scriptsSqlite/1.19.1";
|
||||
import m43 from "./scriptsSqlite/1.19.5";
|
||||
import m43 from "./scriptsSqlite/1.20.0";
|
||||
|
||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||
@@ -89,7 +89,7 @@ const migrations = [
|
||||
{ version: "1.18.4", run: m40 },
|
||||
{ version: "1.19.0", run: m41 },
|
||||
{ version: "1.19.1", run: m42 },
|
||||
{ version: "1.19.5", run: m43 }
|
||||
{ version: "1.20.0", run: m43 }
|
||||
// Add new migrations here as they are created
|
||||
] as const;
|
||||
|
||||
|
||||
104
server/setup/scriptsPg/1.20.0.ts
Normal file
104
server/setup/scriptsPg/1.20.0.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { db } from "@server/db/pg/driver";
|
||||
import { sql } from "drizzle-orm";
|
||||
|
||||
const version = "1.20.0";
|
||||
|
||||
export default async function migration() {
|
||||
console.log(`Running setup script ${version}...`);
|
||||
|
||||
try {
|
||||
await db.execute(sql`BEGIN`);
|
||||
|
||||
await db.execute(sql`
|
||||
CREATE TABLE "remoteExitNodePreferenceLabels" (
|
||||
"remoteExitNodePreferenceLabelId" serial PRIMARY KEY NOT NULL,
|
||||
"remoteExitNodeId" varchar NOT NULL,
|
||||
"labelId" integer NOT NULL,
|
||||
CONSTRAINT "remote_exit_node_preference_label_uniq" UNIQUE("remoteExitNodeId","labelId")
|
||||
);
|
||||
`);
|
||||
|
||||
await db.execute(sql`
|
||||
CREATE TABLE "remoteExitNodeResources" (
|
||||
"remoteExitNodeResourceId" serial PRIMARY KEY NOT NULL,
|
||||
"remoteExitNodeId" varchar NOT NULL,
|
||||
"destination" varchar NOT NULL
|
||||
);
|
||||
`);
|
||||
|
||||
await db.execute(sql`
|
||||
CREATE TABLE "launcherViews" (
|
||||
"viewId" serial PRIMARY KEY NOT NULL,
|
||||
"orgId" varchar NOT NULL,
|
||||
"userId" varchar,
|
||||
"name" varchar NOT NULL,
|
||||
"config" text NOT NULL,
|
||||
"isDefault" boolean DEFAULT false NOT NULL,
|
||||
"createdAt" varchar NOT NULL,
|
||||
"updatedAt" varchar NOT NULL
|
||||
);
|
||||
`);
|
||||
|
||||
await db.execute(
|
||||
sql`ALTER TABLE "domains" ADD COLUMN "lastCheckedAt" integer;`
|
||||
);
|
||||
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "remoteExitNodePreferenceLabels" ADD CONSTRAINT "remoteExitNodePreferenceLabels_remoteExitNodeId_remoteExitNode_id_fk" FOREIGN KEY ("remoteExitNodeId") REFERENCES "public"."remoteExitNode"("id") ON DELETE cascade ON UPDATE no action;
|
||||
`);
|
||||
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "remoteExitNodePreferenceLabels" ADD CONSTRAINT "remoteExitNodePreferenceLabels_labelId_labels_labelId_fk" FOREIGN KEY ("labelId") REFERENCES "public"."labels"("labelId") ON DELETE cascade ON UPDATE no action;
|
||||
`);
|
||||
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "remoteExitNodeResources" ADD CONSTRAINT "remoteExitNodeResources_remoteExitNodeId_remoteExitNode_id_fk" FOREIGN KEY ("remoteExitNodeId") REFERENCES "public"."remoteExitNode"("id") ON DELETE cascade ON UPDATE no action;
|
||||
`);
|
||||
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "launcherViews" ADD CONSTRAINT "launcherViews_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;
|
||||
`);
|
||||
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "launcherViews" ADD CONSTRAINT "launcherViews_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
||||
`);
|
||||
|
||||
await db.execute(sql`
|
||||
CREATE INDEX "idx_clients_orgid_niceid" ON "clients" USING btree ("orgId","niceId");
|
||||
`);
|
||||
|
||||
await db.execute(sql`
|
||||
CREATE INDEX "idx_networks_orgid" ON "networks" USING btree ("orgId");
|
||||
`);
|
||||
|
||||
await db.execute(sql`
|
||||
CREATE INDEX "idx_resourcepolicies_orgid_niceid" ON "resourcePolicies" USING btree ("orgId","niceId");
|
||||
`);
|
||||
|
||||
await db.execute(sql`
|
||||
CREATE INDEX "idx_resources_niceid" ON "resources" USING btree ("niceId");
|
||||
`);
|
||||
|
||||
await db.execute(sql`
|
||||
CREATE INDEX "idx_resources_orgid_niceid" ON "resources" USING btree ("orgId","niceId");
|
||||
`);
|
||||
|
||||
await db.execute(sql`
|
||||
CREATE INDEX "idx_siteresources_orgid_niceid" ON "siteResources" USING btree ("orgId","niceId");
|
||||
`);
|
||||
|
||||
await db.execute(sql`
|
||||
CREATE INDEX "idx_sites_orgid_niceid" ON "sites" USING btree ("orgId","niceId");
|
||||
`);
|
||||
|
||||
await db.execute(sql`COMMIT`);
|
||||
console.log("Migrated database");
|
||||
} catch (e) {
|
||||
await db.execute(sql`ROLLBACK`);
|
||||
console.log("Unable to migrate database");
|
||||
console.log(e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
console.log(`${version} migration complete`);
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { APP_PATH } from "@server/lib/consts";
|
||||
import Database from "better-sqlite3";
|
||||
import path from "path";
|
||||
|
||||
const version = "1.19.5";
|
||||
const version = "1.20.0";
|
||||
|
||||
export default async function migration() {
|
||||
console.log(`Running setup script ${version}...`);
|
||||
@@ -92,6 +92,64 @@ export default async function migration() {
|
||||
db.prepare(
|
||||
`ALTER TABLE 'resourceSessions_new' RENAME TO 'resourceSessions';`
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE 'remoteExitNodePreferenceLabels' (
|
||||
'remoteExitNodePreferenceLabelId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
'remoteExitNodeId' text NOT NULL,
|
||||
'labelId' integer NOT NULL,
|
||||
FOREIGN KEY ('remoteExitNodeId') REFERENCES 'remoteExitNode'('id') ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY ('labelId') REFERENCES 'labels'('labelId') ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
`
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
CREATE UNIQUE INDEX 'remote_exit_node_preference_label_uniq' ON 'remoteExitNodePreferenceLabels' ('remoteExitNodeId','labelId');
|
||||
`
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE 'remoteExitNodeResources' (
|
||||
'remoteExitNodeResourceId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
'remoteExitNodeId' text NOT NULL,
|
||||
'destination' text NOT NULL,
|
||||
FOREIGN KEY ('remoteExitNodeId') REFERENCES 'remoteExitNode'('id') ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
`
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE 'launcherViews' (
|
||||
'viewId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
'orgId' text NOT NULL,
|
||||
'userId' text,
|
||||
'name' text NOT NULL,
|
||||
'config' text NOT NULL,
|
||||
'isDefault' integer DEFAULT false NOT NULL,
|
||||
'createdAt' text NOT NULL,
|
||||
'updatedAt' text NOT NULL,
|
||||
FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY ('userId') REFERENCES 'user'('id') ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
`
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
ALTER TABLE 'domains' ADD 'customCertResolver' text;
|
||||
`
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
ALTER TABLE 'domains' ADD 'lastCheckedAt' integer;
|
||||
`
|
||||
).run();
|
||||
})();
|
||||
|
||||
db.pragma("foreign_keys = ON");
|
||||
@@ -111,7 +111,7 @@ export default function AccessControlsPage() {
|
||||
if (values.roles.length === 0) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("accessRoleErrorAdd"),
|
||||
title: t("accessRoleRequired"),
|
||||
description: t("accessRoleSelectPlease")
|
||||
});
|
||||
return;
|
||||
@@ -173,15 +173,13 @@ export default function AccessControlsPage() {
|
||||
if (values.roles.length === 0) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("accessRoleErrorAdd"),
|
||||
title: t("accessRoleRequired"),
|
||||
description: t("accessRoleSelectPlease")
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const willHaveAdminRole = values.roles.some(
|
||||
(r) => r.isAdmin === true
|
||||
);
|
||||
const willHaveAdminRole = values.roles.some((r) => r.isAdmin === true);
|
||||
|
||||
const isRemovingOwnAdmin =
|
||||
sessionUser.userId === user.userId &&
|
||||
@@ -226,7 +224,9 @@ export default function AccessControlsPage() {
|
||||
<SettingsSectionForm>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={(e) => void handleAccessControlsSubmit(e)}
|
||||
onSubmit={(e) =>
|
||||
void handleAccessControlsSubmit(e)
|
||||
}
|
||||
className="space-y-4"
|
||||
id="access-controls-form"
|
||||
>
|
||||
|
||||
@@ -39,7 +39,6 @@ export function CreateOrgLabelDialog({
|
||||
const t = useTranslations();
|
||||
const api = createApiClient(useEnvContext());
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
const canManageLabels = isPaidUser(tierMatrix.labels);
|
||||
const [isSubmitting, startTransition] = useTransition();
|
||||
|
||||
async function createOrgLabel(data: { name: string; color: string }) {
|
||||
@@ -84,11 +83,8 @@ export function CreateOrgLabelDialog({
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
<PaidFeaturesAlert tiers={tierMatrix.labels} />
|
||||
<OrgLabelForm
|
||||
disabled={!canManageLabels}
|
||||
onSubmit={(data) => {
|
||||
if (!canManageLabels) return;
|
||||
startTransition(async () => createOrgLabel(data));
|
||||
}}
|
||||
/>
|
||||
@@ -106,7 +102,7 @@ export function CreateOrgLabelDialog({
|
||||
<Button
|
||||
type="submit"
|
||||
form="org-label-form"
|
||||
disabled={isSubmitting || !canManageLabels}
|
||||
disabled={isSubmitting}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
{t("labelCreate")}
|
||||
|
||||
@@ -44,8 +44,6 @@ export function EditOrgLabelDialog({
|
||||
}: EditOrgLabelDialogProps) {
|
||||
const t = useTranslations();
|
||||
const api = createApiClient(useEnvContext());
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
const canManageLabels = isPaidUser(tierMatrix.labels);
|
||||
const [isSubmitting, startTransition] = useTransition();
|
||||
|
||||
async function editOrgLabel(data: { name: string; color: string }) {
|
||||
@@ -90,12 +88,9 @@ export function EditOrgLabelDialog({
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
<PaidFeaturesAlert tiers={tierMatrix.labels} />
|
||||
<OrgLabelForm
|
||||
disabled={!canManageLabels}
|
||||
defaultValue={label}
|
||||
onSubmit={(data) => {
|
||||
if (!canManageLabels) return;
|
||||
startTransition(async () => editOrgLabel(data));
|
||||
}}
|
||||
/>
|
||||
@@ -113,7 +108,7 @@ export function EditOrgLabelDialog({
|
||||
<Button
|
||||
type="submit"
|
||||
form="org-label-form"
|
||||
disabled={isSubmitting || !canManageLabels}
|
||||
disabled={isSubmitting}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
{t("labelEdit")}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { useRef } from "react";
|
||||
import type { FieldValues, Path, UseFormReturn } from "react-hook-form";
|
||||
import { RolesSelector, type SelectedRole } from "./roles-selector";
|
||||
|
||||
@@ -41,6 +42,46 @@ export default function OrgRolesTagField<TFieldValues extends FieldValues>({
|
||||
disabled
|
||||
}: OrgRolesTagFieldProps<TFieldValues>) {
|
||||
const t = useTranslations();
|
||||
const isPopoverOpenRef = useRef(false);
|
||||
const lastValidRolesRef = useRef<SelectedRole[]>(
|
||||
(form.getValues(name) as SelectedRole[]) ?? []
|
||||
);
|
||||
|
||||
function validateRolesSelection() {
|
||||
const current = form.getValues(name) as SelectedRole[];
|
||||
|
||||
if (current.length === 0 && lastValidRolesRef.current.length > 0) {
|
||||
form.setValue(name, lastValidRolesRef.current as never, {
|
||||
shouldDirty: true
|
||||
});
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("accessRoleRequired"),
|
||||
description: t("accessRoleSelectPlease")
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (current.length > 0) {
|
||||
lastValidRolesRef.current = current;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function handlePopoverOpenChange(open: boolean) {
|
||||
isPopoverOpenRef.current = open;
|
||||
|
||||
if (open) {
|
||||
const current = form.getValues(name) as SelectedRole[];
|
||||
if (current.length > 0) {
|
||||
lastValidRolesRef.current = current;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
validateRolesSelection();
|
||||
}
|
||||
|
||||
function setRoleTags(nextValue: SelectedRole[]) {
|
||||
const prev = form.getValues(name) as SelectedRole[];
|
||||
@@ -61,39 +102,44 @@ export default function OrgRolesTagField<TFieldValues extends FieldValues>({
|
||||
return;
|
||||
}
|
||||
|
||||
if (next.length === 0) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("accessRoleErrorAdd"),
|
||||
description: t("accessRoleSelectPlease")
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
form.setValue(name, next as never, { shouldDirty: true });
|
||||
|
||||
if (next.length > 0 && !isPopoverOpenRef.current) {
|
||||
lastValidRolesRef.current = next;
|
||||
} else if (!isPopoverOpenRef.current) {
|
||||
validateRolesSelection();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col items-start">
|
||||
<FormLabel>{label ?? t("roles")}</FormLabel>
|
||||
<FormControl>
|
||||
<RolesSelector
|
||||
orgId={orgId}
|
||||
selectedRoles={field.value ?? []}
|
||||
onSelectRoles={setRoleTags}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</FormControl>
|
||||
{showMultiRolePaywallMessage && (
|
||||
<FormDescription>{paywallMessage}</FormDescription>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
render={({ field }) => {
|
||||
const selectedRoles = (field.value ?? []) as SelectedRole[];
|
||||
if (!isPopoverOpenRef.current && selectedRoles.length > 0) {
|
||||
lastValidRolesRef.current = selectedRoles;
|
||||
}
|
||||
|
||||
return (
|
||||
<FormItem className="flex flex-col items-start">
|
||||
<FormLabel>{label ?? t("roles")}</FormLabel>
|
||||
<FormControl>
|
||||
<RolesSelector
|
||||
orgId={orgId}
|
||||
selectedRoles={selectedRoles}
|
||||
onSelectRoles={setRoleTags}
|
||||
onPopoverOpenChange={handlePopoverOpenChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</FormControl>
|
||||
{showMultiRolePaywallMessage && (
|
||||
<FormDescription>{paywallMessage}</FormDescription>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -111,6 +111,20 @@ function getActionsCategories(root: boolean) {
|
||||
[t("actionUpdateResourceRule")]: "updateResourceRule"
|
||||
},
|
||||
|
||||
"Resource Policy": {
|
||||
[t("actionGetResourcePolicy")]: "getResourcePolicy",
|
||||
[t("actionUpdateResourcePolicy")]: "updateResourcePolicy",
|
||||
[t("actionSetResourcePolicyUsers")]: "setResourcePolicyUsers",
|
||||
[t("actionSetResourcePolicyRoles")]: "setResourcePolicyRoles",
|
||||
[t("actionSetResourcePolicyPassword")]: "setResourcePolicyPassword",
|
||||
[t("actionSetResourcePolicyPincode")]: "setResourcePolicyPincode",
|
||||
[t("actionSetResourcePolicyHeaderAuth")]:
|
||||
"setResourcePolicyHeaderAuth",
|
||||
[t("actionSetResourcePolicyWhitelist")]:
|
||||
"setResourcePolicyWhitelist",
|
||||
[t("actionSetResourcePolicyRules")]: "setResourcePolicyRules"
|
||||
},
|
||||
|
||||
Client: {
|
||||
[t("actionCreateClient")]: "createClient",
|
||||
[t("actionDeleteClient")]: "deleteClient",
|
||||
|
||||
@@ -427,6 +427,27 @@ export default function PrivateResourcesTable({
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "labels",
|
||||
accessorKey: "labels",
|
||||
header: () => (
|
||||
<LabelColumnFilterButton
|
||||
orgId={orgId}
|
||||
selectedValues={searchParams.getAll("labels")}
|
||||
onSelectedValuesChange={(value) =>
|
||||
handleFilterChange("labels", value)
|
||||
}
|
||||
label={t("labels")}
|
||||
className="p-3"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }: { row: { original: InternalResourceRow } }) => (
|
||||
<ClientResourceLabelCell
|
||||
resource={row.original}
|
||||
orgId={orgId}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
enableHiding: false,
|
||||
@@ -487,27 +508,6 @@ export default function PrivateResourcesTable({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "labels",
|
||||
accessorKey: "labels",
|
||||
header: () => (
|
||||
<LabelColumnFilterButton
|
||||
orgId={orgId}
|
||||
selectedValues={searchParams.getAll("labels")}
|
||||
onSelectedValuesChange={(value) =>
|
||||
handleFilterChange("labels", value)
|
||||
}
|
||||
label={t("labels")}
|
||||
className="p-3"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }: { row: { original: InternalResourceRow } }) => (
|
||||
<ClientResourceLabelCell
|
||||
resource={row.original}
|
||||
orgId={orgId}
|
||||
/>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -551,6 +551,24 @@ export default function PublicResourcesTable({
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: "labels",
|
||||
accessorKey: "labels",
|
||||
header: () => (
|
||||
<LabelColumnFilterButton
|
||||
orgId={orgId}
|
||||
selectedValues={searchParams.getAll("labels")}
|
||||
onSelectedValuesChange={(value) =>
|
||||
handleFilterChange("labels", value)
|
||||
}
|
||||
label={t("labels")}
|
||||
className="p-3"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }: { row: { original: ResourceRow } }) => (
|
||||
<ResourceLabelCell resource={row.original} orgId={orgId} />
|
||||
)
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
enableHiding: false,
|
||||
@@ -603,24 +621,6 @@ export default function PublicResourcesTable({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "labels",
|
||||
accessorKey: "labels",
|
||||
header: () => (
|
||||
<LabelColumnFilterButton
|
||||
orgId={orgId}
|
||||
selectedValues={searchParams.getAll("labels")}
|
||||
onSelectedValuesChange={(value) =>
|
||||
handleFilterChange("labels", value)
|
||||
}
|
||||
label={t("labels")}
|
||||
className="p-3"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }: { row: { original: ResourceRow } }) => (
|
||||
<ResourceLabelCell resource={row.original} orgId={orgId} />
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -498,6 +498,23 @@ export default function SitesTable({
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "labels",
|
||||
header: () => (
|
||||
<LabelColumnFilterButton
|
||||
orgId={orgId}
|
||||
selectedValues={searchParams.getAll("labels")}
|
||||
onSelectedValuesChange={(value) =>
|
||||
handleFilterChange("labels", value)
|
||||
}
|
||||
label={t("labels")}
|
||||
className="p-3"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }: { row: { original: SiteRow } }) => (
|
||||
<SiteLabelCell site={row.original} orgId={orgId} />
|
||||
)
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
enableHiding: false,
|
||||
@@ -599,23 +616,6 @@ export default function SitesTable({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "labels",
|
||||
header: () => (
|
||||
<LabelColumnFilterButton
|
||||
orgId={orgId}
|
||||
selectedValues={searchParams.getAll("labels")}
|
||||
onSelectedValuesChange={(value) =>
|
||||
handleFilterChange("labels", value)
|
||||
}
|
||||
label={t("labels")}
|
||||
className="p-3"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }: { row: { original: SiteRow } }) => (
|
||||
<SiteLabelCell site={row.original} orgId={orgId} />
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -17,11 +17,13 @@ export interface MultiSelectInputProps<
|
||||
> extends MultiSelectTagsProps<T> {
|
||||
buttonText?: string;
|
||||
lockedIds?: Set<string>;
|
||||
onPopoverOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function MultiSelectTagInput<T extends TagValue>({
|
||||
buttonText,
|
||||
lockedIds,
|
||||
onPopoverOpenChange,
|
||||
...props
|
||||
}: MultiSelectInputProps<T>) {
|
||||
const selectedValues = new Set(props.value.map((v) => v.id));
|
||||
@@ -33,6 +35,7 @@ export function MultiSelectTagInput<T extends TagValue>({
|
||||
// clear input when popover is closed
|
||||
props.onSearch("");
|
||||
}
|
||||
onPopoverOpenChange?.(open);
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
|
||||
@@ -587,7 +587,9 @@ export default function ResourceLauncher({
|
||||
|
||||
const renderToolbarSearch = (searchClassName: string) => (
|
||||
<div className={cn("relative shrink-0", searchClassName)}>
|
||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
||||
<span className="pointer-events-none absolute inset-y-0 left-2 flex items-center">
|
||||
<Search className="size-4 text-muted-foreground" />
|
||||
</span>
|
||||
<Input
|
||||
key={`${activeViewId}-${searchInputResetKey}`}
|
||||
defaultValue={config.query}
|
||||
|
||||
@@ -12,6 +12,7 @@ export type RolesSelectorProps = {
|
||||
orgId: string;
|
||||
selectedRoles?: SelectedRole[];
|
||||
onSelectRoles: (roles: SelectedRole[]) => void;
|
||||
onPopoverOpenChange?: (open: boolean) => void;
|
||||
disabled?: boolean;
|
||||
restrictAdminRole?: boolean;
|
||||
mapRolesByName?: boolean;
|
||||
@@ -23,6 +24,7 @@ export function RolesSelector({
|
||||
orgId,
|
||||
selectedRoles = [],
|
||||
onSelectRoles,
|
||||
onPopoverOpenChange,
|
||||
disabled,
|
||||
restrictAdminRole,
|
||||
mapRolesByName,
|
||||
@@ -77,6 +79,7 @@ export function RolesSelector({
|
||||
options={rolesShown}
|
||||
value={selectedRoles}
|
||||
onChange={onSelectRoles}
|
||||
onPopoverOpenChange={onPopoverOpenChange}
|
||||
disabled={disabled}
|
||||
lockedIds={lockedIds}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user