admin dashbaord; started member managment ui
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
export type AppConfig = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const appConfig: AppConfig = {
|
||||
name: "Wiki Antifa",
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { hasPermission, Role } from "@/lib/validation/has-permission";
|
||||
import { hasPermission, Role } from "@/lib/validation/permissions";
|
||||
import { auth } from "@/server/auth";
|
||||
import { api } from "@/trpc/server";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { hasPermission, Role } from "@/lib/validation/has-permission";
|
||||
import { hasPermission, Role } from "@/lib/validation/permissions";
|
||||
import { auth } from "@/server/auth";
|
||||
import { api } from "@/trpc/server";
|
||||
import { Edit, Edit2Icon, Edit3, MoreVertical, Trash } from "lucide-react";
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { auth } from "@/server/auth";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
async function AdminNavbar() {
|
||||
const session = await auth();
|
||||
return (
|
||||
<div className="flex h-14 w-full items-center justify-between gap-4 border-b bg-sidebar px-4">
|
||||
<span>Admin Dashboard</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AdminNavbar;
|
||||
@@ -0,0 +1,76 @@
|
||||
import * as React from "react";
|
||||
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
} from "@/components/ui/sidebar";
|
||||
import Link from "next/link";
|
||||
import SidebarLink from "@/components/layout/sidebar-link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { LogOutIcon } from "lucide-react";
|
||||
import { appConfig } from "@/app.config";
|
||||
|
||||
const data = {
|
||||
navMain: [
|
||||
{
|
||||
title: "Benutzer",
|
||||
url: "/admin",
|
||||
items: [
|
||||
{
|
||||
title: "Team Mitglieder",
|
||||
url: "/admin/team",
|
||||
},
|
||||
{
|
||||
title: "Alle Benutzer",
|
||||
url: "/admin/benutzer",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export function AdminSidebar({
|
||||
...props
|
||||
}: React.ComponentProps<typeof Sidebar>) {
|
||||
return (
|
||||
<Sidebar {...props}>
|
||||
<SidebarHeader className="flex h-14 items-center justify-center border-b">
|
||||
<Link href={"/admin"}>
|
||||
<span className="text-2xl font-bold">{appConfig.name}</span>
|
||||
</Link>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
{/* We create a SidebarGroup for each parent. */}
|
||||
{data.navMain.map((item) => (
|
||||
<SidebarGroup key={item.title}>
|
||||
<SidebarGroupLabel>{item.title}</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{item.items.map((item) => (
|
||||
<SidebarLink key={item.title} {...item} />
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
))}
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<Button
|
||||
asChild
|
||||
variant={"destructive"}
|
||||
className="border-2 bg-sidebar text-foreground hover:text-destructive-foreground"
|
||||
>
|
||||
<Link href={"/"}>
|
||||
<LogOutIcon className="size-4 rotate-180" /> Verlassen
|
||||
</Link>
|
||||
</Button>
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { hasPermission, Role } from "@/lib/validation/permissions";
|
||||
import { auth } from "@/server/auth";
|
||||
import { notFound } from "next/navigation";
|
||||
import React from "react";
|
||||
import AdminNavbar from "./_components/admin-navbar";
|
||||
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||
import { AdminSidebar } from "./_components/admin-sidebar";
|
||||
|
||||
async function Layout({ children }: { children: React.ReactNode }) {
|
||||
const session = await auth();
|
||||
const isAdmin = session?.user
|
||||
? hasPermission(session.user.role, Role.ADMIN)
|
||||
: false;
|
||||
if (!isAdmin) return notFound();
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<AdminSidebar />
|
||||
<div className="w-full">
|
||||
{/* <AdminNavbar /> */}
|
||||
<main className="space-y-4 p-4"> {children}</main>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default Layout;
|
||||
@@ -0,0 +1,8 @@
|
||||
import { api } from "@/trpc/server";
|
||||
import React from "react";
|
||||
|
||||
async function Page() {
|
||||
return <div>Admin Dashboard</div>;
|
||||
}
|
||||
|
||||
export default Page;
|
||||
@@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { getUserPermissions } from "@/lib/validation/permissions";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Plus } from "lucide-react";
|
||||
import { User } from "next-auth";
|
||||
|
||||
export const columns: ColumnDef<User>[] = [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Name",
|
||||
},
|
||||
{
|
||||
accessorKey: "email",
|
||||
header: "Email",
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: "role",
|
||||
header: "Rollen",
|
||||
cell: ({ row }) => {
|
||||
const roles = getUserPermissions(Number(row.getValue("role")));
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{roles
|
||||
.filter(({ name }) => name !== "User")
|
||||
.map((role) => (
|
||||
<Badge
|
||||
key={role.name}
|
||||
variant={"outline"}
|
||||
className={`${Object.values(role.color).join(" ")}`}
|
||||
>
|
||||
{role.name}
|
||||
</Badge>
|
||||
))}
|
||||
<Button
|
||||
size={"icon"}
|
||||
variant={"outline"}
|
||||
className="size-max gap-1 bg-muted p-px text-muted-foreground"
|
||||
>
|
||||
<Plus className="size-3" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,22 @@
|
||||
import { api } from "@/trpc/server";
|
||||
import React from "react";
|
||||
import { DataTable } from "@/components/data-table";
|
||||
import { columns } from "./columns";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
async function Page() {
|
||||
const users = await api.users.getAll();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">Team Mitglieder</h1>
|
||||
<Button variant={"outline"}>Mitglied einladen</Button>
|
||||
</div>
|
||||
|
||||
<DataTable columns={columns} data={users} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
@@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
}
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import SidebarLink from "./sidebar-link";
|
||||
import { Button } from "../ui/button";
|
||||
import { HomeIcon } from "lucide-react";
|
||||
import { api } from "@/trpc/server";
|
||||
import { appConfig } from "@/app.config";
|
||||
|
||||
export async function AppSidebar({
|
||||
...props
|
||||
@@ -25,7 +26,7 @@ export async function AppSidebar({
|
||||
<Sidebar {...props}>
|
||||
<SidebarHeader className="flex h-14 items-center justify-center border-b">
|
||||
<Link href={"/"}>
|
||||
<span className="text-2xl font-bold">Wiki</span>
|
||||
<span className="text-2xl font-bold">{appConfig.name}</span>
|
||||
</Link>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { auth } from "@/server/auth";
|
||||
import { Button } from "../ui/button";
|
||||
import Link from "next/link";
|
||||
import Avatar from "../avatar";
|
||||
import { hasPermission, Role } from "@/lib/validation/has-permission";
|
||||
import { hasPermission, Role } from "@/lib/validation/permissions";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
@@ -17,11 +17,19 @@ async function Navbar() {
|
||||
const isEditor = session?.user
|
||||
? hasPermission(session.user.role, Role.EDITOR)
|
||||
: false;
|
||||
const isAdmin = session?.user
|
||||
? hasPermission(session.user.role, Role.ADMIN)
|
||||
: false;
|
||||
return (
|
||||
<div className="flex h-14 items-center justify-between border-b bg-sidebar px-4">
|
||||
<Input className="w-full max-w-xs" placeholder="Suche..." />
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
{isAdmin && (
|
||||
<Button asChild>
|
||||
<Link href={"/admin"}>Admin Dashboard</Link>
|
||||
</Button>
|
||||
)}
|
||||
{isEditor && (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
@@ -0,0 +1,120 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
Table.displayName = "Table"
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
))
|
||||
TableHeader.displayName = "TableHeader"
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableBody.displayName = "TableBody"
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableFooter.displayName = "TableFooter"
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableRow.displayName = "TableRow"
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableHead.displayName = "TableHead"
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCell.displayName = "TableCell"
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCaption.displayName = "TableCaption"
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
/* Bitmap Based Roles
|
||||
* Admin and editor role === 6
|
||||
* User role === 1
|
||||
* Editor role === 2
|
||||
* Admin role === 4
|
||||
* Highest role is 7
|
||||
*/
|
||||
export enum Role {
|
||||
USER = 1, // 001
|
||||
EDITOR = 2, // 010
|
||||
ADMIN = 4, // 100
|
||||
}
|
||||
|
||||
export function hasPermission(userRole: number, requiredRole: Role): boolean {
|
||||
return (userRole & requiredRole) !== 0;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/* Bitmap Based Roles
|
||||
* User role === 1
|
||||
* Editor role === 2
|
||||
* Admin role === 4
|
||||
* Admin and editor role === 6
|
||||
* Highest role is 7
|
||||
*/
|
||||
export enum Role {
|
||||
USER = 1, // 001
|
||||
EDITOR = 2, // 010
|
||||
ADMIN = 4, // 100
|
||||
}
|
||||
|
||||
export function hasPermission(userRole: number, requiredRole: Role): boolean {
|
||||
return (userRole & requiredRole) !== 0;
|
||||
}
|
||||
|
||||
type RoleDefinition = {
|
||||
name: string;
|
||||
color: {
|
||||
bg: string;
|
||||
fg: string;
|
||||
border: string;
|
||||
};
|
||||
};
|
||||
|
||||
const roles: { [key: number]: RoleDefinition } = {
|
||||
[Role.USER]: {
|
||||
name: "User",
|
||||
color: {
|
||||
bg: "bg-muted",
|
||||
fg: "text-muted-foreground",
|
||||
border: "border-muted-foreground",
|
||||
},
|
||||
},
|
||||
[Role.EDITOR]: {
|
||||
name: "Editor",
|
||||
color: {
|
||||
bg: "bg-blue-300",
|
||||
fg: "text-blue-700",
|
||||
border: "border-blue-700",
|
||||
},
|
||||
},
|
||||
[Role.ADMIN]: {
|
||||
name: "Admin",
|
||||
color: {
|
||||
bg: "bg-destructive",
|
||||
fg: "text-destructive-foreground",
|
||||
border: "border-destructive",
|
||||
},
|
||||
},
|
||||
};
|
||||
export function getUserPermissions(userRole: number): RoleDefinition[] {
|
||||
return Object.keys(roles)
|
||||
.map(Number) // Convert keys to numbers
|
||||
.filter((role) => (userRole & role) !== 0) // Check if role is set
|
||||
.map((role) => roles[role]!); // Convert back to role name
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { articleRouter } from "./routers/article";
|
||||
import { categoryRouter } from "@/server/api/routers/category";
|
||||
import { createCallerFactory, createTRPCRouter } from "@/server/api/trpc";
|
||||
import { usersRouter } from "./routers/users";
|
||||
|
||||
/**
|
||||
* This is the primary router for your server.
|
||||
@@ -10,6 +11,7 @@ import { createCallerFactory, createTRPCRouter } from "@/server/api/trpc";
|
||||
export const appRouter = createTRPCRouter({
|
||||
article: articleRouter,
|
||||
category: categoryRouter,
|
||||
users: usersRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { articles } from "@/server/db/schema";
|
||||
import { articleSchema } from "@/lib/validation/zod/article";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { hasPermission, Role } from "@/lib/validation/has-permission";
|
||||
import { hasPermission, Role } from "@/lib/validation/permissions";
|
||||
import { generateSlug } from "@/lib/utils";
|
||||
|
||||
export const articleRouter = createTRPCRouter({
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import { categories } from "@/server/db/schema";
|
||||
|
||||
import { eq } from "drizzle-orm";
|
||||
import { hasPermission, Role } from "@/lib/validation/has-permission";
|
||||
import { hasPermission, Role } from "@/lib/validation/permissions";
|
||||
import { categorySchema } from "@/lib/validation/zod/category";
|
||||
import { generateSlug } from "@/lib/utils";
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { hasPermission, Role } from "@/lib/validation/permissions";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
|
||||
export const usersRouter = createTRPCRouter({
|
||||
getAll: protectedProcedure.query(async ({ ctx }) => {
|
||||
const isAdmin = hasPermission(ctx.session.user.role, Role.ADMIN);
|
||||
if (!isAdmin) throw new Error("You are not allowed to get all users");
|
||||
return await ctx.db.query.users.findMany();
|
||||
}),
|
||||
});
|
||||
Reference in New Issue
Block a user