admin dashbaord; started member managment ui

This commit is contained in:
2025-03-07 20:18:00 +01:00
parent 4a1397651f
commit 5c09688f1a
24 changed files with 1466 additions and 29 deletions
+7
View File
@@ -0,0 +1,7 @@
export type AppConfig = {
name: string;
};
export const appConfig: AppConfig = {
name: "Wiki Antifa",
};
+1 -1
View File
@@ -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 -1
View File
@@ -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>
);
}
+26
View File
@@ -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;
+8
View File
@@ -0,0 +1,8 @@
import { api } from "@/trpc/server";
import React from "react";
async function Page() {
return <div>Admin Dashboard</div>;
}
export default Page;
+50
View File
@@ -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>
);
},
},
];
+22
View File
@@ -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;
+80
View File
@@ -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>
);
}
+2 -1
View File
@@ -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>
+9 -1
View File
@@ -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>
+36
View File
@@ -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 }
+120
View File
@@ -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,
}
-16
View File
@@ -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;
}
+58
View File
@@ -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
}
+2
View File
@@ -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
+1 -1
View File
@@ -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({
+1 -1
View File
@@ -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";
+10
View File
@@ -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();
}),
});