This commit is contained in:
Dhananjaya99
2026-07-18 23:42:58 +05:30
parent 80b130dffb
commit 92c4b14a6c
55 changed files with 8815 additions and 43 deletions
@@ -0,0 +1,63 @@
"use client"
// Authoritative session context: fetches GET /auth/me once per mount and exposes
// the current role + permitted sidebar nav codes. Replaces trusting the stale,
// client-only `roleId` cached by lib/auth-session.ts for anything access-related
// (that cache remains display-only, e.g. for the header's user name).
import { createContext, useContext, useEffect, useState } from "react"
import { authApi } from "@/lib/api/auth"
import { MeResponse } from "@/types/rbac"
interface AuthContextValue {
roleCode: string | null
roleName: string | null
navCodes: string[]
/** True until the first `/auth/me` response lands. */
loading: boolean
}
const AuthContext = createContext<AuthContextValue>({
roleCode: null,
roleName: null,
navCodes: [],
loading: true,
})
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [me, setMe] = useState<MeResponse | null>(null)
useEffect(() => {
let cancelled = false
authApi
.me()
.then((res) => {
if (!cancelled) setMe(res)
})
.catch(() => {
// Unauthenticated/unreachable — fall back to "no permissions" rather than
// crash the shell; proxy.ts already redirects unauthenticated users to /login.
if (!cancelled) setMe({ roleCode: null, roleName: null, navCodes: [] })
})
return () => {
cancelled = true
}
}, [])
return (
<AuthContext.Provider
value={{
roleCode: me?.roleCode ?? null,
roleName: me?.roleName ?? null,
navCodes: me?.navCodes ?? [],
loading: me === null,
}}
>
{children}
</AuthContext.Provider>
)
}
export function useAuth(): AuthContextValue {
return useContext(AuthContext)
}
@@ -0,0 +1,85 @@
"use client"
// Checkbox tree for assigning a role's visible sidebar sections. Leaf nav items
// (no children) are toggled directly; parent nav items with children are a
// "select all children" convenience toggle — the parent's own visibility is
// derived from its children on the frontend (see AppSidebar.tsx's filter), so
// only the children need to carry the actual grant for those groups.
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"
import { NavItem } from "@/types/rbac"
interface Props {
tree: NavItem[]
selectedNavItemIds: Set<number>
selectedSubNavItemIds: Set<number>
onChange: (navItemIds: Set<number>, subNavItemIds: Set<number>) => void
}
export function RolePermissionTree({ tree, selectedNavItemIds, selectedSubNavItemIds, onChange }: Props) {
function toggleLeaf(navItemId: number) {
const next = new Set(selectedNavItemIds)
if (next.has(navItemId)) next.delete(navItemId)
else next.add(navItemId)
onChange(next, selectedSubNavItemIds)
}
function toggleChild(subNavItemId: number) {
const next = new Set(selectedSubNavItemIds)
if (next.has(subNavItemId)) next.delete(subNavItemId)
else next.add(subNavItemId)
onChange(selectedNavItemIds, next)
}
function toggleAllChildren(item: NavItem, checked: boolean) {
const next = new Set(selectedSubNavItemIds)
for (const child of item.children) {
if (checked) next.add(child.subNavItemId)
else next.delete(child.subNavItemId)
}
onChange(selectedNavItemIds, next)
}
return (
<div className="flex flex-col gap-1 rounded-lg border p-4">
{tree.map((item) => {
if (item.children.length === 0) {
return (
<label key={item.navItemId} className="flex items-center gap-3 rounded-md px-2 py-2 hover:bg-muted/50">
<Checkbox
checked={selectedNavItemIds.has(item.navItemId)}
onCheckedChange={() => toggleLeaf(item.navItemId)}
/>
<Label className="cursor-pointer text-base font-medium">{item.label}</Label>
</label>
)
}
const checkedCount = item.children.filter((c) => selectedSubNavItemIds.has(c.subNavItemId)).length
const allChecked = checkedCount === item.children.length
return (
<div key={item.navItemId} className="flex flex-col gap-1 border-t pt-2 first:border-t-0 first:pt-0">
<label className="flex items-center gap-3 rounded-md px-2 py-2 hover:bg-muted/50">
<Checkbox checked={allChecked} onCheckedChange={(checked) => toggleAllChildren(item, !!checked)} />
<Label className="cursor-pointer text-base font-semibold">
{item.label} {checkedCount > 0 && !allChecked && <span className="text-sm font-normal text-muted-foreground">({checkedCount} of {item.children.length})</span>}
</Label>
</label>
<div className="flex flex-col gap-1 pl-9">
{item.children.map((child) => (
<label key={child.subNavItemId} className="flex items-center gap-3 rounded-md px-2 py-1.5 hover:bg-muted/50">
<Checkbox
checked={selectedSubNavItemIds.has(child.subNavItemId)}
onCheckedChange={() => toggleChild(child.subNavItemId)}
/>
<Label className="cursor-pointer text-sm">{child.label}</Label>
</label>
))}
</div>
</div>
)
})}
</div>
)
}