Files
IdP/apps/frontend/lib/api.ts
2026-06-26 16:43:17 +03:00

1185 lines
34 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.
const API_ORIGIN_PROXY_PREFIX = '/idp-api';
const configuredApiUrl = (process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3000').replace(/\/$/, '');
const configuredWsUrl = (process.env.NEXT_PUBLIC_WS_URL ?? 'ws://localhost:8085/ws').replace(/\/$/, '');
function resolveBrowserApiBaseUrl(): string {
if (typeof window === 'undefined') {
return configuredApiUrl;
}
try {
const configured = new URL(configuredApiUrl);
if (configured.hostname !== window.location.hostname) {
return API_ORIGIN_PROXY_PREFIX;
}
} catch {
// keep configured URL
}
return configuredApiUrl;
}
function resolveBrowserWsBaseUrl(): string {
if (typeof window === 'undefined') {
return configuredWsUrl;
}
const apiBase = resolveBrowserApiBaseUrl();
if (apiBase === API_ORIGIN_PROXY_PREFIX) {
const scheme = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
return `${scheme}//${window.location.host}${API_ORIGIN_PROXY_PREFIX}/ws`;
}
try {
const configured = new URL(configuredWsUrl);
if (configured.hostname !== window.location.hostname) {
const scheme = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
return `${scheme}//${window.location.host}${API_ORIGIN_PROXY_PREFIX}/ws`;
}
} catch {
// keep configured URL
}
return configuredWsUrl;
}
export function getApiUrl(): string {
return resolveBrowserApiBaseUrl();
}
export function getWsUrl(): string {
return resolveBrowserWsBaseUrl();
}
/** @deprecated Используйте getApiUrl() — URL может переключаться на same-origin /idp-api в браузере */
export const API_URL = configuredApiUrl;
export interface PublicUser {
id: string;
email?: string | null;
phone?: string | null;
backupEmail?: string | null;
backupPhone?: string | null;
displayName: string;
username?: string | null;
avatarUrl?: string | null;
hasAvatar?: boolean;
isSuperAdmin: boolean;
status: string;
roles?: string[];
permissions?: string[];
canAccessAdmin?: boolean;
canManageRoles?: boolean;
canViewOAuth?: boolean;
canManageOAuth?: boolean;
canManageAllOAuth?: boolean;
canManageUsers?: boolean;
canManageAllUsers?: boolean;
canManageSettings?: boolean;
canViewUsers?: boolean;
canViewUserDocuments?: boolean;
hasPassword?: boolean;
isVerified?: boolean;
verificationIcon?: string | null;
canVerifyUsers?: boolean;
canManageBots?: boolean;
}
export interface AdminUser {
id: string;
email?: string | null;
phone?: string | null;
displayName: string;
username?: string | null;
isSuperAdmin: boolean;
status: string;
createdAt: string;
roles: string[];
directPermissions?: string[];
isVerified?: boolean;
verificationIcon?: string | null;
isBot?: boolean;
linkedBotUsername?: string | null;
isSystemBot?: boolean;
}
export interface AdminRole {
id: string;
slug: string;
name: string;
description?: string;
isSystem?: boolean;
isDefault?: boolean;
permissions: { id: string; slug: string; name: string; description?: string }[];
}
export interface AdminPermission {
id: string;
slug: string;
name: string;
description?: string;
}
export interface OAuthScope {
id: string;
slug: string;
name: string;
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;
clientId: string;
redirectUris: string[];
scopes: string[];
isActive: boolean;
type: string;
clientSecret?: string;
createdById?: string | null;
createdByDisplayName?: string | null;
canManage?: boolean;
}
export interface AuthTokens {
accessToken: string;
refreshToken: string;
expiresAt: string;
pinVerified: boolean;
user: PublicUser;
sessionId: string;
requiresTotp?: boolean;
totpChallengeToken?: string;
}
export interface UserProfileResponse {
birthDate?: string | null;
email?: string | null;
phone?: string | null;
backupEmail?: string | null;
backupPhone?: string | null;
displayName?: string;
firstName?: string | null;
lastName?: string | null;
}
export interface QrLoginSession {
sessionId: string;
status: 'PENDING' | 'APPROVED' | 'EXPIRED';
expiresAt: string;
qrPayload?: string;
auth?: AuthTokens & { user?: PublicUser };
}
export async function createQrLoginSession(deviceName: string) {
return apiFetch<QrLoginSession>('/auth/advanced/qr/session', {
method: 'POST',
body: JSON.stringify({
deviceName,
fingerprint: getDeviceFingerprint(),
deviceType: 'WEB'
})
});
}
export async function pollQrLoginSession(sessionId: string) {
return apiFetch<QrLoginSession>(`/auth/advanced/qr/session/${sessionId}`);
}
export interface TotpSetupResponse {
secret: string;
otpauthUrl: string;
}
export interface TotpStatusResponse {
isEnabled: boolean;
}
export interface PresignedUploadResponse {
uploadUrl: string;
storageKey: string;
expiresAt?: string;
uploadToken?: string;
}
export async function uploadMediaObject(
presigned: PresignedUploadResponse,
file: Blob,
contentType: string
) {
if (presigned.uploadToken) {
const apiBase = getApiUrl().replace(/\/$/, '');
const formData = new FormData();
formData.append('file', file, file instanceof File ? file.name : 'upload.bin');
const response = await fetch(`${apiBase}/media/upload`, {
method: 'POST',
headers: { 'X-Upload-Token': presigned.uploadToken },
body: formData
});
if (!response.ok) {
throw new Error('Не удалось загрузить файл');
}
return;
}
const response = await fetch(presigned.uploadUrl, {
method: 'PUT',
headers: { 'Content-Type': contentType },
body: file
});
if (!response.ok) {
throw new Error('Не удалось загрузить файл');
}
}
export interface FamilyInviteCandidate {
id: string;
displayName: string;
email?: string | null;
phone?: string | null;
username?: string | null;
hasAvatar?: boolean;
isVerified?: boolean;
verificationIcon?: string | null;
isBot?: boolean;
botUsername?: string;
botId?: string;
ownerDisplayName?: string;
}
export async function searchFamilyInviteUsers(groupId: string, query: string, token?: string | null) {
return apiFetch<{ users?: FamilyInviteCandidate[] }>(
`/family/groups/${groupId}/invite-search?q=${encodeURIComponent(query)}`,
{},
token
);
}
export interface PasswordlessAuthResponse {
requiresPassword: boolean;
requiresTotp?: boolean;
totpChallengeToken?: string;
tempAuthToken?: string;
auth?: AuthTokens;
}
export interface OtpChannel {
channel: 'email' | 'phone' | 'backupEmail' | 'backupPhone' | string;
masked: string;
}
export interface LoginMethod {
kind: 'password' | 'otp';
channel: string;
masked: string;
}
export interface IdentifyResponse {
exists: boolean;
hasPassword: boolean;
isPinEnabled: boolean;
isTotpEnabled?: boolean;
otpChannels?: OtpChannel[];
methods?: LoginMethod[];
}
export interface BeginTotpLoginResponse {
totpChallengeToken: string;
}
export interface OtpSendResponse {
sent: boolean;
expiresAt: string;
maskedTarget: string;
resendAvailableAt?: string;
}
export interface PinVerificationResponse {
accessToken: string;
pinVerified: boolean;
}
export interface RefreshSessionResponse {
requiresPin: boolean;
accessToken?: string;
sessionId?: string;
pinVerified: boolean;
user?: PublicUser;
}
export interface AuthSessionResponse {
requiresPin: boolean;
sessionId?: string | null;
pinVerified: boolean;
user: PublicUser;
}
export const AUTH_TOKEN_KEY = 'lendry_access_token';
export const AUTH_REFRESH_KEY = 'lendry_refresh_token';
export const AUTH_SESSION_KEY = 'lendry_session_id';
export const AUTH_USER_CACHE_KEY = 'lendry_cached_user';
export class ApiError extends Error {
status: number;
code?: string;
sessionId?: string;
constructor(message: string, status: number, code?: string, sessionId?: string) {
super(message);
this.name = 'ApiError';
this.status = status;
this.code = code;
this.sessionId = sessionId;
}
}
function mapFetchError(error: unknown): ApiError {
if (error instanceof TypeError) {
const message = error.message.toLowerCase();
if (message.includes('failed to fetch') || message.includes('networkerror')) {
return new ApiError(
'Нет связи с сервером. При самоподписанном HTTPS примите сертификат сайта или выполните ./install.sh --fix-proxy на сервере.',
0,
'NETWORK_ERROR'
);
}
}
if (error instanceof ApiError) {
return error;
}
return new ApiError(error instanceof Error ? error.message : 'Не удалось выполнить запрос', 0, 'UNKNOWN');
}
type PinRequiredHandler = (sessionId: string) => void;
export function setPinRequiredHandler(handler: PinRequiredHandler | null) {
pinRequiredHandler = handler;
if (!handler) {
pinNotificationSent = false;
}
}
interface ErrorBody {
message?: string | string[];
error?: string;
code?: string;
sessionId?: string;
}
async function parseErrorResponse(response: Response): Promise<ApiError> {
try {
const body = (await response.clone().json()) as ErrorBody;
const message = Array.isArray(body.message)
? body.message.join('\n')
: (body.message ?? body.error ?? 'Не удалось выполнить запрос');
const pinRequired =
body.code === 'PIN_REQUIRED' ||
(response.status === 403 && typeof message === 'string' && message.toLowerCase().includes('pin'));
return new ApiError(message, response.status, pinRequired ? 'PIN_REQUIRED' : body.code, body.sessionId);
} catch {
return new ApiError('Не удалось выполнить запрос', response.status);
}
}
export function isPinRequiredError(error: unknown): error is ApiError {
return error instanceof ApiError && error.code === 'PIN_REQUIRED';
}
export function getApiErrorMessage(error: unknown, fallback: string): string | null {
if (isPinRequiredError(error)) return null;
if (error instanceof ApiError && error.status === 403 && error.message.toLowerCase().includes('pin')) {
return null;
}
return error instanceof Error ? error.message : fallback;
}
let pinRequiredHandler: PinRequiredHandler | null = null;
let pinNotificationSent = false;
export function resetPinRequiredNotification() {
pinNotificationSent = false;
}
function notifyPinRequired(sessionId: string) {
if (!pinRequiredHandler || pinNotificationSent) return;
pinNotificationSent = true;
pinRequiredHandler(sessionId);
}
export async function fetchAuthSession(token?: string | null): Promise<AuthSessionResponse> {
return apiFetch<AuthSessionResponse>('/auth/session', {}, token);
}
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();
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 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);
}
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> {
if (typeof window === 'undefined') {
throw new ApiError('Refresh token недоступен на сервере', 401, 'NO_REFRESH');
}
const refreshToken = window.localStorage.getItem(AUTH_REFRESH_KEY);
const sessionId = window.localStorage.getItem(AUTH_SESSION_KEY);
if (!refreshToken) {
throw new ApiError('Refresh token отсутствует', 401, 'NO_REFRESH');
}
const response = await fetch(`${getApiUrl()}/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken, sessionId: sessionId ?? undefined })
});
if (!response.ok) {
throw await parseErrorResponse(response);
}
return response.json() as Promise<RefreshSessionResponse>;
}
async function trySilentTokenRefresh(): Promise<RefreshSessionResponse | null> {
try {
const refreshed = await refreshAuthSession();
if (refreshed.requiresPin) {
notifyPinRequired(refreshed.sessionId ?? window.localStorage.getItem(AUTH_SESSION_KEY) ?? '');
return refreshed;
}
if (refreshed.accessToken) {
window.localStorage.setItem(AUTH_TOKEN_KEY, refreshed.accessToken);
if (refreshed.sessionId) {
window.localStorage.setItem(AUTH_SESSION_KEY, refreshed.sessionId);
}
if (refreshed.user) {
window.dispatchEvent(new CustomEvent('lendry:auth-refreshed', { detail: refreshed }));
}
}
return refreshed;
} catch {
return null;
}
}
export interface UserDocument {
id: string;
userId: string;
type: string;
number: string;
issuedAt?: string;
expiresAt?: string;
metadataJson?: string;
}
export interface SystemSetting {
id: string;
key: string;
value: string;
description?: string | null;
isSecret: boolean;
}
export interface SocialProvider {
id: string;
providerName: string;
clientId: string;
isEnabled: boolean;
}
export interface ActiveDevice {
id: string;
name: string;
type: string;
lastIp?: string | null;
lastSeenAt: string;
trusted: boolean;
}
export interface SignInEvent {
id: string;
success: boolean;
reason?: string | null;
ipAddress?: string | null;
userAgent?: string | null;
createdAt: string;
}
export interface ActiveSession {
id: string;
userId: string;
deviceId?: string | null;
pinVerified: boolean;
status: string;
ipAddress?: string | null;
userAgent?: string | null;
expiresAt: string;
createdAt: string;
updatedAt: string;
}
async function parseError(response: Response): Promise<string> {
const error = await parseErrorResponse(response);
return error.message;
}
export function buildSystemSettingPayload(setting: Pick<SystemSetting, 'key' | 'value' | 'description' | 'isSecret'>) {
return {
key: setting.key,
value: setting.value,
description: setting.description ?? undefined,
isSecret: setting.isSecret
};
}
export async function apiFetch<T>(path: string, options: RequestInit = {}, token?: string | null, allowRetry = true): Promise<T> {
const resolvedToken = token ?? (typeof window !== 'undefined' ? window.localStorage.getItem(AUTH_TOKEN_KEY) : null);
let response: Response;
try {
response = await fetch(`${getApiUrl()}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
...(resolvedToken ? { Authorization: `Bearer ${resolvedToken}` } : {}),
...options.headers
}
});
} catch (error) {
throw mapFetchError(error);
}
if (!response.ok) {
const error = await parseErrorResponse(response);
if (error.code === 'PIN_REQUIRED') {
notifyPinRequired(error.sessionId ?? window.localStorage.getItem(AUTH_SESSION_KEY) ?? '');
throw error;
}
if (allowRetry && resolvedToken && error.code === 'TOKEN_EXPIRED') {
const refreshed = await trySilentTokenRefresh();
if (refreshed?.accessToken) {
return apiFetch<T>(path, options, refreshed.accessToken, false);
}
if (refreshed?.requiresPin) {
throw error;
}
}
throw error;
}
return response.json() as Promise<T>;
}
export function getDeviceFingerprint(): string {
if (typeof window === 'undefined') return 'server';
const key = 'lendry_device_fingerprint';
const existing = window.localStorage.getItem(key);
if (existing) return existing;
const value = crypto.randomUUID();
window.localStorage.setItem(key, value);
return value;
}
export interface MediaAccessResponse {
accessUrl: string;
expiresAt: string;
}
export async function fetchDocumentPhotoUrl(userId: string, storageKey: string, token?: string | null) {
const query = new URLSearchParams({ storageKey });
return apiFetch<MediaAccessResponse>(`/media/users/${userId}/documents/photo-url?${query.toString()}`, {}, token);
}
export interface AccountDeletionStatus {
pending: boolean;
deletionRequestedAt?: string;
effectiveAt?: string;
graceDays: number;
}
export async function requestAccountDeletion(userId: string, token?: string | null) {
return apiFetch<AccountDeletionStatus & { userId: string }>(
`/profile/users/${userId}/self-delete`,
{ method: 'POST' },
token
);
}
export async function cancelAccountDeletion(userId: string, token?: string | null) {
return apiFetch<{ userId: string; cancelled: boolean }>(
`/profile/users/${userId}/self-delete/cancel`,
{ method: 'POST' },
token
);
}
export async function fetchAccountDeletionStatus(userId: string, token?: string | null) {
return apiFetch<AccountDeletionStatus>(`/profile/users/${userId}/self-delete/status`, {}, token);
}
/** @deprecated Используйте requestAccountDeletion — удаление теперь отложенное */
export async function softDeleteProfile(userId: string, token?: string | null) {
return requestAccountDeletion(userId, token);
}
export interface UserAddress {
id: string;
userId: string;
label: 'HOME' | 'WORK' | 'OTHER';
city: string;
street: string;
house: string;
apartment?: string;
comment?: string;
latitude?: number;
longitude?: number;
fullAddress?: string;
createdAt: string;
updatedAt: string;
}
export async function fetchUserAddresses(userId: string, token?: string | null) {
return apiFetch<{ addresses?: UserAddress[] }>(`/addresses/users/${userId}`, {}, token);
}
export async function upsertUserAddress(
userId: string,
body: {
addressId?: string;
label: UserAddress['label'];
city: string;
street: string;
house: string;
apartment?: string;
comment?: string;
latitude?: number;
longitude?: number;
fullAddress?: string;
},
token?: string | null
) {
return apiFetch<UserAddress>(`/addresses/users/${userId}`, { method: 'POST', body: JSON.stringify(body) }, token);
}
export async function deleteUserAddress(userId: string, addressId: string, token?: string | null) {
return apiFetch<{ count: number }>(`/addresses/users/${userId}/${addressId}`, { method: 'DELETE' }, token);
}
export interface FamilyMember {
id: string;
userId: string;
role: string;
displayName: string;
hasAvatar: boolean;
isVerified?: boolean;
verificationIcon?: string | null;
isBot?: boolean;
botUsername?: string;
}
export interface FamilyGroup {
id: string;
ownerId: string;
name: string;
hasAvatar: boolean;
members?: FamilyMember[];
botFatherAvailable?: boolean;
getMyIdBotAvailable?: boolean;
}
export interface BotChatMessage {
id: string;
direction: 'in' | 'out';
text: string;
messageType: string;
messageId: number;
createdAt: string;
replyMarkupJson?: string | null;
isPinned?: boolean;
pinnedAt?: string | null;
}
export interface BotChatMeta {
botId?: string;
botOwnerId?: string;
botUsername?: string;
botDisplayName?: string;
composerWebAppUrl?: string | null;
composerMenuButtonJson?: string | null;
manageWebAppUrl?: string;
}
export interface FamilyInvite {
id: string;
groupId: string;
groupName: string;
inviterId: string;
inviterName: string;
inviteeUserId?: string;
targetEmail?: string;
targetPhone?: string;
status: string;
expiresAt: string;
}
export interface AppNotification {
id: string;
type: string;
title: string;
message: string;
payloadJson?: string;
isRead: boolean;
createdAt: string;
}
export interface ChatPollOption {
id: string;
text: string;
voteCount: number;
votedByMe: boolean;
}
export interface ChatMessageReaction {
emoji: string;
count: number;
reactedByMe: boolean;
}
export interface ChatMessage {
id: string;
roomId: string;
senderId: string;
senderName: string;
senderHasAvatar: boolean;
senderIsVerified?: boolean;
senderVerificationIcon?: string | null;
type: string;
content?: string;
isEncrypted?: boolean;
replyToId?: string;
storageKey?: string;
mimeType?: string;
metadataJson?: string;
createdAt: string;
editedAt?: string;
isDeleted?: boolean;
isPinned?: boolean;
pinnedAt?: string;
pinnedById?: string;
readByAll?: boolean;
poll?: {
id: string;
question: string;
allowsMultiple: boolean;
isAnonymous: boolean;
options: ChatPollOption[];
};
reactions?: ChatMessageReaction[];
}
export interface FamilyPresenceMember {
userId: string;
displayName: string;
online: boolean;
lastSeenAt?: string;
}
export interface FamilyPresence {
members: FamilyPresenceMember[];
onlineCount: number;
}
export interface ChatRoomMember {
id: string;
userId: string;
displayName: string;
hasAvatar: boolean;
isVerified?: boolean;
verificationIcon?: string | null;
notificationsMuted: boolean;
isPinned?: boolean;
pinnedAt?: string;
familyRole?: string;
isChatCreator?: boolean;
}
export interface ChatRoom {
id: string;
groupId: string;
type: string;
name: string;
peerUserId?: string;
botUsername?: string;
isE2E?: boolean;
hasAvatar: boolean;
createdById?: string;
updatedAt: string;
isPinned?: boolean;
pinnedAt?: string;
members: ChatRoomMember[];
lastMessage?: ChatMessage;
}
export async function fetchFamilyGroups(userId: string, token?: string | null) {
return apiFetch<{ groups?: FamilyGroup[] }>(`/family/users/${userId}/groups`, {}, token);
}
export async function fetchFamilyGroup(groupId: string, token?: string | null) {
return apiFetch<FamilyGroup>(`/family/groups/${groupId}`, {}, token);
}
export async function updateFamilyGroup(groupId: string, name: string, token?: string | null) {
return apiFetch<FamilyGroup>(`/family/groups/${groupId}`, { method: 'PATCH', body: JSON.stringify({ name }) }, token);
}
export async function deleteFamilyGroup(groupId: string, token?: string | null) {
return apiFetch<{ count: number }>(`/family/groups/${groupId}`, { method: 'DELETE' }, token);
}
export async function sendFamilyInvite(groupId: string, payload: { inviteeUserId: string; target?: string }, token?: string | null) {
return apiFetch<FamilyInvite>(`/family/groups/${groupId}/invites`, { method: 'POST', body: JSON.stringify(payload) }, token);
}
export async function fetchBotChatMessages(botRef: string, token?: string | null, roomId?: string) {
const suffix = roomId ? `?roomId=${encodeURIComponent(roomId)}` : '';
return apiFetch<{
messages?: BotChatMessage[];
botUsername?: string;
botDisplayName?: string;
botId?: string;
botOwnerId?: string;
composerWebAppUrl?: string;
composerMenuButtonJson?: string;
manageWebAppUrl?: string;
}>(`/bots/by-username/${encodeURIComponent(botRef)}/messages${suffix}`, {}, token);
}
export async function sendBotMessage(botRef: string, text: string, token?: string | null, roomId?: string) {
return apiFetch<{ updateId?: number; messageId?: number; chatId?: string }>(
`/bots/by-username/${encodeURIComponent(botRef)}/messages`,
{ method: 'POST', body: JSON.stringify({ text, roomId }) },
token
);
}
export async function submitBotCallback(botRef: string, messageId: number, callbackData: string, token?: string | null) {
return apiFetch<{ updateId?: number; callbackQueryId?: string; messageId?: number }>(
`/bots/by-username/${encodeURIComponent(botRef)}/callback`,
{ method: 'POST', body: JSON.stringify({ messageId, callbackData }) },
token
);
}
export async function fetchFamilyInvites(token?: string | null) {
return apiFetch<{ invites?: FamilyInvite[] }>('/family/invites', {}, token);
}
export async function respondFamilyInvite(inviteId: string, accept: boolean, token?: string | null) {
return apiFetch<FamilyInvite>(`/family/invites/${inviteId}/respond`, { method: 'POST', body: JSON.stringify({ accept }) }, token);
}
export async function fetchNotifications(token?: string | null, unreadOnly = false) {
return apiFetch<{ notifications?: AppNotification[] }>(`/notifications?unreadOnly=${unreadOnly}`, {}, token);
}
export async function fetchUnreadNotificationCount(token?: string | null) {
return apiFetch<{ count: number }>('/notifications/unread-count', {}, token);
}
export async function markNotificationRead(notificationId: string, token?: string | null) {
return apiFetch(`/notifications/${notificationId}/read`, { method: 'PATCH', body: '{}' }, token);
}
export async function markAllNotificationsRead(token?: string | null) {
return apiFetch('/notifications/read-all', { method: 'POST' }, token);
}
export async function fetchChatRooms(groupId: string, token?: string | null) {
return apiFetch<{ rooms?: ChatRoom[] }>(`/chat/groups/${groupId}/rooms`, {}, token);
}
export async function createChatRoom(groupId: string, name: string, memberUserIds: string[], token?: string | null) {
return apiFetch<ChatRoom>(`/chat/groups/${groupId}/rooms`, {
method: 'POST',
body: JSON.stringify({ name, memberUserIds })
}, token);
}
export async function addChatRoomMember(roomId: string, memberUserId: string, token?: string | null) {
return apiFetch<ChatRoom>(`/chat/rooms/${roomId}/members`, {
method: 'POST',
body: JSON.stringify({ memberUserId })
}, token);
}
export async function removeChatRoomMember(roomId: string, memberUserId: string, token?: string | null) {
return apiFetch<ChatRoom>(`/chat/rooms/${roomId}/members/${memberUserId}`, { method: 'DELETE' }, token);
}
export async function removeFamilyMember(memberId: string, token?: string | null) {
return apiFetch<{ count: number }>(`/family/members/${memberId}`, { method: 'DELETE' }, token);
}
export async function fetchChatMessages(roomId: string, token?: string | null, beforeMessageId?: string) {
const query = beforeMessageId ? `?beforeMessageId=${beforeMessageId}` : '';
return apiFetch<{ messages?: ChatMessage[] }>(`/chat/rooms/${roomId}/messages${query}`, {}, token);
}
export async function sendChatMessage(
roomId: string,
body: {
type: string;
content?: string;
isEncrypted?: boolean;
replyToId?: string;
storageKey?: string;
mimeType?: string;
metadataJson?: string;
poll?: { question: string; options: string[]; allowsMultiple?: boolean; isAnonymous?: boolean };
},
token?: string | null
) {
return apiFetch<ChatMessage>(`/chat/rooms/${roomId}/messages`, { method: 'POST', body: JSON.stringify(body) }, token);
}
export async function fetchUserE2EPublicKey(userId: string, token?: string | null) {
return apiFetch<{ userId?: string; e2ePublicKey?: string }>(`/profile/users/${userId}/e2e-public-key`, {}, token);
}
export async function setUserE2EPublicKey(userId: string, publicKey: string, token?: string | null) {
return apiFetch<{ userId?: string; e2ePublicKey?: string }>(
`/profile/users/${userId}/e2e-public-key`,
{ method: 'PATCH', body: JSON.stringify({ publicKey }) },
token
);
}
export interface ManagedBot {
id: string;
name: string;
username: string;
tokenPrefix?: string;
description?: string | null;
aboutText?: string | null;
botPicUrl?: string | null;
menuButtonJson?: string | null;
webAppUrl?: string | null;
isActive?: boolean;
isSystemBot?: boolean;
ownerId?: string;
createdAt?: string;
updatedAt?: string;
owner?: { id: string; displayName: string; username?: string };
messageCount?: number;
chatCount?: number;
}
export interface AdminBotMetrics {
totalBots?: number;
activeBots?: number;
blockedBots?: number;
totalMessages?: number;
totalChats?: number;
}
export async function fetchAdminBots(
params: { search?: string; page?: number; limit?: number },
token?: string | null
) {
const query = new URLSearchParams();
if (params.search?.trim()) query.set('search', params.search.trim());
if (params.page) query.set('page', String(params.page));
if (params.limit) query.set('limit', String(params.limit));
const suffix = query.toString() ? `?${query.toString()}` : '';
return apiFetch<{ bots?: ManagedBot[]; total?: number }>(`/admin/bots${suffix}`, {}, token);
}
export async function fetchAdminBotMetrics(token?: string | null) {
return apiFetch<AdminBotMetrics>('/admin/bots/metrics', {}, token);
}
export async function fetchAdminBotAccounts(search: string | undefined, token?: string | null) {
const suffix = search?.trim() ? `?search=${encodeURIComponent(search.trim())}` : '';
return apiFetch<{ users: AdminUser[] }>(`/admin/users/bot-accounts${suffix}`, {}, token);
}
export async function fetchUserTotpStatus(userId: string, token?: string | null) {
return apiFetch<TotpStatusResponse>(`/security/users/${userId}/totp/status`, {}, token);
}
export async function adminDisableUserTotp(userId: string, token?: string | null) {
return apiFetch<TotpStatusResponse>(`/admin/users/${userId}/totp/admin-disable`, { method: 'POST' }, token);
}
export async function setAdminBotActive(botId: string, isActive: boolean, token?: string | null) {
return apiFetch<ManagedBot>(`/admin/bots/${botId}/active`, {
method: 'PATCH',
body: JSON.stringify({ isActive })
}, token);
}
export async function createE2ERoom(groupId: string, peerUserId: string, token?: string | null) {
return apiFetch<ChatRoom>(`/chat/groups/${groupId}/e2e-rooms`, {
method: 'POST',
body: JSON.stringify({ peerUserId })
}, token);
}
export async function fetchMyBots(token?: string | null) {
return apiFetch<{ bots?: ManagedBot[]; total?: number }>('/bots', {}, token);
}
export async function createManagedBot(body: { name: string; username: string }, token?: string | null) {
return apiFetch<{ bot?: ManagedBot; token?: string }>('/bots', { method: 'POST', body: JSON.stringify(body) }, token);
}
export async function fetchBot(botId: string, token?: string | null) {
return apiFetch<ManagedBot>(`/bots/${botId}`, {}, token);
}
export async function updateManagedBot(botId: string, body: { name?: string; username?: string }, token?: string | null) {
return apiFetch<ManagedBot>(`/bots/${botId}`, { method: 'PATCH', body: JSON.stringify(body) }, token);
}
export async function updateManagedBotProfile(
botId: string,
body: {
description?: string;
aboutText?: string;
botPicUrl?: string;
menuButtonUrl?: string;
menuButtonText?: string;
menuButtonJson?: string;
},
token?: string | null
) {
return apiFetch<ManagedBot>(`/bots/${botId}/profile`, { method: 'PATCH', body: JSON.stringify(body) }, token);
}
export async function revokeManagedBotToken(botId: string, token?: string | null) {
return apiFetch<{ bot?: ManagedBot; token?: string }>(`/bots/${botId}/revoke-token`, { method: 'POST' }, token);
}
export async function voteChatPoll(messageId: string, optionIds: string[], token?: string | null) {
return apiFetch<ChatMessage>(`/chat/messages/${messageId}/vote`, { method: 'POST', body: JSON.stringify({ optionIds }) }, token);
}
export async function setChatRoomMuted(roomId: string, muted: boolean, token?: string | null) {
return apiFetch(`/chat/rooms/${roomId}/mute`, { method: 'POST', body: JSON.stringify({ muted }) }, token);
}
export async function setChatRoomPinned(roomId: string, pinned: boolean, token?: string | null) {
return apiFetch<ChatRoom>(`/chat/rooms/${roomId}`, {
method: 'PATCH',
body: JSON.stringify({ pinned })
}, token);
}
export async function editChatMessage(messageId: string, content: string, token?: string | null) {
return apiFetch<ChatMessage>(`/chat/messages/${messageId}`, { method: 'PATCH', body: JSON.stringify({ content }) }, token);
}
export async function deleteChatMessage(messageId: string, token?: string | null) {
return apiFetch<ChatMessage>(`/chat/messages/${messageId}`, { method: 'DELETE' }, token);
}
export async function toggleChatMessageReaction(messageId: string, emoji: string, token?: string | null) {
return apiFetch<ChatMessage>(`/chat/messages/${messageId}/reactions`, {
method: 'POST',
body: JSON.stringify({ emoji })
}, token);
}
export async function setChatMessagePinned(messageId: string, pinned: boolean, token?: string | null) {
return apiFetch<ChatMessage>(`/chat/messages/${messageId}/pin`, {
method: 'POST',
body: JSON.stringify({ pinned })
}, token);
}
export async function forwardChatMessages(targetRoomId: string, messageIds: string[], token?: string | null) {
return apiFetch<{ messages?: ChatMessage[] }>(`/chat/rooms/${targetRoomId}/forward`, {
method: 'POST',
body: JSON.stringify({ messageIds })
}, token);
}
export async function deleteChatRoom(roomId: string, token?: string | null) {
return apiFetch<{ count?: number }>(`/chat/rooms/${roomId}`, { method: 'DELETE' }, token);
}
export async function markChatRoomRead(roomId: string, lastMessageId: string | undefined, token?: string | null) {
return apiFetch<{ count: number }>(`/chat/rooms/${roomId}/read`, {
method: 'POST',
body: JSON.stringify({ lastMessageId })
}, token);
}
export async function reportChatTyping(roomId: string, token?: string | null) {
return apiFetch<{ success?: boolean }>(`/chat/rooms/${roomId}/typing`, { method: 'POST' }, token);
}
export async function fetchFamilyPresence(groupId: string, token?: string | null) {
return apiFetch<FamilyPresence>(`/family/groups/${groupId}/presence`, {}, token);
}
/** @deprecated Используйте getWsUrl() — URL может переключаться на same-origin /idp-api/ws в браузере */
export const WS_URL = configuredWsUrl;