Switch to supabase (not fully complete)
This commit is contained in:
@@ -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
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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 }))
|
||||
)
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user