add oauth app connected

This commit is contained in:
lendry
2026-06-26 10:49:34 +03:00
parent d5e6b58955
commit b81c0cedbb
18 changed files with 838 additions and 59 deletions

View File

@@ -127,6 +127,30 @@ export interface OAuthScope {
description?: string;
}
export interface OAuthConsentScopeInfo {
slug: string;
name: string;
description?: string;
}
export interface OAuthConsentCheckResponse {
granted: boolean;
client?: {
clientId: string;
name: string;
};
requestedScopes?: OAuthConsentScopeInfo[];
}
export interface OAuthUserConsent {
id: string;
clientId: string;
clientName: string;
scopes: OAuthConsentScopeInfo[];
grantedAt: string;
updatedAt: string;
}
export interface OAuthClient {
id: string;
name: string;
@@ -411,15 +435,35 @@ export async function fetchAuthSession(token?: string | null): Promise<AuthSessi
}
export async function approveOAuthAuthorization(params: URLSearchParams, token: string) {
const body = {
client_id: params.get('client_id') ?? params.get('clientId') ?? undefined,
redirect_uri: params.get('redirect_uri') ?? params.get('redirectUri') ?? undefined,
scope: params.get('scope') ?? 'openid profile',
state: params.get('state') ?? undefined,
nonce: params.get('nonce') ?? undefined
};
return apiFetch<{ redirectUrl?: string }>('/oauth/consent/approve', {
method: 'POST',
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
body: JSON.stringify(body)
}, token);
}
export async function checkOAuthConsent(params: URLSearchParams, token: string) {
const query = new URLSearchParams();
params.forEach((value, key) => {
if (key !== 'userId') query.set(key, value);
});
return apiFetch<{ redirectUrl?: string }>(
`/oauth/authorize?${query.toString()}`,
{ headers: { Accept: 'application/json' } },
token
);
const clientId = params.get('client_id') ?? params.get('clientId');
const scope = params.get('scope') ?? 'openid profile';
if (clientId) query.set('client_id', clientId);
query.set('scope', scope);
return apiFetch<OAuthConsentCheckResponse>(`/oauth/consent/check?${query.toString()}`, {}, token);
}
export async function fetchUserOAuthConsents(userId: string, token: string) {
return apiFetch<{ consents?: OAuthUserConsent[] }>(`/oauth/consents/users/${userId}`, {}, token);
}
export async function revokeOAuthConsent(userId: string, consentId: string, token: string) {
return apiFetch(`/oauth/consents/users/${userId}/${consentId}`, { method: 'DELETE' }, token);
}
export async function refreshAuthSession(): Promise<RefreshSessionResponse> {

View File

@@ -36,6 +36,16 @@ export function getMessageCopyText(message: ChatMessage, visibleText?: string) {
return '';
}
function looksLikeEncryptedPayload(content?: string | null) {
if (!content?.trim().startsWith('{')) return false;
try {
const parsed = JSON.parse(content) as { v?: unknown; ciphertext?: unknown; kind?: unknown };
return typeof parsed === 'object' && parsed !== null && ('v' in parsed || 'ciphertext' in parsed || 'kind' in parsed);
} catch {
return false;
}
}
export function getMessagePreviewText(message: ChatMessage, visibleText?: string) {
if (message.type === 'SYSTEM') {
return resolveNativeEmojiContent(message.content?.trim() ?? 'Системное сообщение');
@@ -44,7 +54,7 @@ export function getMessagePreviewText(message: ChatMessage, visibleText?: string
return `📊 Опрос: ${message.poll.question}`;
}
const text = getMessageCopyText(message, visibleText);
if (text) return text;
if (text && !looksLikeEncryptedPayload(text)) return text;
if (message.type === 'IMAGE') return '📷 Фото';
if (message.type === 'VOICE') return '🎤 Голосовое';
if (message.type === 'AUDIO') return '🎵 Аудио';

View File

@@ -0,0 +1,55 @@
'use client';
import { useEffect, useState } from 'react';
import { AUTH_REFRESH_KEY, AUTH_SESSION_KEY, AUTH_TOKEN_KEY } from '@/lib/api';
export const EMBEDDED_AUTH_MESSAGE = 'lendry-id-embedded-auth';
export interface EmbeddedAuthPayload {
type: typeof EMBEDDED_AUTH_MESSAGE;
token: string;
refreshToken?: string;
sessionId?: string;
}
export function postEmbeddedAuthToIframe(iframe: HTMLIFrameElement, auth: { token: string; refreshToken?: string | null; sessionId?: string | null }) {
const target = iframe.contentWindow;
if (!target || !auth.token) return;
const payload: EmbeddedAuthPayload = {
type: EMBEDDED_AUTH_MESSAGE,
token: auth.token,
refreshToken: auth.refreshToken ?? undefined,
sessionId: auth.sessionId ?? undefined
};
target.postMessage(payload, window.location.origin);
}
export function useEmbeddedAuthFallback() {
const [embeddedToken, setEmbeddedToken] = useState<string | null>(null);
useEffect(() => {
function handleMessage(event: MessageEvent) {
if (event.origin !== window.location.origin) return;
const data = event.data as Partial<EmbeddedAuthPayload> | null;
if (!data || data.type !== EMBEDDED_AUTH_MESSAGE || !data.token?.trim()) return;
const token = data.token.trim();
setEmbeddedToken(token);
window.localStorage.setItem(AUTH_TOKEN_KEY, token);
if (data.refreshToken) {
window.localStorage.setItem(AUTH_REFRESH_KEY, data.refreshToken);
}
if (data.sessionId) {
window.localStorage.setItem(AUTH_SESSION_KEY, data.sessionId);
}
}
window.addEventListener('message', handleMessage);
if (window.parent !== window) {
window.parent.postMessage({ type: 'lendry-id-embedded-auth-request' }, window.location.origin);
}
return () => window.removeEventListener('message', handleMessage);
}, []);
return embeddedToken;
}