Skip to content

Dev#201

Merged
InfinityBowman merged 50 commits into
mainfrom
dev
Jan 1, 2026
Merged

Dev#201
InfinityBowman merged 50 commits into
mainfrom
dev

Conversation

@InfinityBowman
Copy link
Copy Markdown
Owner

@InfinityBowman InfinityBowman commented Dec 30, 2025

Summary by CodeRabbit

  • New Features

    • Added multi-tenant organization (workspace) support with creation and switching capabilities.
    • Introduced organization-scoped projects; projects now belong to organizations.
    • Added workspace switcher in the navigation bar for easy organization switching.
    • Enhanced sidebar with mobile overlay support and resizable width on desktop.
  • Improvements

    • Updated invitation flow to support combined organization and project membership.
    • Reorganized frontend routing to use organization context throughout the app.
  • Documentation

    • Added comprehensive Organizations Guide with best practices and patterns.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 30, 2025

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

📝 Walkthrough

Walkthrough

This PR introduces a comprehensive multi-tenant organization model to CoRATES, adding organization entities with membership hierarchies, org-scoped API routes with new authorization middleware, organization context primitives for the frontend, database schema changes with migrations, and extensive documentation covering organization patterns, routing, and API design.

Changes

Cohort / File(s) Summary
Documentation & Guides
.cursor/rules/api-routes.mdc, .cursor/rules/corates.mdc, .cursor/rules/organizations.mdc, .cursor/rules/workers.mdc, packages/docs/.vitepress/config.js, packages/docs/guides/index.md, packages/docs/guides/organizations.md, packages/docs/guides/api-development.md, packages/docs/guides/authentication.md, packages/docs/guides/database.md, packages/docs/guides/primitives.md, packages/docs/architecture/diagrams/04-data-model.md, packages/docs/architecture/diagrams/05-frontend-routes.md, packages/docs/architecture/diagrams/06-api-routes.md
Rewrote route middleware ordering documentation; introduced new Organizations Guide and organization-scoped architecture patterns; updated frontend/backend routing diagrams to reflect org-scoped paths; extended API and database guides with organization examples; reorganized VitePress sidebar from route-specific to flat array structure
Database Schema & Migrations
packages/workers/src/db/schema.js, packages/workers/migrations/0001_nice_blonde_phantom.sql, packages/workers/migrations/0002_equal_iron_man.sql, packages/workers/migrations/meta/0001_snapshot.json, packages/workers/migrations/meta/0002_snapshot.json, packages/workers/migrations/meta/_journal.json
Added new tables for organization, member, invitation with cascading deletes; extended projects with orgId, session with activeOrganizationId, projectInvitations with orgId/orgRole, mediaFiles with nullable uploadedBy; migrated mediaFiles schema with foreign key constraints
Backend Authentication & Middleware
packages/workers/src/auth/config.js, packages/workers/src/middleware/requireOrg.js
Integrated Better Auth organization plugin with auto-bootstrap personal org on first login; introduced requireOrgMembership() and requireProjectAccess() middleware with role hierarchy checks and context attachment (getOrgContext, getProjectContext)
Backend Routes
packages/workers/src/routes/orgs/index.js, packages/workers/src/routes/orgs/projects.js, packages/workers/src/routes/orgs/members.js, packages/workers/src/routes/orgs/invitations.js, packages/workers/src/routes/orgs/pdfs.js, packages/workers/src/routes/invitations.js, packages/workers/src/routes/users.js, packages/workers/src/routes/admin/users.js, packages/workers/src/routes/billing/index.js, packages/workers/src/index.js
Introduced org-scoped REST endpoints for projects, members, PDFs, invitations; migrated legacy routes to return 410 Gone; enhanced invitation acceptance with org membership enrollment; extended user deletion to null-out mediaFiles; normalized subscription tier response
Backend Durable Objects & Store
packages/workers/src/durable-objects/ProjectDoc.js
Added org-aware authorization checks on WebSocket connect; replaced Y.Doc membership with explicit D1 database verification; added orgId caching and persistence to reduce DB queries
Backend Configuration
packages/workers/src/config/validation.js
Added invitation create schema with email, role, and orgRole validation
Backend Tests & Helpers
packages/workers/src/__tests__/helpers.js, packages/workers/src/__tests__/seed-schemas.js, packages/workers/src/__tests__/admin.test.js, packages/workers/src/__tests__/app.test.js, packages/workers/src/routes/__tests__/*.test.js
Introduced test helpers for seeding organizations, org members, media files; migrated member/project/PDF/invitation tests from project-scoped to org-scoped routes; added org auth integration tests; extended user deletion and subscription tests
Shared Errors
packages/shared/src/errors/domains/domain.ts
Added PROJECT_NOT_IN_ORG error code for cross-org project access violations
Frontend Primitives & Hooks
packages/web/src/primitives/useOrgContext.js, packages/web/src/primitives/useOrgProjectContext.js, packages/web/src/primitives/useOrgProjectList.js, packages/web/src/primitives/useRecentsNav.js, packages/web/src/primitives/useSubscription.js
Added useOrgContext for org slug/id resolution and org list caching; added useOrgProjectContext combining org and project context with path builders; added useOrgProjectList for fetching org-scoped projects; added useRecentsNav tracking visited pages with localStorage persistence; enhanced useSubscription with error flag exposure
Frontend Org Components
packages/web/src/components/org/OrgRedirect.jsx, packages/web/src/components/org/OrgProjectsPage.jsx, packages/web/src/components/org/CreateOrgPage.jsx, packages/web/src/components/org/index.js
Introduced OrgRedirect for login redirect logic; OrgProjectsPage for org dashboard with create/delete workflows; CreateOrgPage for org creation with auto-slug generation and validation
Frontend Project Components
packages/web/src/components/project/ProjectView.jsx, packages/web/src/components/project/ProjectContext.jsx, packages/web/src/components/project/CreateProjectForm.jsx, packages/web/src/components/project/ProjectDashboard.jsx
Extended ProjectView with orgId propagation to PDF/Google Drive APIs; added org-scoped path helpers to ProjectContext; added org context validation in CreateProjectForm; removed ProjectDashboard component in favor of OrgProjectsPage
Frontend Tab Components
packages/web/src/components/project/completed-tab/CompletedTab.jsx, packages/web/src/components/project/reconcile-tab/ReconcileTab.jsx, packages/web/src/components/project/todo-tab/ToDoTab.jsx, packages/web/src/components/project/overview-tab/OverviewTab.jsx, packages/web/src/components/project/overview-tab/AddMemberModal.jsx
Updated navigation paths to use org-scoped helpers from ProjectContext; added orgId requirement and validation in member addition; extended context destructuring to include org-aware path builders
Frontend Sidebar
packages/web/src/components/sidebar/ProjectTreeItem.jsx, packages/web/src/components/sidebar/StudyTreeItem.jsx, packages/web/src/components/sidebar/ChecklistTreeItem.jsx, packages/web/src/components/sidebar/Sidebar.jsx
Introduced org-aware path resolution; added orgSlug prop threading; integrated mobile overlay via Portal; added org-scoped project listing via useOrgProjectList; added recents navigation via useRecentsNav
Frontend Navbar & Layout
packages/web/src/components/Navbar.jsx, packages/web/src/Layout.jsx, packages/web/src/components/Dashboard.jsx, packages/web/src/components/checklist/ChecklistsDashboard.jsx, packages/web/src/components/checklist/LocalAppraisalsPanel.jsx
Added workspace/org switcher UI in Navbar; introduced desktop/mobile sidebar state management in Layout with width persistence; replaced ProjectDashboard with OrgRedirect; refactored ChecklistsDashboard to delegate to LocalAppraisalsPanel
Frontend Checklist
packages/web/src/components/checklist/ChecklistYjsWrapper.jsx, packages/web/src/components/checklist/compare/ReconciliationWrapper.jsx
Integrated useOrgContext for org-aware PDF operations and navigation; propagated orgId to all PDF API calls; added active project tracking with org context; updated access-denied redirects to org pages
Frontend API & Stores
packages/web/src/api/pdf-api.js, packages/web/src/api/google-drive.js, packages/web/src/api/auth-client.js, packages/web/src/api/__tests__/better-auth-store.test.js, packages/web/src/stores/projectActionsStore/index.js, packages/web/src/stores/projectActionsStore/pdfs.js, packages/web/src/stores/projectActionsStore/project.js, packages/web/src/stores/projectActionsStore/members.js, packages/web/src/stores/projectActionsStore/studies.js, packages/web/src/stores/adminStore.js
Added orgId parameter to all PDF operations (upload, download, delete, list); extended google-drive import with orgId; integrated organization client in auth-client; refactored projectActionsStore to track activeOrgId and propagate to action modules; updated all action signatures to accept getActiveOrgId
Frontend Routes & Query
packages/web/src/Routes.jsx, packages/web/src/lib/queryKeys.js
Introduced org-scoped route structure (/orgs/:orgSlug/*); relocated project routes under org scope; added query key namespace for org-scoped projects (projects.byOrg(orgId))
Frontend Styling
packages/web/src/global.css
Added desktop-only sidebar width rule for expanded state
Test Mocks & Configuration
packages/web/src/primitives/__tests__/projectStore.test.js
Removed project list tests in favor of query-based testing; updated mock expectations
Build Scripts
scripts/loc-report.mjs
Added --code-only flag and directory filtering for code-only line count reporting

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Browser
    participant AuthServer
    participant DB as D1 Database
    participant BootstrapLogic as Bootstrap Logic

    User->>Browser: Login
    Browser->>AuthServer: Authenticate
    AuthServer->>AuthServer: Validate credentials
    AuthServer->>DB: Check user exists
    DB-->>AuthServer: User confirmed
    AuthServer->>BootstrapLogic: Trigger after-auth hook
    BootstrapLogic->>DB: Check: user has orgs?
    DB-->>BootstrapLogic: No orgs found
    BootstrapLogic->>DB: Create personal organization
    DB-->>BootstrapLogic: org created (id, slug)
    BootstrapLogic->>DB: Create organization membership<br/>(user as owner)
    DB-->>BootstrapLogic: membership created
    BootstrapLogic->>DB: Set activeOrganizationId<br/>in session
    DB-->>BootstrapLogic: session updated
    AuthServer-->>Browser: Auth success + session
    Browser->>Browser: Redirect to /orgs/{slug}
    User->>User: Logged in with org
Loading
sequenceDiagram
    participant Owner
    participant Browser
    participant APIServer
    participant EmailService
    participant Invitee as Invited User
    participant Email
    participant AuthService
    participant DB

    Owner->>Browser: Create project invitation
    Browser->>APIServer: POST /api/orgs/{orgId}/projects/{projectId}/invitations<br/>{email, role, orgRole}
    APIServer->>APIServer: requireOrgMembership (owner)
    APIServer->>APIServer: requireProjectAccess (owner)
    APIServer->>DB: Check existing invitation
    APIServer->>DB: Create invitation record<br/>(orgId, projectId, email, roles, token)
    APIServer->>EmailService: Queue invitation email<br/>(magic-link with token)
    EmailService->>Email: Send magic-link
    Email-->>Invitee: Click magic-link
    Invitee->>AuthService: Accept invitation (token)
    AuthService->>DB: Verify token & expiry
    AuthService->>DB: Check org membership
    AuthService->>DB: Create org membership<br/>(if missing)
    AuthService->>DB: Create project membership
    AuthService->>DB: Mark invitation accepted
    AuthService-->>Invitee: Redirect to project
    Invitee->>Invitee: Access project with<br/>org+project roles
Loading
sequenceDiagram
    participant Client
    participant Middleware
    participant AuthCheck as Auth Check
    participant OrgCheck as Org Membership
    participant ProjectCheck as Project Access
    participant EntitlementCheck
    participant Validation
    participant Handler

    Client->>Middleware: Request to protected route
    Middleware->>AuthCheck: Verify session/token
    AuthCheck-->>Middleware: User ID extracted
    Middleware->>OrgCheck: requireOrgMembership(minRole?)
    OrgCheck->>OrgCheck: Extract orgId from params
    OrgCheck->>OrgCheck: Query DB: user org member?
    OrgCheck-->>Middleware: Attach org context<br/>(orgId, orgRole, org)
    Middleware->>ProjectCheck: requireProjectAccess(minRole?)
    ProjectCheck->>ProjectCheck: Use org context
    ProjectCheck->>ProjectCheck: Query DB: project in org?
    ProjectCheck->>ProjectCheck: Query DB: user project member?
    ProjectCheck-->>Middleware: Attach project context<br/>(projectId, projectRole)
    Middleware->>EntitlementCheck: Verify entitlement/quota
    EntitlementCheck-->>Middleware: OK
    Middleware->>Validation: validateRequest(schema)
    Validation-->>Middleware: OK
    Middleware->>Handler: Invoke route handler
    Handler-->>Client: 200 + response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Dev' is vague and generic, providing no meaningful information about what changes are included in this pull request. Replace with a descriptive title that summarizes the main change, such as 'Implement organization-scoped multi-tenant architecture' or 'Add organizations system with org-based routing and permissions'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 81.82% which is sufficient. The required threshold is 80.00%.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/web/src/api/google-drive.js (1)

49-63: Fix missing orgId argument in GoogleDrivePickerModal.

The function signature change to include orgId is correctly updated in two of three call sites. However, GoogleDrivePickerModal.jsx:31 still calls importFromGoogleDrive(file.id, projectId(), studyId) without passing the required orgId parameter.

Additionally, the component needs to accept orgId as a prop (currently missing from JSDoc lines 12-18), and AllStudiesTab.jsx needs to destructure orgId from useProjectContext() and pass it to the modal.

🧹 Nitpick comments (29)
packages/workers/src/routes/billing/index.js (2)

55-57: Good tier normalization with defensive validation.

The tier normalization and validation logic is well-implemented. Consider applying similar validation to the default response (lines 38-45) for consistency, where TIER_INFO.free is hardcoded instead of using TIER_INFO[DEFAULT_SUBSCRIPTION_TIER].

Optional consistency improvement
  return c.json({
    tier: DEFAULT_SUBSCRIPTION_TIER,
    status: DEFAULT_SUBSCRIPTION_STATUS,
-   tierInfo: TIER_INFO.free,
+   tierInfo: TIER_INFO[DEFAULT_SUBSCRIPTION_TIER] ?? TIER_INFO.free,
    stripeSubscriptionId: null,
    currentPeriodEnd: null,
    cancelAtPeriodEnd: false,
  });

60-66: Response construction looks good.

The response correctly uses the normalized validTier and converted currentPeriodEnd. The nullish coalescing operator on line 62 (?? TIER_INFO.free) is redundant since validTier is guaranteed to be a valid key in TIER_INFO, but it doesn't hurt as defensive coding.

packages/web/src/components/org/CreateOrgPage.jsx (1)

24-37: Consider simplifying slug generation logic.

The slug generation works correctly but uses multiple chained .replace() calls. While this is functional, you could consider combining some operations for better readability.

Proposed simplification
-    const generatedSlug = newName
-      .toLowerCase()
-      .replace(/\s+/g, '-')
-      .replace(/[^a-z0-9-]/g, '')
-      .replace(/-+/g, '-')
-      .replace(/^-|-$/g, '');
+    const generatedSlug = newName
+      .toLowerCase()
+      .replace(/\s+/g, '-')           // spaces to hyphens
+      .replace(/[^a-z0-9-]/g, '')     // keep only alphanumeric and hyphens
+      .replace(/-{2,}/g, '-')         // collapse multiple hyphens
+      .replace(/^-+|-+$/g, '');       // trim hyphens from start/end

The changes are minimal but add clarity with comments and slightly more efficient regex patterns (-{2,} instead of -+ and ^-+|-+$ to be more explicit about trimming multiple hyphens).

packages/web/src/primitives/useRecentsNav.js (1)

58-109: Consider adding comments to clarify nested regex matching logic.

The path parsing logic is correct but uses nested regex matches that progressively check for more specific routes. While functional, adding brief comments could improve readability.

Proposed clarification
 function parsePathToRecentItem(path) {
-  // Match project pages: /orgs/:slug/projects/:projectId
+  // Try to match org-scoped project routes (most specific match wins)
   const projectMatch = path.match(/^\/orgs\/([^/]+)\/projects\/([^/]+)/);
   if (projectMatch) {
     const [, orgSlug, projectId] = projectMatch;
-    // Check if it's a study page: /orgs/:slug/projects/:projectId/studies/:studyId
+    // Check if path also includes a study segment
     const studyMatch = path.match(/^\/orgs\/([^/]+)\/projects\/([^/]+)\/studies\/([^/]+)/);
     if (studyMatch) {
-      const [, , , studyId] = studyMatch;
-      // Check if it's a checklist page
+      const [, , , studyId] = studyMatch; // Skip orgSlug, projectId captures
+      // Check if path also includes a checklist segment
       const checklistMatch = path.match(
         /^\/orgs\/([^/]+)\/projects\/([^/]+)\/studies\/([^/]+)\/checklists\/([^/]+)/,
       );

This makes the progressive matching strategy more explicit.

packages/web/src/components/sidebar/Sidebar.jsx (1)

458-640: Consider extracting duplicated sidebar content into a shared component.

The mobile overlay Portal correctly implements a slide-in panel with backdrop and proper z-indexing. However, the sidebar content (lines 497-636) is duplicated from the desktop expanded content (lines 250-391).

While the current implementation works correctly, extracting the common sidebar body into a shared component would:

  • Reduce maintenance burden (changes only need to be made in one place)
  • Follow DRY principles
  • Reduce bundle size

Example approach:

// Extract to SidebarContent.jsx
function SidebarContent(props) {
  return (
    <>
      {/* Dashboard/Workspace Link */}
      <div class='p-2 pt-3'>...</div>
      
      {/* Recents Section */}
      <Show when={props.recents().length > 0}>...</Show>
      
      {/* Cloud Projects Section */}
      <Show when={props.isLoggedIn() && props.orgSlug()}>...</Show>
      
      {/* Local Checklists Section */}
      <div class='px-3 pt-6 pb-2'>...</div>
    </>
  );
}

// Then use in both desktop and mobile contexts
<SidebarContent {...sharedProps} />
packages/workers/src/middleware/requireOrg.js (1)

236-240: Clarify role hierarchy comments.

The comments use > which typically implies "greater than," but the arrays are ordered from lowest to highest privilege. Consider clarifying to avoid confusion:

Suggested clarification
-// Org role hierarchy: owner > admin > member
+// Org role hierarchy (lowest to highest): member < admin < owner
 const ORG_ROLE_HIERARCHY = ['member', 'admin', 'owner'];

-// Project role hierarchy: viewer > member > collaborator > owner
+// Project role hierarchy (lowest to highest): viewer < member < collaborator < owner
 const PROJECT_ROLE_HIERARCHY = ['viewer', 'member', 'collaborator', 'owner'];
packages/workers/src/index.js (1)

192-205: Consider using domain error pattern for consistency.

The legacy endpoint handler correctly returns 410 Gone to indicate the endpoints have moved. However, the error response format differs from the domain error pattern used elsewhere in the codebase.

Optional: Align with domain error pattern

For consistency with error handling patterns in the codebase, consider using a domain error:

+import { createDomainError, SYSTEM_ERRORS } from '@corates/shared';
+
-const legacyGoneHandler = c =>
-  c.json(
-    {
-      error: 'ENDPOINT_MOVED',
-      message: 'This endpoint has been moved. Use /api/orgs/:orgId/projects/... instead.',
-      statusCode: 410,
-    },
-    410,
-  );
+const legacyGoneHandler = c => {
+  const error = createDomainError(
+    SYSTEM_ERRORS.ENDPOINT_MOVED || 'ENDPOINT_MOVED',
+    'This endpoint has been moved. Use /api/orgs/:orgId/projects/... instead.',
+    { statusCode: 410 }
+  );
+  return c.json(error, 410);
+};

Note: This assumes ENDPOINT_MOVED exists in SYSTEM_ERRORS or can be added. The current implementation is acceptable for a simple deprecation notice.

packages/workers/src/config/validation.js (1)

82-90: Consider extracting org role enum to constants.

The new invitationSchemas.create correctly validates invitation data with appropriate field constraints. The schema follows established patterns and reuses PROJECT_ROLES as recommended.

Optional: Extract org role enum for consistency

For consistency with the PROJECT_ROLES pattern and to enable reuse across the codebase, consider extracting the org role enum to constants:

In packages/workers/src/config/constants.js:

+export const ORG_ROLES = ['member', 'admin', 'owner'];

Then update the schema:

-import { PROJECT_ROLES } from './constants.js';
+import { PROJECT_ROLES, ORG_ROLES } from './constants.js';

 export const invitationSchemas = {
   create: z.object({
     email: z.string().email('Invalid email address'),
     role: z.enum(PROJECT_ROLES, {
       error: `Role must be one of: ${PROJECT_ROLES.join(', ')}`,
     }),
-    orgRole: z.enum(['member', 'admin', 'owner'], {
-      error: "Org role must be one of: 'member', 'admin', 'owner'",
+    orgRole: z.enum(ORG_ROLES, {
+      error: `Org role must be one of: ${ORG_ROLES.join(', ')}`,
     }),
   }),

Based on coding guidelines for validation schemas.

packages/web/src/components/org/OrgRedirect.jsx (1)

44-68: Consider: Brief content flash during authenticated redirect.

When logged in with orgs, the outer Show resolves to true (not loading) before the effect navigates away. This may briefly render an empty container. Consider adding a condition to avoid rendering when navigation is imminent.

Optional: Add logged-in-with-orgs check to prevent flash
       >
-        <Show when={!isLoggedIn()}>
+        <Show when={!isLoggedIn() && !hasNoOrgs()}>
           <div class='text-center'>
             <h2 class='mb-2 text-xl font-semibold text-gray-900'>Welcome to CoRATES</h2>

Alternatively, you could add a navigating signal that's set before navigation to suppress the empty state.

packages/workers/src/auth/config.js (1)

344-344: Consider slug uniqueness edge case.

The slug is generated as ${userName}-${orgId.slice(0,8)}. While the UUID suffix makes collisions unlikely, the slug column has a unique constraint (per schema). If a collision somehow occurs (e.g., same 8-char UUID prefix), the insert will fail silently due to the try-catch, leaving the user without an org.

Consider adding retry logic or using the full UUID for guaranteed uniqueness.

packages/docs/architecture/diagrams/06-api-routes.md (1)

147-155: Add language specifier to fenced code block.

The fenced code block should have a language specified for proper rendering and accessibility.

Proposed fix
-```
+```text
 requireAuth -> requireOrgMembership(minRole) -> requireProjectAccess(minRole) -> handler
</details>

</blockquote></details>
<details>
<summary>packages/workers/src/routes/orgs/index.js (3)</summary><blockquote>

`47-56`: **Use `VALIDATION_ERRORS` instead of `AUTH_ERRORS.FORBIDDEN` for input validation.**

The error for missing `name` field is a validation concern, not an authorization issue. Using `AUTH_ERRORS.FORBIDDEN` with status 403 is semantically incorrect for missing input.

As per coding guidelines, use validation middleware or appropriate validation errors.

<details>
<summary>Proposed fix</summary>

```diff
     if (!body.name?.trim()) {
-      const error = createDomainError(AUTH_ERRORS.FORBIDDEN, {
-        reason: 'name_required',
+      const error = createDomainError(VALIDATION_ERRORS.FIELD_REQUIRED, {
+        field: 'name',
       });
       return c.json(error, error.statusCode);
     }

Also add VALIDATION_ERRORS to the import on line 24:

-import { createDomainError, AUTH_ERRORS, SYSTEM_ERRORS } from '@corates/shared';
+import { createDomainError, AUTH_ERRORS, SYSTEM_ERRORS, VALIDATION_ERRORS } from '@corates/shared';

231-243: Use VALIDATION_ERRORS.FIELD_REQUIRED for missing userId.

Consistent with the previous comment, missing required fields should use validation errors.

Proposed fix
     if (!userId) {
-      const error = createDomainError(AUTH_ERRORS.FORBIDDEN, {
-        reason: 'user_id_required',
+      const error = createDomainError(VALIDATION_ERRORS.FIELD_REQUIRED, {
+        field: 'userId',
       });
       return c.json(error, error.statusCode);
     }

281-290: Use VALIDATION_ERRORS.FIELD_REQUIRED for missing role.

Proposed fix
     if (!role) {
-      const error = createDomainError(AUTH_ERRORS.FORBIDDEN, {
-        reason: 'role_required',
+      const error = createDomainError(VALIDATION_ERRORS.FIELD_REQUIRED, {
+        field: 'role',
       });
       return c.json(error, error.statusCode);
     }
packages/workers/migrations/0001_nice_blonde_phantom.sql (2)

14-22: Consider adding unique constraint on (userId, organizationId) to prevent duplicate memberships.

The member table allows a user to be added multiple times to the same organization. A unique constraint would enforce data integrity at the database level.

Proposed addition after line 22
--> statement-breakpoint
CREATE UNIQUE INDEX `member_user_org_unique` ON `member` (`userId`, `organizationId`);

34-36: Migration may fail on existing data due to NOT NULL constraint.

Adding orgId as NOT NULL to existing tables (project_invitations, projects) will fail if these tables contain existing rows without a default value or data migration.

If this is a greenfield deployment, this is fine. Otherwise, consider a multi-step migration:

  1. Add column as nullable
  2. Backfill existing rows with a default org
  3. Alter to NOT NULL

Would you like me to help create a safe multi-step migration for existing data?

packages/web/src/stores/projectActionsStore/members.js (1)

29-50: Consider handling org-specific error codes.

The fetch call now uses org-scoped endpoints, but error handling only checks for generic errors. Based on the test file packages/workers/src/routes/__tests__/org-auth.test.js, the backend may return org-specific error codes like PROJECT_NOT_IN_ORG, AUTH_FORBIDDEN (with not_org_member reason), etc.

Consider checking for org-specific error codes and providing more specific error messages:

Suggested enhancement for org-specific error handling
     try {
       const response = await fetch(
         `${API_BASE}/api/orgs/${orgId}/projects/${projectId}/members/${memberId}`,
         {
           method: 'DELETE',
           credentials: 'include',
         },
       );
       if (!response.ok) {
         const data = await response.json();
+        // Handle org-specific error codes
+        if (data.code === 'PROJECT_NOT_IN_ORG') {
+          throw new Error('Project does not belong to the current organization');
+        }
+        if (data.code === 'AUTH_FORBIDDEN' && data.details?.reason === 'not_org_member') {
+          throw new Error('You are no longer a member of this organization');
+        }
         throw new Error(data.error || 'Failed to remove member');
       }
packages/web/src/components/org/OrgProjectsPage.jsx (1)

100-127: Consider using handleFetchError from error-utils.

Per coding guidelines, frontend fetch calls should use handleFetchError with optional showToast parameter. The current implementation manually parses errors and uses a dynamic import.

Suggested refactor using handleFetchError
   const handleDeleteProject = async targetProjectId => {
     const confirmed = await confirmDialog.open({
       title: 'Delete Project',
       description:
         'Are you sure you want to delete this entire project? This action cannot be undone.',
       confirmText: 'Delete Project',
       variant: 'danger',
     });
     if (!confirmed) return;

     try {
       const response = await fetch(`${API_BASE}/api/orgs/${orgId()}/projects/${targetProjectId}`, {
         method: 'DELETE',
         credentials: 'include',
       });
       if (!response.ok) {
-        const data = await response.json().catch(() => ({}));
-        throw new Error(data.error || 'Failed to delete project');
+        await handleFetchError(response, { showToast: true, toastTitle: 'Delete Failed' });
+        return;
       }

       queryClient.invalidateQueries({ queryKey: queryKeys.projects.byOrg(orgId()) });
       showToast.success('Project Deleted', 'The project has been deleted successfully');
     } catch (err) {
-      const { handleError } = await import('@/lib/error-utils.js');
-      await handleError(err, { toastTitle: 'Delete Failed' });
+      console.error('Delete project error:', err);
     }
   };

This requires importing handleFetchError from @/lib/error-utils.js at the top of the file.

packages/docs/guides/organizations.md (1)

20-34: Consider adding language identifier to fenced code blocks.

The markdownlint hints flag blocks at lines 20, 150, 274, and 406 for missing language specifiers. For ASCII diagrams like this entity hierarchy, you can use text or plaintext as the language identifier to silence the lint warning while preserving formatting.

Example fix
-```
+```text
 Organization
     |
     +-- Projects (owned by org)
packages/web/src/components/Navbar.jsx (1)

99-157: Workspace switcher missing keyboard accessibility.

The workspace switcher dropdown is only dismissible via mouse clicks. Consider adding keyboard support for accessibility (e.g., Escape key to close, arrow key navigation).

Suggested enhancement for keyboard accessibility
+  // Add keyboard handler for dropdown
+  const handleOrgKeyDown = (e) => {
+    if (e.key === 'Escape') {
+      setShowOrgMenu(false);
+    }
+  };

   <div class='relative' ref={orgMenuRef}>
     <button
       onClick={() => setShowOrgMenu(!showOrgMenu())}
+      onKeyDown={handleOrgKeyDown}
+      aria-expanded={showOrgMenu()}
+      aria-haspopup="listbox"
       class='flex h-8 items-center gap-2 rounded-lg bg-white/10 px-3 text-sm font-medium transition hover:bg-white/20'
     >
packages/web/src/stores/projectActionsStore/project.js (1)

58-77: Missing null check for orgId in deleteById.

If neither targetOrgId nor getActiveOrgId() returns a value, the API call will have an undefined segment in the URL. Consider adding validation.

Suggested fix
 async function deleteById(targetProjectId, targetOrgId) {
   const orgId = targetOrgId || getActiveOrgId();
+  if (!orgId) {
+    throw new Error('Organization ID is required for deletion');
+  }

   try {
     const response = await fetch(`${API_BASE}/api/orgs/${orgId}/projects/${targetProjectId}`, {
packages/workers/src/routes/orgs/members.js (2)

142-153: Potential race condition in ensureOrgMembership followed by project member insert.

The ensureOrgMembership call and subsequent projectMembers.insert are not atomic. Under concurrent requests, this could lead to duplicate membership attempts or inconsistent state.

Consider using db.batch() to make these operations atomic, as per the coding guidelines.

Suggested approach
-     // Ensure org membership first (combined flow)
-     await ensureOrgMembership(db, orgId, userToAdd.id);
-
-     // Add project membership
-     const now = new Date();
-     await db.insert(projectMembers).values({
-       id: crypto.randomUUID(),
-       projectId,
-       userId: userToAdd.id,
-       role,
-       joinedAt: now,
-     });
+     // Ensure org membership and add project membership atomically
+     const now = new Date();
+     const projectMemberId = crypto.randomUUID();
+     
+     // Check existing org membership
+     const existingOrgMembership = await db
+       .select({ id: member.id })
+       .from(member)
+       .where(and(eq(member.organizationId, orgId), eq(member.userId, userToAdd.id)))
+       .get();
+     
+     if (!existingOrgMembership) {
+       // Need to add both memberships
+       await db.batch([
+         db.insert(member).values({
+           id: crypto.randomUUID(),
+           userId: userToAdd.id,
+           organizationId: orgId,
+           role: 'member',
+           createdAt: now,
+         }),
+         db.insert(projectMembers).values({
+           id: projectMemberId,
+           projectId,
+           userId: userToAdd.id,
+           role,
+           joinedAt: now,
+         }),
+       ]);
+     } else {
+       // Only add project membership
+       await db.insert(projectMembers).values({
+         id: projectMemberId,
+         projectId,
+         userId: userToAdd.id,
+         role,
+         joinedAt: now,
+       });
+     }

507-620: Consider extracting email sending logic.

The handleInvitation function is quite long (~115 lines) with email template generation and magic link creation embedded. Consider extracting the email-related logic into a separate utility for better maintainability.

packages/web/src/api/pdf-api.js (1)

54-81: Consider adding parameter validation.

The uploadPdf function doesn't validate that orgId, projectId, or studyId are provided, which could lead to malformed URLs with undefined segments.

Suggested validation
 export async function uploadPdf(orgId, projectId, studyId, file, fileName = null) {
+  if (!orgId || !projectId || !studyId) {
+    throw new Error('orgId, projectId, and studyId are required');
+  }
   const url = buildPdfBaseUrl(orgId, projectId, studyId);
packages/web/src/stores/projectActionsStore/index.js (1)

168-170: Consider consistent naming for public getters.

The public API exposes getActiveProjectId which maps to getActiveProjectIdOrNull, and getActiveOrgId which maps to getActiveOrgIdOrNull. This is slightly misleading as the names suggest they might throw.

Consider renaming for clarity:

// Option A: Rename the mappings to be explicit
getActiveProjectIdOrNull: getActiveProjectIdOrNull,
getActiveOrgIdOrNull: getActiveOrgIdOrNull,

// Option B: Keep short names but document clearly
packages/workers/src/routes/orgs/invitations.js (2)

173-207: Dynamic imports on every request are inefficient.

These modules are imported dynamically on each POST request, adding latency. Move them to top-level static imports unless there's a specific code-splitting requirement.

Proposed fix

Move these imports to the top of the file:

+import { betterAuth } from 'better-auth';
+import { magicLink } from 'better-auth/plugins';
+import { drizzleAdapter } from 'better-auth/adapters/drizzle';
+import { drizzle } from 'drizzle-orm/d1';
+import * as schema from '../../db/schema.js';
+import { MAGIC_LINK_EXPIRY_MINUTES, getProjectInvitationEmailHtml, getProjectInvitationEmailText } from '../../auth/emailTemplates.js';
+import { escapeHtml } from '../../lib/escapeHtml.js';

Then remove the dynamic imports inside the route handler.


308-314: Use a semantically correct error for "not found" case.

VALIDATION_ERRORS.FIELD_INVALID_FORMAT indicates a format issue, not a missing resource. Consider using a more appropriate error or creating an INVITATION_NOT_FOUND error.

Proposed fix
 if (!invitation) {
-  const error = createDomainError(VALIDATION_ERRORS.FIELD_INVALID_FORMAT, {
-    field: 'invitationId',
-    value: invitationId,
-  });
+  const error = createDomainError(PROJECT_ERRORS.NOT_FOUND, {
+    resource: 'invitation',
+    invitationId,
+  });
   return c.json(error, error.statusCode);
 }
packages/workers/src/routes/orgs/projects.js (1)

319-342: Consider batching R2 deletes to avoid rate limits.

For projects with many PDFs, Promise.all on all delete operations could hit R2 rate limits. Consider using a chunked approach or sequential deletion with concurrency limits.

Proposed improvement using chunked deletion
 if (listed.objects.length > 0) {
   const keysToDelete = listed.objects.map(obj => obj.key);
-  await Promise.all(keysToDelete.map(key => c.env.PDF_BUCKET.delete(key)));
+  // Delete in chunks to avoid rate limits
+  const chunkSize = 10;
+  for (let i = 0; i < keysToDelete.length; i += chunkSize) {
+    const chunk = keysToDelete.slice(i, i + chunkSize);
+    await Promise.all(chunk.map(key => c.env.PDF_BUCKET.delete(key)));
+  }
   deletedCount += keysToDelete.length;
 }
packages/workers/src/db/schema.js (1)

36-47: Add indexes on foreign key columns for improved query performance.

The foreign key columns member.userId, member.organizationId, invitation.inviterId, and invitation.organizationId lack indexes. SQLite does not automatically create indexes for non-unique foreign keys, which can impact performance on queries filtering or joining by these columns. Consider adding indexes in a migration.

Comment on lines +300 to 308
it('should signout successfully', async () => {
authClient.signOut.mockResolvedValue({
error: null,
});

await authStore.signout();

expect(authClient.signOut).toHaveBeenCalled();
expect(projectStore.clearProjectList).toHaveBeenCalled();
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Test coverage gap: signout should verify project data cleanup.

The test name was simplified from "signout successfully and clear data" to "signout successfully", and the test no longer verifies that project data is cleared. Even if the cleanup responsibility has moved to a different module, this test should verify the end-to-end behavior to ensure users' local data is properly cleared on logout.

Consider adding an assertion or integration test to verify that:

  • Local project data is cleared from stores
  • IndexedDB project data is cleared
  • Any cached data related to projects is invalidated
Suggested test enhancement

If project cleanup is handled by the signout implementation, add verification:

  it('should signout successfully', async () => {
    authClient.signOut.mockResolvedValue({
      error: null,
    });

    await authStore.signout();

    expect(authClient.signOut).toHaveBeenCalled();
+   // Verify project data cleanup happens (adjust based on actual implementation)
+   // Example: expect(projectStore.someCleanupMethod).toHaveBeenCalled();
  });

If cleanup is handled elsewhere, add a note explaining where this behavior is tested.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/web/src/api/__tests__/better-auth-store.test.js around lines 300 to
308, the test "should signout successfully" was simplified and no longer asserts
that project-related data is cleaned up on signout; restore end-to-end
verification by adding assertions after await authStore.signout() that confirm
(1) the in-memory project store(s) are emptied or reset to initial state, (2)
IndexedDB entries for projects have been removed (query the test DB or mock the
IDB delete calls), and (3) any project-related caches or query clients have been
invalidated (e.g., ensure cache keys are cleared or invalidate calls were made);
if cleanup is now the responsibility of another module, update the test to call
that module or add a comment referencing the specific test that covers cleanup
instead of removing these assertions.

Comment on lines +46 to +54
// Set active project for action store
createEffect(() => {
const pid = params.projectId;
const oid = orgId();
if (pid && oid) {
projectActionsStore._setActiveProject(pid, oid);
connect();
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the full ReconciliationWrapper.jsx file to see the complete context
wc -l packages/web/src/components/checklist/compare/ReconciliationWrapper.jsx

Repository: InfinityBowman/corates

Length of output: 141


🏁 Script executed:

#!/bin/bash
# Read the component file to check for onCleanup usage and full connect() context
cat -n packages/web/src/components/checklist/compare/ReconciliationWrapper.jsx | head -100

Repository: InfinityBowman/corates

Length of output: 4218


🏁 Script executed:

#!/bin/bash
# Find and examine the useProject hook implementation
fd "useProject" packages/web/src/primitives --type f

Repository: InfinityBowman/corates

Length of output: 214


🏁 Script executed:

#!/bin/bash
# Search for onCleanup patterns with connect/disconnect in the web package
rg -n "onCleanup.*disconnect|onCleanup.*connect" packages/web/src --type js --type jsx --type ts --type tsx -A 2

Repository: InfinityBowman/corates

Length of output: 93


🏁 Script executed:

#!/bin/bash
# Check for connect() usage patterns with cleanup in the codebase
rg -n "connect\(\)" packages/web/src --type js --type jsx --type ts --type tsx -B 2 -A 3 | head -50

Repository: InfinityBowman/corates

Length of output: 93


🏁 Script executed:

#!/bin/bash
# Find useProject hook - check index.js in useProject directory
ls -la packages/web/src/primitives/useProject/

Repository: InfinityBowman/corates

Length of output: 611


🏁 Script executed:

#!/bin/bash
# Read the useProject hook implementation
cat -n packages/web/src/primitives/useProject/index.js | head -150

Repository: InfinityBowman/corates

Length of output: 6050


🏁 Script executed:

#!/bin/bash
# Search for onCleanup with connect in web package - use -C for context instead
rg -n "onCleanup" packages/web/src --type js -A 3 | grep -A 3 -B 3 "connect\|disconnect"

Repository: InfinityBowman/corates

Length of output: 2145


🏁 Script executed:

#!/bin/bash
# Check if there's an onCleanup in ReconciliationWrapper by reading more of the file
cat -n packages/web/src/components/checklist/compare/ReconciliationWrapper.jsx | sed -n '40,100p'

Repository: InfinityBowman/corates

Length of output: 2420


🏁 Script executed:

#!/bin/bash
# Read more of the useProject hook to understand connect and disconnect functions
cat -n packages/web/src/primitives/useProject/index.js | sed -n '149,250p'

Repository: InfinityBowman/corates

Length of output: 5620


🏁 Script executed:

#!/bin/bash
# Check the connection.js file to see what connect/disconnect do
cat -n packages/web/src/primitives/useProject/connection.js

Repository: InfinityBowman/corates

Length of output: 9805


🏁 Script executed:

#!/bin/bash
# Read the rest of the useProject hook to see the full onCleanup and disconnect handling
cat -n packages/web/src/primitives/useProject/index.js | sed -n '250,310p'

Repository: InfinityBowman/corates

Length of output: 2262


🏁 Script executed:

#!/bin/bash
# Check if there are examples of tracking projectId changes in effects within the codebase
rg -n "params\.projectId" packages/web/src/components/checklist/compare/ReconciliationWrapper.jsx -A 2 -B 2

Repository: InfinityBowman/corates

Length of output: 1885


Remove the redundant connect() call—useProject already auto-connects.

The useProject hook automatically establishes the connection when projectId is provided (its internal createEffect handles this). Calling connect() again in the component's effect is redundant and unnecessary. The cleanup is already properly managed by the hook's onCleanup handler.

Keep only the projectActionsStore._setActiveProject() call:

Suggested change
createEffect(() => {
  const pid = params.projectId;
  const oid = orgId();
  if (pid && oid) {
    projectActionsStore._setActiveProject(pid, oid);
    // Remove: connect() is already called by useProject hook
  }
});
🤖 Prompt for AI Agents
In packages/web/src/components/checklist/compare/ReconciliationWrapper.jsx
around lines 46-54, remove the redundant connect() invocation inside the
createEffect so the effect only calls projectActionsStore._setActiveProject(pid,
oid) when pid and oid are present; leave the existing createEffect structure and
ensure no other logic is removed or altered (the useProject hook manages
connection and cleanup), then run lint/tests to verify no usages depended on the
removed call.

Comment on lines +375 to +382
// Build org-scoped project path
const getProjectPath = () => {
const slug = orgSlug();
if (slug) {
return `/orgs/${slug}/projects/${params.projectId}`;
}
return `/projects/${params.projectId}`;
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find Routes.jsx file
fd -t f "Routes\.jsx" packages/web/src

Repository: InfinityBowman/corates

Length of output: 93


🏁 Script executed:

# Examine Routes.jsx to see project route definitions
cat -n packages/web/src/Routes.jsx | grep -A 2 -B 2 "projects\|orgs"

Repository: InfinityBowman/corates

Length of output: 829


🏁 Script executed:

# Search entire Routes.jsx for /projects/ pattern (without org prefix)
grep -n "'/projects/" packages/web/src/Routes.jsx

Repository: InfinityBowman/corates

Length of output: 48


🏁 Script executed:

# Get the full Routes.jsx to see all route definitions
wc -l packages/web/src/Routes.jsx

Repository: InfinityBowman/corates

Length of output: 96


🏁 Script executed:

# Read entire Routes.jsx
cat -n packages/web/src/Routes.jsx

Repository: InfinityBowman/corates

Length of output: 4109


🏁 Script executed:

# Examine ReconciliationWrapper to understand orgSlug signal
cat -n packages/web/src/components/checklist/compare/ReconciliationWrapper.jsx | head -400 | tail -100

Repository: InfinityBowman/corates

Length of output: 4035


🏁 Script executed:

# Check where orgSlug is defined in ReconciliationWrapper
head -50 packages/web/src/components/checklist/compare/ReconciliationWrapper.jsx

Repository: InfinityBowman/corates

Length of output: 1869


🏁 Script executed:

# Check useOrgContext implementation
fd -t f "useOrgContext" packages/web/src

Repository: InfinityBowman/corates

Length of output: 110


🏁 Script executed:

# Check useOrgContext to see how orgSlug is derived
cat -n packages/web/src/primitives/useOrgContext.js

Repository: InfinityBowman/corates

Length of output: 4506


Remove the unreachable fallback path—orgSlug is guaranteed to exist.

ReconciliationWrapper is only accessible via the route /orgs/:orgSlug/projects/:projectId/studies/:studyId/reconcile/:checklist1Id/:checklist2Id, which always includes :orgSlug in the URL parameters. The fallback /projects/${params.projectId} can never be reached, and this route no longer exists in Routes.jsx anyway. Remove the dead code and return the org-scoped path directly:

const getProjectPath = () => {
  return `/orgs/${orgSlug()}/projects/${params.projectId}`;
};
🤖 Prompt for AI Agents
packages/web/src/components/checklist/compare/ReconciliationWrapper.jsx around
lines 375-382: the current getProjectPath includes an unreachable fallback that
returns `/projects/${params.projectId}` because orgSlug is guaranteed to exist
for this route; remove the conditional and dead code and have getProjectPath
unconditionally return the org-scoped path using orgSlug() and params.projectId
(e.g., return `/orgs/${orgSlug()}/projects/${params.projectId}`), eliminating
the unused branch.

Comment on lines +21 to +29
const response = await fetch(`${API_BASE}/api/orgs/${orgId}/projects`, {
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
});

if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw new Error(data.error || data.message || 'Failed to fetch projects');
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use handleFetchError for fetch calls.

According to the coding guidelines, all frontend fetch calls should use handleFetchError from @/lib/error-utils.js. The current implementation manually handles errors.

Based on coding guidelines: Always use handleFetchError from @/lib/error-utils.js for frontend fetch calls.

Refactor to use handleFetchError
+import { handleFetchError } from '@/lib/error-utils.js';
+
 async function fetchOrgProjects(orgId) {
   if (!orgId) {
     return [];
   }
 
-  const response = await fetch(`${API_BASE}/api/orgs/${orgId}/projects`, {
+  const response = await handleFetchError(
+    fetch(`${API_BASE}/api/orgs/${orgId}/projects`, {
-    credentials: 'include',
-    headers: { 'Content-Type': 'application/json' },
-  });
-
-  if (!response.ok) {
-    const data = await response.json().catch(() => ({}));
-    throw new Error(data.error || data.message || 'Failed to fetch projects');
-  }
+      credentials: 'include',
+      headers: { 'Content-Type': 'application/json' },
+    }),
+    { showToast: false }
+  );
 
   return response.json();
 }
🤖 Prompt for AI Agents
In packages/web/src/primitives/useOrgProjectList.js around lines 21 to 29,
replace the manual response.ok check and custom error parsing with the shared
helper: import handleFetchError from '@/lib/error-utils.js' (or update existing
imports), call handleFetchError(response) immediately after awaiting fetch to
let it throw if the response is an error, and then proceed to parse
response.json() and return the data; remove the manual .catch(() => ({})) and
the custom throw so all fetch errors use the centralized handler.

Comment on lines +66 to 68
{/* Local checklists (not org-scoped, work offline) */}
<Route path='/checklist/*' component={LocalChecklistView} />
<Route path='/checklist/:checklistId' component={LocalChecklistView} />
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, verify the exact route definitions in Routes.jsx
cat -n packages/web/src/Routes.jsx | head -80

Repository: InfinityBowman/corates

Length of output: 4109


🏁 Script executed:

# Check LocalChecklistView implementation to understand how it uses route params
find packages/web/src -name "LocalChecklistView.jsx" -o -name "LocalChecklistView.js"

Repository: InfinityBowman/corates

Length of output: 126


🏁 Script executed:

# Search for route parameter usage in the codebase
rg "useParams|useNavigate" packages/web/src/components/checklist-ui -A 2

Repository: InfinityBowman/corates

Length of output: 146


🏁 Script executed:

# Examine LocalChecklistView to understand parameter usage
cat -n packages/web/src/components/checklist/LocalChecklistView.jsx | head -60

Repository: InfinityBowman/corates

Length of output: 2603


🏁 Script executed:

# Search for route parameter usage in the checklist components
rg "useParams" packages/web/src/components/checklist -A 3

Repository: InfinityBowman/corates

Length of output: 2834


🏁 Script executed:

# Check if there are any nested paths under /checklist that might need the wildcard
rg "navigate.*checklist" packages/web/src/components/checklist -A 2

Repository: InfinityBowman/corates

Length of output: 1300


🏁 Script executed:

# Look for any route definitions or navigation patterns that might clarify intent
rg "\/checklist\/" packages/web/src -t jsx -t js | head -20

Repository: InfinityBowman/corates

Length of output: 145


🏁 Script executed:

# Check if there are any sub-routes or nested paths under /checklist
rg '/checklist' packages/web/src/components/checklist -A 1 -B 1 | grep -E "(navigate|Route|path)" | head -20

Repository: InfinityBowman/corates

Length of output: 514


🏁 Script executed:

# Check Solid Router documentation or examples in the codebase for route ordering
rg "Route path=" packages/web/src/Routes.jsx -A 1

Repository: InfinityBowman/corates

Length of output: 1526


🏁 Script executed:

# See if the wildcard route is used elsewhere or if it's truly redundant
cat packages/web/src/Routes.jsx | grep -A 5 -B 5 "checklist"

Repository: InfinityBowman/corates

Length of output: 1898


Remove the redundant /checklist/* wildcard route or reorder it after /checklist/:checklistId.

Lines 67–68 have overlapping patterns where /checklist/* (wildcard) will match /checklist/abc123 before the more specific /checklist/:checklistId route gets evaluated. This causes params.checklistId to be undefined, and LocalChecklistView shows the create form instead of loading the checklist. Navigation in the codebase uses only /checklist (base) and /checklist/${checklistId} (with ID)—no nested sub-routes requiring the wildcard. Remove line 67 or move it after line 68 if future nested routes are planned.

🤖 Prompt for AI Agents
In packages/web/src/Routes.jsx around lines 66 to 68, the wildcard route Route
path='/checklist/*' is shadowing the more specific Route
path='/checklist/:checklistId' so params.checklistId is undefined and
LocalChecklistView shows the create form; remove the redundant '/checklist/*'
route (line 67) or alternatively move it after the '/checklist/:checklistId'
route so the more specific param route matches first.

}

const db = createDb(c.env.DB);
const { projectMembers } = await import('../db/schema.js');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use static import for projectMembers.

projectMembers is dynamically imported inside the middleware, but other schema entities (member, organization, projects) are already statically imported at line 7. This adds unnecessary overhead on each request.

Proposed fix

Update the static import at the top of the file:

-import { member, organization, projects } from '../db/schema.js';
+import { member, organization, projects, projectMembers } from '../db/schema.js';

Then remove the dynamic import at line 109:

     const db = createDb(c.env.DB);
-    const { projectMembers } = await import('../db/schema.js');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { projectMembers } = await import('../db/schema.js');
import { member, organization, projects, projectMembers } from '../db/schema.js';
Suggested change
const { projectMembers } = await import('../db/schema.js');
const db = createDb(c.env.DB);
🤖 Prompt for AI Agents
In packages/workers/src/middleware/requireOrg.js around line 7 and line 109,
projectMembers is being dynamically imported at line 109 while member,
organization, and projects are statically imported at the top; add
projectMembers to the existing static import statement at the top of the file
(the line around 7 that imports member, organization, projects) and remove the
dynamic import at line 109 so projectMembers is imported once at module load
instead of per request.

Comment on lines +41 to +74
/**
* GET /api/orgs/:orgId/projects/:projectId/invitations
* List pending invitations for a project
*/
orgInvitationRoutes.get('/', requireOrgMembership(), requireProjectAccess(), async c => {
const { projectId } = getProjectContext(c);
const db = createDb(c.env.DB);

try {
const invitations = await db
.select({
id: projectInvitations.id,
email: projectInvitations.email,
role: projectInvitations.role,
orgRole: projectInvitations.orgRole,
expiresAt: projectInvitations.expiresAt,
acceptedAt: projectInvitations.acceptedAt,
createdAt: projectInvitations.createdAt,
invitedBy: projectInvitations.invitedBy,
})
.from(projectInvitations)
.where(eq(projectInvitations.projectId, projectId))
.orderBy(desc(projectInvitations.createdAt));

return c.json(invitations);
} catch (error) {
console.error('Error listing invitations:', error);
const dbError = createDomainError(SYSTEM_ERRORS.DB_ERROR, {
operation: 'list_invitations',
originalError: error.message,
});
return c.json(dbError, dbError.statusCode);
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Comment says "pending" but query returns all invitations.

The route docstring states "List pending invitations" but the query fetches all invitations without filtering by acceptedAt IS NULL. Either update the comment or add a filter.

Proposed fix to filter only pending invitations
 const invitations = await db
   .select({
     id: projectInvitations.id,
     email: projectInvitations.email,
     role: projectInvitations.role,
     orgRole: projectInvitations.orgRole,
     expiresAt: projectInvitations.expiresAt,
     acceptedAt: projectInvitations.acceptedAt,
     createdAt: projectInvitations.createdAt,
     invitedBy: projectInvitations.invitedBy,
   })
   .from(projectInvitations)
-  .where(eq(projectInvitations.projectId, projectId))
+  .where(
+    and(
+      eq(projectInvitations.projectId, projectId),
+      sql`${projectInvitations.acceptedAt} IS NULL`
+    )
+  )
   .orderBy(desc(projectInvitations.createdAt));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* GET /api/orgs/:orgId/projects/:projectId/invitations
* List pending invitations for a project
*/
orgInvitationRoutes.get('/', requireOrgMembership(), requireProjectAccess(), async c => {
const { projectId } = getProjectContext(c);
const db = createDb(c.env.DB);
try {
const invitations = await db
.select({
id: projectInvitations.id,
email: projectInvitations.email,
role: projectInvitations.role,
orgRole: projectInvitations.orgRole,
expiresAt: projectInvitations.expiresAt,
acceptedAt: projectInvitations.acceptedAt,
createdAt: projectInvitations.createdAt,
invitedBy: projectInvitations.invitedBy,
})
.from(projectInvitations)
.where(eq(projectInvitations.projectId, projectId))
.orderBy(desc(projectInvitations.createdAt));
return c.json(invitations);
} catch (error) {
console.error('Error listing invitations:', error);
const dbError = createDomainError(SYSTEM_ERRORS.DB_ERROR, {
operation: 'list_invitations',
originalError: error.message,
});
return c.json(dbError, dbError.statusCode);
}
});
const invitations = await db
.select({
id: projectInvitations.id,
email: projectInvitations.email,
role: projectInvitations.role,
orgRole: projectInvitations.orgRole,
expiresAt: projectInvitations.expiresAt,
acceptedAt: projectInvitations.acceptedAt,
createdAt: projectInvitations.createdAt,
invitedBy: projectInvitations.invitedBy,
})
.from(projectInvitations)
.where(
and(
eq(projectInvitations.projectId, projectId),
isNull(projectInvitations.acceptedAt)
)
)
.orderBy(desc(projectInvitations.createdAt));
🤖 Prompt for AI Agents
In packages/workers/src/routes/orgs/invitations.js around lines 41-74 the route
comment says "List pending invitations" but the query returns all invitations;
update the query to only return pending invitations by adding a filter for
acceptedAt IS NULL (and place it alongside the existing projectId where clause,
before orderBy), or alternatively change the comment to reflect that it
intentionally returns all invitations; ensure the chosen behavior is consistent
with any callers/tests.

Comment on lines +416 to +424
console.error(
`[Invitation] Email mismatch: user email="${currentUser.email}", invitation email="${invitation.email}"`,
);
const error = createDomainError(AUTH_ERRORS.FORBIDDEN, {
reason: 'email_mismatch',
userEmail: currentUser.email,
invitationEmail: invitation.email,
});
return c.json(error, error.statusCode);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

**Avoid logging user email addresses - potential PII/compliance issue.**Email addresses are considered Personal Identifiable Information (PII) under GDPR. GDPR regulations encourage organizations not to collect or log information about users (including email addresses) unless necessary. Logging both user and invitation emails in error messages creates compliance risk. As per coding guidelines, avoid logging sensitive data like emails.

Proposed fix
 if (normalizedUserEmail !== normalizedInvitationEmail) {
-  console.error(
-    `[Invitation] Email mismatch: user email="${currentUser.email}", invitation email="${invitation.email}"`,
-  );
+  console.error('[Invitation] Email mismatch for invitation:', invitation.id);
   const error = createDomainError(AUTH_ERRORS.FORBIDDEN, {
     reason: 'email_mismatch',
-    userEmail: currentUser.email,
-    invitationEmail: invitation.email,
   });
   return c.json(error, error.statusCode);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.error(
`[Invitation] Email mismatch: user email="${currentUser.email}", invitation email="${invitation.email}"`,
);
const error = createDomainError(AUTH_ERRORS.FORBIDDEN, {
reason: 'email_mismatch',
userEmail: currentUser.email,
invitationEmail: invitation.email,
});
return c.json(error, error.statusCode);
console.error('[Invitation] Email mismatch for invitation:', invitation.id);
const error = createDomainError(AUTH_ERRORS.FORBIDDEN, {
reason: 'email_mismatch',
});
return c.json(error, error.statusCode);
🤖 Prompt for AI Agents
In packages/workers/src/routes/orgs/invitations.js around lines 416 to 424, the
code logs plain email addresses and returns them inside the domain error
payload; remove PII by deleting the console.error that prints user/invitation
emails and change the createDomainError payload to omit or redact emails (e.g.,
only include reason: 'email_mismatch' or a non-identifying flag). Ensure the
HTTP JSON response does not contain raw email addresses (replace with masked
values or omit entirely) and keep a generic log message like "Invitation email
mismatch" if telemetry is required.

Comment on lines +262 to +265
await db
.update(projectMembers)
.set({ role })
.where(and(eq(projectMembers.projectId, projectId), eq(projectMembers.userId, memberId)));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing check for target member existence in PUT handler.

The PUT handler updates the member role without first verifying the member exists. If the member doesn't exist, the update will silently succeed with no rows affected.

Suggested fix
+     // Verify member exists before update
+     const memberExists = await db
+       .select({ id: projectMembers.id })
+       .from(projectMembers)
+       .where(and(eq(projectMembers.projectId, projectId), eq(projectMembers.userId, memberId)))
+       .get();
+     
+     if (!memberExists) {
+       const error = createDomainError(
+         PROJECT_ERRORS.NOT_FOUND,
+         { projectId, userId: memberId },
+         'Member not found',
+       );
+       return c.json(error, error.statusCode);
+     }

      await db
        .update(projectMembers)
        .set({ role })
        .where(and(eq(projectMembers.projectId, projectId), eq(projectMembers.userId, memberId)));
🤖 Prompt for AI Agents
In packages/workers/src/routes/orgs/members.js around lines 262 to 265, the PUT
handler updates a member's role without verifying the target member exists, so
the update can silently affect 0 rows; before performing the update, query the
projectMembers table for a row matching projectId and memberId (or perform the
update with a RETURNING/affected-rows check) and if no row is found respond with
a 404 (or appropriate error) instead of proceeding, otherwise apply the role
update; ensure the handler returns the correct status/body based on existence
check or update result.

Comment thread scripts/loc-report.mjs
Comment on lines +61 to +64
function isCodeFile(file) {
const ext = file.substring(file.lastIndexOf('.'));
return CODE_EXTENSIONS.has(ext.toLowerCase());
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Handle files without extensions.

The isCodeFile() function doesn't handle files without extensions correctly. When lastIndexOf('.') returns -1, substring(-1) returns the entire string except the first character, which may incorrectly match or fail to match against CODE_EXTENSIONS.

Proposed fix
 function isCodeFile(file) {
-  const ext = file.substring(file.lastIndexOf('.'));
+  const lastDot = file.lastIndexOf('.');
+  if (lastDot === -1) {
+    return false;
+  }
+  const ext = file.substring(lastDot);
   return CODE_EXTENSIONS.has(ext.toLowerCase());
 }
🤖 Prompt for AI Agents
In scripts/loc-report.mjs around lines 61 to 64, isCodeFile() fails for
filenames without a dot because substring(-1) returns almost the whole name;
update the function to first get the lastIndexOf('.') and if it is -1 return
false (or use path.extname and check for empty string), then extract the
extension, normalize to lowercase, and test against CODE_EXTENSIONS so files
with no extension are not treated as code files.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Dec 30, 2025

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
corates d22b5eb Commit Preview URL

Branch Preview URL
Jan 01 2026, 11:32 PM

InfinityBowman and others added 23 commits December 30, 2025 17:22
@InfinityBowman InfinityBowman merged commit 20dafe5 into main Jan 1, 2026
2 of 4 checks passed
@InfinityBowman InfinityBowman deleted the dev branch January 1, 2026 23:31
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