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);
+ }
+};