diff --git a/package-lock.json b/package-lock.json index 6a01c88..1bea6d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", + "@supabase/ssr": "^0.8.0", "@supabase/supabase-js": "^2.75.0", "@types/axios": "^0.9.36", "@types/date-fns": "^2.5.3", @@ -53,7 +54,7 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4", - "@types/node": "^20.19.21", + "@types/node": "^20.19.27", "@types/pg": "^8.15.5", "@types/react": "^19", "@types/react-dom": "^19", @@ -1918,77 +1919,95 @@ "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==", + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.89.0.tgz", + "integrity": "sha512-wiWZdz8WMad8LQdJMWYDZ2SJtZP5MwMqzQq3ehtW2ngiI3UTgbKiFrvMUUS3KADiVlk4LiGfODB2mrYx7w2f8w==", "license": "MIT", "dependencies": { - "@supabase/node-fetch": "2.6.15" + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" } }, "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==", + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.89.0.tgz", + "integrity": "sha512-XEueaC5gMe5NufNYfBh9kPwJlP5M2f+Ogr8rvhmRDAZNHgY6mI35RCkYDijd92pMcNM7g8pUUJov93UGUnqfyw==", "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" + "tslib": "2.8.1" }, "engines": { - "node": "4.x || >=6.0.0" + "node": ">=20.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==", + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.89.0.tgz", + "integrity": "sha512-/b0fKrxV9i7RNOEXMno/I1862RsYhuUo+Q6m6z3ar1f4ulTMXnDfv0y4YYxK2POcgrOXQOgKYQx1eArybyNvtg==", "license": "MIT", "dependencies": { - "@supabase/node-fetch": "2.6.15" + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" } }, "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==", + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.89.0.tgz", + "integrity": "sha512-aMOvfDb2a52u6PX6jrrjvACHXGV3zsOlWRzZsTIOAJa0hOVvRp01AwC1+nLTGUzxzezejrYeCX+KnnM1xHdl+w==", "license": "MIT", "dependencies": { - "@supabase/node-fetch": "2.6.15", "@types/phoenix": "^1.6.6", "@types/ws": "^8.18.1", + "tslib": "2.8.1", "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/ssr": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.8.0.tgz", + "integrity": "sha512-/PKk8kNFSs8QvvJ2vOww1mF5/c5W8y42duYtXvkOSe+yZKRgTTZywYG2l41pjhNomqESZCpZtXuWmYjFRMV+dw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.2" + }, + "peerDependencies": { + "@supabase/supabase-js": "^2.76.1" } }, "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==", + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.89.0.tgz", + "integrity": "sha512-6zKcXofk/M/4Eato7iqpRh+B+vnxeiTumCIP+Tz26xEqIiywzD9JxHq+udRrDuv6hXE+pmetvJd8n5wcf4MFRQ==", "license": "MIT", "dependencies": { - "@supabase/node-fetch": "2.6.15" + "iceberg-js": "^0.8.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" } }, "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==", + "version": "2.89.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.89.0.tgz", + "integrity": "sha512-KlaRwSfFA0fD73PYVMHj5/iXFtQGCcX7PSx0FdQwYEEw9b2wqM7GxadY+5YwcmuEhalmjFB/YvqaoNVF+sWUlg==", "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" + "@supabase/auth-js": "2.89.0", + "@supabase/functions-js": "2.89.0", + "@supabase/postgrest-js": "2.89.0", + "@supabase/realtime-js": "2.89.0", + "@supabase/storage-js": "2.89.0" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@swc/helpers": { @@ -2353,9 +2372,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.21.tgz", - "integrity": "sha512-CsGG2P3I5y48RPMfprQGfy4JPRZ6csfC3ltBZSRItG3ngggmNY/qs2uZKp4p9VbrpqNNSMzUZNFZKzgOGnd/VA==", + "version": "20.19.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", + "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -2374,9 +2393,9 @@ } }, "node_modules/@types/phoenix": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", - "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz", + "integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==", "license": "MIT" }, "node_modules/@types/react": { @@ -2687,6 +2706,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -3140,6 +3172,15 @@ "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", "license": "MIT" }, + "node_modules/iceberg-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", + "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/inline-style-parser": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", @@ -5221,12 +5262,6 @@ "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", @@ -5463,22 +5498,6 @@ "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", diff --git a/package.json b/package.json index 8909f37..d89a45b 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", + "@supabase/ssr": "^0.8.0", "@supabase/supabase-js": "^2.75.0", "@types/axios": "^0.9.36", "@types/date-fns": "^2.5.3", @@ -56,7 +57,7 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4", - "@types/node": "^20.19.21", + "@types/node": "^20.19.27", "@types/pg": "^8.15.5", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/src/actions/auth.ts b/src/actions/auth.ts new file mode 100644 index 0000000..3b78532 --- /dev/null +++ b/src/actions/auth.ts @@ -0,0 +1,29 @@ +"use server"; + +import createClient from "@/actions/supa"; +import getSession from "./get-session"; + +/** + * Request a temporary relay token for a DM node or server. + */ +export async function authRelay(server_id: string) { + const supabase = await createClient(); + + const user_id = await getSession(); + + if (!user_id) { + throw new Error("Invalid user id"); + } + + const { data: relayToken, error: errorRelay } = await supabase + .from("server_auth") + .insert({ user_id: user_id, server_id }) + .select("key") + .maybeSingle(); + + if (!relayToken || errorRelay) { + throw new Error("Failed to create relay token"); + } + + return relayToken.key as string; +} diff --git a/src/actions/get-profile.ts b/src/actions/get-profile.ts new file mode 100644 index 0000000..be1577c --- /dev/null +++ b/src/actions/get-profile.ts @@ -0,0 +1,58 @@ +"use server"; + +import createClient from "@/actions/supa"; +import { ProfileSettings } from "@/types/settings"; +import getSession from "./get-session"; +import { UserProfile } from "@/hooks/get-user"; + +export async function getProfileById(id: string): Promise { + const supabase = await createClient(); + + const { error, data } = await supabase + .from("profiles") + .select("id, username, display_name, avatar_url, node_address") + .eq("id", id) + .maybeSingle(); + + if (error) { + throw error; + } + + return data; +} + +export async function getProfileByUsername( + username: string +): Promise { + const supabase = await createClient(); + + const { error, data } = await supabase + .from("profiles") + .select("id, username, display_name, avatar_url, node_address") + .eq("username", username) + .maybeSingle(); + + if (error) { + throw error; + } + + return data; +} + +export async function updateProfile(profile: ProfileSettings) { + const supabase = await createClient(); + const user_id = await getSession(); + + if (!user_id) { + throw new Error("User id invalid"); + } + + const { error } = await supabase + .from("profiles") + .update(profile) + .eq("id", user_id); + + if (error) { + throw error; + } +} diff --git a/src/actions/get-server.ts b/src/actions/get-server.ts new file mode 100644 index 0000000..d9c32f3 --- /dev/null +++ b/src/actions/get-server.ts @@ -0,0 +1,19 @@ +"use server"; + +import createClient from "@/actions/supa"; + +export async function getServerById(id: string) { + const supabase = await createClient(); + + const { error, data: server } = await supabase + .from("servers") + .select("id, name, created_at, owner, address, icon_url") + .eq("id", id) + .maybeSingle(); + + if (error) throw error; + + if (!server) throw "Server is null"; + + return server; +} diff --git a/src/actions/get-session.ts b/src/actions/get-session.ts new file mode 100644 index 0000000..1ef26f8 --- /dev/null +++ b/src/actions/get-session.ts @@ -0,0 +1,16 @@ +"use server"; + +import createClient from "@/actions/supa"; + +export default async function getSessionUserId() { + const supabase = await createClient(); + const { data, error } = await supabase.auth.getUser(); + + if (error) throw error; + + const user = data.user; + + if (!user) return null; + + return user.id; +} diff --git a/src/actions/supa.ts b/src/actions/supa.ts new file mode 100644 index 0000000..5c7f41e --- /dev/null +++ b/src/actions/supa.ts @@ -0,0 +1,24 @@ +import { cookies } from "next/headers"; +import { createServerClient } from "@supabase/ssr"; + +export default async function createClient() { + const cookieStore = await cookies(); + + return createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY!, + { + cookies: { + get(name) { + return cookieStore.get(name)?.value; + }, + set(name, value) { + cookieStore.set(name, value); + }, + remove(name) { + cookieStore.delete(name); + }, + }, + } + ); +} diff --git a/src/actions/user.ts b/src/actions/user.ts new file mode 100644 index 0000000..4d72e63 --- /dev/null +++ b/src/actions/user.ts @@ -0,0 +1,78 @@ +"use server"; + +import createClient from "@/actions/supa"; + +/** + * Register a new user + */ +export async function registerUser( + username: string, + email: string, + password: string +) { + if (!username || !email || !password) { + throw new Error("Missing required fields"); + } + + const supabase = await createClient(); + + // Check if username is already taken + const { data: existing } = await supabase + .from("profiles") + .select("username") + .eq("username", username) + .maybeSingle(); + + if (existing) { + throw new Error("Username already used"); + } + + // Sign up user + const { data: authData, error: authError } = await supabase.auth.signUp({ + email, + password, + }); + + if (authError || !authData.user) { + throw new Error(authError?.message ?? "User registration failed"); + } + + // Insert profile + await supabase.from("profiles").insert({ + id: authData.user.id, + username, + display_name: username, + avatar_url: "", + node_address: "", + }); + + return { + message: "User registered. Please check your email to verify your account.", + user_id: authData.user.id, + email_sent: true, + }; +} + +/** + * Login a user + */ +export async function loginUser(email: string, password: string) { + if (!email || !password) { + throw new Error("Email and password are required"); + } + + const supabase = await createClient(); + + const { data, error } = await supabase.auth.signInWithPassword({ + email, + password, + }); + + if (error || !data.session) { + throw new Error("Invalid credentials"); + } + + const { session } = data; + + return session.user?.id; +} diff --git a/src/app/api/auth/route.ts b/src/app/api/auth/route.ts index 55b1387..5be2a90 100644 --- a/src/app/api/auth/route.ts +++ b/src/app/api/auth/route.ts @@ -49,86 +49,3 @@ export async function GET(req: NextRequest) { 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_id: string } - * Header: Authorization: Bearer - */ -export async function POST(req: NextRequest) { - const { server_id } = await req.json(); - const authHeader = req.cookies.get("token"); - - if (!server_id) - return NextResponse.json( - { message: "Missing server_id in the json body" }, - { status: StatusCodes.BAD_REQUEST } - ); - - if (!authHeader) - return NextResponse.json( - { message: "Missing Supabase Authorization header" }, - { status: StatusCodes.BAD_REQUEST } - ); - - const { - data: { user }, - error, - } = await supabase.auth.getUser(authHeader.value); - - if (error || !user) { - return NextResponse.json( - { message: "Invalid Supabase token" }, - { status: StatusCodes.UNAUTHORIZED } - ); - } - - const { data: relayToken, error: errorRelay } = await supabase - .from("server_auth") - .insert({ - user_id: user.id, - server_id, - }) - .select() - .maybeSingle(); - - if (!relayToken || errorRelay) - return NextResponse.json( - { message: "Failed to create relay token" }, - { status: StatusCodes.INTERNAL_SERVER_ERROR } - ); - - return NextResponse.json({ message: "ok", token: relayToken.key }); -} - -/** - * PUT /api/auth - * Called by the client to refresh the access token. - */ -export async function PUT(req: NextRequest) { - const refresh_token = req.cookies.get("refresh_token")?.value; - - if (!refresh_token) - return NextResponse.json( - { message: "Missing refresh_token cookie" }, - { status: StatusCodes.BAD_REQUEST } - ); - - const session = await supabase.auth.refreshSession({ refresh_token }); - - if (session.error) - return NextResponse.json( - { - message: session.error.message, - }, - { status: StatusCodes.INTERNAL_SERVER_ERROR } - ); - - return NextResponse.json({ - message: "ok", - access_token: session.data.session?.access_token, - refresh_token: session.data.session?.refresh_token, - }); -} diff --git a/src/app/api/profile/route.ts b/src/app/api/profile/route.ts deleted file mode 100644 index dcf9960..0000000 --- a/src/app/api/profile/route.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { StatusCodes } from "http-status-codes"; -import { NextRequest, NextResponse } from "next/server"; -import { supabase } from "../supa"; -import { ProfileSettings } from "@/types/settings"; - -/** - * POST /api/profile - * body { username, display_name, avatar_url } - */ -export async function POST(req: NextRequest) { - const url = new URL(req.url); - const token = req.cookies.get("token")?.value; - const { username, display_name, avatar_url, node_address } = - (await req.json()) as ProfileSettings; - - if (!token) - return NextResponse.json( - { message: "Token cookie not found" }, - { status: StatusCodes.BAD_REQUEST } - ); - - if ( - !username || - !display_name || - avatar_url === undefined || - avatar_url === null || - node_address === undefined || - node_address === null - ) - return NextResponse.json( - { - message: - "Invalid body, requires username, display_name, avatar_url, node_address", - }, - { status: StatusCodes.BAD_REQUEST } - ); - - const { - data: { user }, - } = await supabase.auth.getUser(token); - - if (!user) - return NextResponse.json( - { message: "Invalid token" }, - { status: StatusCodes.NOT_FOUND } - ); - - const { error } = await supabase - .from("profiles") - .update({ - username, - display_name, - avatar_url, - node_address, - }) - .eq("id", user.id); - - if (error) { - return NextResponse.json( - { message: error.message }, - { status: StatusCodes.INTERNAL_SERVER_ERROR } - ); - } - - return NextResponse.json({ message: "ok" }); -} - -/** - * GET /api/profile?id= - * or cookie-based lookup - */ -export async function GET(req: NextRequest) { - const url = new URL(req.url); - const id = url.searchParams.get("id"); - const username = url.searchParams.get("username"); - const token = req.cookies.get("token")?.value; - - if (!token && !id && !username) - return NextResponse.json( - { message: "Query arg should be token or id or username" }, - { status: StatusCodes.BAD_REQUEST } - ); - - const { data: profile, error } = id - ? await supabase - .from("profiles") - .select("id, username, display_name, avatar_url, node_address") - .eq("id", id) - .maybeSingle() - : username - ? await supabase - .from("profiles") - .select("id, username, display_name, avatar_url, node_address") - .eq("username", username) - .maybeSingle() - : await supabase - .from("profiles") - .select("id, username, display_name, avatar_url, node_address") - .eq("id", (await supabase.auth.getUser(token)).data.user?.id) - .maybeSingle(); - - if (error) - return NextResponse.json( - { message: error.message }, - { status: StatusCodes.INTERNAL_SERVER_ERROR } - ); - - if (!profile) - return NextResponse.json( - { message: "Profile not found" }, - { status: StatusCodes.NOT_FOUND } - ); - - return NextResponse.json({ message: "ok", ...profile }); -} diff --git a/src/app/api/server/[id]/route.ts b/src/app/api/server/[id]/route.ts deleted file mode 100644 index edbe085..0000000 --- a/src/app/api/server/[id]/route.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { supabase } from "../../supa"; -import { StatusCodes } from "http-status-codes"; - -/** - * GET /api/server/ - */ -export async function GET( - req: NextRequest, - { params }: { params: Promise<{ id: string }> } -) { - const { id } = await params; - - const { data: profile } = await supabase - .from("servers") - .select("id, name, created_at, owner, address, icon_url") - .eq("id", id) - .maybeSingle(); - - if (!profile) - return NextResponse.json( - { message: "not found" }, - { status: StatusCodes.NOT_FOUND } - ); - - return NextResponse.json({ message: "ok", ...profile }); -} diff --git a/src/app/api/user/route.ts b/src/app/api/user/route.ts deleted file mode 100644 index 11e57fd..0000000 --- a/src/app/api/user/route.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { StatusCodes } from "http-status-codes"; -import { supabase } from "../supa"; - -/** - * POST /api/user - * Register new user - * Body: { username, email, password } - */ -export async function POST(req: NextRequest) { - const { username, email, password } = await req.json(); - - if (!email || !password || !username) - return NextResponse.json( - { message: "Missing required fields" }, - { status: StatusCodes.BAD_REQUEST } - ); - - 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 { data: authData, error: authError } = await supabase.auth.signUp({ - email, - password, - }); - - if (authError) - return NextResponse.json( - { message: authError.message }, - { status: StatusCodes.INTERNAL_SERVER_ERROR } - ); - - if (!authData.user) - return NextResponse.json( - { message: "User is null" }, - { status: StatusCodes.INTERNAL_SERVER_ERROR } - ); - - await supabase.from("profiles").insert({ - id: authData.user.id, - username, - display_name: username, - avatar_url: "", - node_address: "", - }); - - return NextResponse.json({ - message: "User registered. Please check your email to verify your account.", - user_id: authData.user?.id, - email_sent: true, - }); -} - -/** - * GET /api/user - * Login user - * Params: { username, password } - */ -export async function GET(req: NextRequest) { - const params = new URL(req.url).searchParams; - const [email, password] = [params.get("email"), params.get("password")]; - - if (!email || !password) - return NextResponse.json( - { message: "Params email and password are required" }, - { status: StatusCodes.BAD_REQUEST } - ); - - const { data, error } = await supabase.auth.signInWithPassword({ - email, - password, - }); - - if (error) - return NextResponse.json( - { message: "Invalid credentials" }, - { status: StatusCodes.UNAUTHORIZED } - ); - - const { session } = data; - const res = NextResponse.json({ - message: "ok", - access_token: session?.access_token, - refresh_token: session?.refresh_token, - user_id: session?.user?.id, - }); - - res.cookies.set("token", session?.access_token ?? "", { httpOnly: true }); - - return res; -} - -/** - * DELETE /api/auth - * Body: { username } - */ -export async function DELETE(req: NextRequest) { - const { username } = await req.json(); - - // 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" }); -} diff --git a/src/app/auth/CreateAccount.tsx b/src/app/auth/CreateAccount.tsx index d1c63a8..6fa1286 100644 --- a/src/app/auth/CreateAccount.tsx +++ b/src/app/auth/CreateAccount.tsx @@ -10,6 +10,7 @@ import { Label } from "@/components/ui/label"; import { toast } from "sonner"; import { useRouter } from "next/navigation"; import Cookies from "js-cookie"; +import { registerUser } from "@/actions/user"; export default function CreateAccount() { const [username, setUsername] = useState(""); @@ -58,8 +59,6 @@ export default function CreateAccount() { }; const signup = async () => { - localStorage.removeItem("chat-messages"); - localStorage.removeItem("server-messages"); setFeedback(undefined); const validationError = validateInput(); if (validationError) { @@ -69,16 +68,8 @@ export default function CreateAccount() { try { setLoading(true); - const { data } = await axios.post("/api/user", { - username, - password, - email, - }); - const response = data as { - message: string; - email_sent: boolean; - user_id: string; - }; + const response = await registerUser(username, email, password); + if (response.email_sent) { toast.info( "We sent you a verification email. Please verify to continue." diff --git a/src/app/auth/Login.tsx b/src/app/auth/Login.tsx index b5419d9..d3fbc2f 100644 --- a/src/app/auth/Login.tsx +++ b/src/app/auth/Login.tsx @@ -2,32 +2,25 @@ 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 { useRouter } from "next/navigation"; import { CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { loginUser } from "@/actions/user"; + +type Feedback = { kind: "error" | "info"; message: string }; export default function Login() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); - const [feedback, setFeedback] = useState< - undefined | { kind: "error" | "info"; message: string } - >(); + const [feedback, setFeedback] = useState(); const router = useRouter(); const login = async () => { - localStorage.removeItem("chat-messages"); - localStorage.removeItem("server-messages"); setFeedback(undefined); try { - const res = await axios.get("/api/user", { - params: { email, password }, - }); + await loginUser(email, password); - Cookies.set("token", (res.data as any).token); - Cookies.set("refresh_token", (res.data as any).refresh_token); setFeedback({ kind: "info", message: "Login successful! Redirecting...", diff --git a/src/app/chat/[id]/page.tsx b/src/app/chat/[id]/page.tsx index 58a8bc3..7053bb2 100644 --- a/src/app/chat/[id]/page.tsx +++ b/src/app/chat/[id]/page.tsx @@ -9,6 +9,7 @@ import auth from "@/lib/auth"; import useAsync from "@/hooks/use-async"; import { Button } from "@/components/ui/button"; import useIndicators from "@/hooks/use-indicators"; +import { useEffectOnceWhenReady } from "@/hooks/use-once"; export default function DMs() { const { id } = useParams<{ id: string }>(); @@ -18,25 +19,29 @@ export default function DMs() { const router = useRouter(); const { indicators, addIndicator } = useIndicators(); - useEffect(() => { - async function connectTargetNode() { - if (!target) return; - auth({ - id: target.node_address, - wsRef: targetNode, - messageStore: app.privateMessages, - onIndicator: addIndicator, + useEffectOnceWhenReady( + () => { + async function connectTargetNode() { + if (!target) return; + auth({ + id: target.node_address, + wsRef: targetNode, + messageStore: app.privateMessages, + onIndicator: addIndicator, + }); + } + + connectTargetNode(); + + app.setCurrentChannel({ + id: id, + name: target?.display_name || id, + kind: "text", }); - } - - connectTargetNode(); - - app.setCurrentChannel({ - id: id, - name: target?.display_name || id, - kind: "text", - }); - }, [target]); + }, + [target], + [(v) => v] + ); return loading || target ? ( @@ -68,7 +73,9 @@ export default function DMs() { break; } }} - indicators={indicators} + indicators={indicators.filter( + (indicator) => indicator.indicator.params.user_id !== app.profile?.id + )} /> ) : ( diff --git a/src/app/favicon.ico b/src/app/favicon.ico deleted file mode 100644 index 0cf8212..0000000 Binary files a/src/app/favicon.ico and /dev/null differ diff --git a/src/components/app/AppLayout.tsx b/src/components/app/AppLayout.tsx index 77de524..164250e 100644 --- a/src/components/app/AppLayout.tsx +++ b/src/components/app/AppLayout.tsx @@ -13,6 +13,7 @@ import SidebarServers from "./SidebarServers"; import SidebarDMs from "./SidebarDMs"; import SidebarChannels from "./SidebarChannels"; import DMItem from "./DMItem"; +import { useEffectOnceWhenReady } from "@/hooks/use-once"; export default function AppLayout({ children, @@ -28,36 +29,42 @@ export default function AppLayout({ const router = useRouter(); const isMobile = useIsMobile(); - useEffect(() => { - if (!app.profile?.node_address) return; + useEffectOnceWhenReady( + () => { + if (!app.profile?.node_address) return; - auth({ - id: app.profile.node_address, - wsRef: app.node, - onNewMessage: (m) => { - if (m.from !== chatWith && m.from !== app.profile?.id) - toast( - `New message from ${app.profiles[m.from]?.display_name || m.from}`, - { - description: m.contents.slice(0, 80), - action: { - label: "View", - onClick: () => router.push(`/chat/${m.from}`), - }, - position: "top-right", - } - ); + auth({ + id: app.profile.node_address, + wsRef: app.node, + onNewMessage: (m) => { + if (m.from !== chatWith && m.from !== app.profile?.id) + toast( + `New message from ${ + app.profiles[m.from]?.display_name || m.from + }`, + { + description: m.contents.slice(0, 80), + action: { + label: "View", + onClick: () => router.push(`/chat/${m.from}`), + }, + position: "top-right", + } + ); - if (onNewMessage) onNewMessage(m); - }, - messageStore: app.privateMessages, - }); + if (onNewMessage) onNewMessage(m); + }, + messageStore: app.privateMessages, + }); - app.setProfiles((prev) => { - if (app.profile) prev[app.profile.id] = app.profile; - return prev; - }); - }, [chatWith, app.profile]); + app.setProfiles((prev) => { + if (app.profile) prev[app.profile.id] = app.profile; + return prev; + }); + }, + [chatWith, app.profile], + [undefined, undefined] + ); return (
diff --git a/src/components/app/SidebarServers.tsx b/src/components/app/SidebarServers.tsx index fb69aa1..c6a4502 100644 --- a/src/components/app/SidebarServers.tsx +++ b/src/components/app/SidebarServers.tsx @@ -23,6 +23,7 @@ import App from "@/types/app"; import ProfilePicture from "../ProfilePicture"; import SettingsDialog from "../settings/SettingsDialog"; import { useRouter } from "next/navigation"; +import { getServerById } from "@/actions/get-server"; export default function SidebarServers({ app }: { app: App }) { const [newServer, setNewServer] = useState(""); @@ -31,7 +32,7 @@ export default function SidebarServers({ app }: { app: App }) { const handleAddServer = async () => { if (!newServer || Object.hasOwn(app.servers, newServer)) return; try { - const server = (await get(`/api/server/${newServer}`)).data as Server; + const server = await getServerById(newServer); app.setServers((prev) => ({ ...prev, [server.id]: server })); Cookies.set("servers", Object.keys(app.servers).join(",")); setNewServer(""); diff --git a/src/components/channel/ChannelView.tsx b/src/components/channel/ChannelView.tsx index bd78cfa..8871f8f 100644 --- a/src/components/channel/ChannelView.tsx +++ b/src/components/channel/ChannelView.tsx @@ -33,7 +33,10 @@ export default function ChannelView({ while (serverRef.current?.CONNECTING) {} serverRef.current?.send(JSON.stringify(req)); }} - indicators={indicators} + indicators={indicators.filter( + (indicator) => + indicator.indicator.params.user_id !== app.profile?.id + )} /> ); diff --git a/src/components/channel/MessageBox.tsx b/src/components/channel/MessageBox.tsx index 22c438f..110608c 100644 --- a/src/components/channel/MessageBox.tsx +++ b/src/components/channel/MessageBox.tsx @@ -27,6 +27,7 @@ import { } from "@/components/ui/context-menu"; import { ClientMessage, IndicatorContext } from "@/types/protocol"; import { AnimatePresence, motion } from "framer-motion"; +import AsyncValue from "../AsyncValue"; function MessageContainer({ message, @@ -429,13 +430,20 @@ export default function MessageBox({ exit={{ opacity: 0, y: -10 }} transition={{ duration: 0.3 }} > - {indicators - .map( - (indicator) => - app.profiles[indicator.indicator.params.user_id] - ?.display_name ?? null - ) - .join(", ")} + + await Promise.all( + indicators.map((indicator) => + app.getUserById(indicator.indicator.params.user_id) + ) + ) + } + render={({ value: indicators }) => + indicators + ?.map((indicator) => indicator?.display_name) + .join(", ") + } + /> {indicators.length > 0 && " is typing"} )} diff --git a/src/components/settings/Settings.tsx b/src/components/settings/Settings.tsx index 78b02f2..1547981 100644 --- a/src/components/settings/Settings.tsx +++ b/src/components/settings/Settings.tsx @@ -14,6 +14,7 @@ import { useRouter } from "next/navigation"; import axios from "axios"; import { useClientSettings } from "@/hooks/use-settings"; import { useState } from "react"; +import { updateProfile } from "@/actions/get-profile"; export default function Settings({ tab, @@ -55,7 +56,7 @@ export default function Settings({ setSettings={setProfileSettings} schema={PROFILE_SCHEMA} onSave={async () => { - await axios.post("/api/profile", profileSettings); + await updateProfile(profileSettings); }} /> diff --git a/src/hooks/get-user.ts b/src/hooks/get-user.ts index 71f990f..98fe782 100644 --- a/src/hooks/get-user.ts +++ b/src/hooks/get-user.ts @@ -7,6 +7,8 @@ import { useEffect, useState } from "react"; import { get } from "@/lib/request"; import { toast } from "sonner"; import axios from "axios"; +import getSession from "@/actions/get-session"; +import { getProfileById } from "@/actions/get-profile"; export interface UserProfile extends ProfileSettings { id: string; @@ -19,20 +21,15 @@ export default function useUser() { useEffect(() => { async function fetchUser() { try { - setUser((await get("/api/profile", (r) => setUser(r.data))).data); - } catch (e: any) { - try { - console.log("Failed to obtain current user, refreshing token.."); - const refresh = await axios.put("/api/auth/"); - const { access_token, refresh_token } = refresh.data as any; - Cookies.set("token", access_token); - Cookies.set("refresh_token", refresh_token); - - setUser((await get("/api/profile", (r) => setUser(r.data))).data); - } catch (e: any) { - router.push("/auth?t=login"); - toast.error(`Error: ${e}`); + const user_id = await getSession(); + if (!user_id) { + throw new Error("Session user id is not valid"); } + + setUser(await getProfileById(user_id)); + } catch (e: any) { + router.push("/auth?t=login"); + toast.error(`Error: ${e}`); } } diff --git a/src/hooks/use-app.ts b/src/hooks/use-app.ts index 141ada0..58a04d3 100644 --- a/src/hooks/use-app.ts +++ b/src/hooks/use-app.ts @@ -8,6 +8,8 @@ import { get, Response } from "@/lib/request"; import { Channel, Server } from "@/types/types"; import Cookies from "js-cookie"; import { toast } from "sonner"; +import { getProfileById } from "@/actions/get-profile"; +import { getServerById } from "@/actions/get-server"; export default function useApp(): App { const node = useRef(null); @@ -29,11 +31,9 @@ export default function useApp(): App { .split(",") .map((x) => x.trim()) .filter((id) => id) - .map((id) => get(`/api/server/${id}`)) + .map((id) => getServerById(id)) ) - .then((o) => - setServers(Object.fromEntries(o.map((i) => [i.data.id, i.data]))) - ) + .then((o) => setServers(Object.fromEntries(o.map((i) => [i.id, i])))) .catch(toast.error); }, []); @@ -58,17 +58,13 @@ export default function useApp(): App { getUserById: async (id: string) => { if (profiles[id] !== undefined) return profiles[id]; - const onResponse = (res: Response) => { + try { + const user = await getProfileById(id); setProfiles((prev) => { - prev[id] = res.data as UserProfile; + prev[id] = user; return prev; }); - }; - - try { - const user = await get(`/api/profile/?id=${id}`, onResponse); - onResponse(user); - return user.data; + return user; } catch { return null; } diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 4f95497..a887b92 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,10 +1,10 @@ import { Message, Server } from "@/types/types"; -import axios from "axios"; import { Dispatch, RefObject, SetStateAction } from "react"; -import { get } from "./request"; import { toast } from "sonner"; import { MessageStore } from "@/hooks/use-messages"; import { IndicatorContext, ServerMessage } from "@/types/protocol"; +import { authRelay } from "@/actions/auth"; +import { getServerById } from "@/actions/get-server"; type AuthParams = { id: string; @@ -27,12 +27,10 @@ export default async function auth({ if (!id) return; - const ip = ((await get(`/api/server/${id}`)).data as any).address as string; + const ip = (await getServerById(id)).address; console.log("Authenticating with server at:", ip); - const server_auth = ( - (await axios.post("/api/auth", { server_id: id })).data as any - ).token; + const server_auth = await authRelay(id); var name = id; @@ -71,15 +69,8 @@ export default async function auth({ switch (msg.type) { case "authenticated": - // ws.send( - // JSON.stringify({ - // type: "load_chunk", - // params: { - // channel_id: "general", - // chunk_id: 0, - // }, - // }) - // ); + if (!onIndicator) break; + msg.params.indicators.forEach(onIndicator); break; case "message_create":