commit 393e6adfac6f4129ddd1643c9843f188bf3eed82
parent dfb79283f199386e71afbf311a2ca83d648b7448
Author: MTRNord <mtrnord1@gmail.com>
Date: Tue, 29 Apr 2025 13:00:20 +0200
Implement the actual login code
Signed-off-by: MTRNord <mtrnord1@gmail.com>
Diffstat:
12 files changed, 499 insertions(+), 241 deletions(-)
diff --git a/src/app/api/session/route.ts b/src/app/api/session/route.ts
@@ -0,0 +1,46 @@
+import { cookies } from "next/headers";
+import { NextResponse } from "next/server";
+
+export async function POST(req: Request) {
+ const { openidToken } = 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 });
+ }
+
+ const user = {
+ id: "user_" + Math.random().toString(36).substring(2, 9),
+ isAdmin: false,
+ };
+
+ const response = NextResponse.json(user);
+ response.cookies.set("session", JSON.stringify(user), { httpOnly: true, path: "/" });
+ return response;
+}
+
+export async function GET() {
+ const session = (await cookies()).get("session")?.value;
+
+ if (!session) {
+ return NextResponse.json({ error: "No active session" }, { status: 401 });
+ }
+
+ return NextResponse.json(JSON.parse(session));
+}
+
+export async function DELETE() {
+ const response = NextResponse.json({}, { status: 204 });
+ 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/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 { redirect, useSearchParams } from "next/navigation"
+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";
@@ -16,7 +16,6 @@ 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[] = [
@@ -65,15 +64,6 @@ export default function AnalyticsDashboard() {
const teamIdParam = searchParams.get("team")
const [selectedTeam, setSelectedTeam] = useState(mockTeams.find((t) => t.id === teamIdParam) || mockTeams[0])
- const { user } = useSession()
-
-
- useEffect(() => {
- // Check if the user is logged in
- if (!user) {
- redirect("/login")
- }
- }, [user])
// Update selected team when URL param changes
useEffect(() => {
diff --git a/src/app/dashboard/bans/page.tsx b/src/app/dashboard/bans/page.tsx
@@ -8,9 +8,8 @@ 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 { redirect, useSearchParams } from "next/navigation";
+import { useSearchParams } from "next/navigation";
import TabNavigation from "../../../components/dashboard/tab-navigation";
-import { useSession } from "@/contexts/session-context";
export default function Bans() {
@@ -18,15 +17,6 @@ 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()
-
-
- useEffect(() => {
- // Check if the user is logged in
- if (!user) {
- redirect("/login")
- }
- }, [user])
// Update selected team when URL param changes
useEffect(() => {
diff --git a/src/app/dashboard/overview/page.tsx b/src/app/dashboard/overview/page.tsx
@@ -5,21 +5,12 @@ import AddProtectedRoom from "../../../components/modals/add-protected-room"
import ProtectedRomsList from "../../../components/dashboard/protected-rooms-list"
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"
-import { useEffect } from "react"
+import { useSearchParams } from "next/navigation"
export default function OverviewPage() {
const searchParams = useSearchParams()
- const { user } = useSession()
const teamIdParam = searchParams.get("team");
- useEffect(() => {
- // Check if the user is logged in
- if (!user) {
- redirect("/login")
- }
- }, [user])
const selectedTeam = mockTeams.find((t) => t.id === teamIdParam) || mockTeams[0];
const reports = mockReports.filter((report) => report.teamId === selectedTeam.id);
diff --git a/src/app/dashboard/reports/page.tsx b/src/app/dashboard/reports/page.tsx
@@ -18,23 +18,13 @@ import {
DialogTrigger,
} from "@/components/ui/dialog"
import { mockReports, mockTeams } from "../mockData";
-import { redirect, useSearchParams } from "next/navigation"
+import { 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()
-
-
- useEffect(() => {
- // Check if the user is logged in
- if (!user) {
- redirect("/login")
- }
- }, [user])
// Update selected team when URL param changes
useEffect(() => {
diff --git a/src/app/dashboard/settings/page.tsx b/src/app/dashboard/settings/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useEffect, useState } from "react"
+import { useState } from "react"
import { UserPlus, Settings, Trash2, LogOut } from "lucide-react"
import { Button } from "@/components/ui/button"
@@ -26,26 +26,18 @@ import {
} from "@/components/ui/dropdown-menu"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
-import { redirect, useSearchParams } from "next/navigation"
+import { 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()
- useEffect(() => {
- // Check if the user is logged in
- if (!user) {
- redirect("/login")
- }
- }, [user])
// TODO: Make this actual pages so we can use more ssr
const [activeTab, setActiveTab] = useState("members")
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx
@@ -54,12 +54,13 @@ export default function LoginPage() {
setErrorMessage("")
try {
- await login(matrixId)
+ await login(matrixId, password)
} catch (err) {
console.error("Login failed:", err)
setErrorMessage("Login failed. Please check your credentials.")
+ setStep("error")
}
- }, [login, matrixId])
+ }, [login, matrixId, password])
return (
<div className="flex min-h-screen flex-col bg-black text-white">
@@ -162,7 +163,7 @@ export default function LoginPage() {
setMatrixId("")
setPassword("")
}}
- disabled={isLoading}
+ disabled={isLoading || loginLoading}
>
Cancel
</Button>
@@ -174,7 +175,7 @@ export default function LoginPage() {
</form>
)}
- {(isLoading || loginLoading) && (
+ {((isLoading || loginLoading) && discoveryStatus !== "loading") && (
<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>
@@ -182,6 +183,12 @@ export default function LoginPage() {
</div>
)}
+ {((isLoading || loginLoading) && discoveryStatus === "loading") && (
+ <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">Discovering the Homeserver URL...</p>
+ </div>
+ )}
{step === "error" && (
<div className="rounded-md bg-red-900/20 border border-red-800 p-4 text-center">
<p className="text-red-300 font-medium">Login Failed</p>
@@ -189,7 +196,7 @@ export default function LoginPage() {
<Button
variant="outline"
className="mt-4 border-red-500 text-red-400 hover:bg-red-950 hover:text-red-300"
- onClick={() => setStep("initial")}
+ onClick={() => { setStep("initial"); setMatrixId(""); setPassword(""); }}
>
Try Again
</Button>
@@ -202,7 +209,7 @@ export default function LoginPage() {
<Button
className="w-full bg-purple-600 text-white hover:bg-purple-700"
onClick={startMatrixAuth}
- disabled={!matrixId}
+ disabled={!matrixId || isLoading || loginLoading}
>
Continue
<ArrowRight className="ml-2 h-4 w-4" />
@@ -218,6 +225,6 @@ export default function LoginPage() {
</CardFooter>
</Card>
</main>
- </div>
+ </div >
)
}
diff --git a/src/app/register/page.tsx b/src/app/register/page.tsx
@@ -1,39 +1,64 @@
"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 RegisterPage() {
- const { register, isLoading } = useSession()
+ const { isLoading, register, discoveryStatus, homeserverUrl, discoverHomeserver, user } = useSession();
+ const [loginLoading, setLoginLoading] = useState(isLoading)
const [matrixId, setMatrixId] = useState("")
const [step, setStep] = useState<"initial" | "login" | "error">("initial")
+ const [password, setPassword] = useState("")
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) => {
- try {
- // Use the register function from session context
- await register(matrixId)
- } catch (error) {
- console.error("Registration failed", error)
- setErrorMessage(error instanceof Error ? error.message : "Registration failed")
+ 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 {
+ await register(matrixId, password)
+ } catch (err) {
+ console.error("Login failed:", err)
+ setErrorMessage("Login failed. Please check your credentials.")
}
- }
+ }, [register, matrixId, password])
return (
<div className="flex min-h-screen flex-col bg-black text-white">
@@ -71,10 +96,86 @@ export default function RegisterPage() {
)}
{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) && discoveryStatus !== "loading") && (
<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">Registering your account...</p>
@@ -82,6 +183,13 @@ export default function RegisterPage() {
</div>
)}
+ {((isLoading || loginLoading) && discoveryStatus === "loading") && (
+ <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">Discovering the Homeserver URL...</p>
+ </div>
+ )}
+
{step === "error" && (
<div className="rounded-md bg-red-900/20 border border-red-800 p-4 text-center">
<p className="text-red-300 font-medium">Registration Failed</p>
diff --git a/src/contexts/session-context.tsx b/src/contexts/session-context.tsx
@@ -1,196 +1,246 @@
-"use client"
+"use client";
-import { createContext, useContext, useState, type ReactNode } from "react"
-import { useRouter } from "next/navigation"
-import { createSession, deleteSession, getSessionUser, User } from "@/lib/auth"
+import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from "react";
+import { useRouter } from "next/navigation";
+import { generateOpenIDToken, login as realLogin, logout as realLogout } from "@/lib/clientSideAuth";
+import { User } from "@/lib/auth";
type SessionContextType = {
- 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
- }
-}
+ user?: User;
+ isLoading: boolean;
+ isAuthenticated: boolean;
+ discoveryStatus: "idle" | "loading" | "success" | "error";
+ homeserverUrl?: string;
+ login: (matrixId: string, password: string) => Promise<void>;
+ register: (matrixId: string, password: string) => Promise<void>;
+ logout: () => Promise<void>;
+ discoverHomeserver: (matrixId: string) => Promise<void>;
+ fetchWithTokenRefresh: (url: string, options?: RequestInit) => Promise<Response>;
+};
-const SessionContext = createContext<SessionContextType | undefined>(undefined)
+const SessionContext = createContext<SessionContextType | undefined>(undefined);
export function SessionProvider({ children }: { children: ReactNode }) {
- 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()
+ 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 router = useRouter();
+
+ // Restore session from cookies on initial load
+ useEffect(() => {
+ const fetchSession = async () => {
+ try {
+ const response = await fetch("/api/session");
+ if (response.ok) {
+ const userData = await response.json();
+ setUser(userData);
+ }
+ } catch (error) {
+ console.error("Failed to restore session:", error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ fetchSession();
+ }, []);
- const discoverHomeserver = async (matrixId: string) => {
- if (matrixId === "") {
- return
+ const refreshOpenIDToken = useCallback(async () => {
+ if (!homeserverUrl || !accessToken || !user) {
+ throw new Error("Missing required information to refresh OpenID token.");
}
- setDiscoveryStatus("loading")
+
+ 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 }),
+ });
+
+ if (!response.ok) {
+ throw new Error("Failed to update session with new OpenID token.");
+ }
+
+ const userData = await response.json();
+ setUser(userData);
+ } catch (error) {
+ console.error("Failed to refresh OpenID token:", error);
+ throw error;
+ }
+ }, [homeserverUrl, accessToken, user]);
+
+ // Automatically refresh OpenID token before it expires
+ useEffect(() => {
+ if (openidExpiration) {
+ const now = Date.now();
+ const refreshTime = openidExpiration - 60 * 1000; // Refresh 1 minute before expiration
+
+ if (refreshTime > now) {
+ const timeout = setTimeout(() => {
+ refreshOpenIDToken().catch((err) => {
+ console.error("Failed to refresh OpenID token:", err);
+ });
+ }, refreshTime - now);
+
+ return () => clearTimeout(timeout);
+ }
+ }
+ }, [openidExpiration, refreshOpenIDToken]);
+
+ const discoverHomeserver = async (matrixId: string) => {
+ setDiscoveryStatus("loading");
if (!matrixId.includes(":")) {
- setDiscoveryStatus("error")
- throw new Error("Invalid Matrix ID format. Please include a domain (e.g., @user:domain).")
+ 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()
+
+ const serverName = matrixId.split(":").pop();
+
try {
- // Try to fetch well-known data
- const wellKnownUrl = `https://${serverName}/.well-known/matrix/client`
+ const url = new URL("/.well-known/matrix/client", `https://${serverName}`);
+ const response = await fetch(url);
+ if (response.ok) {
+ // Check if the response is empty
+ if (!response.body) {
+ setHomeserverUrl(`https://${serverName}`);
+ setDiscoveryStatus("success");
+ return;
+ }
- 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)
+ const data = await response.json();
+ if (data["m.homeserver"]?.base_url) {
+ // Check if the base_url is a valid URL by checking /_matrix/client/versions on the endpoint
+ const url = new URL("/_matrix/client/versions", data["m.homeserver"].base_url)
+ const versionsResponse = await fetch(url);
+ console.log("Versions response:", versionsResponse.status);
+ if (versionsResponse.ok) {
+ setHomeserverUrl(data["m.homeserver"].base_url);
+ setDiscoveryStatus("success");
return;
+ } else {
+ setDiscoveryStatus("error");
+ throw new Error("Homeserver did not respond successfully to versions request.");
}
}
- } catch (e) {
- console.warn("Well-known discovery failed, falling back to direct server", e)
}
+ setHomeserverUrl(`https://${serverName}`);
+ setDiscoveryStatus("success");
+ } catch (error: unknown) {
+ console.warn("Failed to discover homeserver:", error);
+ setHomeserverUrl(`https://${serverName}`);
+ setDiscoveryStatus("success");
+ }
+ };
- // 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, password: string) => {
+ setIsLoading(true);
+ if (!homeserverUrl) {
+ throw new Error("Homeserver URL is not set. Please discover your homeserver first.");
}
- }
- const login = async (matrixId: string) => {
- setIsLoading(true)
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)
-
- // 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,
- })
- router.replace("/dashboard")
- return;
- } catch (err) {
- console.error("Matrix login failed:", err)
- throw err instanceof Error ? err.message : "Authentication failed"
+ // Step 1: Login to Matrix and get access token
+ const accessToken = await realLogin(homeserverUrl, matrixId, password);
+ setAccessToken(accessToken);
+
+ // Step 2: Generate OpenID token
+ const { access_token: openidToken, expires_in } = await generateOpenIDToken(homeserverUrl, matrixId, accessToken);
+ setOpenIDExpiration(expires_in);
+
+ // Step 3: Send OpenID token to the server for validation and session creation
+ const response = await fetch("/api/session", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ openidToken }),
+ });
+
+ if (!response.ok) {
+ throw new Error("Failed to create session on the server.");
+ }
+
+ const userData = await response.json();
+ setUser(userData);
+ router.replace("/dashboard");
+ } catch (error) {
+ console.error("Login failed:", error);
+ throw error;
} finally {
- setIsLoading(false)
+ setIsLoading(false);
+ }
+ };
+
+ const register = async (matrixId: string, password: string) => {
+ setIsLoading(true);
+ if (!homeserverUrl) {
+ throw new Error("Homeserver URL is not set. Please discover your homeserver first.");
}
- }
- const register = async (matrixId: string) => {
- setIsLoading(true)
try {
- const response = await fetch("/api/auth/register", {
+
+ // Step 1: Login to Matrix and get access token
+ const accessToken = await realLogin(homeserverUrl, matrixId, password);
+ setAccessToken(accessToken);
+
+ // Step 2: Generate OpenID token
+ const { access_token: openidToken, expires_in } = await generateOpenIDToken(homeserverUrl, matrixId, accessToken);
+ setOpenIDExpiration(expires_in);
+
+ // Step 3: Send OpenID token to the server for validation and session creation
+ const response = await fetch("/api/session", {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ matrixId }),
- })
+ body: JSON.stringify({ openidToken }),
+ });
if (!response.ok) {
- throw new Error("Registration failed")
+ throw new Error("Failed to create session on the server.");
}
-
- const data = await response.json()
- setUser(data.user)
- router.replace("/dashboard")
+ const userData = await response.json();
+ setUser(userData);
+ router.replace("/dashboard");
} catch (error) {
- console.error("Registration failed:", error)
- throw error
+ console.error("Login failed:", error);
+ throw error;
} finally {
- setIsLoading(false)
+ setIsLoading(false);
}
- }
+ };
const logout = async () => {
try {
- setUser(undefined)
- deleteSession()
- router.push("/")
+ await fetch("/api/session", { method: "DELETE" });
+
+ if (homeserverUrl && accessToken) {
+ await realLogout(homeserverUrl, accessToken);
+ }
+
+ setUser(undefined);
+ setAccessToken(undefined);
+ router.push("/");
} catch (error) {
- console.error("Logout failed:", error)
+ console.error("Logout failed:", error);
}
- }
+ };
+
+ const fetchWithTokenRefresh = async (url: string, options: RequestInit = {}) => {
+ const response = await fetch(url, options);
+
+ if (response.status === 401) {
+ // Token expired, refresh it
+ await refreshOpenIDToken();
+
+ // Retry the request
+ return fetch(url, options);
+ }
+
+ return response;
+ };
return (
<SessionContext.Provider
@@ -200,22 +250,22 @@ export function SessionProvider({ children }: { children: ReactNode }) {
homeserverUrl,
discoveryStatus,
isAuthenticated: !!user,
- token,
login,
register,
logout,
discoverHomeserver,
+ fetchWithTokenRefresh,
}}
>
{children}
</SessionContext.Provider>
- )
+ );
}
export function useSession() {
- const context = useContext(SessionContext)
+ const context = useContext(SessionContext);
if (context === undefined) {
- throw new Error("useSession must be used within a SessionProvider")
+ throw new Error("useSession must be used within a SessionProvider");
}
- return context
-}
+ return context;
+}
+\ No newline at end of file
diff --git a/src/lib/auth.ts b/src/lib/auth.ts
@@ -6,6 +6,7 @@ export type User = {
displayName: string
avatarUrl?: string
isAdmin: boolean
+ token: string
}
export function createSession(user: User) {
diff --git a/src/lib/clientSideAuth.ts b/src/lib/clientSideAuth.ts
@@ -0,0 +1,72 @@
+export async function login(homeserverUrl: string, username: string, password: string): Promise<string> {
+ console.log(`Authenticating with ${homeserverUrl}`);
+
+ const url = new URL("/_matrix/client/v3/login", homeserverUrl);
+ const loginResponse = await fetch(url, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ type: 'm.login.password',
+ identifier: {
+ type: 'm.id.user',
+ user: username,
+ },
+ initial_device_display_name: "Draupnir4All Web",
+ password,
+ }),
+ });
+
+ const loginData = await loginResponse.json();
+ if (!loginData.access_token) {
+ throw new Error(loginData.error || 'Login failed');
+ }
+
+ return loginData.access_token;
+}
+
+interface OpenIDTokenResponse {
+ access_token: string;
+ expires_in: number;
+ matrix_server_name: string;
+ token_type: string;
+ error?: string;
+}
+
+export async function generateOpenIDToken(homeserverUrl: string, username: string, accessToken: string): Promise<OpenIDTokenResponse> {
+ const url = new URL("/_matrix/client/v3/user/" + encodeURIComponent(username) + "/openid/request_token", homeserverUrl);
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${accessToken}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ type: 'm.login.openid',
+ }),
+ });
+
+ const data = await response.json();
+ if (!data.access_token) {
+ throw new Error(data.error || 'Failed to generate OpenID token');
+ }
+
+ const dataAsResponse = data as OpenIDTokenResponse;
+
+ return dataAsResponse;
+}
+
+export async function logout(homeserverUrl: string, accessToken: string): Promise<void> {
+ console.log(`Logging out with ${homeserverUrl}`);
+
+ const url = new URL("/_matrix/client/v3/logout", homeserverUrl);
+ try {
+ await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${accessToken}`
+ },
+ });
+ } catch (error: unknown) {
+ console.warn("Failed to logout:", error);
+ }
+}
+\ No newline at end of file
diff --git a/src/middleware.ts b/src/middleware.ts
@@ -0,0 +1,17 @@
+import { NextResponse } from "next/server";
+import type { NextRequest } from "next/server";
+
+export function middleware(req: NextRequest) {
+ const session = req.cookies.get("session");
+
+ // 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));
+ }
+
+ return NextResponse.next();
+}
+
+export const config = {
+ matcher: ["/dashboard/:path*"], // Add other protected routes here
+};
+\ No newline at end of file