Switch to supabase (not fully complete)

This commit is contained in:
2025-10-12 12:57:04 +02:00
parent 2122d6d74f
commit 6bdf2233fe
11 changed files with 336 additions and 156 deletions
-11
View File
@@ -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<number> = {};
export const SERVER_AUTH_TOKENS: StringMap<ServerAuth> = {};
+48 -26
View File
@@ -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=<relayToken>
* 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 <supabase_jwt>
*/
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 });
}
-33
View File
@@ -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;
+41
View File
@@ -0,0 +1,41 @@
import { StatusCodes } from "http-status-codes";
import { NextRequest, NextResponse } from "next/server";
import { supabase } from "../supa";
/**
* GET /api/profile?id=<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 });
}
+6
View File
@@ -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!
);
+102 -81
View File
@@ -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();
+1 -1
View File
@@ -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 }))
)
+3 -1
View File
@@ -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({
+1 -1
View File
@@ -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 {