global fix and add tauri app

This commit is contained in:
lendry
2026-06-26 13:56:54 +03:00
parent aa228d84eb
commit 3b05b7e4d4
50 changed files with 3947 additions and 80 deletions

View File

@@ -0,0 +1,75 @@
import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from 'react';
import type { AuthTokens, PublicUser } from '@/lib/api';
import {
clearMobileAuth,
loginWithPassword,
persistMobileAuth,
readStoredMobileAuth
} from './mobile-api';
interface MobileAuthContextValue {
user: PublicUser | null;
token: string | null;
sessionId: string | null;
isAuthenticated: boolean;
login: (login: string, password: string) => Promise<void>;
applyAuth: (auth: AuthTokens) => void;
logout: () => void;
}
const MobileAuthContext = createContext<MobileAuthContextValue | null>(null);
export function MobileAuthProvider({ children }: { children: ReactNode }) {
const stored = typeof window === 'undefined' ? { token: null, sessionId: null, user: null } : readStoredMobileAuth();
const [token, setToken] = useState<string | null>(stored.token);
const [sessionId, setSessionId] = useState<string | null>(stored.sessionId);
const [user, setUser] = useState<PublicUser | null>(stored.user);
const applyAuth = useCallback((auth: AuthTokens) => {
persistMobileAuth(auth);
setToken(auth.accessToken);
setSessionId(auth.sessionId);
setUser(auth.user);
}, []);
const login = useCallback(
async (loginValue: string, password: string) => {
const auth = await loginWithPassword(loginValue, password);
if ('requiresTotp' in auth) {
throw new Error('Для этого аккаунта требуется TOTP-код');
}
applyAuth(auth);
},
[applyAuth]
);
const logout = useCallback(() => {
clearMobileAuth();
setToken(null);
setSessionId(null);
setUser(null);
}, []);
const value = useMemo(
() => ({
user,
token,
sessionId,
isAuthenticated: Boolean(user && token),
login,
applyAuth,
logout
}),
[applyAuth, login, logout, sessionId, token, user]
);
return <MobileAuthContext.Provider value={value}>{children}</MobileAuthContext.Provider>;
}
export function useMobileAuth() {
const value = useContext(MobileAuthContext);
if (!value) {
throw new Error('useMobileAuth должен использоваться внутри MobileAuthProvider');
}
return value;
}

View File

@@ -0,0 +1,6 @@
export function getErrorMessage(error: unknown, fallback: string) {
if (error instanceof Error && error.message.trim()) {
return error.message;
}
return fallback;
}

View File

@@ -0,0 +1,138 @@
import type { ActiveSession, AuthTokens, PublicUser } from '@/lib/api';
const DEFAULT_API_URL = 'http://localhost:3000';
export const MOBILE_AUTH_TOKEN_KEY = 'lendry_mobile_access_token';
export const MOBILE_REFRESH_TOKEN_KEY = 'lendry_mobile_refresh_token';
export const MOBILE_SESSION_ID_KEY = 'lendry_mobile_session_id';
export const MOBILE_USER_KEY = 'lendry_mobile_user';
export const MOBILE_API_URL_KEY = 'lendry_mobile_api_url';
export class MobileApiError extends Error {
constructor(
message: string,
readonly status: number,
readonly code?: string
) {
super(message);
this.name = 'MobileApiError';
}
}
export function getMobileApiUrl() {
return (window.localStorage.getItem(MOBILE_API_URL_KEY) ?? import.meta.env.VITE_API_URL ?? DEFAULT_API_URL).replace(/\/$/, '');
}
export function setMobileApiUrl(url: string) {
window.localStorage.setItem(MOBILE_API_URL_KEY, url.replace(/\/$/, ''));
}
export function readStoredMobileAuth() {
const token = window.localStorage.getItem(MOBILE_AUTH_TOKEN_KEY);
const refreshToken = window.localStorage.getItem(MOBILE_REFRESH_TOKEN_KEY);
const sessionId = window.localStorage.getItem(MOBILE_SESSION_ID_KEY);
const rawUser = window.localStorage.getItem(MOBILE_USER_KEY);
let user: PublicUser | null = null;
if (rawUser) {
try {
user = JSON.parse(rawUser) as PublicUser;
} catch {
user = null;
}
}
return { token, refreshToken, sessionId, user };
}
export function persistMobileAuth(auth: AuthTokens) {
window.localStorage.setItem(MOBILE_AUTH_TOKEN_KEY, auth.accessToken);
window.localStorage.setItem(MOBILE_REFRESH_TOKEN_KEY, auth.refreshToken);
window.localStorage.setItem(MOBILE_SESSION_ID_KEY, auth.sessionId);
window.localStorage.setItem(MOBILE_USER_KEY, JSON.stringify(auth.user));
}
export function clearMobileAuth() {
window.localStorage.removeItem(MOBILE_AUTH_TOKEN_KEY);
window.localStorage.removeItem(MOBILE_REFRESH_TOKEN_KEY);
window.localStorage.removeItem(MOBILE_SESSION_ID_KEY);
window.localStorage.removeItem(MOBILE_USER_KEY);
}
async function parseError(response: Response) {
try {
const body = (await response.json()) as { message?: string | string[]; error?: string; code?: string };
const message = Array.isArray(body.message) ? body.message.join('\n') : body.message ?? body.error ?? 'Ошибка запроса';
return new MobileApiError(message, response.status, body.code);
} catch {
return new MobileApiError('Ошибка запроса', response.status);
}
}
export async function mobileFetch<T>(path: string, options: RequestInit = {}, token?: string | null): Promise<T> {
const response = await fetch(`${getMobileApiUrl()}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
...options.headers
}
});
if (!response.ok) {
throw await parseError(response);
}
return response.json() as Promise<T>;
}
export interface TotpChallengeResponse {
totpChallengeToken: string;
}
export type PasswordLoginResponse = AuthTokens | {
requiresTotp: true;
totpChallengeToken: string;
};
export async function loginWithPassword(login: string, password: string) {
return mobileFetch<PasswordLoginResponse>('/auth/login/password', {
method: 'POST',
body: JSON.stringify({
login,
password,
fingerprint: crypto.randomUUID(),
deviceName: 'Lendry ID Mobile',
deviceType: 'MOBILE'
})
});
}
export async function beginTotpLogin(recipient: string) {
return mobileFetch<TotpChallengeResponse>('/auth/totp/begin', {
method: 'POST',
body: JSON.stringify({
recipient,
fingerprint: crypto.randomUUID(),
deviceName: 'Lendry ID Mobile',
deviceType: 'MOBILE'
})
});
}
export async function verifyTotpLogin(totpChallengeToken: string, code: string) {
return mobileFetch<AuthTokens>('/auth/totp/verify', {
method: 'POST',
body: JSON.stringify({ totpChallengeToken, code })
});
}
export async function approveQrLogin(sessionId: string, token: string) {
return mobileFetch(`/auth/advanced/qr/session/${encodeURIComponent(sessionId)}/approve`, { method: 'POST' }, token);
}
export async function fetchActiveSessions(userId: string, token: string) {
return mobileFetch<{ sessions?: ActiveSession[] }>(`/security/users/${encodeURIComponent(userId)}/sessions`, {}, token);
}
export async function revokeSession(userId: string, sessionId: string, token: string) {
return mobileFetch(`/security/users/${encodeURIComponent(userId)}/sessions/${encodeURIComponent(sessionId)}/revoke`, { method: 'POST' }, token);
}