Skip to content

feat: refactor to add workspace for future features#6

Merged
adiologydev merged 7 commits into
mainfrom
feat/tenants
Jun 11, 2025
Merged

feat: refactor to add workspace for future features#6
adiologydev merged 7 commits into
mainfrom
feat/tenants

Conversation

@adiologydev
Copy link
Copy Markdown
Member

@adiologydev adiologydev commented Jun 11, 2025

Summary by CodeRabbit

  • New Features

    • Introduced workspace management with support for multiple workspaces, roles, and memberships.
    • Added workspace selection and switching in the sidebar via a new workspace selector component.
    • Implemented workspace-aware access control for database connections and queries.
    • Added a new alert dialog UI component for consistent confirmation dialogs.
    • Introduced a new API endpoint to check PostgreSQL connection status.
  • Enhancements

    • Database connections and queries are now scoped to workspaces instead of individual users.
    • Updated shared UI and middleware to provide workspace-related data throughout the app.
    • Improved PostgreSQL connection cards with real-time status monitoring.
  • Bug Fixes

    • Adjusted SQL error display for correct text color in light and dark themes.
    • Updated user menu label from "Settings" to "User Settings" for clarity.
  • Chores

    • Upgraded and added several backend and frontend dependencies.
    • Added development tools and updated configuration files for workspace features.
    • Updated Docker and environment configuration for workspace support.
  • Refactor

    • Replaced all usage of ULID identifiers with standard IDs for database connections.
    • Centralized rendering of PostgreSQL connection cards into a dedicated component.
  • Documentation

    • Added and updated TypeScript interfaces for workspace and connection status data.
  • Style

    • Improved dropdown and sidebar UI for workspace selection.
  • Tests

    • Added .gitignore for debugbar storage to maintain a clean development environment.

- Added workspace context management with middleware for setting and switching workspaces.
- Introduced `HasWorkspaceScope` trait for automatic workspace filtering on database connections.
- Updated database connection handling to associate connections with workspaces instead of users.
- Enhanced PostgreSQL connection status handling and UI components for better user experience.
- Updated migrations to support new workspace and membership structures.
- Refactored related components and API routes to accommodate workspace-based logic.
- Implemented workspace selection UI in the sidebar with a new `TeamSelector` component.
- Enhanced user model to include relationships for workspaces and memberships.
- Updated middleware to share user workspaces data with the frontend.
- Refactored related components to utilize the new workspace management features.
- Added `@radix-ui/react-alert-dialog` for enhanced alert dialog functionality.
- Updated various dependencies including `@inertiajs/react`, `laravel-vite-plugin`, and `react-hook-form` to their latest versions for improved performance and features.
- Refactored components for better code consistency and readability.
- Fixed minor bugs in the Postgres connection handling and user workspace data sharing.
@adiologydev
Copy link
Copy Markdown
Member Author

@coderabbitai

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 11, 2025

Tip

For best results, initiate chat on the files or code changes.

@adiologydev Hi there! How can I assist you on this PR? Let me know if you’d like a code review, help writing tests, planning refactors, or anything else.

@adiologydev
Copy link
Copy Markdown
Member Author

adiologydev commented Jun 11, 2025

Tip

For best results, initiate chat on the files or code changes.

@adiologydev Hi there! How can I assist you on this PR? Let me know if you’d like a code review, help writing tests, planning refactors, or anything else.

@coderabbitai Can you review this PR?

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 11, 2025

Tip

For best results, initiate chat on the files or code changes.

@adiologydev I’m pulling the latest diff for this PR so I can provide a thorough review. One moment while I gather the changes…

@adiologydev
Copy link
Copy Markdown
Member Author

Tip

For best results, initiate chat on the files or code changes.

@adiologydev I’m pulling the latest diff for this PR so I can provide a thorough review. One moment while I gather the changes…

@coderabbitai any updates on the PR review?

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 11, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e

# 1. Determine the base branch (main / master) to diff against
if git show-ref --verify --quiet refs/remotes/origin/main; then
  BASE=origin/main
elif git show-ref --verify --quiet refs/remotes/origin/master; then
  BASE=origin/master
else
  # Fallback: pick the first branch that looks like main/master
  BASE=$(git branch -r | grep -E 'origin/(main|master)' | head -n 1 | awk '{print $1}')
fi
echo "Diffing against $BASE"

# 2. List changed files in the PR
echo "=== Changed files ==="
git diff --name-only "$BASE"...HEAD

# 3. Show the unified diff for the PR
echo ""
echo "=== Full diff ==="
git diff "$BASE"...HEAD

Length of output: 285


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Inspecting remotes..."
git remote -v || { echo "⚠️  No remotes configured in this sandbox clone"; exit 1; }

echo ""
echo "Discovering the default branch on origin…"
DEFAULT_BRANCH=$(git remote show origin | awk '/HEAD branch/ {print $NF}')
if [[ -z "$DEFAULT_BRANCH" ]]; then
  echo "❌ Could not determine default branch of origin"
  exit 1
fi
echo "Default branch is: $DEFAULT_BRANCH"

echo ""
echo "Fetching the default branch (shallow)…"
git fetch --depth=1 origin "$DEFAULT_BRANCH"

echo ""
echo "=== Changed files in PR (vs origin/$DEFAULT_BRANCH) ==="
git diff --name-only "origin/$DEFAULT_BRANCH"...HEAD || {
  echo "❌ git diff failed; trying fallback to previous commit"
  git diff --name-only HEAD~1..HEAD || true
}

echo ""
echo "=== Full diff ==="
git diff "origin/$DEFAULT_BRANCH"...HEAD || {
  echo "❌ git diff failed; trying fallback to previous commit"
  git diff HEAD~1..HEAD || true
}

Length of output: 1702


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo ""
echo "Attempting simple two-dot diff (does not require a merge base)…"
echo "=== Changed files (origin/main..HEAD) ==="
git diff --name-only origin/main HEAD || {
  echo "❌ Could not generate two-dot diff either. Listing branches for debugging:"
  git branch -a -v
  exit 0
}

echo ""
echo "=== Full diff (origin/main..HEAD) ==="
git diff origin/main HEAD

Length of output: 67372


@adiologydev here’s an initial high-level review of the PR.
(I focused on the PHP/Laravel back-end pieces since they introduce the largest behaviour changes. If you’d like a deeper dive into the React/TS or lock-file bumps, let me know.)

────────────────────────────────────────────────────────
Key themes & observations
• Multi-tenancy groundwork – A full “Workspace” domain (models, middleware, config) has been added. Most existing resources that used user_id now pivot to workspace_id, and a global scope (HasWorkspaceScope) limits queries to the active workspace.

• ULID overhaul – HasUlid now returns id instead of ulid, and the main Users migration switches to ulid('id')->primary(). All controllers/migrations were partly updated, but a few hard-coded references to ulid remain (see notes).

• Middleware additions – WorkspaceMiddleware and SetWorkspaceContext both set Context::set('workspace_id', …). They overlap; you probably only need one.

• Dependency bumps – Large JS & composer lockfile upgrades (Laravel 12.18, React 19.1, etc.). I didn’t spot breaking-change usage but recommend running the full test suite.

────────────────────────────────────────────────────────
Potential issues / improvement points

  1. HasWorkspaceScope – use of parent::addGlobalScope

    parent::addGlobalScope('workspace', …);

    In a trait, parent:: refers to the model’s parent class (Illuminate\Database\Eloquent\Model). That works but is unusual; Laravel docs use static::addGlobalScope. Consider switching for clarity and IDE support.

  2. Duplicate workspace middlewares
    SetWorkspaceContext (new)
    WorkspaceMiddleware (new, more feature-rich)
    Running both could set the context twice and do redundant DB queries. Choose one (likely the richer WorkspaceMiddleware) and register only that in app/Http/Kernel.php.

  3. Mixed id vs ulid usage
    • Controllers now route using $connection->id, but DatabaseQueryController::executeQuery still accepts connection_id from the request body and does where('id', …)—OK.
    • Comment blocks & some route parameters still reference ulid. A quick grep shows:

    resources/js/pages/postgres/**                       // several ULID assumptions
    app/Http/Controllers/Postgres/PostgresQueryController // still has comment “ulid”
    

    Make sure the front-end now sends id strings; otherwise look-ups will fail.

  4. New global scope and seeders
    The HasWorkspaceScope trait automatically injects workspace_id on creating. This is great, but any factories/seeders that create models outside an HTTP request will now miss Context::get('workspace_id') and may throw SQL errors (NULL not allowed). Consider:

    DatabaseConnection::factory()->for($workspace)->create();

    or setting a fallback: $model->workspace_id ??= config('workspace.default_id');

  5. Security check in WorkspaceMiddleware

    $workspaceBelongsToUser = $user->workspaces()
        ->where('workspace_id', $workspaceId)->exists();

    workspaces() is a hasMany(WorkspaceMembership::class) so the column is id not workspace_id. Should be:

    ->where('id', $workspaceId)

    or better relate via the pivot.

  6. Tiny style / consistency nits
    • Spacing around concatenation ('error: ' . $e->getMessage() – good in one place, missing in others).
    fn (): array vs fn(): array – pick one style (Laravel uses no space).
    Context::set('workspace_id', …) – consider a constant to avoid magic strings.

────────────────────────────────────────────────────────
Suggested next steps

• Decide which workspace middleware to keep and register only that.
• Audit all ulid string usages on front- & back-end; migrate to id or keep both columns temporarily.
• Fix minor query/relationship column names (workspaces() vs workspace_id).
• Add tests:
– Middleware switching workspace & access control.
HasWorkspaceScope automatically filtering results and setting IDs.
• Run DB seeders/factories in CI to catch NULL workspace_id issues early.

Let me know if you’d like me to draft specific code changes, write PHPUnit tests, or review the React side in detail!

- Added `barryvdh/laravel-debugbar` for improved debugging capabilities.
- Updated `laravel/pint` to version 1.22 for enhanced code quality checks.
- Refactored workspace context management in middleware and models for better clarity and performance.
- Removed unused `SetWorkspaceContext` middleware to streamline the codebase.
- Improved error handling in PostgreSQL connection and query execution.
- Cleaned up code formatting for consistency across various files.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 11, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

This update introduces workspace-based multi-tenancy to the application. Models, controllers, and middleware are refactored to associate database connections and queries with workspaces instead of individual users. New models and migrations implement workspaces, roles, and memberships. The frontend adds workspace selection, updates connection handling to use workspace-scoped IDs, and centralizes connection status display. Supporting configuration, middleware, and event listeners are added for workspace management and context switching.

Changes

File(s) / Group Change Summary
app/Concerns/HasUlid.php uniqueIds() now returns ['id'] instead of ['ulid'].
app/Concerns/HasWorkspaceScope.php New trait adds global workspace scoping to models.
app/Http/Controllers/Postgres/PostgresConnectionController.php Shifts ownership and access control from user to workspace, adds connectionStatus endpoint, updates routing and authorization logic.
app/Http/Controllers/Postgres/PostgresQueryController.php Authorization checks now use workspace_id instead of user_id; connection lookup uses id not ulid.
app/Http/Middleware/HandleInertiaRequests.php Shares workspace data with frontend via new helper method.
app/Http/Middleware/WorkspaceMiddleware.php New middleware for workspace context switching and sharing.
app/Listeners/RegisteredEvent.php New listener creates a workspace and membership for new users.
app/Models/DatabaseConnection.php Now uses workspace_id, adds HasWorkspaceScope, updates relationships.
app/Models/DatabaseQuery.php Adds HasUlid trait.
app/Models/Role.php New model for roles with permissions accessor and memberships relation.
app/Models/User.php Adds HasUlid, workspace relationships, and current workspace accessor.
app/Models/Workspace.php New model for workspaces with owner and member relationships.
app/Models/WorkspaceMembership.php New model for workspace memberships, permissions, and role helpers.
bootstrap/app.php Registers WorkspaceMiddleware in the web middleware group.
composer.json Adds barryvdh/laravel-debugbar, updates laravel/pint, adds pint script.
config/workspace.php New config for workspace enablement and role permissions.
database/migrations/0001_01_01_000000_create_users_table.php Users table uses ULID primary key; sessions table references user by ULID.
database/migrations/2025_03_13_184947_create_database_connections_table.php database_connections uses ULID primary key; removes user_id.
database/migrations/2025_03_14_185943_create_database_queries_table.php database_queries uses ULID primary key; foreign keys now ULIDs.
database/migrations/2025_05_29_133435_create_workspaces_table.php Adds workspaces, roles, memberships tables; updates users and connections for workspace support.
docker/production/Dockerfile Sets WORKSPACE_ENABLED env variable in build and production stages.
package.json Updates multiple dependencies; adds @radix-ui/react-alert-dialog.
resources/js/components/app-sidebar.tsx Adds workspace selector to sidebar, updates imports.
resources/js/components/database/sql-error-display.tsx Adjusts error details text color for light/dark themes.
resources/js/components/postgres/postgres-connection.tsx New component for displaying PostgreSQL connection status and details.
resources/js/components/ui/alert-dialog.tsx New alert dialog component set based on Radix UI primitives.
resources/js/components/user-menu-content.tsx Renames "Settings" to "User Settings" in user menu.
resources/js/components/workspace-selector.tsx New TeamSelector component for workspace switching.
resources/js/pages/postgres/explorer.tsx Updates connection identifier from ulid to id in navigation and props.
resources/js/pages/postgres/postgres.tsx Refactors connection card rendering to use new PostgresConnection component; updates connection prop types.
resources/js/stores/postgres-store.ts All API calls now use connection.id instead of connection.ulid.
resources/js/types/database.d.ts Renames DatabaseConnection.ulid to id.
resources/js/types/index.d.ts Adds Workspace interface and workspace info to SharedData.
resources/js/types/postgres.d.ts Adds ConnectionStatus interface.
routes/web.php Route params for connections use id instead of ulid; adds /postgres/connection-status/{id} endpoint.
storage/debugbar/.gitignore New .gitignore to ignore all files in storage/debugbar except itself.

Sequence Diagram(s)

Workspace Context and Connection Access

sequenceDiagram
    participant User
    participant Middleware
    participant Controller
    participant DatabaseConnection
    participant Workspace

    User->>Middleware: Sends request (optionally with switch_workspace)
    Middleware->>Middleware: Checks authentication and workspace membership
    Middleware->>Middleware: Sets current_workspace_id in context
    Middleware->>Controller: Passes request with workspace context
    Controller->>DatabaseConnection: Queries connections filtered by workspace_id
    DatabaseConnection->>Workspace: BelongsTo relationship for workspace
    Controller->>User: Checks if user's current_workspace_id matches connection's workspace_id
    Controller-->>User: Returns workspace-scoped data
Loading

User Registration and Workspace Creation

sequenceDiagram
    participant User
    participant RegisteredEventListener
    participant Workspace
    participant WorkspaceMembership

    User->>RegisteredEventListener: Triggers Registered event
    RegisteredEventListener->>Workspace: Creates new workspace (owner = user)
    RegisteredEventListener->>WorkspaceMembership: Creates membership (role = owner)
    RegisteredEventListener->>User: Sets current_workspace_id
    RegisteredEventListener-->>User: Registration completes with workspace context
Loading

Possibly related PRs

Poem

🐇
A warren of workspaces, each with its own key,
Now users can hop from one team to three!
Connections and queries, all neatly aligned,
With roles and permissions carefully defined.
The sidebar now sparkles with teams you can pick,
This rabbit is thrilled—multi-tenancy’s slick!
🌱


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f9ce78b and d196261.

📒 Files selected for processing (2)
  • database/migrations/2025_03_14_185943_create_database_queries_table.php (1 hunks)
  • resources/js/components/workspace-selector.tsx (1 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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: 18

🔭 Outside diff range comments (6)
resources/js/stores/postgres-store.ts (2)

14-21: 🛠️ Refactor suggestion

Fix type definitions for ULID identifiers
QueryHistoryItem.id and database_connection_id are ULIDs (strings), not numbers. Update their types accordingly:

-export interface QueryHistoryItem {
-    id: number;
-    database_connection_id: number;
+export interface QueryHistoryItem {
+    id: string;
+    database_connection_id: string;

168-176: ⚠️ Potential issue

Standardize route parameter naming
The route("postgres.query.execute") call uses connection_id, whereas other route helpers use connection. Confirm the expected route parameter in routes/web.php and make it consistent (e.g., use connection for both).

database/migrations/2025_03_13_184947_create_database_connections_table.php (1)

14-22: 🛠️ Refactor suggestion

Consider encrypting the password column and adding workspace-scoped uniqueness.

  1. $table->string('password') stores credentials as plain text. Either:
-$table->string('password');
+$table->text('password_encrypted');

and encrypt/decrypt in the model accessor/mutator, or leverage Laravel’s built-in encrypted cast.

  1. With multi-tenancy you probably want a uniqueness guard such as
    unique(['workspace_id', 'name']) to prevent duplicate connection names inside the same workspace.

Both changes can be done in a follow-up migration before release.

app/Http/Controllers/Postgres/PostgresQueryController.php (1)

58-62: ⚠️ Potential issue

Validation rule should match ULID format.

connection_id column is a ULID (char(26)), yet the rule only asserts string.
Laravel provides a ulid rule:

-        $request->validate([
-            'connection_id' => 'required|string',
+        $request->validate([
+            'connection_id' => 'required|ulid',

This prevents malformed IDs from hitting the database.

app/Models/DatabaseConnection.php (1)

12-28: ⚠️ Potential issue

Avoid mass-assignment of workspace_id; rely on the trait instead

workspace_id is now assignable via $fillable.
Because the model also uses HasWorkspaceScope, an attacker could forge a request that overrides the ID before the trait’s creating callback runs (or if the trait only sets the ID when it is null). That breaks tenant isolation.

-        'metadata',
-        'workspace_id',
+        'metadata',

Let the trait populate workspace_id exclusively, or at minimum validate the value against the current workspace in a model event listener.

app/Http/Controllers/Postgres/PostgresConnectionController.php (1)

307-308: 💡 Verification agent

🧩 Analysis chain

Verify if user ID is still needed for row insertion.

While the authorization uses workspace context, the insertRow service call still passes the user ID. This might be for audit logging purposes.


🏁 Script executed:

#!/bin/bash
# Description: Check how the user ID is used in the PostgresService insertRow method

# Search for the insertRow method implementation
ast-grep --pattern 'function insertRow($_connection, $_database, $_schema, $_table, $_data, $_userId) {
  $$$
}'

# Also check with different parameter names
rg -A 10 "function insertRow.*userId" app/Services/

Length of output: 168


I wasn’t able to locate the insertRow implementation or any usage of the passed user ID. Let’s broaden our search to ensure we catch all definitions and calls:


🏁 Script executed:

#!/bin/bash
# Find any definitions of insertRow in the Services directory
rg -n "function insertRow" -R app/Services -A 5

# Find all calls to insertRow to see parameter usage
rg -n "insertRow\(" -R app -A 3

Length of output: 893


Let’s correct the ripgrep flags and search the entire PHP code for both the definition and calls of insertRow:


🏁 Script executed:

#!/bin/bash
# Find the insertRow method definition in Services
rg -n "function insertRow" -g "*.php" app/Services -A 5

# Find all calls to insertRow throughout the app to see passed parameters
rg -n "insertRow\(" -g "*.php" app/ -A 3

Length of output: 1807


Remove the now-unused user ID argument from the insertRow call

The PostgresService::insertRow signature accepts only five parameters (connection, database, schema, table, data), so passing the sixth argument ($request->user()->id) will cause an argument-count error. Please remove it:

• File: app/Http/Controllers/Postgres/PostgresConnectionController.php
Around lines 301–308

Suggested diff:

-        $result = $this->postgresService->insertRow(
-            $connection,
-            $request->input('database'),
-            $request->input('schema'),
-            $request->input('table'),
-            $request->input('data'),
-            $request->user()->id
-        );
+        $result = $this->postgresService->insertRow(
+            $connection,
+            $request->input('database'),
+            $request->input('schema'),
+            $request->input('table'),
+            $request->input('data')
+        );

If audit logging is still required, consider handling it inside the service or via a dedicated event/listener instead of passing the user ID here.

♻️ Duplicate comments (5)
resources/js/stores/postgres-store.ts (5)

188-195: Standardize route parameter naming
route("postgres.query.history") uses connection as the parameter key, but the execute route uses connection_id. Align parameter keys across all route helpers.


297-302: Standardize route parameter naming
Verify the route parameter key for postgres.schemas matches the backend definition (likely {connection}) and unify naming.


330-334: Standardize route parameter naming
Ensure postgres.tables route parameter naming is consistent with other endpoints.


359-366: Standardize route parameter naming
Ensure postgres.columns endpoint uses the correct route parameter key for connection (should match backend route model binding).


368-376: Standardize route parameter naming
Ensure postgres.data endpoint parameter aligns with the route definition ({connection} vs connection_id).

🧹 Nitpick comments (22)
resources/js/components/user-menu-content.tsx (1)

25-28: Label hard-coded; consider i18n key instead

"User Settings" is now hard-coded, whereas the rest of the app appears to rely on translation keys in many places. If you intend to localise the UI, swap the literal for a translation helper (e.g. t("user.settings")).

bootstrap/app.php (1)

24-25: Ordering hint

WorkspaceMiddleware runs after AddLinkHeadersForPreloadedAssets. If any link headers rely on workspace context (unlikely, but possible when preloading tenant-specific assets) you may want to move WorkspaceMiddleware earlier.

resources/js/types/postgres.d.ts (1)

55-62: Consider camelCase mapping or consistent naming
The new ConnectionStatus interface uses snake_case to match backend JSON. For cleaner TS usage, consider converting to camelCase properties in the store or use a transformation layer.

resources/js/pages/postgres/explorer.tsx (1)

165-170: connectionId prop may be redundant.

<PostgresContent … connectionId={connection.id} connection={connection} /> passes both the id and the full object. Unless the child component explicitly needs only the primitive id for memoisation, one prop is enough.

-connectionId={connection.id}

inside PostgresContent simplifies the API surface.

docker/production/Dockerfile (2)

40-43: Duplicate WORKSPACE_ENABLED definition – keep one canonical source.

WORKSPACE_ENABLED=false is defined in both the build and runtime stages. That’s fine, but if you intend to toggle it at deploy time you’ll need to override two ENV declarations. Consider setting it only in the final stage and passing it during docker build for compile-time purposes.


100-104: Hard-coding SSL_MODE=off.

If SSL may be required in certain deployments expose this as a build-time ARG/ENV instead of a fixed value here to avoid rebuilding images just to flip the switch.

composer.json (1)

55-57: Convenience script naming – match Pint’s convention.

Laravel Pint already ships with a default vendor/bin/pint executable; many teams alias it to format. Consider:

-"pint": ["pint"]
+"format": ["pint"]

Minor, but keeps the scripts section self-documenting.

resources/js/types/index.d.ts (2)

26-31: Workspace.role could benefit from an enum.

role: string has no type safety. Defining:

export type WorkspaceRole = "owner" | "admin" | "member" | "viewer";

and using role: WorkspaceRole; prevents accidental misspellings.


39-40: Consider renaming all to list for clarity.

workspaces.all reads oddly in code (workspaces.all.map(...)). A more conventional shape might be { items: Workspace[] } or { list: Workspace[] }, but this is purely cosmetic.

app/Models/Workspace.php (1)

14-19: Consider exposing factory & slug semantics.

  1. The model omits the HasFactory trait; this makes seeding / model factories less ergonomic.
  2. slug is listed as fillable but no mechanism (observer / mutator) guarantees uniqueness or automatic generation.
    Leaving this to callers increases the risk of duplicate slugs.
 use Illuminate\Database\Eloquent\Relations\HasMany;
+use Illuminate\Database\Eloquent\Factories\HasFactory;

 class Workspace extends Model
 {
-    use HasUlid;
+    use HasUlid, HasFactory;

A simple creating callback could populate the slug from the name if absent.

app/Http/Controllers/Postgres/PostgresQueryController.php (1)

38-41: Comment is outdated — now workspace-level auth, not user-level.

The code correctly checks workspace_id, but the inline comment still says “their own connections”.
Update to avoid confusion for future maintainers.

app/Concerns/HasWorkspaceScope.php (1)

13-17: Avoid duplicate Context::get calls & tighten guard.

Calling Context::get twice incurs needless work. Prefer caching & an explicit has check:

-        static::addGlobalScope('workspace', function (Builder $builder) {
-            if ($workspaceId = Context::get('workspace_id')) {
+        static::addGlobalScope('workspace', function (Builder $builder) {
+            if (Context::has('workspace_id')) {
+                $workspaceId = Context::get('workspace_id');
                 $builder->where('workspace_id', $workspaceId);
             }
resources/js/components/app-sidebar.tsx (1)

50-52: Null-safety when workspaces prop is absent.

usePage<SharedData>().props might not include workspaces during SSR errors or legacy pages.
Destructure defensively:

-    const { workspaces } = usePage<SharedData>().props;
+    const { workspaces = { enabled: false, data: [] } } = usePage<SharedData>().props;

Prevents runtime crashes in edge cases.

app/Models/User.php (1)

71-74: Duplicate accessor: workspaceMemberships() duplicates (old) workspaces() purpose
If you adopt the change above, workspaceMemberships() becomes the clear place to access membership rows.
Consider renaming to memberships() for brevity or removing the earlier mis-named method entirely.

resources/js/components/workspace-selector.tsx (1)

7-18: Component/product naming drift (TeamSelector vs. workspaces)
The backend, config and permissions consistently talk about “workspace”. Consider renaming the component to WorkspaceSelector for clarity and future grep-ability.

app/Http/Middleware/WorkspaceMiddleware.php (1)

24-34: Single extra query per request – eager-load memberships or cache ID list
$user->workspaces()->where('workspace_id', …)->exists() runs an additional query every time the user clicks a workspace link. With the relationship fix (belongsToMany) you can instead rely on an in-memory collection:

if ($user->workspaces->contains('id', $workspaceId)) { … }

or cache the allowed IDs in session. This removes one DB hit from every navigation.

app/Listeners/RegisteredEvent.php (1)

28-42: Wrap workspace/user bootstrap in a DB transaction
User creation fires the Registered event inside a transaction, but this listener performs three more write queries. If any of them fails, data may be left half-created (e.g. workspace without membership).

DB::transaction(function () use ($user) {
    $workspace = Workspace::create([...]);
    WorkspaceMembership::create([...]);
    $user->forceFill(['current_workspace_id' => $workspace->id])->save();
});

Import Illuminate\Support\Facades\DB; at the top.

resources/js/pages/postgres/postgres.tsx (1)

7-11: Remove unused dayjs import to satisfy linters

dayjs and relativeTime are imported and extended but no longer referenced.

-import dayjs from "dayjs";
-import relativeTime from "dayjs/plugin/relativeTime";
-
-dayjs.extend(relativeTime);

This keeps the bundle lean and avoids biome / eslint warnings.

app/Models/WorkspaceMembership.php (1)

44-57: Replace magic strings with constants / enum

Hard-coding 'owner' | 'admin' | 'member' in three separate helpers is error-prone. Consider:

enum WorkspaceRole: string
{
    case Owner = 'owner';
    case Admin = 'admin';
    case Member = 'member';
}

public function isOwner(): bool
{
    return $this->role === WorkspaceRole::Owner->value;
}

This guarantees consistency across the codebase.

routes/web.php (1)

20-22: Inconsistent param naming ({id} vs {connection})

All other endpoints use {connection} and leverage implicit model binding.
Consider making connectionStatus use {connection} too for consistency and automatic authorization.

resources/js/components/ui/alert-dialog.tsx (1)

54-57: Very long className string – consider extracting to a constant

This improves readability and keeps future tweaks in one place.

const contentClasses =
  "bg-background ... sm:max-w-lg"

<AlertDialogPrimitive.Content className={cn(contentClasses, className)} ... />
app/Http/Controllers/Postgres/PostgresConnectionController.php (1)

124-134: Update comment to reflect workspace-based ownership.

The comment mentions "authenticated user" but the implementation associates connections with workspaces.

-// Ensure the connection is created for the authenticated user only
+// Create the connection for the user's current workspace
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3be9274 and cdc2747.

⛔ Files ignored due to path filters (3)
  • bun.lock is excluded by !**/*.lock
  • composer.lock is excluded by !**/*.lock
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (36)
  • app/Concerns/HasUlid.php (1 hunks)
  • app/Concerns/HasWorkspaceScope.php (1 hunks)
  • app/Http/Controllers/Postgres/PostgresConnectionController.php (8 hunks)
  • app/Http/Controllers/Postgres/PostgresQueryController.php (3 hunks)
  • app/Http/Middleware/HandleInertiaRequests.php (2 hunks)
  • app/Http/Middleware/WorkspaceMiddleware.php (1 hunks)
  • app/Listeners/RegisteredEvent.php (1 hunks)
  • app/Models/DatabaseConnection.php (3 hunks)
  • app/Models/DatabaseQuery.php (1 hunks)
  • app/Models/Role.php (1 hunks)
  • app/Models/User.php (2 hunks)
  • app/Models/Workspace.php (1 hunks)
  • app/Models/WorkspaceMembership.php (1 hunks)
  • bootstrap/app.php (2 hunks)
  • composer.json (2 hunks)
  • config/workspace.php (1 hunks)
  • database/migrations/0001_01_01_000000_create_users_table.php (2 hunks)
  • database/migrations/2025_03_13_184947_create_database_connections_table.php (1 hunks)
  • database/migrations/2025_03_14_185943_create_database_queries_table.php (1 hunks)
  • database/migrations/2025_05_29_133435_create_workspaces_table.php (1 hunks)
  • docker/production/Dockerfile (2 hunks)
  • package.json (2 hunks)
  • resources/js/components/app-sidebar.tsx (3 hunks)
  • resources/js/components/database/sql-error-display.tsx (1 hunks)
  • resources/js/components/postgres/postgres-connection.tsx (1 hunks)
  • resources/js/components/ui/alert-dialog.tsx (1 hunks)
  • resources/js/components/user-menu-content.tsx (1 hunks)
  • resources/js/components/workspace-selector.tsx (1 hunks)
  • resources/js/pages/postgres/explorer.tsx (2 hunks)
  • resources/js/pages/postgres/postgres.tsx (2 hunks)
  • resources/js/stores/postgres-store.ts (6 hunks)
  • resources/js/types/database.d.ts (1 hunks)
  • resources/js/types/index.d.ts (1 hunks)
  • resources/js/types/postgres.d.ts (1 hunks)
  • routes/web.php (1 hunks)
  • storage/debugbar/.gitignore (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (8)
bootstrap/app.php (1)
app/Http/Middleware/WorkspaceMiddleware.php (1)
  • WorkspaceMiddleware (11-44)
app/Models/Role.php (1)
app/Models/WorkspaceMembership.php (2)
  • WorkspaceMembership (8-58)
  • getPermissionsAttribute (39-42)
resources/js/components/workspace-selector.tsx (2)
resources/js/types/index.d.ts (1)
  • SharedData (33-40)
app/Models/User.php (2)
  • workspaces (61-64)
  • currentWorkspace (66-69)
app/Models/WorkspaceMembership.php (3)
app/Models/Workspace.php (1)
  • Workspace (10-30)
app/Models/User.php (1)
  • User (13-75)
app/Models/Role.php (2)
  • Role (8-27)
  • getPermissionsAttribute (23-26)
app/Http/Middleware/HandleInertiaRequests.php (2)
app/Models/User.php (4)
  • User (13-75)
  • workspaceMemberships (71-74)
  • workspaces (61-64)
  • currentWorkspace (66-69)
app/Models/WorkspaceMembership.php (3)
  • user (28-31)
  • workspace (23-26)
  • role (33-36)
resources/js/components/postgres/postgres-connection.tsx (3)
app/Models/DatabaseConnection.php (1)
  • DatabaseConnection (10-53)
resources/js/types/database.d.ts (1)
  • DatabaseConnection (21-29)
resources/js/types/postgres.d.ts (1)
  • ConnectionStatus (56-61)
resources/js/pages/postgres/postgres.tsx (2)
app/Models/DatabaseConnection.php (1)
  • DatabaseConnection (10-53)
resources/js/types/database.d.ts (1)
  • DatabaseConnection (21-29)
routes/web.php (2)
app/Http/Controllers/Postgres/PostgresConnectionController.php (1)
  • PostgresConnectionController (13-443)
app/Http/Controllers/Postgres/PostgresQueryController.php (1)
  • PostgresQueryController (14-163)
🔇 Additional comments (15)
storage/debugbar/.gitignore (1)

1-2: Ignored correctly – no further action needed

Adding the pattern to keep the directory out of VCS (while retaining the .gitignore file itself) is standard practice for Debugbar storage.

resources/js/types/database.d.ts (1)

21-23:

Details

✅ Verification successful

Verify all frontend code migrated from ulidid

Changing the primary identifier is breaking if any leftover code still reads connection.ulid. A quick grep helps:

Please confirm only valid historical references remain (e.g. model-level).


🏁 Script executed:

rg '\.ulid\b' --type ts --type tsx --type js

Length of output: 74


🏁 Script executed:

#!/bin/bash
# Search for .ulid references in TS, TSX, and JS files using file globs
rg '\.ulid\b' -g '*.ts' -g '*.tsx' -g '*.js'

Length of output: 46


All .ulid references removed from frontend code
Verified with rg '\.ulid\b' across all .ts, .tsx, and .js files—no occurrences found. Migration to id is complete; no further action required.

bootstrap/app.php (1)

5-6:

Details

❌ Incorrect review comment

Potential duplicate workspace context middleware

WorkspaceMiddleware is now imported, but SetWorkspaceContext (added elsewhere in the PR) still exists. Having both in the web stack will execute two similar DB look-ups on every request.

Action: pick one (the richer WorkspaceMiddleware seems sufficient) and remove / un-register the other to avoid redundant queries.


🏁 Script executed:

#!/bin/bash
echo "=== Searching for SetWorkspaceContext definitions and usages ==="
rg "SetWorkspaceContext" -n .

echo -e "\n=== Searching for WorkspaceMiddleware definitions and usages ==="
rg "WorkspaceMiddleware" -n .

echo -e "\n=== Inspecting middleware registration in bootstrap/app.php ==="
sed -n '1,200p' bootstrap/app.php | grep -n -C3 "middleware"

Length of output: 1147


No duplicate workspace middleware detected

I searched the codebase for any SetWorkspaceContext class or registration and found zero references—only WorkspaceMiddleware is imported and registered in bootstrap/app.php. There’s no second “workspace” middleware to remove, so no action is required here.

Likely an incorrect or invalid review comment.

app/Concerns/HasUlid.php (1)

13-15: ULID now generated into id – audit migrations & casts

Good move toward a single primary key column. Double-check:

  1. All migrations now use ulid('id')->primary().
  2. Models have public $incrementing = false; and protected $keyType = 'string'; as needed.
  3. Old ulid columns are dropped or aliased.

A brief migration audit will prevent silent key mismatches.

resources/js/components/database/sql-error-display.tsx (1)

40-46: Consistent high-contrast text styling
The update to use text-gray-900 for light mode paired with dark:text-gray-200 ensures good readability across themes. No issues detected.

config/workspace.php (1)

3-17: Configuration structure looks solid
The new workspace.php config properly defines the enabled flag and role permissions. Ensure environment variable documentation is updated accordingly.

app/Models/DatabaseQuery.php (1)

5-12: Ensure ULID trait configuration
The addition of HasUlid aligns with the global identifier strategy. Please verify that the trait sets $incrementing = false and $keyType = 'string' on the model to properly support ULIDs.

resources/js/pages/postgres/explorer.tsx (1)

24-27: Route param update – verify backend route signature.

route("postgres.explore", { connection: connection.id }) assumes the Laravel route parameter is now {connection} (no :ulid constraint). Double-check that routes/web.php has been updated accordingly, otherwise a 404 will occur.

composer.json (1)

14-19: Debugbar in dev only – ensure it never leaks to prod.

barryvdh/laravel-debugbar is great for local debugging but can reveal sensitive queries in production if accidentally enabled. Verify that APP_DEBUGBAR_ENABLED=false (or the service provider is conditioned on app()->isLocal()).

app/Http/Controllers/Postgres/PostgresQueryController.php (1)

83-91: Potential 255-char truncation when persisting queries.

database_queries.query is defined as string (see migration) which maps to varchar(255).
SQL editors routinely exceed that length, so longer statements will be cut and log integrity lost.
Upgrade the column to text and adjust the migration accordingly.

app/Models/User.php (1)

6-16: Verify ULID migration vs. external API expectations
HasUlid is now mixed into User, yet external callers will still see/accept the numeric id column. Double-check that:

  1. the users table migration sets id as ULID (ulid('id')->primary()),
  2. any foreign keys now reference CHAR(26) strings, and
  3. no code (e.g. policies, route-model binding) still assumes an auto-increment integer.
app/Models/DatabaseConnection.php (1)

49-52: LGTM – correct relationship rename

The new workspace() relation matches the foreign key and keeps IDE type-hints intact.

package.json (1)

41-57: Confirm React 19 ecosystem readiness before merging

react/react-dom are bumped to 19.1.x, but React 19 is still in RC/beta for most packages.
Down-stream libraries (@inertiajs/react, Radix, etc.) may still peer-depend on ^18. Build or run-time conflicts are likely.

Please verify:

  1. pnpm i --strict-peer-deps (or equivalent) installs without warnings.
  2. All E2E/UI tests pass in CI.
  3. SSR still works if used (React 19 alters use / suspense semantics).

Roll back to the latest 18.x stable if any incompatibility surfaces.

database/migrations/0001_01_01_000000_create_users_table.php (1)

15-33: ULID migration looks correct

Primary and foreign ULIDs are defined with Laravel helpers; no issues spotted.

resources/js/pages/postgres/postgres.tsx (1)

24-27: Route param change: ensure backend route expects id not ulid

route("postgres.explore", { connection: connection.id }) now sends the numeric/string id.
Double-check the Laravel route definition (routes/web.php) and controller (PostgresConnectionController) were updated to use {connection:id} or Route::model binding on id.

Comment thread app/Models/Workspace.php
Comment on lines +26 to +29
public function members(): HasMany
{
return $this->hasMany(WorkspaceMembership::class);
}
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

Membership relation likely needs BelongsToMany instead of HasMany.

members() returns a HasMany to WorkspaceMembership, so the actual User models are two hops away.
If the intention is Workspace -> users you probably want:

return $this->belongsToMany(User::class, 'workspace_memberships');

Leaving it as-is forces every call‐site to traverse the pivot manually and defeats eager-loading convenience.

🤖 Prompt for AI Agents
In app/Models/Workspace.php around lines 26 to 29, the members() method
currently returns a HasMany relation to WorkspaceMembership, but it should
return a BelongsToMany relation directly to User through the
workspace_memberships pivot table. Change the return type to BelongsToMany and
update the relation to use belongsToMany(User::class, 'workspace_memberships')
so that you can directly access users and benefit from eager loading without
manually traversing the pivot.

Comment on lines +65 to 69
$connection = DatabaseConnection::where('id', $request->input('connection_id'))->firstOrFail();

// Ensure the user can only access their own connections
if ($connection->user_id !== $request->user()->id) {
if ($connection->workspace_id !== $request->user()->current_workspace_id) {
return response()->json([
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

Early exit still allows N + 1 lookup when mismatched.

You fetch the DatabaseConnection and only then assert workspace membership.
Consider folding the workspace filter into the query to avoid an unnecessary DB hit:

-$connection = DatabaseConnection::where('id', $request->input('connection_id'))->firstOrFail();
+$connection = DatabaseConnection::where('id', $request->input('connection_id'))
+    ->where('workspace_id', $request->user()->current_workspace_id)
+    ->firstOrFail();

One query, one round-trip, clearer intent.

🤖 Prompt for AI Agents
In app/Http/Controllers/Postgres/PostgresQueryController.php around lines 65 to
69, the code fetches the DatabaseConnection by ID first and then checks if the
workspace_id matches the user's current workspace, causing an unnecessary
database query if the workspace does not match. To fix this, modify the query to
include a condition that filters by both the connection ID and the user's
current workspace ID in a single query, ensuring only connections belonging to
the user's workspace are retrieved and preventing extra database lookups.

Comment thread app/Concerns/HasWorkspaceScope.php
Comment on lines +15 to 20
$table->ulid('id')->primary();
$table->foreignUlid('database_connection_id')->constrained('database_connections')->cascadeOnDelete();
$table->string('query');
$table->enum('executor', ['user', 'system']);
$table->foreignIdFor(User::class, 'executor_id')->nullable()->constrained()->cascadeOnDelete();
$table->foreignUlid('executor_id')->nullable()->constrained('users')->cascadeOnDelete();
$table->json('parameters')->nullable()->comment('JSON encoded parameters required for the query');
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

query column too small & lacks index for fast history lookups.

  1. string caps at 255 chars – insufficient for many SQL statements.
  2. History endpoints order by created_at; adding an index dramatically improves performance.
-            $table->string('query');
+            $table->text('query');
+
+            // index for /history endpoints
+            $table->index('created_at');
📝 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
$table->ulid('id')->primary();
$table->foreignUlid('database_connection_id')->constrained('database_connections')->cascadeOnDelete();
$table->string('query');
$table->enum('executor', ['user', 'system']);
$table->foreignIdFor(User::class, 'executor_id')->nullable()->constrained()->cascadeOnDelete();
$table->foreignUlid('executor_id')->nullable()->constrained('users')->cascadeOnDelete();
$table->json('parameters')->nullable()->comment('JSON encoded parameters required for the query');
$table->ulid('id')->primary();
$table->foreignUlid('database_connection_id')->constrained('database_connections')->cascadeOnDelete();
- $table->string('query');
+ $table->text('query');
+
+ // index for /history endpoints
+ $table->index('created_at');
$table->enum('executor', ['user', 'system']);
$table->foreignUlid('executor_id')->nullable()->constrained('users')->cascadeOnDelete();
$table->json('parameters')->nullable()->comment('JSON encoded parameters required for the query');
🤖 Prompt for AI Agents
In database/migrations/2025_03_14_185943_create_database_queries_table.php
around lines 15 to 20, the 'query' column is defined as a string which limits it
to 255 characters, too small for many SQL statements. Change the 'query' column
type to 'text' to allow longer queries. Additionally, add an index on the
'created_at' column to improve performance of history lookups that order by this
field.

Comment thread app/Models/User.php
Comment on lines +61 to +65
public function workspaces(): HasMany
{
return $this->hasMany(WorkspaceMembership::class);
}

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

workspaces() points to memberships – rename or change relation type
The method returns HasMany of WorkspaceMembership, not actual Workspace models.
This is confusing for anyone calling $user->workspaces() expecting real workspaces.

Recommended fix:

-use Illuminate\Database\Eloquent\Relations\HasMany;
+use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 ...
-    public function workspaces(): HasMany
-    {
-        return $this->hasMany(WorkspaceMembership::class);
-    }
+    /**
+     * Workspaces the user belongs to.
+     */
+    public function workspaces(): BelongsToMany
+    {
+        return $this->belongsToMany(
+            Workspace::class,
+            'workspace_memberships'
+        )->withPivot('role')->withTimestamps();
+    }

This delivers the expected models and still keeps role data on the pivot.

🤖 Prompt for AI Agents
In app/Models/User.php lines 61 to 65, the workspaces() method currently returns
a HasMany relation to WorkspaceMembership models, which is misleading. Change
this method to return a BelongsToMany relation to Workspace models through the
workspace_memberships pivot table, including pivot data like roles. This will
provide actual Workspace models when calling $user->workspaces() while
preserving membership details on the pivot.

Comment on lines +51 to +53
<p className="text-sm">Online</p>
<p className="text-muted-foreground text-xs">{dayjs(status?.last_tested).fromNow()}</p>
</TooltipContent>
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

dayjs(...).fromNow() requires the relativeTime plugin

Without it, fromNow() is undefined and the component crashes.

-import dayjs from "dayjs";
+import dayjs from "dayjs";
+import relativeTime from "dayjs/plugin/relativeTime";
+
+dayjs.extend(relativeTime);
📝 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
<p className="text-sm">Online</p>
<p className="text-muted-foreground text-xs">{dayjs(status?.last_tested).fromNow()}</p>
</TooltipContent>
// at the top of resources/js/components/postgres/postgres-connection.tsx
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
dayjs.extend(relativeTime);
// ...rest of your imports and component code
🤖 Prompt for AI Agents
In resources/js/components/postgres/postgres-connection.tsx around lines 51 to
53, the usage of dayjs(...).fromNow() requires the relativeTime plugin to be
imported and extended. To fix this, import the relativeTime plugin from dayjs,
then extend dayjs with this plugin before using fromNow(), ensuring the
component does not crash due to undefined method.

Comment on lines +15 to +21
useEffect(() => {
const fetchStatus = async () => {
const response = await axios.get(route("postgres.connection-status", { id: connection.id }));
setStatus(response.data);
};
fetchStatus();
}, [connection.id]);
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

No error / unmount handling in fetchStatus

A failed request will throw an unhandled promise rejection and the effect keeps running on unmounted components.

-        const fetchStatus = async () => {
-            const response = await axios.get(route("postgres.connection-status", { id: connection.id }));
-            setStatus(response.data);
-        };
+        const controller = new AbortController();
+        const fetchStatus = async () => {
+            try {
+                const { data } = await axios.get(
+                    route("postgres.connection-status", { id: connection.id }),
+                    { signal: controller.signal }
+                );
+                setStatus(data);
+            } catch (e) {
+                if (!axios.isCancel(e)) console.error(e);
+            }
+        };
         fetchStatus();
-    }, [connection.id]);
+        return () => controller.abort();
+    }, [connection.id]);
📝 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
useEffect(() => {
const fetchStatus = async () => {
const response = await axios.get(route("postgres.connection-status", { id: connection.id }));
setStatus(response.data);
};
fetchStatus();
}, [connection.id]);
useEffect(() => {
const controller = new AbortController();
const fetchStatus = async () => {
try {
const { data } = await axios.get(
route("postgres.connection-status", { id: connection.id }),
{ signal: controller.signal }
);
setStatus(data);
} catch (e) {
if (!axios.isCancel(e)) console.error(e);
}
};
fetchStatus();
return () => controller.abort();
}, [connection.id]);
🤖 Prompt for AI Agents
In resources/js/components/postgres/postgres-connection.tsx around lines 15 to
21, the fetchStatus async function inside useEffect does not handle errors or
component unmounting, which can cause unhandled promise rejections and state
updates on unmounted components. To fix this, add a try-catch block around the
axios request to handle errors gracefully, and use a flag or AbortController to
check if the component is still mounted before calling setStatus, preventing
state updates after unmount.

Comment thread routes/web.php
Comment on lines 18 to 23
Route::get('/postgres', [PostgresConnectionController::class, 'index'])->name('postgres.index');
Route::post('/postgres', [PostgresConnectionController::class, 'store'])->name('postgres.store');
Route::get('/postgres/{connection:ulid}', [PostgresConnectionController::class, 'explore'])->name('postgres.explore');
Route::get('/postgres/{connection}', [PostgresConnectionController::class, 'explore'])->name('postgres.explore');
Route::post('/postgres/test-connection', [PostgresConnectionController::class, 'testConnection'])->name('postgres.test-connection');
Route::get('/postgres/connection-status/{id}', [PostgresConnectionController::class, 'connectionStatus'])->name('postgres.connection-status');

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

Route ordering causes /postgres/test-connection to hit the generic {connection} route

/postgres/{connection} precedes the more specific /postgres/test-connection and will capture the string test-connection.

Re-order specific routes above the catch-all:

-Route::get('/postgres/{connection}', ...);
-Route::post('/postgres/test-connection', ...);
+Route::post('/postgres/test-connection', ...);
+Route::get('/postgres/{connection}', ...);

Do the same for connection-status if you observe similar clashes.

📝 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
Route::get('/postgres', [PostgresConnectionController::class, 'index'])->name('postgres.index');
Route::post('/postgres', [PostgresConnectionController::class, 'store'])->name('postgres.store');
Route::get('/postgres/{connection:ulid}', [PostgresConnectionController::class, 'explore'])->name('postgres.explore');
Route::get('/postgres/{connection}', [PostgresConnectionController::class, 'explore'])->name('postgres.explore');
Route::post('/postgres/test-connection', [PostgresConnectionController::class, 'testConnection'])->name('postgres.test-connection');
Route::get('/postgres/connection-status/{id}', [PostgresConnectionController::class, 'connectionStatus'])->name('postgres.connection-status');
Route::get('/postgres', [PostgresConnectionController::class, 'index'])->name('postgres.index');
Route::post('/postgres', [PostgresConnectionController::class, 'store'])->name('postgres.store');
- Route::get('/postgres/{connection}', [PostgresConnectionController::class, 'explore'])->name('postgres.explore');
- Route::post('/postgres/test-connection', [PostgresConnectionController::class, 'testConnection'])->name('postgres.test-connection');
+ Route::post('/postgres/test-connection', [PostgresConnectionController::class, 'testConnection'])->name('postgres.test-connection');
+ Route::get('/postgres/{connection}', [PostgresConnectionController::class, 'explore'])->name('postgres.explore');
Route::get('/postgres/connection-status/{id}', [PostgresConnectionController::class, 'connectionStatus'])->name('postgres.connection-status');
🤖 Prompt for AI Agents
In routes/web.php around lines 18 to 23, the route for '/postgres/{connection}'
is defined before the more specific routes '/postgres/test-connection' and
'/postgres/connection-status/{id}', causing the specific routes to be captured
by the generic '{connection}' route. To fix this, reorder the routes by placing
the more specific routes '/postgres/test-connection' and
'/postgres/connection-status/{id}' above the generic '/postgres/{connection}'
route to ensure correct route matching.

Comment on lines 49 to 50
$connections = DatabaseConnection::where('type', 'postgres')
->where('user_id', $request->user()->id)
->get();
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

Add workspace filtering to the index query.

The query retrieves all postgres connections without filtering by workspace. This could expose connections from other workspaces to unauthorized users.

Apply this diff to add workspace filtering:

 $connections = DatabaseConnection::where('type', 'postgres')
+    ->where('workspace_id', $request->user()->current_workspace_id)
     ->get();
📝 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
$connections = DatabaseConnection::where('type', 'postgres')
->where('user_id', $request->user()->id)
->get();
$connections = DatabaseConnection::where('type', 'postgres')
->where('workspace_id', $request->user()->current_workspace_id)
->get();
🤖 Prompt for AI Agents
In app/Http/Controllers/Postgres/PostgresConnectionController.php around lines
49 to 50, the query fetching postgres connections lacks filtering by workspace,
risking exposure of connections across workspaces. Modify the query to include a
where condition that filters connections by the current workspace ID, ensuring
only connections belonging to the authorized workspace are retrieved.

Comment on lines +62 to 81
public function connectionStatus(Request $request, $id)
{
$connection = DatabaseConnection::where('id', $id)->first();

return $connection;
});
try {
$credentials = [
'host' => $connection->host,
'port' => $connection->port,
'username' => $connection->username,
'password' => $connection->password,
'database' => $connection->database,
];

return Inertia::render('postgres/postgres', [
'connections' => $connectionsWithStatus,
]);
$isOnline = $this->connectionManager->testConnection($credentials);

return response()->json(['connection_status' => $isOnline, 'last_tested' => now(), 'connection_error' => null]);
} catch (\Exception $e) {
return response()->json(['connection_status' => false, 'last_tested' => now(), 'connection_error' => $e->getMessage()]);
}
}
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

Add authorization check before revealing connection status.

The method retrieves and tests a connection without verifying the user has access to it. This could allow unauthorized users to probe connection status.

Add authorization check after retrieving the connection:

 $connection = DatabaseConnection::where('id', $id)->first();
+
+if (!$connection || $connection->workspace_id !== $request->user()->current_workspace_id) {
+    return response()->json(['error' => 'Connection not found'], 404);
+}
📝 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
public function connectionStatus(Request $request, $id)
{
$connection = DatabaseConnection::where('id', $id)->first();
return $connection;
});
try {
$credentials = [
'host' => $connection->host,
'port' => $connection->port,
'username' => $connection->username,
'password' => $connection->password,
'database' => $connection->database,
];
return Inertia::render('postgres/postgres', [
'connections' => $connectionsWithStatus,
]);
$isOnline = $this->connectionManager->testConnection($credentials);
return response()->json(['connection_status' => $isOnline, 'last_tested' => now(), 'connection_error' => null]);
} catch (\Exception $e) {
return response()->json(['connection_status' => false, 'last_tested' => now(), 'connection_error' => $e->getMessage()]);
}
}
public function connectionStatus(Request $request, $id)
{
$connection = DatabaseConnection::where('id', $id)->first();
if (!$connection || $connection->workspace_id !== $request->user()->current_workspace_id) {
return response()->json(['error' => 'Connection not found'], 404);
}
try {
$credentials = [
'host' => $connection->host,
'port' => $connection->port,
'username' => $connection->username,
'password' => $connection->password,
'database' => $connection->database,
];
$isOnline = $this->connectionManager->testConnection($credentials);
return response()->json([
'connection_status' => $isOnline,
'last_tested' => now(),
'connection_error' => null,
]);
} catch (\Exception $e) {
return response()->json([
'connection_status' => false,
'last_tested' => now(),
'connection_error' => $e->getMessage(),
]);
}
}
🤖 Prompt for AI Agents
In app/Http/Controllers/Postgres/PostgresConnectionController.php around lines
62 to 81, the connectionStatus method lacks an authorization check before
returning connection status, which risks exposing sensitive information. After
retrieving the connection by ID, add an authorization check to verify the
current user has permission to access this connection, using Laravel's
authorization features like $this->authorize or a policy. If unauthorized,
return an appropriate error response to prevent unauthorized access.

- Updated workspace ID assignment in the HasWorkspaceScope trait to use null coalescing assignment.
- Enhanced error message formatting in PostgresQueryController for better readability.
- Refactored workspace data sharing in HandleInertiaRequests middleware to streamline user workspace information.
- Improved workspace membership verification in WorkspaceMiddleware for better clarity.
- Wrapped workspace and membership creation in a transaction in RegisteredEvent to ensure data integrity.
- Changed the query column type in the database_queries migration from string to longText for better handling of complex queries.
- Updated foreign key constraints in user sessions and workspace migrations for improved data integrity.
- Added an index on the 'created_at' column in the database_queries migration for improved query performance.
- Updated the workspace selector component to use 'switch_workspace' instead of 'switch_team' for better clarity in data handling.
@adiologydev adiologydev merged commit ec80db8 into main Jun 11, 2025
2 of 3 checks passed
@adiologydev adiologydev deleted the feat/tenants branch June 11, 2025 18:43
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.

1 participant