From 6bdf2233fef94d80fd03e231ee685ef9d42c3a4e Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 12 Oct 2025 12:57:04 +0200 Subject: [PATCH] Switch to supabase (not fully complete) --- package-lock.json | 135 ++++++++++++++++++++++++- package.json | 1 + src/app/api/auth/auth.ts | 11 -- src/app/api/auth/route.ts | 74 +++++++++----- src/app/api/db.ts | 33 ------ src/app/api/profile/route.ts | 41 ++++++++ src/app/api/supa.ts | 6 ++ src/app/api/user/route.ts | 183 +++++++++++++++++++--------------- src/components/MessageBox.tsx | 2 +- src/components/auth/Login.tsx | 4 +- src/hooks/get-user.ts | 2 +- 11 files changed, 336 insertions(+), 156 deletions(-) delete mode 100644 src/app/api/auth/auth.ts delete mode 100644 src/app/api/db.ts create mode 100644 src/app/api/profile/route.ts create mode 100644 src/app/api/supa.ts diff --git a/package-lock.json b/package-lock.json index 2d536ec..6211150 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", + "@supabase/supabase-js": "^2.75.0", "@types/axios": "^0.9.36", "@types/date-fns": "^2.5.3", "@types/http-status-codes": "^1.2.0", @@ -1884,6 +1885,80 @@ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, + "node_modules/@supabase/auth-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.75.0.tgz", + "integrity": "sha512-J8TkeqCOMCV4KwGKVoxmEBuDdHRwoInML2vJilthOo7awVCro2SM+tOcpljORwuBQ1vHUtV62Leit+5wlxrNtw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.75.0.tgz", + "integrity": "sha512-18yk07Moj/xtQ28zkqswxDavXC3vbOwt1hDuYM3/7xPnwwpKnsmPyZ7bQ5th4uqiJzQ135t74La9tuaxBR6e7w==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.75.0.tgz", + "integrity": "sha512-YfBz4W/z7eYCFyuvHhfjOTTzRrQIvsMG2bVwJAKEVVUqGdzqfvyidXssLBG0Fqlql1zJFgtsPpK1n4meHrI7tg==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.75.0.tgz", + "integrity": "sha512-B4Xxsf2NHd5cEnM6MGswOSPSsZKljkYXpvzKKmNxoUmNQOfB7D8HOa6NwHcUBSlxcjV+vIrYKcYXtavGJqeGrw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15", + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "ws": "^8.18.2" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.75.0.tgz", + "integrity": "sha512-wpJMYdfFDckDiHQaTpK+Ib14N/O2o0AAWWhguKvmmMurB6Unx17GGmYp5rrrqCTf8S1qq4IfIxTXxS4hzrUySg==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.75.0.tgz", + "integrity": "sha512-8UN/vATSgS2JFuJlMVr51L3eUDz+j1m7Ww63wlvHLKULzCDaVWYzvacCjBTLW/lX/vedI2LBI4Vg+01G9ufsJQ==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.75.0", + "@supabase/functions-js": "2.75.0", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "2.75.0", + "@supabase/realtime-js": "2.75.0", + "@supabase/storage-js": "2.75.0" + } + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -2249,7 +2324,6 @@ "version": "20.19.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.11.tgz", "integrity": "sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -2267,6 +2341,12 @@ "pg-types": "^2.2.0" } }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.1.10", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz", @@ -2292,6 +2372,15 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -5051,6 +5140,12 @@ "node": ">=18" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -5105,7 +5200,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unified": { @@ -5275,6 +5369,43 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 611ca6a..1f0707e 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", + "@supabase/supabase-js": "^2.75.0", "@types/axios": "^0.9.36", "@types/date-fns": "^2.5.3", "@types/http-status-codes": "^1.2.0", diff --git a/src/app/api/auth/auth.ts b/src/app/api/auth/auth.ts deleted file mode 100644 index fa28cea..0000000 --- a/src/app/api/auth/auth.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { StringMap } from "@/types/typeUtils"; - -type ServerAuth = { - server_ip: string; - user_id: number; -}; - -// { token: userid } -export const CLIENT_AUTH_TOKENS: StringMap = {}; - -export const SERVER_AUTH_TOKENS: StringMap = {}; diff --git a/src/app/api/auth/route.ts b/src/app/api/auth/route.ts index 888177f..c2ce122 100644 --- a/src/app/api/auth/route.ts +++ b/src/app/api/auth/route.ts @@ -1,32 +1,40 @@ import { NextRequest, NextResponse } from "next/server"; -import { CLIENT_AUTH_TOKENS, SERVER_AUTH_TOKENS } from "./auth"; import { StatusCodes } from "http-status-codes"; -import axios from "axios"; +import { supabase } from "../supa"; +const SERVER_AUTH_TOKENS = new Map< + string, + { user_id: string; server_ip: string } +>(); + +/** + * GET /api/auth?token= + * Called by a (DM node / Server) to verify a temporary relay token. + */ export async function GET(req: NextRequest) { const url = new URL(req.url); const token = url.searchParams.get("token"); - if (!token) + if (!token) { return NextResponse.json( { message: "There should be a token parameter" }, { status: StatusCodes.BAD_REQUEST } ); + } - const auth = SERVER_AUTH_TOKENS[token]; - - if (!auth) + const auth = SERVER_AUTH_TOKENS.get(token); + if (!auth) { return NextResponse.json( - { message: "Invalid token" }, + { message: "Invalid or expired token" }, { status: StatusCodes.NOT_FOUND } ); + } const ip = req.headers.get("x-real-ip") || req.headers.get("x-forwarded-for")?.split(",")[0] || "127.0.0.1"; - // ::1 for testing if (auth.server_ip !== ip && ip !== "::1") { return NextResponse.json( { message: "Invalid address" }, @@ -34,35 +42,49 @@ export async function GET(req: NextRequest) { ); } - delete SERVER_AUTH_TOKENS[token]; + SERVER_AUTH_TOKENS.delete(token); return NextResponse.json({ message: "ok", ...auth }); } +/** + * POST /api/auth + * Called by the client to request a temporary relay token for a DM server. + * + * Body: { server_ip: string } + * Header: Authorization: Bearer + */ export async function POST(req: NextRequest) { - let { server_ip } = await req.json(); - const session_token = req.cookies.get("token")?.value; + const { server_ip } = await req.json(); + const authHeader = req.cookies.get("token"); - if (!session_token) + if (!authHeader) { return NextResponse.json( - { message: "Token cookie not found" }, + { message: "Missing Supabase Authorization header" }, + { status: StatusCodes.BAD_REQUEST } + ); + } + + const supabaseToken = authHeader.value; + + const { + data: { user }, + error, + } = await supabase.auth.getUser(supabaseToken); + + if (error || !user) { + return NextResponse.json( + { message: "Invalid Supabase token" }, { status: StatusCodes.UNAUTHORIZED } ); + } - const token = crypto.randomUUID(); + const relayToken = crypto.randomUUID(); - const user_id = CLIENT_AUTH_TOKENS[session_token]; - - if (!user_id) - return NextResponse.json( - { message: "Invalid token" }, - { status: StatusCodes.NOT_FOUND } - ); - - SERVER_AUTH_TOKENS[token] = { - user_id, + SERVER_AUTH_TOKENS.set(relayToken, { + user_id: user.id, server_ip: String(server_ip), - }; + }); - return NextResponse.json({ message: "ok", token }); + return NextResponse.json({ message: "ok", token: relayToken }); } diff --git a/src/app/api/db.ts b/src/app/api/db.ts deleted file mode 100644 index fd45a72..0000000 --- a/src/app/api/db.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { envEnsure, envNumber } from "@/lib/env"; -import { Pool } from "pg"; - -const pool = new Pool({ - user: envEnsure("DB_USER"), - host: envEnsure("DB_HOST"), - database: "postgres", - password: envEnsure("DB_PASSWORD"), - port: envNumber("DB_PORT") || 5432, -}); - -const createUsersTableQuery = ` - CREATE TABLE IF NOT EXISTS users ( - id BIGSERIAL PRIMARY KEY, - username VARCHAR(32) UNIQUE NOT NULL, - email VARCHAR(255) UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - created_at TIMESTAMP DEFAULT NOW() NOT NULL, - updated_at TIMESTAMP DEFAULT NOW() NOT NULL - ); -`; - -export async function initDb() { - try { - await pool.query(createUsersTableQuery); - console.log("Users table is ready"); - } catch (err) { - console.error("Error creating users table:", err); - throw err; - } -} - -export default pool; diff --git a/src/app/api/profile/route.ts b/src/app/api/profile/route.ts new file mode 100644 index 0000000..195ef7f --- /dev/null +++ b/src/app/api/profile/route.ts @@ -0,0 +1,41 @@ +import { StatusCodes } from "http-status-codes"; +import { NextRequest, NextResponse } from "next/server"; +import { supabase } from "../supa"; + +/** + * GET /api/profile?id= + * or cookie-based lookup + */ +export async function GET(req: NextRequest) { + const url = new URL(req.url); + const query_id = url.searchParams.get("id"); + const token = req.cookies.get("token")?.value; + + if (!query_id && !token) + return NextResponse.json( + { message: "Query arg should be token or id" }, + { status: StatusCodes.BAD_REQUEST } + ); + + let user_id = query_id; + if (!user_id && token) { + const { + data: { user }, + } = await supabase.auth.getUser(token); + user_id = user?.id ?? null; + } + + if (!user_id) + return NextResponse.json( + { message: "Invalid token" }, + { status: StatusCodes.NOT_FOUND } + ); + + const { data: profile } = await supabase + .from("profiles") + .select("id, username, name, email") + .eq("id", user_id) + .maybeSingle(); + + return NextResponse.json({ message: "ok", ...profile }); +} diff --git a/src/app/api/supa.ts b/src/app/api/supa.ts new file mode 100644 index 0000000..31ad6ab --- /dev/null +++ b/src/app/api/supa.ts @@ -0,0 +1,6 @@ +import { createClient } from "@supabase/supabase-js"; + +export const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY! +); diff --git a/src/app/api/user/route.ts b/src/app/api/user/route.ts index 3bab295..40f25fe 100644 --- a/src/app/api/user/route.ts +++ b/src/app/api/user/route.ts @@ -1,112 +1,133 @@ import { NextRequest, NextResponse } from "next/server"; -import pool, { initDb } from "../db"; import { StatusCodes } from "http-status-codes"; -import { CLIENT_AUTH_TOKENS } from "../auth/auth"; +import { supabase } from "../supa"; +/** + * POST /api/user + * Register new user + * Body: { name, username, email, password } + */ export async function POST(req: NextRequest) { - let { name, username, email, password } = await req.json(); + const { name, username, email, password } = await req.json(); - const user_ = await pool.query( - "SELECT id, username, email FROM users WHERE username = $1;", - [username] - ); + if (!email || !password || !username) + return NextResponse.json( + { message: "Missing required fields" }, + { status: StatusCodes.BAD_REQUEST } + ); - if (user_.rows.length > 0) { + const { data: existing } = await supabase + .from("profiles") + .select("username") + .eq("username", username) + .maybeSingle(); + + if (existing) return NextResponse.json( { message: "Username already used" }, { status: StatusCodes.CONFLICT } ); - } - const user_email = await pool.query( - "SELECT id, username, email FROM users WHERE email = $1;", - [email] - ); + const { data: authData, error: authError } = + await supabase.auth.admin.createUser({ + email, + password, + email_confirm: true, + user_metadata: { name, username }, + }); - if (user_email.rows.length > 0) { + if (authError) 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 token = crypto.randomUUID(); - - CLIENT_AUTH_TOKENS[token] = parseInt(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" }, + { message: authError.message }, { 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] - ); + await supabase.from("profiles").insert({ + id: authData.user.id, + name, + username, + }); - if (user.rowCount === 0) + return NextResponse.json({ + message: "ok", + user_id: authData.user.id, + }); +} + +/** + * GET /api/user + * Login user + * Params: { username, password } + */ +export async function GET(req: NextRequest) { + const params = new URL(req.url).searchParams; + const [username, password] = [params.get("username"), params.get("password")]; + + if (!username || !password) + return NextResponse.json( + { message: "Params username and password are required" }, + { status: StatusCodes.BAD_REQUEST } + ); + + let email = username; + if (!username.includes("@")) { + const { data } = await supabase + .from("profiles") + .select("email") + .eq("email", username) + .maybeSingle(); + email = data?.email; + } + + if (!email) + return NextResponse.json( + { message: "Invalid username" }, + { status: StatusCodes.UNAUTHORIZED } + ); + + const { data, error } = await supabase.auth.signInWithPassword({ + email, + password, + }); + + if (error) return NextResponse.json( { message: "Invalid credentials" }, { status: StatusCodes.UNAUTHORIZED } ); - const token = crypto.randomUUID(); + const { session } = data; + const res = NextResponse.json({ + message: "ok", + access_token: session?.access_token, + refresh_token: session?.refresh_token, + user_id: session?.user?.id, + }); - CLIENT_AUTH_TOKENS[token] = parseInt(user.rows[0].id); + // Optionally set cookie + res.cookies.set("token", session?.access_token ?? "", { httpOnly: true }); - return NextResponse.json({ message: "ok", token }); -} - -export async function GET(req: NextRequest) { - const url = new URL(req.url); - let query_id = url.searchParams.get("id"); - const token = req.cookies.get("token")?.value; - - if (!query_id && !token) - return NextResponse.json( - { message: "Query arg should be token or id" }, - { status: StatusCodes.BAD_REQUEST } - ); - else { - const id = query_id ? parseInt(query_id) : token ? CLIENT_AUTH_TOKENS[token] : null; - - if (!id) - return NextResponse.json( - { message: "Invalid token" }, - { status: StatusCodes.NOT_FOUND } - ); - - const user = await pool.query( - "SELECT id, username, email FROM users WHERE id = $1;", - [id] - ); - - return NextResponse.json({ message: "ok", ...user.rows[0] }); - } + return res; } +/** + * DELETE /api/auth + * Body: { username } + */ export async function DELETE(req: NextRequest) { - let { username } = await req.json(); + const { username } = await req.json(); - await pool.query("DELETE FROM users WHERE username = $1;", [username]); + // Delete both profile + auth user + const { data: profile } = await supabase + .from("profiles") + .select("id") + .eq("username", username) + .maybeSingle(); + + if (profile) { + await supabase.auth.admin.deleteUser(profile.id); + await supabase.from("profiles").delete().eq("id", profile.id); + } return NextResponse.json({ message: "ok" }); } - -initDb(); diff --git a/src/components/MessageBox.tsx b/src/components/MessageBox.tsx index dd62415..9511613 100644 --- a/src/components/MessageBox.tsx +++ b/src/components/MessageBox.tsx @@ -32,7 +32,7 @@ function MessageContainer({ }) { useEffect(() => { axios - .get("/api/user/", { params: { id: message.from } }) + .get("/api/profile/", { params: { id: message.from } }) .then((res) => setUserList((prev) => ({ ...prev, [message.from]: res.data as User })) ) diff --git a/src/components/auth/Login.tsx b/src/components/auth/Login.tsx index 77cc57a..aed855d 100644 --- a/src/components/auth/Login.tsx +++ b/src/components/auth/Login.tsx @@ -28,7 +28,9 @@ export default function Login() { const login = async () => { setFeedback(undefined); try { - const res = await axios.put("/api/user", { username, password }); + const res = await axios.get("/api/user", { + params: { username, password }, + }); Cookies.set("token", (res.data as any).token); setFeedback({ diff --git a/src/hooks/get-user.ts b/src/hooks/get-user.ts index 73d2504..dcb7952 100644 --- a/src/hooks/get-user.ts +++ b/src/hooks/get-user.ts @@ -25,7 +25,7 @@ export default function useUser() { } try { - const res = (await axios.get("/api/user")).data as User; + const res = (await axios.get("/api/profile")).data as User; setUser(res); } catch {