Skip to content

Conversation

@appflowy
Copy link
Contributor

@appflowy appflowy commented Dec 9, 2025

Description


Checklist

General

  • I've included relevant documentation or comments for the changes introduced.
  • I've tested the changes in multiple environments (e.g., different browsers, operating systems).

Testing

  • I've added or updated tests to validate the changes introduced for AppFlowy Web.

Feature-Specific

  • For feature additions, I've added a preview (video, screenshot, or demo) in the "Feature Preview" section.
  • I've verified that this feature integrates seamlessly with existing functionality.

Summary by Sourcery

Serve static assets from the built dist directory in the SSR server, adjust publish namespace editability rules, and update local/CI dev server configuration.

New Features:

  • Add a static file route to serve marketing and asset files directly from the SSR dist directory.

Bug Fixes:

  • Tighten namespace editability checks so only workspace owners on eligible plans and hosts can update the namespace, and disable related UI for non-editable namespaces.

Enhancements:

  • Detect the appropriate dist directory location for dev vs production deployments.
  • Hide home page settings when the publish namespace is not editable (e.g., UUID namespaces).

Build:

  • Add a dev:server script to start the Bun SSR server for local development.

CI:

  • Run the Bun-based SSR server in integration tests instead of the Vite preview server, including Bun setup and cleanup changes.

@sourcery-ai
Copy link

sourcery-ai bot commented Dec 9, 2025

Reviewer's Guide

Adds an SSR static file route for serving built assets from the dist directory, adjusts dist path resolution for dev vs prod, tightens publish namespace editability rules, and updates CI integration tests to run against the Bun-based SSR server instead of the Vite preview server.

Sequence diagram for SSR static file handling route

sequenceDiagram
  actor Browser
  participant SSRServer
  participant staticRoute
  participant marketingRoute
  participant publishRoute
  participant FileSystem as FileSystem

  Browser->>SSRServer: HTTP GET /static/app.js
  SSRServer->>staticRoute: handle(RequestContext)
  alt request_method_not_GET
    staticRoute-->>SSRServer: undefined
    SSRServer->>marketingRoute: handle(RequestContext)
  else not_static_path_or_file
    staticRoute-->>SSRServer: undefined
    SSRServer->>marketingRoute: handle(RequestContext)
  else static_path_or_file
    staticRoute->>FileSystem: resolve(distDir, pathname)
    alt path_traversal_detected
      staticRoute-->>SSRServer: Response 403 Forbidden
      SSRServer-->>Browser: 403 Forbidden
    else path_valid
      FileSystem-->>staticRoute: file bytes or error
      alt file_found
        staticRoute-->>SSRServer: Response 200 with ContentType
        SSRServer-->>Browser: 200 OK (static asset)
      else file_not_found
        staticRoute-->>SSRServer: undefined
        SSRServer->>marketingRoute: handle(RequestContext)
      end
    end
  end

  opt marketing_route_does_not_handle
    SSRServer->>publishRoute: handle(RequestContext)
  end
Loading

Class diagram for updated publish namespace and homepage settings

classDiagram
  class PublishManage {
    +onClose() void
    -isOwner boolean
    -activeSubscription SubscriptionPlan|null
    -namespace string
    -handleRemoveHomePage() void
    -handleUpdateHomePage() void
    -render() void
  }

  class HomePageSetting {
    +spaceId string
    +namespace string
    +siteId string
    +homePage View|null
    +publishViews View[]
    +isOwner boolean
    +activePlan SubscriptionPlan|null
    +canEdit boolean
    -removeLoading boolean
    -updateLoading boolean
    -onRemoveHomePage() void
    -onUpdateHomePage() void
    -render() void
  }

  class SubscriptionPlan {
  }

  class View {
  }

  PublishManage o-- HomePageSetting : uses
  HomePageSetting --> SubscriptionPlan : activePlan
  HomePageSetting --> View : publishViews

  class NamespaceEditGuard {
    +canEditNamespace(namespace string) boolean
    +hasEditPermission(isOwner boolean, isOfficialHost boolean, activeSubscription SubscriptionPlan|null) boolean
  }

  PublishManage ..> NamespaceEditGuard : computes_canEdit

  class TooltipBehavior {
    +getTooltipTitle(isOwner boolean, isOfficialHost boolean, activeSubscription SubscriptionPlan|null) string|null
    +isEditBlocked(isOwner boolean, isOfficialHost boolean, activeSubscription SubscriptionPlan|null) boolean
  }

  PublishManage ..> TooltipBehavior : uses

  class UuidValidator {
    +uuidValidate(value string) boolean
  }

  PublishManage ..> UuidValidator : determines_canEdit
Loading

File-Level Changes

Change Details Files
Serve static assets from the SSR server with path traversal protection and MIME type handling.
  • Introduce a static route handler that serves files from the dist directory for specific static URL prefixes and files.
  • Add a MIME type lookup table and use file extensions to set appropriate Content-Type headers.
  • Guard against non-GET requests, non-static paths, and path traversal by verifying resolved paths stay within distDir.
  • Wire the static route into the route chain ahead of the marketing/publish routes.
deploy/routes.ts
Make dist directory resolution work both in deploy (prod) and root (dev) environments.
  • Define separate production and development dist directory paths and choose between them based on existence.
  • Export distDir using the selected path and update indexPath to be based on this directory.
deploy/config.ts
Run integration tests against the Bun-based SSR server in CI instead of the Vite preview server.
  • Install Bun in the integration-test GitHub Actions workflow and install required SSR dependencies via bun.
  • Start the SSR server using pnpm run dev:server, wait for it to become healthy, and store its PID for later cleanup.
  • Adjust cleanup to kill the SSR server process and bun deploy/server.ts instead of the vite preview server.
.github/workflows/integration-test.yml
package.json
Tighten publish namespace editability and homepage settings visibility based on owner, plan, and namespace format.
  • Pass a canEdit flag to the HomePageSetting component depending on whether the namespace is a UUID (non-editable).
  • Hide the homepage settings UI entirely when canEdit is false.
  • Update tooltip and click-guard logic so only workspace owners on official hosts with a non-free, loaded subscription can modify the namespace; self-hosted instances remain unrestricted by plan.
src/components/app/publish-manage/PublishManage.tsx
src/components/app/publish-manage/HomePageSetting.tsx

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes and found some issues that need to be addressed.

  • The staticRoute handler uses fs.readFileSync, which will block the event loop for larger files; consider switching to fs.promises.readFile or a streaming approach to avoid synchronous disk I/O in the request path.
  • HomePageSetting still accepts an isOwner prop that is no longer used (other than in a useEffect dependency), which can cause confusion; either remove the prop entirely or wire it back into the logic where it meaningfully affects behavior.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `staticRoute` handler uses `fs.readFileSync`, which will block the event loop for larger files; consider switching to `fs.promises.readFile` or a streaming approach to avoid synchronous disk I/O in the request path.
- `HomePageSetting` still accepts an `isOwner` prop that is no longer used (other than in a `useEffect` dependency), which can cause confusion; either remove the prop entirely or wire it back into the logic where it meaningfully affects behavior.

## Individual Comments

### Comment 1
<location> `deploy/routes.ts:59` </location>
<code_context>
+  }
+
+  try {
+    const file = fs.readFileSync(filePath);
+    const ext = path.extname(filePath);
+    const contentType = MIME_TYPES[ext] || 'application/octet-stream';
</code_context>

<issue_to_address>
**suggestion (performance):** Using synchronous fs.readFileSync in an async route handler can block the event loop.

Since this handler is async, using `fs.readFileSync` will block the event loop during disk I/O and can hurt throughput under load or with larger files. Prefer an async alternative like `await fs.promises.readFile(filePath)` (or Bun’s async API) to keep this path non-blocking.

Suggested implementation:

```typescript
  try {
    const file = await fs.promises.readFile(filePath);
    const ext = path.extname(filePath);
    const contentType = MIME_TYPES[ext] || 'application/octet-stream';

    return new Response(file, {
      headers: { 'Content-Type': contentType },
    });
  } catch {
    logger.warn(`Static file not found: ${filePath}`);
    return;
  }
};

```

1. Ensure that the enclosing route handler function is declared as `async` (e.g., `export default async function ...`), otherwise `await` will not be allowed.
2. Confirm that `fs` is imported from Node's `fs` module in a way that supports `fs.promises`, e.g.: `import fs from 'fs';` or `import * as fs from 'fs';`. If it is currently destructured (e.g., `import { readFileSync } from 'fs';`), adjust the import to allow `fs.promises.readFile`.
</issue_to_address>

### Comment 2
<location> `deploy/routes.ts:53` </location>
<code_context>
+  // Prevent path traversal attacks - ensure resolved path stays within distDir
+  const normalizedDistDir = path.resolve(distDir);
+
+  if (!filePath.startsWith(normalizedDistDir + path.sep) && filePath !== normalizedDistDir) {
+    logger.warn(`Path traversal attempt blocked: ${url.pathname}`);
+    return new Response('Forbidden', { status: 403 });
</code_context>

<issue_to_address>
**🚨 issue (security):** Path traversal protection based on string startsWith can be brittle across platforms.

`startsWith` assumes consistent casing and path semantics, which can be bypassed on case‑insensitive filesystems or via symlinks. A safer pattern is to compare normalized, relative paths instead of raw prefixes, e.g.:

```ts
const rel = path.relative(normalizedDistDir, filePath);
if (rel.startsWith('..') || path.isAbsolute(rel)) {
  logger.warn(`Path traversal attempt blocked: ${url.pathname}`);
  return new Response('Forbidden', { status: 403 });
}
```
This enforces containment within `distDir` in a more portable way.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

// Prevent path traversal attacks - ensure resolved path stays within distDir
const normalizedDistDir = path.resolve(distDir);

if (!filePath.startsWith(normalizedDistDir + path.sep) && filePath !== normalizedDistDir) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 issue (security): Path traversal protection based on string startsWith can be brittle across platforms.

startsWith assumes consistent casing and path semantics, which can be bypassed on case‑insensitive filesystems or via symlinks. A safer pattern is to compare normalized, relative paths instead of raw prefixes, e.g.:

const rel = path.relative(normalizedDistDir, filePath);
if (rel.startsWith('..') || path.isAbsolute(rel)) {
  logger.warn(`Path traversal attempt blocked: ${url.pathname}`);
  return new Response('Forbidden', { status: 403 });
}

This enforces containment within distDir in a more portable way.

@appflowy appflowy merged commit 46c2593 into main Dec 9, 2025
12 checks passed
@appflowy appflowy deleted the fix_namespace_check branch December 9, 2025 10:38
josue693 pushed a commit to josue693/AppFlowy-Web that referenced this pull request Dec 21, 2025
* chore: fix namespace check

* chore: lint

* chore: add test
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants