From 24aefe78388b34bf25b38d5f54ab2b9e73c7d523 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sat, 27 Sep 2025 09:17:22 +0200 Subject: [PATCH] Login and working api with simple chat --- src/app/api/user/route.ts | 69 ++++++++++++++++++++++++++------ src/app/login/page.tsx | 39 ++++++++++++++++-- src/app/server/[id]/page.tsx | 75 +++++++++++++++++++++-------------- src/components/MessageBox.tsx | 28 ++++++++++--- src/hooks/get-user.ts | 56 ++++++++++++++++---------- src/lib/api-wrapper/auth.ts | 10 ----- src/lib/api-wrapper/user.ts | 16 -------- 7 files changed, 194 insertions(+), 99 deletions(-) delete mode 100644 src/lib/api-wrapper/auth.ts delete mode 100644 src/lib/api-wrapper/user.ts diff --git a/src/app/api/user/route.ts b/src/app/api/user/route.ts index dbbfc46..a3447e4 100644 --- a/src/app/api/user/route.ts +++ b/src/app/api/user/route.ts @@ -12,7 +12,10 @@ export async function POST(req: NextRequest) { ); if (user_.rows.length > 0) { - return NextResponse.json({ message: "Username already used" }, { status: StatusCodes.CONFLICT }); + return NextResponse.json( + { message: "Username already used" }, + { status: StatusCodes.CONFLICT } + ); } const user_email = await pool.query( @@ -21,16 +24,53 @@ export async function POST(req: NextRequest) { ); if (user_email.rows.length > 0) { - return NextResponse.json({ message: "Email already used" }, { status: StatusCodes.CONFLICT }); + return NextResponse.json( + { message: "Email already used" }, + { status: StatusCodes.CONFLICT } + ); } // TODO: Hash the password - const result = await pool.query("INSERT INTO users (username, email, password_hash) VALUES ($1, $2, $3) RETURNING id;", [username, email, password]); + const result = await pool.query( + "INSERT INTO users (username, email, password_hash) VALUES ($1, $2, $3) RETURNING id;", + [username, email, password] + ); const token = crypto.randomUUID(); CLIENT_AUTH_TOKENS[token] = parseInt(result.rows[0].id); - return NextResponse.json({ message: "ok", token, user_id: result.rows[0].id }); + return NextResponse.json({ + message: "ok", + token, + user_id: result.rows[0].id, + }); +} + +export async function PUT(req: NextRequest) { + const { username, password } = await req.json(); + + if (!username || !password) + return NextResponse.json( + { message: "Body must have username and password" }, + { status: StatusCodes.BAD_REQUEST } + ); + + const user = await pool.query( + "SELECT id, username, email FROM users WHERE password_hash = $2 AND username = $1 OR email = $1;", + [username, password] + ); + + if (user.rowCount === 0) + return NextResponse.json( + { message: "Invalid credentials" }, + { status: StatusCodes.UNAUTHORIZED } + ); + + const token = crypto.randomUUID(); + + CLIENT_AUTH_TOKENS[token] = parseInt(user.rows[0].id); + + return NextResponse.json({ message: "ok", token }); } export async function GET(req: NextRequest) { @@ -39,12 +79,18 @@ export async function GET(req: NextRequest) { const token = url.searchParams.get("token"); if (!query_id && !token) - return NextResponse.json({ message: "Query arg should be token or id" }, { status: StatusCodes.BAD_REQUEST }); + return NextResponse.json( + { message: "Query arg should be token or id" }, + { status: StatusCodes.BAD_REQUEST } + ); else { const id = query_id ? query_id : token ? CLIENT_AUTH_TOKENS[token] : null; if (!id) - return NextResponse.json({ message: "Invalid token" }, { status: StatusCodes.NOT_FOUND }); + return NextResponse.json( + { message: "Invalid token" }, + { status: StatusCodes.NOT_FOUND } + ); const user = await pool.query( "SELECT id, username, email FROM users WHERE id = $1;", @@ -57,13 +103,10 @@ export async function GET(req: NextRequest) { export async function DELETE(req: NextRequest) { let { username } = await req.json(); - - await pool.query( - "DELETE FROM users WHERE username = $1;", - [username] - ); - + + await pool.query("DELETE FROM users WHERE username = $1;", [username]); + return NextResponse.json({ message: "ok" }); } -initDb(); \ No newline at end of file +initDb(); diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 95e3ac5..c672cb8 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,7 +1,40 @@ +"use client"; + +import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import axios from "axios"; +import { useState } from "react"; +import Cookies from "js-cookie"; +import { redirect } from "next/navigation"; export default function Login() { - return
- + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + + return ( +
+
+

Login

+ + Username + setUsername(e.target.value)} /> + + + Password + setPassword(e.target.value)} /> + + + +
-} \ No newline at end of file + ); +} diff --git a/src/app/server/[id]/page.tsx b/src/app/server/[id]/page.tsx index a93b503..7f0b851 100644 --- a/src/app/server/[id]/page.tsx +++ b/src/app/server/[id]/page.tsx @@ -5,60 +5,68 @@ import { useParams } from "next/navigation"; import MessageBox from "@/components/MessageBox"; import AppSidebar from "@/components/Sidebar"; import Message from "@/types/message"; -import getUser from "@/hooks/get-user"; +import useUser, { User } from "@/hooks/get-user"; import Cookies from "js-cookie"; import axios from "axios"; +import { StringMap } from "@/types/typeUtils"; export default function Server() { const { id: ip } = useParams<{ id: string }>(); const wsRef = useRef(null); const [messages, setMessages] = useState([]); - const user = getUser(); + const { user, loading } = useUser(); + const [userList, setUserList] = useState>({}); useEffect(() => { - if (!ip || !user) return; + if (!ip || !user || loading) return; async function auth() { - const server_auth = ((await axios.post('http://localhost:3000/api/auth', { - server_ip: ip === 'localhost' ? '127.0.0.1' : ip, - session_token: Cookies.get('token'), - })).data as any).token; + const server_auth = ( + ( + await axios.post("http://localhost:3000/api/auth", { + server_ip: ip === "localhost" ? "127.0.0.1" : ip, + session_token: Cookies.get("token"), + }) + ).data as any + ).token; // Open the WebSocket connection const ws = new WebSocket(`ws://${ip}:7080`); wsRef.current = ws; - + ws.onopen = () => { console.log("Connected to WebSocket:", ip); - ws.send(JSON.stringify({ - version: '0.0.1', - auth_token: server_auth, - last_message: 0 - })) + ws.send( + JSON.stringify({ + version: "0.0.1", + auth_token: server_auth, + last_message: 0, + }) + ); }; - + ws.onmessage = (m) => { const data = JSON.parse(m.data); console.log("Message received:", data); switch (data.type) { - case 'authenticated': + case "authenticated": setMessages(data.params.messages); - - case 'message_create': + + case "message_create": // setMessages([...messages, data.params]); setMessages((prev) => [...prev, data.params]); } }; - + ws.onerror = (err) => { console.error("WebSocket error:", err); }; - + ws.onclose = () => { console.log("WebSocket closed:", ip); }; - + // Cleanup on unmount or id change return () => { ws.close(); @@ -66,7 +74,11 @@ export default function Server() { } auth(); - }, [ip]); + setUserList((prev) => { + prev[user.id] = user; + return prev; + }); + }, [ip, user, loading]); return ( m.channel_id === 'general').toReversed()} + userList={userList} + messages={messages + .filter((m) => m.channel_id === "general") + .toReversed()} sendMessage={(message) => { - wsRef.current?.send(JSON.stringify( - { - type: 'send_message', + wsRef.current?.send( + JSON.stringify({ + type: "send_message", params: { - channel_id: 'general', - contents: message - } - } - )); + channel_id: "general", + contents: message, + }, + }) + ); }} /> diff --git a/src/components/MessageBox.tsx b/src/components/MessageBox.tsx index 0f277fa..f65686e 100644 --- a/src/components/MessageBox.tsx +++ b/src/components/MessageBox.tsx @@ -15,15 +15,25 @@ import { useRef, useState } from "react"; import { format, isToday, isYesterday } from "date-fns"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; +import { StringMap } from "@/types/typeUtils"; +import { User } from "@/hooks/get-user"; -function MessageContainer({ message }: { message: Message }) { +function MessageContainer({ + message, + userList, +}: { + message: Message; + userList: StringMap; +}) { return (
- {message.from} + {userList[message.from] + ? userList[message.from].username + : message.from}

{isToday(message.timestamp * 1000) @@ -86,14 +96,22 @@ function MessageContainer({ message }: { message: Message }) { ); } -export default function MessageBox({ messages, sendMessage }: { messages: Message[], sendMessage: (m: string) => void }) { +export default function MessageBox({ + messages, + userList, + sendMessage, +}: { + messages: Message[]; + userList: StringMap; + sendMessage: (m: string) => void; +}) { const [text, setText] = useState(""); return (

{messages.map((msg) => ( - + ))}
@@ -105,7 +123,7 @@ export default function MessageBox({ messages, sendMessage }: { messages: Messag onKeyDown={(e) => { if (e.key === "Enter") { sendMessage(text); - setText(''); + setText(""); } }} /> diff --git a/src/hooks/get-user.ts b/src/hooks/get-user.ts index 84f392a..85cdcb6 100644 --- a/src/hooks/get-user.ts +++ b/src/hooks/get-user.ts @@ -1,34 +1,46 @@ +"use client"; + import axios from "axios"; import Cookies from "js-cookie"; -import { redirect } from "next/navigation"; +import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; export interface User { - id: number; - username: string; - email: string; + id: number; + username: string; + email: string; } -export default function getUser() { - const [user, setUser] = useState(); - - useEffect(() => { - async function fetchUser() { - const token = Cookies.get("token"); +export default function useUser() { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const router = useRouter(); - if (!token) { - redirect('/login') - } + useEffect(() => { + async function fetchUser() { + const token = Cookies.get("token"); + if (!token) { + router.push("/login"); + return; + } - const res = (await axios.get('http://localhost:3000/api/user', { - params: { token } - })).data as User; + try { + const res = ( + await axios.get("http://localhost:3000/api/user", { + params: { token }, + }) + ).data as User; - setUser(res); - } - - fetchUser(); - }); + setUser(res); + } catch { + router.push("/login"); + } finally { + setLoading(false); + } + } - return user; + fetchUser(); + }, [router]); + + return { user, loading }; } diff --git a/src/lib/api-wrapper/auth.ts b/src/lib/api-wrapper/auth.ts deleted file mode 100644 index 733b931..0000000 --- a/src/lib/api-wrapper/auth.ts +++ /dev/null @@ -1,10 +0,0 @@ -import axios from "axios"; - -export async function auth(session_token: string, server_ip: string) { - const res = (await axios.post('http://localhost:3000/api/auth', { - server_ip, - session_token, - })).data; - - return (res as any).token; -} diff --git a/src/lib/api-wrapper/user.ts b/src/lib/api-wrapper/user.ts deleted file mode 100644 index 4e506e9..0000000 --- a/src/lib/api-wrapper/user.ts +++ /dev/null @@ -1,16 +0,0 @@ -import axios from "axios"; - -export async function getUser() { - console.log((await axios.delete("http://localhost:3000/api/user", { - data: { username: "leo" }, - })).data); - - return ((await axios.post('http://localhost:3000/api/user', { - username: 'leo', - name: 'Leo', - email: '', - password: 'Idk123', - })).data).token; -} - -// await getUser(); \ No newline at end of file