From 8ab3f136b8a10b03579e0d58b2240b3ab8ce2b15 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 8 Aug 2026 17:41:54 +0200 Subject: [PATCH] fix(workshop): harden deployment and deep links (#6542) --- apps/docs/docs/management/workshop/index.mdx | 3 +- apps/docs/docusaurus.config.ts | 49 +++++++++++++++++++ .../src/components/workshop/DetailPage.tsx | 4 ++ apps/workshop/Dockerfile | 2 +- apps/workshop/README.md | 1 + apps/workshop/pb_hooks/workshop-utils.js | 2 +- apps/workshop/pb_hooks/workshop.pb.js | 1 + .../workshop/tests/oauth-sync.integration.mjs | 7 +++ apps/workshop/tests/workshop-identity.mjs | 4 +- 9 files changed, 68 insertions(+), 5 deletions(-) diff --git a/apps/docs/docs/management/workshop/index.mdx b/apps/docs/docs/management/workshop/index.mdx index 4170d1681..7caffcd42 100644 --- a/apps/docs/docs/management/workshop/index.mdx +++ b/apps/docs/docs/management/workshop/index.mdx @@ -95,7 +95,8 @@ irreversible development reset. GitHub needs only the central callback `https:///api/oauth2-redirect`. Keep PocketBase origins unrestricted unless every self-hosted Homarr caller is explicitly listed. Configure both `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET`; supplying only one is a startup error. Set `WORKSHOP_PUBLIC_ORIGIN` in production so generated -links and email use the public host. Configure SMTP for comment, report, and removal notices. +links and email use the public host. Workshop uses the OAuth provider username as the public display name instead of +the profile's full name. Configure SMTP for comment, report, and removal notices. The public URL contract is: diff --git a/apps/docs/docusaurus.config.ts b/apps/docs/docusaurus.config.ts index 0d26722f6..13040b826 100644 --- a/apps/docs/docusaurus.config.ts +++ b/apps/docs/docusaurus.config.ts @@ -345,6 +345,55 @@ const config: Config = { function workshopRoutesPlugin() { return { name: "workshop-routes", + injectHtmlTags() { + return { + headTags: [ + { + tagName: "style", + innerHTML: ` +#workshop-detail-route-fallback { display: none; } +html[data-workshop-detail-loading] #__docusaurus { display: none; } +html[data-workshop-detail-loading] #workshop-detail-route-fallback { + align-items: center; + background: #fff; + box-sizing: border-box; + color: #4b5563; + display: flex; + font: 500 0.875rem/1.5 Inter, system-ui, sans-serif; + justify-content: center; + min-height: 100vh; + padding: 2rem; +} +@media (prefers-color-scheme: dark) { + html[data-workshop-detail-loading] #workshop-detail-route-fallback { + background: #18191a; + color: #d1d5db; + } +}`, + }, + { + tagName: "script", + innerHTML: `(function () { + var match = window.location.pathname.match(/^\\/workshop\\/([^/]+)\\/?$/); + if (match && match[1] !== "admin") { + document.documentElement.setAttribute("data-workshop-detail-loading", ""); + } +})();`, + }, + ], + preBodyTags: [ + { + tagName: "div", + attributes: { + id: "workshop-detail-route-fallback", + role: "status", + "aria-live": "polite", + }, + innerHTML: "Loading Workshop…", + }, + ], + }; + }, async contentLoaded({ actions }) { actions.addRoute({ path: "/workshop/:id", diff --git a/apps/docs/src/components/workshop/DetailPage.tsx b/apps/docs/src/components/workshop/DetailPage.tsx index 7d5c3c360..9c04274b1 100644 --- a/apps/docs/src/components/workshop/DetailPage.tsx +++ b/apps/docs/src/components/workshop/DetailPage.tsx @@ -1018,6 +1018,10 @@ export default function MarketplaceDetailPage() { const { siteConfig } = useDocusaurusContext(); const configuredWorkshopUrl = (siteConfig.customFields?.workshopUrl as string | undefined) ?? ""; + useEffect(() => { + document.documentElement.removeAttribute("data-workshop-detail-loading"); + }, []); + return (
diff --git a/apps/workshop/Dockerfile b/apps/workshop/Dockerfile index a11dd5c3b..ab38f9df5 100644 --- a/apps/workshop/Dockerfile +++ b/apps/workshop/Dockerfile @@ -26,7 +26,7 @@ FROM ${POCKETBASE_IMAGE} AS pocketbase-runtime ENV PORT=8090 RUN addgroup -S pocketbase && adduser -S -G pocketbase pocketbase COPY apps/workshop/entrypoint.sh /usr/local/bin/workshop-entrypoint -RUN chmod +x /usr/local/bin/workshop-entrypoint && mkdir -p /pb_data /pb_public && chown pocketbase:pocketbase /pb_data +RUN chmod 755 /usr/local/bin/workshop-entrypoint && mkdir -p /pb_data /pb_public && chown pocketbase:pocketbase /pb_data ENTRYPOINT ["workshop-entrypoint"] FROM pocketbase-runtime AS development diff --git a/apps/workshop/README.md b/apps/workshop/README.md index 59878a24b..a66208514 100644 --- a/apps/workshop/README.md +++ b/apps/workshop/README.md @@ -90,6 +90,7 @@ GITHUB_CLIENT_SECRET=... OAuth settings synchronize on every PocketBase startup. Restart after rotation. A production deployment must never supply only one credential. `PB_ALLOWED_ORIGINS=*` is appropriate for the central community because arbitrary self-hosted Homarr origins must open its OAuth popup; a restricted list must include every allowed browser origin. +Workshop uses the OAuth provider username as the public display name instead of the profile's full name. ## Promote a Workshop moderator diff --git a/apps/workshop/pb_hooks/workshop-utils.js b/apps/workshop/pb_hooks/workshop-utils.js index abca9ba98..00315df1a 100644 --- a/apps/workshop/pb_hooks/workshop-utils.js +++ b/apps/workshop/pb_hooks/workshop-utils.js @@ -25,7 +25,7 @@ const deriveGithubIdentity = (oauthUser, recordId) => { const githubUsername = GITHUB_USERNAME_PATTERN.test(candidateUsername) && !candidateUsername.includes("--") ? candidateUsername : ""; const displayName = - normalizedIdentityText(oauthUser && oauthUser.name, rawUser.name, githubUsername, rawUser.login).slice(0, 100) || + normalizedIdentityText(githubUsername, rawUser.login, oauthUser && oauthUser.name, rawUser.name).slice(0, 100) || `GitHub user ${String(recordId || "").slice(0, 8)}`; const candidateAvatarUrl = normalizedIdentityText( oauthUser && oauthUser.avatarURL, diff --git a/apps/workshop/pb_hooks/workshop.pb.js b/apps/workshop/pb_hooks/workshop.pb.js index b9e461ae2..3459c794c 100644 --- a/apps/workshop/pb_hooks/workshop.pb.js +++ b/apps/workshop/pb_hooks/workshop.pb.js @@ -11,6 +11,7 @@ onBootstrap((event) => { event.next(); const users = event.app.findCollectionByNameOrId("users"); users.oauth2.enabled = configured; + users.oauth2.mappedFields = { id: "", name: "", username: "displayName", avatarURL: "avatarUrl" }; users.oauth2.providers = configured ? [{ name: "github", clientId, clientSecret }] : []; event.app.save(users); console.log(JSON.stringify({ event: "workshop_oauth_synchronized", enabled: configured })); diff --git a/apps/workshop/tests/oauth-sync.integration.mjs b/apps/workshop/tests/oauth-sync.integration.mjs index 7fb2c1f35..6a34320be 100644 --- a/apps/workshop/tests/oauth-sync.integration.mjs +++ b/apps/workshop/tests/oauth-sync.integration.mjs @@ -23,5 +23,12 @@ const github = users.oauth2?.providers?.find((provider) => provider.name === "gi if (!users.oauth2?.enabled || github?.clientId !== expectedClientId) { throw new Error(`GitHub OAuth was not synchronized for ${expectedClientId}`); } +if ( + users.oauth2?.mappedFields?.username !== "displayName" || + users.oauth2?.mappedFields?.name || + users.oauth2?.mappedFields?.avatarURL !== "avatarUrl" +) { + throw new Error(`GitHub OAuth field mapping was not synchronized: ${JSON.stringify(users.oauth2?.mappedFields)}`); +} console.log(`Workshop OAuth synchronization passed for ${expectedClientId}`); diff --git a/apps/workshop/tests/workshop-identity.mjs b/apps/workshop/tests/workshop-identity.mjs index ecd72e296..cac345b60 100644 --- a/apps/workshop/tests/workshop-identity.mjs +++ b/apps/workshop/tests/workshop-identity.mjs @@ -13,7 +13,7 @@ const identity = deriveGithubIdentity( "record123456", ); if ( - identity.displayName !== "Octo Cat" || + identity.displayName !== "octocat" || identity.githubUsername !== "octocat" || identity.githubProfileUrl !== "https://github.com/octocat" || identity.avatarUrl !== "https://avatars.githubusercontent.com/u/1?v=4" @@ -35,7 +35,7 @@ const rawFallback = deriveGithubIdentity( "record123456", ); if ( - rawFallback.displayName !== "Raw User" || + rawFallback.displayName !== "raw-user" || rawFallback.githubUsername !== "raw-user" || rawFallback.githubProfileUrl !== "https://github.com/raw-user" ) {