Skip to content

[FEAT] Connections Page#38

Merged
OverDsh merged 38 commits intodevfrom
feat/connections_page
Mar 27, 2026
Merged

[FEAT] Connections Page#38
OverDsh merged 38 commits intodevfrom
feat/connections_page

Conversation

@OverDsh
Copy link
Copy Markdown
Contributor

@OverDsh OverDsh commented Mar 27, 2026

  • Renamed Database tab in Teams page to Connections to be used to manage different off-site connections
  • Implemented Roblox Credentials linking (under the connections page)
  • Added introspection on both database and roblox credentials to have visibility on connections status
  • Ran security audit using Claude, fixed 2x IDOR (when performing an action like delete or rotate, didn't check if the team owned the target ressource)
  • Various QOL improvements (icons in dropdowns, consistent time formatting)

@OverDsh OverDsh self-assigned this Mar 27, 2026
Copilot AI review requested due to automatic review settings March 27, 2026 16:59
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 27, 2026

🚀 Preview Deployment:

OverDsh and others added 16 commits March 27, 2026 18:02
- createLogger(service) factory with info/warn/error/debug methods; structured output with timestamp, extracts error.name/message without leaking stacks or raw objects
- isSafeEndpointUrl() blocks non-HTTPS and all private/loopback/link-local IP ranges (RFC-1918, 127.x, 169.254.x, 0.x)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_useCredential and _useDatabase previously fetched by ID only, allowing a credential/database belonging to team A to be accessed via a team B scoped endpoint. WHERE clauses now include an AND on teamId so a cross-team lookup returns nothing and hits the existing AccessDenied guard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- POST /databases endpoint now rejects non-HTTPS and private/internal endpoint URLs via isSafeEndpointUrl refine
- All console.error calls in ExternalDatabaseService and api-utils replaced with createLogger structured calls (teamId/resourceId/operation context, no sensitive values)
- Silent empty catch on _applyIntrospectResults in RefreshRobloxCredential now logs the error instead of swallowing it

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new Connections area to manage external integrations (S3-compatible databases + Roblox API credentials), including connection “health” introspection/status and some UI/QOL improvements, while also refactoring client controllers around a shared fetcher.

Changes:

  • Introduces Roblox credentials linking/listing/rename/rotate/refresh (DB schema, service layer, API routes, hooks, UI tables).
  • Adds database introspection/status tracking plus refresh/credential rotation flows (schema + service/API/UI updates).
  • Refactors client-side controllers to a fetcher helper and renames the Teams “Databases” navigation to “Connections”.

Reviewed changes

Copilot reviewed 50 out of 53 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
src/services/TeamService.ts Switches error imports to api-utils.
src/services/TeamService.test.ts Updates error import path for tests.
src/services/RobloxCredentialsService.ts New service for Roblox credential CRUD + introspection.
src/services/ExternalDatabaseService.ts Adds DB status/introspection + refresh/rotate support and logging.
src/lib/utils/url-utils.ts Adds endpoint URL safety validator for DB endpoint inputs.
src/lib/utils/team-utils.ts Adds new permissions for refresh/rotate + Roblox credentials actions.
src/lib/utils/logger.ts Adds lightweight structured logger used across services/utils.
src/lib/utils/api-utils.ts Adds fetcher helper and switches unhandled error logging to new logger.
src/lib/types/roblox-credentials-types.ts Adds shared types + zod schemas for Roblox credential flows.
src/lib/types/database-types.ts Adds DB status fields/messages + rotate schema.
src/lib/config.ts Adds TEAM/USER limits constants.
src/hooks/useTeam.ts Refactors to TeamController object + adds query stale times.
src/hooks/useMember.ts Refactors member listing to TeamController + stale time.
src/hooks/useDatabase.ts Refactors to ExternalDatabaseController + adds refresh/rotate mutations.
src/hooks/useRobloxCredential.ts New hooks for Roblox credentials queries/mutations.
src/db/schema/roblox_credentials.ts New table schema for storing encrypted Roblox API keys + metadata.
src/db/schema/index.ts Exports new roblox_credentials schema.
src/db/schema/database.ts Adds status/error + timestamps (lastUsed/lastRefreshedAt).
src/controllers/TeamController.ts Refactors to a single TeamController object using fetcher.
src/controllers/RobloxCredentialController.ts New controller for Roblox credentials API calls.
src/controllers/ExternalDatabaseController.ts Refactors to controller object + adds refresh/rotate calls.
src/components/ui/popover.tsx Adds popover wrapper component.
src/components/ui/badge.tsx Adds badge component for status display.
src/components/StatusBadge.tsx Adds status badge UI with popover details.
src/components/LocalTime.tsx Updates time formatting (relative/absolute modes).
src/components/FormDialog.tsx Adds internal open-state support and auto-close/reset after submit.
src/app/dashboard/components/TeamColumn.tsx Adds dropdown icons/UX tweaks and uses TeamController.removeMember.
src/app/dashboard/[teamSlug]/databases/page.tsx Removes old Databases page in favor of Connections.
src/app/dashboard/[teamSlug]/connections/page.tsx Adds Connections page combining DB + Roblox credentials tables.
src/app/dashboard/[teamSlug]/connections/* Adds DB/Roblox column components + link dialogs.
src/app/dashboard/[teamSlug]/components/TeamSidebar.tsx Renames nav item to Connections + updates permission gating.
src/app/api/teams/** Updates routes to use api-utils errors; adds new DB refresh/rotate + Roblox credentials routes.
package.json Adds radix-ui dependency.
package-lock.json Locks new radix-ui dependency tree.
Comments suppressed due to low confidence (1)

src/lib/utils/api-utils.ts:75

  • fetcher assumes every response has a JSON body and that error bodies match {code,message}. If an endpoint returns 204/empty body or ErrorToNextResponse returns {error: ...} for unknown errors, response.json()/ResponseToError will throw or produce an unhelpful Error. Consider guarding JSON parsing (e.g., handle empty bodies) and normalizing server error responses to a single shape before calling ResponseToError.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

"next-themes": "^0.4.6",
"nodemailer": "^7.0.10",
"qrcode": "^1.5.4",
"radix-ui": "^1.4.3",
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

radix-ui is added as a dependency, but the codebase already depends on individual @radix-ui/react-* packages. Pulling in the umbrella package can duplicate dependencies and increase bundle size; consider removing radix-ui and adding only the specific missing primitive packages (e.g. @radix-ui/react-popover) to match existing usage.

Suggested change
"radix-ui": "^1.4.3",

Copilot uses AI. Check for mistakes.
Comment on lines +49 to +65
callback={({ name }) => {
const id = toast.loading("Renaming Roblox credential...");
renameRobloxCredential
.mutateAsync({ teamId: team!.id, credId, newName: name })
.then(() => {
toast.success("Successfully renamed Roblox credential!", { id });
})
.catch((error) => {
if (error instanceof Error) {
toast.error(error.message, { id });
} else {
toast.error(
"An unknown error happened while renaming Roblox credential. Please try again later.",
{ id },
);
}
});
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

The callback passed to FormDialog here starts an async mutation via .mutateAsync(...).then().catch() but does not return/await that promise. With the updated FormDialog implementation awaiting the callback, this means the dialog may close/reset immediately while the mutation is still in flight. Make the callback async and await/return the mutation promise so FormDialog can reliably manage loading/close behavior.

Suggested change
callback={({ name }) => {
const id = toast.loading("Renaming Roblox credential...");
renameRobloxCredential
.mutateAsync({ teamId: team!.id, credId, newName: name })
.then(() => {
toast.success("Successfully renamed Roblox credential!", { id });
})
.catch((error) => {
if (error instanceof Error) {
toast.error(error.message, { id });
} else {
toast.error(
"An unknown error happened while renaming Roblox credential. Please try again later.",
{ id },
);
}
});
callback={async ({ name }) => {
const id = toast.loading("Renaming Roblox credential...");
try {
await renameRobloxCredential.mutateAsync({ teamId: team!.id, credId, newName: name });
toast.success("Successfully renamed Roblox credential!", { id });
} catch (error) {
if (error instanceof Error) {
toast.error(error.message, { id });
} else {
toast.error(
"An unknown error happened while renaming Roblox credential. Please try again later.",
{ id },
);
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +25
import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/src/components/ui/dropdown-menu";
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/src/components/ui/form";
import { Input } from "@/src/components/ui/input";
import { Skeleton } from "@/src/components/ui/skeleton";
import { useRobloxCredentialMutations } from "@/src/hooks/useRobloxCredential";
import { useTeam } from "@/src/hooks/useTeam";
import {
RobloxCredential,
RobloxCredentialRenameSchema,
RobloxCredentialRotateSchema,
} from "@/src/lib/types/roblox-credentials-types";
import { hasPermission } from "@/src/lib/utils/team-utils";
import { zodResolver } from "@hookform/resolvers/zod";
import { DropdownMenu } from "@radix-ui/react-dropdown-menu";
import { ColumnDef } from "@tanstack/react-table";
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

This file mixes the project’s UI dropdown menu wrappers with a direct import from @radix-ui/react-dropdown-menu (DropdownMenu). For consistency (and to ensure styling/props match), import DropdownMenu from @/src/components/ui/dropdown-menu alongside the other dropdown menu components instead of using the Radix primitive directly.

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +66
callback={({ name, key }) => {
const id = toast.loading("Linking Roblox API key to your team...");
linkRobloxCredential
.mutateAsync({
teamId: team!.id,
data: { name, key },
})
.then(() => {
toast.success("Successfully linked Roblox API key to your team!", {
id,
});
setIsOpen(false);
})
.catch((error) => {
if (error instanceof Error) {
toast.error(error.message, { id });
} else {
toast.error(
"An unknown error happened while linking Roblox API key to your team. Please try again later.",
{ id },
);
}
});
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

Similarly, this FormDialog callback triggers mutateAsync(...).then().catch() without returning the promise. With FormDialog now awaiting callback, the dialog can close/reset immediately and isLoading won’t reflect the actual request lifecycle. Consider making the callback async and await/return the mutation promise (and remove the redundant setIsOpen(false) since FormDialog can handle closing on success).

Suggested change
callback={({ name, key }) => {
const id = toast.loading("Linking Roblox API key to your team...");
linkRobloxCredential
.mutateAsync({
teamId: team!.id,
data: { name, key },
})
.then(() => {
toast.success("Successfully linked Roblox API key to your team!", {
id,
});
setIsOpen(false);
})
.catch((error) => {
if (error instanceof Error) {
toast.error(error.message, { id });
} else {
toast.error(
"An unknown error happened while linking Roblox API key to your team. Please try again later.",
{ id },
);
}
});
callback={async ({ name, key }) => {
const id = toast.loading("Linking Roblox API key to your team...");
try {
await linkRobloxCredential.mutateAsync({
teamId: team!.id,
data: { name, key },
});
toast.success("Successfully linked Roblox API key to your team!", {
id,
});
} catch (error) {
if (error instanceof Error) {
toast.error(error.message, { id });
} else {
toast.error(
"An unknown error happened while linking Roblox API key to your team. Please try again later.",
{ id },
);
}
throw error;
}

Copilot uses AI. Check for mistakes.
Comment on lines +311 to +322
return await db.transaction(async (tx) => {
await tx
.update(roblox_credentials)
.set({
keyCiphertext: newKeyEncrypted.encryptedData,
keyIv: newKeyEncrypted.initializationVector,
keyTag: newKeyEncrypted.authTag,
})
.where(eq(roblox_credentials.id, credId));

return this._applyIntrospectResults(credId, keyInfo, tx);
});
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

RotateRobloxCredential updates the credential row using only id, without verifying that the credential belongs to the provided teamId. This reintroduces an IDOR risk (a user with admin on one team could rotate another team’s credential if they know the credId). Use the existing _useCredential helper or add teamId to the update WHERE clause and ensure you throw AccessDenied when no row is updated/returned.

Copilot uses AI. Check for mistakes.
Comment on lines 68 to 71
await callback(data);
onOpenChange(false);
form.reset();
setIsLoading(false);
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

handleSubmit sets isLoading to true, awaits callback, and then unconditionally closes/resets the dialog. If callback throws/rejects, isLoading will never be reset (no try/finally) and the dialog state can get stuck. Consider wrapping in try/finally and only closing/resetting on success (or allow callers to control close behavior).

Suggested change
await callback(data);
onOpenChange(false);
form.reset();
setIsLoading(false);
try {
await callback(data);
onOpenChange(false);
form.reset();
} finally {
setIsLoading(false);
}

Copilot uses AI. Check for mistakes.
@OverDsh OverDsh merged commit 4282df4 into dev Mar 27, 2026
2 checks passed
@OverDsh OverDsh deleted the feat/connections_page branch March 27, 2026 17:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants