[CLAUDE] Phase1.2: CRUD Master + Permission Matrix + FE admin pages
Backend:
- Domain/Master: Supplier (+ SupplierType 5 loai), Project, Department (AuditableEntity)
- Domain/Identity: MenuItem, Permission, MenuKeys const (12 menu)
- EF Configurations voi unique Code + query filter IsDeleted
- DbSets + IApplicationDbContext interface update
- Application: PagedResult + PagedRequest generic
- Application/Master CQRS CRUD 3 entity (Create/Update/Delete/Get/List voi paging search sort)
- Application/Permissions: GetMyMenuTree (union OR role, filter tree), ListMenuItems, ListPermissionsByRole, UpsertPermission (guard admin khong tu giam quyen), ListRoles
- Api/Authorization: MenuPermissionRequirement + Handler (Admin bypass, query DB)
- Program.cs: register 48 policy {menu}.{action} tu MenuKeys x Actions
- Api/Controllers: Suppliers, Projects, Departments, Menus, Roles, Permissions
- DbInitializer: seed 12 menu + admin full CRUD permissions
- Migration AddMasterData + AddPermissions
Frontend (fe-admin):
- Types: menuKeys.ts const, menu.ts (MenuNode/Role/Permission), master.ts (Supplier/Project/Department + SupplierType const-object)
- AuthContext: load menu from /menus/me, cache localStorage, refreshMenu()
- usePermission hook + PermissionGuard component (wrap button)
- UI kit them: Dialog (modal overlay), Textarea, Select
- Generic: DataTable (column config, sortable, loading, empty) + Pagination
- PageHeader component
- apiError helper extract message tu ProblemDetails
- Layout rewrite: render menu dong tu AuthContext.menu (MenuGroup collapsible + NavLink + lucide icon map)
- Pages: master/Suppliers, master/Projects, master/Departments (CRUD + search + sort + paging + Dialog form)
- Page system/Permissions: ma tran Role x MenuKey x CRUD checkbox (tick tu dong PUT upsert)
- App.tsx them 4 route moi
Bug fix:
- MenuPermissionHandler: EF expression tree khong support switch expression -> tach switch ra ngoai AnyAsync
- TS erasableSyntaxOnly khong cho enum -> SupplierType const-object pattern (typeof[keyof])
E2E verified via Vite proxy:
- GET /menus/me -> 6 root + 6 child nodes (12 menus)
- GET /roles -> 12 roles
- POST/GET/PUT/DELETE /suppliers -> full CRUD, soft delete OK
- tsc -b fe-admin pass
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -1,29 +1,48 @@
|
||||
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
|
||||
import { api, TOKEN_KEY, USER_KEY } from '@/lib/api'
|
||||
import type { AuthResponse, LoginPayload, UserInfo } from '@/types/auth'
|
||||
import type { MenuNode } from '@/types/menu'
|
||||
|
||||
type AuthContextValue = {
|
||||
user: UserInfo | null
|
||||
menu: MenuNode[]
|
||||
isAuthenticated: boolean
|
||||
isBootstrapping: boolean
|
||||
login: (payload: LoginPayload) => Promise<void>
|
||||
logout: () => void
|
||||
refreshMenu: () => Promise<void>
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextValue | null>(null)
|
||||
const MENU_KEY = 'solution-erp-admin-menu'
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<UserInfo | null>(null)
|
||||
const [menu, setMenu] = useState<MenuNode[]>([])
|
||||
const [isBootstrapping, setIsBootstrapping] = useState(true)
|
||||
|
||||
async function loadMenu() {
|
||||
try {
|
||||
const res = await api.get<MenuNode[]>('/menus/me')
|
||||
setMenu(res.data)
|
||||
localStorage.setItem(MENU_KEY, JSON.stringify(res.data))
|
||||
} catch {
|
||||
// keep cached menu if request fails
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem(TOKEN_KEY)
|
||||
const raw = localStorage.getItem(USER_KEY)
|
||||
if (token && raw) {
|
||||
const userRaw = localStorage.getItem(USER_KEY)
|
||||
const menuRaw = localStorage.getItem(MENU_KEY)
|
||||
if (token && userRaw) {
|
||||
try {
|
||||
setUser(JSON.parse(raw))
|
||||
setUser(JSON.parse(userRaw))
|
||||
if (menuRaw) setMenu(JSON.parse(menuRaw))
|
||||
loadMenu()
|
||||
} catch {
|
||||
localStorage.removeItem(USER_KEY)
|
||||
localStorage.removeItem(MENU_KEY)
|
||||
}
|
||||
}
|
||||
setIsBootstrapping(false)
|
||||
@ -34,16 +53,19 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
localStorage.setItem(TOKEN_KEY, res.data.accessToken)
|
||||
localStorage.setItem(USER_KEY, JSON.stringify(res.data.user))
|
||||
setUser(res.data.user)
|
||||
await loadMenu()
|
||||
}
|
||||
|
||||
function logout() {
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
localStorage.removeItem(USER_KEY)
|
||||
localStorage.removeItem(MENU_KEY)
|
||||
setUser(null)
|
||||
setMenu([])
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, isAuthenticated: !!user, isBootstrapping, login, logout }}>
|
||||
<AuthContext.Provider value={{ user, menu, isAuthenticated: !!user, isBootstrapping, login, logout, refreshMenu: loadMenu }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user