fix(workshop): harden deployment and deep links (#6542)
This commit is contained in:
1 parent
557ebe0a2f
commit
8ab3f136b8
9 files changed
+68
-5
No files matched your search
@@ -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:
|
||||
|
||||
|
||||
@@ -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]">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}`);
|
||||
@@ -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"
|
||||
) {
|
||||
|
||||
Reference in new issue
Block a user