commit 8e17c300a656a82a01cff2470210834516d8c96f
parent 6fe6a9ae55a6791f597edff226594439979a96cc
Author: MTRNord <mtrnord1@gmail.com>
Date: Mon, 28 Apr 2025 20:48:24 +0200
Prepare login being fully client side. However this will require future changes for the SSR sadly.
Signed-off-by: MTRNord <mtrnord1@gmail.com>
Diffstat:
20 files changed, 484 insertions(+), 613 deletions(-)
diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts
@@ -1,32 +0,0 @@
-import { type NextRequest, NextResponse } from "next/server"
-import { createSession } from "@/lib/auth"
-
-export async function POST(request: NextRequest) {
- try {
- const { matrixId } = await request.json()
-
- if (!matrixId) {
- return NextResponse.json({ error: "Missing required fields" }, { status: 400 })
- }
-
- // In a real app, you would verify the user's credentials with Matrix
- // For demo purposes, we'll create a mock user
- const user = {
- id: "user_" + Math.random().toString(36).substring(2, 9),
- matrixId,
- displayName: matrixId.split(":")[0].substring(1),
- isAdmin: matrixId.includes("admin"),
- }
-
- // Create a session token
- await createSession(user)
-
- // Create a response
- const response = NextResponse.json({ success: true, user })
-
- return response
- } catch (error) {
- console.error("Login failed:", error)
- return NextResponse.json({ error: "Authentication failed" }, { status: 500 })
- }
-}
diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts
@@ -1,16 +0,0 @@
-import { NextResponse } from "next/server"
-import { deleteSession } from "@/lib/auth"
-
-export async function POST() {
- try {
- // Create a response
- const response = NextResponse.json({ success: true })
-
- // Clear the session cookie
- await deleteSession()
- return response
- } catch (error) {
- console.error("Logout failed:", error)
- return NextResponse.json({ error: "Logout failed" }, { status: 500 })
- }
-}
diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts
@@ -1,32 +0,0 @@
-import { type NextRequest, NextResponse } from "next/server"
-import { createSession } from "@/lib/auth"
-
-export async function POST(request: NextRequest) {
- try {
- const { matrixId } = await request.json()
-
- if (!matrixId) {
- return NextResponse.json({ error: "Missing required fields" }, { status: 400 })
- }
-
- // In a real app, you would register the user with Matrix
- // For demo purposes, we'll create a mock user
- const user = {
- id: "user_" + Math.random().toString(36).substring(2, 9),
- matrixId,
- displayName: matrixId.split(":")[0].substring(1),
- isAdmin: false,
- }
-
- // Create a session token
- await createSession(user)
-
- // Create a response
- const response = NextResponse.json({ success: true, user })
-
- return response
- } catch (error) {
- console.error("Registration failed:", error)
- return NextResponse.json({ error: "Registration failed" }, { status: 500 })
- }
-}
diff --git a/src/app/api/auth/session/route.ts b/src/app/api/auth/session/route.ts
@@ -1,17 +0,0 @@
-import { NextResponse } from "next/server"
-import { getSessionUser } from "@/lib/auth"
-
-export async function GET() {
- try {
- const user = await getSessionUser()
-
- if (!user) {
- return NextResponse.json({ authenticated: false }, { status: 401 })
- }
-
- return NextResponse.json({ authenticated: true, user })
- } catch (error) {
- console.error("Session check failed:", error)
- return NextResponse.json({ authenticated: false }, { status: 500 })
- }
-}
diff --git a/src/app/api/verify-matrix-token/route.ts b/src/app/api/verify-matrix-token/route.ts
@@ -1,68 +0,0 @@
-import { NextResponse } from "next/server"
-
-interface WellKnownResponse {
- "m.homeserver"?: {
- base_url: string
- }
-}
-
-export async function POST(request: Request) {
- try {
- const { matrixId, token } = await request.json()
-
- if (!matrixId || !token) {
- return NextResponse.json({ error: "Missing required fields" }, { status: 400 })
- }
-
- if (!matrixId.includes(":")) {
- return NextResponse.json({ error: "Invalid Matrix ID format" }, { status: 400 })
- }
-
- // Extract server name from Matrix ID
- const serverName = matrixId.split(":").pop()
-
- // Discover homeserver URL
- let homeserverUrl = `https://${serverName}`
-
- try {
- // Try to fetch well-known data
- const wellKnownUrl = `https://${serverName}/.well-known/matrix/client`
- const response = await fetch(wellKnownUrl)
-
- if (response.ok) {
- const data = (await response.json()) as WellKnownResponse
- if (data["m.homeserver"]?.base_url) {
- // TODO: Fix me
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- homeserverUrl = data["m.homeserver"].base_url
- }
- }
- } catch (e) {
- console.warn("Well-known discovery failed, falling back to direct server", e)
- }
-
- // In a real implementation, you would:
- // 1. Make a request to the Matrix server to verify the token
- // const response = await fetch(
- // `${homeserverUrl}/_matrix/federation/v1/openid/userinfo?access_token=${token}`,
- // { method: "GET" }
- // )
- // const data = await response.json()
-
- // 2. Verify that the user ID in the response matches the provided matrixId
- // if (data.sub !== matrixId) {
- // return NextResponse.json(
- // { error: "Token verification failed" },
- // { status: 401 }
- // )
- // }
-
- // 3. Create a user account or session in your system
-
- // For demo purposes, we'll just return success
- return NextResponse.json({ success: true })
- } catch (error) {
- console.error("Error verifying Matrix token:", error)
- return NextResponse.json({ error: "Internal server error" }, { status: 500 })
- }
-}
diff --git a/src/app/dashboard/analytics/page.tsx b/src/app/dashboard/analytics/page.tsx
@@ -8,7 +8,7 @@ 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 { redirect, 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";
@@ -16,6 +16,7 @@ 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 { useSession } from "@/contexts/session-context";
// Mock data for charts
const roomActivityData: RoomActivityData[] = [
@@ -64,6 +65,10 @@ export default function AnalyticsDashboard() {
const teamIdParam = searchParams.get("team")
const [selectedTeam, setSelectedTeam] = useState(mockTeams.find((t) => t.id === teamIdParam) || mockTeams[0])
+ const { user } = useSession()
+ if (!user) {
+ redirect("/login")
+ }
// Update selected team when URL param changes
useEffect(() => {
diff --git a/src/app/dashboard/bans/loading.tsx b/src/app/dashboard/bans/loading.tsx
@@ -0,0 +1,60 @@
+import { Skeleton } from "@/components/ui/skeleton"
+import { Card, CardContent, CardHeader } from "@/components/ui/card"
+
+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>
+ <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>
+ </CardContent>
+ </Card>
+ </div>
+ )
+}
diff --git a/src/app/dashboard/bans/page.tsx b/src/app/dashboard/bans/page.tsx
@@ -8,8 +8,9 @@ import { ChangeEvent, useEffect, useState } from "react";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { EBanTypes, mockPolicyLists, mockTeams, } from "../mockData";
import AddBan from "../../../components/modals/add-ban";
-import { useSearchParams } from "next/navigation";
+import { redirect, useSearchParams } from "next/navigation";
import TabNavigation from "../../../components/dashboard/tab-navigation";
+import { useSession } from "@/contexts/session-context";
export default function Bans() {
@@ -17,6 +18,10 @@ export default function Bans() {
const teamIdParam = searchParams.get("team")
const [selectedTeam, setSelectedTeam] = useState(mockTeams.find((t) => t.id === teamIdParam) || mockTeams[0])
const [searchTerm, setSearchTerm] = useState<string>("");
+ const { user } = useSession()
+ if (!user) {
+ redirect("/login")
+ }
// Update selected team when URL param changes
useEffect(() => {
diff --git a/src/app/dashboard/overview/page.tsx b/src/app/dashboard/overview/page.tsx
@@ -1,36 +1,26 @@
+"use client"
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, PolicyList, Report, Team } from "../mockData"
+import { mockPolicyLists, mockReports, mockTeams } from "../mockData"
import TabNavigation from "../../../components/dashboard/tab-navigation";
+import { useSession } from "@/contexts/session-context"
+import { redirect, useSearchParams } from "next/navigation"
-interface OverviewPageProps {
- selectedTeam: Team;
- reports: Report[];
- policyLists: PolicyList[];
-}
-async function fetchData(teamIdParam?: string): Promise<OverviewPageProps> {
- "use cache";
- // TODO: Check if we really can cache the real data here
+export default function OverviewPage() {
+ const searchParams = useSearchParams()
+ const { user } = useSession()
+ if (!user) {
+ redirect("/login")
+ }
+ const teamIdParam = searchParams.get("team");
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);
- return {
- selectedTeam,
- reports,
- policyLists,
- };
-}
-
-export default async function OverviewPage({ searchParams }: { searchParams: Promise<{ team?: string }> }) {
- const { team } = await searchParams;
- const { selectedTeam, reports, policyLists } = await fetchData(team);
-
return (
<>
<TabNavigation selectedTeam={selectedTeam} currentTab="overview" />
@@ -77,6 +67,9 @@ export default async function OverviewPage({ searchParams }: { searchParams: Pro
<div className="h-3 w-3 rounded-full bg-green-500"></div>
<div className="text-sm font-medium">Online</div>
</div>
+ if (!user) {
+ redirect("/login")
+ }
<p className="text-xs text-gray-400">Last restart: 7d ago</p>
</CardContent>
</Card>
diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx
@@ -1,11 +1,14 @@
-import { redirect } from "next/navigation"
+"use client"
+import { useSession } from "@/contexts/session-context"
+import { redirect, useSearchParams } from "next/navigation"
-export default async function DashboardPage({
- searchParams,
-}: {
- searchParams: Promise<{ [key: string]: string | string[] | undefined }>
-}) {
- const teamId = (await searchParams).team as string | undefined
+export default function DashboardPage() {
+ const { user } = useSession()
+ const searchParams = useSearchParams()
+ const teamId = searchParams.get("team")
+ if (!user) {
+ redirect("/login")
+ }
// Redirect to overview with team parameter
const redirectUrl = teamId ? `/dashboard/overview?team=${teamId}` : "/dashboard/overview"
diff --git a/src/app/dashboard/reports/loading.tsx b/src/app/dashboard/reports/loading.tsx
@@ -0,0 +1,96 @@
+import { Skeleton } from "@/components/ui/skeleton"
+import { Card, CardContent, CardHeader } from "@/components/ui/card"
+
+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>
+ </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" />
+ </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>
+ ))}
+ </div>
+
+ <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-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="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>
+
+ <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>
+ </CardContent>
+ </Card>
+ </div>
+ )
+}
diff --git a/src/app/dashboard/reports/page.tsx b/src/app/dashboard/reports/page.tsx
@@ -18,13 +18,18 @@ import {
DialogTrigger,
} from "@/components/ui/dialog"
import { mockReports, mockTeams } from "../mockData";
-import { useSearchParams } from "next/navigation"
+import { redirect, useSearchParams } from "next/navigation"
import TabNavigation from "../../../components/dashboard/tab-navigation"
+import { useSession } from "@/contexts/session-context"
export default function ReportsPage() {
const searchParams = useSearchParams()
const teamIdParam = searchParams.get("team")
const [selectedTeam, setSelectedTeam] = useState(mockTeams.find((t) => t.id === teamIdParam) || mockTeams[0])
+ const { user } = useSession()
+ if (!user) {
+ redirect("/login")
+ }
// Update selected team when URL param changes
useEffect(() => {
diff --git a/src/app/dashboard/settings/page.tsx b/src/app/dashboard/settings/page.tsx
@@ -26,17 +26,22 @@ import {
} from "@/components/ui/dropdown-menu"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
-import { useSearchParams } from "next/navigation"
+import { redirect, useSearchParams } from "next/navigation"
import { mockTeams } from "../mockData"
import AddProtectedRoom from "../../../components/modals/add-protected-room"
import ProtectedRomsList from "../../../components/dashboard/protected-rooms-list"
import TabNavigation from "../../../components/dashboard/tab-navigation"
+import { useSession } from "@/contexts/session-context";
export default function TeamManagement() {
const searchParams = useSearchParams()
const teamIdParam = searchParams.get("team")
const [selectedTeam] = useState(mockTeams.find((t) => t.id === teamIdParam) || mockTeams[0])
+ const { user } = useSession()
+ if (!user) {
+ redirect("/login")
+ }
// TODO: Make this actual pages so we can use more ssr
const [activeTab, setActiveTab] = useState("members")
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
@@ -16,11 +16,15 @@ export default function RootLayout({
}: Readonly<{
children: React.ReactNode
}>) {
+
+
return (
<html lang="en" className="dark" style={{ colorScheme: "dark" }}>
<body className={inter.className}>
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem disableTransitionOnChange>
- <SessionProvider>{children}</SessionProvider>
+ <SessionProvider>
+ {children}
+ </SessionProvider>
</ThemeProvider>
</body>
</html>
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx
@@ -1,39 +1,65 @@
"use client"
-import { useState } from "react"
+import { useCallback, useEffect, useState } from "react"
import Link from "next/link"
-import { Shield, ArrowRight, Loader2 } from "lucide-react"
+import { Shield, ArrowRight, Loader2, X } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
-import { MatrixLoginForm } from "@/components/matrix-login-form"
import { useSession } from "@/contexts/session-context"
+import { redirect } from "next/navigation"
export default function LoginPage() {
- const { login, isLoading } = useSession()
const [matrixId, setMatrixId] = useState("")
const [step, setStep] = useState<"initial" | "login" | "error">("initial")
+ const [password, setPassword] = useState("")
+ const { isLoading, login, discoveryStatus, homeserverUrl, discoverHomeserver, user } = useSession();
+ const [loginLoading, setLoginLoading] = useState(isLoading)
const [errorMessage, setErrorMessage] = useState("")
- const startMatrixAuth = () => {
- if (!matrixId) return
- setStep("login")
+ if (user) {
+ redirect("/dashboard")
}
- // TODO: Fix me
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const handleLoginSuccess = async (token: string) => {
+ useEffect(() => {
+ console.log("Discovery status changed:", discoveryStatus)
+ if (discoveryStatus === "loading") {
+ setLoginLoading(true)
+ } else if (discoveryStatus === "error") {
+ setErrorMessage("Failed to discover homeserver. Please check your Matrix ID format.")
+ setLoginLoading(false)
+ } else if (discoveryStatus === "success") {
+ setErrorMessage("")
+ setLoginLoading(false)
+ }
+ }, [discoveryStatus])
+
+ const startMatrixAuth = useCallback(() => {
+ if (!matrixId) return
+ setLoginLoading(true)
+ discoverHomeserver(matrixId).then(() => {
+ setStep("login")
+ }).catch((err) => {
+ console.error("Error discovering homeserver:", err)
+ setErrorMessage("Please check your Matrix ID format. It should be like @username:matrix.org")
+ setStep("error")
+ setLoginLoading(false)
+ });
+ }, [matrixId, discoverHomeserver]);
+
+ const handleSubmit = useCallback(async (e: React.FormEvent) => {
+ e.preventDefault()
+ setErrorMessage("")
+
try {
- // Use the login function from session context
await login(matrixId)
- } catch (error) {
- console.error("Login failed", error)
- setErrorMessage(error instanceof Error ? error.message : "Authentication failed")
- setStep("error")
+ } catch (err) {
+ console.error("Login failed:", err)
+ setErrorMessage("Login failed. Please check your credentials.")
}
- }
+ }, [login, matrixId])
return (
<div className="flex min-h-screen flex-col bg-black text-white">
@@ -69,10 +95,86 @@ export default function LoginPage() {
)}
{step === "login" && (
- <MatrixLoginForm matrixId={matrixId} onSuccess={handleLoginSuccess} onCancel={() => setStep("initial")} />
+ <form onSubmit={handleSubmit} className="space-y-4">
+ <div className="space-y-2">
+ <div className="flex items-center justify-between">
+ <Label htmlFor="matrix-username">Matrix ID</Label>
+ <Button type="button" variant="ghost" size="sm" className="h-6 px-2 text-gray-400" onClick={() => setStep("initial")}>
+ <X className="h-4 w-4" />
+ </Button>
+ </div>
+ <div className="flex items-center gap-2 rounded-md bg-gray-900 px-3 py-2 text-sm">
+ <span>{matrixId}</span>
+ </div>
+
+ {discoveryStatus === "loading" && (
+ <div className="flex items-center gap-2 text-xs text-gray-400">
+ <Loader2 className="h-3 w-3 animate-spin" />
+ Discovering homeserver...
+ </div>
+ )}
+
+ {discoveryStatus === "success" && homeserverUrl && (
+ <p className="text-xs text-gray-400">Authenticating with {homeserverUrl.replace(/^https?:\/\//, "")}</p>
+ )}
+
+ {discoveryStatus === "error" && (
+ <p className="text-xs text-red-400">Failed to discover homeserver. Please check your Matrix ID and ensure it's format is "@localpart:example.com".</p>
+ )}
+ </div>
+
+ {discoveryStatus === "success" && (
+ <div className="space-y-2">
+ <Label htmlFor="matrix-password">Password</Label>
+ <Input
+ id="matrix-password"
+ type="password"
+ placeholder="Enter your Matrix password"
+ className="bg-gray-900 border-gray-800"
+ value={password}
+ onChange={(e) => setPassword(e.target.value)}
+ required
+ />
+ </div>
+ )}
+
+ <div className="flex gap-2">
+ <Button
+ type="submit"
+ className="flex-1 bg-purple-600 text-white hover:bg-purple-700"
+ disabled={isLoading || !password || discoveryStatus !== "success"}
+ >
+ {isLoading ? (
+ <>
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+ Authenticating...
+ </>
+ ) : (
+ "Log in"
+ )}
+ </Button>
+ <Button
+ type="button"
+ variant="outline"
+ className="border-gray-700 text-gray-400 hover:bg-gray-900 hover:text-gray-300"
+ onClick={() => {
+ setStep("initial")
+ setMatrixId("")
+ setPassword("")
+ }}
+ disabled={isLoading}
+ >
+ Cancel
+ </Button>
+ </div>
+
+ <p className="text-xs text-gray-400 text-center">
+ Your credentials are sent directly to your Matrix homeserver. Draupnir4All only receives a verification token.
+ </p>
+ </form>
)}
- {isLoading && (
+ {(isLoading || loginLoading) && (
<div className="flex flex-col items-center justify-center py-6 space-y-4">
<Loader2 className="h-8 w-8 animate-spin text-purple-400" />
<p className="text-gray-300">Logging in...</p>
diff --git a/src/app/page.tsx b/src/app/page.tsx
@@ -7,7 +7,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
export const experimental_ppr = true
-const d4all_support_url = process.env.D4ALL_SUPPORT_URL || "#"
+const d4all_support_url = process.env.NEXT_PUBLIC_D4ALL_SUPPORT_URL || "#"
export default function Home() {
return (
diff --git a/src/components/matrix-login-form.tsx b/src/components/matrix-login-form.tsx
@@ -1,240 +0,0 @@
-"use client"
-
-import type React from "react"
-
-import { useState, useEffect } from "react"
-import { Loader2, X, AlertCircle } from "lucide-react"
-
-import { Button } from "@/components/ui/button"
-import { Input } from "@/components/ui/input"
-import { Label } from "@/components/ui/label"
-import { Alert, AlertDescription } from "@/components/ui/alert"
-
-interface MatrixLoginFormProps {
- matrixId: string
- onSuccess: (token: string) => void
- onCancel: () => void
-}
-
-interface WellKnownResponse {
- "m.homeserver"?: {
- base_url: string
- }
-}
-
-export function MatrixLoginForm({ matrixId, onSuccess, onCancel }: MatrixLoginFormProps) {
- const [password, setPassword] = useState("")
- const [isLoading, setIsLoading] = useState(false)
- const [error, setError] = useState("")
- const [homeserverUrl, setHomeserverUrl] = useState<string | null>(null)
- const [discoveryStatus, setDiscoveryStatus] = useState<"idle" | "loading" | "success" | "error">("idle")
-
- // Discover homeserver URL
- useEffect(() => {
- async function discoverHomeserver() {
- if (!matrixId.includes(":")) {
- setError("Invalid Matrix ID format. Please include a domain (e.g., @user:domain).")
- return null
- }
- // Extract username and server from Matrix ID
- const serverName = matrixId.split(":").pop()
- if (!serverName)
-
- setDiscoveryStatus("loading")
- try {
- // Try to fetch well-known data
- const wellKnownUrl = `https://${serverName}/.well-known/matrix/client`
-
- try {
- const response = await fetch(wellKnownUrl)
- if (response.ok) {
- const data = (await response.json()) as WellKnownResponse
- if (data["m.homeserver"]?.base_url) {
- setHomeserverUrl(data["m.homeserver"].base_url)
- setDiscoveryStatus("success")
- return
- }
- }
- } catch (e) {
- console.warn("Well-known discovery failed, falling back to direct server", e)
- }
-
- // Fallback to direct server
- setHomeserverUrl(`https://${serverName}`)
- setDiscoveryStatus("success")
- } catch (err) {
- console.error("Homeserver discovery failed:", err)
- setError("Could not discover Matrix homeserver. Please check your Matrix ID.")
- setDiscoveryStatus("error")
- }
- }
-
- discoverHomeserver()
- }, [matrixId])
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault()
- if (!homeserverUrl) {
- setError("Homeserver URL not available. Please try again.")
- return
- }
-
- setIsLoading(true)
- setError("")
-
- try {
- // Step 1: Authenticate with the Matrix homeserver
- console.log(`Authenticating with ${homeserverUrl}`)
-
- // In a real implementation:
- // const loginResponse = await fetch(`${homeserverUrl}/_matrix/client/v3/login`, {
- // method: 'POST',
- // headers: { 'Content-Type': 'application/json' },
- // body: JSON.stringify({
- // type: 'm.login.password',
- // identifier: {
- // type: 'm.id.user',
- // user: username
- // },
- // password
- // })
- // })
-
- // const loginData = await loginResponse.json()
- // if (!loginData.access_token) {
- // throw new Error(loginData.error || 'Login failed')
- // }
-
- // Simulate login delay
- await new Promise((resolve) => setTimeout(resolve, 1000))
-
- // Simulate access token
- // TODO: Fix me
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const mockAccessToken = "syt_" + Math.random().toString(36).substring(2, 15)
-
- // Step 2: Request an OpenID token
- console.log(`Requesting OpenID token for ${matrixId}`)
-
- // In a real implementation:
- // const openIdResponse = await fetch(
- // `${homeserverUrl}/_matrix/client/v3/user/${encodeURIComponent(matrixId)}/openid/request_token`,
- // {
- // method: 'POST',
- // headers: {
- // 'Content-Type': 'application/json',
- // 'Authorization': `Bearer ${loginData.access_token}`
- // },
- // body: JSON.stringify({})
- // }
- // )
-
- // const openIdData = await openIdResponse.json()
- // if (!openIdData.access_token) {
- // throw new Error(openIdData.error || 'Failed to get OpenID token')
- // }
-
- // Simulate OpenID token request delay
- await new Promise((resolve) => setTimeout(resolve, 500))
-
- // Simulate OpenID token
- const mockOpenIdToken =
- "ey" +
- Math.random().toString(36).substring(2, 15) +
- "." +
- Math.random().toString(36).substring(2, 15) +
- "." +
- Math.random().toString(36).substring(2, 15)
-
- // Pass the token to the parent component
- onSuccess(mockOpenIdToken)
- } catch (err) {
- console.error("Matrix login failed:", err)
- setError(err instanceof Error ? err.message : "Authentication failed")
- setIsLoading(false)
- }
- }
-
- return (
- <form onSubmit={handleSubmit} className="space-y-4">
- <div className="space-y-2">
- <div className="flex items-center justify-between">
- <Label htmlFor="matrix-username">Matrix ID</Label>
- <Button type="button" variant="ghost" size="sm" className="h-6 px-2 text-gray-400" onClick={onCancel}>
- <X className="h-4 w-4" />
- </Button>
- </div>
- <div className="flex items-center gap-2 rounded-md bg-gray-900 px-3 py-2 text-sm">
- <span>{matrixId}</span>
- </div>
-
- {discoveryStatus === "loading" && (
- <div className="flex items-center gap-2 text-xs text-gray-400">
- <Loader2 className="h-3 w-3 animate-spin" />
- Discovering homeserver...
- </div>
- )}
-
- {discoveryStatus === "success" && homeserverUrl && (
- <p className="text-xs text-gray-400">Authenticating with {homeserverUrl.replace(/^https?:\/\//, "")}</p>
- )}
-
- {discoveryStatus === "error" && (
- <p className="text-xs text-red-400">Failed to discover homeserver. Please check your Matrix ID.</p>
- )}
- </div>
-
- {discoveryStatus === "success" && (
- <div className="space-y-2">
- <Label htmlFor="matrix-password">Password</Label>
- <Input
- id="matrix-password"
- type="password"
- placeholder="Enter your Matrix password"
- className="bg-gray-900 border-gray-800"
- value={password}
- onChange={(e) => setPassword(e.target.value)}
- required
- />
- </div>
- )}
-
- {error && (
- <Alert variant="destructive" className="bg-red-900/20 border-red-800">
- <AlertCircle className="h-4 w-4 text-red-400" />
- <AlertDescription className="text-sm text-red-300">{error}</AlertDescription>
- </Alert>
- )}
-
- <div className="flex gap-2">
- <Button
- type="submit"
- className="flex-1 bg-purple-600 text-white hover:bg-purple-700"
- disabled={isLoading || !password || discoveryStatus !== "success"}
- >
- {isLoading ? (
- <>
- <Loader2 className="mr-2 h-4 w-4 animate-spin" />
- Authenticating...
- </>
- ) : (
- "Log in"
- )}
- </Button>
- <Button
- type="button"
- variant="outline"
- className="border-gray-700 text-gray-400 hover:bg-gray-900 hover:text-gray-300"
- onClick={onCancel}
- disabled={isLoading}
- >
- Cancel
- </Button>
- </div>
-
- <p className="text-xs text-gray-400 text-center">
- Your credentials are sent directly to your Matrix homeserver. Draupnir4All only receives a verification token.
- </p>
- </form>
- )
-}
diff --git a/src/contexts/session-context.tsx b/src/contexts/session-context.tsx
@@ -1,71 +1,158 @@
"use client"
-import { createContext, useContext, useEffect, useState, type ReactNode } from "react"
+import { createContext, useContext, useState, type ReactNode } from "react"
import { useRouter } from "next/navigation"
-
-type User = {
- id: string
- matrixId: string
- displayName: string
- avatarUrl?: string
- isAdmin: boolean
-}
+import { createSession, deleteSession, getSessionUser, User } from "@/lib/auth"
type SessionContextType = {
- user: User | null
+ user?: User
isLoading: boolean
isAuthenticated: boolean
+ discoveryStatus: "idle" | "loading" | "success" | "error"
+ homeserverUrl?: string
+ token?: string
login: (matrixId: string) => Promise<void>
register: (matrixId: string) => Promise<void>
logout: () => Promise<void>
+ discoverHomeserver: (matrixId: string) => Promise<void>
+}
+
+interface WellKnownResponse {
+ "m.homeserver"?: {
+ base_url: string
+ }
}
const SessionContext = createContext<SessionContextType | undefined>(undefined)
export function SessionProvider({ children }: { children: ReactNode }) {
- const [user, setUser] = useState<User | null>(null)
- const [isLoading, setIsLoading] = useState(true)
+ const [user, setUser] = useState<User | undefined>(getSessionUser())
+ const [isLoading, setIsLoading] = useState(false)
+ const [homeserverUrl, setHomeserverUrl] = useState<string | undefined>()
+ const [discoveryStatus, setDiscoveryStatus] = useState<"idle" | "loading" | "success" | "error">("idle")
+ const [token, setToken] = useState<string | undefined>()
const router = useRouter()
- // Check for existing session on mount
- useEffect(() => {
- const checkSession = async () => {
- try {
- const response = await fetch("/api/auth/session")
- const data = await response.json()
- if (data.authenticated && data.user) {
- setUser(data.user)
+ const discoverHomeserver = async (matrixId: string) => {
+ if (matrixId === "") {
+ return
+ }
+ setDiscoveryStatus("loading")
+ if (!matrixId.includes(":")) {
+ setDiscoveryStatus("error")
+ throw new Error("Invalid Matrix ID format. Please include a domain (e.g., @user:domain).")
+ }
+ // Extract username and server from Matrix ID
+ const serverName = matrixId.split(":").pop()
+ try {
+ // Try to fetch well-known data
+ const wellKnownUrl = `https://${serverName}/.well-known/matrix/client`
+
+ try {
+ const response = await fetch(wellKnownUrl)
+ if (response.ok) {
+ const data = (await response.json()) as WellKnownResponse
+ if (data["m.homeserver"]?.base_url) {
+ setDiscoveryStatus("success")
+ setHomeserverUrl(data["m.homeserver"].base_url)
+ return;
+ }
}
- } catch (error) {
- console.error("Failed to check session:", error)
- } finally {
- setIsLoading(false)
+ } catch (e) {
+ console.warn("Well-known discovery failed, falling back to direct server", e)
}
- }
- checkSession()
- }, [])
+ // Fallback to direct server
+ setDiscoveryStatus("success")
+ setHomeserverUrl(`https://${serverName}`)
+ } catch (err) {
+ setDiscoveryStatus("error")
+ setHomeserverUrl(undefined)
+ console.error("Homeserver discovery failed:", err)
+ throw new Error("Could not discover Matrix homeserver. Please check your Matrix ID.")
+ }
+ }
const login = async (matrixId: string) => {
setIsLoading(true)
try {
- const response = await fetch("/api/auth/login", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ matrixId }),
+ // Step 1: Authenticate with the Matrix homeserver
+ console.log(`Authenticating with ${homeserverUrl}`)
+
+ // In a real implementation:
+ // const loginResponse = await fetch(`${homeserverUrl}/_matrix/client/v3/login`, {
+ // method: 'POST',
+ // headers: { 'Content-Type': 'application/json' },
+ // body: JSON.stringify({
+ // type: 'm.login.password',
+ // identifier: {
+ // type: 'm.id.user',
+ // user: username
+ // },
+ // password
+ // })
+ // })
+
+ // const loginData = await loginResponse.json()
+ // if (!loginData.access_token) {
+ // throw new Error(loginData.error || 'Login failed')
+ // }
+
+ // Simulate login delay
+ await new Promise((resolve) => setTimeout(resolve, 1000))
+
+ // Simulate access token
+ // TODO: Fix me
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const mockAccessToken = "syt_" + Math.random().toString(36).substring(2, 15)
+
+ // Step 2: Request an OpenID token
+ console.log(`Requesting OpenID token for ${matrixId}`)
+
+ // In a real implementation:
+ // const openIdResponse = await fetch(
+ // `${homeserverUrl}/_matrix/client/v3/user/${encodeURIComponent(matrixId)}/openid/request_token`,
+ // {
+ // method: 'POST',
+ // headers: {
+ // 'Content-Type': 'application/json',
+ // 'Authorization': `Bearer ${loginData.access_token}`
+ // },
+ // body: JSON.stringify({})
+ // }
+ // )
+
+ // const openIdData = await openIdResponse.json()
+ // if (!openIdData.access_token) {
+ // throw new Error(openIdData.error || 'Failed to get OpenID token')
+ // }
+
+ // Simulate OpenID token request delay
+ await new Promise((resolve) => setTimeout(resolve, 500))
+
+ // Simulate OpenID token
+ const mockOpenIdToken =
+ "ey" +
+ Math.random().toString(36).substring(2, 15) +
+ "." +
+ Math.random().toString(36).substring(2, 15) +
+ "." +
+ Math.random().toString(36).substring(2, 15)
+
+ // Save the OpenID token to the session
+ setToken(mockOpenIdToken)
+ createSession({
+ matrixId,
+ displayName: matrixId.split(":")[0].substring(1),
+ id: "user_" + Math.random().toString(36).substring(2, 9),
+ isAdmin: false,
})
-
- if (!response.ok) {
- throw new Error("Authentication failed")
- }
-
- const data = await response.json()
- setUser(data.user)
- router.push("/dashboard")
- } catch (error) {
- console.error("Login failed:", error)
- throw error
+ router.replace("/dashboard")
+ return;
+ } catch (err) {
+ console.error("Matrix login failed:", err)
+ throw err instanceof Error ? err.message : "Authentication failed"
} finally {
setIsLoading(false)
}
@@ -86,7 +173,7 @@ export function SessionProvider({ children }: { children: ReactNode }) {
const data = await response.json()
setUser(data.user)
- router.push("/dashboard")
+ router.replace("/dashboard")
} catch (error) {
console.error("Registration failed:", error)
throw error
@@ -97,8 +184,8 @@ export function SessionProvider({ children }: { children: ReactNode }) {
const logout = async () => {
try {
- await fetch("/api/auth/logout", { method: "POST" })
- setUser(null)
+ setUser(undefined)
+ deleteSession()
router.push("/")
} catch (error) {
console.error("Logout failed:", error)
@@ -110,10 +197,14 @@ export function SessionProvider({ children }: { children: ReactNode }) {
value={{
user,
isLoading,
+ homeserverUrl,
+ discoveryStatus,
isAuthenticated: !!user,
+ token,
login,
register,
logout,
+ discoverHomeserver,
}}
>
{children}
diff --git a/src/lib/auth.ts b/src/lib/auth.ts
@@ -1,8 +1,4 @@
-import { cookies } from "next/headers"
-import { jwtVerify, SignJWT } from "jose"
-
-const secretKey = process.env.SESSION_SECRET
-const encodedKey = new TextEncoder().encode(secretKey)
+'use client'
export type User = {
id: string
@@ -12,88 +8,33 @@ export type User = {
isAdmin: boolean
}
-export async function encrypt(user: User) {
- return new SignJWT({ user })
- .setProtectedHeader({ alg: 'HS256' })
- .setIssuedAt()
- .setExpirationTime('7d')
- .sign(encodedKey)
-}
-
-export async function decrypt(session: string | undefined = '') {
- try {
- const { payload } = await jwtVerify(session, encodedKey, {
- algorithms: ['HS256'],
- })
- return payload
- } catch (error: unknown) {
- console.log('Failed to verify session', error)
+export function createSession(user: User) {
+ const localStorage = globalThis.localStorage;
+ if (localStorage) {
+ localStorage.setItem('session', JSON.stringify(user))
}
}
-export async function createSession(user: User) {
- const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
- const session = await encrypt(user)
- const cookieStore = await cookies()
-
- cookieStore.set('session', session, {
- httpOnly: true,
- secure: true,
- expires: expiresAt,
- sameSite: 'lax',
- path: '/',
- })
-}
-
-export async function updateSession() {
- const session = (await cookies()).get('session')?.value
- const payload = await decrypt(session)
-
- if (!session || !payload) {
- return null
+export function deleteSession() {
+ const localStorage = globalThis.localStorage;
+ if (localStorage) {
+ localStorage.removeItem('session')
}
-
- const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
-
- const cookieStore = await cookies()
- cookieStore.set('session', session, {
- httpOnly: true,
- secure: true,
- expires: expires,
- sameSite: 'lax',
- path: '/',
- })
-}
-
-export async function deleteSession() {
- const cookieStore = await cookies()
- cookieStore.delete('session')
}
-export async function getSessionUser(): Promise<User | null> {
+export function getSessionUser(): User | undefined {
try {
- // Get the session cookie
- const sessionCookie = (await cookies()).get('session')
-
- if (!sessionCookie?.value) {
- return null
+ // Get the session from local storage
+ const sessionCookie = globalThis.localStorage?.getItem('session')
+ if (!sessionCookie) {
+ return
}
- // Verify the JWT token
- try {
- const { payload } = await jwtVerify(sessionCookie.value, encodedKey)
-
- if (!payload.user) {
- return null
- }
-
- return payload.user as User
- } catch (jwtError) {
- console.error("JWT verification failed:", jwtError)
- return null
- }
+ return JSON.parse(sessionCookie) as User
} catch (error) {
+ globalThis.localStorage?.removeItem('session')
+
console.error("Failed to verify session:", error)
- return null
+ return
}
}
\ No newline at end of file
diff --git a/src/middleware.ts b/src/middleware.ts
@@ -1,34 +0,0 @@
-import { NextResponse } from "next/server"
-import type { NextRequest } from "next/server"
-import { getSessionUser } from "./lib/auth"
-
-export async function middleware(request: NextRequest) {
- // Get the pathname of the request
- const path = request.nextUrl.pathname
-
- // Define public paths that don't require authentication
- const isPublicPath = path === "/" || path === "/login" || path === "/register"
-
- // Check if the user is authenticated
- const user = await getSessionUser()
- const isAuthenticated = !!user
-
- // If the path is dashboard and the user is not authenticated, redirect to login
- if (path.startsWith("/dashboard") && !isAuthenticated) {
- const url = new URL("/login", request.url)
- return NextResponse.redirect(url)
- }
-
- // If the user is authenticated and trying to access login/register, redirect to dashboard
- if (isAuthenticated && isPublicPath) {
- const url = new URL("/dashboard", request.url)
- return NextResponse.redirect(url)
- }
-
- return NextResponse.next()
-}
-
-// Configure the middleware to run only on specific paths
-export const config = {
- matcher: ["/", "/login", "/register", "/dashboard/:path*"],
-}