Попытка добавления проходок, аккаунтов, а так же доработка прокси

This commit is contained in:
Sashegdev
2026-04-07 17:50:29 +00:00
parent c03d7a788f
commit 9bee361ea4
11 changed files with 1198 additions and 219 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>me.sashegdev</groupId>
<artifactId>ZernMCLauncher</artifactId>
<version>1.0.5</version>
<version>1.0.6</version>
<build>
<plugins>
<plugin>
@@ -1,5 +1,6 @@
package me.sashegdev.zernmc.launcher;
import me.sashegdev.zernmc.launcher.auth.AuthManager;
import me.sashegdev.zernmc.launcher.menu.*;
import me.sashegdev.zernmc.launcher.ui.ArrowMenu;
import me.sashegdev.zernmc.launcher.utils.*;
@@ -17,8 +18,12 @@ public class Main {
private static final String CURRENT_VERSION = Version.getCurrentVersion();
public static void main(String[] args) {
public static void main(String[] args) throws IOException {
System.setProperty("org.jline.terminal.disableDeprecatedProviderWarning", "true");
System.setProperty("file.encoding", "UTF-8");
System.setProperty("sun.err.encoding", "UTF-8");
System.setProperty("sun.stdout.encoding", "UTF-8");
java.nio.charset.Charset.defaultCharset();
ZAnsi.install();
System.out.print("\033[H\033[2J");
@@ -29,6 +34,23 @@ public class Main {
checkAndAutoUpdateLauncher();
// === АВТОРИЗАЦИЯ ===
System.out.println(ZAnsi.cyan("Проверка авторизации..."));
boolean sessionRestored = AuthManager.loadSavedSession();
if (!sessionRestored) {
LoginMenu loginMenu = new LoginMenu();
boolean loggedIn = loginMenu.show();
if (!loggedIn) {
System.out.println(ZAnsi.yellow("До свидания!"));
ZAnsi.uninstall();
System.exit(0);
}
} else {
System.out.println(ZAnsi.brightGreen("Добро пожаловать обратно, " + AuthManager.getUsername() + "!"));
}
// === КОНЕЦ АВТОРИЗАЦИИ ===
try {
mainLoop();
} catch (Exception e) {
@@ -0,0 +1,206 @@
package me.sashegdev.zernmc.launcher.auth;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.SerializedName;
import me.sashegdev.zernmc.launcher.utils.Config;
import me.sashegdev.zernmc.launcher.utils.ZAnsi;
import me.sashegdev.zernmc.launcher.utils.ZHttpClient;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
public class AuthManager {
private static final Path AUTH_FILE = Config.getConfigDir().resolve("auth.json");
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final HttpClient HTTP = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(15))
.build();
private static AuthSession session = null;
public static boolean loadSavedSession() {
if (!Files.exists(AUTH_FILE)) return false;
try {
String json = Files.readString(AUTH_FILE);
AuthSession loaded = GSON.fromJson(json, AuthSession.class);
if (loaded == null || loaded.accessToken == null) return false;
session = loaded;
if (isAccessTokenExpired()) {
return tryRefresh();
}
return true;
} catch (Exception e) {
return false;
}
}
public static AuthResult login(String username, String password) {
return authRequest("/auth/login", username, password);
}
public static AuthResult register(String username, String password) {
return authRequest("/auth/register", username, password);
}
private static AuthResult authRequest(String endpoint, String username, String password) {
try {
String body = GSON.toJson(new LoginRequest(username, password));
HttpResponse<String> resp = post(endpoint, body);
if (resp.statusCode() == 200) {
session = GSON.fromJson(resp.body(), AuthSession.class);
session.expiresAt = System.currentTimeMillis() / 1000L + session.expiresIn;
saveSession();
return AuthResult.ok();
} else {
return AuthResult.fail(extractError(resp.body()));
}
} catch (Exception e) {
return AuthResult.fail("Ошибка соединения: " + e.getMessage());
}
}
public static void logout() {
if (session != null && session.refreshToken != null) {
try { post("/auth/logout", "{\"refresh_token\":\"" + session.refreshToken + "\"}"); }
catch (Exception ignored) {}
}
session = null;
try { Files.deleteIfExists(AUTH_FILE); } catch (Exception ignored) {}
}
public static boolean isLoggedIn() {
return session != null && session.accessToken != null;
}
public static String getUsername() {
return session != null ? session.username : "Player";
}
public static String getUuid() {
return session != null ? session.uuid : "00000000-0000-0000-0000-000000000000";
}
public static String getAccessToken() {
if (session == null) return "0";
if (isAccessTokenExpired()) tryRefresh();
return session.accessToken != null ? session.accessToken : "0";
}
private static boolean isAccessTokenExpired() {
if (session == null) return true;
return System.currentTimeMillis() / 1000L >= session.expiresAt - 300;
}
private static boolean tryRefresh() {
if (session == null || session.refreshToken == null) return false;
try {
String body = "{\"refresh_token\":\"" + session.refreshToken + "\"}";
HttpResponse<String> resp = post("/auth/refresh", body);
if (resp.statusCode() == 200) {
AuthSession newSession = GSON.fromJson(resp.body(), AuthSession.class);
newSession.expiresAt = System.currentTimeMillis() / 1000L + newSession.expiresIn;
session = newSession;
saveSession();
return true;
}
} catch (Exception ignored) {}
session = null;
try { Files.deleteIfExists(AUTH_FILE); } catch (Exception ignored) {}
return false;
}
private static void saveSession() {
try {
Files.createDirectories(AUTH_FILE.getParent());
Files.writeString(AUTH_FILE, GSON.toJson(session));
} catch (IOException e) {
System.err.println(ZAnsi.yellow("Не удалось сохранить сессию: " + e.getMessage()));
}
}
private static HttpResponse<String> post(String endpoint, String jsonBody) throws Exception {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(ZHttpClient.getBaseUrl() + endpoint))
.header("Content-Type", "application/json")
.timeout(Duration.ofSeconds(15))
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
return HTTP.send(req, HttpResponse.BodyHandlers.ofString());
}
private static String extractError(String body) {
try {
int idx = body.indexOf("\"detail\"");
if (idx != -1) {
int s = body.indexOf("\"", idx + 9) + 1;
int e = body.indexOf("\"", s);
if (e > s) return body.substring(s, e);
}
} catch (Exception ignored) {}
return "Неизвестная ошибка";
}
// ====================== ВНУТРЕННИЕ КЛАССЫ ======================
public static class AuthSession {
@SerializedName("access_token") public String accessToken;
@SerializedName("refresh_token") public String refreshToken;
@SerializedName("expires_in") public int expiresIn;
public long expiresAt;
public String username;
public String uuid;
}
private static class LoginRequest {
final String username;
final String password;
LoginRequest(String u, String p) { this.username = u; this.password = p; }
}
public static class AuthResult {
public final boolean success;
public final String error;
private AuthResult(boolean s, String e) { success = s; error = e; }
public static AuthResult ok() { return new AuthResult(true, null); }
public static AuthResult fail(String msg) { return new AuthResult(false, msg); }
}
public static boolean hasActivePass() {
if (!isLoggedIn()) return false;
try {
String response = ZHttpClient.get("/auth/pass/my");
// Простая проверка — есть ли хотя бы одна активная проходка
return response.contains("\"is_active\":true");
} catch (Exception e) {
System.err.println("Не удалось проверить проходки: " + e.getMessage());
return false;
}
}
public static String activatePass(String passCode) {
try {
String json = "{\"pass_code\":\"" + passCode.toUpperCase() + "\"}";
HttpResponse<String> resp = post("/auth/pass/activate", json);
if (resp.statusCode() == 200) {
return "Проходка успешно активирована!";
} else {
String error = extractError(resp.body());
return "Ошибка: " + error;
}
} catch (Exception e) {
return "Ошибка соединения: " + e.getMessage();
}
}
}
@@ -1,5 +1,6 @@
package me.sashegdev.zernmc.launcher.menu;
import me.sashegdev.zernmc.launcher.auth.AuthManager;
import me.sashegdev.zernmc.launcher.minecraft.Instance;
import me.sashegdev.zernmc.launcher.minecraft.InstanceManager;
import me.sashegdev.zernmc.launcher.minecraft.MinecraftLib;
@@ -80,6 +81,32 @@ public class LaunchMenu {
}
private void installServerPack() throws Exception {
if (!AuthManager.hasActivePass()) {
ConsoleUtils.clearScreen();
System.out.println(ZAnsi.brightRed("У вас нет активной проходки!"));
System.out.println(ZAnsi.white("Чтобы скачивать сборки с сервера ZernMC, необходимо активировать проходку."));
System.out.println();
System.out.print(ZAnsi.white("Введите код проходки (ZERN-XXXXXXX) или Enter для отмены: "));
String code = Input.readLine();
if (code.isEmpty()) return;
String result = AuthManager.activatePass(code);
System.out.println(ZAnsi.cyan(result));
if (!result.contains("успешно")) {
ConsoleUtils.pause();
return;
}
// Повторная проверка
if (!AuthManager.hasActivePass()) {
System.out.println(ZAnsi.brightRed("Не удалось активировать проходку."));
ConsoleUtils.pause();
return;
}
}
ConsoleUtils.clearScreen();
System.out.println(ZAnsi.cyan("Получение списка доступных сборок с сервера..."));
@@ -502,12 +529,23 @@ public class LaunchMenu {
}
private void launchExistingInstance(Instance instance) {
if (instance.isServerPack() && !AuthManager.hasActivePass()) {
ConsoleUtils.clearScreen();
System.out.println(ZAnsi.brightRed("Для запуска серверной сборки требуется активная проходка!"));
ConsoleUtils.pause();
return;
}
ConsoleUtils.clearScreen();
System.out.println(ZAnsi.brightGreen("Запуск сборки: " + instance.getName()));
MinecraftLib lib = new MinecraftLib(instance);
LaunchOptions options = new LaunchOptions();
// Авторизация Minecraft
options.setUsername(AuthManager.getUsername());
options.setUuid(AuthManager.getUuid());
options.setAccessToken(AuthManager.getAccessToken());
try {
lib.launch(options);
} catch (Exception e) {
@@ -0,0 +1,167 @@
package me.sashegdev.zernmc.launcher.menu;
import me.sashegdev.zernmc.launcher.auth.AuthManager;
import me.sashegdev.zernmc.launcher.auth.AuthManager.AuthResult;
import me.sashegdev.zernmc.launcher.ui.ArrowMenu;
import me.sashegdev.zernmc.launcher.utils.ConsoleUtils;
import me.sashegdev.zernmc.launcher.utils.Input;
import me.sashegdev.zernmc.launcher.utils.ZAnsi;
import java.io.IOException;
import java.util.List;
/**
* Экран входа/регистрации.
* Показывается при старте лаунчера, если нет сохранённой сессии.
*
* show() возвращает true пользователь вошёл/зарегистрировался
* false пользователь выбрал выход из лаунчера
*/
public class LoginMenu {
/**
* Главный экран выбора действия.
*/
public boolean show() throws IOException {
while (true) {
ConsoleUtils.clearScreen();
printBanner();
List<String> options = List.of(
"Войти в аккаунт",
"Создать аккаунт",
"Выйти из лаунчера"
);
ArrowMenu menu = new ArrowMenu("Добро пожаловать в ZernMC!", options);
int choice = menu.show();
if (choice == -1 || choice == 2) return false;
boolean success = switch (choice) {
case 0 -> doLogin();
case 1 -> doRegister();
default -> false;
};
if (success) return true;
// Если не успех покажем меню снова (ошибка уже напечатана внутри методов)
}
}
/**
* Показывается когда пользователь уже вошёл предлагает выйти из аккаунта.
*/
public void showAccountMenu() throws IOException {
ConsoleUtils.clearScreen();
System.out.println(ZAnsi.header("=== Аккаунт ==="));
System.out.println();
System.out.println(ZAnsi.white(" Игрок: ") + ZAnsi.brightGreen(AuthManager.getUsername()));
System.out.println(ZAnsi.white(" UUID: ") + ZAnsi.cyan(AuthManager.getUuid()));
System.out.println();
List<String> options = List.of(
"Выйти из аккаунта",
"Назад"
);
ArrowMenu menu = new ArrowMenu("Управление аккаунтом", options);
int choice = menu.show();
if (choice == 0) {
AuthManager.logout();
System.out.println(ZAnsi.yellow("Вы вышли из аккаунта."));
ConsoleUtils.pause();
}
}
// ====================== ПРИВАТНЫЕ МЕТОДЫ ======================
private boolean doLogin() throws IOException {
ConsoleUtils.clearScreen();
printBanner();
System.out.println(ZAnsi.cyan(" [ Вход в аккаунт ]"));
System.out.println();
String username = Input.readLine(ZAnsi.white(" Имя пользователя: "));
if (username.isEmpty()) return false;
String password = readPassword(" Пароль: ");
if (password.isEmpty()) return false;
System.out.println();
System.out.print(ZAnsi.cyan(" Выполняем вход..."));
AuthResult result = AuthManager.login(username, password);
if (result.success) {
System.out.println("\r" + ZAnsi.brightGreen(" Добро пожаловать, " + AuthManager.getUsername() + "! "));
ConsoleUtils.pause();
return true;
} else {
System.out.println("\r" + ZAnsi.brightRed(" Ошибка: " + result.error + " "));
ConsoleUtils.pause();
return false;
}
}
private boolean doRegister() throws IOException {
ConsoleUtils.clearScreen();
printBanner();
System.out.println(ZAnsi.cyan(" [ Создание аккаунта ]"));
System.out.println();
System.out.println(ZAnsi.yellow(" Допустимые символы в имени: a-z, A-Z, 0-9, _"));
System.out.println(ZAnsi.yellow(" Длина имени: 3–16 символов | Длина пароля: от 6 символов"));
System.out.println();
String username = Input.readLine(ZAnsi.white(" Имя пользователя: "));
if (username.isEmpty()) return false;
String password = readPassword(" Пароль: ");
if (password.isEmpty()) return false;
String confirm = readPassword(" Повторите пароль: ");
if (!password.equals(confirm)) {
System.out.println(ZAnsi.brightRed("\n Пароли не совпадают!"));
ConsoleUtils.pause();
return false;
}
System.out.println();
System.out.print(ZAnsi.cyan(" Создаём аккаунт..."));
AuthResult result = AuthManager.register(username, password);
if (result.success) {
System.out.println("\r" + ZAnsi.brightGreen(" Аккаунт создан! Добро пожаловать, " + AuthManager.getUsername() + "! "));
ConsoleUtils.pause();
return true;
} else {
System.out.println("\r" + ZAnsi.brightRed(" Ошибка: " + result.error + " "));
ConsoleUtils.pause();
return false;
}
}
/**
* Читаем пароль стараемся скрыть вывод через Console,
* если недоступно (IDE/терминал без TTY) читаем обычным способом.
*/
private String readPassword(String prompt) {
java.io.Console console = System.console();
if (console != null) {
char[] chars = console.readPassword(prompt);
return chars != null ? new String(chars) : "";
}
// Fallback: в IDE пароль будет виден
return Input.readLine(prompt);
}
private void printBanner() {
System.out.println(ZAnsi.header("╔══════════════════════════════╗"));
System.out.println(ZAnsi.header("║ ZernMC Launcher ║"));
System.out.println(ZAnsi.header("╚══════════════════════════════╝"));
System.out.println();
}
}
@@ -113,6 +113,10 @@ public class Config {
return CONFIG_DIR.resolve("jre");
}
public static Path getConfigDir() {
return CONFIG_DIR;
}
/**
* Полезная информация для пользователя
*/
@@ -1,10 +1,18 @@
package me.sashegdev.zernmc.launcher.utils;
import me.sashegdev.zernmc.launcher.ui.ArrowMenu;
import java.io.IOException;
import java.util.List;
import java.util.Scanner;
/**
* Улучшенный Input с поддержкой кириллицы и confirm через ArrowMenu
*/
public class Input {
private static final Scanner scanner = new Scanner(System.in);
// Используем UTF-8 явно это помогает на Windows
private static final Scanner scanner = new Scanner(System.in, "UTF-8");
public static String readLine() {
return scanner.nextLine().trim();
@@ -36,9 +44,39 @@ public class Input {
}
}
public static boolean confirm(String prompt) {
System.out.print(prompt + " (да/нет): ");
String answer = scanner.nextLine().trim().toLowerCase();
return answer.equals("да") || answer.equals("y") || answer.equals("yes");
/**
* Новый confirm через ArrowMenu
* @throws IOException
*/
public static boolean confirm(String question) throws IOException {
ConsoleUtils.clearScreen(); // опционально, можно убрать
List<String> options = List.of(
"Да",
"Нет"
);
ArrowMenu menu = new ArrowMenu(question, options);
int choice = menu.show();
return choice == 0; // 0 = "Да"
}
/**
* Альтернативный confirm без очистки экрана
* @throws IOException
*/
public static boolean confirmInline(String question) throws IOException {
List<String> options = List.of("Да", "Нет");
ArrowMenu menu = new ArrowMenu(question, options);
int choice = menu.show();
return choice == 0;
}
/**
* Закрытие сканнера (вызывать при выходе из программы, если нужно)
*/
public static void close() {
scanner.close();
}
}
@@ -33,7 +33,7 @@ public class ZHttpClient {
// Умное проксирование по сервисам
public enum ServiceType {
ZERN_SERVER(BASE_URL, true), // Всегда прямое подключение
ZERN_SERVER(BASE_URL, true),
FABRIC_META("https://meta.fabricmc.net", false),
FABRIC_MAVEN("https://maven.fabricmc.net", false),
MOJANG_META("https://piston-meta.mojang.com", false),
@@ -50,13 +50,8 @@ public class ZHttpClient {
this.alwaysDirect = alwaysDirect;
}
public String getBaseUrl() {
return baseUrl;
}
public boolean isAlwaysDirect() {
return alwaysDirect;
}
public String getBaseUrl() { return baseUrl; }
public boolean isAlwaysDirect() { return alwaysDirect; }
}
// Статусы сервисов
@@ -67,7 +62,7 @@ public class ZHttpClient {
private static final int MAX_FAILS_BEFORE_PROXY = 2;
private static final long HEALTH_CHECK_INTERVAL_MS = 60000; // 1 минута
private static final long CHECK_TIMEOUT_MS = 5000; // 5 секунд на проверку
private static final long CHECK_TIMEOUT_MS = 7000; // 7 секунд на проверку
// Статистика
private static int directSuccessCount = 0;
@@ -83,7 +78,7 @@ public class ZHttpClient {
}
/**
* Проверить все сервисы при старте
* Вызывать один раз при запуске лаунчера
*/
public static void checkAllServicesOnStartup() {
if (proxyTested.get()) return;
@@ -96,6 +91,7 @@ public class ZHttpClient {
ServiceType.FABRIC_META,
ServiceType.FABRIC_MAVEN,
ServiceType.MOJANG_META,
ServiceType.MOJANG_RESOURCES,
ServiceType.FORGE_MAVEN
);
@@ -104,11 +100,9 @@ public class ZHttpClient {
serviceHealthy.put(service, isHealthy);
if (service.isAlwaysDirect()) {
if (!isHealthy) {
System.out.println(ZAnsi.red(" " + service.name() + " - НЕ ДОСТУПЕН (критично!)"));
} else {
System.out.println(ZAnsi.green(" " + service.name() + " - OK"));
}
System.out.println(isHealthy ?
ZAnsi.green(" " + service.name() + " - OK") :
ZAnsi.red(" " + service.name() + " - НЕ ДОСТУПЕН (критично!)"));
} else {
if (isHealthy) {
System.out.println(ZAnsi.green(" " + service.name() + " - прямое подключение работает"));
@@ -120,51 +114,69 @@ public class ZHttpClient {
}
}
// Проверяем, нужно ли включить глобальный прокси режим
boolean anyCriticalDown = !serviceHealthy.get(ServiceType.ZERN_SERVER);
if (anyCriticalDown) {
if (!serviceHealthy.get(ServiceType.ZERN_SERVER)) {
System.out.println(ZAnsi.brightRed("Критическая ошибка: Zern сервер недоступен!"));
}
proxyTested.set(true);
// Запускаем фоновую проверку
startHealthCheckThread();
printStats();
}
/**
* Проверить здоровье конкретного сервиса
* Принудительная проверка Mojang-сервисов (рекомендуется вызывать перед установкой сборки)
*/
private static boolean checkServiceHealth(ServiceType service) {
if (service.isAlwaysDirect()) {
return checkDirectConnection(service.getBaseUrl());
public static void forceCheckMojangServices() {
System.out.println(ZAnsi.cyan("Принудительная проверка Mojang сервисов..."));
for (ServiceType service : List.of(ServiceType.MOJANG_META, ServiceType.MOJANG_RESOURCES)) {
boolean healthy = checkServiceHealth(service);
serviceHealthy.put(service, healthy);
if (healthy) {
System.out.println(ZAnsi.green(" " + service.name() + " доступен напрямую"));
serviceProxyMode.put(service, false);
serviceFailCount.put(service, 0);
} else {
System.out.println(ZAnsi.yellow(" " + service.name() + " недоступен → прокси режим активирован"));
serviceProxyMode.put(service, true);
serviceFailCount.put(service, MAX_FAILS_BEFORE_PROXY);
}
}
}
private static boolean checkServiceHealth(ServiceType service) {
return checkDirectConnection(service.getBaseUrl());
}
/**
* Проверить прямое подключение к URL
* Улучшенная проверка прямого подключения
*/
private static boolean checkDirectConnection(String url) {
private static boolean checkDirectConnection(String baseUrl) {
String testUrl = baseUrl;
if (baseUrl.contains("piston-meta.mojang.com") || baseUrl.contains("launchermeta.mojang.com")) {
testUrl = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json";
} else if (baseUrl.contains("resources.download.minecraft.net")) {
testUrl = "https://resources.download.minecraft.net/00/0000000000000000000000000000000000000000";
}
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.uri(URI.create(testUrl))
.timeout(Duration.ofMillis(CHECK_TIMEOUT_MS))
.HEAD()
.GET()
.header("User-Agent", "ZernMC-Launcher/HealthCheck")
.build();
HttpResponse<Void> response = client.send(request, HttpResponse.BodyHandlers.discarding());
return response.statusCode() < 500;
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
int code = response.statusCode();
return code == 200 || code == 404; // 404 для ресурсов нормально
} catch (Exception e) {
return false;
}
}
/**
* Запустить фоновый поток для периодической проверки
*/
private static void startHealthCheckThread() {
Thread healthThread = new Thread(() -> {
while (true) {
@@ -180,9 +192,6 @@ public class ZHttpClient {
healthThread.start();
}
/**
* Периодическая проверка здоровья сервисов
*/
private static void performHealthCheck() {
for (ServiceType service : ServiceType.values()) {
if (service.isAlwaysDirect()) continue;
@@ -191,13 +200,9 @@ public class ZHttpClient {
serviceHealthy.put(service, isHealthy);
if (isHealthy && serviceProxyMode.get(service)) {
// Сервис восстановился - пробуем переключить обратно
Long lastCheck = serviceLastCheckTime.get(service);
if (lastCheck == null || System.currentTimeMillis() - lastCheck > HEALTH_CHECK_INTERVAL_MS) {
serviceProxyMode.put(service, false);
serviceFailCount.put(service, 0);
System.out.println(ZAnsi.green("[NET] " + service.name() + " восстановлен, переключен на прямое подключение"));
}
} else if (!isHealthy && !serviceProxyMode.get(service)) {
int fails = serviceFailCount.getOrDefault(service, 0) + 1;
serviceFailCount.put(service, fails);
@@ -211,9 +216,6 @@ public class ZHttpClient {
}
}
/**
* Определить тип сервиса по URL
*/
private static ServiceType detectService(String url) {
if (url.contains("meta.fabricmc.net")) return ServiceType.FABRIC_META;
if (url.contains("maven.fabricmc.net")) return ServiceType.FABRIC_MAVEN;
@@ -226,32 +228,82 @@ public class ZHttpClient {
return null;
}
/**
* Нужно ли использовать прокси для URL
*/
private static boolean shouldUseProxyForUrl(String url) {
if (useProxyMode.get()) return true;
ServiceType service = detectService(url);
if (service == null) return false;
if (service.isAlwaysDirect()) return false;
if (service == null || service.isAlwaysDirect()) return false;
return serviceProxyMode.getOrDefault(service, false);
}
/**
* Получить URL через прокси если нужно
*/
public static String getWithSmartProxy(String url) throws IOException, InterruptedException {
if (shouldUseProxyForUrl(url)) {
String encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8);
return proxyGet("/download?url=" + encodedUrl);
private static boolean isConnectionError(Throwable e) {
Throwable cause = e.getCause() != null ? e.getCause() : e;
String msg = cause.getMessage() != null ? cause.getMessage().toLowerCase() : "";
return cause instanceof java.net.ConnectException ||
cause instanceof java.net.UnknownHostException ||
cause instanceof java.nio.channels.ClosedChannelException ||
msg.contains("connection") ||
msg.contains("timeout") ||
msg.contains("refused") ||
msg.contains("closed");
}
private static void markServiceAsBlocked(String url) {
ServiceType service = detectService(url);
if (service == null || service.isAlwaysDirect()) return;
int fails = serviceFailCount.getOrDefault(service, 0) + 1;
serviceFailCount.put(service, fails);
serviceLastCheckTime.put(service, System.currentTimeMillis());
if (fails >= MAX_FAILS_BEFORE_PROXY && !serviceProxyMode.get(service)) {
serviceProxyMode.put(service, true);
System.out.println(ZAnsi.yellow("[NET] " + service.name() + " заблокирован, переключаемся на прокси"));
}
}
/**
* Универсальный GET с умным прокси + автоматическим fallback
*/
public static String getWithSmartProxy(String url) throws IOException, InterruptedException {
// Попытка прямого подключения
if (!shouldUseProxyForUrl(url)) {
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(Duration.ofSeconds(30))
.timeout(Duration.ofSeconds(25))
.header("User-Agent", "ZernMC-Launcher/1.0")
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
directSuccessCount++;
return response.body();
}
if (response.statusCode() >= 400) {
throw new IOException("HTTP " + response.statusCode());
}
} catch (Exception e) {
if (isConnectionError(e)) {
directFailCount++;
markServiceAsBlocked(url);
}
// Если ошибка соединения пробуем через прокси
}
}
// Через прокси
try {
String encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8);
String proxyUrl = BASE_URL + "/download?url=" + encodedUrl;
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(proxyUrl))
.timeout(Duration.ofSeconds(40))
.header("User-Agent", "ZernMC-Launcher/1.0")
.GET()
.build();
@@ -259,39 +311,52 @@ public class ZHttpClient {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new IOException("HTTP " + response.statusCode());
throw new IOException("Proxy HTTP " + response.statusCode());
}
directSuccessCount++;
proxySuccessCount++;
return response.body();
} catch (Exception e) {
directFailCount++;
ServiceType service = detectService(url);
if (service != null && !service.isAlwaysDirect()) {
int fails = serviceFailCount.getOrDefault(service, 0) + 1;
serviceFailCount.put(service, fails);
if (fails >= MAX_FAILS_BEFORE_PROXY) {
serviceProxyMode.put(service, true);
}
}
throw e;
throw new IOException("Не удалось получить данные ни напрямую, ни через прокси: " + e.getMessage(), e);
}
}
/**
* Скачать файл с умным прокси
* Скачивание файла с умным прокси + fallback
*/
public static void downloadFileWithSmartProxy(String url, Path target) throws Exception {
if (shouldUseProxyForUrl(url)) {
downloadViaProxy(url, target);
return;
}
if (!shouldUseProxyForUrl(url)) {
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(Duration.ofSeconds(30))
.timeout(Duration.ofSeconds(40))
.header("User-Agent", "ZernMC-Launcher/1.0")
.GET()
.build();
HttpResponse<Path> response = client.send(request, HttpResponse.BodyHandlers.ofFile(target));
if (response.statusCode() == 200) {
directSuccessCount++;
return;
}
} catch (Exception e) {
if (isConnectionError(e)) {
directFailCount++;
markServiceAsBlocked(url);
}
// fallback на прокси ниже
}
}
// Скачивание через прокси
String encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8);
String proxyUrl = BASE_URL + "/proxy/download?url=" + encodedUrl;
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(proxyUrl))
.timeout(Duration.ofMinutes(5))
.header("User-Agent", "ZernMC-Launcher/1.0")
.GET()
.build();
@@ -299,26 +364,13 @@ public class ZHttpClient {
HttpResponse<Path> response = client.send(request, HttpResponse.BodyHandlers.ofFile(target));
if (response.statusCode() != 200) {
throw new IOException("HTTP " + response.statusCode());
throw new IOException("Proxy download failed: HTTP " + response.statusCode());
}
directSuccessCount++;
} catch (Exception e) {
directFailCount++;
ServiceType service = detectService(url);
if (service != null && !service.isAlwaysDirect()) {
int fails = serviceFailCount.getOrDefault(service, 0) + 1;
serviceFailCount.put(service, fails);
if (fails >= MAX_FAILS_BEFORE_PROXY) {
serviceProxyMode.put(service, true);
}
}
throw e;
}
proxySuccessCount++;
}
// ====================== ОСНОВНЫЕ МЕТОДЫ (СОХРАНЕНЫ) ======================
// ====================== СТАРЫЕ МЕТОДЫ (обновлённые) ======================
public static String get(String endpoint) throws IOException, InterruptedException {
checkAllServicesOnStartup();
@@ -341,7 +393,6 @@ public class ZHttpClient {
throw new IOException("HTTP " + response.statusCode());
}
return response.body();
} catch (Exception e) {
directFailCount++;
throw e;
@@ -365,38 +416,16 @@ public class ZHttpClient {
proxySuccessCount++;
return response.body();
} catch (Exception e) {
throw new IOException("Ошибка прокси запроса: " + e.getMessage(), e);
throw new IOException("Ошибка прокси: " + e.getMessage(), e);
}
}
private static void downloadViaProxy(String url, Path target) throws Exception {
String encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8.toString());
String proxyUrl = BASE_URL + "/proxy/download?url=" + encodedUrl;
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(proxyUrl))
.timeout(Duration.ofMinutes(5))
.header("User-Agent", "ZernMC-Launcher/1.0")
.GET()
.build();
HttpResponse<Path> response = client.send(request, HttpResponse.BodyHandlers.ofFile(target));
if (response.statusCode() != 200) {
throw new IOException("HTTP " + response.statusCode());
}
proxySuccessCount++;
}
// ====================== МЕТОДЫ ДЛЯ EXTERNAL РЕСУРСОВ ======================
public static List<String> getFabricLoaderVersions() throws IOException, InterruptedException {
String url = "https://meta.fabricmc.net/v2/versions/loader";
String response = getWithSmartProxy(url);
return parseFabricVersionsFromJson(response);
return parseFabricVersionsFromJson(getWithSmartProxy(url));
}
public static JSONObject getMojangVersionManifest() throws IOException, InterruptedException {
@@ -409,23 +438,15 @@ public class ZHttpClient {
JSONObject manifest = getMojangVersionManifest();
JSONArray versions = manifest.getJSONArray("versions");
String versionUrl = null;
for (int i = 0; i < versions.length(); i++) {
JSONObject v = versions.getJSONObject(i);
if (v.getString("id").equals(versionId)) {
versionUrl = v.getString("url");
break;
return new JSONObject(getWithSmartProxy(v.getString("url")));
}
}
if (versionUrl == null) {
throw new IOException("Version " + versionId + " not found");
}
String response = getWithSmartProxy(versionUrl);
return new JSONObject(response);
}
public static String getForgeVersionsXml() throws IOException, InterruptedException {
String url = "https://maven.minecraftforge.net/net/minecraftforge/forge/maven-metadata.xml";
return getWithSmartProxy(url);
@@ -444,8 +465,6 @@ public class ZHttpClient {
return getWithSmartProxy(url);
}
// ====================== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ======================
private static List<String> parseFabricVersionsFromJson(String json) {
JSONArray array = new JSONArray(json);
List<String> versions = new ArrayList<>();
@@ -458,6 +477,8 @@ public class ZHttpClient {
return versions;
}
// ====================== ВСПОМОГАТЕЛЬНЫЕ ======================
public static String getBaseUrl() {
return BASE_URL;
}
@@ -488,7 +509,7 @@ public class ZHttpClient {
public static void printStats() {
System.out.println(ZAnsi.cyan("\n=== Статистика сети ==="));
System.out.println(ZAnsi.white("Глобальный прокси режим: ") + (useProxyMode.get() ? "ВКЛЮЧЕН" : "ВЫКЛЮЧЕН"));
System.out.println(ZAnsi.white("Глобальный прокси: ") + (useProxyMode.get() ? "ВКЛ" : "ВЫКЛ"));
System.out.println(ZAnsi.white("Прямых успехов: ") + directSuccessCount);
System.out.println(ZAnsi.white("Прямых неудач: ") + directFailCount);
System.out.println(ZAnsi.white("Прокси успехов: ") + proxySuccessCount);
@@ -497,7 +518,7 @@ public class ZHttpClient {
for (ServiceType type : ServiceType.values()) {
if (type.isAlwaysDirect()) continue;
String status = serviceProxyMode.get(type) ? ZAnsi.red("ПРОКСИ") : ZAnsi.green("ПРЯМО");
String health = serviceHealthy.get(type) ? ZAnsi.green("") : ZAnsi.red("");
String health = serviceHealthy.get(type) ? ZAnsi.green("[+]") : ZAnsi.red("[-]");
System.out.println(ZAnsi.white(" " + type.name() + ": ") + status + " " + health);
}
}
+357
View File
@@ -0,0 +1,357 @@
# auth.py
import base64
import json
import sqlite3
import hashlib
import hmac
import secrets
import time
from datetime import datetime
from pathlib import Path
from typing import Optional
import structlog
from fastapi import APIRouter, HTTPException, Request, Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel, Field
logger = structlog.get_logger(__name__)
# ====================== КОНФИГ ======================
AUTH_DB = Path("data/auth.db")
AUTH_DB.parent.mkdir(exist_ok=True)
SECRET_KEY = Path("data/.secret_key")
ACCESS_TOKEN_EXPIRE_SECONDS = 24 * 3600 # 24 часа
REFRESH_TOKEN_EXPIRE_SECONDS = 30 * 86400 # 30 дней
# ====================== СЕКРЕТНЫЙ КЛЮЧ ======================
def _get_secret() -> bytes:
if SECRET_KEY.exists():
return SECRET_KEY.read_bytes()
key = secrets.token_bytes(64)
SECRET_KEY.write_bytes(key)
SECRET_KEY.chmod(0o600)
return key
_SECRET = _get_secret()
def create_jwt(payload: dict) -> str:
import base64, json
header = base64.urlsafe_b64encode(json.dumps({"alg": "HS256", "typ": "JWT"}).encode()).rstrip(b'=').decode()
body = base64.urlsafe_b64encode(json.dumps(payload).encode()).rstrip(b'=').decode()
msg = f"{header}.{body}".encode()
sig = hmac.new(_SECRET, msg, hashlib.sha256).digest()
return f"{header}.{body}.{base64.urlsafe_b64encode(sig).rstrip(b'=').decode()}"
def verify_jwt(token: str) -> Optional[dict]:
try:
parts = token.split(".")
if len(parts) != 3:
return None
header, body, sig = parts
msg = f"{header}.{body}".encode()
expected = hmac.new(_SECRET, msg, hashlib.sha256).digest()
if not hmac.compare_digest(base64.urlsafe_b64decode(sig + '==='[:3]), expected):
return None
payload = json.loads(base64.urlsafe_b64decode(body + '==='[:3]))
if payload.get("exp", 0) < time.time():
return None
return payload
except:
return None
# ====================== БАЗА ДАННЫХ ======================
def get_db():
conn = sqlite3.connect(str(AUTH_DB), check_same_thread=False)
conn.row_factory = sqlite3.Row
return conn
def init_db():
conn = get_db()
conn.executescript("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
username TEXT UNIQUE COLLATE NOCASE,
password_hash TEXT NOT NULL,
uuid TEXT UNIQUE NOT NULL,
created_at REAL NOT NULL,
last_login REAL
);
CREATE TABLE IF NOT EXISTS passes ( -- НОВАЯ ТАБЛИЦА
code TEXT PRIMARY KEY, -- ZERN-XXXXXX
is_used BOOLEAN DEFAULT 0,
activated_by INTEGER REFERENCES users(id),
activated_at REAL,
expires_at REAL, -- NULL = без срока
max_uses INTEGER DEFAULT 1, -- пока 1, можно больше
uses INTEGER DEFAULT 0
);
CREATE TABLE IF NOT EXISTS user_passes ( -- Связь пользователь-пасс
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
pass_code TEXT REFERENCES passes(code),
activated_at REAL NOT NULL,
PRIMARY KEY (user_id, pass_code)
);
""")
conn.commit()
conn.close()
logger.info("Auth database initialized")
# ====================== ХЕЛПЕРЫ ======================
def hash_password(password: str) -> str:
salt = secrets.token_hex(16)
hash_obj = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 300000)
return f"{salt}${hash_obj.hex()}"
def verify_password(password: str, stored: str) -> bool:
try:
salt, stored_hash = stored.split('$')
hash_obj = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 300000)
return hmac.compare_digest(hash_obj.hex(), stored_hash)
except:
return False
def generate_uuid() -> str:
return f"{secrets.token_hex(4)}-{secrets.token_hex(2)}-{secrets.token_hex(2)}-{secrets.token_hex(2)}-{secrets.token_hex(6)}"
# ====================== МОДЕЛИ ======================
class LoginRequest(BaseModel):
username: str
password: str
class RegisterRequest(BaseModel):
username: str = Field(..., min_length=3, max_length=16, pattern=r"^[a-zA-Z0-9_]+$")
password: str = Field(..., min_length=6, max_length=128)
class TokenResponse(BaseModel):
access_token: str
refresh_token: str
expires_in: int
username: str
uuid: str
# ====================== ROUTER ======================
router = APIRouter(prefix="/auth", tags=["auth"])
bearer = HTTPBearer(auto_error=False)
@router.post("/register", response_model=TokenResponse)
async def register(body: RegisterRequest, request: Request):
conn = get_db()
try:
if conn.execute("SELECT 1 FROM users WHERE username = ? COLLATE NOCASE", (body.username,)).fetchone():
raise HTTPException(409, "Имя пользователя уже занято")
uuid = generate_uuid()
pw_hash = hash_password(body.password)
now = time.time()
conn.execute(
"INSERT INTO users (username, password_hash, uuid, created_at) VALUES (?, ?, ?, ?)",
(body.username, pw_hash, uuid, now)
)
user_id = conn.lastrowid
conn.commit()
return _issue_tokens(conn, user_id, body.username, uuid)
finally:
conn.close()
@router.post("/login", response_model=TokenResponse)
async def login(body: LoginRequest, request: Request):
conn = get_db()
try:
row = conn.execute(
"SELECT id, username, password_hash, uuid FROM users WHERE username = ? COLLATE NOCASE",
(body.username,)
).fetchone()
if not row or not verify_password(body.password, row["password_hash"]):
raise HTTPException(401, "Неверное имя пользователя или пароль")
conn.execute("UPDATE users SET last_login = ? WHERE id = ?", (time.time(), row["id"]))
conn.commit()
return _issue_tokens(conn, row["id"], row["username"], row["uuid"])
finally:
conn.close()
def _issue_tokens(conn, user_id: int, username: str, uuid: str) -> TokenResponse:
now = time.time()
access_token = create_jwt({
"sub": user_id,
"username": username,
"uuid": uuid,
"type": "access",
"exp": now + ACCESS_TOKEN_EXPIRE_SECONDS
})
refresh_token = create_jwt({
"sub": user_id,
"type": "refresh",
"exp": now + REFRESH_TOKEN_EXPIRE_SECONDS
})
token_hash = hashlib.sha256(refresh_token.encode()).hexdigest()
# Удаляем старые refresh-токены пользователя
conn.execute("DELETE FROM refresh_tokens WHERE user_id = ?", (user_id,))
conn.execute(
"INSERT INTO refresh_tokens (user_id, token_hash, expires_at) VALUES (?, ?, ?)",
(user_id, token_hash, now + REFRESH_TOKEN_EXPIRE_SECONDS)
)
conn.commit()
return TokenResponse(
access_token=access_token,
refresh_token=refresh_token,
expires_in=ACCESS_TOKEN_EXPIRE_SECONDS,
username=username,
uuid=uuid
)
# Добавь этот эндпоинт, он используется в tryRefresh()
@router.post("/refresh")
async def refresh(body: dict):
refresh_token = body.get("refresh_token")
if not refresh_token:
raise HTTPException(400, "refresh_token обязателен")
payload = verify_jwt(refresh_token)
if not payload or payload.get("type") != "refresh":
raise HTTPException(401, "Недействительный refresh token")
conn = get_db()
try:
token_hash = hashlib.sha256(refresh_token.encode()).hexdigest()
row = conn.execute(
"SELECT user_id FROM refresh_tokens WHERE token_hash = ? AND expires_at > ?",
(token_hash, time.time())
).fetchone()
if not row:
raise HTTPException(401, "Refresh token истёк или недействителен")
user_row = conn.execute(
"SELECT id, username, uuid FROM users WHERE id = ?", (row["user_id"],)
).fetchone()
if not user_row:
raise HTTPException(401, "Пользователь не найден")
return _issue_tokens(conn, user_row["id"], user_row["username"], user_row["uuid"])
finally:
conn.close()
class ActivatePassRequest(BaseModel):
pass_code: str = Field(..., min_length=8, max_length=20, pattern=r"^ZERN-[A-Z0-9]+$")
class PassInfo(BaseModel):
code: str
expires_at: Optional[float] = None
is_active: bool
@router.post("/pass/activate")
async def activate_pass(body: ActivatePassRequest, credentials: HTTPAuthorizationCredentials = Depends(bearer)):
token = credentials.credentials if credentials else None
if not token:
raise HTTPException(401, "Требуется авторизация")
payload = verify_jwt(token)
if not payload or payload.get("type") != "access":
raise HTTPException(401, "Недействительный токен")
user_id = payload["sub"]
conn = get_db()
try:
# Проверяем существование и доступность пасса
pass_row = conn.execute(
"SELECT code, expires_at, uses, max_uses FROM passes WHERE code = ?",
(body.pass_code.upper(),)
).fetchone()
if not pass_row:
raise HTTPException(404, "Проходка не найдена")
if pass_row["expires_at"] and pass_row["expires_at"] < time.time():
raise HTTPException(410, "Проходка истекла")
if pass_row["uses"] >= pass_row["max_uses"]:
raise HTTPException(410, "Проходка уже использована")
# Проверяем, не активировал ли уже этот пользователь
already = conn.execute(
"SELECT 1 FROM user_passes WHERE user_id = ? AND pass_code = ?",
(user_id, body.pass_code.upper())
).fetchone()
if already:
raise HTTPException(409, "Эта проходка уже активирована на вашем аккаунте")
now = time.time()
# Активируем
conn.execute(
"INSERT INTO user_passes (user_id, pass_code, activated_at) VALUES (?, ?, ?)",
(user_id, body.pass_code.upper(), now)
)
# Увеличиваем счётчик использований
conn.execute(
"UPDATE passes SET uses = uses + 1, activated_by = ?, activated_at = ? WHERE code = ?",
(user_id, now, body.pass_code.upper())
)
conn.commit()
logger.info("Pass activated", user_id=user_id, pass_code=body.pass_code)
return {"success": True, "message": "Проходка успешно активирована!"}
finally:
conn.close()
@router.get("/pass/my")
async def get_my_passes(credentials: HTTPAuthorizationCredentials = Depends(bearer)):
token = credentials.credentials if credentials else None
if not token:
raise HTTPException(401, "Требуется авторизация")
payload = verify_jwt(token)
if not payload:
raise HTTPException(401, "Недействительный токен")
user_id = payload["sub"]
conn = get_db()
try:
rows = conn.execute("""
SELECT p.code, p.expires_at, up.activated_at
FROM user_passes up
JOIN passes p ON up.pass_code = p.code
WHERE up.user_id = ?
""", (user_id,)).fetchall()
passes = []
now = time.time()
for row in rows:
expires = row["expires_at"]
is_active = expires is None or expires > now
passes.append({
"code": row["code"],
"activated_at": row["activated_at"],
"expires_at": expires,
"is_active": is_active
})
return {"passes": passes}
finally:
conn.close()
+50 -1
View File
@@ -9,7 +9,6 @@ import logging
from datetime import datetime
from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol
# Import local modules
from pack_manager import DATA_DIR, scan_pack, get_cached_manifest, PACKS_DIR
from models import PackMeta
from middleware import LoggingMiddleware
@@ -20,6 +19,9 @@ import httpx
import base64
from fastapi.responses import StreamingResponse
from auth import router as auth_router, init_db, verify_jwt
from pass_manager import activate_pass, has_active_pass, get_user_passes
logger = structlog.get_logger(__name__)
# Cache for manifests - expires after 5 minutes
@@ -51,6 +53,10 @@ async def lifespan(app: FastAPI):
PACKS_DIR.mkdir(exist_ok=True)
DATA_DIR.mkdir(exist_ok=True)
init_db()
app.include_router(auth_router)
if args.test:
await run_test_mode()
yield
@@ -820,6 +826,49 @@ async def proxy_status():
}
@app.post("/auth/pass/activate")
async def api_activate_pass(request: Request):
try:
body = await request.json()
pass_code = body.get("pass_code")
if not pass_code:
raise HTTPException(400, "pass_code обязателен")
except:
raise HTTPException(400, "Неверный JSON")
# Получаем текущего пользователя из токена
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
raise HTTPException(401, "Требуется авторизация")
token = auth_header.split(" ")[1]
payload = verify_jwt(token) # функция из auth.py
if not payload:
raise HTTPException(401, "Недействительный токен")
result = activate_pass(pass_code, payload["username"], payload["sub"])
if result["success"]:
return result
else:
raise HTTPException(400, result["error"])
@app.get("/auth/pass/my")
async def api_my_passes(request: Request):
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
raise HTTPException(401, "Требуется авторизация")
token = auth_header.split(" ")[1]
payload = verify_jwt(token)
if not payload:
raise HTTPException(401, "Недействительный токен")
passes = get_user_passes(payload["username"])
return {"passes": passes, "has_active": any(p["is_active"] for p in passes)}
# Cleanup on shutdown
@app.on_event("shutdown")
async def shutdown_proxy():
+77
View File
@@ -0,0 +1,77 @@
import json
from pathlib import Path
from datetime import datetime
import structlog
logger = structlog.get_logger(__name__)
PASSES_FILE = Path("data/passes.json")
def load_passes():
if not PASSES_FILE.exists():
PASSES_FILE.parent.mkdir(exist_ok=True)
default = {"passes": {}}
PASSES_FILE.write_text(json.dumps(default, indent=2, ensure_ascii=False))
return default
try:
return json.loads(PASSES_FILE.read_text(encoding="utf-8"))
except:
return {"passes": {}}
def save_passes(data):
PASSES_FILE.write_text(json.dumps(data, indent=2, ensure_ascii=False))
def activate_pass(pass_code: str, username: str, user_id: int) -> dict:
data = load_passes()
pass_code = pass_code.upper().strip()
if pass_code not in data["passes"]:
return {"success": False, "error": "Проходка не найдена"}
p = data["passes"][pass_code]
if not p.get("is_active", True):
return {"success": False, "error": "Проходка деактивирована"}
if p.get("expires_at") and p.get("expires_at") < datetime.now().timestamp():
return {"success": False, "error": "Проходка истекла"}
if p.get("owner") is not None:
if p.get("owner") != username:
return {"success": False, "error": "Проходка уже активирована другим пользователем"}
return {"success": True, "message": "Проходка уже активирована на вашем аккаунте"}
# Активация
now = datetime.now().timestamp()
p["owner"] = username
p["activated_at"] = now
p["uses"] = p.get("uses", 0) + 1
save_passes(data)
logger.info("Pass activated", pass_code=pass_code, username=username)
return {"success": True, "message": "Проходка успешно активирована!"}
def has_active_pass(username: str) -> bool:
data = load_passes()
for p in data["passes"].values():
if p.get("owner") == username:
if p.get("expires_at") and p.get("expires_at") < datetime.now().timestamp():
continue
if p.get("is_active", True):
return True
return False
def get_user_passes(username: str) -> list:
data = load_passes()
result = []
now = datetime.now().timestamp()
for p in data["passes"].values():
if p.get("owner") == username:
result.append({
"code": p["code"],
"activated_at": p.get("activated_at"),
"expires_at": p.get("expires_at"),
"is_active": p.get("is_active", True) and (not p.get("expires_at") or p.get("expires_at") > now)
})
return result