Conversation
in RobloxCredentialService
roblox-credentials-types.ts
…ExternalDatabaseService
- 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>
🚀 Preview Deployment:
|
There was a problem hiding this comment.
Pull request overview
This PR expands the “connections” feature set (S3 databases + Roblox API keys) with status/refresh/rotate capabilities, introduces shared client fetch + server logging utilities, and hardens CI/CD workflows to ensure Drizzle migrations are present and applied correctly.
Changes:
- Add Roblox credentials storage/service/API + UI (list/link/rename/rotate/refresh) and consolidate the dashboard under a new Connections page.
- Extend external database connections with status tracking, credential rotation, refresh endpoints, and safer endpoint URL validation.
- Update GitHub Actions workflows to gate deploys on migration generation checks and use
drizzle-kit migratefor preview/dev DB setup.
Reviewed changes
Copilot reviewed 51 out of 54 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| src/services/TeamService.ts | Switch error imports to the consolidated api utils module. |
| src/services/TeamService.test.ts | Align tests with new error import location. |
| src/services/RobloxCredentialsService.ts | New service for storing/using Roblox API keys with introspection, rotation, refresh, and status tracking. |
| src/services/ExternalDatabaseService.ts | Adds DB status tracking, refresh/rotate flows, introspection, decryption, and structured logging. |
| src/lib/utils/url-utils.ts | New helper to validate that S3 endpoints are public HTTPS (SSRF mitigation attempt). |
| src/lib/utils/team-utils.ts | Adds permissions for new database and Roblox credential actions. |
| src/lib/utils/logger.ts | New lightweight structured logger utility. |
| src/lib/utils/api-utils.ts | Adds logger integration and a shared fetcher for controllers; centralizes ApiError helpers. |
| src/lib/types/roblox-credentials-types.ts | New types + zod schemas for Roblox credentials and UI status messages. |
| src/lib/types/database-types.ts | Adds status fields/select shape, and schema for rotating DB credentials. |
| src/lib/config.ts | Introduces free-tier limits config constants. |
| src/hooks/useTeam.ts | Refactors to use TeamController object + adds query staleTime. |
| src/hooks/useRobloxCredential.ts | New hooks for Roblox credential queries/mutations. |
| src/hooks/useMember.ts | Refactors to use TeamController object + adds query staleTime. |
| src/hooks/useDatabase.ts | Refactors to controller object, adds refresh/rotate mutations + query staleTime. |
| src/db/schema/roblox_credentials.ts | New table for encrypted Roblox keys + status/usage timestamps. |
| src/db/schema/index.ts | Exports the new roblox_credentials schema. |
| src/db/schema/database.ts | Adds status/error + lastUsed/lastRefreshedAt fields. |
| src/controllers/TeamController.ts | Refactors free functions into a controller object using shared fetcher. |
| src/controllers/RobloxCredentialController.ts | New controller for Roblox credentials API calls. |
| src/controllers/ExternalDatabaseController.ts | Refactors into controller object and adds rotate/refresh endpoints. |
| src/components/ui/popover.tsx | New popover UI wrapper. |
| src/components/ui/badge.tsx | New badge UI component using cva variants. |
| src/components/StatusBadge.tsx | New popover-enabled status badge component for “healthy/warning/error”. |
| src/components/LocalTime.tsx | Adds relative-time rendering mode. |
| src/components/FormDialog.tsx | Adds internal open state support and closes dialog after submit. |
| src/app/dashboard/components/TeamColumn.tsx | Uses TeamController.removeMember, adds copy/leave menu items & separator. |
| src/app/dashboard/[teamSlug]/databases/page.tsx | Removes the dedicated Databases page (replaced by Connections). |
| src/app/dashboard/[teamSlug]/connections/page.tsx | New consolidated Connections page showing DBs and Roblox credentials. |
| src/app/dashboard/[teamSlug]/connections/RobloxCredentialColumn.tsx | New table columns + dialogs/actions for Roblox credentials. |
| src/app/dashboard/[teamSlug]/connections/LinkRoCredDialog.tsx | New dialog for linking Roblox API keys. |
| src/app/dashboard/[teamSlug]/connections/LinkDialog/S3DatabaseDialog.tsx | Cleanup: rely on hook mutations rather than controller direct calls. |
| src/app/dashboard/[teamSlug]/connections/LinkDialog/LinkDatabaseDialog.tsx | New wrapper dialog to select DB type and render the right linking dialog. |
| src/app/dashboard/[teamSlug]/connections/LinkDialog/DatabaseTypeSelectorDialog.tsx | New dialog for choosing database type (currently S3). |
| src/app/dashboard/[teamSlug]/connections/DatabaseColumn.tsx | Adds status/lastUsed/created columns + refresh/rotate actions. |
| src/app/dashboard/[teamSlug]/components/TeamSidebar.tsx | Renames nav item to Connections and gates it by either DB or Roblox perms. |
| src/app/api/teams/route.ts | Switches ErrorToNextResponse import to api-utils. |
| src/app/api/teams/resolve-slug/[slug]/route.ts | Switches ErrorToNextResponse import; removes unused req param. |
| src/app/api/teams/[teamId]/transfer-ownership/route.ts | Switches ErrorToNextResponse import to api-utils. |
| src/app/api/teams/[teamId]/route.ts | Switches ErrorToNextResponse import; removes unused req param. |
| src/app/api/teams/[teamId]/roblox-credentials/route.ts | New endpoints for listing/linking Roblox credentials. |
| src/app/api/teams/[teamId]/roblox-credentials/[credentialId]/route.ts | New endpoints for delete/rename/rotate Roblox credentials. |
| src/app/api/teams/[teamId]/roblox-credentials/[credentialId]/refresh/route.ts | New endpoint for refreshing Roblox credential status. |
| src/app/api/teams/[teamId]/members/route.ts | Switches ErrorToNextResponse import; removes unused req param. |
| src/app/api/teams/[teamId]/members/[memberId]/route.ts | Switches ErrorToNextResponse import; removes unused req param. |
| src/app/api/teams/[teamId]/databases/route.ts | Adds safe endpoint validation and switches ErrorToNextResponse import. |
| src/app/api/teams/[teamId]/databases/[databaseId]/route.ts | Returns deleted DB object; adds rotate credentials POST handler. |
| src/app/api/teams/[teamId]/databases/[databaseId]/refresh/route.ts | New endpoint for refreshing database status. |
| package.json | Adds radix-ui dependency. |
| package-lock.json | Locks radix-ui and transitive Radix dependencies. |
| .github/workflows/production_deploy.yml | Adds migration check job + deployment concurrency and safer token quoting. |
| .github/workflows/pr_preview.yml | Adds migration check job, improves concurrency key, uses drizzle-kit migrate, safer Turso branch creation, safer token quoting, and guards PR comment step. |
| .github/workflows/dev_deploy.yml | Uses drizzle-kit migrate and disables cancel-in-progress. |
| .github/workflows/database_cleanup.yml | Adds paths-ignore and moves TURSO token to job env. |
Comments suppressed due to low confidence (2)
src/app/dashboard/[teamSlug]/connections/DatabaseColumn.tsx:136
- Spelling: “occured” → “occurred” in this toast message.
src/app/dashboard/[teamSlug]/connections/DatabaseColumn.tsx:271 - Spelling: “occured” → “occurred” in this toast message.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (mode === "relative") { | ||
| const diffInSec = Math.floor((time.getTime() - Date.now()) / 1000); | ||
| for (const { unit, amount } of units) { | ||
| if (Math.abs(diffInSec) >= amount) { | ||
| const timeString = relativeFormatter.format(Math.floor(diffInSec / amount), unit); | ||
| return <span>{timeString.charAt(0).toUpperCase() + timeString.slice(1)}</span>; | ||
| } | ||
| } |
There was a problem hiding this comment.
Rendering relative time using Date.now() directly during render can cause hydration mismatches (server-rendered HTML vs client hydrate) and will also “freeze” until a rerender. Consider restoring a mount guard and/or updating the value on an interval, or render absolute time on the server and switch to relative after mount.
| const BadgeElement = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( | ||
| ({ className, onClick, ...props }, ref) => { | ||
| return ( | ||
| <div | ||
| ref={ref} | ||
| {...props} | ||
| className={className} | ||
| onClick={(e) => { | ||
| onClick?.(e); | ||
| }} | ||
| > | ||
| <Badge className={config.className}> | ||
| <span>{config.label}</span> | ||
| {config.Icon} | ||
| </Badge> | ||
| </div> | ||
| ); | ||
| }, | ||
| ); | ||
|
|
||
| return ( | ||
| <Popover> | ||
| <PopoverTrigger asChild> | ||
| <BadgeElement /> | ||
| </PopoverTrigger> |
There was a problem hiding this comment.
The popover trigger is rendered as a <div> via BadgeElement, which is not keyboard-focusable by default. For accessibility, use a <button type="button"> (or add appropriate role, tabIndex, and keyboard handlers) so the popover can be opened via keyboard.
| popoverDescription: | ||
| errorMessage ?? | ||
| "A critical connection issue occured. Please check your key in the Roblox Creator Dashboard", | ||
| }, |
There was a problem hiding this comment.
User-facing text contains a spelling error: “occured” → “occurred”.
| } 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"; | ||
| import { Copy, MoreHorizontal, Pencil, RefreshCw, RotateCcwKey, Trash } from "lucide-react"; |
There was a problem hiding this comment.
This column imports DropdownMenu directly from @radix-ui/react-dropdown-menu while using the project’s wrapper components for trigger/content/items. For consistency (and to ensure wrapper data attributes/styling are applied), import and use DropdownMenu from @/src/components/ui/dropdown-menu like other columns do.
| <CallbackDialog | ||
| title="Delete Roblox Credential" | ||
| description="Are you sure you wanna unlink this roblox credential from your team?" | ||
| callback={async () => { | ||
| const id = toast.loading("Deleting roblox credential..."); | ||
| try { | ||
| await deleteRobloxCredential.mutateAsync({ | ||
| teamId: team.id, | ||
| credId: cred.id, | ||
| }); | ||
| toast.success("Successfully deleted roblox credential!", { | ||
| id, | ||
| }); | ||
| } catch (error) { | ||
| if (error instanceof Error) { | ||
| toast.error(error.message, { id }); | ||
| } else { | ||
| toast.error( | ||
| "An unknown error occured while deleting your roblox credential. Please try again later.", | ||
| { id }, | ||
| ); | ||
| } |
There was a problem hiding this comment.
User-facing copy uses informal/incorrect spelling/casing (e.g., “wanna”, “roblox”, “occured”). Consider updating to “want to”, “Roblox”, and “occurred” for a professional UI and correct spelling.
| export const RobloxCredentialInfoSchema = z.object({ | ||
| name: z | ||
| .string() | ||
| .min(3, { error: "Name must be at least 3 characters" }) | ||
| .max(32, { error: "Name must be at most 32 characters" }), | ||
| key: z | ||
| .string() | ||
| .min(1, { error: "Key must contain at least 1 character" }) | ||
| .max(2048, { error: "Key must be at most 2048 characters" }), | ||
| }); |
There was a problem hiding this comment.
Zod option objects use message, not error. Using { error: "…" } won’t set the validation message and will fall back to defaults. Replace these with { message: "…" } so clients get the intended error messages.
| import * as React from "react" | ||
| import { cva, type VariantProps } from "class-variance-authority" | ||
| import { Slot } from "radix-ui" | ||
|
|
||
| import { cn } from "@/src/lib/utils" | ||
|
|
||
| const badgeVariants = cva( | ||
| "inline-flex items-center justify-center rounded-full border border-transparent px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", | ||
| { | ||
| variants: { | ||
| variant: { | ||
| default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90", | ||
| secondary: | ||
| "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", | ||
| destructive: | ||
| "bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", | ||
| outline: | ||
| "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", | ||
| ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground", | ||
| link: "text-primary underline-offset-4 [a&]:hover:underline", | ||
| }, | ||
| }, | ||
| defaultVariants: { | ||
| variant: "default", | ||
| }, | ||
| } | ||
| ) | ||
|
|
||
| function Badge({ | ||
| className, | ||
| variant = "default", | ||
| asChild = false, | ||
| ...props | ||
| }: React.ComponentProps<"span"> & | ||
| VariantProps<typeof badgeVariants> & { asChild?: boolean }) { | ||
| const Comp = asChild ? Slot.Root : "span" | ||
|
|
There was a problem hiding this comment.
Slot from Radix is a component (see other UI components using @radix-ui/react-slot), so Slot.Root is not valid and will fail type-check/build. Import Slot from @radix-ui/react-slot and use asChild ? Slot : "span" (not Slot.Root).
| @@ -1,23 +1,38 @@ | |||
| "use client"; | |||
| import { useEffect, useState } from "react"; | |||
There was a problem hiding this comment.
useEffect/useState are imported but no longer used. With the repo’s Next/TypeScript ESLint config, this will fail linting (no-unused-vars). Remove the unused imports.
| import { useEffect, useState } from "react"; |
| @@ -21,16 +16,11 @@ export function useDatabases() { | |||
|
|
|||
There was a problem hiding this comment.
useEffect, hasPermission, and useRouter/router are now unused. This will fail the repo’s ESLint rules; remove the unused imports and the unused router variable.
| const PRIVATE_IP_RE = [ | ||
| /^localhost$/i, | ||
| /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, | ||
| /^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, | ||
| /^172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}$/, | ||
| /^192\.168\.\d{1,3}\.\d{1,3}$/, | ||
| /^169\.254\.\d{1,3}\.\d{1,3}$/, | ||
| /^0\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, | ||
| ]; | ||
|
|
||
| export function isSafeEndpointUrl(raw: string): boolean { | ||
| let parsed: URL; | ||
| try { | ||
| parsed = new URL(raw); | ||
| } catch { | ||
| return false; | ||
| } | ||
|
|
||
| if (parsed.protocol !== "https:") return false; | ||
|
|
||
| const host = parsed.hostname; | ||
| return !PRIVATE_IP_RE.some((re) => re.test(host)); | ||
| } |
There was a problem hiding this comment.
isSafeEndpointUrl blocks some private IPv4 hosts but does not handle IPv6 loopback/private ranges (e.g. ::1) or hostnames that resolve to private IPs (DNS rebinding). If this is meant as SSRF protection, consider expanding checks (IPv6 + optional DNS resolution) and/or renaming to avoid implying stronger guarantees than it provides.
No description provided.