fix and update

This commit is contained in:
lendry
2026-06-29 12:17:25 +03:00
parent 923a028cdd
commit 75ccbe5fc4
39 changed files with 1619 additions and 63 deletions

View File

@@ -1,5 +1,6 @@
import type { DocBlock } from '@/lib/docs-pages';
import { OAuthCodeTabs } from '@/components/oauth-code-tabs';
import { OneTapCodeTabs } from '@/components/one-tap-code-tabs';
import { AuthLoginCodeTabs } from '@/components/auth-code-tabs';
import { AuthLdapCodeTabs } from '@/components/auth-ldap-code-tabs';
import { BotCodeTabs } from '@/components/bot-code-tabs';
@@ -67,6 +68,8 @@ export function DocBlockRenderer({ block }: { block: DocBlock }) {
);
case 'oauth-examples':
return <OAuthCodeTabs />;
case 'one-tap-examples':
return <OneTapCodeTabs />;
case 'auth-login-examples':
return <AuthLoginCodeTabs />;
case 'auth-ldap-examples':

View File

@@ -1,7 +1,7 @@
'use client';
import Link from 'next/link';
import { ArrowRight, BookOpen, Bot, Code2, Rocket, Shield } from 'lucide-react';
import { ArrowRight, BookOpen, Bot, Code2, MousePointerClick, Rocket, Shield } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { docNavigation, groupDocNavigation } from '@/lib/navigation';
@@ -20,6 +20,12 @@ const highlights = [
description: 'Стандартный OpenID Connect: Discovery, client_id, PKCE, form-urlencoded token. Примеры для PHP без доработки OidcProvider.',
href: '/docs/oauth'
},
{
icon: MousePointerClick,
title: 'One Tap Login',
description: 'FedCM и виджет sso-widget.js: вход в один клик на сайтах-клиентов без полного редиректа.',
href: '/docs/one-tap-login'
},
{
icon: Bot,
title: 'Telegram Bot API',

View File

@@ -0,0 +1,42 @@
'use client';
import { useEffect, useMemo, useState } from 'react';
import { fetchPublicSettingsClient } from '@/lib/api';
import { buildOneTapExamples, buildOneTapUrls } from '@/lib/one-tap-examples';
import { resolveFrontendBase, resolveOAuthApiBase } from '@/lib/oauth-url';
import { CodeExampleTabs } from '@/components/code-example-tabs';
export function OneTapCodeTabs() {
const [apiBase, setApiBase] = useState('http://localhost:3000');
const [frontendBase, setFrontendBase] = useState('http://localhost:3002');
const [projectName, setProjectName] = useState('MVK ID');
useEffect(() => {
void fetchPublicSettingsClient()
.then((settings) => {
setApiBase(resolveOAuthApiBase(settings));
setFrontendBase(resolveFrontendBase(settings));
if (settings.PROJECT_NAME?.trim()) {
setProjectName(settings.PROJECT_NAME.trim());
}
})
.catch(() => undefined);
}, []);
const urls = useMemo(() => buildOneTapUrls(apiBase, frontendBase, projectName), [apiBase, frontendBase, projectName]);
const examples = useMemo(() => buildOneTapExamples(urls), [urls]);
return (
<div className="space-y-3">
<p className="text-sm text-zinc-500 dark:text-zinc-400">
API (issuer): <code className="rounded bg-zinc-100 px-1.5 py-0.5 dark:bg-zinc-800">{apiBase}</code>
{' · '}
Frontend / виджет: <code className="rounded bg-zinc-100 px-1.5 py-0.5 dark:bg-zinc-800">{frontendBase}</code>
{' — '}
из настроек <strong>PUBLIC_API_URL</strong>, <strong>PUBLIC_FRONTEND_URL</strong> и <strong>PROJECT_NAME</strong>.
</p>
<CodeExampleTabs examples={examples} />
</div>
);
}

View File

@@ -66,6 +66,48 @@ export const apiReference: ApiTagGroup[] = [
}
]
},
{
tag: 'FedCM / One Tap Login',
endpoints: [
{
method: 'GET',
path: '/.well-known/web-identity',
summary: 'FedCM web identity manifest',
description: 'Манифест Federated Credential Management: provider_urls → /fedcm/config.json.'
},
{
method: 'GET',
path: '/fedcm/config.json',
summary: 'Конфигурация FedCM IdP',
description: 'accounts_endpoint, id_assertion_endpoint, login_url, branding.'
},
{
method: 'GET',
path: '/fedcm/accounts',
summary: 'Список аккаунтов FedCM',
description: 'Credentialed GET по cookie lendry_fedcm_sess. CORS с credentials для RP.'
},
{
method: 'POST',
path: '/fedcm/id_assertion',
summary: 'Выдача id_token FedCM',
description: 'Тело: client_id, account_id. Возвращает { token } — OIDC id_token.'
},
{
method: 'GET',
path: '/fedcm/client_metadata',
summary: 'Метаданные клиента FedCM',
description: 'Query: client_id. privacy_policy_url, terms_of_service_url.'
},
{
method: 'POST',
path: '/fedcm/session/sync',
summary: 'Синхронизация FedCM cookie',
description: 'Bearer access token → установка HttpOnly cookie lendry_fedcm_sess.',
auth: true
}
]
},
{
tag: 'Профиль и биометрия',
endpoints: [

View File

@@ -5,6 +5,7 @@ export type DocBlock =
| { type: 'callout'; variant: 'info' | 'warning' | 'tip'; title: string; text: string }
| { type: 'table'; headers: string[]; rows: string[][] }
| { type: 'oauth-examples' }
| { type: 'one-tap-examples' }
| { type: 'auth-login-examples' }
| { type: 'auth-ldap-examples' }
| { type: 'bot-examples' }
@@ -956,6 +957,23 @@ $oidc->authenticate(); // client_id, redirect_uri, response_type=code — авт
title: 'Примеры интеграции',
blocks: [{ type: 'oauth-examples' }]
},
{
id: 'one-tap',
title: 'One Tap Login',
blocks: [
{
type: 'paragraph',
text: 'Для входа в один клик на сторонних сайтах (FedCM + виджет sso-widget.js) см. отдельный раздел документации.'
},
{
type: 'list',
items: [
'One Tap Login — FedCM endpoints, подключение sso-widget.js, popup fallback',
'Требуется зарегистрированный OAuth client_id и redirect_uri вашего сайта'
]
}
]
},
{
id: 'scopes',
title: 'Scopes',
@@ -974,6 +992,201 @@ $oidc->authenticate(); // client_id, redirect_uri, response_type=code — авт
}
]
},
{
slug: 'one-tap-login',
title: 'One Tap Login',
description: 'Вход в один клик для сайтов-клиентов: FedCM (основной) и JS SDK с popup (fallback).',
sections: [
{
id: 'overview',
title: 'Обзор',
blocks: [
{
type: 'paragraph',
text: 'One Tap Login позволяет пользователям вашего сайта войти через Lendry ID без полного редиректа на страницу авторизации. Поддерживаются два режима: Federated Credential Management (FedCM) в современных браузерах и виджет sso-widget.js с popup-окном для остальных.'
},
{
type: 'callout',
variant: 'info',
title: 'Предварительные требования',
text: 'Создайте OAuth-приложение в админ-панели (RBAC → OAuth приложения): укажите redirect_uri вашего сайта, scopes openid profile email и сохраните client_id. Подробнее — раздел «OAuth 2.0 / OIDC».'
},
{
type: 'list',
items: [
'FedCM — браузер показывает нативный диалог «Войти как …», если пользователь уже залогинен на домене IdP',
'Fallback — скрипт sso-widget.js рисует плашку «Войти через …» в правом верхнем углу и открывает popup с OAuth',
'Оба режима возвращают OIDC id_token (FedCM) или authorization code / токены (popup) — проверяйте на backend'
]
}
]
},
{
id: 'fedcm-endpoints',
title: 'FedCM endpoints',
blocks: [
{
type: 'paragraph',
text: 'Endpoints размещены на PUBLIC_API_URL (issuer). Локально: http://localhost:3000. При same-origin деплое: https://ваш-домен/idp-api. CORS настроен с credentials: true для запросов с сайтов-клиентов.'
},
{
type: 'table',
headers: ['Endpoint', 'Метод', 'Описание'],
rows: [
['{PUBLIC_API_URL}/.well-known/web-identity', 'GET', 'Манифест FedCM → provider_urls'],
['{PUBLIC_API_URL}/fedcm/config.json', 'GET', 'Конфигурация IdP (accounts, id_assertion, branding)'],
['{PUBLIC_API_URL}/fedcm/accounts', 'GET', 'Список аккаунтов по cookie lendry_fedcm_sess'],
['{PUBLIC_API_URL}/fedcm/id_assertion', 'POST', 'Выдача id_token для client_id + account_id'],
['{PUBLIC_API_URL}/fedcm/client_metadata', 'GET', '?client_id=… — privacy/terms для UI FedCM'],
['{PUBLIC_API_URL}/fedcm/session/sync', 'POST', 'Установить FedCM cookie по Bearer access token']
]
},
{
type: 'callout',
variant: 'warning',
title: 'FedCM cookie',
text: 'FedCM использует HttpOnly cookie lendry_fedcm_sess на домене IdP (SameSite=None; Secure в production). Cookie устанавливается при входе на IdP или через POST /fedcm/session/sync. localStorage JWT с сайта клиента для FedCM не подходит.'
}
]
},
{
id: 'widget',
title: 'Подключение виджета (sso-widget.js)',
blocks: [
{
type: 'paragraph',
text: 'Скрипт размещён на frontend IdP по адресу {PUBLIC_FRONTEND_URL}/sso-widget.js. Достаточно одного тега script — инициализация выполняется автоматически при загрузке страницы.'
},
{
type: 'table',
headers: ['Атрибут data-*', 'Обязательный', 'Описание'],
rows: [
['data-client-id', 'Да', 'client_id OAuth-приложения из админки'],
['data-idp-url', 'Нет', 'PUBLIC_API_URL; по умолчанию origin скрипта + /idp-api'],
['data-idp-frontend-url', 'Нет', 'URL frontend IdP для popup; по умолчанию origin скрипта'],
['data-provider-name', 'Нет', 'Название в плашке (по умолчанию MVK ID)'],
['data-redirect-uri', 'Нет', 'redirect_uri OAuth; по умолчанию origin + /auth/callback'],
['data-scope', 'Нет', 'Scopes OAuth (по умолчанию openid profile email)'],
['data-on-success', 'Нет', 'Имя глобальной функции-callback'],
['data-auto-init', 'Нет', 'false — отключить автозапуск; вызовите LendryIdOneTap.init()']
]
},
{
type: 'callout',
variant: 'tip',
title: 'Логика выбора режима',
text: 'Если браузер поддерживает IdentityCredential — скрипт вызывает FedCM. Если нет — показывается плашка в правом верхнем углу; по клику открывается popup OAuth (параметр display=popup).'
}
]
},
{
id: 'fedcm-integration',
title: 'Интеграция FedCM вручную',
blocks: [
{
type: 'paragraph',
text: 'Если вы не используете sso-widget.js, можно вызвать FedCM API напрямую. configURL должен указывать на /fedcm/config.json провайдера.'
},
{
type: 'code',
language: 'javascript',
title: 'Минимальный пример',
code: `const credential = await navigator.credentials.get({
identity: {
providers: [{
configURL: 'https://id.lendry.ru/idp-api/fedcm/config.json',
clientId: 'YOUR_CLIENT_ID'
}]
},
mediation: 'optional'
});
const idToken = credential?.token;
// Передайте idToken на ваш backend для проверки`
},
{
type: 'list',
items: [
'mediation: "optional" — показать диалог только при наличии сессии IdP',
'mediation: "required" — всегда показывать UI выбора аккаунта',
'mediation: "silent" — без UI; вернёт ошибку, если пользователь не залогинен'
]
}
]
},
{
id: 'popup-fallback',
title: 'Popup fallback и postMessage',
blocks: [
{
type: 'paragraph',
text: 'При клике на виджет открывается popup с OAuth authorize. После успешного входа IdP отправляет результат родительскому окну через window.postMessage с типом lendry-sso-onetap.'
},
{
type: 'code',
language: 'javascript',
title: 'Обработка на сайте клиента',
code: `window.addEventListener('message', (event) => {
if (event.data?.type !== 'lendry-sso-onetap') return;
// Проверяйте event.origin — домен вашего IdP
const { token, idToken, accessToken, code, method } = event.data;
console.log('Вход через', method);
});`
},
{
type: 'callout',
variant: 'info',
title: 'redirect_uri',
text: 'redirect_uri должен быть зарегистрирован у OAuth-клиента. Для SPA часто используют https://your-app.com/auth/callback — на этой странице backend обменивает code на токены, либо popup передаёт токен через postMessage.'
}
]
},
{
id: 'verify-token',
title: 'Проверка токена на backend',
blocks: [
{
type: 'paragraph',
text: 'FedCM возвращает id_token (JWT). Проверьте подпись (HS256, секрет JWT_ACCESS_SECRET IdP), issuer = PUBLIC_API_URL и aud = ваш client_id. Альтернатива — обмен authorization code через POST /oauth/token и запрос GET /oauth/userinfo.'
},
{
type: 'list',
items: [
'Не доверяйте id_token только на frontend — всегда валидируйте на сервере',
'client_secret храните только на backend при обмене code → token',
'После проверки создайте локальную сессию пользователя (cookie / JWT вашего приложения)'
]
}
]
},
{
id: 'examples',
title: 'Примеры интеграции',
blocks: [{ type: 'one-tap-examples' }]
},
{
id: 'local-dev',
title: 'Локальная разработка',
blocks: [
{
type: 'table',
headers: ['Сервис', 'URL'],
rows: [
['API (FedCM endpoints)', 'http://localhost:3000'],
['Frontend (sso-widget.js)', 'http://localhost:3002/sso-widget.js'],
['Документация', 'http://localhost:3003/docs/one-tap-login']
]
},
{
type: 'callout',
variant: 'tip',
title: 'Тест FedCM локально',
text: 'FedCM cookie в dev использует SameSite=Lax (без Secure). Войдите на http://localhost:3002, затем откройте тестовую страницу клиента с подключённым виджетом. Chrome может требовать флаг chrome://flags/#identity-credentials-api или HTTPS для FedCM — в этом случае проверяйте popup fallback.'
}
]
}
]
},
{
slug: 'ldap',
title: 'LDAP / LDAPS',

View File

@@ -10,6 +10,7 @@ export const docNavigation: DocNavItem[] = [
{ slug: 'deployment', title: 'Развёртывание на сервере', group: 'Введение' },
{ slug: 'authentication', title: 'Аутентификация', group: 'Интеграция' },
{ slug: 'oauth', title: 'OAuth 2.0 / OIDC', group: 'Интеграция' },
{ slug: 'one-tap-login', title: 'One Tap Login', group: 'Интеграция' },
{ slug: 'ldap', title: 'LDAP / LDAPS', group: 'Интеграция' },
{ slug: 'sessions', title: 'Сессии, PIN и удаление аккаунта', group: 'Безопасность' },
{ slug: 'family-chat', title: 'Семья и чат', group: 'Функции' },

View File

@@ -19,6 +19,23 @@ export function resolveOAuthApiBase(settings: Record<string, string>, fallback =
return normalizeBaseUrl(fallback);
}
export function resolveFrontendBase(settings: Record<string, string>, fallback = 'http://localhost:3002') {
const publicFrontend = settings.PUBLIC_FRONTEND_URL?.trim();
if (publicFrontend) {
return normalizeBaseUrl(publicFrontend);
}
const domain = settings.PROJECT_DOMAIN?.trim();
if (domain) {
if (domain.startsWith('http://') || domain.startsWith('https://')) {
return normalizeBaseUrl(domain);
}
return `https://${domain.replace(/^\/+/, '')}`;
}
return normalizeBaseUrl(fallback);
}
export interface OAuthEndpoints {
issuer: string;
authorizationEndpoint: string;
@@ -26,6 +43,10 @@ export interface OAuthEndpoints {
userInfoEndpoint: string;
openIdConfigurationUrl: string;
jwksUrl: string;
webIdentityUrl: string;
fedcmConfigUrl: string;
fedcmAccountsUrl: string;
fedcmIdAssertionUrl: string;
}
export function buildOAuthEndpoints(apiBase: string): OAuthEndpoints {
@@ -36,7 +57,11 @@ export function buildOAuthEndpoints(apiBase: string): OAuthEndpoints {
tokenEndpoint: `${base}/oauth/token`,
userInfoEndpoint: `${base}/oauth/userinfo`,
openIdConfigurationUrl: `${base}/.well-known/openid-configuration`,
jwksUrl: `${base}/.well-known/jwks.json`
jwksUrl: `${base}/.well-known/jwks.json`,
webIdentityUrl: `${base}/.well-known/web-identity`,
fedcmConfigUrl: `${base}/fedcm/config.json`,
fedcmAccountsUrl: `${base}/fedcm/accounts`,
fedcmIdAssertionUrl: `${base}/fedcm/id_assertion`
};
}

View File

@@ -0,0 +1,163 @@
import type { OAuthExample } from '@/lib/oauth-examples';
export interface OneTapUrls {
apiBase: string;
frontendBase: string;
widgetUrl: string;
fedcmConfigUrl: string;
webIdentityUrl: string;
projectName: string;
}
export function buildOneTapUrls(
apiBase: string,
frontendBase: string,
projectName = 'MVK ID'
): OneTapUrls {
const base = apiBase.replace(/\/+$/, '');
const front = frontendBase.replace(/\/+$/, '');
return {
apiBase: base,
frontendBase: front,
widgetUrl: `${front}/sso-widget.js`,
fedcmConfigUrl: `${base}/fedcm/config.json`,
webIdentityUrl: `${base}/.well-known/web-identity`,
projectName
};
}
export function buildOneTapExamples(urls: OneTapUrls, clientIdPlaceholder = 'YOUR_CLIENT_ID'): OAuthExample[] {
const { apiBase, frontendBase, widgetUrl, fedcmConfigUrl, projectName } = urls;
const redirectUri = 'https://app.example.com/auth/callback';
return [
{
id: 'widget-script',
label: 'Виджет (script tag)',
language: 'html',
code: `<!-- Подключите на любой странице вашего сайта -->
<script
src="${widgetUrl}"
data-client-id="${clientIdPlaceholder}"
data-idp-url="${apiBase}"
data-idp-frontend-url="${frontendBase}"
data-provider-name="${projectName}"
data-redirect-uri="${redirectUri}"
data-on-success="handleLendryLogin"
></script>
<script>
function handleLendryLogin(payload) {
// payload.token — id_token (FedCM) или токен из popup
// payload.method — 'fedcm' | 'popup'
console.log('Вход через', payload.method, payload.token);
// Отправьте token на ваш backend для проверки и создания сессии
}
</script>`
},
{
id: 'fedcm-native',
label: 'FedCM (нативный API)',
language: 'javascript',
code: `// Работает в Chrome/Edge при поддержке IdentityCredential.
// Пользователь должен быть залогинен на ${frontendBase} (FedCM cookie).
async function loginWithFedCM() {
if (!('IdentityCredential' in window)) {
throw new Error('FedCM не поддерживается в этом браузере');
}
const credential = await navigator.credentials.get({
identity: {
providers: [{
configURL: '${fedcmConfigUrl}',
clientId: '${clientIdPlaceholder}'
}]
},
mediation: 'optional'
});
if (!credential?.token) {
throw new Error('Пользователь отменил вход или сессия IdP отсутствует');
}
return credential.token; // OIDC id_token
}
loginWithFedCM()
.then((idToken) => fetch('/api/auth/lendry', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ idToken })
}))
.catch(console.error);`
},
{
id: 'sdk-manual',
label: 'SDK — ручной вызов',
language: 'javascript',
code: `<script src="${widgetUrl}" data-auto-init="false"></script>
<script>
LendryIdOneTap.init({
clientId: '${clientIdPlaceholder}',
idpUrl: '${apiBase}',
frontendUrl: '${frontendBase}',
providerName: '${projectName}',
redirectUri: '${redirectUri}',
scope: 'openid profile email'
});
window.addEventListener('lendry-sso-onetap-success', (event) => {
const { token, method, accessToken, idToken } = event.detail;
console.log(method, token ?? idToken ?? accessToken);
});
</script>`
},
{
id: 'verify-backend',
label: 'Проверка токена на backend',
language: 'javascript',
code: `// Node.js — после получения id_token от FedCM или popup
import jwt from 'jsonwebtoken';
const ISSUER = '${apiBase}';
const CLIENT_ID = '${clientIdPlaceholder}';
function verifyIdToken(idToken) {
const payload = jwt.verify(idToken, process.env.IDP_JWT_SECRET, {
issuer: ISSUER,
audience: CLIENT_ID
});
return payload; // { sub, email, name, ... }
}
// Альтернатива: userinfo по access_token
async function fetchProfile(accessToken) {
const res = await fetch('${apiBase}/oauth/userinfo', {
headers: { Authorization: \`Bearer \${accessToken}\` }
});
if (!res.ok) throw new Error('userinfo failed');
return res.json();
}`
},
{
id: 'curl-fedcm',
label: 'FedCM endpoints (curl)',
language: 'bash',
code: `# Манифест FedCM
curl -s ${apiBase}/.well-known/web-identity | jq
# Конфигурация провайдера
curl -s ${apiBase}/fedcm/config.json | jq
# Список аккаунтов (нужна cookie lendry_fedcm_sess после входа на IdP)
curl -s ${apiBase}/fedcm/accounts \\
-H "Cookie: lendry_fedcm_sess=..." \\
--include
# Синхронизация FedCM cookie для уже залогиненного пользователя IdP
curl -s -X POST ${apiBase}/fedcm/session/sync \\
-H "Authorization: Bearer ACCESS_TOKEN"`
}
];
}

View File

@@ -17,6 +17,10 @@ const nextConfig: NextConfig = {
source: '/oauth/:path*',
destination: `${internalApiUrl}/oauth/:path*`
},
{
source: '/fedcm/:path*',
destination: `${internalApiUrl}/fedcm/:path*`
},
{
source: '/.well-known/:path*',
destination: `${internalApiUrl}/.well-known/:path*`