Skip to content

Fix/workflow database setup#36

Closed
OverDsh wants to merge 40 commits intodevfrom
fix/workflow-database-setup
Closed

Fix/workflow database setup#36
OverDsh wants to merge 40 commits intodevfrom
fix/workflow-database-setup

Conversation

@OverDsh
Copy link
Copy Markdown
Contributor

@OverDsh OverDsh commented Mar 26, 2026

No description provided.

OverDsh and others added 10 commits March 18, 2026 18:35
- 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>
@OverDsh OverDsh self-assigned this Mar 26, 2026
Copilot AI review requested due to automatic review settings March 26, 2026 19:48
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 26, 2026

🚀 Preview Deployment:

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

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 migrate for 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.

Comment on lines +28 to +35
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>;
}
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +79
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>
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +48 to +51
popoverDescription:
errorMessage ??
"A critical connection issue occured. Please check your key in the Roblox Creator Dashboard",
},
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

User-facing text contains a spelling error: “occured” → “occurred”.

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +26
} 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";
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +295 to +316
<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 },
);
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +40
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" }),
});
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +37
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"

Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

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).

Copilot uses AI. Check for mistakes.
@@ -1,23 +1,38 @@
"use client";
import { useEffect, useState } from "react";
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
import { useEffect, useState } from "react";

Copilot uses AI. Check for mistakes.
Comment on lines 3 to 16
@@ -21,16 +16,11 @@ export function useDatabases() {

Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +23
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));
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
@OverDsh OverDsh closed this Mar 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants