diff --git a/apps/docs/docs/management/workshop/index.mdx b/apps/docs/docs/management/workshop/index.mdx index 977ec05ff..956e516fb 100644 --- a/apps/docs/docs/management/workshop/index.mdx +++ b/apps/docs/docs/management/workshop/index.mdx @@ -37,4 +37,4 @@ Signed-in members can vote to help useful creations rank higher. Report maliciou Workshop accounts use GitHub OAuth and are separate from users in a self-hosted Homarr instance. Workshop moderators can remove submissions, prevent an account from publishing, disable an account, and resolve reports. These actions require a reason and are recorded. -The Workshop is a central service operated by Homarr at `store.homarr.dev`. Running the backend locally is supported for contributors developing or testing this feature, not as a private production marketplace. +The Workshop is a central service operated by Homarr at `workshop.homarr.dev`. Running the backend locally is supported for contributors developing or testing this feature, not as a private production marketplace. diff --git a/apps/docs/docusaurus.config.ts b/apps/docs/docusaurus.config.ts index 354da65aa..e35182bff 100644 --- a/apps/docs/docusaurus.config.ts +++ b/apps/docs/docusaurus.config.ts @@ -17,7 +17,8 @@ const config: Config = { locales: ["en"], }, customFields: { - workshopApiUrl: process.env.NEXT_PUBLIC_WORKSHOP_API_URL ?? "https://store.homarr.dev", + workshopApiUrl: + process.env.WORKSHOP_API_URL ?? process.env.NEXT_PUBLIC_WORKSHOP_API_URL ?? "https://workshop.homarr.dev", }, onBrokenLinks: "throw", onBrokenAnchors: "throw", diff --git a/apps/store/.env.example b/apps/store/.env.example index f3070f59d..6d409e25b 100644 --- a/apps/store/.env.example +++ b/apps/store/.env.example @@ -3,3 +3,4 @@ GITHUB_CLIENT_SECRET= PB_SUPERUSER_EMAIL= PB_SUPERUSER_PASSWORD= PB_EXPOSE_PORT=8090 +PB_ALLOWED_ORIGINS=http://localhost:3003,http://127.0.0.1:3003 diff --git a/apps/store/README.md b/apps/store/README.md index 9fe923184..abaeb505c 100644 --- a/apps/store/README.md +++ b/apps/store/README.md @@ -1,6 +1,6 @@ # Homarr Workshop backend -PocketBase provides the central Workshop API at `store.homarr.dev`. It is not a per-instance Homarr service. Local deployment is for development and integration testing. +PocketBase provides the central Workshop API at `workshop.homarr.dev`. It is not a per-instance Homarr service. Local deployment is for development and integration testing. ## Local startup @@ -15,6 +15,8 @@ Use `pnpm dev:workshop:backend` when only the API is needed. If port 8090 is una The image downloads the official PocketBase `0.39.6` release and verifies it against that release's `checksums.txt`. Migrations and hooks are mounted read-only. +GitHub OAuth configuration is synchronized from the environment on every boot, including for an existing development volume. Local CORS is restricted to the Docusaurus origins in `PB_ALLOWED_ORIGINS`; production must set it to `https://homarr.dev`. + ## Roles and account states - `member`: publish, update/delete owned submissions, vote, and report. diff --git a/apps/store/docker-compose.yml b/apps/store/docker-compose.yml index 03d8d278f..21d1d6f6f 100644 --- a/apps/store/docker-compose.yml +++ b/apps/store/docker-compose.yml @@ -12,6 +12,7 @@ services: PB_SUPERUSER_EMAIL: ${PB_SUPERUSER_EMAIL:-} PB_SUPERUSER_PASSWORD: ${PB_SUPERUSER_PASSWORD:-} PB_PUBLIC_URL: http://localhost:${PB_EXPOSE_PORT:-8090} + PB_ALLOWED_ORIGINS: ${PB_ALLOWED_ORIGINS:-http://localhost:3003,http://127.0.0.1:3003} ports: - "127.0.0.1:${PB_EXPOSE_PORT:-8090}:8090" volumes: diff --git a/apps/store/entrypoint.sh b/apps/store/entrypoint.sh index 8c5cdd3a9..90271fef3 100644 --- a/apps/store/entrypoint.sh +++ b/apps/store/entrypoint.sh @@ -12,6 +12,7 @@ echo "Workshop website: http://localhost:3003/workshop" exec su-exec pocketbase pocketbase serve \ --http=0.0.0.0:8090 \ + --origins="${PB_ALLOWED_ORIGINS:-*}" \ --dir=/pb_data \ --hooksDir=/pb_hooks \ --migrationsDir=/pb_migrations diff --git a/apps/store/pb_hooks/workshop.pb.js b/apps/store/pb_hooks/workshop.pb.js index 8041e97bc..41af477d4 100644 --- a/apps/store/pb_hooks/workshop.pb.js +++ b/apps/store/pb_hooks/workshop.pb.js @@ -1,5 +1,15 @@ /// +onBootstrap((e) => { + e.next(); + const users = e.app.findCollectionByNameOrId("users"); + const clientId = $os.getenv("GITHUB_CLIENT_ID"); + const clientSecret = $os.getenv("GITHUB_CLIENT_SECRET"); + users.oauth2.enabled = Boolean(clientId && clientSecret); + users.oauth2.providers = clientId && clientSecret ? [{ name: "github", clientId, clientSecret }] : []; + e.app.save(users); +}); + onRecordAuthWithOAuth2Request((e) => { const utils = require(`${__hooks}/workshop-utils.js`); if (e.record && e.record.getString("state") === "disabled") diff --git a/apps/store/tests/run-integration.sh b/apps/store/tests/run-integration.sh index 4263580e9..f13af1bcc 100755 --- a/apps/store/tests/run-integration.sh +++ b/apps/store/tests/run-integration.sh @@ -15,6 +15,8 @@ cleanup PB_EXPOSE_PORT="$port" \ PB_SUPERUSER_EMAIL="$email" \ PB_SUPERUSER_PASSWORD="$password" \ +GITHUB_CLIENT_ID="integration-client" \ +GITHUB_CLIENT_SECRET="integration-secret" \ docker compose --file apps/store/docker-compose.yml --project-name "$project" up --detach --build --wait if ! WORKSHOP_TEST_URL="http://127.0.0.1:$port" \ diff --git a/apps/store/tests/workshop.integration.mjs b/apps/store/tests/workshop.integration.mjs index ca26a658e..3ff7e9343 100644 --- a/apps/store/tests/workshop.integration.mjs +++ b/apps/store/tests/workshop.integration.mjs @@ -9,6 +9,12 @@ assert(baseUrl && superuserEmail && superuserPassword, "Workshop integration tes const root = new PocketBase(baseUrl); await root.collection("_superusers").authWithPassword(superuserEmail, superuserPassword); +const authMethods = await root.collection("users").listAuthMethods(); +assert.equal(authMethods.oauth2.enabled, true, "GitHub OAuth must be configured from the runtime environment"); +assert.deepEqual( + authMethods.oauth2.providers.map((provider) => provider.name), + ["github"], +); const createUser = async (name, role = "member", state = "active") => { const password = `Workshop${name}Password123!`; diff --git a/audit.md b/audit.md index c02c3d0c9..4152fc2b1 100644 --- a/audit.md +++ b/audit.md @@ -36,7 +36,7 @@ The abandoned prototype is preserved at `origin/feat/widget-store-prototype-back - Launch content: custom widgets (`homarr-custom-widget-v2`) and CSS (`homarr-custom-css-v1`). - Identity: GitHub OAuth only. - Community features: votes and reports; comments are removed. -- Hosting: one Homarr-operated service at `store.homarr.dev`; overrides are development-only. +- Hosting: one Homarr-operated service at `workshop.homarr.dev`; overrides are development-only. - Publishing: immediate, with source inspection, reporting, and staff removal. - Staff: GitHub-authenticated moderators/admins use `/workshop/admin`; PocketBase superusers are bootstrap/emergency access. diff --git a/package.json b/package.json index 625d32a28..2cf612019 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "dev:docs": "pnpm -F @homarr/docs dev", "dev:workshop": "concurrently --kill-others --names workshop-api,workshop-web \"pnpm dev:workshop:backend\" \"pnpm dev:workshop:website\"", "dev:workshop:backend": "docker compose -f apps/store/docker-compose.yml up --build --remove-orphans", - "dev:workshop:website": "cross-env NEXT_PUBLIC_WORKSHOP_API_URL=http://localhost:8090 pnpm -F @homarr/docs dev", + "dev:workshop:website": "pnpm -F @homarr/workshop build && pnpm -F @homarr/docs clear && cross-env WORKSHOP_API_URL=http://localhost:8090 NEXT_PUBLIC_WORKSHOP_API_URL=http://localhost:8090 pnpm -F @homarr/docs dev", "docker:dev": "docker compose -f ./development/development.docker-compose.yml up", "format": "turbo format --continue -- --check", "format:fix": "turbo format --continue", diff --git a/packages/workshop/src/client.ts b/packages/workshop/src/client.ts index a96b4b323..ea457eaad 100644 --- a/packages/workshop/src/client.ts +++ b/packages/workshop/src/client.ts @@ -109,6 +109,18 @@ export class WorkshopClient { } async signInWithGitHub() { + let methods: Awaited["listAuthMethods"]>>; + try { + methods = await this.pocketBase.collection("users").listAuthMethods(); + } catch (error) { + throw asWorkshopError(error, "Failed to load Workshop authentication methods"); + } + if (!methods.oauth2.providers.some((provider) => provider.name === "github")) { + throw new WorkshopError( + "unavailable", + "GitHub login is not configured. Set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET, then restart Workshop.", + ); + } try { await this.pocketBase.collection("users").authWithOAuth2({ provider: "github" }); return this.currentUser; diff --git a/packages/workshop/src/schema.ts b/packages/workshop/src/schema.ts index f864afaf3..d348685f7 100644 --- a/packages/workshop/src/schema.ts +++ b/packages/workshop/src/schema.ts @@ -1,7 +1,7 @@ import { customWidgetImportSchema } from "@homarr/validation/custom-widget"; import { z } from "zod"; -export const WORKSHOP_API_URL = "https://store.homarr.dev"; +export const WORKSHOP_API_URL = "https://workshop.homarr.dev"; export const WORKSHOP_WEB_URL = "https://homarr.dev/workshop"; export const WIDGET_SCHEMA_VERSION = "homarr-custom-widget-v2"; export const CSS_SCHEMA_VERSION = "homarr-custom-css-v1";