test fix authorization

This commit is contained in:
2025-12-16 17:09:51 +05:00
parent c15c36891e
commit 70ec57d6fb
4 changed files with 277 additions and 145 deletions

View File

@ -27,6 +27,7 @@ import DailyQuests from './pages/DailyQuests';
import Settings from './pages/Settings';
import Inventory from './pages/Inventory';
import { TrayBridge } from './utils/TrayBridge';
import { API_BASE_URL } from './api';
const AuthCheck = ({ children }: { children: ReactNode }) => {
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
@ -35,18 +36,53 @@ const AuthCheck = ({ children }: { children: ReactNode }) => {
const checkAuth = async () => {
try {
const savedConfig = localStorage.getItem('launcher_config');
if (savedConfig) {
const config = JSON.parse(savedConfig);
if (config.accessToken) {
// Можно добавить дополнительную проверку токена
const isValid = await validateToken(
config.accessToken,
config.clientToken,
);
setIsAuthenticated(isValid);
return;
}
if (!savedConfig) {
setIsAuthenticated(false);
return;
}
const config = JSON.parse(savedConfig);
if (config.accessToken && config.clientToken) {
// 1. Проверяем валидность токена через ваш API
const response = await fetch(`${API_BASE_URL}/auth/validate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
accessToken: config.accessToken,
clientToken: config.clientToken,
}),
});
const isValid = response.ok;
// 2. ДОПОЛНИТЕЛЬНО: проверяем доступ к API через /auth/me
if (isValid) {
try {
const meResponse = await fetch(
`${API_BASE_URL}/auth/me?${new URLSearchParams({
accessToken: config.accessToken,
clientToken: config.clientToken,
})}`,
);
if (!meResponse.ok) {
// Токен валиден для Yggdrasil, но нет доступа к API
console.warn('Токен валиден, но нет доступа к API лаунчера');
setIsAuthenticated(false);
return;
}
} catch (error) {
console.error('Ошибка проверки доступа к API:', error);
setIsAuthenticated(false);
return;
}
}
setIsAuthenticated(isValid);
return;
}
setIsAuthenticated(false);
} catch (error) {
console.error('Ошибка проверки авторизации:', error);
@ -95,7 +131,7 @@ const AuthCheck = ({ children }: { children: ReactNode }) => {
};
if (isAuthenticated === null) {
return <FullScreenLoader message="Загрузка..." />;
return <FullScreenLoader message="Проверка авторизации..." />;
}
return isAuthenticated ? children : <Navigate to="/login" replace />;
@ -144,8 +180,14 @@ const AppLayout = () => {
const settings = JSON.parse(raw);
document.body.classList.toggle('reduce-motion', Boolean(settings.reduceMotion));
document.body.classList.toggle('no-blur', settings.blurEffects === false);
document.body.classList.toggle(
'reduce-motion',
Boolean(settings.reduceMotion),
);
document.body.classList.toggle(
'no-blur',
settings.blurEffects === false,
);
const ui = document.getElementById('app-ui');
if (ui && typeof settings.uiScale === 'number') {
@ -186,7 +228,10 @@ const AppLayout = () => {
return () => {
window.removeEventListener('settings-updated', applySettings);
window.removeEventListener('settings-updated', pushLauncherSettingsToMain);
window.removeEventListener(
'settings-updated',
pushLauncherSettingsToMain,
);
};
}, []);
@ -215,14 +260,23 @@ const AppLayout = () => {
}, []);
return (
<Box sx={{ width: '100vw', height: '100vh', position: 'relative', overflow: 'hidden' }}>
<Box
sx={{
width: '100vw',
height: '100vh',
position: 'relative',
overflow: 'hidden',
}}
>
{/* ФОН — НЕ масштабируется */}
<Box sx={{ position: 'fixed', inset: 0, zIndex: 0, pointerEvents: 'none', }}>
<Box
sx={{ position: 'fixed', inset: 0, zIndex: 0, pointerEvents: 'none' }}
>
<MinecraftBackground />
</Box>
{/* UI — масштабируется */}
<Box
<Box
id="app-scroll"
sx={{
position: 'relative',
@ -344,7 +398,7 @@ const AppLayout = () => {
</AuthCheck>
}
/>
</Routes>
</Routes>
</Box>
</Box>
</Box>

View File

@ -1,5 +1,95 @@
export const API_BASE_URL = 'https://minecraft.api.popa-popa.ru';
// АВТОРИЗАЦИЯ \\
export interface AuthSession {
accessToken: string;
clientToken: string;
selectedProfile: {
id: string; // UUID пользователя
name: string; // Имя игрока
};
user?: {
id: string;
properties?: Array<{
name: string;
value: string;
}>;
};
}
export async function authenticate(
username: string,
password: string,
): Promise<AuthSession> {
const clientToken = Math.random().toString(36).substring(2);
const response = await fetch(`${API_BASE_URL}/auth/authenticate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username,
password,
clientToken,
requestUser: true,
}),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Ошибка авторизации: ${errorText}`);
}
return await response.json();
}
// Валидация токена
export async function validateToken(
accessToken: string,
clientToken: string,
): Promise<boolean> {
const response = await fetch(`${API_BASE_URL}/auth/validate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accessToken, clientToken }),
});
return response.ok;
}
// Обновление токена
export async function refreshToken(
accessToken: string,
clientToken: string,
): Promise<AuthSession | null> {
const response = await fetch(`${API_BASE_URL}/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accessToken, clientToken }),
});
if (!response.ok) return null;
return await response.json();
}
// Получение информации о себе (проверяет, что токен валидный И есть доступ к API)
export async function getMe(): Promise<MeResponse> {
const { accessToken, clientToken } = getAuthTokens();
if (!accessToken || !clientToken) {
throw new Error('Нет токенов авторизации');
}
const params = new URLSearchParams({ accessToken, clientToken });
const response = await fetch(`${API_BASE_URL}/auth/me?${params.toString()}`);
if (!response.ok) {
throw new Error('Не удалось получить данные пользователя');
}
return await response.json();
}
export interface Player {
uuid: string;
username: string;
@ -604,7 +694,9 @@ export interface DailyDaysResponse {
count: number;
}
export async function fetchDailyClaimDays(limit = 120): Promise<DailyDaysResponse> {
export async function fetchDailyClaimDays(
limit = 120,
): Promise<DailyDaysResponse> {
const { accessToken, clientToken } = getAuthTokens();
if (!accessToken || !clientToken) {
@ -695,14 +787,20 @@ export async function fetchDailyQuestsStatus(): Promise<DailyQuestsStatusRespons
return await response.json();
}
export async function claimDailyQuest(questKey: string): Promise<DailyQuestClaimResponse> {
export async function claimDailyQuest(
questKey: string,
): Promise<DailyQuestClaimResponse> {
const { accessToken, clientToken } = getAuthTokens();
if (!accessToken || !clientToken) {
throw new Error('Нет токенов авторизации');
}
const params = new URLSearchParams({ accessToken, clientToken, quest_key: questKey });
const params = new URLSearchParams({
accessToken,
clientToken,
quest_key: questKey,
});
const response = await fetch(
`${API_BASE_URL}/users/daily-quests/claim?${params.toString()}`,
{ method: 'POST' },

View File

@ -1,83 +1,45 @@
import { useState } from 'react';
interface AuthSession {
accessToken: string;
clientToken: string;
selectedProfile: {
id: string;
name: string;
};
}
import {
authenticate,
validateToken,
refreshToken,
type AuthSession,
} from '../api';
export default function useAuth() {
const [status, setStatus] = useState('idle');
const [status, setStatus] = useState<
'idle' | 'validating' | 'refreshing' | 'authenticating' | 'error'
>('idle');
// Проверка валидности токена
const validateSession = async (accessToken: string): Promise<boolean> => {
try {
setStatus('validating');
const response = await window.electron.ipcRenderer.invoke(
'validate-token',
accessToken,
);
setStatus('idle');
return response.valid;
} catch (error) {
console.error('Ошибка при валидации токена:', error);
setStatus('error');
return false;
}
};
// Обновление токена
const refreshSession = async (
accessToken: string,
clientToken: string,
): Promise<AuthSession | null> => {
try {
setStatus('refreshing');
const response = await window.electron.ipcRenderer.invoke(
'refresh-token',
{ accessToken, clientToken },
);
setStatus('idle');
return response;
} catch (error) {
console.error('Ошибка при обновлении токена:', error);
setStatus('error');
return null;
}
};
// Аутентификация в Ely.by
const authenticateWithElyBy = async (
// Аутентификация (HTTP напрямую, без IPC!)
const authenticateUser = async (
username: string,
password: string,
saveConfigFunc: Function,
saveConfigFunc: (config: any) => void,
): Promise<AuthSession | null> => {
try {
setStatus('authenticating');
const response = await window.electron.ipcRenderer.invoke(
'authenticate',
{ username, password },
);
if (response && response.accessToken) {
// Правильно сохраняем данные в конфигурации
saveConfigFunc({
username: response.selectedProfile.name, // Имя игрока как строка
uuid: response.selectedProfile.id,
accessToken: response.accessToken,
clientToken: response.clientToken,
memory: 4096, // Сохраняем значение по умолчанию или из предыдущей конфигурации
});
// Прямой HTTP-запрос к вашему серверу
const session = await authenticate(username, password);
setStatus('authenticated');
return response;
}
// Сохраняем в конфигурацию
saveConfigFunc({
username: session.selectedProfile.name,
uuid: session.selectedProfile.id,
accessToken: session.accessToken,
clientToken: session.clientToken,
memory: 4096,
});
setStatus('error');
return null;
// Уведомляем Electron (для запуска Minecraft)
await window.electron.ipcRenderer.invoke('auth-changed', {
isAuthed: true,
minecraftSession: session, // Передаём всю сессию
});
setStatus('authenticating');
return session;
} catch (error) {
console.error('Ошибка при аутентификации:', error);
setStatus('error');
@ -85,10 +47,53 @@ export default function useAuth() {
}
};
// Валидация токена (HTTP напрямую)
const validateSession = async (accessToken: string): Promise<boolean> => {
try {
setStatus('validating');
// Получаем clientToken из localStorage
const savedConfig = localStorage.getItem('launcher_config');
if (!savedConfig) return false;
const config = JSON.parse(savedConfig);
// Прямой HTTP-запрос на валидацию
const isValid = await validateToken(accessToken, config.clientToken);
setStatus('idle');
return isValid;
} catch (error) {
console.error('Ошибка при валидации токена:', error);
setStatus('error');
return false;
}
};
// Обновление токена (HTTP напрямую)
const refreshSession = async (
accessToken: string,
clientToken: string,
): Promise<AuthSession | null> => {
try {
setStatus('refreshing');
// Прямой HTTP-запрос на обновление
const session = await refreshToken(accessToken, clientToken);
setStatus('idle');
return session;
} catch (error) {
console.error('Ошибка при обновлении токена:', error);
setStatus('error');
return null;
}
};
return {
status,
authenticateUser,
validateSession,
refreshSession,
authenticateWithElyBy,
};
}

View File

@ -15,91 +15,66 @@ interface LoginProps {
const Login = ({ onLoginSuccess }: LoginProps) => {
const navigate = useNavigate();
const { config, setConfig, saveConfig, handleInputChange } = useConfig();
const { status, validateSession, refreshSession, authenticateWithElyBy } =
useAuth();
const auth = useAuth();
const [loading, setLoading] = useState(false);
const authorization = async () => {
console.log('Начинаем процесс авторизации...');
const handleLogin = async () => {
if (!config.username.trim()) {
console.log('Ошибка: не указан никнейм');
alert('Введите никнейм!');
return;
}
if (!config.password) {
alert('Введите пароль!');
return;
}
setLoading(true);
try {
// Проверяем, есть ли сохранённый токен
// Проверяем существующий токен
if (config.accessToken && config.clientToken) {
console.log('Проверка валидности существующего токена...');
const isValid = await validateSession(config.accessToken);
const isValid = await auth.validateSession(config.accessToken);
if (!isValid) {
console.log('Токен недействителен, пытаемся обновить...');
const refreshedSession = await refreshSession(
// Пробуем обновить
const refreshed = await auth.refreshSession(
config.accessToken,
config.clientToken,
);
if (!refreshedSession) {
console.log(
'Не удалось обновить токен, требуется новая авторизация',
if (!refreshed) {
// Нужна новая авторизация
const session = await auth.authenticateUser(
config.username,
config.password,
saveConfig,
);
// Очищаем недействительные токены
saveConfig({
accessToken: '',
clientToken: '',
});
// Пытаемся выполнить новую авторизацию
if (config.password) {
const newSession = await authenticateWithElyBy(
config.username,
config.password,
saveConfig,
);
if (!newSession) {
console.log('Авторизация не удалась');
return;
}
} else {
console.log('Требуется ввод пароля для новой авторизации');
return;
}
if (!session) throw new Error('Авторизация не удалась');
}
} else {
console.log('Токен действителен');
}
} else {
console.log('Токен отсутствует, выполняем авторизацию...');
// Проверяем наличие пароля
if (!config.password) {
console.log('Ошибка: не указан пароль');
alert('Введите пароль!');
return;
}
const session = await authenticateWithElyBy(
// Новая авторизация
const session = await auth.authenticateUser(
config.username,
config.password,
saveConfig,
);
if (!session) {
console.log('Авторизация не удалась');
return;
}
if (!session) throw new Error('Авторизация не удалась');
}
console.log('Авторизация успешно завершена');
// Успех
if (onLoginSuccess) {
onLoginSuccess(config.username);
}
await window.electron.ipcRenderer.invoke('auth-changed', { isAuthed: true });
navigate('/');
} catch (error: any) {
console.log(`ОШИБКА при авторизации: ${error.message}`);
console.error('Ошибка авторизации:', error);
alert(`Ошибка: ${error.message}`);
// Очищаем невалидные токены
saveConfig({
accessToken: '',
clientToken: '',
@ -119,7 +94,7 @@ const Login = ({ onLoginSuccess }: LoginProps) => {
<AuthForm
config={config}
handleInputChange={handleInputChange}
onLogin={authorization}
onLogin={handleLogin}
/>
</>
)}