global fix and add tauri app
This commit is contained in:
75
tauri_app/src/lib/auth.tsx
Normal file
75
tauri_app/src/lib/auth.tsx
Normal 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;
|
||||
}
|
||||
6
tauri_app/src/lib/errors.ts
Normal file
6
tauri_app/src/lib/errors.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export function getErrorMessage(error: unknown, fallback: string) {
|
||||
if (error instanceof Error && error.message.trim()) {
|
||||
return error.message;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
138
tauri_app/src/lib/mobile-api.ts
Normal file
138
tauri_app/src/lib/mobile-api.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user