page.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. import { redirect } from 'next/navigation';
  2. import { db } from '@/db';
  3. import { groups, permissions, rolePermissions, roles, users } from '@/db/schema/auth';
  4. import { aclRules, resources } from '@/db/schema/resource';
  5. import { requireAdmin } from '@/lib/auth/admin';
  6. import { Badge } from '@/components/ui/badge';
  7. import { Button } from '@/components/ui/button';
  8. import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
  9. import { Dialog } from '@/components/ui/dialog';
  10. import { Input } from '@/components/ui/input';
  11. import { Label } from '@/components/ui/label';
  12. import { Select } from '@/components/ui/select';
  13. import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
  14. import { Tabs } from '@/components/ui/tabs';
  15. import {
  16. assignPermissionToRoleAction,
  17. createAclRuleAction,
  18. createPermissionAction,
  19. createResourceAction,
  20. deleteAclRuleAction,
  21. removePermissionFromRoleAction,
  22. } from './actions';
  23. export default async function PermissionsAdminPage() {
  24. try { await requireAdmin(); } catch { redirect('/login'); }
  25. const [roleRows, permissionRows, rolePermissionRows, resourceRows, aclRows, userRows, groupRows] = await Promise.all([
  26. db.select().from(roles).orderBy(roles.name),
  27. db.select().from(permissions).orderBy(permissions.resourceType, permissions.action),
  28. db.select().from(rolePermissions),
  29. db.select().from(resources).orderBy(resources.path),
  30. db.select().from(aclRules).orderBy(aclRules.createdAt),
  31. db.select().from(users).orderBy(users.email),
  32. db.select().from(groups).orderBy(groups.name),
  33. ]);
  34. const permissionById = new Map(permissionRows.map((permission) => [permission.id, permission]));
  35. const resourceById = new Map(resourceRows.map((resource) => [resource.id, resource]));
  36. const userById = new Map(userRows.map((user) => [user.id, user.email]));
  37. const groupById = new Map(groupRows.map((group) => [group.id, group.name]));
  38. const actions = (
  39. <div className="flex flex-wrap gap-2">
  40. <Dialog title="创建权限" trigger="创建权限">
  41. <form className="grid gap-4" action={createPermissionAction}>
  42. <Label>动作<Input name="action" placeholder="read" required /></Label>
  43. <Label>资源类型<Input name="resourceType" placeholder="video" required /></Label>
  44. <Button type="submit">创建</Button>
  45. </form>
  46. </Dialog>
  47. <Dialog title="角色授权" trigger="角色授权">
  48. <form className="grid gap-4" action={assignPermissionToRoleAction}>
  49. <Label>角色<Select name="roleId">{roleRows.map((role) => <option key={role.id} value={role.id}>{role.name}</option>)}</Select></Label>
  50. <Label>权限<Select name="permissionId">{permissionRows.map((permission) => <option key={permission.id} value={permission.id}>{permission.resourceType}:{permission.action}</option>)}</Select></Label>
  51. <Button type="submit">授权</Button>
  52. </form>
  53. </Dialog>
  54. <Dialog title="登记资源" trigger="登记资源">
  55. <form className="grid gap-4" action={createResourceAction}>
  56. <Label>名称<Input name="name" required /></Label>
  57. <Label>路径<Input name="path" placeholder="/media/example.mp4" required /></Label>
  58. <Label>类型<Input name="type" placeholder="file" required /></Label>
  59. <Button type="submit">登记</Button>
  60. </form>
  61. </Dialog>
  62. <Dialog title="添加 ACL" trigger="添加 ACL">
  63. <form className="grid gap-4" action={createAclRuleAction}>
  64. <Label>资源<Select name="resourceId">{resourceRows.map((resource) => <option key={resource.id} value={resource.id}>{resource.path}</option>)}</Select></Label>
  65. <Label>主体类型<Select name="subjectType"><option value="user">user</option><option value="group">group</option></Select></Label>
  66. <Label>主体<Select name="subjectId">{userRows.map((user) => <option key={user.id} value={user.id}>user:{user.email}</option>)}{groupRows.map((group) => <option key={group.id} value={group.id}>group:{group.name}</option>)}</Select></Label>
  67. <Label>权限类型<Select name="permissionType"><option value="allow">allow</option><option value="deny">deny</option></Select></Label>
  68. <Label>动作<Input name="action" placeholder="read" required /></Label>
  69. <Button type="submit">添加</Button>
  70. </form>
  71. </Dialog>
  72. </div>
  73. );
  74. const rolePermissionsContent = (
  75. <Card>
  76. <CardHeader><CardTitle>角色权限</CardTitle></CardHeader>
  77. <CardContent>
  78. <Table>
  79. <TableHeader>
  80. <TableRow><TableHead>角色</TableHead><TableHead>权限</TableHead></TableRow>
  81. </TableHeader>
  82. <TableBody>
  83. {roleRows.map((role) => (
  84. <TableRow key={role.id}>
  85. <TableCell className="font-medium">{role.name}</TableCell>
  86. <TableCell>
  87. <div className="flex flex-wrap gap-2">
  88. {rolePermissionRows.filter((row) => row.roleId === role.id).map((row) => {
  89. const permission = row.permissionId ? permissionById.get(row.permissionId) : null;
  90. if (!permission) return null;
  91. return (
  92. <form action={removePermissionFromRoleAction} key={permission.id}>
  93. <input type="hidden" name="roleId" value={role.id} />
  94. <input type="hidden" name="permissionId" value={permission.id} />
  95. <button className="rounded-full border px-3 py-1 text-xs" type="submit">{permission.resourceType}:{permission.action} x</button>
  96. </form>
  97. );
  98. })}
  99. </div>
  100. </TableCell>
  101. </TableRow>
  102. ))}
  103. </TableBody>
  104. </Table>
  105. </CardContent>
  106. </Card>
  107. );
  108. const aclContent = (
  109. <Card>
  110. <CardHeader><CardTitle>ACL 规则</CardTitle></CardHeader>
  111. <CardContent>
  112. <Table>
  113. <TableHeader>
  114. <TableRow><TableHead>资源</TableHead><TableHead>主体</TableHead><TableHead>规则</TableHead><TableHead>操作</TableHead></TableRow>
  115. </TableHeader>
  116. <TableBody>
  117. {aclRows.map((rule) => (
  118. <TableRow key={rule.id}>
  119. <TableCell className="[overflow-wrap:anywhere]">{resourceById.get(rule.resourceId)?.path || rule.resourceId}</TableCell>
  120. <TableCell className="[overflow-wrap:anywhere]">{rule.subjectType}:{rule.subjectType === 'user' ? userById.get(rule.subjectId) : groupById.get(rule.subjectId)}</TableCell>
  121. <TableCell><Badge>{rule.permissionType}:{rule.action}</Badge></TableCell>
  122. <TableCell>
  123. <form action={deleteAclRuleAction}>
  124. <input type="hidden" name="aclRuleId" value={rule.id} />
  125. <Button variant="outline" size="sm" type="submit">删除</Button>
  126. </form>
  127. </TableCell>
  128. </TableRow>
  129. ))}
  130. </TableBody>
  131. </Table>
  132. </CardContent>
  133. </Card>
  134. );
  135. return (
  136. <main className="min-h-screen p-8 max-md:p-5">
  137. <section className="mx-auto max-w-6xl">
  138. <div className="mb-6 flex items-end justify-between gap-5 max-md:items-start">
  139. <div>
  140. <p className="mb-2 text-xs uppercase text-[hsl(var(--muted-foreground))]">EKB Admin</p>
  141. <h1 className="text-4xl font-semibold max-md:text-3xl">权限管理</h1>
  142. </div>
  143. {actions}
  144. </div>
  145. <Tabs
  146. defaultValue="roles"
  147. items={[
  148. { value: 'roles', label: '角色权限', content: rolePermissionsContent },
  149. { value: 'acl', label: 'ACL 规则', content: aclContent },
  150. ]}
  151. />
  152. </section>
  153. </main>
  154. );
  155. }