Files
IdP/apps/frontend/components/id/oauth-client-detail-dialog.tsx
2026-06-25 15:14:50 +03:00

146 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { Copy, ExternalLink, KeyRound, RefreshCw, Trash2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { OAuthClient } from '@/lib/api';
import { OAuthEndpoints, buildAuthorizeUrl } from '@/lib/oauth-url';
function CopyRow({ label, value, onCopy }: { label: string; value: string; onCopy: (value: string, label: string) => void }) {
return (
<div className="rounded-2xl bg-[#f4f5f8] p-4 text-sm">
<div className="mb-2 flex items-center justify-between gap-2">
<span className="font-medium">{label}</span>
<Button variant="ghost" size="icon" aria-label={`Скопировать ${label}`} onClick={() => onCopy(value, label)}>
<Copy className="h-4 w-4" />
</Button>
</div>
<code className="block break-all text-xs">{value}</code>
</div>
);
}
export function OAuthClientDetailDialog({
client,
endpoints,
open,
onOpenChange,
onCopy,
onRotateSecret,
onToggleActive,
onDelete
}: {
client: OAuthClient | null;
endpoints: OAuthEndpoints;
open: boolean;
onOpenChange: (open: boolean) => void;
onCopy: (value: string, label: string) => void;
onRotateSecret: (clientId: string) => void;
onToggleActive: (client: OAuthClient) => void;
onDelete: (client: OAuthClient) => void;
}) {
if (!client) return null;
const primaryRedirect = client.redirectUris[0] ?? 'https://app.example.com/oauth/callback';
const scope = client.scopes.join(' ');
const sampleAuthorizeUrl = buildAuthorizeUrl(endpoints.issuer, {
clientId: client.clientId,
redirectUri: primaryRedirect,
scope
});
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>{client.name}</DialogTitle>
</DialogHeader>
<div className="space-y-4 text-sm">
<div className="flex flex-wrap gap-2">
<span className="rounded-full bg-[#eef4ff] px-3 py-1 text-xs font-medium text-[#1d4ed8]">
{client.type === 'PUBLIC' ? 'Public' : 'Confidential'}
</span>
<span className={`rounded-full px-3 py-1 text-xs font-medium ${client.isActive ? 'bg-emerald-50 text-emerald-700' : 'bg-zinc-100 text-zinc-600'}`}>
{client.isActive ? 'Активно' : 'Отключено'}
</span>
{client.createdByDisplayName ? (
<span className="rounded-full bg-zinc-100 px-3 py-1 text-xs text-zinc-600">Создал: {client.createdByDisplayName}</span>
) : null}
</div>
<section className="space-y-3">
<h3 className="font-medium">Данные клиента</h3>
<CopyRow label="Client ID" value={client.clientId} onCopy={onCopy} />
{client.type !== 'PUBLIC' ? (
<p className="text-xs text-[#667085]">
Client Secret выдаётся один раз при создании или после «Новый secret». Храните только на backend.
</p>
) : null}
<CopyRow label="Redirect URI" value={client.redirectUris.join('\n')} onCopy={onCopy} />
<CopyRow label="Scopes" value={scope} onCopy={onCopy} />
</section>
<section className="space-y-3">
<h3 className="font-medium">Подключение OAuth 2.0 / OpenID Connect</h3>
<p className="text-xs text-[#667085]">
Issuer и endpoints берутся из настройки <strong>PUBLIC_API_URL</strong> в админке. В стороннем сервисе укажите issuer = базовый URL API, не authorization endpoint.
</p>
<CopyRow label="Issuer (PUBLIC_API_URL)" value={endpoints.issuer} onCopy={onCopy} />
<CopyRow label="Authorization endpoint" value={endpoints.authorizationEndpoint} onCopy={onCopy} />
<CopyRow label="Token endpoint" value={endpoints.tokenEndpoint} onCopy={onCopy} />
<CopyRow label="UserInfo endpoint" value={endpoints.userInfoEndpoint} onCopy={onCopy} />
<CopyRow label="OIDC Discovery" value={endpoints.openIdConfigurationUrl} onCopy={onCopy} />
<CopyRow label="Пример authorize URL" value={sampleAuthorizeUrl} onCopy={onCopy} />
</section>
<section className="rounded-2xl border border-[#eceef4] p-4">
<div className="mb-2 flex items-center gap-2 font-medium">
<KeyRound className="h-4 w-4" />
Быстрый старт
</div>
<ol className="list-decimal space-y-1 pl-5 text-xs text-[#667085]">
<li>Авторизуйте пользователя в IdP и получите userId.</li>
<li>Перенаправьте на authorization endpoint с clientId, redirectUri, scope, userId.</li>
<li>На backend обменяйте code через POST /oauth/token с client_secret.</li>
<li>Запросите профиль через GET /oauth/userinfo с access token.</li>
</ol>
<a
href={endpoints.openIdConfigurationUrl}
target="_blank"
rel="noreferrer"
className="mt-2 inline-flex items-center gap-1 text-xs text-[#1d4ed8] hover:underline"
>
Открыть OpenID Configuration
<ExternalLink className="h-3.5 w-3.5" />
</a>
</section>
{client.canManage ? (
<div className="flex flex-wrap gap-2 border-t border-[#eceef4] pt-4">
{client.type !== 'PUBLIC' ? (
<Button variant="secondary" size="sm" onClick={() => onRotateSecret(client.clientId)}>
<RefreshCw className="h-4 w-4" />
Новый secret
</Button>
) : null}
<Button variant="secondary" size="sm" onClick={() => onToggleActive(client)}>
{client.isActive ? 'Отключить' : 'Включить'}
</Button>
<Button
variant="secondary"
size="sm"
className="text-red-600 hover:text-red-700"
onClick={() => onDelete(client)}
>
<Trash2 className="h-4 w-4" />
Удалить
</Button>
</div>
) : null}
</div>
</DialogContent>
</Dialog>
);
}