change public app name for oauth
This commit is contained in:
@@ -5,7 +5,7 @@ import { firstValueFrom } from 'rxjs';
|
||||
import { CoreGrpcService } from '../core-grpc.service';
|
||||
import { OAuthAuthorizeIncomingDto, OAuthConsentActionDto, OAuthTokenIncomingDto } from '../dto/oauth.dto';
|
||||
import { extractBearerToken } from '../auth-token';
|
||||
import { appendQueryParams, mapUserInfoToOidc, mergeTokenCredentials, mapTokenResponseToStandard, normalizeAuthorizeQuery, normalizeConsentQuery, normalizeTokenBody } from '../lib/oauth-params';
|
||||
import { appendQueryParams, mapOAuthClientPublicInfo, mapOAuthConsentCheckResponse, mapUserInfoToOidc, mapUserOAuthConsentsResponse, mergeTokenCredentials, mapTokenResponseToStandard, normalizeAuthorizeQuery, normalizeConsentQuery, normalizeTokenBody } from '../lib/oauth-params';
|
||||
import { resolveFrontendUrl } from '../lib/oauth-issuer';
|
||||
import { verifyAccessToken } from '../session-auth';
|
||||
|
||||
@@ -87,13 +87,14 @@ export class OAuthController {
|
||||
|
||||
if (!consentCheck.granted) {
|
||||
if (wantsJson) {
|
||||
return (await firstValueFrom(
|
||||
const pendingConsent = (await firstValueFrom(
|
||||
this.core.oauth.CheckOAuthConsent({
|
||||
userId,
|
||||
clientId: normalized.clientId,
|
||||
scope: normalized.scope
|
||||
})
|
||||
)) as Record<string, unknown>;
|
||||
return mapOAuthConsentCheckResponse(pendingConsent);
|
||||
}
|
||||
const frontendUrl = await resolveFrontendUrl(this.core);
|
||||
const consentQuery = { ...(query as Record<string, unknown>) };
|
||||
@@ -139,13 +140,26 @@ export class OAuthController {
|
||||
) {
|
||||
const payload = await verifyAccessToken(this.jwt, authorization);
|
||||
const normalized = normalizeConsentQuery(query as Record<string, unknown>);
|
||||
return firstValueFrom(
|
||||
const result = (await firstValueFrom(
|
||||
this.core.oauth.CheckOAuthConsent({
|
||||
userId: payload.sub,
|
||||
clientId: normalized.clientId,
|
||||
scope: normalized.scope
|
||||
})
|
||||
);
|
||||
)) as Record<string, unknown>;
|
||||
return mapOAuthConsentCheckResponse(result);
|
||||
}
|
||||
|
||||
@Get('clients/:clientId/public')
|
||||
@ApiOperation({
|
||||
summary: 'Публичная информация об OAuth-приложении',
|
||||
description: 'Возвращает человекочитаемое название приложения для экрана согласия.'
|
||||
})
|
||||
async getClientPublicInfo(@Param('clientId') clientId: string) {
|
||||
const result = (await firstValueFrom(
|
||||
this.core.oauth.GetOAuthClientPublicInfo({ clientId })
|
||||
)) as Record<string, unknown>;
|
||||
return mapOAuthClientPublicInfo(result);
|
||||
}
|
||||
|
||||
@Post('consent/approve')
|
||||
@@ -186,7 +200,8 @@ export class OAuthController {
|
||||
if (payload.sub !== userId) {
|
||||
throw new UnauthorizedException('Можно просматривать только свои согласия');
|
||||
}
|
||||
return firstValueFrom(this.core.oauth.ListUserOAuthConsents({ userId }));
|
||||
const result = (await firstValueFrom(this.core.oauth.ListUserOAuthConsents({ userId }))) as Record<string, unknown>;
|
||||
return mapUserOAuthConsentsResponse(result);
|
||||
}
|
||||
|
||||
@Delete('consents/users/:userId/:consentId')
|
||||
|
||||
@@ -186,3 +186,63 @@ export function mergeTokenCredentials(
|
||||
clientSecret: body.clientSecret || basic.clientSecret
|
||||
};
|
||||
}
|
||||
|
||||
function readRecord(value: unknown) {
|
||||
return value && typeof value === 'object' ? (value as Record<string, unknown>) : null;
|
||||
}
|
||||
|
||||
export function mapOAuthClientPublicInfo(raw: Record<string, unknown>) {
|
||||
return {
|
||||
clientId: String(raw.clientId ?? raw.client_id ?? ''),
|
||||
name: String(raw.name ?? '')
|
||||
};
|
||||
}
|
||||
|
||||
export function mapOAuthScopeInfo(raw: Record<string, unknown>) {
|
||||
const slug = String(raw.slug ?? '');
|
||||
return {
|
||||
slug,
|
||||
name: String(raw.name ?? slug),
|
||||
description: raw.description ? String(raw.description) : undefined
|
||||
};
|
||||
}
|
||||
|
||||
export function mapOAuthConsentCheckResponse(raw: Record<string, unknown>) {
|
||||
const clientRaw = readRecord(raw.client);
|
||||
const scopesRaw = Array.isArray(raw.requestedScopes)
|
||||
? raw.requestedScopes
|
||||
: Array.isArray(raw.requested_scopes)
|
||||
? raw.requested_scopes
|
||||
: [];
|
||||
|
||||
return {
|
||||
granted: Boolean(raw.granted),
|
||||
client: clientRaw
|
||||
? mapOAuthClientPublicInfo(clientRaw)
|
||||
: undefined,
|
||||
requestedScopes: scopesRaw
|
||||
.map((item) => readRecord(item))
|
||||
.filter((item): item is Record<string, unknown> => Boolean(item))
|
||||
.map(mapOAuthScopeInfo)
|
||||
};
|
||||
}
|
||||
|
||||
export function mapUserOAuthConsentsResponse(raw: Record<string, unknown>) {
|
||||
const consentsRaw = Array.isArray(raw.consents) ? raw.consents : [];
|
||||
return {
|
||||
consents: consentsRaw
|
||||
.map((item) => readRecord(item))
|
||||
.filter((item): item is Record<string, unknown> => Boolean(item))
|
||||
.map((consent) => ({
|
||||
id: String(consent.id ?? ''),
|
||||
clientId: String(consent.clientId ?? consent.client_id ?? ''),
|
||||
clientName: String(consent.clientName ?? consent.client_name ?? consent.clientId ?? consent.client_id ?? ''),
|
||||
scopes: (Array.isArray(consent.scopes) ? consent.scopes : [])
|
||||
.map((item) => readRecord(item))
|
||||
.filter((item): item is Record<string, unknown> => Boolean(item))
|
||||
.map(mapOAuthScopeInfo),
|
||||
grantedAt: String(consent.grantedAt ?? consent.granted_at ?? ''),
|
||||
updatedAt: String(consent.updatedAt ?? consent.updated_at ?? '')
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,11 +6,13 @@ import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { CheckCircle2, Loader2, ShieldCheck } from 'lucide-react';
|
||||
import { BrandLogo } from '@/components/id/brand-logo';
|
||||
import { useAuth } from '@/components/id/auth-provider';
|
||||
import { usePublicSettings } from '@/components/id/public-settings-provider';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
approveOAuthAuthorization,
|
||||
checkOAuthConsent,
|
||||
fetchAuthSession,
|
||||
fetchOAuthClientPublicInfo,
|
||||
type OAuthConsentCheckResponse
|
||||
} from '@/lib/api';
|
||||
|
||||
@@ -26,11 +28,13 @@ function OAuthAuthorizeContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const { user, token, isPinLocked, isLoading } = useAuth();
|
||||
const { projectName } = usePublicSettings();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [checkingConsent, setCheckingConsent] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [sessionChecked, setSessionChecked] = useState(false);
|
||||
const [consentInfo, setConsentInfo] = useState<OAuthConsentCheckResponse | null>(null);
|
||||
const [clientName, setClientName] = useState<string | null>(null);
|
||||
const autoApproveStartedRef = useRef(false);
|
||||
|
||||
const oauthQuery = useMemo(() => buildOAuthQuery(searchParams), [searchParams]);
|
||||
@@ -42,7 +46,22 @@ function OAuthAuthorizeContent() {
|
||||
|
||||
const returnUrl = useMemo(() => `/auth/oauth/authorize?${oauthQuery.toString()}`, [oauthQuery]);
|
||||
|
||||
const clientLabel = consentInfo?.client?.name ?? clientId ?? 'Приложение';
|
||||
const clientLabel = clientName ?? consentInfo?.client?.name ?? 'Приложение';
|
||||
|
||||
useEffect(() => {
|
||||
if (!clientId) return;
|
||||
let cancelled = false;
|
||||
void fetchOAuthClientPublicInfo(clientId)
|
||||
.then((info) => {
|
||||
if (!cancelled && info.name.trim()) {
|
||||
setClientName(info.name.trim());
|
||||
}
|
||||
})
|
||||
.catch(() => undefined);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [clientId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!searchParams.has('userId')) return;
|
||||
@@ -107,6 +126,9 @@ function OAuthAuthorizeContent() {
|
||||
.then((result) => {
|
||||
if (cancelled) return;
|
||||
setConsentInfo(result);
|
||||
if (result.client?.name?.trim()) {
|
||||
setClientName(result.client.name.trim());
|
||||
}
|
||||
if (result.granted && !autoApproveStartedRef.current) {
|
||||
autoApproveStartedRef.current = true;
|
||||
void approve();
|
||||
@@ -193,7 +215,7 @@ function OAuthAuthorizeContent() {
|
||||
</div>
|
||||
<h1 className="text-2xl font-semibold">Разрешить доступ?</h1>
|
||||
<p className="mt-2 text-sm leading-relaxed text-[#667085]">
|
||||
Приложение <span className="font-medium text-[#1f2430]">{clientLabel}</span> запрашивает доступ к данным вашего аккаунта Lendry ID.
|
||||
Приложение <span className="font-medium text-[#1f2430]">{clientLabel}</span> запрашивает доступ к данным вашего аккаунта {projectName}.
|
||||
</p>
|
||||
<div className="mt-4 space-y-3 rounded-2xl bg-[#f4f5f8] p-4 text-sm">
|
||||
<p>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation';
|
||||
import { Ban, CheckCircle2, ChevronRight, FileKey2, Loader2 } from 'lucide-react';
|
||||
import { useAuth } from '@/components/id/auth-provider';
|
||||
import { IdShell } from '@/components/id/shell';
|
||||
import { usePublicSettings } from '@/components/id/public-settings-provider';
|
||||
import { useToast } from '@/components/id/toast-provider';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
@@ -29,6 +30,7 @@ function formatGrantedAt(value: string) {
|
||||
export default function DataConsentsPage() {
|
||||
const router = useRouter();
|
||||
const { user, token } = useAuth();
|
||||
const { projectName } = usePublicSettings();
|
||||
const { isReady, isPinLocked } = useRequireAuth();
|
||||
const { showToast } = useToast();
|
||||
const [consents, setConsents] = useState<OAuthUserConsent[]>([]);
|
||||
@@ -126,7 +128,7 @@ export default function DataConsentsPage() {
|
||||
Выданы {formatGrantedAt(selectedConsent.grantedAt)}
|
||||
</p>
|
||||
<div className="mt-4 rounded-2xl bg-[#f4f5f8] p-4">
|
||||
<p className="text-sm font-medium">API Lendry ID</p>
|
||||
<p className="text-sm font-medium">API {projectName}</p>
|
||||
<ul className="mt-3 space-y-2">
|
||||
{selectedConsent.scopes.map((scope) => (
|
||||
<li key={scope.slug} className="flex items-start gap-2 text-sm">
|
||||
|
||||
@@ -458,6 +458,10 @@ export async function checkOAuthConsent(params: URLSearchParams, token: string)
|
||||
return apiFetch<OAuthConsentCheckResponse>(`/oauth/consent/check?${query.toString()}`, {}, token);
|
||||
}
|
||||
|
||||
export async function fetchOAuthClientPublicInfo(clientId: string) {
|
||||
return apiFetch<{ clientId: string; name: string }>(`/oauth/clients/${encodeURIComponent(clientId)}/public`);
|
||||
}
|
||||
|
||||
export async function fetchUserOAuthConsents(userId: string, token: string) {
|
||||
return apiFetch<{ consents?: OAuthUserConsent[] }>(`/oauth/consents/users/${userId}`, {}, token);
|
||||
}
|
||||
|
||||
@@ -826,6 +826,11 @@ export class AuthGrpcController {
|
||||
return this.oauthCore.revokeConsent(command.userId, command.consentId);
|
||||
}
|
||||
|
||||
@GrpcMethod('OAuthCoreService', 'GetOAuthClientPublicInfo')
|
||||
getOAuthClientPublicInfo(command: { clientId: string }) {
|
||||
return this.oauthCore.getClientPublicInfo(command.clientId);
|
||||
}
|
||||
|
||||
@GrpcMethod('OtpService', 'SendOtp')
|
||||
sendOtp(command: { target: string; channel: string; purpose: string; userId?: string }) {
|
||||
return this.otp.send(command);
|
||||
|
||||
@@ -307,6 +307,15 @@ export class OAuthCoreService {
|
||||
return { count: 1 };
|
||||
}
|
||||
|
||||
async getClientPublicInfo(clientId: string) {
|
||||
const client = await this.prisma.oAuthClient.findUnique({ where: { clientId } });
|
||||
if (!client?.isActive) {
|
||||
throw new BadRequestException('OAuth приложение не найдено');
|
||||
}
|
||||
|
||||
return { clientId: client.clientId, name: client.name };
|
||||
}
|
||||
|
||||
private parseScopeSlugs(scope: string) {
|
||||
return [...new Set(scope.split(/\s+/).map((item) => item.trim()).filter(Boolean))];
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ service OAuthCoreService {
|
||||
rpc GrantOAuthConsent (GrantOAuthConsentRequest) returns (GrantOAuthConsentResponse);
|
||||
rpc ListUserOAuthConsents (UserIdRequest) returns (ListUserOAuthConsentsResponse);
|
||||
rpc RevokeOAuthConsent (RevokeOAuthConsentRequest) returns (MutationResponse);
|
||||
rpc GetOAuthClientPublicInfo (GetOAuthClientPublicInfoRequest) returns (OAuthClientPublicInfo);
|
||||
}
|
||||
|
||||
service OtpService {
|
||||
@@ -102,6 +103,10 @@ message OAuthClientPublicInfo {
|
||||
string name = 2;
|
||||
}
|
||||
|
||||
message GetOAuthClientPublicInfoRequest {
|
||||
string clientId = 1;
|
||||
}
|
||||
|
||||
message CheckOAuthConsentRequest {
|
||||
string userId = 1;
|
||||
string clientId = 2;
|
||||
|
||||
Reference in New Issue
Block a user