diff --git a/.env.template b/.env.template index 310c22c..21cb167 100644 --- a/.env.template +++ b/.env.template @@ -3,3 +3,8 @@ HARMONY_CUB_DATABASE_HOSTNAME= HARMONY_CUB_DATABASE_PORT= HARMONY_CUB_DATABASE_USER= HARMONY_CUB_DATABASE_PASS= +HARMONY_FRONTEND_DATABASE_NAME=harmony_frontend +HARMONY_FRONTEND_DATABASE_HOSTNAME= +HARMONY_FRONTEND_DATABASE_PORT= +HARMONY_FRONTEND_DATABASE_USER= +HARMONY_FRONTEND_DATABASE_PASS= diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml index 6df4889..5933e23 100644 --- a/.idea/sqldialects.xml +++ b/.idea/sqldialects.xml @@ -3,4 +3,7 @@ + + + \ No newline at end of file diff --git a/centralUserBackend/src/database.ts b/centralUserBackend/src/database.ts index a80e9e9..1d316fd 100644 --- a/centralUserBackend/src/database.ts +++ b/centralUserBackend/src/database.ts @@ -1,3 +1,5 @@ +"use server"; + import postgres from "postgres"; const sql = postgres({ diff --git a/centralUserBackend/src/index.ts b/centralUserBackend/src/index.ts index c03e774..f94a01d 100644 --- a/centralUserBackend/src/index.ts +++ b/centralUserBackend/src/index.ts @@ -66,6 +66,7 @@ app.get("/oauth/callback", async (req, reply) => { } const sessionToken = randomString(128); + const frontendAccessToken = randomString(128); if (activeUserSessions.has(session.did)) { activeUserSessions.set(session.did, [ @@ -97,7 +98,7 @@ app.get("/oauth/callback", async (req, reply) => { return reply .header("Set-Cookie", `did=${session.did}; ${cookieAttributes}`) .header("Set-Cookie", `session_token=${sessionToken}; ${cookieAttributes}`) - .redirect(`${PUBLIC_FRONTEND_URL}/app`) + .redirect(`${PUBLIC_FRONTEND_URL}/oauth/token/${frontendAccessToken}`) .send({ farewell: true }); } catch (error) { console.error("OAuth callback error:", error); diff --git a/centralUserBackend/src/session.ts b/centralUserBackend/src/session.ts index b47d370..e2261ed 100644 --- a/centralUserBackend/src/session.ts +++ b/centralUserBackend/src/session.ts @@ -8,23 +8,13 @@ import { INDEV, PUBLIC_CUB_URL } from "./index.ts"; export const PDS_AUTH_SCOPE = "atproto"; // GCI Sessions -// sessionToken -> domain +// sessionToken -> did const activeGCISessions = new Map(); -export function isValidGCISession(sessionToken: string, domain: string): boolean { - if (!domain || !sessionToken) return false; - - return activeGCISessions.get(sessionToken) === domain; -} - -// Frontend Sessions -// sessionToken -> domain -const activeFrontendSessions = new Map(); - -export function isValidFrontendSession(sessionToken: string, domain: string): boolean { - if (!domain || !sessionToken) return false; +export function isValidGCISession(sessionToken: string, did: string): boolean { + if (!did || !sessionToken) return false; - return activeGCISessions.get(sessionToken) === domain; + return activeGCISessions.get(sessionToken) === did; } // User Sessions @@ -113,4 +103,4 @@ export async function getPDSOAuthClient(): Promise { return PDSClient; } -export { activeFrontendSessions, activeGCISessions, activePDSSessions, activeUserSessions }; +export { type activeFrontendSessions, activeGCISessions, activePDSSessions, activeUserSessions }; diff --git a/frontend/package.json b/frontend/package.json index ed974bd..348a1e3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ "dependencies": { "@atproto/jwk-jose": "^0.1.11", "@atproto/oauth-client-node": "^0.3.17", + "@biomejs/biome": "^2.4.15", "@ewsgit/uikit-solid": "file:./uikit-solid", "@material-symbols/svg-700": "^0.44.6", "@solid-primitives/lifecycle": "^0.1.2", @@ -19,7 +20,8 @@ "@solidjs/start": "2.0.0-alpha.2", "@solidjs/vite-plugin-nitro-2": "^0.1.0", "@types/node": "^25.6.2", - "@biomejs/biome": "^2.4.15", + "dotenv": "^17.4.2", + "postgres": "^3.4.9", "solid-js": "^1.9.5", "vite": "^7.0.0" }, diff --git a/frontend/src/entry-server.tsx b/frontend/src/entry-server.tsx index 9d21d2d..23dc85a 100644 --- a/frontend/src/entry-server.tsx +++ b/frontend/src/entry-server.tsx @@ -1,5 +1,6 @@ // @refresh reload import { createHandler, StartServer } from "@solidjs/start/server"; +import "~/lib/databaseTables"; export default createHandler(() => ( = (props) => { + const navigate = useNavigate(); + + onMount(() => { + navigate(props.to); + }); + + return <>; +}; + +export default Redirect; diff --git a/frontend/src/lib/auth.ts b/frontend/src/lib/auth.ts new file mode 100644 index 0000000..c6f65fe --- /dev/null +++ b/frontend/src/lib/auth.ts @@ -0,0 +1,19 @@ +import { getCookie } from "@solidjs/start/http"; +import sql from "~/lib/database"; + +export default async function getUser() { + const did = getCookie("did"); + const cubFrontendAccessToken = getCookie("cub_frontend_access_token"); + + if (!did || !cubFrontendAccessToken) return undefined; + + const databaseResponse = await sql`SELECT cub_frontend_access_token FROM user_sessions WHERE did = ${did}`; + + if (databaseResponse[0].cub_frontend_access_token === cubFrontendAccessToken) { + return { + did, + cubFrontendAccessToken, + authenticated: true, + }; + } +} diff --git a/frontend/src/lib/database.ts b/frontend/src/lib/database.ts new file mode 100644 index 0000000..e4c3a36 --- /dev/null +++ b/frontend/src/lib/database.ts @@ -0,0 +1,11 @@ +import postgres from "postgres"; + +const sql = postgres({ + db: process.env.HARMONY_FRONTEND_DATABASE_NAME || "harmony_frontend", + hostname: process.env.HARMONY_FRONTEND_DATABASE_HOSTNAME || "localhost", + port: Number(process.env.HARMONY_FRONTEND_DATABASE_PORT) || 5432, + user: process.env.HARMONY_FRONTEND_DATABASE_USER || "postgres", + pass: process.env.HARMONY_FRONTEND_DATABASE_PASS || "postgres", +}); + +export default sql; diff --git a/frontend/src/lib/databaseTables.ts b/frontend/src/lib/databaseTables.ts new file mode 100644 index 0000000..bf519f7 --- /dev/null +++ b/frontend/src/lib/databaseTables.ts @@ -0,0 +1,11 @@ +import sql from "~/lib/database"; + +await sql`CREATE TABLE IF NOT EXISTS user_sessions ( + did TEXT, + cub_frontend_access_token TEXT +)`; + +await sql`CREATE TABLE IF NOT EXISTS users ( + did TEXT, + has_completed_tutorial BOOLEAN DEFAULT FALSE +)`; diff --git a/frontend/src/routes/(default)/oauth/token/[token].tsx b/frontend/src/routes/(default)/oauth/token/[token].tsx new file mode 100644 index 0000000..59a8064 --- /dev/null +++ b/frontend/src/routes/(default)/oauth/token/[token].tsx @@ -0,0 +1,10 @@ +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/app.tsx b/frontend/src/routes/app.tsx index 90a6ab2..3743cbe 100644 --- a/frontend/src/routes/app.tsx +++ b/frontend/src/routes/app.tsx @@ -1,33 +1,77 @@ -import { Component, createEffect, ParentProps, useContext } from "solid-js"; -import styles from "./app.module.scss" -import { AppCentralStore, AppContext } from "./app/appContext"; +import { action, createAsync, query, useNavigate } from "@solidjs/router"; +import { type Component, createEffect, type ParentProps, useContext } from "solid-js"; import { createStore } from "solid-js/store"; -import TopNavigation from "./app/components/TopNavigation/TopNavigation"; +import getUser from "~/lib/auth"; +import sql from "~/lib/database"; +import { type AppCentralStore, AppContext } from "./app/appContext"; import PrimarySidebar from "./app/components/PrimarySidebar/PrimarySidebar"; import SecondarySidebar from "./app/components/SecondarySidebar/SecondarySidebar"; -import { useNavigate } from "@solidjs/router"; +import TopNavigation from "./app/components/TopNavigation/TopNavigation"; +import styles from "./app.module.scss"; + +const getAppContextQuery = query(async () => { + "use server"; + + const user = await getUser(); + + if (!user) + return { + hasCompletedTutorial: false, + }; + + const databaseQueryData = (await sql`SELECT has_completed_tutorial FROM users WHERE did = ${user.did}`)[0]; + + return { + hasCompletedTutorial: databaseQueryData.has_completed_tutorial || false, + }; +}, "appContextQuery"); + +const updateAppContextAction = action(async (props: { hasCompletedTutorial: boolean }) => { + "use server"; + + const user = await getUser(); + + if (!user) return; + + await sql`UPDATE users SET has_completed_tutorial = ${props.hasCompletedTutorial} WHERE did = ${user.did}`; +}); const App: Component = (props) => { - const appContext = useContext(AppContext) + const appContext = useContext(AppContext); const navigate = useNavigate(); - const [appCentralStore, setAppCentralStore] = createStore({ - hasCompletedTutorial: false - }) + const [appCentralStore, setAppCentralStore] = createStore({ + hasCompletedTutorial: false, + }); + const appContextQueryData = createAsync(() => getAppContextQuery()); createEffect(() => { - if(!appContext?.store.hasCompletedTutorial) { - navigate("/app/welcome") + const acqd = appContextQueryData(); + + if (acqd) { + setAppCentralStore("hasCompletedTutorial", acqd.hasCompletedTutorial); } - }) - - return -
- - - {props.children} - -
-
-} + + if (!appContext?.store.hasCompletedTutorial) { + navigate("/app/welcome"); + } + }); + + createEffect(async () => { + await updateAppContextAction({ + hasCompletedTutorial: appCentralStore.hasCompletedTutorial, + }); + }); + + return ( + +
+ + + {props.children} + +
+
+ ); +}; export default App; diff --git a/frontend/src/routes/app/index.tsx b/frontend/src/routes/app/index.tsx index b00cb4f..6a59e01 100644 --- a/frontend/src/routes/app/index.tsx +++ b/frontend/src/routes/app/index.tsx @@ -1,18 +1,21 @@ -import { Component, Show, useContext } from "solid-js"; -import { AppContext } from "./appContext"; -import styles from "./index.module.scss" -import UKText from "@ewsgit/uikit-solid/src/components/text/UKText.jsx"; import UKStack from "@ewsgit/uikit-solid/src/components/stack/UKStack.jsx"; import UKStackItem from "@ewsgit/uikit-solid/src/components/stack/UKStackItem.jsx"; +import UKText from "@ewsgit/uikit-solid/src/components/text/UKText.jsx"; +import { type Component, Show, useContext } from "solid-js"; +import Redirect from "~/lib/Redirect"; +import { AppContext } from "./appContext"; +import styles from "./index.module.scss"; const AppIndexPage: Component = () => { - const appContext = useContext(AppContext) + const appContext = useContext(AppContext); - return <> - Not completed tutorial yet} when={appContext?.store.hasCompletedTutorial}> + return ( + } when={appContext?.store.hasCompletedTutorial}>
- While you've been gone... + + While you've been gone... + @@ -39,7 +42,7 @@ const AppIndexPage: Component = () => {
- -} + ); +}; -export default AppIndexPage +export default AppIndexPage; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 3e7614c..066918f 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,14 +1,13 @@ -import { defineConfig } from "vite"; +import { solidStart } from "@solidjs/start/config"; import { nitroV2Plugin as nitro } from "@solidjs/vite-plugin-nitro-2"; +import dotenv from "dotenv"; +import { defineConfig } from "vite"; -import { solidStart } from "@solidjs/start/config"; +dotenv.config({ quiet: true, path: "../.env" }); export default defineConfig({ - plugins: [ - solidStart({ ssr: true, solid: { ssr: true } }), - nitro() - ], + plugins: [solidStart({ ssr: true, solid: { ssr: true } }), nitro()], server: { - host: "0.0.0.0" - } + host: "0.0.0.0", + }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a15f500..8b1b347 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -252,7 +252,7 @@ importers: devDependencies: '@types/node': specifier: latest - version: 25.9.0 + version: 25.9.1 frontend: dependencies: @@ -267,7 +267,7 @@ importers: version: 2.4.15 '@ewsgit/uikit-solid': specifier: file:./uikit-solid - version: file:frontend/uikit-solid(@types/node@25.8.0)(esbuild@0.28.0)(jiti@2.7.0)(postcss@8.5.14)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.1)(yaml@2.9.0) + version: file:frontend/uikit-solid(@types/node@25.8.0)(esbuild@0.28.0)(jiti@2.7.0)(postcss@8.5.14)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.1)(typescript@5.9.3)(yaml@2.9.0) '@material-symbols/svg-700': specifier: ^0.44.6 version: 0.44.7 @@ -289,6 +289,12 @@ importers: '@types/node': specifier: ^25.6.2 version: 25.8.0 + dotenv: + specifier: ^17.4.2 + version: 17.4.2 + postgres: + specifier: ^3.4.9 + version: 3.4.9 solid-js: specifier: ^1.9.5 version: 1.9.12 @@ -1698,8 +1704,8 @@ packages: '@types/node@25.8.0': resolution: {integrity: sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==} - '@types/node@25.9.0': - resolution: {integrity: sha512-AOQwYUNolgy3VosiRqXrACUXTN8nJUtPl7FJXMqZVyxiiCLhQuG3jXKvCS1ALr+Y2OmZhzzLVlYPEqJaiqkaJQ==} + '@types/node@25.9.1': + resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} @@ -4487,7 +4493,7 @@ snapshots: '@esbuild/win32-x64@0.28.0': optional: true - '@ewsgit/uikit-solid@file:frontend/uikit-solid(@types/node@25.8.0)(esbuild@0.28.0)(jiti@2.7.0)(postcss@8.5.14)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.1)(yaml@2.9.0)': + '@ewsgit/uikit-solid@file:frontend/uikit-solid(@types/node@25.8.0)(esbuild@0.28.0)(jiti@2.7.0)(postcss@8.5.14)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.1)(typescript@5.9.3)(yaml@2.9.0)': dependencies: '@material-symbols/svg-700': 0.42.3 '@solid-primitives/media': 2.3.5(solid-js@1.9.12) @@ -4498,7 +4504,7 @@ snapshots: sass-embedded: 1.99.0 solid-js: 1.9.12 solid-styled-components: 0.28.5(solid-js@1.9.12) - tsup: 8.5.1(jiti@2.7.0)(postcss@8.5.14)(tsx@4.22.1)(yaml@2.9.0) + tsup: 8.5.1(jiti@2.7.0)(postcss@8.5.14)(tsx@4.22.1)(typescript@5.9.3)(yaml@2.9.0) vite: 8.0.13(@types/node@25.8.0)(esbuild@0.28.0)(jiti@2.7.0)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.1)(yaml@2.9.0) vite-plugin-solid: 2.11.12(solid-js@1.9.12)(vite@8.0.13(@types/node@25.8.0)(esbuild@0.28.0)(jiti@2.7.0)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.1)(yaml@2.9.0)) transitivePeerDependencies: @@ -5164,7 +5170,7 @@ snapshots: dependencies: undici-types: 7.24.6 - '@types/node@25.9.0': + '@types/node@25.9.1': dependencies: undici-types: 7.24.6 @@ -7027,7 +7033,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.1(jiti@2.7.0)(postcss@8.5.14)(tsx@4.22.1)(yaml@2.9.0): + tsup@8.5.1(jiti@2.7.0)(postcss@8.5.14)(tsx@4.22.1)(typescript@5.9.3)(yaml@2.9.0): dependencies: bundle-require: 5.1.0(esbuild@0.27.7) cac: 6.7.14 @@ -7048,6 +7054,7 @@ snapshots: tree-kill: 1.2.2 optionalDependencies: postcss: 8.5.14 + typescript: 5.9.3 transitivePeerDependencies: - jiti - supports-color