diff --git a/.env.template b/.env.template index 21cb167..b2c81fc 100644 --- a/.env.template +++ b/.env.template @@ -1,3 +1,4 @@ +HARMONY_INDEV=true HARMONY_CUB_DATABASE_NAME=harmony_cub HARMONY_CUB_DATABASE_HOSTNAME= HARMONY_CUB_DATABASE_PORT= @@ -8,3 +9,4 @@ HARMONY_FRONTEND_DATABASE_HOSTNAME= HARMONY_FRONTEND_DATABASE_PORT= HARMONY_FRONTEND_DATABASE_USER= HARMONY_FRONTEND_DATABASE_PASS= +NODE_EXTRA_CA_CERTS=$HOME/.local/share/caddy/pki/authorities/local/root.crt diff --git a/.idea/icon.svg b/.idea/icon.svg new file mode 100644 index 0000000..d1409b2 --- /dev/null +++ b/.idea/icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/.idea/runConfigurations/Start_Caddy.xml b/.idea/runConfigurations/Start_Caddy.xml new file mode 100644 index 0000000..02d0b47 --- /dev/null +++ b/.idea/runConfigurations/Start_Caddy.xml @@ -0,0 +1,17 @@ + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Start_Dev.xml b/.idea/runConfigurations/Start_Dev.xml new file mode 100644 index 0000000..bc238af --- /dev/null +++ b/.idea/runConfigurations/Start_Dev.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/centralUserBackend/src/database.ts b/centralUserBackend/src/database.ts index 1d316fd..a80e9e9 100644 --- a/centralUserBackend/src/database.ts +++ b/centralUserBackend/src/database.ts @@ -1,5 +1,3 @@ -"use server"; - import postgres from "postgres"; const sql = postgres({ diff --git a/centralUserBackend/src/databaseTables.ts b/centralUserBackend/src/databaseTables.ts new file mode 100644 index 0000000..2753dc2 --- /dev/null +++ b/centralUserBackend/src/databaseTables.ts @@ -0,0 +1,6 @@ +import sql from "./database.ts"; + +await sql`CREATE TABLE IF NOT EXISTS user_sessions ( + did TEXT, + cub_frontend_access_token TEXT +)`; diff --git a/centralUserBackend/src/env.ts b/centralUserBackend/src/env.ts new file mode 100644 index 0000000..e50d6ac --- /dev/null +++ b/centralUserBackend/src/env.ts @@ -0,0 +1,3 @@ +import dotenv from "dotenv"; + +dotenv.config({ quiet: true, path: "../.env" }); diff --git a/centralUserBackend/src/index.ts b/centralUserBackend/src/index.ts index f94a01d..18797ae 100644 --- a/centralUserBackend/src/index.ts +++ b/centralUserBackend/src/index.ts @@ -1,225 +1,24 @@ +import "./env.ts"; import fastifyCookie from "@fastify/cookie"; import fastifyUrlData from "@fastify/url-data"; -import dotenv from "dotenv"; import Fastify from "fastify"; import { serializerCompiler, validatorCompiler } from "fastify-type-provider-zod"; -import { activeUserSessions, getPDSOAuthClient, isValidUserSession, PDS_AUTH_SCOPE, validateUserSessionMiddleware } from "./session"; - -dotenv.config({ quiet: true, path: "../.env" }); +import loadRoutes from "./routes.ts"; export const PUBLIC_FRONTEND_URL = "https://127.0.0.1"; export const PUBLIC_CUB_URL = `${PUBLIC_FRONTEND_URL}/cub`; export const INDEV = true; -const app = Fastify(); +import "./databaseTables.ts"; + +export const app = Fastify(); app.setValidatorCompiler(validatorCompiler); app.setSerializerCompiler(serializerCompiler); app.register(fastifyCookie); app.register(fastifyUrlData); -app.get("/", () => { - return "Hello from the HarmonyChat Central User Backend!"; -}); - -app.get("/oauth-client-metadata.json", async (_, reply) => { - const client = await getPDSOAuthClient(); - return reply.send(client.clientMetadata); -}); - -app.post("/oauth/new", async (req, reply) => { - try { - const { handle } = await ((await req.body) as Promise); - - if (!handle || typeof handle !== "string") { - return reply.status(400).send({ error: "Handle is required" }); - } - - const client = await getPDSOAuthClient(); - - // Resolves handle, finds their auth server, returns authorization URL - const authUrl = await client.authorize(handle, { - scope: PDS_AUTH_SCOPE, - }); - - return reply.status(200).send({ redirectUrl: authUrl.toString() }); - } catch (error) { - return reply.status(500).send({ error: error instanceof Error ? error.message : "Login failed" }); - } -}); - -app.get("/oauth/callback", async (req, reply) => { - try { - const params = new URLSearchParams(req.urlData().query); - - const client = await getPDSOAuthClient(); - - const { session } = await client.callback(params); - - function randomString(length: number) { - const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*_-+=.~"; - const arr = new Uint8Array(length); - crypto.getRandomValues(arr); - return Array.from(arr) - .map((n) => chars[n % chars.length]) - .join(""); - } - - const sessionToken = randomString(128); - const frontendAccessToken = randomString(128); - - if (activeUserSessions.has(session.did)) { - activeUserSessions.set(session.did, [ - ...(activeUserSessions.get(session.did) || []), - { - sessionToken, - validUntil: Date.now() + 604_800_000 /* 7 days in ms */, - }, - ]); - } else { - activeUserSessions.set(session.did, [ - { - sessionToken, - validUntil: Date.now() + 604_800_000 /* 7 days in ms */, - }, - ]); - } - - const cookieAttributes = [ - "HttpOnly", - "Path=/", - "SameSite=Lax", - `Max-Age=${60 * 60 * 24 * 7}`, // 1 week - !INDEV ? "Secure" : "", - ] - .filter(Boolean) - .join("; "); - - return reply - .header("Set-Cookie", `did=${session.did}; ${cookieAttributes}`) - .header("Set-Cookie", `session_token=${sessionToken}; ${cookieAttributes}`) - .redirect(`${PUBLIC_FRONTEND_URL}/oauth/token/${frontendAccessToken}`) - .send({ farewell: true }); - } catch (error) { - console.error("OAuth callback error:", error); - return reply.redirect(`${PUBLIC_FRONTEND_URL}/oauth/error/login_failed`).send({ farewell: true }); - } -}); - -app.get("/oauth/check", async (req, reply) => { - const did = req.cookies.did; - const sessionToken = req.cookies.session_token; - - if (!did) { - return reply.status(401).send({ authenticated: false }); - } - - if (!sessionToken) { - return reply.status(401).send({ authenticated: false }); - } - - if (isValidUserSession(did, sessionToken)) { - return reply.send({ authenticated: true }); - } - - return reply.status(401).send({ authenticated: false }); -}); - -app.post( - "/oauth/logout", - validateUserSessionMiddleware(({ reply, did, sessionToken }) => { - activeUserSessions.set( - did, - (activeUserSessions.get(did) || []).filter((session) => session.sessionToken !== sessionToken), - ); - - return reply.send({ success: true }); - }), -); - -app.get( - "/user/profiles/detailed", - validateUserSessionMiddleware(({ reply }) => { - return reply.send({ no: true }); - }), -); - -app.get( - "/user/profiles", - validateUserSessionMiddleware(({ reply }) => { - return reply.send({ no: true }); - }), -); - -app.get( - "/user/profile", - validateUserSessionMiddleware(({ reply }) => { - return reply.send({ no: true }); - }), -); - -app.post( - "/user/profile", - validateUserSessionMiddleware(({ reply }) => { - return reply.send({ no: true }); - }), -); - -app.put( - "/user/profile", - validateUserSessionMiddleware(({ reply }) => { - return reply.send({ no: true }); - }), -); - -app.delete( - "/user/profile", - validateUserSessionMiddleware(({ reply }) => { - return reply.send({ no: true }); - }), -); - -app.get( - "/user/avatar", - validateUserSessionMiddleware(({ reply }) => { - return reply.send({ no: true }); - }), -); - -app.put( - "/user/avatar", - validateUserSessionMiddleware(({ reply }) => { - return reply.send({ no: true }); - }), -); - -app.delete( - "/user/avatar", - validateUserSessionMiddleware(({ reply }) => { - return reply.send({ no: true }); - }), -); - -app.get( - "/user/communities", - validateUserSessionMiddleware(({ reply }) => { - return reply.send({ no: true }); - }), -); - -app.post( - "/user/join-community", - validateUserSessionMiddleware(({ reply }) => { - return reply.send({ no: true }); - }), -); - -app.delete( - "/user/community", - validateUserSessionMiddleware(({ reply }) => { - return reply.send({ no: true }); - }), -); +loadRoutes(); await app.listen({ port: 3000, host: "127.0.0.1" }); console.log(`HarmonyChat Central User Backend listening at ${app.listeningOrigin}`); diff --git a/centralUserBackend/src/routes.ts b/centralUserBackend/src/routes.ts new file mode 100644 index 0000000..1795428 --- /dev/null +++ b/centralUserBackend/src/routes.ts @@ -0,0 +1,227 @@ +import sql from "./database.ts"; +import { app, INDEV, PUBLIC_FRONTEND_URL } from "./index.ts"; +import { activeUserSessions, getPDSOAuthClient, isValidUserSession, PDS_AUTH_SCOPE, validateUserSessionMiddleware } from "./session.ts"; + +export default function main() { + app.get("/", () => { + return "Hello from the HarmonyChat Central User Backend!"; + }); + + app.get("/oauth-client-metadata.json", async (_, reply) => { + const client = await getPDSOAuthClient(); + return reply.send(client.clientMetadata); + }); + + app.post("/oauth/new", async (req, reply) => { + try { + const { handle } = await ((await req.body) as Promise); + + if (!handle || typeof handle !== "string") { + return reply.status(400).send({ error: "Handle is required" }); + } + + const client = await getPDSOAuthClient(); + + // Resolves handle, finds their auth server, returns authorization URL + const authUrl = await client.authorize(handle, { + scope: PDS_AUTH_SCOPE, + }); + + return reply.status(200).send({ redirectUrl: authUrl.toString() }); + } catch (error) { + return reply.status(500).send({ error: error instanceof Error ? error.message : "Login failed" }); + } + }); + + app.get("/oauth/callback", async (req, reply) => { + try { + const params = new URLSearchParams(req.urlData().query); + + const client = await getPDSOAuthClient(); + + const { session } = await client.callback(params); + + function randomString(length: number) { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@$%^&*_-+=.~"; + const arr = new Uint8Array(length); + crypto.getRandomValues(arr); + return Array.from(arr) + .map((n) => chars[n % chars.length]) + .join(""); + } + + const sessionToken = randomString(128); + const frontendAccessToken = encodeURIComponent(randomString(128)); + + await sql`INSERT INTO user_sessions (did, cub_frontend_access_token) VALUES (${session.did}, ${frontendAccessToken})`; + + if (activeUserSessions.has(session.did)) { + activeUserSessions.set(session.did, [ + ...(activeUserSessions.get(session.did) || []), + { + sessionToken, + validUntil: Date.now() + 604_800_000 /* 7 days in ms */, + }, + ]); + } else { + activeUserSessions.set(session.did, [ + { + sessionToken, + validUntil: Date.now() + 604_800_000 /* 7 days in ms */, + }, + ]); + } + + const cookieAttributes = [ + "HttpOnly", + "Path=/", + "SameSite=Lax", + `Max-Age=${60 * 60 * 24 * 7}`, // 1 week + !INDEV ? "Secure" : "", + ] + .filter(Boolean) + .join("; "); + + return reply + .header("Set-Cookie", `did=${session.did}; ${cookieAttributes}`) + .header("Set-Cookie", `session_token=${sessionToken}; ${cookieAttributes}`) + .redirect(`${PUBLIC_FRONTEND_URL}/oauth/token/${encodeURI(session.did)}/${frontendAccessToken}`) + .send({ farewell: true }); + } catch (error) { + console.error("OAuth callback error:", error); + return reply.redirect(`${PUBLIC_FRONTEND_URL}/oauth/error/login_failed`).send({ farewell: true }); + } + }); + + app.post("/oauth/frontend/verify", async (req, reply) => { + try { + const { frontend_access_token, did } = JSON.parse(req.body as string) as { frontend_access_token: string; did: string }; + const databaseData = (await sql`SELECT user_sessions.cub_frontend_access_token FROM user_sessions WHERE did = ${did}`)?.[0]; + + if (!databaseData) return reply.status(401).send({ authorized: false }); + + if (databaseData.cub_frontend_access_token === frontend_access_token) { + return reply.status(200).send({ authorized: true }); + } + } catch (e) { + console.error(e); + + return reply.status(500).send({ authorized: false }); + } + }); + + app.get("/oauth/check", async (req, reply) => { + const did = req.cookies.did; + const sessionToken = req.cookies.session_token; + + if (!did) { + return reply.status(401).send({ authenticated: false }); + } + + if (!sessionToken) { + return reply.status(401).send({ authenticated: false }); + } + + if (isValidUserSession(did, sessionToken)) { + return reply.send({ authenticated: true }); + } + + return reply.status(401).send({ authenticated: false }); + }); + + app.post( + "/oauth/logout", + validateUserSessionMiddleware(({ reply, did, sessionToken }) => { + activeUserSessions.set( + did, + (activeUserSessions.get(did) || []).filter((session) => session.sessionToken !== sessionToken), + ); + + return reply.send({ success: true }); + }), + ); + + app.get( + "/user/profiles/detailed", + validateUserSessionMiddleware(({ reply }) => { + return reply.send({ no: true }); + }), + ); + + app.get( + "/user/profiles", + validateUserSessionMiddleware(({ reply }) => { + return reply.send({ no: true }); + }), + ); + + app.get( + "/user/profile", + validateUserSessionMiddleware(({ reply }) => { + return reply.send({ no: true }); + }), + ); + + app.post( + "/user/profile", + validateUserSessionMiddleware(({ reply }) => { + return reply.send({ no: true }); + }), + ); + + app.put( + "/user/profile", + validateUserSessionMiddleware(({ reply }) => { + return reply.send({ no: true }); + }), + ); + + app.delete( + "/user/profile", + validateUserSessionMiddleware(({ reply }) => { + return reply.send({ no: true }); + }), + ); + + app.get( + "/user/avatar", + validateUserSessionMiddleware(({ reply }) => { + return reply.send({ no: true }); + }), + ); + + app.put( + "/user/avatar", + validateUserSessionMiddleware(({ reply }) => { + return reply.send({ no: true }); + }), + ); + + app.delete( + "/user/avatar", + validateUserSessionMiddleware(({ reply }) => { + return reply.send({ no: true }); + }), + ); + + app.get( + "/user/communities", + validateUserSessionMiddleware(({ reply }) => { + return reply.send({ no: true }); + }), + ); + + app.post( + "/user/join-community", + validateUserSessionMiddleware(({ reply }) => { + return reply.send({ no: true }); + }), + ); + + app.delete( + "/user/community", + validateUserSessionMiddleware(({ reply }) => { + return reply.send({ no: true }); + }), + ); +} diff --git a/centralUserBackend/src/session.ts b/centralUserBackend/src/session.ts index e2261ed..1a2687d 100644 --- a/centralUserBackend/src/session.ts +++ b/centralUserBackend/src/session.ts @@ -103,4 +103,4 @@ export async function getPDSOAuthClient(): Promise { return PDSClient; } -export { type activeFrontendSessions, activeGCISessions, activePDSSessions, activeUserSessions }; +export { activeGCISessions, activePDSSessions, activeUserSessions }; diff --git a/frontend/src/entry-server.tsx b/frontend/src/entry-server.tsx index 23dc85a..0444094 100644 --- a/frontend/src/entry-server.tsx +++ b/frontend/src/entry-server.tsx @@ -2,6 +2,11 @@ import { createHandler, StartServer } from "@solidjs/start/server"; import "~/lib/databaseTables"; +if (process.env.HARMONY_INDEV === "true") { + console.log("Harmony Frontend is INDEV, disabling tls verification"); + process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; +} + export default createHandler(() => ( ( diff --git a/frontend/src/lib/frontend.ts b/frontend/src/lib/frontend.ts new file mode 100644 index 0000000..1e46014 --- /dev/null +++ b/frontend/src/lib/frontend.ts @@ -0,0 +1 @@ +export const FRONTEND_HOSTNAME = "https://127.0.0.1"; diff --git a/frontend/src/routes/(default)/oauth/error/[errorMessage].tsx b/frontend/src/routes/(default)/oauth/error/[errorMessage].tsx index 55cbfdb..7697605 100644 --- a/frontend/src/routes/(default)/oauth/error/[errorMessage].tsx +++ b/frontend/src/routes/(default)/oauth/error/[errorMessage].tsx @@ -4,25 +4,41 @@ import UKCard from "@ewsgit/uikit-solid/src/components/card/UKCard.jsx"; import UKDivider from "@ewsgit/uikit-solid/src/components/divider/UKDivider.jsx"; import UKText from "@ewsgit/uikit-solid/src/components/text/UKText.jsx"; import { Title } from "@solidjs/meta"; -import { Component } from "solid-js"; -import styles from "./[errorMessage].module.scss" -import { useNavigate } from "@solidjs/router"; +import { useNavigate, useParams } from "@solidjs/router"; +import type { Component } from "solid-js"; +import styles from "./[errorMessage].module.scss"; const OAuthErrorPage: Component = () => { - const navigate = useNavigate() + const params = useParams(); + const navigate = useNavigate(); - return
- OAuth Error - - An Error Has Occurred - - Oops! looks like we failed to log you in.
perhaps try again later?
- - navigate("/login")}>Retry - navigate("/")}>Go Home - -
-
-} + return ( +
+ OAuth Error + + + An Error Has Occurred + + + + Oops! looks like we failed to log you in. +
+ perhaps try again later? +
+ + Error Code: {params.errorMessage} + + + navigate("/login")}> + Retry + + navigate("/")}> + Go Home + + +
+
+ ); +}; -export default OAuthErrorPage +export default OAuthErrorPage; diff --git a/frontend/src/routes/(default)/oauth/token/[token].tsx b/frontend/src/routes/(default)/oauth/token/[token].tsx deleted file mode 100644 index 59a8064..0000000 --- a/frontend/src/routes/(default)/oauth/token/[token].tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { setCookie } from "@solidjs/start/http"; -import type { APIEvent } from "@solidjs/start/server"; - -export const GET = ({ params }: APIEvent) => { - setCookie("frontend_access_token", params.token); - - return { - ok: true, - }; -}; diff --git a/frontend/src/routes/login.tsx b/frontend/src/routes/login.tsx index 6e5ea78..8fe52d2 100644 --- a/frontend/src/routes/login.tsx +++ b/frontend/src/routes/login.tsx @@ -62,6 +62,7 @@ const LoginPage: Component = () => { labelBackgroundStyle="background" color="outlined" label="AT Protocol Handle" + autocomplete={"atmosphere-handle"} value={handle()} onValueChange={setHandle} /> diff --git a/frontend/src/routes/oauth/token/[did]/[token].tsx b/frontend/src/routes/oauth/token/[did]/[token].tsx new file mode 100644 index 0000000..006dda7 --- /dev/null +++ b/frontend/src/routes/oauth/token/[did]/[token].tsx @@ -0,0 +1,35 @@ +import { setCookie } from "@solidjs/start/http"; +import type { APIEvent } from "@solidjs/start/server"; +import { CUB_HOSTNAME } from "~/lib/cub"; +import sql from "~/lib/database"; +import { FRONTEND_HOSTNAME } from "~/lib/frontend"; + +export const GET = async ({ params }: APIEvent) => { + try { + const did = decodeURIComponent(params.did); + + const cubResponse = await ( + await fetch(`${CUB_HOSTNAME}/oauth/frontend/verify`, { + method: "POST", + body: JSON.stringify({ + frontend_access_token: params.token, + did: did, + }), + }) + ).json(); + + console.log(cubResponse); + + if (!cubResponse.authorized) + return Response.json({ ok: false }, { status: 308, headers: { Location: `${FRONTEND_HOSTNAME}/oauth/error/failed_cub_frontend_verification` } }); + + await sql`INSERT INTO user_sessions (did, cub_frontend_access_token) VALUES (${did}, ${params.token})`; + + setCookie("frontend_access_token", params.token); + setCookie("did", did); + + return Response.json({ ok: true }, { status: 308, headers: { Location: `${FRONTEND_HOSTNAME}/app` } }); + } catch (e) { + console.error(e); + } +};