Merge pull request #18 from kleo-dev/auth-ui-patch

Auth UI patch
This commit is contained in:
Klesti Selimaj
2025-10-15 18:46:39 +02:00
committed by GitHub
10 changed files with 109 additions and 60 deletions
+24 -21
View File
@@ -1,12 +1,6 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { StatusCodes } from "http-status-codes"; import { StatusCodes } from "http-status-codes";
import { supabase } from "../supa"; import { supabase } from "../supa";
const SERVER_AUTH_TOKENS = new Map<
string,
{ user_id: string; server_id: string }
>();
/** /**
* GET /api/auth?token=<relayToken>?key=<serverKey> * GET /api/auth?token=<relayToken>?key=<serverKey>
* Called by a (DM node / Server) to verify a temporary relay token. * Called by a (DM node / Server) to verify a temporary relay token.
@@ -16,31 +10,36 @@ export async function GET(req: NextRequest) {
const token = url.searchParams.get("token"); const token = url.searchParams.get("token");
const key = url.searchParams.get("key"); const key = url.searchParams.get("key");
if (!token || !key) { if (!token || !key)
return NextResponse.json( return NextResponse.json(
{ message: "There should be a token and a key parameter" }, { message: "There should be a token and a key parameter" },
{ status: StatusCodes.BAD_REQUEST } { status: StatusCodes.BAD_REQUEST }
); );
}
const { data: server } = await supabase const { data: server } = await supabase
.from("servers") .from("servers")
.select("id, created_at, owner, address") .select("id, created_at, owner, address")
.eq("key", key) .eq("key", key)
.maybeSingle(); .maybeSingle();
if (!server) if (!server)
return NextResponse.json( return NextResponse.json(
{ message: "Key unauthorized" }, { message: "Key unauthorized" },
{ status: StatusCodes.UNAUTHORIZED } { status: StatusCodes.UNAUTHORIZED }
); );
const auth = SERVER_AUTH_TOKENS.get(token); const { data: auth } = await supabase
if (!auth) { .from("server_auth")
.delete()
.eq("key", token)
.select()
.maybeSingle();
if (!auth)
return NextResponse.json( return NextResponse.json(
{ message: "Invalid or expired token" }, { message: "Invalid or expired token" },
{ status: StatusCodes.NOT_FOUND } { status: StatusCodes.NOT_FOUND }
); );
}
if (auth.server_id !== server.id) if (auth.server_id !== server.id)
return NextResponse.json( return NextResponse.json(
@@ -48,8 +47,6 @@ export async function GET(req: NextRequest) {
{ status: StatusCodes.UNAUTHORIZED } { status: StatusCodes.UNAUTHORIZED }
); );
SERVER_AUTH_TOKENS.delete(token);
return NextResponse.json({ message: "ok", ...auth }); return NextResponse.json({ message: "ok", ...auth });
} }
@@ -76,12 +73,10 @@ export async function POST(req: NextRequest) {
{ status: StatusCodes.BAD_REQUEST } { status: StatusCodes.BAD_REQUEST }
); );
const supabaseToken = authHeader.value;
const { const {
data: { user }, data: { user },
error, error,
} = await supabase.auth.getUser(supabaseToken); } = await supabase.auth.getUser(authHeader.value);
if (error || !user) { if (error || !user) {
return NextResponse.json( return NextResponse.json(
@@ -90,12 +85,20 @@ export async function POST(req: NextRequest) {
); );
} }
const relayToken = crypto.randomUUID(); const { data: relayToken, error: errorRelay } = await supabase
.from("server_auth")
SERVER_AUTH_TOKENS.set(relayToken, { .insert({
user_id: user.id, user_id: user.id,
server_id, server_id,
}); })
.select()
.maybeSingle();
return NextResponse.json({ message: "ok", token: relayToken }); if (!relayToken || errorRelay)
return NextResponse.json(
{ message: "Failed to create relay token" },
{ status: StatusCodes.INTERNAL_SERVER_ERROR }
);
return NextResponse.json({ message: "ok", token: relayToken.key });
} }
+14 -2
View File
@@ -84,14 +84,26 @@ export async function GET(req: NextRequest) {
if (!user_id) if (!user_id)
return NextResponse.json( return NextResponse.json(
{ message: "Invalid token" }, { message: "Invalid token" },
{ status: StatusCodes.NOT_FOUND } { status: StatusCodes.BAD_REQUEST }
); );
const { data: profile } = await supabase const { data: profile, error } = await supabase
.from("profiles") .from("profiles")
.select("id, username, display_name, avatar_url, node_address") .select("id, username, display_name, avatar_url, node_address")
.eq("id", user_id) .eq("id", user_id)
.maybeSingle(); .maybeSingle();
if (error)
return NextResponse.json(
{ message: error.message },
{ status: StatusCodes.INTERNAL_SERVER_ERROR }
);
if (!profile)
return NextResponse.json(
{ message: "Profile not found" },
{ status: StatusCodes.NOT_FOUND }
);
return NextResponse.json({ message: "ok", ...profile }); return NextResponse.json({ message: "ok", ...profile });
} }
+4 -3
View File
@@ -9,9 +9,6 @@ import { StringMap } from "@/types/typeUtils";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { useMessages } from "@/hooks/use-messages"; import { useMessages } from "@/hooks/use-messages";
// In this testing instance we are assuming that the target's node is localhost
// To make this functional we need to add a settings feature to the api
export default function DMs() { export default function DMs() {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const wsRef = useRef<WebSocket | null>(null); const wsRef = useRef<WebSocket | null>(null);
@@ -19,9 +16,12 @@ export default function DMs() {
const addMessage = useMessages((s) => s.addMessage); const addMessage = useMessages((s) => s.addMessage);
const [userList, setUserList] = useState<StringMap<UserProfile>>({}); const [userList, setUserList] = useState<StringMap<UserProfile>>({});
const [user, setUser] = useState<UserProfile | undefined>(); const [user, setUser] = useState<UserProfile | undefined>();
const [sidebarOpen, setSidebarOpen] = useState(false);
return ( return (
<AppSidebar <AppSidebar
open={sidebarOpen}
setOpen={setSidebarOpen}
addMessage={addMessage} addMessage={addMessage}
wsRef={wsRef} wsRef={wsRef}
userList={userList} userList={userList}
@@ -31,6 +31,7 @@ export default function DMs() {
messages={messages} messages={messages}
> >
<MessageBox <MessageBox
toggleSidebar={() => setSidebarOpen(true)}
channelName={userList[id]?.display_name || id} channelName={userList[id]?.display_name || id}
userList={userList} userList={userList}
setUserList={setUserList} setUserList={setUserList}
+3
View File
@@ -14,9 +14,12 @@ export default function Chat() {
const [userList, setUserList] = useState<StringMap<UserProfile>>({}); const [userList, setUserList] = useState<StringMap<UserProfile>>({});
const messages = useMessages((s) => s.messages); const messages = useMessages((s) => s.messages);
const addMessage = useMessages((s) => s.addMessage); const addMessage = useMessages((s) => s.addMessage);
const [sidebarOpen, setSidebarOpen] = useState(true);
return ( return (
<AppSidebar <AppSidebar
open={sidebarOpen}
setOpen={setSidebarOpen}
wsRef={wsRef} wsRef={wsRef}
userList={userList} userList={userList}
setUserList={setUserList} setUserList={setUserList}
+4
View File
@@ -18,6 +18,7 @@ export default function Server() {
const [userList, setUserList] = useState<StringMap<UserProfile>>({}); const [userList, setUserList] = useState<StringMap<UserProfile>>({});
const messages = useMessages((s) => s.messages); const messages = useMessages((s) => s.messages);
const addMessage = useMessages((s) => s.addMessage); const addMessage = useMessages((s) => s.addMessage);
const [sidebarOpen, setSidebarOpen] = useState(false);
useEffect(() => { useEffect(() => {
if (!ip) return; if (!ip) return;
@@ -26,6 +27,8 @@ export default function Server() {
return ( return (
<AppSidebar <AppSidebar
open={sidebarOpen}
setOpen={setSidebarOpen}
wsRef={wsNodeRef} wsRef={wsNodeRef}
addMessage={() => {}} addMessage={() => {}}
userList={userList} userList={userList}
@@ -33,6 +36,7 @@ export default function Server() {
server={server} server={server}
> >
<MessageBox <MessageBox
toggleSidebar={() => setSidebarOpen(true)}
channelName="General" channelName="General"
userList={userList} userList={userList}
setUserList={setUserList} setUserList={setUserList}
+11 -6
View File
@@ -10,7 +10,7 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "./ui/dropdown-menu"; } from "./ui/dropdown-menu";
import { SmilePlusIcon } from "lucide-react"; import { ChevronLeft, ChevronLeftIcon, SmilePlusIcon } from "lucide-react";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { format, isToday, isYesterday } from "date-fns"; import { format, isToday, isYesterday } from "date-fns";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
@@ -38,7 +38,7 @@ function MessageContainer({
[message.from]: res.data as UserProfile, [message.from]: res.data as UserProfile,
})) }))
) )
.catch(console.error); .catch(() => {});
}, [message.from, setUserList]); }, [message.from, setUserList]);
return ( return (
@@ -144,23 +144,28 @@ export default function MessageBox({
setUserList, setUserList,
sendMessage, sendMessage,
channelName, channelName,
toggleSidebar,
}: { }: {
messages: Message[]; messages: Message[];
userList: NumberMap<UserProfile>; userList: NumberMap<UserProfile>;
setUserList: React.Dispatch<React.SetStateAction<NumberMap<UserProfile>>>; setUserList: React.Dispatch<React.SetStateAction<NumberMap<UserProfile>>>;
sendMessage: (m: string) => void; sendMessage: (m: string) => void;
channelName?: string; channelName?: string;
toggleSidebar: () => void;
}) { }) {
const [text, setText] = useState(""); const [text, setText] = useState("");
return ( return (
<div className="h-screen w-full flex flex-col pb-5 pl-5 gap-5"> <div className="h-full w-full flex flex-col pb-5 pl-5 gap-5">
{channelName && ( {channelName && (
<header className="h-12 py-4 flex items-center border-b text-sm font-semibold"> <header className="h-12 py-4 flex items-center border-b text-sm font-semibold">
<span onClick={toggleSidebar} className="cursor-pointer flex">
<ChevronLeftIcon className="w-5 h-5" />
{channelName} {channelName}
</span>
</header> </header>
)} )}
<div className="w-full flex-1 overflow-y-scroll flex gap-2 flex-col-reverse pr-5"> <div className="w-full h-max flex-1 overflow-y-scroll flex gap-2 flex-col-reverse pr-5">
{messages.map((msg) => ( {messages.map((msg) => (
<MessageContainer <MessageContainer
key={msg.id} key={msg.id}
@@ -170,7 +175,7 @@ export default function MessageBox({
/> />
))} ))}
</div> </div>
<div className="w-full flex gap-3 pr-5"> <footer className="w-full flex gap-3 pr-5 mt-auto">
<Input <Input
className="" className=""
placeholder="Type a message..." placeholder="Type a message..."
@@ -197,7 +202,7 @@ export default function MessageBox({
/> />
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </footer>
</div> </div>
); );
} }
+33 -12
View File
@@ -35,6 +35,7 @@ import SettingsDialog from "./settings/SettingsDialog";
import Link from "next/link"; import Link from "next/link";
import axios from "axios"; import axios from "axios";
import { get, Response } from "@/lib/request"; import { get, Response } from "@/lib/request";
import { cn } from "@/lib/utils";
export default function AppSidebar({ export default function AppSidebar({
children, children,
@@ -45,6 +46,8 @@ export default function AppSidebar({
server, server,
messages, messages,
addMessage, addMessage,
open,
setOpen,
}: Readonly<{ }: Readonly<{
wsRef: React.RefObject<WebSocket | null>; wsRef: React.RefObject<WebSocket | null>;
addMessage: (msg: Message) => void; addMessage: (msg: Message) => void;
@@ -56,6 +59,8 @@ export default function AppSidebar({
children?: React.ReactNode; children?: React.ReactNode;
setUser?: React.Dispatch<React.SetStateAction<UserProfile | undefined>>; setUser?: React.Dispatch<React.SetStateAction<UserProfile | undefined>>;
server?: Server | undefined; server?: Server | undefined;
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
}>) { }>) {
const [servers, setServers] = useState<[string, string][]>([]); const [servers, setServers] = useState<[string, string][]>([]);
const [newServer, setNewServer] = useState({ name: "", ip: "" }); const [newServer, setNewServer] = useState({ name: "", ip: "" });
@@ -64,7 +69,11 @@ export default function AppSidebar({
const [userList, setUserList] = useState<StringMap<UserProfile>>({}); const [userList, setUserList] = useState<StringMap<UserProfile>>({});
const dms = Array.from( const dms = Array.from(
new Set(messages?.flatMap((item) => [item.from, item.channel_id]) || []) new Set(
messages
?.flatMap((item) => [item.from, item.channel_id])
.filter((item) => item !== user?.id) || []
)
); );
useEffect(() => { useEffect(() => {
@@ -90,6 +99,7 @@ export default function AppSidebar({
label: "View", label: "View",
onClick: () => router.push(`/chat/${m.from}`), onClick: () => router.push(`/chat/${m.from}`),
}, },
position: "top-right",
} }
); );
@@ -118,8 +128,13 @@ export default function AppSidebar({
}; };
return ( return (
<div className="flex h-screen"> <div className="flex h-svh md:h-screen">
<div className="h-screen flex border-r"> <div
className={cn(
"border-r transition-all duration-300 ease-in-out overflow-hidden flex",
open ? "w-5xl md:w-md" : "w-0 md:w-md"
)}
>
<div className="w-16 flex flex-col items-center gap-4 py-4 bg-muted border-r"> <div className="w-16 flex flex-col items-center gap-4 py-4 bg-muted border-r">
<Button <Button
onClick={() => router.push(`/chat`)} onClick={() => router.push(`/chat`)}
@@ -208,7 +223,7 @@ export default function AppSidebar({
</div> </div>
{/* Right column */} {/* Right column */}
<div className="w-64 flex flex-col bg-card"> <div className="w-full flex flex-col bg-card">
{server ? ( {server ? (
<div className="p-3 border-b flex items-center gap-2"> <div className="p-3 border-b flex items-center gap-2">
<h2>{server.name}</h2> <h2>{server.name}</h2>
@@ -261,9 +276,19 @@ export default function AppSidebar({
</footer> </footer>
</div> </div>
</div> </div>
{open ? (
<div
className="w-screen h-full overflow-x-hidden"
onClick={() => setOpen(false)}
>
<div className="overflow-x-hidden w-screen min-w-max flex-shrink-0">
{children} {children}
</div> </div>
</div>
) : (
children
)}
</div>
); );
} }
@@ -280,12 +305,10 @@ function DMItem({
status: string; status: string;
settings?: boolean; settings?: boolean;
}) { }) {
const router = useRouter();
return ( return (
<Link <Link
href={`/chat/${id}`} href={settings ? "" : `/chat/${id}`}
className="p-2 flex flex-row items-center gap-2 cursor-pointer hover:bg-accent" className="p-2 flex flex-row items-center gap-2 cursor-pointer hover:bg-accent rounded-md"
> >
<ProfilePicture name={name} url={avatar} /> <ProfilePicture name={name} url={avatar} />
<div className="flex flex-col"> <div className="flex flex-col">
@@ -338,7 +361,5 @@ function getUser(
get(`/api/profile/?id=${id}`, onResponse) get(`/api/profile/?id=${id}`, onResponse)
?.then(onResponse) ?.then(onResponse)
.catch((e) => { .catch((e) => {});
console.error(e);
});
} }
+3 -1
View File
@@ -5,6 +5,7 @@ import Cookies from "js-cookie";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { get } from "@/lib/request"; import { get } from "@/lib/request";
import { toast } from "sonner";
export interface UserProfile extends ProfileSettings { export interface UserProfile extends ProfileSettings {
id: string; id: string;
@@ -24,8 +25,9 @@ export default function useUser() {
try { try {
setUser((await get("/api/profile")).data); setUser((await get("/api/profile")).data);
} catch { } catch (e: any) {
router.push("/login"); router.push("/login");
toast.error(`Error: ${e}`);
} }
} }
+3 -5
View File
@@ -2,6 +2,7 @@
import { defaultSettings, getClientSettings } from "@/lib/clientSettings"; import { defaultSettings, getClientSettings } from "@/lib/clientSettings";
import { ProfileSettings, ClientSettings } from "@/types/settings"; import { ProfileSettings, ClientSettings } from "@/types/settings";
import axios from "axios";
import { Dispatch, SetStateAction, useEffect, useState } from "react"; import { Dispatch, SetStateAction, useEffect, useState } from "react";
export function useClientSettings(): [ export function useClientSettings(): [
@@ -31,11 +32,8 @@ export function useProfileSettings(): [
useEffect(() => { useEffect(() => {
async function fetchProfileSettings() { async function fetchProfileSettings() {
try { try {
const res = await fetch("/api/profile"); const res = await axios.get("/api/profile");
if (res.ok) { setSettings(res.data as ProfileSettings);
const data = (await res.json()) as ProfileSettings;
setSettings(data);
}
} catch (error) { } catch (error) {
console.error("Failed to fetch profile settings:", error); console.error("Failed to fetch profile settings:", error);
} }
+7 -7
View File
@@ -26,13 +26,6 @@ export default async function auth(
ws.onopen = () => { ws.onopen = () => {
console.log("Connected to WebSocket:", ip); console.log("Connected to WebSocket:", ip);
ws.send(
JSON.stringify({
version: "0.0.1",
auth_token: server_auth,
last_message: lastMessage || 0,
})
);
}; };
ws.onmessage = (m) => { ws.onmessage = (m) => {
@@ -41,6 +34,13 @@ export default async function auth(
if (data.version) { if (data.version) {
setServer({ channels: [], ...data }); setServer({ channels: [], ...data });
ws.send(
JSON.stringify({
version: "0.0.1",
auth_token: server_auth,
last_message: lastMessage || 0,
})
);
return; return;
} }