Switch to supabase (not fully complete)
This commit is contained in:
Generated
+133
-2
@@ -21,6 +21,7 @@
|
|||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
|
"@supabase/supabase-js": "^2.75.0",
|
||||||
"@types/axios": "^0.9.36",
|
"@types/axios": "^0.9.36",
|
||||||
"@types/date-fns": "^2.5.3",
|
"@types/date-fns": "^2.5.3",
|
||||||
"@types/http-status-codes": "^1.2.0",
|
"@types/http-status-codes": "^1.2.0",
|
||||||
@@ -1884,6 +1885,80 @@
|
|||||||
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@swc/helpers": {
|
||||||
"version": "0.5.15",
|
"version": "0.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||||
@@ -2249,7 +2324,6 @@
|
|||||||
"version": "20.19.11",
|
"version": "20.19.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.11.tgz",
|
||||||
"integrity": "sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==",
|
"integrity": "sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
@@ -2267,6 +2341,12 @@
|
|||||||
"pg-types": "^2.2.0"
|
"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": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.1.10",
|
"version": "19.1.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz",
|
||||||
@@ -2292,6 +2372,15 @@
|
|||||||
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@ungap/structured-clone": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
|
||||||
@@ -5051,6 +5140,12 @@
|
|||||||
"node": ">=18"
|
"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": {
|
"node_modules/trim-lines": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
|
||||||
@@ -5105,7 +5200,6 @@
|
|||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/unified": {
|
"node_modules/unified": {
|
||||||
@@ -5275,6 +5369,43 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"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": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
|
"@supabase/supabase-js": "^2.75.0",
|
||||||
"@types/axios": "^0.9.36",
|
"@types/axios": "^0.9.36",
|
||||||
"@types/date-fns": "^2.5.3",
|
"@types/date-fns": "^2.5.3",
|
||||||
"@types/http-status-codes": "^1.2.0",
|
"@types/http-status-codes": "^1.2.0",
|
||||||
|
|||||||
@@ -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 { NextRequest, NextResponse } from "next/server";
|
||||||
import { CLIENT_AUTH_TOKENS, SERVER_AUTH_TOKENS } from "./auth";
|
|
||||||
import { StatusCodes } from "http-status-codes";
|
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) {
|
export async function GET(req: NextRequest) {
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
const token = url.searchParams.get("token");
|
const token = url.searchParams.get("token");
|
||||||
|
|
||||||
if (!token)
|
if (!token) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ message: "There should be a token parameter" },
|
{ message: "There should be a token parameter" },
|
||||||
{ status: StatusCodes.BAD_REQUEST }
|
{ status: StatusCodes.BAD_REQUEST }
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const auth = SERVER_AUTH_TOKENS[token];
|
const auth = SERVER_AUTH_TOKENS.get(token);
|
||||||
|
if (!auth) {
|
||||||
if (!auth)
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ message: "Invalid token" },
|
{ message: "Invalid or expired token" },
|
||||||
{ status: StatusCodes.NOT_FOUND }
|
{ status: StatusCodes.NOT_FOUND }
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const ip =
|
const ip =
|
||||||
req.headers.get("x-real-ip") ||
|
req.headers.get("x-real-ip") ||
|
||||||
req.headers.get("x-forwarded-for")?.split(",")[0] ||
|
req.headers.get("x-forwarded-for")?.split(",")[0] ||
|
||||||
"127.0.0.1";
|
"127.0.0.1";
|
||||||
|
|
||||||
// ::1 for testing
|
|
||||||
if (auth.server_ip !== ip && ip !== "::1") {
|
if (auth.server_ip !== ip && ip !== "::1") {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ message: "Invalid address" },
|
{ 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 });
|
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) {
|
export async function POST(req: NextRequest) {
|
||||||
let { server_ip } = await req.json();
|
const { server_ip } = await req.json();
|
||||||
const session_token = req.cookies.get("token")?.value;
|
const authHeader = req.cookies.get("token");
|
||||||
|
|
||||||
if (!session_token)
|
if (!authHeader) {
|
||||||
return NextResponse.json(
|
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 }
|
{ status: StatusCodes.UNAUTHORIZED }
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const token = crypto.randomUUID();
|
const relayToken = crypto.randomUUID();
|
||||||
|
|
||||||
const user_id = CLIENT_AUTH_TOKENS[session_token];
|
SERVER_AUTH_TOKENS.set(relayToken, {
|
||||||
|
user_id: user.id,
|
||||||
if (!user_id)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ message: "Invalid token" },
|
|
||||||
{ status: StatusCodes.NOT_FOUND }
|
|
||||||
);
|
|
||||||
|
|
||||||
SERVER_AUTH_TOKENS[token] = {
|
|
||||||
user_id,
|
|
||||||
server_ip: String(server_ip),
|
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 { NextRequest, NextResponse } from "next/server";
|
||||||
import pool, { initDb } from "../db";
|
|
||||||
import { StatusCodes } from "http-status-codes";
|
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) {
|
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(
|
if (!email || !password || !username)
|
||||||
"SELECT id, username, email FROM users WHERE username = $1;",
|
return NextResponse.json(
|
||||||
[username]
|
{ 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(
|
return NextResponse.json(
|
||||||
{ message: "Username already used" },
|
{ message: "Username already used" },
|
||||||
{ status: StatusCodes.CONFLICT }
|
{ status: StatusCodes.CONFLICT }
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
const user_email = await pool.query(
|
const { data: authData, error: authError } =
|
||||||
"SELECT id, username, email FROM users WHERE email = $1;",
|
await supabase.auth.admin.createUser({
|
||||||
[email]
|
email,
|
||||||
);
|
password,
|
||||||
|
email_confirm: true,
|
||||||
|
user_metadata: { name, username },
|
||||||
|
});
|
||||||
|
|
||||||
if (user_email.rows.length > 0) {
|
if (authError)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ message: "Email already used" },
|
{ message: authError.message },
|
||||||
{ 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" },
|
|
||||||
{ status: StatusCodes.BAD_REQUEST }
|
{ status: StatusCodes.BAD_REQUEST }
|
||||||
);
|
);
|
||||||
|
|
||||||
const user = await pool.query(
|
await supabase.from("profiles").insert({
|
||||||
"SELECT id, username, email FROM users WHERE password_hash = $2 AND username = $1 OR email = $1;",
|
id: authData.user.id,
|
||||||
[username, password]
|
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(
|
return NextResponse.json(
|
||||||
{ message: "Invalid credentials" },
|
{ message: "Invalid credentials" },
|
||||||
{ status: StatusCodes.UNAUTHORIZED }
|
{ 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 });
|
return res;
|
||||||
}
|
|
||||||
|
|
||||||
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] });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/auth
|
||||||
|
* Body: { username }
|
||||||
|
*/
|
||||||
export async function DELETE(req: NextRequest) {
|
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" });
|
return NextResponse.json({ message: "ok" });
|
||||||
}
|
}
|
||||||
|
|
||||||
initDb();
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ function MessageContainer({
|
|||||||
}) {
|
}) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
axios
|
axios
|
||||||
.get("/api/user/", { params: { id: message.from } })
|
.get("/api/profile/", { params: { id: message.from } })
|
||||||
.then((res) =>
|
.then((res) =>
|
||||||
setUserList((prev) => ({ ...prev, [message.from]: res.data as User }))
|
setUserList((prev) => ({ ...prev, [message.from]: res.data as User }))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ export default function Login() {
|
|||||||
const login = async () => {
|
const login = async () => {
|
||||||
setFeedback(undefined);
|
setFeedback(undefined);
|
||||||
try {
|
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);
|
Cookies.set("token", (res.data as any).token);
|
||||||
setFeedback({
|
setFeedback({
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default function useUser() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = (await axios.get("/api/user")).data as User;
|
const res = (await axios.get("/api/profile")).data as User;
|
||||||
|
|
||||||
setUser(res);
|
setUser(res);
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
Reference in New Issue
Block a user