fix(workshop): harden deployment and deep links (#6542)

This commit is contained in:
Thomas authored and GitHub committed 2026-08-08 17:41:54 +02:00
1 parent 557ebe0a2f
commit 8ab3f136b8
9 files changed
+68 -5

No files matched your search

+2 -1
View File
@@ -95,7 +95,8 @@ irreversible development reset.
GitHub needs only the central callback `https://<workshop-host>/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:
+49
View File
@@ -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",
@@ -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 (
<Layout title="Workshop" description="Community custom CSS and custom widgets for Homarr">
<main className="marketplace bg-background text-foreground min-h-[80vh]">
+1 -1
View File
@@ -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
+1
View File
@@ -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
+1 -1
View File
@@ -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,
+1
View File
@@ -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 }));
@@ -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}`);
+2 -2
View File
@@ -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"
) {