update oauth
This commit is contained in:
134
apps/frontend/components/id/oauth-client-detail-dialog.tsx
Normal file
134
apps/frontend/components/id/oauth-client-detail-dialog.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
'use client';
|
||||
|
||||
import { Copy, ExternalLink, KeyRound, RefreshCw } 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
|
||||
}: {
|
||||
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;
|
||||
}) {
|
||||
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>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user