- {oauthFlow
- ? "After signing in you'll review what the application can access before anything is shared."
- : "New here? Continue with an account to get started. You can add a passkey once you're signed in."}
+ By continuing, you acknowledge that you have read and understood our{" "}
+
+ Privacy Policy.
+
diff --git a/src/components/rbac/catalog-search.tsx b/src/components/rbac/catalog-search.tsx
index 80fe17b..a5f67a6 100644
--- a/src/components/rbac/catalog-search.tsx
+++ b/src/components/rbac/catalog-search.tsx
@@ -1,9 +1,62 @@
import { useRef, type ReactNode } from "react";
import { Search, X } from "lucide-react";
+import { cn } from "cn";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
+/** The search box on its own, for pages that lay out the rest of the toolbar themselves. */
+export function SearchField({
+ kind,
+ query,
+ onQueryChange,
+ className,
+ inputClassName,
+}: {
+ kind: "roles" | "permissions";
+ query: string;
+ onQueryChange: (query: string) => void;
+ className?: string;
+ inputClassName?: string;
+}) {
+ const input = useRef(null);
+
+ return (
+
{query.trim() ? `${count} of ${total}` : total} {kind}
diff --git a/src/components/rbac/icons.tsx b/src/components/rbac/icons.tsx
new file mode 100644
index 0000000..01ee946
--- /dev/null
+++ b/src/components/rbac/icons.tsx
@@ -0,0 +1,48 @@
+import { CircleQuestionMark, Crown, KeyRound, type LucideProps, Plus, Users } from "lucide-react";
+import { cn } from "cn";
+
+/*
+ * The eRBACo designs draw lucide's glyphs with their own line widths, given in pixels at the size
+ * each icon renders. `absoluteStrokeWidth` keeps them at those pixels, which is why every icon
+ * here takes a fixed `size` rather than a size class.
+ */
+
+type IconProps = Omit;
+
+export function PermissionsIcon({ className, ...props }: IconProps) {
+ // The design's key hole is a heavier dot than the outline around it.
+ return (
+
+ );
+}
+
+export function MembersIcon(props: IconProps) {
+ return ;
+}
+
+export function MasterAdminIcon(props: IconProps) {
+ return ;
+}
+
+export function HelpIcon(props: IconProps) {
+ return (
+
+ );
+}
+
+export function NewIcon(props: IconProps) {
+ return ;
+}
diff --git a/src/components/route-error.tsx b/src/components/route-error.tsx
index 752dc4c..160e03e 100644
--- a/src/components/route-error.tsx
+++ b/src/components/route-error.tsx
@@ -16,7 +16,7 @@ export function SectionShell({
children: ReactNode;
}) {
return (
-
+
{children}
diff --git a/src/dev/personas.ts b/src/dev/personas.ts
index eabd847..0cec4ff 100644
--- a/src/dev/personas.ts
+++ b/src/dev/personas.ts
@@ -78,7 +78,7 @@ export const DEV_PERSONAS: DevPersona[] = [
id: DEV_ADMIN_ID,
name: "Ada Admin",
email: "admin@polinetwork.test",
- description: "Master Admin: every permission",
+ description: "Master Admin",
accounts: [{ providerId: "google", accountId: "dev-admin" }],
},
{
@@ -86,7 +86,7 @@ export const DEV_PERSONAS: DevPersona[] = [
id: "dev-staff",
name: "Sam Staff",
email: "staff@polinetwork.test",
- description: "Can read the user directory and roles, nothing else",
+ description: "Can read the user directory and roles",
accounts: [{ providerId: "google", accountId: "dev-staff" }],
roles: [
{
@@ -103,7 +103,7 @@ export const DEV_PERSONAS: DevPersona[] = [
id: "dev-student",
name: "Stella Student",
email: "student@polinetwork.test",
- description: "Verified Polimi student with Telegram linked, no admin access",
+ description: "Verified Polimi student with Telegram linked",
accounts: [
{ providerId: "google", accountId: "dev-student" },
{
@@ -119,7 +119,7 @@ export const DEV_PERSONAS: DevPersona[] = [
id: "dev-member",
name: "Nico Newcomer",
email: "member@polinetwork.test",
- description: "Just signed up with Google: no statuses, no roles",
+ description: "Blank login with Google",
accounts: [{ providerId: "google", accountId: "dev-member" }],
},
];
@@ -135,8 +135,8 @@ export function listPersonas(): DevPersonaSummary[] {
description,
...(id === DEV_ADMIN_ID && !env.IDP_ADMIN_USER_IDS.includes(DEV_ADMIN_ID)
? {
- warning: `Add ${DEV_ADMIN_ID} to IDP_ADMIN_USER_IDS in .env.local to make this Master Admin.`,
- }
+ warning: `Add ${DEV_ADMIN_ID} to IDP_ADMIN_USER_IDS in .env.local to make this Master Admin.`,
+ }
: {}),
}));
}
@@ -144,7 +144,7 @@ export function listPersonas(): DevPersonaSummary[] {
type Writer = Parameters[0]>[0];
/** Thrown instead of silently moving an identity between people. */
-export class DevFixtureConflict extends Error {}
+export class DevFixtureConflict extends Error { }
/** Finds the fixture role by its stable ID, so a key renamed in the role editor still matches;
* then by key, so a role created by hand under that key is reused rather than collided with. */
diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx
index 4dfbcc2..4acc1ba 100644
--- a/src/routes/__root.tsx
+++ b/src/routes/__root.tsx
@@ -41,8 +41,28 @@ function RootDocument({ children }: { children: React.ReactNode }) {
-
- {children}
+
+
{children}
+
requireAccess(context.viewer, "idp:roles:read"),
@@ -21,56 +28,105 @@ export const Route = createFileRoute("/access/roles/")({
component: RolesIndex,
});
+const plural = (count: number, one: string, many: string) => `${count} ${count === 1 ? one : many}`;
+
+function BuiltInRoleIcon({ roleKey }: { roleKey: string }) {
+ if (roleKey === "master-admin") return ;
+ if (roleKey === "student")
+ // The seal is drawn in white, so the light theme gets a disc to hold it.
+ return (
+
+
+
+ );
+ // The mark is drawn in white too, so the light theme shows the full-colour logo instead.
+ return (
+ <>
+
+
+ >
+ );
+}
+
/** `effective` is null for a role that holds every permission there is. */
-function RoleRow({ role, effective }: { role: RoleSummary; effective: number | null }) {
- const inferred = role.managed ? staticRole(role.key) : undefined;
+function BuiltInRoleCard({ role, effective }: { role: RoleSummary; effective: number | null }) {
return (
- A role is a named bundle of permissions. Applications read them from the{" "}
- polinetwork:identity scope.
-
+
+
+
+
+ Built-in
+
+
+
+
+
+
+ These roles always exist and nobody hands them out: they follow the evidence the
+ identity provider already collects, and Master Admin follows the administrator
+ allowlist configured for this deployment. You can still choose what they grant and
+ where they sit in the hierarchy.
+
+
-
-
-
- {can("idp:roles:write") && (
-
- )}
-
+
+ {managed.map((role) => (
+
+ ))}
+
+
- {search && filtered.length === 0 && }
-
- {
-
- {(managed.length > 0 || !search) && (
-
-
- Built into the identity provider
-
- These roles always exist and nobody hands them out: they follow the evidence the
- identity provider already collects, and Master Admin follows the administrator
- allowlist configured for this deployment. You can still choose what they grant and
- where they sit in the hierarchy.
-
-
-
- {managed.length === 0 ? (
-