draupnir4all-web

git clone git://archive.git.mtrnord.blog/MTRNord/draupnir4all-web.git
Log | Files | Refs | README | LICENSE

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:
Msrc/app/api/session/route.ts | 24++++++++++--------------
Msrc/app/dashboard/analytics/loading.tsx | 257++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Msrc/app/dashboard/analytics/page.tsx | 124+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Msrc/app/dashboard/bans/loading.tsx | 109++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Msrc/app/dashboard/bans/page.tsx | 165++++++++++++++-----------------------------------------------------------------
Msrc/app/dashboard/overview/loading.tsx | 161++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Msrc/app/dashboard/overview/page.tsx | 22++++++++++++++--------
Msrc/app/dashboard/reports/loading.tsx | 167+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Msrc/app/dashboard/reports/page.tsx | 343++++++++++++++++++++-----------------------------------------------------------
Msrc/app/dashboard/settings/bot/page.tsx | 2+-
Msrc/app/dashboard/settings/members/page.tsx | 2+-
Msrc/app/dashboard/settings/protectedRooms/page.tsx | 2+-
Asrc/app/refresh/page.tsx | 20++++++++++++++++++++
Asrc/components/dashboard/bans-list.tsx | 131+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/dashboard/bot-selector.tsx | 5++---
Msrc/components/dashboard/layoutWrapper.tsx | 3+--
Msrc/components/dashboard/protected-rooms-list.tsx | 11++++++-----
Asrc/components/dashboard/report-list.tsx | 228+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/dashboard/tab-navigation.tsx | 18+++++++++---------
Msrc/contexts/session-context.tsx | 119++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Msrc/lib/api.ts | 23++++++++++++++++-------
Msrc/lib/auth.ts | 5+----
Msrc/lib/clientSideAuth.ts | 1+
Msrc/middleware.ts | 13+++++++++++++
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));