feat(frontend): Экран авторизации
This commit is contained in:
162
apps/frontend/lib/api.ts
Normal file
162
apps/frontend/lib/api.ts
Normal file
@ -0,0 +1,162 @@
|
||||
import { API_BASE_URL } from '@/config/api';
|
||||
import type {
|
||||
SaveListItem,
|
||||
SaveDetailResponse,
|
||||
CreateSaveFromUrlRequest,
|
||||
UpdateSaveRequest,
|
||||
SaveResponse,
|
||||
User
|
||||
} from '@shared-types';
|
||||
|
||||
// Получить токен из сессии Better Auth
|
||||
async function getAuthToken(): Promise<string | null> {
|
||||
// Better Auth хранит токен в cookies, но для React Native нужно использовать другой подход
|
||||
// Временно возвращаем null, токен будет передаваться через headers в Better Auth клиенте
|
||||
return null;
|
||||
}
|
||||
|
||||
async function apiRequest<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const token = await getAuthToken();
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers as Record<string, string> || {}),
|
||||
};
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||
...options,
|
||||
headers,
|
||||
credentials: 'include', // Для cookies Better Auth
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
||||
throw new Error(error.error || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// API для работы с сейвами
|
||||
export const savesApi = {
|
||||
// Получить все сейвы текущего пользователя
|
||||
async getMySaves(): Promise<SaveListItem[]> {
|
||||
return apiRequest<SaveListItem[]>('/saves/my');
|
||||
},
|
||||
|
||||
// Получить сейв по ID
|
||||
async getSaveById(id: number, shareToken?: string): Promise<SaveDetailResponse> {
|
||||
const url = shareToken
|
||||
? `/saves/${id}?share=${shareToken}`
|
||||
: `/saves/${id}`;
|
||||
return apiRequest<SaveDetailResponse>(url);
|
||||
},
|
||||
|
||||
// Создать сейв из URL
|
||||
async createFromUrl(data: CreateSaveFromUrlRequest): Promise<SaveResponse> {
|
||||
return apiRequest<SaveResponse>('/saves/external', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
},
|
||||
|
||||
// Загрузить файл
|
||||
async uploadFile(
|
||||
file: File | { uri: string; type: string; name: string },
|
||||
metadata?: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
tags?: string[];
|
||||
visibility?: 'public' | 'link';
|
||||
}
|
||||
): Promise<SaveResponse> {
|
||||
const formData = new FormData();
|
||||
|
||||
// Для React Native используем другой формат
|
||||
if ('uri' in file) {
|
||||
// React Native
|
||||
formData.append('file', {
|
||||
uri: file.uri,
|
||||
type: file.type,
|
||||
name: file.name,
|
||||
} as any);
|
||||
} else {
|
||||
// Web
|
||||
formData.append('file', file);
|
||||
}
|
||||
|
||||
if (metadata?.name) formData.append('name', metadata.name);
|
||||
if (metadata?.description) formData.append('description', metadata.description);
|
||||
if (metadata?.tags) {
|
||||
metadata.tags.forEach(tag => formData.append('tags[]', tag));
|
||||
}
|
||||
if (metadata?.visibility) formData.append('visibility', metadata.visibility);
|
||||
|
||||
const token = await getAuthToken();
|
||||
const headers: Record<string, string> = {};
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
// Не устанавливаем Content-Type для FormData - браузер/платформа сделает это автоматически
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/saves/upload`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: formData,
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
||||
throw new Error(error.error || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
|
||||
// Обновить сейв
|
||||
async updateSave(id: number, data: UpdateSaveRequest): Promise<SaveDetailResponse> {
|
||||
return apiRequest<SaveDetailResponse>(`/saves/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
},
|
||||
|
||||
// Удалить сейв
|
||||
async deleteSave(id: number): Promise<{ success: boolean; message: string }> {
|
||||
return apiRequest<{ success: boolean; message: string }>(`/saves/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
},
|
||||
|
||||
// Получить URL для скачивания
|
||||
getDownloadUrl(id: number, shareToken?: string): string {
|
||||
const baseUrl = `${API_BASE_URL}/saves/${id}/download`;
|
||||
return shareToken ? `${baseUrl}?share=${shareToken}` : baseUrl;
|
||||
},
|
||||
|
||||
// Получить публичные сейвы пользователя по slug (userId)
|
||||
async getPublicSavesByUser(slug: string): Promise<SaveListItem[]> {
|
||||
return apiRequest<SaveListItem[]>(`/saves/u/${slug}`);
|
||||
},
|
||||
};
|
||||
|
||||
// API для работы с пользователями
|
||||
export const usersApi = {
|
||||
// Получить пользователя по имени
|
||||
async getUserByName(name: string): Promise<User> {
|
||||
return apiRequest<User>(`/users/by-name?name=${encodeURIComponent(name)}`);
|
||||
},
|
||||
|
||||
// Получить пользователя по ID
|
||||
async getUserById(id: string): Promise<User> {
|
||||
return apiRequest<User>(`/users/${id}`);
|
||||
},
|
||||
};
|
||||
144
apps/frontend/lib/auth.tsx
Normal file
144
apps/frontend/lib/auth.tsx
Normal file
@ -0,0 +1,144 @@
|
||||
import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
||||
import { createAuthClient } from 'better-auth/react';
|
||||
import { AUTH_BASE_URL } from '@/config/api';
|
||||
import type { User } from '@shared-types';
|
||||
|
||||
// Создаем клиент Better Auth
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: AUTH_BASE_URL,
|
||||
});
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
session: any | null;
|
||||
loading: boolean;
|
||||
signIn: (email: string, password: string) => Promise<void>;
|
||||
signUp: (email: string, password: string, name: string) => Promise<void>;
|
||||
signOut: () => Promise<void>;
|
||||
refresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [session, setSession] = useState<any | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const refresh = async () => {
|
||||
try {
|
||||
const sessionData = await authClient.getSession();
|
||||
setSession(sessionData.data?.session || null);
|
||||
const userData = sessionData.data?.user;
|
||||
if (userData) {
|
||||
setUser({
|
||||
...userData,
|
||||
image: userData.image ?? null,
|
||||
} as User);
|
||||
} else {
|
||||
setUser(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения сессии:', error);
|
||||
setSession(null);
|
||||
setUser(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, []);
|
||||
|
||||
const signIn = async (email: string, password: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await authClient.signIn.email({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (result.data?.user) {
|
||||
const userData = result.data.user;
|
||||
setUser({
|
||||
...userData,
|
||||
image: userData.image ?? null,
|
||||
} as User);
|
||||
setSession(result.data as any);
|
||||
} else {
|
||||
throw new Error('Ошибка входа');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка входа:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const signUp = async (email: string, password: string, name: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await authClient.signUp.email({
|
||||
email,
|
||||
password,
|
||||
name,
|
||||
});
|
||||
|
||||
if (result.data?.user) {
|
||||
const userData = result.data.user;
|
||||
setUser({
|
||||
...userData,
|
||||
image: userData.image ?? null,
|
||||
} as User);
|
||||
setSession(result.data as any);
|
||||
} else {
|
||||
throw new Error('Ошибка регистрации');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка регистрации:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const signOut = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await authClient.signOut();
|
||||
setUser(null);
|
||||
setSession(null);
|
||||
} catch (error) {
|
||||
console.error('Ошибка выхода:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
user,
|
||||
session,
|
||||
loading,
|
||||
signIn,
|
||||
signUp,
|
||||
signOut,
|
||||
refresh,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
Reference in New Issue
Block a user