commit cc3d046e71b532da3eaf5511481885f5fe88e8b0
parent 92fa6a2f317c7aecfb1aa8114b2a9f7c45cf64cf
Author: MTRNord <mtrnord1@gmail.com>
Date: Thu, 1 May 2025 17:03:52 +0200
Implement first APis and make pages SSR
Signed-off-by: MTRNord <mtrnord1@gmail.com>
Diffstat:
24 files changed, 1105 insertions(+), 850 deletions(-)
diff --git a/src/app/api/session/route.ts b/src/app/api/session/route.ts
@@ -3,24 +3,26 @@ import { cookies } from "next/headers";
import { NextResponse } from "next/server";
export async function POST(req: Request) {
- const { openidToken, homeserverUrl, matrixId } = await req.json();
+ const { openidToken, homeserverUrl, matrixId, openidExpiration } = await req.json();
if (!openidToken) {
return NextResponse.json({ error: "OpenID token is required" }, { status: 400 });
}
-
- const isValid = await validateOpenIDToken(openidToken);
- if (!isValid) {
- return NextResponse.json({ error: "Invalid OpenID token" }, { status: 401 });
+ if (!homeserverUrl) {
+ return NextResponse.json({ error: "Homeserver URL is required" }, { status: 400 });
+ }
+ if (!matrixId) {
+ return NextResponse.json({ error: "Matrix ID is required" }, { status: 400 });
+ }
+ if (!openidExpiration) {
+ return NextResponse.json({ error: "OpenID expiration is required" }, { status: 400 });
}
const user: User = {
- id: "user_" + Math.random().toString(36).substring(2, 9),
- displayName: matrixId,
matrixId: matrixId,
token: openidToken,
- isAdmin: false,
homeserverUrl: homeserverUrl,
+ openidExpiration: openidExpiration,
};
const response = NextResponse.json(user);
@@ -44,10 +46,4 @@ export async function DELETE(req: Request) {
response.cookies.set("session", "", { httpOnly: true, path: "/", maxAge: 0 });
return response;
-}
-
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-async function validateOpenIDToken(openidToken: string): Promise<boolean> {
- // TODO: Implement OpenID token validation logic with the backend
- return true;
}
\ No newline at end of file
diff --git a/src/app/dashboard/analytics/loading.tsx b/src/app/dashboard/analytics/loading.tsx
@@ -1,143 +1,160 @@
import { Skeleton } from "@/components/ui/skeleton"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
+import Link from "next/link"
+import { Shield } from "lucide-react"
export default function Loading() {
return (
- <div>
- <div className="flex items-center justify-between mb-8">
- <Skeleton className="h-9 w-64" />
- <div className="hidden md:block">
- <Skeleton className="h-10 w-[400px]" />
+ <div className="flex min-h-screen flex-col bg-black text-white">
+ <header className="sticky top-0 z-10 border-b border-gray-800 bg-black/80 backdrop-blur-sm">
+ <div className="container flex h-16 items-center justify-between px-4 md:px-6">
+ <div className="flex items-center gap-4">
+ <Link href="/" className="flex items-center gap-2">
+ <Shield className="h-6 w-6 text-purple-400" />
+ <span className="text-xl font-bold tracking-tighter">Draupnir4All</span>
+ </Link>
+ </div>
</div>
- </div>
+ </header>
+ <main className="flex-1 container px-4 py-8 md:px-6 md:py-12">
+ <div>
+ <div className="flex items-center justify-between mb-8">
+ <Skeleton className="h-9 w-64" />
+ <div className="hidden md:block">
+ <Skeleton className="h-10 w-[400px]" />
+ </div>
+ </div>
+
+ <div className="space-y-4">
+ {/* Stats Cards */}
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
+ {Array(4)
+ .fill(0)
+ .map((_, i) => (
+ <Card key={i} className="border-gray-800 bg-gray-950">
+ <CardHeader className="pb-2">
+ <Skeleton className="h-5 w-32" />
+ </CardHeader>
+ <CardContent>
+ <Skeleton className="h-8 w-16 mb-2" />
+ <Skeleton className="h-4 w-32" />
+ </CardContent>
+ </Card>
+ ))}
+ </div>
- <div className="space-y-4">
- {/* Stats Cards */}
- <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
- {Array(4)
- .fill(0)
- .map((_, i) => (
- <Card key={i} className="border-gray-800 bg-gray-950">
- <CardHeader className="pb-2">
- <Skeleton className="h-5 w-32" />
+ {/* Heatmap Card */}
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader>
+ <div className="flex items-center justify-between">
+ <div>
+ <Skeleton className="h-6 w-40 mb-2" />
+ <Skeleton className="h-4 w-64" />
+ </div>
+ <Skeleton className="h-10 w-[240px]" />
+ </div>
+ </CardHeader>
+ <CardContent className="overflow-hidden">
+ <Skeleton className="h-[300px] w-full" />
+ </CardContent>
+ </Card>
+
+ {/* Two Column Charts */}
+ <div className="grid gap-4 md:grid-cols-2">
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader>
+ <Skeleton className="h-6 w-40 mb-2" />
+ <Skeleton className="h-4 w-56" />
</CardHeader>
<CardContent>
- <Skeleton className="h-8 w-16 mb-2" />
- <Skeleton className="h-4 w-32" />
+ <Skeleton className="h-[280px] w-full" />
</CardContent>
</Card>
- ))}
- </div>
- {/* Heatmap Card */}
- <Card className="border-gray-800 bg-gray-950">
- <CardHeader>
- <div className="flex items-center justify-between">
- <div>
- <Skeleton className="h-6 w-40 mb-2" />
- <Skeleton className="h-4 w-64" />
- </div>
- <Skeleton className="h-10 w-[240px]" />
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader>
+ <Skeleton className="h-6 w-40 mb-2" />
+ <Skeleton className="h-4 w-56" />
+ </CardHeader>
+ <CardContent>
+ <Skeleton className="h-[280px] w-full" />
+ </CardContent>
+ </Card>
</div>
- </CardHeader>
- <CardContent className="overflow-hidden">
- <Skeleton className="h-[300px] w-full" />
- </CardContent>
- </Card>
- {/* Two Column Charts */}
- <div className="grid gap-4 md:grid-cols-2">
- <Card className="border-gray-800 bg-gray-950">
- <CardHeader>
- <Skeleton className="h-6 w-40 mb-2" />
- <Skeleton className="h-4 w-56" />
- </CardHeader>
- <CardContent>
- <Skeleton className="h-[280px] w-full" />
- </CardContent>
- </Card>
-
- <Card className="border-gray-800 bg-gray-950">
- <CardHeader>
- <Skeleton className="h-6 w-40 mb-2" />
- <Skeleton className="h-4 w-56" />
- </CardHeader>
- <CardContent>
- <Skeleton className="h-[280px] w-full" />
- </CardContent>
- </Card>
- </div>
-
- {/* Two Column Charts */}
- <div className="grid gap-4 md:grid-cols-2">
- <Card className="border-gray-800 bg-gray-950">
- <CardHeader>
- <Skeleton className="h-6 w-40 mb-2" />
- <Skeleton className="h-4 w-56" />
- </CardHeader>
- <CardContent>
- <Skeleton className="h-[280px] w-full" />
- </CardContent>
- </Card>
-
- <Card className="border-gray-800 bg-gray-950">
- <CardHeader>
- <Skeleton className="h-6 w-40 mb-2" />
- <Skeleton className="h-4 w-56" />
- </CardHeader>
- <CardContent>
- <Skeleton className="h-[220px] w-full mb-4" />
- <div className="flex items-center justify-center">
- <Skeleton className="h-9 w-48" />
- </div>
- </CardContent>
- </Card>
- </div>
+ {/* Two Column Charts */}
+ <div className="grid gap-4 md:grid-cols-2">
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader>
+ <Skeleton className="h-6 w-40 mb-2" />
+ <Skeleton className="h-4 w-56" />
+ </CardHeader>
+ <CardContent>
+ <Skeleton className="h-[280px] w-full" />
+ </CardContent>
+ </Card>
- {/* Efficiency Card */}
- <Card className="border-gray-800 bg-gray-950">
- <CardHeader>
- <Skeleton className="h-6 w-48 mb-2" />
- <Skeleton className="h-4 w-64" />
- </CardHeader>
- <CardContent>
- <div className="grid gap-4 md:grid-cols-3">
- {Array(3)
- .fill(0)
- .map((_, i) => (
- <div key={i} className="space-y-2">
- <Skeleton className="h-5 w-40" />
- <div className="flex items-end gap-2">
- <Skeleton className="h-8 w-16" />
- <Skeleton className="h-4 w-32" />
- </div>
- <Skeleton className="h-4 w-48" />
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader>
+ <Skeleton className="h-6 w-40 mb-2" />
+ <Skeleton className="h-4 w-56" />
+ </CardHeader>
+ <CardContent>
+ <Skeleton className="h-[220px] w-full mb-4" />
+ <div className="flex items-center justify-center">
+ <Skeleton className="h-9 w-48" />
</div>
- ))}
+ </CardContent>
+ </Card>
</div>
- <div className="mt-6">
- <Skeleton className="h-5 w-48 mb-4" />
- <div className="space-y-4">
- {Array(3)
- .fill(0)
- .map((_, i) => (
- <div key={i} className="flex items-center gap-3">
- <Skeleton className="h-8 w-8 rounded-full" />
- <div className="flex-1">
- <div className="flex items-center justify-between mb-1">
- <Skeleton className="h-5 w-32" />
- <Skeleton className="h-4 w-16" />
+ {/* Efficiency Card */}
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader>
+ <Skeleton className="h-6 w-48 mb-2" />
+ <Skeleton className="h-4 w-64" />
+ </CardHeader>
+ <CardContent>
+ <div className="grid gap-4 md:grid-cols-3">
+ {Array(3)
+ .fill(0)
+ .map((_, i) => (
+ <div key={i} className="space-y-2">
+ <Skeleton className="h-5 w-40" />
+ <div className="flex items-end gap-2">
+ <Skeleton className="h-8 w-16" />
+ <Skeleton className="h-4 w-32" />
</div>
- <Skeleton className="h-1.5 w-full rounded-full" />
+ <Skeleton className="h-4 w-48" />
</div>
- </div>
- ))}
- </div>
- </div>
- </CardContent>
- </Card>
- </div>
+ ))}
+ </div>
+
+ <div className="mt-6">
+ <Skeleton className="h-5 w-48 mb-4" />
+ <div className="space-y-4">
+ {Array(3)
+ .fill(0)
+ .map((_, i) => (
+ <div key={i} className="flex items-center gap-3">
+ <Skeleton className="h-8 w-8 rounded-full" />
+ <div className="flex-1">
+ <div className="flex items-center justify-between mb-1">
+ <Skeleton className="h-5 w-32" />
+ <Skeleton className="h-4 w-16" />
+ </div>
+ <Skeleton className="h-1.5 w-full rounded-full" />
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+ </div>
+ </div>
+ </main>
</div>
+
)
}
diff --git a/src/app/dashboard/analytics/page.tsx b/src/app/dashboard/analytics/page.tsx
@@ -1,22 +1,18 @@
-"use client";
-
-import { useEffect, useState } from "react"
-import { Users } from "lucide-react"
-
+import { ChevronDown, Users } from "lucide-react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
-
import { Heatmap } from "@/components/analytics/heatmap"
-import { useSearchParams } from "next/navigation"
-import { mockTeams } from "../mockData"
-import TabNavigation from "../../../components/dashboard/tab-navigation";
import { BannedServerData, generateHeatmapData, getBannedServersConfig, getMonthlyActivityConfig, getReportTypesConfig, getRoomActivityConfig, MonthlyActivityData, ReportTypeData, RoomActivityData } from "@/components/analytics/chart-configs";
import { PlotlyChart } from "@/components/analytics/plotly-chart";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import InfoCardWithTrend from "@/components/analytics/info-card-with-trend";
import LayoutWrapper from "@/components/dashboard/layoutWrapper";
+import { cookies } from "next/headers"
+import { User } from "@/lib/auth"
+import { listBots } from "@/lib/api"
+import TabNavigation from "@/components/dashboard/tab-navigation";
+import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
// Mock data for charts
const roomActivityData: RoomActivityData[] = [
@@ -60,22 +56,27 @@ const monthlyActivityData: MonthlyActivityData[] = [
]
-export default function AnalyticsDashboard() {
- const searchParams = useSearchParams()
- const teamIdParam = searchParams.get("team") || undefined
- const [selectedTeam, setSelectedTeam] = useState(mockTeams.find((t) => t.id === teamIdParam) || mockTeams[0])
-
+export default async function AnalyticsDashboard({
+ searchParams,
+}: {
+ searchParams: Promise<{ [key: string]: string | string[] | undefined }>
+}) {
+ const params = await searchParams;
+ const teamIdParam = params.team as string | undefined;
+ const timeRange = (params.timeRange as string | undefined) ?? "year"
+ const heatmapType = (params.heatmapType as string | undefined) ?? "reports"
+ const cookieStore = await cookies()
+ const session: User = JSON.parse(cookieStore.get("session")?.value || "{}")
+ if (!session.token) {
+ return <div className="flex h-screen w-full items-center justify-center">Loading...</div>
+ }
+ const listData = await listBots(session.matrixId, session.token);
- // Update selected team when URL param changes
- useEffect(() => {
- const team = mockTeams.find((t) => t.id === teamIdParam)
- if (team) {
- setSelectedTeam(team)
- }
- }, [teamIdParam])
+ if (!listData) {
+ return <div className="flex h-screen w-full items-center justify-center">Loading...</div>
+ }
- const [timeRange, setTimeRange] = useState("year")
- const [heatmapType, setHeatmapType] = useState("reports")
+ const selectedBot = listData.bots.find((team) => team.id === teamIdParam) || listData.bots[0];
// Generate mock data
const reportsHeatmapData = generateHeatmapData(1.5, 42)
@@ -91,21 +92,52 @@ export default function AnalyticsDashboard() {
const reportChangePercent = 12
return (
- <LayoutWrapper activeTab="analytics" teamIdParam={teamIdParam}>
- <TabNavigation selectedTeam={selectedTeam} currentTab="analytics" />
+ <LayoutWrapper listData={listData} activeTab="analytics" teamIdParam={teamIdParam}>
+ <TabNavigation selectedBot={selectedBot} currentTab="analytics" />
<div className="space-y-4">
<div className="flex items-center justify-end">
- <Select value={timeRange} onValueChange={setTimeRange}>
- <SelectTrigger className="w-[120px] h-8 bg-gray-900 border-gray-800">
- <SelectValue placeholder="Time Range" />
- </SelectTrigger>
- <SelectContent className="bg-gray-900 border-gray-800">
- <SelectItem value="month">Past Month</SelectItem>
- <SelectItem value="quarter">Past Quarter</SelectItem>
- <SelectItem value="year">Past Year</SelectItem>
- <SelectItem value="all">All Time</SelectItem>
- </SelectContent>
- </Select>
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button variant="outline" className="h-8 w-[120px] bg-gray-900 border border-gray-800 text-gray-400 rounded-md flex items-center justify-between">
+ <span>{timeRange.charAt(0).toUpperCase() + timeRange.slice(1)}</span>
+ <ChevronDown className="ml-2 h-4 w-4" />
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent className="flex flex-col gap-1 w-56 bg-gray-900 border-gray-800">
+ <DropdownMenuItem asChild>
+ <Link
+ href={`/dashboard/analytics?team=${teamIdParam}&timeRange=month&heatmapType=${heatmapType}`}
+ className={timeRange === "month" ? "bg-gray-800 text-white" : "text-gray-400 hover:bg-gray-800"}
+ >
+ Past Month
+ </Link>
+ </DropdownMenuItem>
+ <DropdownMenuItem asChild>
+ <Link
+ href={`/dashboard/analytics?team=${teamIdParam}&timeRange=quarter&heatmapType=${heatmapType}`}
+ className={timeRange === "quarter" ? "bg-gray-800 text-white" : "text-gray-400 hover:bg-gray-800"}
+ >
+ Past Quarter
+ </Link>
+ </DropdownMenuItem>
+ <DropdownMenuItem asChild>
+ <Link
+ href={`/dashboard/analytics?team=${teamIdParam}&timeRange=year&heatmapType=${heatmapType}`}
+ className={timeRange === "year" ? "bg-gray-800 text-white" : "text-gray-400 hover:bg-gray-800"}
+ >
+ Past Year
+ </Link>
+ </DropdownMenuItem>
+ <DropdownMenuItem asChild>
+ <Link
+ href={`/dashboard/analytics?team=${teamIdParam}&timeRange=all&heatmapType=${heatmapType}`}
+ className={timeRange === "all" ? "bg-gray-800 text-white" : "text-gray-400 hover:bg-gray-800"}
+ >
+ All Time
+ </Link>
+ </DropdownMenuItem>
+ </DropdownMenuContent>
+ </DropdownMenu>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
@@ -124,13 +156,19 @@ export default function AnalyticsDashboard() {
Visualization of moderation activity over time
</CardDescription>
</div>
- <Tabs value={heatmapType} onValueChange={setHeatmapType}>
+ <Tabs value={heatmapType}>
<TabsList className="bg-gray-900">
- <TabsTrigger value="reports" className="text-xs">
- Reports
+ <TabsTrigger value="reports" className="text-xs" asChild>
+ <Link
+ href={`/dashboard/analytics?team=${teamIdParam}&timeRange=${timeRange}&heatmapType=reports`}>
+ Reports
+ </Link>
</TabsTrigger>
- <TabsTrigger value="bans" className="text-xs">
- Bans
+ <TabsTrigger value="bans" className="text-xs" asChild>
+ <Link
+ href={`/dashboard/analytics?team=${teamIdParam}&timeRange=${timeRange}&heatmapType=bans`}>
+ Bans
+ </Link>
</TabsTrigger>
</TabsList>
</Tabs>
@@ -187,7 +225,7 @@ export default function AnalyticsDashboard() {
<PlotlyChart data={bannedServersConfig.data} layout={bannedServersConfig.layout} />
<div className="mt-4 flex items-center justify-center">
<Button variant="outline" size="sm" className="border-gray-800 text-gray-400 hover:text-white" asChild>
- <Link href={`/dashboard/bans?team=${selectedTeam.id}`}>
+ <Link href={`/dashboard/bans?team=${selectedBot.id}&heatmapType=${heatmapType}&timeRange=${timeRange}`}>
<Users className="mr-2 h-4 w-4" />
View All Banned Servers
</Link>
diff --git a/src/app/dashboard/bans/loading.tsx b/src/app/dashboard/bans/loading.tsx
@@ -1,60 +1,77 @@
import { Skeleton } from "@/components/ui/skeleton"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
+import Link from "next/link"
+import { Shield } from "lucide-react"
export default function Loading() {
return (
- <div>
- {/* Tab Navigation Skeleton */}
- <div className="flex items-center justify-between mb-8">
- <Skeleton className="h-9 w-64" />
- <div className="hidden md:block">
- <Skeleton className="h-10 w-[400px]" />
- </div>
- </div>
-
- <Card className="border-gray-800 bg-gray-950">
- <CardHeader>
- <div className="flex items-center justify-between">
- <Skeleton className="h-6 w-32 mb-1" />
- <Skeleton className="h-9 w-24" /> {/* Add Ban button */}
+ <div className="flex min-h-screen flex-col bg-black text-white">
+ <header className="sticky top-0 z-10 border-b border-gray-800 bg-black/80 backdrop-blur-sm">
+ <div className="container flex h-16 items-center justify-between px-4 md:px-6">
+ <div className="flex items-center gap-4">
+ <Link href="/" className="flex items-center gap-2">
+ <Shield className="h-6 w-6 text-purple-400" />
+ <span className="text-xl font-bold tracking-tighter">Draupnir4All</span>
+ </Link>
</div>
- <Skeleton className="h-4 w-64" />
- </CardHeader>
- <CardContent>
- <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
- {/* Policy Lists Column */}
- <div className="space-y-4">
- <Skeleton className="h-5 w-32" />
- <div className="space-y-2">
- {Array(3)
- .fill(0)
- .map((_, i) => (
- <div key={i} className="p-3 rounded-md border border-gray-800 space-y-2">
- <div className="flex items-center justify-between mb-1">
- <Skeleton className="h-5 w-32" />
- <Skeleton className="h-4 w-4" /> {/* Edit icon */}
- </div>
- <Skeleton className="h-4 w-full" />
- <Skeleton className="h-4 w-16" />
- </div>
- ))}
- </div>
+ </div>
+ </header>
+ <main className="flex-1 container px-4 py-8 md:px-6 md:py-12">
+ <div>
+ {/* Tab Navigation Skeleton */}
+ <div className="flex items-center justify-between mb-8">
+ <Skeleton className="h-9 w-64" />
+ <div className="hidden md:block">
+ <Skeleton className="h-10 w-[400px]" />
</div>
+ </div>
- {/* Ban Entries Columns (spans 2 columns) */}
- <div className="md:col-span-2 space-y-4">
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader>
<div className="flex items-center justify-between">
- <Skeleton className="h-5 w-32" />
- <div className="flex items-center gap-2">
- <Skeleton className="h-8 w-[200px]" /> {/* Search input */}
- <Skeleton className="h-8 w-[120px]" /> {/* Filter dropdown */}
+ <Skeleton className="h-6 w-32 mb-1" />
+ <Skeleton className="h-9 w-24" /> {/* Add Ban button */}
+ </div>
+ <Skeleton className="h-4 w-64" />
+ </CardHeader>
+ <CardContent>
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
+ {/* Policy Lists Column */}
+ <div className="space-y-4">
+ <Skeleton className="h-5 w-32" />
+ <div className="space-y-2">
+ {Array(3)
+ .fill(0)
+ .map((_, i) => (
+ <div key={i} className="p-3 rounded-md border border-gray-800 space-y-2">
+ <div className="flex items-center justify-between mb-1">
+ <Skeleton className="h-5 w-32" />
+ <Skeleton className="h-4 w-4" /> {/* Edit icon */}
+ </div>
+ <Skeleton className="h-4 w-full" />
+ <Skeleton className="h-4 w-16" />
+ </div>
+ ))}
+ </div>
+ </div>
+
+ {/* Ban Entries Columns (spans 2 columns) */}
+ <div className="md:col-span-2 space-y-4">
+ <div className="flex items-center justify-between">
+ <Skeleton className="h-5 w-32" />
+ <div className="flex items-center gap-2">
+ <Skeleton className="h-8 w-[200px]" /> {/* Search input */}
+ <Skeleton className="h-8 w-[120px]" /> {/* Filter dropdown */}
+ </div>
+ </div>
+ <Skeleton className="h-[400px] w-full rounded-md border border-gray-800" /> {/* Ban entries list */}
</div>
</div>
- <Skeleton className="h-[400px] w-full rounded-md border border-gray-800" /> {/* Ban entries list */}
- </div>
- </div>
- </CardContent>
- </Card>
+ </CardContent>
+ </Card>
+ </div>
+ </main>
</div>
+
)
}
diff --git a/src/app/dashboard/bans/page.tsx b/src/app/dashboard/bans/page.tsx
@@ -1,53 +1,42 @@
-"use client";
-import { ScrollArea } from "@/components/ui/scroll-area"
-import { Ban, PenOff, Pen } from "lucide-react";
-import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
-import { Input } from "@/components/ui/input";
-import { ChangeEvent, useEffect, useState } from "react";
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
-import { EBanTypes, mockPolicyLists, mockTeams, } from "../mockData";
+import { mockPolicyLists, } from "../mockData";
import AddBan from "../../../components/modals/add-ban";
-import { useSearchParams } from "next/navigation";
import TabNavigation from "../../../components/dashboard/tab-navigation";
import LayoutWrapper from "@/components/dashboard/layoutWrapper";
+import { cookies } from "next/headers";
+import { User } from "@/lib/auth";
+import { listBots } from "@/lib/api";
+import BansList from "@/components/dashboard/bans-list";
-export default function Bans() {
- const searchParams = useSearchParams()
- const teamIdParam = searchParams.get("team") || undefined
- const [selectedTeam, setSelectedTeam] = useState(mockTeams.find((t) => t.id === teamIdParam) || mockTeams[0])
- const [searchTerm, setSearchTerm] = useState<string>("");
+export default async function Bans({
+ searchParams,
+}: {
+ searchParams: Promise<{ [key: string]: string | string[] | undefined }>
+}) {
+ const params = await searchParams;
+ const teamIdParam = params.team as string | undefined;
- // Update selected team when URL param changes
- useEffect(() => {
- const team = mockTeams.find((t) => t.id === teamIdParam)
- if (team) {
- setSelectedTeam(team)
- }
- }, [teamIdParam])
+ const cookieStore = await cookies()
+ const session: User = JSON.parse(cookieStore.get("session")?.value || "{}")
+ if (!session.token) {
+ return <div className="flex h-screen w-full items-center justify-center">Loading...</div>
+ }
+ const listData = await listBots(session.matrixId, session.token);
- // Filter policy lists based on selected team
- const policyLists = mockPolicyLists.filter((list) => list.teamId === selectedTeam.id)
+ if (!listData) {
+ return <div className="flex h-screen w-full items-center justify-center">Loading...</div>
+ }
+
+ const selectedBot = listData.bots.find((team) => team.id === teamIdParam) || listData.bots[0];
- const [selectedPolicyList, setSelectedPolicyList] = useState("global")
- const [filter, setFilter] = useState("all")
- const selectedList = policyLists.find((list) => list.id === selectedPolicyList) || policyLists[0] || { entries: [] }
- const entries = selectedList.entries.filter((entry) => {
- if (filter === "all") return true;
- if (filter === "users") return entry.type === EBanTypes.User;
- if (filter === "servers") return entry.type === EBanTypes.Server;
- if (filter === "rooms") return entry.type === EBanTypes.Room;
- return false;
- }).filter((entry) => {
- if (!searchTerm) return true;
- const searchLower = searchTerm.toLowerCase();
- return entry.target.toLowerCase().includes(searchLower) || entry.reason.toLowerCase().includes(searchLower);
- });
+ // Filter policy lists based on selected team
+ // TODO: Fetch from API
+ const policyLists = mockPolicyLists
return (
- <LayoutWrapper activeTab="bans" teamIdParam={teamIdParam}>
- <TabNavigation selectedTeam={selectedTeam} currentTab="bans" />
+ <LayoutWrapper listData={listData} activeTab="bans" teamIdParam={teamIdParam}>
+ <TabNavigation selectedBot={selectedBot} currentTab="bans" />
<Card className="border-gray-800 bg-gray-950">
<CardHeader className="pb-2">
@@ -58,103 +47,7 @@ export default function Bans() {
<CardDescription className="text-gray-400">Manage ban lists across your Matrix rooms</CardDescription>
</CardHeader>
<CardContent>
- <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
- <div className="space-y-4">
- <h3 className="text-sm font-medium text-gray-400">Policy Lists</h3>
- <div className="space-y-2">
- {policyLists.length > 0 ? (
- policyLists.map((list) => (
- <div
- key={list.id}
- className={`p-3 rounded-md border cursor-pointer transition-colors ${selectedPolicyList === list.id
- ? "bg-gray-800 border-purple-600"
- : "bg-gray-900 border-gray-800 hover:border-gray-700"
- }`}
- onClick={() => setSelectedPolicyList(list.id)}
- >
- <div className="flex items-center justify-between mb-1">
- <span className="font-medium">{list.name}</span>
- {list.readOnly ? (
- <PenOff className=" text-gray-400">Read Only</PenOff>
- ) : (
- <Pen className="text-green-300">Editable</Pen>
- )}
- </div>
- <p className="text-xs text-gray-400">{list.description}</p>
- <p className="text-xs text-gray-500 mt-1">{list.entries.length} entries</p>
- </div>
- ))
- ) : (
- <div className="flex flex-col items-center justify-center p-8 text-center">
- <Ban className="h-8 w-8 text-gray-500 mb-2" />
- <p className="text-gray-400">No policy lists available</p>
- </div>
- )}
- </div>
- </div>
-
- <div className="md:col-span-2 space-y-4">
- <div className="flex items-center justify-between">
- <h3 className="text-sm font-medium text-gray-400">
- {selectedList.name || "Policy List"} Entries
- </h3>
- <div className="flex items-center gap-2">
- <Input onChange={(e: ChangeEvent) => {
- const value = (e.target as HTMLInputElement).value;
- setSearchTerm(value)
- }} value={searchTerm} placeholder="Search entries..." className="h-8 w-[200px] bg-gray-900 border-gray-800" />
- <div className="flex items-center gap-2">
- <Select defaultValue="all" onValueChange={setFilter}>
- <SelectTrigger className="h-8 w-[120px] bg-gray-900 border-gray-800">
- <SelectValue placeholder="Filter" />
- </SelectTrigger>
- <SelectContent className="bg-gray-900 border-gray-800">
- <SelectItem value="all">All Bans</SelectItem>
- <SelectItem value="users">Users</SelectItem>
- <SelectItem value="servers">Servers</SelectItem>
- <SelectItem value="rooms">Rooms</SelectItem>
- </SelectContent>
- </Select>
- </div>
- </div>
- </div>
-
- <ScrollArea className="h-[400px] rounded-md border border-gray-800">
- <div className="p-4 space-y-3">
- {selectedList && entries && entries.length > 0 ? (
- entries.map((entry) => (
- <div key={entry.id} className="p-3 rounded-md bg-gray-900 border border-gray-800">
- <div className="flex items-start justify-between">
- <div>
- <p className="font-medium">{entry.target}</p>
- <p className="text-sm text-gray-400 mt-1">{entry.reason}</p>
- <p className="text-xs text-gray-500 mt-1">
- {new Date(entry.timestamp).toLocaleString()}
- </p>
- </div>
- {!selectedList.readOnly && (
- <Button
- variant="ghost"
- size="sm"
- className="h-8 w-8 p-0 text-gray-400 hover:text-red-400"
- >
- <Ban className="h-4 w-4" />
- <span className="sr-only">Remove</span>
- </Button>
- )}
- </div>
- </div>
- ))
- ) : (
- <div className="flex flex-col items-center justify-center h-full p-6 text-center">
- <Ban className="h-8 w-8 text-gray-500 mb-2" />
- <p className="text-gray-400">No ban entries found</p>
- </div>
- )}
- </div>
- </ScrollArea>
- </div>
- </div>
+ <BansList policyLists={policyLists} />
</CardContent>
</Card>
</LayoutWrapper>
diff --git a/src/app/dashboard/overview/loading.tsx b/src/app/dashboard/overview/loading.tsx
@@ -1,90 +1,107 @@
import { Skeleton } from "@/components/ui/skeleton"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
+import Link from "next/link"
+import { Shield } from "lucide-react"
export default function Loading() {
return (
- <div>
- <div className="flex items-center justify-between mb-8">
- <Skeleton className="h-9 w-64" />
- <div className="hidden md:block">
- <Skeleton className="h-10 w-[400px]" />
+ <div className="flex min-h-screen flex-col bg-black text-white">
+ <header className="sticky top-0 z-10 border-b border-gray-800 bg-black/80 backdrop-blur-sm">
+ <div className="container flex h-16 items-center justify-between px-4 md:px-6">
+ <div className="flex items-center gap-4">
+ <Link href="/" className="flex items-center gap-2">
+ <Shield className="h-6 w-6 text-purple-400" />
+ <span className="text-xl font-bold tracking-tighter">Draupnir4All</span>
+ </Link>
+ </div>
</div>
- </div>
+ </header>
- <div className="space-y-8">
- {/* Stats Cards */}
- <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
- {Array(4)
- .fill(0)
- .map((_, i) => (
- <Card key={i} className="border-gray-800 bg-gray-950">
- <CardHeader className="pb-2">
- <Skeleton className="h-5 w-32" />
+ <main className="flex-1 container px-4 py-8 md:px-6 md:py-12">
+ <div>
+ <div className="flex items-center justify-between mb-8">
+ <Skeleton className="h-9 w-64" />
+ <div className="hidden md:block">
+ <Skeleton className="h-10 w-[400px]" />
+ </div>
+ </div>
+
+ <div className="space-y-8">
+ {/* Stats Cards */}
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
+ {Array(4)
+ .fill(0)
+ .map((_, i) => (
+ <Card key={i} className="border-gray-800 bg-gray-950">
+ <CardHeader className="pb-2">
+ <Skeleton className="h-5 w-32" />
+ </CardHeader>
+ <CardContent>
+ <Skeleton className="h-8 w-16 mb-2" />
+ <div className="flex justify-between items-center">
+ <Skeleton className="h-4 w-32" />
+ <Skeleton className="h-6 w-24" />
+ </div>
+ </CardContent>
+ </Card>
+ ))}
+ </div>
+
+ {/* Two Column Cards */}
+ <div className="grid gap-4 md:grid-cols-2">
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader>
+ <Skeleton className="h-6 w-40 mb-2" />
+ <Skeleton className="h-4 w-56" />
</CardHeader>
<CardContent>
- <Skeleton className="h-8 w-16 mb-2" />
- <div className="flex justify-between items-center">
- <Skeleton className="h-4 w-32" />
- <Skeleton className="h-6 w-24" />
+ <div className="space-y-4">
+ {Array(3)
+ .fill(0)
+ .map((_, i) => (
+ <div key={i} className="flex items-start gap-4">
+ <Skeleton className="h-8 w-8 rounded-full" />
+ <div className="space-y-1 flex-1">
+ <Skeleton className="h-5 w-32" />
+ <Skeleton className="h-4 w-full" />
+ <Skeleton className="h-4 w-24" />
+ </div>
+ </div>
+ ))}
</div>
</CardContent>
</Card>
- ))}
- </div>
-
- {/* Two Column Cards */}
- <div className="grid gap-4 md:grid-cols-2">
- <Card className="border-gray-800 bg-gray-950">
- <CardHeader>
- <Skeleton className="h-6 w-40 mb-2" />
- <Skeleton className="h-4 w-56" />
- </CardHeader>
- <CardContent>
- <div className="space-y-4">
- {Array(3)
- .fill(0)
- .map((_, i) => (
- <div key={i} className="flex items-start gap-4">
- <Skeleton className="h-8 w-8 rounded-full" />
- <div className="space-y-1 flex-1">
- <Skeleton className="h-5 w-32" />
- <Skeleton className="h-4 w-full" />
- <Skeleton className="h-4 w-24" />
- </div>
- </div>
- ))}
- </div>
- </CardContent>
- </Card>
- <Card className="border-gray-800 bg-gray-950">
- <CardHeader>
- <div className="flex items-center justify-between">
- <div>
- <Skeleton className="h-6 w-40 mb-2" />
- <Skeleton className="h-4 w-56" />
- </div>
- <Skeleton className="h-9 w-24" />
- </div>
- </CardHeader>
- <CardContent>
- <div className="space-y-4">
- {Array(4)
- .fill(0)
- .map((_, i) => (
- <div key={i} className="flex items-center justify-between">
- <div className="flex items-center gap-2">
- <Skeleton className="h-2 w-2 rounded-full" />
- <Skeleton className="h-5 w-40" />
- </div>
- <Skeleton className="h-7 w-7 rounded-md" />
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader>
+ <div className="flex items-center justify-between">
+ <div>
+ <Skeleton className="h-6 w-40 mb-2" />
+ <Skeleton className="h-4 w-56" />
</div>
- ))}
- </div>
- </CardContent>
- </Card>
+ <Skeleton className="h-9 w-24" />
+ </div>
+ </CardHeader>
+ <CardContent>
+ <div className="space-y-4">
+ {Array(4)
+ .fill(0)
+ .map((_, i) => (
+ <div key={i} className="flex items-center justify-between">
+ <div className="flex items-center gap-2">
+ <Skeleton className="h-2 w-2 rounded-full" />
+ <Skeleton className="h-5 w-40" />
+ </div>
+ <Skeleton className="h-7 w-7 rounded-md" />
+ </div>
+ ))}
+ </div>
+ </CardContent>
+ </Card>
+ </div>
+ </div>
</div>
- </div>
+ </main>
</div>
)
}
diff --git a/src/app/dashboard/overview/page.tsx b/src/app/dashboard/overview/page.tsx
@@ -2,7 +2,7 @@ import { AlertTriangle, Ban, CheckCircle } from "lucide-react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import AddProtectedRoom from "../../../components/modals/add-protected-room"
import ProtectedRomsList from "../../../components/dashboard/protected-rooms-list"
-import { mockPolicyLists, mockReports, mockTeams } from "../mockData"
+import { mockPolicyLists, mockReports } from "../mockData"
import TabNavigation from "../../../components/dashboard/tab-navigation";
import LayoutWrapper from "@/components/dashboard/layoutWrapper"
import { listBots } from "@/lib/api"
@@ -20,15 +20,21 @@ export default async function OverviewPage({
if (!session.token) {
return <div className="flex h-screen w-full items-center justify-center">Loading...</div>
}
- const listData = await listBots(session.token);
+ const listData = await listBots(session.matrixId, session.token);
- const selectedTeam = mockTeams.find((t) => t.id === teamIdParam) || mockTeams[0];
- const reports = mockReports.filter((report) => report.teamId === selectedTeam.id);
- const policyLists = mockPolicyLists.filter((list) => list.teamId === selectedTeam.id);
+ if (!listData) {
+ return <div className="flex h-screen w-full items-center justify-center">Loading...</div>
+ }
+
+ const selectedBot = listData.bots.find((team) => team.id === teamIdParam) || listData.bots[0];
+ // TODO: get reports and policy lists from the API
+ const reports = mockReports;
+ // TODO: get policy list details from the API
+ const policyLists = mockPolicyLists;
return (
<LayoutWrapper listData={listData} activeTab="overview" teamIdParam={teamIdParam}>
- <TabNavigation selectedTeam={selectedTeam} currentTab="overview" />
+ <TabNavigation selectedBot={selectedBot} currentTab="overview" />
<div className="space-y-8">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card className="border-gray-800 bg-gray-950">
@@ -36,7 +42,7 @@ export default async function OverviewPage({
<CardTitle className="text-sm font-medium">Protected Rooms</CardTitle>
</CardHeader>
<CardContent>
- <div className="text-2xl font-bold">{selectedTeam.rooms.length}</div>
+ <div className="text-2xl font-bold">{selectedBot.protectedRooms.length}</div>
<div className="flex justify-between items-center">
<p className="text-xs text-gray-400">Monitored by this bot</p>
<AddProtectedRoom />
@@ -130,7 +136,7 @@ export default async function OverviewPage({
</div>
</CardHeader>
<CardContent>
- <ProtectedRomsList team={selectedTeam} />
+ <ProtectedRomsList selectedBot={selectedBot} />
</CardContent>
</Card>
</div>
diff --git a/src/app/dashboard/reports/loading.tsx b/src/app/dashboard/reports/loading.tsx
@@ -1,96 +1,113 @@
import { Skeleton } from "@/components/ui/skeleton"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
+import Link from "next/link"
+import { Shield } from "lucide-react"
export default function Loading() {
return (
- <div>
- <div className="flex items-center justify-between mb-8">
- <Skeleton className="h-9 w-64" />
- <div className="hidden md:block">
- <Skeleton className="h-10 w-[400px]" />
+ <div className="flex min-h-screen flex-col bg-black text-white">
+ <header className="sticky top-0 z-10 border-b border-gray-800 bg-black/80 backdrop-blur-sm">
+ <div className="container flex h-16 items-center justify-between px-4 md:px-6">
+ <div className="flex items-center gap-4">
+ <Link href="/" className="flex items-center gap-2">
+ <Shield className="h-6 w-6 text-purple-400" />
+ <span className="text-xl font-bold tracking-tighter">Draupnir4All</span>
+ </Link>
+ </div>
</div>
- </div>
-
- <Card className="border-gray-800 bg-gray-950">
- <CardHeader className="pb-2">
- <div className="flex items-center justify-between">
- <Skeleton className="h-6 w-40" />
- <Skeleton className="h-8 w-32" />
+ </header>
+ <main className="flex-1 container px-4 py-8 md:px-6 md:py-12">
+ <div>
+ <div className="flex items-center justify-between mb-8">
+ <Skeleton className="h-9 w-64" />
+ <div className="hidden md:block">
+ <Skeleton className="h-10 w-[400px]" />
+ </div>
</div>
- <Skeleton className="h-4 w-64 mt-1" />
- </CardHeader>
- <CardContent>
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
- <div className="space-y-4">
- <Skeleton className="h-5 w-32 mb-2" />
- {Array(3)
- .fill(0)
- .map((_, i) => (
- <div key={i} className="p-3 rounded-md border border-gray-800 space-y-2">
- <div className="flex items-center justify-between mb-2">
- <div className="flex items-center gap-2">
- <Skeleton className="h-4 w-4" />
- <Skeleton className="h-5 w-32" />
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader className="pb-2">
+ <div className="flex items-center justify-between">
+ <Skeleton className="h-6 w-40" />
+ <Skeleton className="h-8 w-32" />
+ </div>
+ <Skeleton className="h-4 w-64 mt-1" />
+ </CardHeader>
+ <CardContent>
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+ <div className="space-y-4">
+ <Skeleton className="h-5 w-32 mb-2" />
+
+ {Array(3)
+ .fill(0)
+ .map((_, i) => (
+ <div key={i} className="p-3 rounded-md border border-gray-800 space-y-2">
+ <div className="flex items-center justify-between mb-2">
+ <div className="flex items-center gap-2">
+ <Skeleton className="h-4 w-4" />
+ <Skeleton className="h-5 w-32" />
+ </div>
+ <Skeleton className="h-5 w-16" />
+ </div>
+ <Skeleton className="h-4 w-48" />
+ <Skeleton className="h-4 w-full" />
+ <Skeleton className="h-4 w-32" />
</div>
- <Skeleton className="h-5 w-16" />
- </div>
- <Skeleton className="h-4 w-48" />
- <Skeleton className="h-4 w-full" />
- <Skeleton className="h-4 w-32" />
- </div>
- ))}
- </div>
+ ))}
+ </div>
- <div className="space-y-4">
- <Skeleton className="h-5 w-32 mb-2" />
+ <div className="space-y-4">
+ <Skeleton className="h-5 w-32 mb-2" />
- <div className="space-y-4">
- <div className="p-4 rounded-md bg-gray-900 border border-gray-800">
- <div className="flex items-center justify-between mb-3">
- <Skeleton className="h-5 w-40" />
- <Skeleton className="h-5 w-24" />
- </div>
+ <div className="space-y-4">
+ <div className="p-4 rounded-md bg-gray-900 border border-gray-800">
+ <div className="flex items-center justify-between mb-3">
+ <Skeleton className="h-5 w-40" />
+ <Skeleton className="h-5 w-24" />
+ </div>
- <div className="space-y-2">
- {Array(4)
- .fill(0)
- .map((_, i) => (
- <div key={i} className="flex justify-between">
- <Skeleton className="h-4 w-24" />
- <Skeleton className="h-4 w-32" />
- </div>
- ))}
- </div>
- </div>
+ <div className="space-y-2">
+ {Array(4)
+ .fill(0)
+ .map((_, i) => (
+ <div key={i} className="flex justify-between">
+ <Skeleton className="h-4 w-24" />
+ <Skeleton className="h-4 w-32" />
+ </div>
+ ))}
+ </div>
+ </div>
- <div className="p-4 rounded-md bg-gray-900 border border-gray-800">
- <Skeleton className="h-5 w-40 mb-3" />
+ <div className="p-4 rounded-md bg-gray-900 border border-gray-800">
+ <Skeleton className="h-5 w-40 mb-3" />
- <div className="space-y-3">
- {Array(3)
- .fill(0)
- .map((_, i) => (
- <div key={i} className="flex justify-between">
- <Skeleton className="h-4 w-24" />
- <Skeleton className="h-4 w-32" />
- </div>
- ))}
+ <div className="space-y-3">
+ {Array(3)
+ .fill(0)
+ .map((_, i) => (
+ <div key={i} className="flex justify-between">
+ <Skeleton className="h-4 w-24" />
+ <Skeleton className="h-4 w-32" />
+ </div>
+ ))}
- <Skeleton className="h-[100px] w-full mt-4" />
- </div>
- </div>
+ <Skeleton className="h-[100px] w-full mt-4" />
+ </div>
+ </div>
- <div className="flex gap-2 mt-4">
- <Skeleton className="h-9 w-28" />
- <Skeleton className="h-9 w-28" />
- <Skeleton className="h-9 w-36" />
+ <div className="flex gap-2 mt-4">
+ <Skeleton className="h-9 w-28" />
+ <Skeleton className="h-9 w-28" />
+ <Skeleton className="h-9 w-36" />
+ </div>
+ </div>
</div>
</div>
- </div>
- </div>
- </CardContent>
- </Card>
+ </CardContent>
+ </Card>
+ </div>
+ </main>
</div>
+
)
}
diff --git a/src/app/dashboard/reports/page.tsx b/src/app/dashboard/reports/page.tsx
@@ -1,278 +1,107 @@
-"use client"
-
-import { useEffect, useState } from "react"
-import { AlertTriangle, CheckCircle, MessageSquare, UserPlus, Filter, Ban } from "lucide-react"
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
-import { Badge } from "@/components/ui/badge"
-import { Button } from "@/components/ui/button"
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
-import { BlurredText } from "@/components/blurred-text"
-import { BlurredImage } from "@/components/blurred-image"
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog"
-import { mockReports, mockTeams } from "../mockData";
-import { useSearchParams } from "next/navigation"
+import { mockReports } from "../mockData";
import TabNavigation from "../../../components/dashboard/tab-navigation"
import LayoutWrapper from "@/components/dashboard/layoutWrapper"
-
-export default function ReportsPage() {
- const searchParams = useSearchParams()
- const teamIdParam = searchParams.get("team") || undefined
- const [selectedTeam, setSelectedTeam] = useState(mockTeams.find((t) => t.id === teamIdParam) || mockTeams[0])
-
- // Update selected team when URL param changes
- useEffect(() => {
- const team = mockTeams.find((t) => t.id === teamIdParam)
- if (team) {
- setSelectedTeam(team)
- }
- }, [teamIdParam])
+import { cookies } from "next/headers"
+import { User } from "@/lib/auth"
+import { listBots } from "@/lib/api"
+import ReportList from "@/components/dashboard/report-list"
+import { redirect } from "next/navigation";
+import Link from "next/link";
+import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
+import { Button } from "@/components/ui/button";
+import { ChevronDown } from "lucide-react";
+
+export default async function ReportsPage({
+ searchParams,
+}: {
+ searchParams: Promise<{ [key: string]: string | string[] | undefined }>
+}) {
+ const params = await searchParams;
+ const teamIdParam = params.team as string | undefined;
+ const filter = (params.filter as "all" | "message" | "invite" | "high" | undefined) ?? "all"
+ if (!["all", "message", "invite", "high"].includes(filter)) {
+ redirect(`/dashboard/reports?team=${teamIdParam}&filter=all`);
+ }
+ const cookieStore = await cookies()
+ const session: User = JSON.parse(cookieStore.get("session")?.value || "{}")
+ if (!session.token) {
+ return <div className="flex h-screen w-full items-center justify-center">Loading...</div>
+ }
+ const listData = await listBots(session.matrixId, session.token);
+
+ if (!listData) {
+ return <div className="flex h-screen w-full items-center justify-center">Loading...</div>
+ }
+
+ const selectedBot = listData.bots.find((team) => team.id === teamIdParam) || listData.bots[0];
// Filter reports based on selected team
- const reports = mockReports.filter((report) => report.teamId === selectedTeam.id)
- const [selectedReport, setSelectedReport] = useState<string | null>(null)
- const [filter, setFilter] = useState("all")
-
- const filteredReports =
- filter === "all"
- ? reports
- : filter === "high"
- ? reports.filter((r) => r.priority === "high")
- : reports.filter((r) => r.type === filter)
+ // TODO: Fetch from API
+ const reports = mockReports
return (
- <LayoutWrapper activeTab="reports" teamIdParam={teamIdParam}>
- <TabNavigation selectedTeam={selectedTeam} currentTab="reports" />
+ <LayoutWrapper listData={listData} activeTab="reports" teamIdParam={teamIdParam}>
+ <TabNavigation selectedBot={selectedBot} currentTab="reports" />
<Card className="border-gray-800 bg-gray-950">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle>Active Reports</CardTitle>
<div className="flex items-center gap-2">
- <Select defaultValue="all" onValueChange={setFilter}>
- <SelectTrigger className="h-8 w-[120px] bg-gray-900 border-gray-800">
- <SelectValue placeholder="Filter" />
- </SelectTrigger>
- <SelectContent className="bg-gray-900 border-gray-800">
- <SelectItem value="all">All Reports</SelectItem>
- <SelectItem value="message">Messages</SelectItem>
- <SelectItem value="invite">Invites</SelectItem>
- <SelectItem value="high">High Priority</SelectItem>
- </SelectContent>
- </Select>
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button className="h-8 w-[120px] bg-gray-900 border border-gray-800 text-gray-400 rounded-md px-2 flex items-center justify-between cursor-pointer">
+ <span>{filter.charAt(0).toUpperCase() + filter.slice(1)}</span>
+ <ChevronDown className="ml-2 h-4 w-4" />
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent className="flex flex-col gap-1 bg-gray-900 border border-gray-800 rounded-md p-2 w-[150px]">
+ <DropdownMenuItem asChild>
+ <Link
+ href={`/dashboard/reports?team=${selectedBot.id}&filter=all`}
+ className={` block px-3 py-2 rounded-md text-sm ${filter === "all" ? "bg-gray-800 text-white" : "text-gray-400 hover:bg-gray-800"
+ }`}
+ >
+ All Reports
+ </Link>
+ </DropdownMenuItem>
+ <DropdownMenuItem asChild>
+ <Link
+ href={`/dashboard/reports?team=${selectedBot.id}&filter=message`}
+ className={`block px-3 py-2 rounded-md text-sm ${filter === "message" ? "bg-gray-800 text-white" : "text-gray-400 hover:bg-gray-800"
+ }`}
+ >
+ Messages
+ </Link>
+ </DropdownMenuItem>
+ <DropdownMenuItem asChild>
+ <Link
+ href={`/dashboard/reports?team=${selectedBot.id}&filter=invite`}
+ className={`block px-3 py-2 rounded-md text-sm ${filter === "invite" ? "bg-gray-800 text-white" : "text-gray-400 hover:bg-gray-800"
+ }`}
+ >
+ Invites
+ </Link>
+ </DropdownMenuItem>
+ <DropdownMenuItem asChild>
+ <Link
+ href={`/dashboard/reports?team=${selectedBot.id}&filter=high`}
+ className={`block px-3 py-2 rounded-md text-sm ${filter === "high" ? "bg-gray-800 text-white" : "text-gray-400 hover:bg-gray-800"
+ }`}
+ >
+ High Priority
+ </Link>
+ </DropdownMenuItem>
+ </DropdownMenuContent>
+ </DropdownMenu>
</div>
</div>
<CardDescription className="text-gray-400">Reports requiring moderation action</CardDescription>
</CardHeader>
<CardContent>
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
- <div className="space-y-4">
- <h3 className="text-sm font-medium text-gray-400">Report List</h3>
- {filteredReports.length > 0 ? (
- filteredReports.map((report) => (
- <div
- key={report.id}
- className={`p-3 rounded-md border cursor-pointer transition-colors ${selectedReport === report.id
- ? "bg-gray-800 border-purple-600"
- : "bg-gray-900 border-gray-800 hover:border-gray-700"
- }`}
- onClick={() => setSelectedReport(report.id)}
- >
- <div className="flex items-center justify-between mb-2">
- <div className="flex items-center gap-2">
- {report.type === "message" ? (
- <MessageSquare className="h-4 w-4 text-blue-400" />
- ) : (
- <UserPlus className="h-4 w-4 text-green-400" />
- )}
- <span className="text-sm font-medium">
- {report.type === "message" ? "Message Report" : "Invite Report"}
- </span>
- </div>
- <Badge
- className={
- report.priority === "high"
- ? "bg-red-900/50 text-red-300 hover:bg-red-900/70"
- : "bg-yellow-900/50 text-yellow-300 hover:bg-yellow-900/70"
- }
- >
- {report.priority}
- </Badge>
- </div>
- <p className="text-xs text-gray-400 mb-1">{report.room}</p>
- <p className="text-xs text-gray-400 mb-1">
- {report.type === "message"
- ? `From: ${report.subject.sender}`
- : `From: ${report.subject.sender} To: ${report.subject.target}`}
- </p>
- <p className="text-xs text-gray-500">{new Date(report.timestamp).toLocaleString()}</p>
- </div>
- ))
- ) : (
- <div className="flex flex-col items-center justify-center p-8 text-center">
- <CheckCircle className="h-8 w-8 text-gray-500 mb-2" />
- <p className="text-gray-400">No active reports</p>
- </div>
- )}
- </div>
-
- <div className="space-y-4">
- <h3 className="text-sm font-medium text-gray-400">Report Details</h3>
- {selectedReport ? (
- (() => {
- const report = reports.find((r) => r.id === selectedReport)
- if (!report) return <p>Select a report to view details</p>
-
- return (
- <div className="space-y-4">
- <div className="p-4 rounded-md bg-gray-900 border border-gray-800">
- <div className="flex items-center justify-between mb-3">
- <h4 className="font-medium">Report Information</h4>
- <Badge className="bg-purple-900/50 text-purple-300">{report.status}</Badge>
- </div>
-
- <div className="space-y-2 text-sm">
- <div className="flex justify-between">
- <span className="text-gray-400">Reported by:</span>
- <span>{report.reporter}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-gray-400">Room:</span>
- <span>{report.room}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-gray-400">Reason:</span>
- <span>{report.reason}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-gray-400">Time:</span>
- <span>{new Date(report.timestamp).toLocaleString()}</span>
- </div>
- </div>
- </div>
-
- <div className="p-4 rounded-md bg-gray-900 border border-gray-800">
- <h4 className="font-medium mb-3">
- {report.type === "message" ? "Message Content" : "Invite Details"}
- </h4>
-
- {report.type === "message" && (
- <div className="space-y-3">
- <div className="flex justify-between text-sm">
- <span className="text-gray-400">Sender:</span>
- <span>{report.subject.sender}</span>
- </div>
- <div className="flex justify-between text-sm">
- <span className="text-gray-400">Message ID:</span>
- <span className="font-mono text-xs">{report.subject.id}</span>
- </div>
- <div className="flex justify-between text-sm">
- <span className="text-gray-400">Time:</span>
- <span>{new Date(report.subject.timestamp).toLocaleString()}</span>
- </div>
-
- <div className="mt-4">
- <div className="flex items-center justify-between mb-2">
- <span className="text-sm text-gray-400">Content:</span>
- {report.subject.content?.msgtype === "m.text" && (
- <Button variant="ghost" size="sm" className="h-6 px-2 text-gray-400">
- <Filter className="h-3 w-3 mr-1" />
- Filter
- </Button>
- )}
- </div>
-
- {report.subject.content?.msgtype === "m.text" ? (
- <BlurredText text={report.subject.content?.body} />
- ) : report.subject.content?.msgtype === "m.image" ? (
- <div className="mt-2">
- <BlurredImage
- src="/placeholder.svg?height=300&width=400"
- alt={report.subject.content?.body}
- width={400}
- height={300}
- />
- <p className="text-xs text-gray-500 mt-1">Image: {report.subject.content.body}</p>
- </div>
- ) : (
- <p className="text-sm">Unsupported content type</p>
- )}
- </div>
- </div>
- )}
-
- {report.type === "invite" && (
- <div className="space-y-3">
- <div className="flex justify-between text-sm">
- <span className="text-gray-400">Sender:</span>
- <span>{report.subject.sender}</span>
- </div>
- <div className="flex justify-between text-sm">
- <span className="text-gray-400">Target:</span>
- <span>{report.subject.target}</span>
- </div>
- <div className="flex justify-between text-sm">
- <span className="text-gray-400">Time:</span>
- <span>{new Date(report.subject.timestamp).toLocaleString()}</span>
- </div>
- </div>
- )}
- </div>
-
- <div className="flex gap-2 mt-4">
- <Button className="bg-green-600 text-white hover:bg-green-700">
- <CheckCircle className="mr-2 h-4 w-4" />
- Resolve
- </Button>
- <Dialog>
- <DialogTrigger asChild>
- <Button
- variant="outline"
- className="border-red-500 text-red-400 hover:bg-red-950 hover:text-red-300"
- >
- <Ban className="mr-2 h-4 w-4" />
- Ban User
- </Button>
- </DialogTrigger>
- <DialogContent className="bg-gray-950 border-gray-800">
- <DialogHeader>
- <DialogTitle>Ban User</DialogTitle>
- <DialogDescription className="text-gray-400">
- Ban {report.subject.sender} from your Matrix rooms
- </DialogDescription>
- </DialogHeader>
- {/* Ban dialog content */}
- </DialogContent>
- </Dialog>
- {report.type === "message" && (
- <Button
- variant="outline"
- className="border-gray-700 text-gray-400 hover:bg-gray-900 hover:text-gray-300"
- >
- <MessageSquare className="mr-2 h-4 w-4" />
- Delete Message
- </Button>
- )}
- </div>
- </div>
- )
- })()
- ) : (
- <div className="flex flex-col items-center justify-center p-8 text-center">
- <AlertTriangle className="h-8 w-8 text-gray-500 mb-2" />
- <p className="text-gray-400">Select a report to view details</p>
- </div>
- )}
- </div>
- </div>
+ <ReportList reports={reports} filter={filter} />
</CardContent>
</Card>
- </LayoutWrapper>
+ </LayoutWrapper >
)
}
diff --git a/src/app/dashboard/settings/bot/page.tsx b/src/app/dashboard/settings/bot/page.tsx
@@ -19,7 +19,7 @@ export default async function BotSettingsPage({
if (!session.token) {
return <div className="flex h-screen w-full items-center justify-center">Loading...</div>
}
- const listData = await listBots(session.token);
+ const listData = await listBots(session.matrixId, session.token);
const selectedTeam = mockTeams.find((t) => t.id === teamIdParam) || mockTeams[0];
return (
diff --git a/src/app/dashboard/settings/members/page.tsx b/src/app/dashboard/settings/members/page.tsx
@@ -24,7 +24,7 @@ export default async function MembersSettingsPage({
if (!session.token) {
return <div className="flex h-screen w-full items-center justify-center">Loading...</div>
}
- const listData = await listBots(session.token);
+ const listData = await listBots(session.matrixId, session.token);
const selectedTeam = mockTeams.find((t) => t.id === teamIdParam) || mockTeams[0];
return (
diff --git a/src/app/dashboard/settings/protectedRooms/page.tsx b/src/app/dashboard/settings/protectedRooms/page.tsx
@@ -18,7 +18,7 @@ export default async function ProtectedRoomsSettingsPage({
if (!session.token) {
return <div className="flex h-screen w-full items-center justify-center">Loading...</div>
}
- const listData = await listBots(session.token);
+ const listData = await listBots(session.matrixId, session.token);
const selectedTeam = mockTeams.find((t) => t.id === teamIdParam) || mockTeams[0];
return (
diff --git a/src/app/refresh/page.tsx b/src/app/refresh/page.tsx
@@ -0,0 +1,19 @@
+"use client";
+
+import RedirectionPageClient from "@/components/redirect-workaround";
+import { useSession } from "@/contexts/session-context";
+import { useEffect } from "react";
+
+export default function RefreshPage() {
+ // This page is used to refresh the session and redirect to the dashboard
+ const { refreshOpenIDToken } = useSession();
+
+ useEffect(() => {
+ // Refresh the OpenID token
+ refreshOpenIDToken()
+ }, [refreshOpenIDToken]);
+
+ return (
+ <RedirectionPageClient redirectUrl="/dashboard" />
+ )
+}
+\ No newline at end of file
diff --git a/src/components/dashboard/bans-list.tsx b/src/components/dashboard/bans-list.tsx
@@ -0,0 +1,130 @@
+"use client"
+
+import { EBanTypes, PolicyList } from "@/app/dashboard/mockData"
+import { Ban, Pen, PenOff } from "lucide-react"
+import { ChangeEvent, useState } from "react"
+import { ScrollArea } from "../ui/scroll-area"
+import { Button } from "../ui/button"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
+import { Input } from "../ui/input"
+
+interface BanListProps {
+ policyLists: PolicyList[],
+}
+
+export default function BansList({ policyLists }: BanListProps) {
+ const [selectedPolicyList, setSelectedPolicyList] = useState("global")
+ const [filter, setFilter] = useState("all")
+ const [searchTerm, setSearchTerm] = useState("")
+ const selectedList = policyLists.find((list) => list.id === selectedPolicyList) || policyLists[0] || { entries: [] }
+ const entries = selectedList.entries.filter((entry) => {
+ if (filter === "all") return true;
+ if (filter === "users") return entry.type === EBanTypes.User;
+ if (filter === "servers") return entry.type === EBanTypes.Server;
+ if (filter === "rooms") return entry.type === EBanTypes.Room;
+ return false;
+ }).filter((entry) => {
+ if (!searchTerm) return true;
+ const searchLower = searchTerm.toLowerCase();
+ return entry.target.toLowerCase().includes(searchLower) || entry.reason.toLowerCase().includes(searchLower);
+ });
+
+ return (
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
+ <div className="space-y-4">
+ <h3 className="text-sm font-medium text-gray-400">Policy Lists</h3>
+ <div className="space-y-2">
+ {policyLists.length > 0 ? (
+ policyLists.map((list) => (
+ <div
+ key={list.id}
+ className={`p-3 rounded-md border cursor-pointer transition-colors ${selectedPolicyList === list.id
+ ? "bg-gray-800 border-purple-600"
+ : "bg-gray-900 border-gray-800 hover:border-gray-700"
+ }`}
+ onClick={() => setSelectedPolicyList(list.id)}
+ >
+ <div className="flex items-center justify-between mb-1">
+ <span className="font-medium">{list.name}</span>
+ {list.readOnly ? (
+ <PenOff className=" text-gray-400">Read Only</PenOff>
+ ) : (
+ <Pen className="text-green-300">Editable</Pen>
+ )}
+ </div>
+ <p className="text-xs text-gray-400">{list.description}</p>
+ <p className="text-xs text-gray-500 mt-1">{list.entries.length} entries</p>
+ </div>
+ ))
+ ) : (
+ <div className="flex flex-col items-center justify-center p-8 text-center">
+ <Ban className="h-8 w-8 text-gray-500 mb-2" />
+ <p className="text-gray-400">No policy lists available</p>
+ </div>
+ )}
+ </div>
+ </div>
+
+ <div className="md:col-span-2 space-y-4">
+ <div className="flex items-center justify-between">
+ <h3 className="text-sm font-medium text-gray-400">
+ {selectedList.name || "Policy List"} Entries
+ </h3>
+ <div className="flex items-center gap-2">
+ <Input onChange={(e: ChangeEvent) => {
+ const value = (e.target as HTMLInputElement).value;
+ setSearchTerm(value)
+ }} value={searchTerm} placeholder="Search entries..." className="h-8 w-[200px] bg-gray-900 border-gray-800" />
+ {/* Filter Dropdown */}
+ <Select defaultValue="all" onValueChange={setFilter}>
+ <SelectTrigger className="h-8 w-[120px] bg-gray-900 border-gray-800">
+ <SelectValue placeholder="Filter" />
+ </SelectTrigger>
+ <SelectContent className="bg-gray-900 border-gray-800">
+ <SelectItem value="all">All Bans</SelectItem>
+ <SelectItem value="users">Users</SelectItem>
+ <SelectItem value="servers">Servers</SelectItem>
+ <SelectItem value="rooms">Rooms</SelectItem>
+ </SelectContent>
+ </Select>
+ </div>
+ </div>
+
+ <ScrollArea className="h-[400px] rounded-md border border-gray-800">
+ <div className="p-4 space-y-3">
+ {selectedList && entries && entries.length > 0 ? (
+ entries.map((entry) => (
+ <div key={entry.id} className="p-3 rounded-md bg-gray-900 border border-gray-800">
+ <div className="flex items-start justify-between">
+ <div>
+ <p className="font-medium">{entry.target}</p>
+ <p className="text-sm text-gray-400 mt-1">{entry.reason}</p>
+ <p className="text-xs text-gray-500 mt-1">
+ {new Date(entry.timestamp).toLocaleString()}
+ </p>
+ </div>
+ {!selectedList.readOnly && (
+ <Button
+ variant="ghost"
+ size="sm"
+ className="h-8 w-8 p-0 text-gray-400 hover:text-red-400"
+ >
+ <Ban className="h-4 w-4" />
+ <span className="sr-only">Remove</span>
+ </Button>
+ )}
+ </div>
+ </div>
+ ))
+ ) : (
+ <div className="flex flex-col items-center justify-center h-full p-6 text-center">
+ <Ban className="h-8 w-8 text-gray-500 mb-2" />
+ <p className="text-gray-400">No ban entries found</p>
+ </div>
+ )}
+ </div>
+ </ScrollArea>
+ </div>
+ </div>
+ )
+}
+\ No newline at end of file
diff --git a/src/components/dashboard/bot-selector.tsx b/src/components/dashboard/bot-selector.tsx
@@ -1,11 +1,10 @@
-"use client";
-
import { ChevronDown, Crown } from "lucide-react";
import { Button } from "../ui/button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "../ui/dropdown-menu";
import CreateBotModal from "../modals/create-bot";
import Link from "next/link";
import { ListResponse } from "@/lib/api";
+import { Skeleton } from "../ui/skeleton";
export default function BotSelector({
selectedTeam,
@@ -15,7 +14,7 @@ export default function BotSelector({
listData: ListResponse | undefined;
}) {
if (!listData) {
- return <div className="flex h-full w-full items-center justify-center">Loading...</div>
+ return <Skeleton className="h-10 w-64" />;
}
const bots = listData.bots;
diff --git a/src/components/dashboard/layoutWrapper.tsx b/src/components/dashboard/layoutWrapper.tsx
@@ -1,6 +1,5 @@
import { Suspense } from "react";
import { DashboardHeader } from "./dashboard-header";
-import { mockTeams } from "@/app/dashboard/mockData";
import { ListResponse } from "@/lib/api";
interface LayoutWrapperProps {
@@ -12,7 +11,7 @@ interface LayoutWrapperProps {
export default function LayoutWrapper({ children, activeTab, teamIdParam, listData }: LayoutWrapperProps) {
// Get first team id if there is no teamIdParam
- const teamId = teamIdParam || mockTeams[0]?.id;
+ const teamId = teamIdParam || listData?.bots[0]?.id;
return (
<div className="flex min-h-screen flex-col bg-black text-white">
diff --git a/src/components/dashboard/protected-rooms-list.tsx b/src/components/dashboard/protected-rooms-list.tsx
@@ -1,17 +1,18 @@
-import { Team } from "../../app/dashboard/mockData";
+import { DraupnirBot } from "@/lib/api";
import AreYouSureDialog from "../modals/are-you-sure";
interface ProtectedRomsListProps {
- team: Team;
+ selectedBot: DraupnirBot;
}
-export default function ProtectedRomsList({ team }: ProtectedRomsListProps) {
+export default function ProtectedRomsList({ selectedBot }: ProtectedRomsListProps) {
return (
<div className="p-4 space-y-3">
- {team.rooms.map((room, index) => (
+ {selectedBot.protectedRooms.map((room, index) => (
<div key={index} className="flex items-center justify-between p-3 px-4 rounded-md bg-gray-900">
<div>
- <p>{room}</p>
+ <p>{room.displayName ?? room.room}</p>
+ {room.displayName && <p className="text-xs text-gray-400">{room.room}</p>}
</div>
<AreYouSureDialog titlePart="Protected Room" description="Are you sure you want to unprotect the room?" />
</div>
diff --git a/src/components/dashboard/report-list.tsx b/src/components/dashboard/report-list.tsx
@@ -0,0 +1,227 @@
+"use client";
+
+import { Report } from "@/app/dashboard/mockData";
+import { AlertTriangle, Badge, Ban, CheckCircle, Filter, MessageSquare, UserPlus } from "lucide-react";
+import { useState } from "react";
+import { Button } from "../ui/button";
+import { BlurredText } from "../blurred-text";
+import { BlurredImage } from "../blurred-image";
+import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "../ui/dialog";
+
+interface ReportListProps {
+ reports: Report[]
+ filter: "all" | "message" | "invite" | "high"
+}
+
+export default function ReportList({ reports, filter }: ReportListProps) {
+ const [selectedReport, setSelectedReport] = useState<string | null>(null)
+
+ const filteredReports =
+ filter === "all"
+ ? reports
+ : filter === "high"
+ ? reports.filter((r) => r.priority === "high")
+ : reports.filter((r) => r.type === filter)
+ return (
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+ <div className="space-y-4">
+ <h3 className="text-sm font-medium text-gray-400">Report List</h3>
+ {filteredReports.length > 0 ? (
+ filteredReports.map((report) => (
+ <div
+ key={report.id}
+ className={`p-3 rounded-md border cursor-pointer transition-colors ${selectedReport === report.id
+ ? "bg-gray-800 border-purple-600"
+ : "bg-gray-900 border-gray-800 hover:border-gray-700"
+ }`}
+ onClick={() => setSelectedReport(report.id)}
+ >
+ <div className="flex items-center justify-between mb-2">
+ <div className="flex items-center gap-2">
+ {report.type === "message" ? (
+ <MessageSquare className="h-4 w-4 text-blue-400" />
+ ) : (
+ <UserPlus className="h-4 w-4 text-green-400" />
+ )}
+ <span className="text-sm font-medium">
+ {report.type === "message" ? "Message Report" : "Invite Report"}
+ </span>
+ </div>
+ <Badge
+ className={
+ report.priority === "high"
+ ? "text-red-300 hover:bg-red-900/70"
+ : "text-yellow-300 hover:bg-yellow-900/70"
+ }
+ >
+ {report.priority}
+ </Badge>
+ </div>
+ <p className="text-xs text-gray-400 mb-1">{report.room}</p>
+ <p className="text-xs text-gray-400 mb-1">
+ {report.type === "message"
+ ? `From: ${report.subject.sender}`
+ : `From: ${report.subject.sender} To: ${report.subject.target}`}
+ </p>
+ <p className="text-xs text-gray-500">{new Date(report.timestamp).toLocaleString()}</p>
+ </div>
+ ))
+ ) : (
+ <div className="flex flex-col items-center justify-center p-8 text-center">
+ <CheckCircle className="h-8 w-8 text-gray-500 mb-2" />
+ <p className="text-gray-400">No active reports</p>
+ </div>
+ )}
+ </div>
+
+ <div className="space-y-4">
+ <h3 className="text-sm font-medium text-gray-400">Report Details</h3>
+ {selectedReport ? (
+ (() => {
+ const report = reports.find((r) => r.id === selectedReport)
+ if (!report) return <p>Select a report to view details</p>
+
+ return (
+ <div className="space-y-4">
+ <div className="p-4 rounded-md bg-gray-900 border border-gray-800">
+ <div className="flex items-center justify-between mb-3">
+ <h4 className="font-medium">Report Information</h4>
+ <Badge className="bg-purple-900/50 text-purple-300">{report.status}</Badge>
+ </div>
+
+ <div className="space-y-2 text-sm">
+ <div className="flex justify-between">
+ <span className="text-gray-400">Reported by:</span>
+ <span>{report.reporter}</span>
+ </div>
+ <div className="flex justify-between">
+ <span className="text-gray-400">Room:</span>
+ <span>{report.room}</span>
+ </div>
+ <div className="flex justify-between">
+ <span className="text-gray-400">Reason:</span>
+ <span>{report.reason}</span>
+ </div>
+ <div className="flex justify-between">
+ <span className="text-gray-400">Time:</span>
+ <span>{new Date(report.timestamp).toLocaleString()}</span>
+ </div>
+ </div>
+ </div>
+
+ <div className="p-4 rounded-md bg-gray-900 border border-gray-800">
+ <h4 className="font-medium mb-3">
+ {report.type === "message" ? "Message Content" : "Invite Details"}
+ </h4>
+
+ {report.type === "message" && (
+ <div className="space-y-3">
+ <div className="flex justify-between text-sm">
+ <span className="text-gray-400">Sender:</span>
+ <span>{report.subject.sender}</span>
+ </div>
+ <div className="flex justify-between text-sm">
+ <span className="text-gray-400">Message ID:</span>
+ <span className="font-mono text-xs">{report.subject.id}</span>
+ </div>
+ <div className="flex justify-between text-sm">
+ <span className="text-gray-400">Time:</span>
+ <span>{new Date(report.subject.timestamp).toLocaleString()}</span>
+ </div>
+
+ <div className="mt-4">
+ <div className="flex items-center justify-between mb-2">
+ <span className="text-sm text-gray-400">Content:</span>
+ {report.subject.content?.msgtype === "m.text" && (
+ <Button variant="ghost" size="sm" className="h-6 px-2 text-gray-400">
+ <Filter className="h-3 w-3 mr-1" />
+ Filter
+ </Button>
+ )}
+ </div>
+
+ {report.subject.content?.msgtype === "m.text" ? (
+ <BlurredText text={report.subject.content?.body} />
+ ) : report.subject.content?.msgtype === "m.image" ? (
+ <div className="mt-2">
+ <BlurredImage
+ src="/placeholder.svg?height=300&width=400"
+ alt={report.subject.content?.body}
+ width={400}
+ height={300}
+ />
+ <p className="text-xs text-gray-500 mt-1">Image: {report.subject.content.body}</p>
+ </div>
+ ) : (
+ <p className="text-sm">Unsupported content type</p>
+ )}
+ </div>
+ </div>
+ )}
+
+ {report.type === "invite" && (
+ <div className="space-y-3">
+ <div className="flex justify-between text-sm">
+ <span className="text-gray-400">Sender:</span>
+ <span>{report.subject.sender}</span>
+ </div>
+ <div className="flex justify-between text-sm">
+ <span className="text-gray-400">Target:</span>
+ <span>{report.subject.target}</span>
+ </div>
+ <div className="flex justify-between text-sm">
+ <span className="text-gray-400">Time:</span>
+ <span>{new Date(report.subject.timestamp).toLocaleString()}</span>
+ </div>
+ </div>
+ )}
+ </div>
+
+ <div className="flex gap-2 mt-4">
+ <Button className="bg-green-600 text-white hover:bg-green-700">
+ <CheckCircle className="mr-2 h-4 w-4" />
+ Resolve
+ </Button>
+ <Dialog>
+ <DialogTrigger asChild>
+ <Button
+ variant="outline"
+ className="border-red-500 text-red-400 hover:bg-red-950 hover:text-red-300"
+ >
+ <Ban className="mr-2 h-4 w-4" />
+ Ban User
+ </Button>
+ </DialogTrigger>
+ <DialogContent className="bg-gray-950 border-gray-800">
+ <DialogHeader>
+ <DialogTitle>Ban User</DialogTitle>
+ <DialogDescription className="text-gray-400">
+ Ban {report.subject.sender} from your Matrix rooms
+ </DialogDescription>
+ </DialogHeader>
+ {/* Ban dialog content */}
+ </DialogContent>
+ </Dialog>
+ {report.type === "message" && (
+ <Button
+ variant="outline"
+ className="border-gray-700 text-gray-400 hover:bg-gray-900 hover:text-gray-300"
+ >
+ <MessageSquare className="mr-2 h-4 w-4" />
+ Delete Message
+ </Button>
+ )}
+ </div>
+ </div>
+ )
+ })()
+ ) : (
+ <div className="flex flex-col items-center justify-center p-8 text-center">
+ <AlertTriangle className="h-8 w-8 text-gray-500 mb-2" />
+ <p className="text-gray-400">Select a report to view details</p>
+ </div>
+ )}
+ </div>
+ </div>
+ )
+}
+\ No newline at end of file
diff --git a/src/components/dashboard/tab-navigation.tsx b/src/components/dashboard/tab-navigation.tsx
@@ -1,32 +1,32 @@
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { DraupnirBot } from "@/lib/api";
import Link from "next/link";
-import { Team } from "../../app/dashboard/mockData";
interface TabNavigationProps {
- selectedTeam: Team;
+ selectedBot: DraupnirBot;
currentTab: string;
}
-export default function TabNavigation({ selectedTeam, currentTab }: TabNavigationProps) {
+export default function TabNavigation({ selectedBot, currentTab }: TabNavigationProps) {
return (
<div className="flex items-center justify-between mb-8">
- <h1 className="text-3xl font-bold tracking-tight">{selectedTeam.name} Dashboard</h1>
+ <h1 className="text-3xl font-bold tracking-tight">{selectedBot.displayName} Dashboard</h1>
<Tabs activationMode="manual" defaultValue={currentTab} className="hidden md:block">
<TabsList className="bg-gray-900">
<TabsTrigger value="overview" asChild>
- <Link href={`/dashboard/overview?team=${selectedTeam.id}`}>Overview</Link>
+ <Link href={`/dashboard/overview?team=${selectedBot.id}`}>Overview</Link>
</TabsTrigger>
<TabsTrigger value="reports" asChild>
- <Link href={`/dashboard/reports?team=${selectedTeam.id}`}>Reports</Link>
+ <Link href={`/dashboard/reports?team=${selectedBot.id}`}>Reports</Link>
</TabsTrigger>
<TabsTrigger value="bans" asChild>
- <Link href={`/dashboard/bans?team=${selectedTeam.id}`}>Bans</Link>
+ <Link href={`/dashboard/bans?team=${selectedBot.id}`}>Bans</Link>
</TabsTrigger>
<TabsTrigger value="analytics" asChild>
- <Link href={`/dashboard/analytics?team=${selectedTeam.id}`}>Analytics</Link>
+ <Link href={`/dashboard/analytics?team=${selectedBot.id}`}>Analytics</Link>
</TabsTrigger>
<TabsTrigger value="settings" asChild>
- <Link href={`/dashboard/settings?team=${selectedTeam.id}`}>Settings</Link>
+ <Link href={`/dashboard/settings?team=${selectedBot.id}`}>Settings</Link>
</TabsTrigger>
</TabsList>
</Tabs>
diff --git a/src/contexts/session-context.tsx b/src/contexts/session-context.tsx
@@ -15,48 +15,75 @@ type SessionContextType = {
logout: () => Promise<void>;
discoverHomeserver: (matrixId: string) => Promise<void>;
fetchWithTokenRefresh: (url: string, options?: RequestInit) => Promise<Response>;
+ refreshOpenIDToken: () => Promise<void>;
};
const SessionContext = createContext<SessionContextType | undefined>(undefined);
+const refreshOpenIDToken = async (homeserverUrl: string, accessToken: string, user: User, setUser: (user: User) => void, setOpenIDExpiration: (expiration: number) => void, logout: () => Promise<void>) => {
+ try {
+ const openidToken = await generateOpenIDToken(homeserverUrl, user.matrixId, accessToken);
+
+ // Update the session cookie with the new OpenID token
+ const response = await fetch("/api/session", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ openidToken, homeserverUrl, matrixId: user.matrixId, openidExpiration: Math.floor(Date.now() / 1000) + openidToken.expires_in }),
+ });
+ setOpenIDExpiration(openidToken.expires_in);
+
+ if (!response.ok) {
+ console.error("Failed to update session with new OpenID token:", response.status, response.statusText, response.body);
+ throw new Error("Failed to update session with new OpenID token.", {
+ cause: response.body
+ });
+ }
+
+ const userData = await response.json();
+ setUser(userData);
+ } catch (error) {
+ console.error("Failed to refresh OpenID token:", error);
+ await logout();
+
+ throw error;
+ }
+};
+
export function SessionProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | undefined>();
const [isLoading, setIsLoading] = useState(false);
const [homeserverUrl, setHomeserverUrl] = useState<string | undefined>();
const [discoveryStatus, setDiscoveryStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
const [accessToken, setAccessToken] = useState<string | undefined>();
- const [openidExpiration, setOpenIDExpiration] = useState<number | undefined>();
-
- const refreshOpenIDToken = useCallback(async () => {
- if (!homeserverUrl || !accessToken || !user) {
- throw new Error("Missing required information to refresh OpenID token.");
- }
+ const [openidExpiration, setOpenIDExpiration] = useState<number | undefined>()
+ const logout = useCallback(async () => {
try {
- const openidToken = await generateOpenIDToken(homeserverUrl, user.matrixId, accessToken);
-
- // Update the session cookie with the new OpenID token
- const response = await fetch("/api/session", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ openidToken, homeserverUrl, matrixId: user.matrixId }),
- });
+ await fetch("/api/session", { method: "DELETE" });
- if (!response.ok) {
- throw new Error("Failed to update session with new OpenID token.");
+ if (homeserverUrl && accessToken) {
+ await realLogout(homeserverUrl, accessToken);
}
- const userData = await response.json();
- setUser(userData);
+ setUser(undefined);
+ setAccessToken(undefined);
+ setOpenIDExpiration(undefined);
+
+ // Clear sessionStorage
+ sessionStorage.removeItem("accessToken");
+ sessionStorage.removeItem("openidExpiration");
} catch (error) {
- console.error("Failed to refresh OpenID token:", error);
- throw error;
+ console.error("Logout failed:", error);
}
- }, [homeserverUrl, accessToken, user]);
+ }, [accessToken, homeserverUrl]);
// Restore session from cookies on initial load
useEffect(() => {
+ if (homeserverUrl && accessToken && user) {
+ return;
+ }
const fetchSession = async () => {
+ console.log("Restoring session...");
try {
if (openidExpiration) {
const now = Date.now();
@@ -64,7 +91,11 @@ export function SessionProvider({ children }: { children: ReactNode }) {
if (refreshTime > now) {
const timeout = setTimeout(() => {
- refreshOpenIDToken().catch((err) => {
+ console.log("Refreshing OpenID token...");
+ if (!homeserverUrl || !accessToken || !user) {
+ throw new Error("Missing required information to refresh OpenID token.");
+ }
+ refreshOpenIDToken(homeserverUrl, accessToken, user, setUser, setOpenIDExpiration, logout).catch((err) => {
console.error("Failed to refresh OpenID token:", err);
});
}, refreshTime - now);
@@ -87,7 +118,7 @@ export function SessionProvider({ children }: { children: ReactNode }) {
};
fetchSession();
- }, [openidExpiration, refreshOpenIDToken]);
+ }, [accessToken, homeserverUrl, logout, openidExpiration, user]);
@@ -99,7 +130,10 @@ export function SessionProvider({ children }: { children: ReactNode }) {
if (refreshTime > now) {
const timeout = setTimeout(() => {
- refreshOpenIDToken().catch((err) => {
+ if (!homeserverUrl || !accessToken || !user) {
+ throw new Error("Missing required information to refresh OpenID token.");
+ }
+ refreshOpenIDToken(homeserverUrl, accessToken, user, setUser, setOpenIDExpiration, logout).catch((err) => {
console.error("Failed to refresh OpenID token:", err);
});
}, refreshTime - now);
@@ -107,7 +141,7 @@ export function SessionProvider({ children }: { children: ReactNode }) {
return () => clearTimeout(timeout);
}
}
- }, [openidExpiration, refreshOpenIDToken]);
+ }, [accessToken, homeserverUrl, logout, openidExpiration, user]);
const discoverHomeserver = async (matrixId: string) => {
setDiscoveryStatus("loading");
@@ -173,7 +207,7 @@ export function SessionProvider({ children }: { children: ReactNode }) {
const response = await fetch("/api/session", {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ openidToken, homeserverUrl, matrixId }),
+ body: JSON.stringify({ openidToken, homeserverUrl, matrixId, openidExpiration: Math.floor(Date.now() / 1000) + expires_in }),
});
if (!response.ok) {
@@ -214,7 +248,7 @@ export function SessionProvider({ children }: { children: ReactNode }) {
const response = await fetch("/api/session", {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ openidToken, homeserverUrl, matrixId }),
+ body: JSON.stringify({ openidToken, homeserverUrl, matrixId, openidExpiration: Math.floor(Date.now() / 1000) + expires_in }),
});
if (!response.ok) {
@@ -234,32 +268,24 @@ export function SessionProvider({ children }: { children: ReactNode }) {
}
};
- const logout = async () => {
- try {
- await fetch("/api/session", { method: "DELETE" });
-
- if (homeserverUrl && accessToken) {
- await realLogout(homeserverUrl, accessToken);
- }
-
- setUser(undefined);
- setAccessToken(undefined);
- setOpenIDExpiration(undefined);
-
- // Clear sessionStorage
- sessionStorage.removeItem("accessToken");
- sessionStorage.removeItem("openidExpiration");
- } catch (error) {
- console.error("Logout failed:", error);
+ const refreshOpenIDTokenHook = async () => {
+ if (!homeserverUrl || !accessToken || !user) {
+ throw new Error("Missing required information to refresh OpenID token.");
}
- };
+ console.log("Refreshing OpenID token...");
+
+ await refreshOpenIDToken(homeserverUrl, accessToken, user, setUser, setOpenIDExpiration, logout);
+ }
const fetchWithTokenRefresh = async (url: string, options: RequestInit = {}) => {
const response = await fetch(url, options);
if (response.status === 401) {
// Token expired, refresh it
- await refreshOpenIDToken();
+ if (!homeserverUrl || !accessToken || !user) {
+ throw new Error("Missing required information to refresh OpenID token.");
+ }
+ await refreshOpenIDToken(homeserverUrl, accessToken, user, setUser, setOpenIDExpiration, logout);
// Retry the request
return fetch(url, options);
@@ -281,6 +307,7 @@ export function SessionProvider({ children }: { children: ReactNode }) {
logout,
discoverHomeserver,
fetchWithTokenRefresh,
+ refreshOpenIDToken: refreshOpenIDTokenHook,
}}
>
{children}
diff --git a/src/lib/api.ts b/src/lib/api.ts
@@ -1,19 +1,28 @@
'use server'
+export interface DraupnirBot {
+ id: string,
+ managementRoom: string,
+ ownerID: string,
+ displayName: string,
+ protectedRooms: {
+ room: string,
+ displayName?: string,
+ }[],
+ subscribedLists: string[],
+}
+
export interface ListResponse {
- bots: {
- id: string,
- managementRoom: string,
- ownerID: string,
- displayName: string,
- }[]
+ bots: DraupnirBot[]
}
-export async function listBots(token: string): Promise<ListResponse | undefined> {
+export async function listBots(mxid: string, token: string): Promise<ListResponse | undefined> {
+ console.log("Fetching teams from Draupnir4All instance for user:", mxid);
const url = new URL("/api/1/appservice/list", process.env.NEXT_PUBLIC_D4ALL_INSTANCE_ADDRESS);
const res = await fetch(url, {
method: "GET",
headers: {
+ "X-Draupnir-UserID": mxid,
Authorization: `Bearer ${token}`,
"Accept": "application/json",
},
diff --git a/src/lib/auth.ts b/src/lib/auth.ts
@@ -1,13 +1,10 @@
'use client'
export type User = {
- id: string
matrixId: string
- displayName: string
- avatarUrl?: string
- isAdmin: boolean
token: string
homeserverUrl: string
+ openidExpiration: number
}
export function createSession(user: User) {
diff --git a/src/lib/clientSideAuth.ts b/src/lib/clientSideAuth.ts
@@ -11,6 +11,7 @@ export async function login(homeserverUrl: string, username: string, password: s
type: 'm.id.user',
user: username,
},
+ refresh_token: false,
initial_device_display_name: "Draupnir4All Web",
password,
}),
diff --git a/src/middleware.ts b/src/middleware.ts
@@ -1,9 +1,22 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
+import { User } from "./lib/auth";
export function middleware(req: NextRequest) {
const session = req.cookies.get("session");
+ // If not root url check if the session cookie is set, and check if the session expired. If it did redirect to /refresh
+ if (req.nextUrl.pathname !== "/" && session) {
+ const sessionData: User = JSON.parse(session.value);
+ const currentTime = Math.floor(Date.now() / 1000); // Current time in seconds
+
+ // Check if the session has expired
+ if (sessionData.openidExpiration && sessionData.openidExpiration < currentTime) {
+ console.log("Session expired, redirecting to /refresh");
+ return NextResponse.redirect(new URL("/refresh", req.url));
+ }
+ }
+
// Redirect to login if session is missing on protected routes
if (!session && req.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", req.url));