From cdf8a04c1353bbf3399c2e1016d5bea7388cc6f7 Mon Sep 17 00:00:00 2001 From: Alberto Quiles Date: Sun, 4 Oct 2026 12:30:32 +0200 Subject: [PATCH 1/2] feat: connect services to auth - fix #9 --- compose.yml | 18 ++++ configs/asset-editor.yml | 1 + configs/client.yml | 1 + configs/static.yml | 1 + configs/web.yml | 1 + seed.ts | 176 ++++++++++++++++++++++++++------------- 6 files changed, 141 insertions(+), 57 deletions(-) diff --git a/compose.yml b/compose.yml index c736e4a..3923625 100644 --- a/compose.yml +++ b/compose.yml @@ -56,6 +56,13 @@ services: client: <<: *dev container_name: client + depends_on: + net: + condition: service_started + auth: + condition: service_healthy + onet: + condition: service_healthy working_dir: /src/openhotel command: bash -c "deno task install && deno task start" volumes: @@ -75,6 +82,12 @@ services: condition: service_healthy working_dir: /src/onet command: bash -c "yarn install && deno task start" + healthcheck: + test: curl -fs http://localhost:9400/api/v1/version + interval: 5s + timeout: 3s + retries: 5 + start_period: 5m volumes: - ${REPOS_DIR:-..}/onet:/src/onet - /src/onet/node_modules @@ -83,6 +96,11 @@ services: web: <<: *dev container_name: web + depends_on: + net: + condition: service_started + auth: + condition: service_healthy working_dir: /src/web command: bash -c "deno task install && deno task start" volumes: diff --git a/configs/asset-editor.yml b/configs/asset-editor.yml index 3536354..1eba44f 100644 --- a/configs/asset-editor.yml +++ b/configs/asset-editor.yml @@ -1,3 +1,4 @@ version: development +# auth.enabled & auth.appToken are set by `deno task seed` auth: url: http://localhost:2024 diff --git a/configs/client.yml b/configs/client.yml index 9172b37..bd62941 100644 --- a/configs/client.yml +++ b/configs/client.yml @@ -1,4 +1,5 @@ version: development +# auth.enabled & auth.licenseToken are set by `deno task seed` auth: api: http://localhost:2024 onet: diff --git a/configs/static.yml b/configs/static.yml index 72d9768..911ae99 100644 --- a/configs/static.yml +++ b/configs/static.yml @@ -1,4 +1,5 @@ version: development +# auth.enabled & auth.appToken are set by `deno task seed` s3: enabled: true endpoint: localhost diff --git a/configs/web.yml b/configs/web.yml index cc1b9c3..f488086 100644 --- a/configs/web.yml +++ b/configs/web.yml @@ -1 +1,2 @@ version: development +# auth.enabled & auth.licenseToken are set by `deno task seed` diff --git a/seed.ts b/seed.ts index 5302733..5959d88 100644 --- a/seed.ts +++ b/seed.ts @@ -15,14 +15,29 @@ type Seed = { users: User[]; hotel: { name: string } }; type Session = Record; type Response = { status: number; data?: Data; message?: string }; +type Integration = { + name: string; + redirectUrl: string; + type: "client" | "web"; +}; + const AUTH_API = "http://localhost:2024/api/v3"; const FINGERPRINT = "devkit-seed"; -const INTEGRATION = { - name: "devkit", - redirectUrl: "http://localhost:1994", - type: "client", +const INTEGRATIONS: Record = { + client: { + name: "devkit", + redirectUrl: "http://localhost:1994", + type: "client", + }, + web: { + name: "devkit-web", + redirectUrl: "http://localhost:2025", + type: "web", + }, }; const ONET_TOKEN_LABEL = "onet"; +const STATIC_URL = "http://localhost:1995"; +const ASSET_EDITOR_URL = "http://localhost:2030"; const request = async ( method: string, @@ -85,17 +100,14 @@ const setAdmin = async (user: User, session: Session) => { else ok(`auth already has an admin`); }; -const getLicenseToken = async ( - name: string, - session: Session, -): Promise => { - type Hotel = { - hotelId: string; - name: string; - integrations: { integrationId: string; name: string }[]; - }; +type Hotel = { + hotelId: string; + name: string; + integrations: { integrationId: string; name: string }[]; +}; - const getHotel = async () => { +const getHotel = async (name: string, session: Session): Promise => { + const findHotel = async () => { const { data } = await request<{ hotels: Hotel[] }>( "GET", "/user/@me/hotel", @@ -105,76 +117,90 @@ const getLicenseToken = async ( return data?.hotels.find((hotel) => hotel.name === name); }; - let hotel = await getHotel(); - if (!hotel) { - const { status } = await request("POST", "/user/@me/hotel", { - headers: session, - body: { name, public: false }, - }); - - if (status !== 200) fail(`hotel: create failed (${status})`); - - hotel = (await getHotel())!; - ok(`hotel: '${name}' created`); - } else { + const hotel = await findHotel(); + if (hotel) { ok(`hotel: '${name}' already exists`); + return hotel; } - let integration = hotel.integrations.find( - ({ name }) => name === INTEGRATION.name, - ); - if (!integration) { + const { status } = await request("POST", "/user/@me/hotel", { + headers: session, + body: { name, public: true }, + }); + + if (status !== 200) fail(`hotel: create failed (${status})`); + + ok(`hotel: '${name}' created`); + + const createdHotel = await findHotel(); + return createdHotel!; +}; + +const getLicenseToken = async ( + hotel: Hotel, + integration: Integration, + session: Session, +): Promise => { + let integrationId = hotel.integrations.find( + ({ name }) => name === integration.name, + )?.integrationId; + + if (!integrationId) { const { status, data } = await request<{ integrationId: string }>( "POST", "/user/@me/hotel/integration", - { headers: session, body: { hotelId: hotel.hotelId, ...INTEGRATION } }, + { headers: session, body: { hotelId: hotel.hotelId, ...integration } }, ); - if (status !== 200 || !data) fail(`hotel: integration failed (${status})`); + if (status !== 200 || !data) { + fail(`${integration.type}: integration failed (${status})`); + } - integration = { - integrationId: data!.integrationId, - name: INTEGRATION.name, - }; + integrationId = data!.integrationId; } const { data } = await request<{ token: string }>( "GET", - `/user/@me/hotel/integration?hotelId=${hotel.hotelId}&integrationId=${integration.integrationId}`, + `/user/@me/hotel/integration?hotelId=${hotel.hotelId}&integrationId=${integrationId}`, { headers: session }, ); + if (!data?.token) { - fail("hotel: license failed"); + fail(`${integration.type}: license failed`); } - ok("hotel: license generated"); + ok(`${integration.type}: license generated`); return data!.token; }; -const getOnetToken = async (session: Session): Promise => { +const regenerateToken = async ( + name: string, + pathname: "/admin/tokens" | "/admin/apps", + body: { label: string } | { url: string }, + session: Session, +): Promise => { const { status, data } = await request<{ - tokens: { id: string; label: string }[]; - }>("GET", "/admin/tokens", { headers: session }); + tokens: { id: string; label?: string; url?: string }[]; + }>("GET", pathname, { headers: session }); if (status !== 200) { - fail(`onet: listing tokens failed (${status}), not admin?`); + fail(`${name}: listing tokens failed (${status}), not admin?`); } - for (const { id } of data!.tokens.filter( - ({ label }) => label === ONET_TOKEN_LABEL, - )) { - await request("DELETE", `/admin/tokens?id=${id}`, { headers: session }); + const [key, value] = Object.entries(body)[0]; + for (const token of data!.tokens) { + if (token[key as "label" | "url"] !== value) continue; + await request("DELETE", `${pathname}?id=${token.id}`, { headers: session }); } - const { data: created } = await request<{ token: string }>( - "POST", - "/admin/tokens", - { headers: session, body: { label: ONET_TOKEN_LABEL } }, - ); + const { data: created } = await request<{ token: string }>("POST", pathname, { + headers: session, + body, + }); - if (!created?.token) fail("onet: token creation failed"); + if (!created?.token) fail(`${name}: token creation failed`); - ok("onet: token generated"); + ok(`${name}: token generated`); return created!.token; }; @@ -207,15 +233,51 @@ const [owner] = seed.users; const [ownerSession] = sessions; await setAdmin(owner, ownerSession); -const licenseToken = await getLicenseToken(seed.hotel.name, ownerSession); -const onetToken = await getOnetToken(ownerSession); +const hotel = await getHotel(seed.hotel.name, ownerSession); + +const clientLicense = await getLicenseToken( + hotel, + INTEGRATIONS.client, + ownerSession, +); +const webLicense = await getLicenseToken(hotel, INTEGRATIONS.web, ownerSession); +const onetToken = await regenerateToken( + "onet", + "/admin/tokens", + { label: ONET_TOKEN_LABEL }, + ownerSession, +); +const staticToken = await regenerateToken( + "static", + "/admin/apps", + { url: STATIC_URL }, + ownerSession, +); +const assetEditorToken = await regenerateToken( + "asset-editor", + "/admin/apps", + { url: ASSET_EDITOR_URL }, + ownerSession, +); await updateConfig(join(reposDir, "openhotel/app/server/config.yml"), (c) => { - c.auth = { ...c.auth, enabled: true, licenseToken }; + c.auth = { ...c.auth, enabled: true, licenseToken: clientLicense }; }); await updateConfig(join(reposDir, "onet/config.yml"), (c) => { c.auth = { ...c.auth, token: onetToken }; }); +await updateConfig(join(reposDir, "web/app/server/config.yml"), (c) => { + c.auth = { ...c.auth, enabled: true, licenseToken: webLicense }; +}); +await updateConfig(join(reposDir, "static/app/server/config.yml"), (c) => { + c.auth = { ...c.auth, enabled: true, appToken: staticToken }; +}); +await updateConfig( + join(reposDir, "asset-editor/app/server/config.yml"), + (c) => { + c.auth = { ...c.auth, enabled: true, appToken: assetEditorToken }; + }, +); console.log(` Ready! Restart the services to use the new tokens: From 7fa2bedec25b8889ffc26faa7d697f872bb8cdbe Mon Sep 17 00:00:00 2001 From: Alberto Quiles Date: Sun, 4 Oct 2026 12:55:38 +0200 Subject: [PATCH 2/2] feat: add reset script - fix #10 --- README.md | 3 ++ deno.json | 1 + reset.ts | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ setup.ts | 41 +------------------- utils.ts | 50 +++++++++++++++++++++++++ 5 files changed, 166 insertions(+), 39 deletions(-) create mode 100644 reset.ts diff --git a/README.md b/README.md index a47cd9c..ffc503e 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,9 @@ deno task up auth client # some services deno task logs # logs of all services deno task logs client # logs of one service deno task down # stop everything +deno task reset # remove containers, s3 data and databases +deno task reset --configs # ...and the service configs +deno task reset --cache # ...and the deno cache ``` ## S3 diff --git a/deno.json b/deno.json index 90ec6f7..ad5a25e 100644 --- a/deno.json +++ b/deno.json @@ -2,6 +2,7 @@ "tasks": { "setup": "deno run --allow-read --allow-write --allow-run=git,docker setup.ts", "seed": "deno run --allow-read --allow-write --allow-net=localhost:2024 seed.ts", + "reset": "deno run --allow-read --allow-write --allow-run=docker reset.ts", "up": "docker compose up -d --build", "logs": "docker compose logs -f --tail=100", "down": "docker compose down" diff --git a/reset.ts b/reset.ts new file mode 100644 index 0000000..2d315d8 --- /dev/null +++ b/reset.ts @@ -0,0 +1,110 @@ +import { exists, expandGlob } from "@std/fs"; +import { join, relative } from "@std/path"; +import { + getReposDir, + loadEnv, + ok, + readYaml, + ROOT, + SERVICES, + warn, +} from "./utils.ts"; + +const SKIP_CONFIRM = Deno.args.includes("--yes"); +const RESET_CONFIGS = Deno.args.includes("--configs"); +const RESET_CACHE = Deno.args.includes("--cache"); + +const docker = async (...args: string[]): Promise => { + try { + const { success } = await new Deno.Command("docker", { + args, + cwd: ROOT, + stdout: "null", + stderr: "null", + }).output(); + + return success; + } catch { + return false; + } +}; + +const getPaths = async (reposDir: string): Promise => { + const paths: string[] = []; + + for (const service of SERVICES) { + const repoPath = join(reposDir, service.repo); + if (!(await exists(repoPath))) continue; + + const patterns = [ + ...service.data, + ...(RESET_CONFIGS ? [service.config] : []), + ]; + + for (const pattern of patterns) { + for await (const { path } of expandGlob(join(repoPath, pattern))) { + paths.push(path); + } + } + } + + return paths; +}; + +const reposDir = getReposDir(await loadEnv()); +const { name: project } = await readYaml<{ name: string }>( + join(ROOT, "compose.yml"), +); +const volumes = ["s3-data", ...(RESET_CACHE ? ["deno-cache"] : [])]; +const paths = await getPaths(reposDir); + +console.log(`This will remove: + - containers and their node_modules + - docker volumes: ${volumes.join(", ")} +${paths.map((path) => ` - ${relative(reposDir, path)}`).join("\n")} +`); + +if (!SKIP_CONFIRM && !confirm("Continue?")) Deno.exit(0); + +const containersRemoved = await docker( + "compose", + "rm", + "--stop", + "--force", + "--volumes", +); + +const networkRemoved = await docker("compose", "down"); + +if (containersRemoved && networkRemoved) { + ok("containers removed"); +} else { + warn("containers could not be removed"); +} + +for (const volume of volumes) { + const name = `${project}_${volume}`; + + const volumeExists = await docker("volume", "inspect", name); + if (!volumeExists) continue; + + const volumeRemoved = await docker("volume", "rm", name); + if (volumeRemoved) ok(`volume ${volume} removed`); + else warn(`volume ${volume} could not be removed`); +} + +for (const path of paths) { + try { + await Deno.remove(path, { recursive: true }); + ok(`${relative(reposDir, path)} removed`); + } catch (e) { + warn(`${relative(reposDir, path)} could not be removed: ${e}`); + } +} + +console.log(` +Done! Next steps:${RESET_CONFIGS ? "\n 0. deno task setup" : ""} + 1. deno task up auth + 2. deno task seed + 3. deno task up +`); diff --git a/setup.ts b/setup.ts index ba054ac..d4960a0 100644 --- a/setup.ts +++ b/setup.ts @@ -7,48 +7,11 @@ import { ok, readYaml, ROOT, + type Service, + SERVICES, warn, } from "./utils.ts"; -type Service = { - name: string; - repo: string; - config: string; -}; - -const SERVICES: Service[] = [ - { - name: "client", - repo: "openhotel", - config: "app/server/config.yml", - }, - { - name: "auth", - repo: "auth", - config: "app/server/config.yml", - }, - { - name: "onet", - repo: "onet", - config: "config.yml", - }, - { - name: "web", - repo: "web", - config: "app/server/config.yml", - }, - { - name: "asset-editor", - repo: "asset-editor", - config: "app/server/config.yml", - }, - { - name: "static", - repo: "static", - config: "app/server/config.yml", - }, -]; - const run = async (cmd: string, ...args: string[]): Promise => { try { const { success } = await new Deno.Command(cmd, { diff --git a/utils.ts b/utils.ts index 0018874..05af705 100644 --- a/utils.ts +++ b/utils.ts @@ -5,6 +5,56 @@ import { join, resolve } from "@std/path"; export const ROOT = import.meta.dirname!; +export type Service = { + name: string; + repo: string; + config: string; + data: string[]; +}; + +export const SERVICES: Service[] = [ + { + name: "client", + repo: "openhotel", + config: "app/server/config.yml", + data: ["app/server/server-database*", "app/server/database-backups"], + }, + { + name: "auth", + repo: "auth", + config: "app/server/config.yml", + data: [ + "app/server/database*", + "app/server/deleteme-database*", + "app/server/backups", + ], + }, + { + name: "onet", + repo: "onet", + config: "config.yml", + data: ["database*", "collections-key"], + }, + { + name: "web", + repo: "web", + config: "app/server/config.yml", + data: ["app/server/database*", "app/server/backups"], + }, + { + name: "asset-editor", + repo: "asset-editor", + config: "app/server/config.yml", + data: [], + }, + { + name: "static", + repo: "static", + config: "app/server/config.yml", + data: ["app/server/database*", "app/server/files"], + }, +]; + export const ok = (message: string) => console.log(`%c✔ ${message}`, "color: green"); export const warn = (message: string) =>