Archived
1
0

feat(frontend): Экран авторизации

This commit is contained in:
2025-11-19 17:18:19 +03:00
parent 5f60d12996
commit 20556231b9
26 changed files with 1177 additions and 183 deletions

162
apps/frontend/lib/api.ts Normal file
View 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
View 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;
}