Feat: RBAC, Keys Extension, Invites

feat: org members page — invite users, cancel invites, change roles
feat: show pending invitations banner on profile page
feat: invite accept flow for existing users (no password needed)
feat: departments page updates
feat: SSH keys page — dept cert policy UI (expiry + extensions)
feat: wire up auth pages to real API (register, verify, reset, OIDC)
feat: CLI auth bridge — login page handles CLI token flow
feat: admin users — suspend/unsuspend, role badges, role filter
feat: add admin OAuth providers management page
feat: activity page — org-wide audit log view for admins
feat: add my memberships page
chore: add isOrgAdmin/isOrgMember to AuthContext, restrict sidebar
chore: update app routing and shared layout
This commit is contained in:
2026-03-01 16:50:19 +05:45
parent 62f767474b
commit 4c01fd0107
22 changed files with 2457 additions and 496 deletions
+158
View File
@@ -0,0 +1,158 @@
import { useEffect, useState } from "react";
import { Layers, GitBranch, Building2, Loader2, Link } from "lucide-react";
import { api, MyOrgMembership } from "@/lib/api";
import { Skeleton } from "@/components/ui/skeleton";
import { Badge } from "@/components/ui/badge";
function MembershipsSkeleton() {
return (
<div className="space-y-6">
{[1, 2].map((i) => (
<div key={i} className="rounded-xl border border-border bg-card p-5 space-y-4">
<Skeleton className="h-5 w-48" />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Skeleton className="h-24 w-full rounded-lg" />
<Skeleton className="h-24 w-full rounded-lg" />
</div>
</div>
))}
</div>
);
}
export default function MyMembershipsPage() {
const [orgs, setOrgs] = useState<MyOrgMembership[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
api.users.getMyMemberships()
.then((res) => setOrgs(res.orgs ?? []))
.catch(console.error)
.finally(() => setIsLoading(false));
}, []);
const totalDepts = orgs.reduce((s, o) => s + o.departments.length, 0);
const totalPrincipals = orgs.reduce((s, o) => s + o.principals.length, 0);
return (
<div className="page-container">
<div className="page-header">
<h1 className="page-title">My Memberships</h1>
<p className="page-description">
Departments and principals you belong to across your organizations
</p>
</div>
{isLoading ? (
<MembershipsSkeleton />
) : orgs.length === 0 ? (
<div className="flex flex-col items-center justify-center py-20 text-center text-muted-foreground">
<Building2 className="w-10 h-10 mb-3 opacity-30" />
<p className="text-sm">You're not a member of any organizations yet.</p>
</div>
) : (
<div className="space-y-6">
{/* Summary chips */}
<div className="flex flex-wrap gap-3">
<div className="inline-flex items-center gap-2 rounded-full border border-border bg-card px-4 py-1.5 text-sm">
<Layers className="w-3.5 h-3.5 text-primary" />
<span className="font-medium">{totalDepts}</span>
<span className="text-muted-foreground">department{totalDepts !== 1 ? "s" : ""}</span>
</div>
<div className="inline-flex items-center gap-2 rounded-full border border-border bg-card px-4 py-1.5 text-sm">
<GitBranch className="w-3.5 h-3.5 text-primary" />
<span className="font-medium">{totalPrincipals}</span>
<span className="text-muted-foreground">principal{totalPrincipals !== 1 ? "s" : ""}</span>
</div>
</div>
{orgs.map((org) => (
<div
key={org.org_id}
className="rounded-xl border border-border bg-card overflow-hidden"
>
{/* Org header */}
<div className="flex items-center gap-3 px-5 py-4 border-b border-border bg-muted/30">
<div className="w-7 h-7 rounded bg-primary/10 flex items-center justify-center shrink-0">
<Building2 className="w-3.5 h-3.5 text-primary" />
</div>
<span className="font-semibold text-foreground text-sm">{org.org_name}</span>
<Badge variant="outline" className="capitalize text-xs ml-auto">
{org.role.toLowerCase()}
</Badge>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 divide-y md:divide-y-0 md:divide-x divide-border">
{/* Departments */}
<div className="p-5 space-y-3">
<div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
<Layers className="w-3.5 h-3.5" />
Departments
</div>
{org.departments.length === 0 ? (
<p className="text-xs text-muted-foreground py-2">
Not assigned to any departments.
</p>
) : (
<ul className="space-y-2">
{org.departments.map((dept) => (
<li
key={dept.id}
className="flex items-start gap-2.5 rounded-lg border border-border bg-background px-3 py-2.5"
>
<Layers className="w-3.5 h-3.5 mt-0.5 text-primary shrink-0" />
<div className="min-w-0">
<p className="text-sm font-medium text-foreground truncate">{dept.name}</p>
{dept.description && (
<p className="text-xs text-muted-foreground mt-0.5 line-clamp-1">{dept.description}</p>
)}
</div>
</li>
))}
</ul>
)}
</div>
{/* Principals */}
<div className="p-5 space-y-3">
<div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
<GitBranch className="w-3.5 h-3.5" />
Principals
</div>
{org.principals.length === 0 ? (
<p className="text-xs text-muted-foreground py-2">
No principals assigned.
</p>
) : (
<ul className="space-y-2">
{org.principals.map((p) => (
<li
key={p.id}
className="flex items-start gap-2.5 rounded-lg border border-border bg-background px-3 py-2.5"
>
<GitBranch className="w-3.5 h-3.5 mt-0.5 text-primary shrink-0" />
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-foreground truncate">{p.name}</p>
{p.description && (
<p className="text-xs text-muted-foreground mt-0.5 line-clamp-1">{p.description}</p>
)}
</div>
{p.via_department && (
<span className="shrink-0 inline-flex items-center gap-1 text-[10px] text-muted-foreground border border-border rounded px-1.5 py-0.5">
<Link className="w-2.5 h-2.5" />
via dept
</span>
)}
</li>
))}
</ul>
)}
</div>
</div>
</div>
))}
</div>
)}
</div>
);
}