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";