mirror of
https://github.com/nexu-io/open-design.git
synced 2026-09-28 21:45:18 +08:00
Attach workspace identity headers to publish-public, restore, delete, and rename requests
Same enforceWorkspaceProjectMutation / canShareProjectsForRequest bypass as the earlier duplicate/design-system-copy/delete fixes: publishing a file publicly, restoring a file version, deleting a file (including brand logo/image cleanup), and renaming a file all sent no workspace headers, so the daemon fell back to a headerless permission read (default-deny for sharing, default-allow for the ownership gate) instead of the caller's real identity.
This commit is contained in:
@@ -2864,6 +2864,7 @@ function FileVersionManagerModal({
|
||||
}) {
|
||||
const { locale, t } = useI18n();
|
||||
const analytics = useAnalytics();
|
||||
const { context: workspaceContext } = useWorkspaceContext();
|
||||
const tRef = useRef(t);
|
||||
const [versions, setVersions] = useState<ProjectFileVersion[]>([]);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
@@ -3356,7 +3357,12 @@ function FileVersionManagerModal({
|
||||
});
|
||||
};
|
||||
try {
|
||||
const result = await restoreProjectFileVersion(projectId, file.name, selectedVersion);
|
||||
const result = await restoreProjectFileVersion(
|
||||
projectId,
|
||||
file.name,
|
||||
selectedVersion,
|
||||
workspaceContext,
|
||||
);
|
||||
if (!result) {
|
||||
fireRestoreResult('failed', 'restore_request_failed');
|
||||
setError(t('fileViewer.versions.restoreFailed'));
|
||||
|
||||
@@ -2340,7 +2340,7 @@ export function FileWorkspace({
|
||||
async function handleDelete(name: string) {
|
||||
if (viewerOnly) return; // read-only viewer of a team-shared project
|
||||
if (!confirm(t('workspace.deleteFileConfirm', { name }))) return;
|
||||
const ok = await deleteProjectFile(projectId, name);
|
||||
const ok = await deleteProjectFile(projectId, name, workspaceContext);
|
||||
if (ok) {
|
||||
await onRefreshFiles();
|
||||
const nextTabs = persistedTabs.filter((n) => n !== name);
|
||||
@@ -2376,7 +2376,7 @@ export function FileWorkspace({
|
||||
const deleted: string[] = [];
|
||||
const failed: string[] = [];
|
||||
for (const name of names) {
|
||||
const ok = await deleteProjectFile(projectId, name);
|
||||
const ok = await deleteProjectFile(projectId, name, workspaceContext);
|
||||
if (ok) deleted.push(name);
|
||||
else failed.push(name);
|
||||
}
|
||||
@@ -2419,7 +2419,7 @@ export function FileWorkspace({
|
||||
);
|
||||
}
|
||||
|
||||
const result = await renameProjectFile(projectId, oldName, nextName);
|
||||
const result = await renameProjectFile(projectId, oldName, nextName, workspaceContext);
|
||||
const renamed = result.file;
|
||||
await onRefreshFiles();
|
||||
await refreshProjectFolders();
|
||||
@@ -4044,6 +4044,7 @@ function DesignSystemProjectPanel({
|
||||
}) {
|
||||
const t = useT();
|
||||
const analytics = useAnalytics();
|
||||
const { context: workspaceContext } = useWorkspaceContext();
|
||||
const [reviewDecisions, setReviewDecisions] = useState<Record<string, DesignSystemReviewDecision>>({});
|
||||
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({});
|
||||
const [feedbackSection, setFeedbackSection] = useState<string | null>(null);
|
||||
@@ -4297,7 +4298,7 @@ function DesignSystemProjectPanel({
|
||||
setKitActionBusy(`delete-logo:${index}`);
|
||||
notifyKitLoading(t('ds.deleteLogo'));
|
||||
try {
|
||||
const ok = await deleteBrandLogo(projectId, index);
|
||||
const ok = await deleteBrandLogo(projectId, index, workspaceContext);
|
||||
if (!ok) throw new Error(t('ds.actionFailed'));
|
||||
await refreshKitDependencies({ finalizeBrand: true });
|
||||
notifyKit('success', t('ds.actionDone'));
|
||||
@@ -4313,7 +4314,7 @@ function DesignSystemProjectPanel({
|
||||
setKitActionBusy(`delete-image:${index}`);
|
||||
notifyKitLoading(t('ds.deleteImage', { caption: '' }).trim());
|
||||
try {
|
||||
const ok = await deleteBrandImage(projectId, index);
|
||||
const ok = await deleteBrandImage(projectId, index, workspaceContext);
|
||||
if (!ok) throw new Error(t('ds.actionFailed'));
|
||||
await refreshKitDependencies({ finalizeBrand: true });
|
||||
notifyKit('success', t('ds.actionDone'));
|
||||
|
||||
@@ -23,6 +23,7 @@ import type {
|
||||
RestoreProjectFileVersionResponse,
|
||||
SocialShareRequest,
|
||||
SocialShareResponse,
|
||||
WorkspaceCollabContext,
|
||||
} from '@open-design/contracts';
|
||||
import type {
|
||||
AgentInfo,
|
||||
@@ -77,6 +78,7 @@ import {
|
||||
openHostExternalUrl,
|
||||
} from '@open-design/host';
|
||||
import { coalescedGet } from '../lib/coalesced-get';
|
||||
import { workspaceProjectHeaders } from '../state/projects';
|
||||
|
||||
export const DEFAULT_DEPLOY_PROVIDER_ID = 'vercel-self';
|
||||
export const CLOUDFLARE_PAGES_PROVIDER_ID = 'cloudflare-pages';
|
||||
@@ -1427,10 +1429,18 @@ export async function deployProjectFile(
|
||||
export async function publishProjectFilePublic(
|
||||
projectId: string,
|
||||
fileName: string,
|
||||
workspaceContext?: WorkspaceCollabContext | null,
|
||||
): Promise<WebPublicProjectFileResponse> {
|
||||
// Carry the active workspace identity so the daemon's `canShareProjectsForRequest`
|
||||
// gate (apps/daemon/src/routes/collab-sync.ts) reads the real permission bit
|
||||
// instead of falling back to a headerless context read — see
|
||||
// workspaceProjectHeaders' call sites in state/projects.ts for the same pattern.
|
||||
const resp = await fetch(
|
||||
`/api/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(fileName)}/publish-public`,
|
||||
{ method: 'POST' },
|
||||
{
|
||||
method: 'POST',
|
||||
...(workspaceContext ? { headers: workspaceProjectHeaders(workspaceContext) } : {}),
|
||||
},
|
||||
);
|
||||
if (!resp.ok) {
|
||||
const payload = (await resp.json().catch(() => null)) as
|
||||
@@ -1450,9 +1460,11 @@ export async function publishProjectFilePublic(
|
||||
export async function fetchProjectFilePublicPublication(
|
||||
projectId: string,
|
||||
fileName: string,
|
||||
workspaceContext?: WorkspaceCollabContext | null,
|
||||
): Promise<WebPublicProjectFileResponse | null> {
|
||||
const resp = await fetch(
|
||||
`/api/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(fileName)}/publish-public`,
|
||||
workspaceContext ? { headers: workspaceProjectHeaders(workspaceContext) } : undefined,
|
||||
);
|
||||
if (!resp.ok) {
|
||||
const payload = (await resp.json().catch(() => null)) as
|
||||
@@ -1474,12 +1486,16 @@ export async function unpublishProjectFilePublic(
|
||||
projectId: string,
|
||||
fileName: string,
|
||||
slug: string,
|
||||
workspaceContext?: WorkspaceCollabContext | null,
|
||||
): Promise<{ ok: true; slug: string; fileName: string }> {
|
||||
const resp = await fetch(
|
||||
`/api/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(fileName)}/publish-public`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
...(workspaceContext ? workspaceProjectHeaders(workspaceContext) : {}),
|
||||
},
|
||||
body: JSON.stringify({ slug }),
|
||||
},
|
||||
);
|
||||
@@ -1959,13 +1975,17 @@ export async function restoreProjectFileVersion(
|
||||
projectId: string,
|
||||
name: string,
|
||||
version: Pick<ProjectFileVersion, 'id'>,
|
||||
workspaceContext?: WorkspaceCollabContext | null,
|
||||
): Promise<RestoreProjectFileVersionResponse | null> {
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`${projectFileVersionsUrl(projectId, name)}/${encodeURIComponent(version.id)}/restore`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(workspaceContext ? workspaceProjectHeaders(workspaceContext) : {}),
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
},
|
||||
);
|
||||
@@ -2309,11 +2329,15 @@ function looksLikeImage(name: string): boolean {
|
||||
export async function deleteProjectFile(
|
||||
projectId: string,
|
||||
name: string,
|
||||
workspaceContext?: WorkspaceCollabContext | null,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const resp = await fetch(
|
||||
projectRawUrl(projectId, name),
|
||||
{ method: 'DELETE' },
|
||||
{
|
||||
method: 'DELETE',
|
||||
...(workspaceContext ? { headers: workspaceProjectHeaders(workspaceContext) } : {}),
|
||||
},
|
||||
);
|
||||
return resp.ok;
|
||||
} catch {
|
||||
@@ -2325,10 +2349,14 @@ export async function renameProjectFile(
|
||||
projectId: string,
|
||||
from: string,
|
||||
to: string,
|
||||
workspaceContext?: WorkspaceCollabContext | null,
|
||||
): Promise<RenameProjectFileResponse> {
|
||||
const resp = await fetch(`/api/projects/${encodeURIComponent(projectId)}/files/rename`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(workspaceContext ? workspaceProjectHeaders(workspaceContext) : {}),
|
||||
},
|
||||
body: JSON.stringify({ from, to }),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Brand, BrandColorRole } from '@open-design/contracts';
|
||||
import type { Brand, BrandColorRole, WorkspaceCollabContext } from '@open-design/contracts';
|
||||
import {
|
||||
deleteProjectFile,
|
||||
fetchProjectFileText,
|
||||
@@ -104,7 +104,11 @@ export function replaceDesignMdColorAtIndex(body: string, index: number, hex: st
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function deleteBrandLogo(projectId: string, index: number): Promise<boolean> {
|
||||
export async function deleteBrandLogo(
|
||||
projectId: string,
|
||||
index: number,
|
||||
workspaceContext?: WorkspaceCollabContext | null,
|
||||
): Promise<boolean> {
|
||||
let fileToDelete: string | null = null;
|
||||
const ok = await patchBrand(projectId, (brand) => {
|
||||
const logo = brand.logo;
|
||||
@@ -119,18 +123,22 @@ export async function deleteBrandLogo(projectId: string, index: number): Promise
|
||||
fileToDelete = relativeProjectAssetPath(alternates[index - 1]);
|
||||
logo.alternates = alternates.filter((_, i) => i !== index - 1);
|
||||
});
|
||||
if (ok && fileToDelete) await deleteProjectFile(projectId, fileToDelete);
|
||||
if (ok && fileToDelete) await deleteProjectFile(projectId, fileToDelete, workspaceContext);
|
||||
return ok;
|
||||
}
|
||||
|
||||
export async function deleteBrandImage(projectId: string, index: number): Promise<boolean> {
|
||||
export async function deleteBrandImage(
|
||||
projectId: string,
|
||||
index: number,
|
||||
workspaceContext?: WorkspaceCollabContext | null,
|
||||
): Promise<boolean> {
|
||||
let fileToDelete: string | null = null;
|
||||
const ok = await patchBrand(projectId, (brand) => {
|
||||
if (!brand.imagery?.samples) return;
|
||||
fileToDelete = relativeProjectAssetPath(brand.imagery.samples[index]?.file);
|
||||
brand.imagery.samples = brand.imagery.samples.filter((_, i) => i !== index);
|
||||
});
|
||||
if (ok && fileToDelete) await deleteProjectFile(projectId, fileToDelete);
|
||||
if (ok && fileToDelete) await deleteProjectFile(projectId, fileToDelete, workspaceContext);
|
||||
return ok;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user