Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 33 additions & 12 deletions app/admin/dashboard/clients/new/page.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,44 @@
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from "@/components/ui/card";
import { ClientForm } from "@/components/admin/client-form";
import { OAuthEndpointsCard } from "@/components/admin/oauth-endpoints-card";
import { getOpenIDConfiguration } from "@/lib/oauth/discovery";

export const metadata = {
title: "New OAuth Client - Admin",
description: "Create a new OAuth client application",
};

export default function NewClientPage() {
const config = getOpenIDConfiguration();

Comment on lines +18 to +19
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

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

The getOpenIDConfiguration function is called without any error handling. If the OAUTH_ISSUER_URL environment variable is not set, this will result in an undefined issuer value that could cause runtime errors. While the page is a Server Component and should fail at build/startup time, consider adding validation to provide a clearer error message for misconfiguration.

Suggested change
const config = getOpenIDConfiguration();
let config;
try {
config = getOpenIDConfiguration();
} catch (error) {
const message =
error instanceof Error ? error.message : String(error);
throw new Error(
`Failed to load OpenID configuration. Ensure the OAUTH_ISSUER_URL environment variable is set correctly. Original error: ${message}`,
);
}
if (!config || !config.issuer) {
throw new Error(
"Invalid OpenID configuration: missing issuer. Please ensure the OAUTH_ISSUER_URL environment variable is set.",
);
}

Copilot uses AI. Check for mistakes.
const endpoints = {
issuer: config.issuer,
authorizationEndpoint: config.authorization_endpoint,
tokenEndpoint: config.token_endpoint,
discoveryUrl: `${config.issuer}/.well-known/openid-configuration`,
};

return (
<Card>
<CardHeader>
<CardTitle>Create OAuth Client</CardTitle>
<CardDescription>
Register a new application that can use OAuth to authenticate users
</CardDescription>
</CardHeader>
<CardContent>
<ClientForm />
</CardContent>
</Card>
<div className="space-y-6">
<OAuthEndpointsCard endpoints={endpoints} />

<Card>
<CardHeader>
<CardTitle>Create OAuth Client</CardTitle>
<CardDescription>
Register a new application that can use OAuth to authenticate users
</CardDescription>
</CardHeader>
<CardContent>
<ClientForm />
</CardContent>
</Card>
</div>
);
}
87 changes: 87 additions & 0 deletions components/admin/oauth-endpoints-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"use client";

import { useState } from "react";
import { Copy, Check } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from "@/components/ui/card";

interface OAuthEndpointsCardProps {
endpoints: {
issuer: string;
authorizationEndpoint: string;
tokenEndpoint: string;
discoveryUrl: string;
};
}

export function OAuthEndpointsCard({ endpoints }: OAuthEndpointsCardProps) {
const [copied, setCopied] = useState<string | null>(null);

const handleCopy = (text: string, key: string) => {
navigator.clipboard.writeText(text);
setCopied(key);
setTimeout(() => setCopied(null), 2000);
Comment on lines +28 to +31
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

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

The handleCopy function uses navigator.clipboard.writeText without error handling. This can fail in non-secure contexts (non-HTTPS) or when clipboard permissions are denied. Consider wrapping the clipboard operation in a try-catch block and providing user feedback if the operation fails.

Suggested change
const handleCopy = (text: string, key: string) => {
navigator.clipboard.writeText(text);
setCopied(key);
setTimeout(() => setCopied(null), 2000);
const handleCopy = async (text: string, key: string) => {
if (!navigator?.clipboard?.writeText) {
window.alert("Copy to clipboard is not supported in this browser.");
return;
}
try {
await navigator.clipboard.writeText(text);
setCopied(key);
setTimeout(() => setCopied(null), 2000);
} catch (error) {
console.error("Failed to copy to clipboard:", error);
window.alert("Failed to copy to clipboard. Please copy the text manually.");
}

Copilot uses AI. Check for mistakes.
};

const urlFields = [
{ key: "issuer", label: "Issuer URL", value: endpoints.issuer },
{
key: "authorization",
label: "Authorization Endpoint",
value: endpoints.authorizationEndpoint,
},
{ key: "token", label: "Token Endpoint", value: endpoints.tokenEndpoint },
{ key: "discovery", label: "Discovery URL", value: endpoints.discoveryUrl },
];

return (
<Card>
<CardHeader>
<CardTitle>OAuth Server Endpoints</CardTitle>
<CardDescription>
Use these URLs to configure your OAuth client application
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{urlFields.map(({ key, label, value }) => (
<div key={key} className="space-y-2">
<Label>{label}</Label>
<div className="flex gap-2">
<Input
data-testid={`oauth-endpoint-${key}`}
value={value}
readOnly
className="font-mono text-xs"
/>
<Button
type="button"
variant="outline"
size="icon"
data-testid={`copy-${key}`}
onClick={() => handleCopy(value, key)}
>
{copied === key ? (
<Check className="size-4" />
) : (
<Copy className="size-4" />
)}
</Button>
Comment on lines +64 to +76
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

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

The copy buttons lack accessible labels. Screen reader users won't know what these buttons do. Add an aria-label attribute to the Button components that describes what will be copied, for example: aria-label={Copy ${label}}.

Copilot uses AI. Check for mistakes.
</div>
</div>
))}
<p className="text-xs text-muted-foreground">
For automatic configuration, most OAuth libraries can use the
Discovery URL.
</p>
</CardContent>
</Card>
);
}
33 changes: 32 additions & 1 deletion e2e/admin/clients.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,42 @@ async function adminLogin(page: import("@playwright/test").Page) {
await expect(page).toHaveURL("/admin/dashboard");
}

test.describe("Client Form Scope Selection", () => {
test.describe("OAuth Endpoints Display", () => {
test.beforeEach(async ({ page }) => {
await adminLogin(page);
});

test("should display OAuth endpoints on new client page", async ({ page }) => {
await page.goto("/admin/dashboard/clients/new");

// Verify OAuth endpoints card is visible
await expect(page.getByText("OAuth Server Endpoints")).toBeVisible();

// Verify key endpoints are displayed
await expect(page.getByTestId("oauth-endpoint-issuer")).toBeVisible();
await expect(page.getByTestId("oauth-endpoint-authorization")).toBeVisible();
await expect(page.getByTestId("oauth-endpoint-token")).toBeVisible();
await expect(page.getByTestId("oauth-endpoint-discovery")).toBeVisible();

// Verify endpoints have values
const issuerValue = await page.getByTestId("oauth-endpoint-issuer").inputValue();
expect(issuerValue).toBeTruthy();

const authValue = await page.getByTestId("oauth-endpoint-authorization").inputValue();
expect(authValue).toContain("/api/oauth/authorize");

const tokenValue = await page.getByTestId("oauth-endpoint-token").inputValue();
expect(tokenValue).toContain("/api/oauth/token");

const discoveryValue = await page.getByTestId("oauth-endpoint-discovery").inputValue();
expect(discoveryValue).toContain("/.well-known/openid-configuration");
});
Comment on lines +17 to +41
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

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

The E2E test verifies that endpoints are displayed but doesn't test the copy functionality. Consider adding a test case that clicks the copy buttons and verifies the copied state (Check icon) appears, to ensure the interactive features work correctly.

Copilot uses AI. Check for mistakes.
});

test.describe("Client Form Scope Selection", () => {
test.beforeEach(async ({ page }) => {
await adminLogin(page);
});

test("should create client with all scopes selected", async ({ page }) => {
await page.goto("/admin/dashboard/clients/new");
Expand Down
4 changes: 2 additions & 2 deletions e2e/oauth/consent.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ test.describe("OAuth Consent Flow", () => {
await page.getByRole("button", { name: "Create Application" }).click();
await expect(page.getByText("Client Created Successfully")).toBeVisible();

clientId = await page.locator('input[readonly]').first().inputValue();
clientSecret = await page.locator('input[readonly]').nth(1).inputValue();
clientId = await page.getByTestId("client-id-display").inputValue();
clientSecret = await page.getByTestId("client-secret-display").inputValue();

await page.close();
await context.close();
Expand Down