auth gui
This commit is contained in:
@@ -11,5 +11,16 @@ public final class AuthConfig {
|
||||
public int pbkdf2Iterations = 210000;
|
||||
public String mojangProfileUrl = "https://api.minecraftservices.com/minecraft/profile/lookup/name/{username}";
|
||||
public String legacyMojangProfileUrl = "https://api.mojang.com/users/profiles/minecraft/{username}";
|
||||
public boolean requireLoginEveryJoin = true;
|
||||
|
||||
/**
|
||||
* false: premium players use Mojang authentication as their login, while local/cracked
|
||||
* players must use their Lobby Systems password on every connection.
|
||||
* true: every player, including premium players, must also register/use a Lobby Systems
|
||||
* password after Mojang authentication.
|
||||
*/
|
||||
public boolean requireLoginEveryJoin = false;
|
||||
|
||||
/** Rules are accepted once per version. Increment to require acceptance again. */
|
||||
public boolean rulesEnabled = true;
|
||||
public int rulesVersion = 1;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.protocol.game.ClientboundSetSubtitleTextPacket;
|
||||
import net.minecraft.network.protocol.game.ClientboundSetTitleTextPacket;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
@@ -22,6 +20,7 @@ import java.nio.file.Files;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
@@ -33,6 +32,10 @@ public final class AuthManager {
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
private static final SecureRandom RANDOM = new SecureRandom();
|
||||
private static final HttpClient HTTP = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(4)).build();
|
||||
private static final String DIALOG_RULES = "dartokloning:auth_rules";
|
||||
private static final String DIALOG_REGISTER = "dartokloning:auth_register";
|
||||
private static final String DIALOG_LOGIN = "dartokloning:auth_login";
|
||||
|
||||
private final Map<String, Account> accounts = new HashMap<>();
|
||||
private final Map<UUID, Session> sessions = new ConcurrentHashMap<>();
|
||||
private final Map<String, PremiumCache> premiumCache = new ConcurrentHashMap<>();
|
||||
@@ -46,13 +49,21 @@ public final class AuthManager {
|
||||
AuthConfig c = GSON.fromJson(r, AuthConfig.class);
|
||||
if (c != null) config = c;
|
||||
}
|
||||
} else saveConfig();
|
||||
}
|
||||
saveConfig(); // writes newly-added defaults/fields during upgrades
|
||||
|
||||
if (Files.exists(ConfigPaths.AUTH_ACCOUNTS)) {
|
||||
try (Reader r = Files.newBufferedReader(ConfigPaths.AUTH_ACCOUNTS)) {
|
||||
Map<String, Account> loaded = GSON.fromJson(r, new TypeToken<Map<String, Account>>(){}.getType());
|
||||
if (loaded != null) { accounts.clear(); accounts.putAll(loaded); }
|
||||
if (loaded != null) {
|
||||
accounts.clear();
|
||||
accounts.putAll(loaded);
|
||||
}
|
||||
}
|
||||
} else saveAccounts();
|
||||
} else {
|
||||
saveAccounts();
|
||||
}
|
||||
migrateAccounts();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
LobbySystemsMod.LOGGER.error("Failed to load Lobby Systems Auth", e);
|
||||
@@ -62,7 +73,56 @@ public final class AuthManager {
|
||||
|
||||
public AuthConfig config() { return config; }
|
||||
|
||||
/** Called from the login mixin. */
|
||||
/** Re-applies the freshly loaded auth configuration to players already online. */
|
||||
public void applyReload(MinecraftServer server) {
|
||||
premiumCache.clear();
|
||||
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
|
||||
if (!config.enabled) {
|
||||
Session existing = sessions.get(player.getUUID());
|
||||
if (existing != null) {
|
||||
existing.authenticated = true;
|
||||
existing.stage = Stage.AUTHENTICATED;
|
||||
clearDialog(player);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
Session s = sessions.get(player.getUUID());
|
||||
if (s == null) continue;
|
||||
Account account = account(player.getName().getString());
|
||||
|
||||
// A rules-version bump is intentionally live: online players must accept
|
||||
// the new version before continuing.
|
||||
if (needsRules(account)) {
|
||||
s.authenticated = false;
|
||||
s.stage = Stage.RULES;
|
||||
s.joinedAt = System.currentTimeMillis();
|
||||
s.attempts = 0;
|
||||
showDialog(player, DIALOG_RULES);
|
||||
continue;
|
||||
}
|
||||
|
||||
// false means premium accounts are satisfied by Mojang authentication.
|
||||
if (s.premium && !config.requireLoginEveryJoin) {
|
||||
authenticate(player);
|
||||
continue;
|
||||
}
|
||||
|
||||
// true makes the Lobby Systems password an additional layer for premium
|
||||
// accounts, and local accounts always require it on each connection.
|
||||
if (s.premium && config.requireLoginEveryJoin && s.authenticated) {
|
||||
s.authenticated = false;
|
||||
s.joinedAt = System.currentTimeMillis();
|
||||
s.attempts = 0;
|
||||
}
|
||||
if (!s.authenticated) {
|
||||
s.stage = isRegistered(player.getName().getString()) ? Stage.LOGIN : Stage.REGISTER;
|
||||
showCurrentDialog(player, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Called from the login mixin before the player enters play state. */
|
||||
public LoginRoute resolveLoginRoute(String username) {
|
||||
if (!config.enabled || !config.premiumAutoLogin) return LoginRoute.PREMIUM;
|
||||
PremiumResult result = premiumStatus(username);
|
||||
@@ -74,7 +134,7 @@ public final class AuthManager {
|
||||
}
|
||||
|
||||
public PremiumResult premiumStatus(String username) {
|
||||
String key = username.toLowerCase(Locale.ROOT);
|
||||
String key = key(username);
|
||||
PremiumCache cached = premiumCache.get(key);
|
||||
long now = System.currentTimeMillis();
|
||||
if (cached != null && now - cached.checkedAt < 86_400_000L) return cached.result;
|
||||
@@ -111,20 +171,41 @@ public final class AuthManager {
|
||||
if (!config.enabled) return;
|
||||
String name = player.getName().getString();
|
||||
boolean local = LobbySystemsMod.consumeLocalLogin(name);
|
||||
if (!local) {
|
||||
sessions.put(player.getUUID(), new Session(true, true, 0, System.currentTimeMillis(), player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot()));
|
||||
player.sendSystemMessage(Component.literal("§aLobby Systems: Premium account verified. Auto-login successful."));
|
||||
boolean premium = !local;
|
||||
Session s = new Session(false, premium, 0, System.currentTimeMillis(), player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot());
|
||||
sessions.put(player.getUUID(), s);
|
||||
|
||||
Account account = account(name);
|
||||
if (premium) {
|
||||
ensurePremiumMetadata(player, account);
|
||||
account = account(name);
|
||||
}
|
||||
|
||||
if (needsRules(account)) {
|
||||
s.stage = Stage.RULES;
|
||||
showDialog(player, DIALOG_RULES);
|
||||
return;
|
||||
}
|
||||
Session s = new Session(false, false, 0, System.currentTimeMillis(), player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot());
|
||||
sessions.put(player.getUUID(), s);
|
||||
sendPrompt(player, true);
|
||||
|
||||
continueAfterRules(player);
|
||||
}
|
||||
|
||||
public void onDisconnect(UUID uuid) { sessions.remove(uuid); }
|
||||
public boolean isAuthenticated(ServerPlayer player) { Session s = sessions.get(player.getUUID()); return !config.enabled || (s != null && s.authenticated); }
|
||||
public boolean isPremium(ServerPlayer player) { Session s = sessions.get(player.getUUID()); return s != null && s.premium; }
|
||||
public boolean isRegistered(String username) { synchronized (accounts) { return accounts.containsKey(username.toLowerCase(Locale.ROOT)); } }
|
||||
|
||||
public boolean isAuthenticated(ServerPlayer player) {
|
||||
Session s = sessions.get(player.getUUID());
|
||||
return !config.enabled || (s != null && s.authenticated);
|
||||
}
|
||||
|
||||
public boolean isPremium(ServerPlayer player) {
|
||||
Session s = sessions.get(player.getUUID());
|
||||
return s != null && s.premium;
|
||||
}
|
||||
|
||||
public boolean isRegistered(String username) {
|
||||
Account a = account(username);
|
||||
return a != null && a.hasPassword();
|
||||
}
|
||||
|
||||
public void tick(MinecraftServer server) {
|
||||
if (!config.enabled) return;
|
||||
@@ -136,84 +217,198 @@ public final class AuthManager {
|
||||
player.connection.disconnect(Component.literal("Authentication timed out. Please reconnect and try again."));
|
||||
continue;
|
||||
}
|
||||
// Server-authoritative freeze. This also prevents walking into Lobby portals before authentication.
|
||||
// Keep the player at the safe Lobby authentication position. World safety is
|
||||
// intentionally handled by the configured Lobby dimension itself.
|
||||
if (player.distanceToSqr(s.x, s.y, s.z) > 0.01) {
|
||||
player.connection.teleport(s.x, s.y, s.z, s.yaw, s.pitch);
|
||||
}
|
||||
if (now - s.lastReminder >= config.reminderIntervalSeconds * 1000L) {
|
||||
s.lastReminder = now;
|
||||
sendPrompt(player, false);
|
||||
showCurrentDialog(player, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int register(ServerPlayer player, String password, String confirm) {
|
||||
if (!config.enabled) return fail(player, "§cAuthentication is disabled.");
|
||||
/** Called by the rules dialog's command-template action. */
|
||||
public int acceptRules(ServerPlayer player, boolean accepted) {
|
||||
Session s = sessions.get(player.getUUID());
|
||||
if (s != null && s.premium) return fail(player, "§ePremium accounts do not need local registration.");
|
||||
if (isAuthenticated(player)) return fail(player, "§eYou are already authenticated.");
|
||||
String name = player.getName().getString();
|
||||
PremiumResult premium = premiumStatus(name);
|
||||
if (premium != PremiumResult.NOT_PREMIUM) return fail(player, premium == PremiumResult.PREMIUM ? "§cThis username belongs to a premium Minecraft account." : "§cPremium username verification is temporarily unavailable. Try again later.");
|
||||
if (!password.equals(confirm)) return fail(player, "§cPasswords do not match.");
|
||||
if (password.length() < config.minimumPasswordLength) return fail(player, "§cPassword must be at least " + config.minimumPasswordLength + " characters.");
|
||||
if (s == null || s.authenticated || s.stage != Stage.RULES) return 0;
|
||||
if (!accepted) {
|
||||
player.sendSystemMessage(Component.literal("§cYou must accept the server rules to continue."));
|
||||
showDialog(player, DIALOG_RULES);
|
||||
return 0;
|
||||
}
|
||||
|
||||
synchronized (accounts) {
|
||||
String key = name.toLowerCase(Locale.ROOT);
|
||||
if (accounts.containsKey(key)) return fail(player, "§cThis local account is already registered. Use /login <password>.");
|
||||
accounts.put(key, hash(password));
|
||||
String name = player.getName().getString();
|
||||
Account a = accounts.computeIfAbsent(key(name), ignored -> Account.metadataOnly(s.premium));
|
||||
a.accountType = s.premium ? "PREMIUM" : "LOCAL";
|
||||
if (s.premium) a.officialUuid = player.getUUID().toString();
|
||||
a.rulesVersionAccepted = config.rulesVersion;
|
||||
a.rulesAcceptedAt = Instant.now().toString();
|
||||
saveAccounts();
|
||||
}
|
||||
authenticate(player, false);
|
||||
continueAfterRules(player);
|
||||
return 1;
|
||||
}
|
||||
|
||||
public int register(ServerPlayer player, String password, String confirm) {
|
||||
if (!config.enabled) return failAndReopen(player, "§cAuthentication is disabled.", DIALOG_REGISTER);
|
||||
Session s = sessions.get(player.getUUID());
|
||||
if (s == null || s.authenticated) return failAndReopen(player, "§eYou are already authenticated.", DIALOG_REGISTER);
|
||||
if (s.stage == Stage.RULES) return failAndReopen(player, "§cPlease accept the server rules first.", DIALOG_RULES);
|
||||
if (s.premium && !config.requireLoginEveryJoin) return fail(player, "§ePremium accounts use Mojang authentication and do not need a local password.");
|
||||
|
||||
String name = player.getName().getString();
|
||||
if (!s.premium) {
|
||||
PremiumResult premium = premiumStatus(name);
|
||||
if (premium != PremiumResult.NOT_PREMIUM) {
|
||||
return failAndReopen(player,
|
||||
premium == PremiumResult.PREMIUM ? "§cThis username belongs to a premium Minecraft account." : "§cPremium username verification is temporarily unavailable. Try again later.",
|
||||
DIALOG_REGISTER);
|
||||
}
|
||||
}
|
||||
if (!password.equals(confirm)) return failAndReopen(player, "§cPasswords do not match.", DIALOG_REGISTER);
|
||||
if (password.length() < config.minimumPasswordLength) return failAndReopen(player, "§cPassword must be at least " + config.minimumPasswordLength + " characters.", DIALOG_REGISTER);
|
||||
|
||||
synchronized (accounts) {
|
||||
String k = key(name);
|
||||
Account existing = accounts.get(k);
|
||||
if (existing != null && existing.hasPassword()) return failAndReopen(player, "§cThis account is already registered. Please sign in.", DIALOG_LOGIN);
|
||||
Account hashed = hash(password);
|
||||
if (existing != null) {
|
||||
hashed.accountType = existing.accountType;
|
||||
hashed.officialUuid = existing.officialUuid;
|
||||
hashed.rulesVersionAccepted = existing.rulesVersionAccepted;
|
||||
hashed.rulesAcceptedAt = existing.rulesAcceptedAt;
|
||||
} else {
|
||||
hashed.accountType = s.premium ? "PREMIUM" : "LOCAL";
|
||||
hashed.officialUuid = s.premium ? player.getUUID().toString() : null;
|
||||
}
|
||||
accounts.put(k, hashed);
|
||||
saveAccounts();
|
||||
}
|
||||
authenticate(player);
|
||||
player.sendSystemMessage(Component.literal("§aRegistration successful. You are now logged in."));
|
||||
return 1;
|
||||
}
|
||||
|
||||
public int login(ServerPlayer player, String password) {
|
||||
Session s = sessions.get(player.getUUID());
|
||||
if (s != null && s.premium) return fail(player, "§ePremium account is already authenticated.");
|
||||
if (isAuthenticated(player)) return fail(player, "§eYou are already authenticated.");
|
||||
Account account;
|
||||
synchronized (accounts) { account = accounts.get(player.getName().getString().toLowerCase(Locale.ROOT)); }
|
||||
if (account == null) return fail(player, "§cAccount not registered. Use /register <password> <password>.");
|
||||
if (s == null || s.authenticated) return fail(player, "§eYou are already authenticated.");
|
||||
if (s.stage == Stage.RULES) return failAndReopen(player, "§cPlease accept the server rules first.", DIALOG_RULES);
|
||||
|
||||
Account account = account(player.getName().getString());
|
||||
if (account == null || !account.hasPassword()) return failAndReopen(player, "§cAccount not registered. Please create a password first.", DIALOG_REGISTER);
|
||||
if (!verify(password, account)) {
|
||||
if (s != null && ++s.attempts >= config.maxLoginAttempts) {
|
||||
if (++s.attempts >= config.maxLoginAttempts) {
|
||||
player.connection.disconnect(Component.literal("Too many failed login attempts."));
|
||||
return 0;
|
||||
}
|
||||
return fail(player, "§cIncorrect password.");
|
||||
int remaining = Math.max(0, config.maxLoginAttempts - s.attempts);
|
||||
return failAndReopen(player, "§cIncorrect password. §7" + remaining + " attempt(s) remaining.", DIALOG_LOGIN);
|
||||
}
|
||||
authenticate(player, false);
|
||||
authenticate(player);
|
||||
player.sendSystemMessage(Component.literal("§aLogin successful. Welcome back!"));
|
||||
return 1;
|
||||
}
|
||||
|
||||
private void authenticate(ServerPlayer player, boolean premium) {
|
||||
Session old = sessions.get(player.getUUID());
|
||||
sessions.put(player.getUUID(), new Session(true, premium, 0, old == null ? System.currentTimeMillis() : old.joinedAt,
|
||||
player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot()));
|
||||
player.connection.send(new ClientboundSetTitleTextPacket(Component.literal("§aAUTHENTICATED")));
|
||||
player.connection.send(new ClientboundSetSubtitleTextPacket(Component.literal("§fWelcome, " + player.getName().getString() + "!")));
|
||||
private void continueAfterRules(ServerPlayer player) {
|
||||
Session s = sessions.get(player.getUUID());
|
||||
if (s == null) return;
|
||||
|
||||
if (s.premium && !config.requireLoginEveryJoin) {
|
||||
authenticate(player);
|
||||
player.sendSystemMessage(Component.literal("§aLobby Systems: Premium account verified. Auto-login successful."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRegistered(player.getName().getString())) {
|
||||
s.stage = Stage.LOGIN;
|
||||
showDialog(player, DIALOG_LOGIN);
|
||||
} else {
|
||||
s.stage = Stage.REGISTER;
|
||||
showDialog(player, DIALOG_REGISTER);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendPrompt(ServerPlayer player, boolean chat) {
|
||||
boolean registered = isRegistered(player.getName().getString());
|
||||
String title = registered ? "§eLOGIN REQUIRED" : "§bREGISTER REQUIRED";
|
||||
String command = registered ? "/login <password>" : "/register <password> <password>";
|
||||
player.connection.send(new ClientboundSetTitleTextPacket(Component.literal(title)));
|
||||
player.connection.send(new ClientboundSetSubtitleTextPacket(Component.literal("§f" + command)));
|
||||
if (chat) player.sendSystemMessage(Component.literal("§6[Lobby Systems Auth] §f" + (registered ? "Please login: §e" : "Please register: §b") + command));
|
||||
private void authenticate(ServerPlayer player) {
|
||||
Session s = sessions.get(player.getUUID());
|
||||
if (s == null) return;
|
||||
s.authenticated = true;
|
||||
s.stage = Stage.AUTHENTICATED;
|
||||
s.attempts = 0;
|
||||
clearDialog(player);
|
||||
}
|
||||
|
||||
private static int fail(ServerPlayer p, String msg) { p.sendSystemMessage(Component.literal(msg)); return 0; }
|
||||
private boolean needsRules(Account account) {
|
||||
return config.rulesEnabled && (account == null || account.rulesVersionAccepted < config.rulesVersion);
|
||||
}
|
||||
|
||||
private void ensurePremiumMetadata(ServerPlayer player, Account existing) {
|
||||
synchronized (accounts) {
|
||||
String k = key(player.getName().getString());
|
||||
Account a = existing;
|
||||
if (a == null) {
|
||||
a = Account.metadataOnly(true);
|
||||
accounts.put(k, a);
|
||||
}
|
||||
a.accountType = "PREMIUM";
|
||||
a.officialUuid = player.getUUID().toString();
|
||||
saveAccounts();
|
||||
}
|
||||
}
|
||||
|
||||
private void showCurrentDialog(ServerPlayer player, Session s) {
|
||||
switch (s.stage) {
|
||||
case RULES -> showDialog(player, DIALOG_RULES);
|
||||
case REGISTER -> showDialog(player, DIALOG_REGISTER);
|
||||
case LOGIN -> showDialog(player, DIALOG_LOGIN);
|
||||
default -> { }
|
||||
}
|
||||
}
|
||||
|
||||
private void showDialog(ServerPlayer player, String id) {
|
||||
try {
|
||||
String command = "dialog show " + player.getGameProfile().name() + " " + id;
|
||||
player.level().getServer().getCommands().getDispatcher().execute(command, player.level().getServer().createCommandSourceStack());
|
||||
} catch (Exception e) {
|
||||
LobbySystemsMod.LOGGER.error("Failed to show auth dialog {} to {}", id, player.getName().getString(), e);
|
||||
player.sendSystemMessage(Component.literal("§cAuthentication UI could not be opened. Please reconnect or contact staff."));
|
||||
}
|
||||
}
|
||||
|
||||
private void clearDialog(ServerPlayer player) {
|
||||
try {
|
||||
String command = "dialog clear " + player.getGameProfile().name();
|
||||
player.level().getServer().getCommands().getDispatcher().execute(command, player.level().getServer().createCommandSourceStack());
|
||||
} catch (Exception e) {
|
||||
LobbySystemsMod.LOGGER.debug("Failed to clear auth dialog for {}: {}", player.getName().getString(), e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private int failAndReopen(ServerPlayer player, String message, String dialog) {
|
||||
player.sendSystemMessage(Component.literal(message));
|
||||
showDialog(player, dialog);
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int fail(ServerPlayer p, String msg) {
|
||||
p.sendSystemMessage(Component.literal(msg));
|
||||
return 0;
|
||||
}
|
||||
|
||||
private Account hash(String password) {
|
||||
try {
|
||||
byte[] salt = new byte[16]; RANDOM.nextBytes(salt);
|
||||
byte[] salt = new byte[16];
|
||||
RANDOM.nextBytes(salt);
|
||||
PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, config.pbkdf2Iterations, 256);
|
||||
byte[] digest = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).getEncoded();
|
||||
spec.clearPassword();
|
||||
return new Account(Base64.getEncoder().encodeToString(salt), Base64.getEncoder().encodeToString(digest), config.pbkdf2Iterations);
|
||||
} catch (Exception e) { throw new IllegalStateException("Unable to hash password", e); }
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Unable to hash password", e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean verify(String password, Account a) {
|
||||
@@ -223,15 +418,98 @@ public final class AuthManager {
|
||||
byte[] actual = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).getEncoded();
|
||||
spec.clearPassword();
|
||||
return MessageDigest.isEqual(actual, Base64.getDecoder().decode(a.hash));
|
||||
} catch (Exception e) { return false; }
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void saveConfig() throws Exception { try (Writer w = Files.newBufferedWriter(ConfigPaths.AUTH_CONFIG)) { GSON.toJson(config, w); } }
|
||||
private void saveAccounts() { try { Files.createDirectories(ConfigPaths.AUTH_DIRECTORY); try (Writer w = Files.newBufferedWriter(ConfigPaths.AUTH_ACCOUNTS)) { GSON.toJson(accounts, w); } } catch (Exception e) { LobbySystemsMod.LOGGER.error("Failed saving auth accounts", e); } }
|
||||
private Account account(String username) {
|
||||
synchronized (accounts) {
|
||||
return accounts.get(key(username));
|
||||
}
|
||||
}
|
||||
|
||||
private void migrateAccounts() {
|
||||
boolean changed = false;
|
||||
synchronized (accounts) {
|
||||
for (Account a : accounts.values()) {
|
||||
if (a.accountType == null || a.accountType.isBlank()) {
|
||||
a.accountType = "LOCAL";
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) saveAccounts();
|
||||
}
|
||||
}
|
||||
|
||||
private static String key(String username) { return username.toLowerCase(Locale.ROOT); }
|
||||
|
||||
private void saveConfig() throws Exception {
|
||||
try (Writer w = Files.newBufferedWriter(ConfigPaths.AUTH_CONFIG)) { GSON.toJson(config, w); }
|
||||
}
|
||||
|
||||
private void saveAccounts() {
|
||||
try {
|
||||
Files.createDirectories(ConfigPaths.AUTH_DIRECTORY);
|
||||
try (Writer w = Files.newBufferedWriter(ConfigPaths.AUTH_ACCOUNTS)) { GSON.toJson(accounts, w); }
|
||||
} catch (Exception e) {
|
||||
LobbySystemsMod.LOGGER.error("Failed saving auth accounts", e);
|
||||
}
|
||||
}
|
||||
|
||||
public enum PremiumResult { PREMIUM, NOT_PREMIUM, UNKNOWN }
|
||||
public enum LoginRoute { PREMIUM, LOCAL, UNKNOWN }
|
||||
private enum Stage { RULES, REGISTER, LOGIN, AUTHENTICATED }
|
||||
private record PremiumCache(PremiumResult result, long checkedAt) {}
|
||||
private static final class Account { String salt; String hash; int iterations; Account(String salt,String hash,int iterations){this.salt=salt;this.hash=hash;this.iterations=iterations;} }
|
||||
private static final class Session { boolean authenticated; boolean premium; int attempts; long joinedAt; long lastReminder; double x,y,z; float yaw,pitch; Session(boolean a,boolean p,int at,long j,double x,double y,double z,float yaw,float pitch){authenticated=a;premium=p;attempts=at;joinedAt=j;lastReminder=0;this.x=x;this.y=y;this.z=z;this.yaw=yaw;this.pitch=pitch;} }
|
||||
|
||||
private static final class Account {
|
||||
String salt;
|
||||
String hash;
|
||||
int iterations;
|
||||
String accountType;
|
||||
String officialUuid;
|
||||
int rulesVersionAccepted;
|
||||
String rulesAcceptedAt;
|
||||
|
||||
Account(String salt, String hash, int iterations) {
|
||||
this.salt = salt;
|
||||
this.hash = hash;
|
||||
this.iterations = iterations;
|
||||
}
|
||||
|
||||
static Account metadataOnly(boolean premium) {
|
||||
Account a = new Account(null, null, 0);
|
||||
a.accountType = premium ? "PREMIUM" : "LOCAL";
|
||||
return a;
|
||||
}
|
||||
|
||||
boolean hasPassword() {
|
||||
return salt != null && !salt.isBlank() && hash != null && !hash.isBlank() && iterations > 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Session {
|
||||
boolean authenticated;
|
||||
final boolean premium;
|
||||
int attempts;
|
||||
long joinedAt;
|
||||
long lastReminder;
|
||||
final double x, y, z;
|
||||
final float yaw, pitch;
|
||||
Stage stage = Stage.RULES;
|
||||
|
||||
Session(boolean authenticated, boolean premium, int attempts, long joinedAt,
|
||||
double x, double y, double z, float yaw, float pitch) {
|
||||
this.authenticated = authenticated;
|
||||
this.premium = premium;
|
||||
this.attempts = attempts;
|
||||
this.joinedAt = joinedAt;
|
||||
this.lastReminder = 0;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.yaw = yaw;
|
||||
this.pitch = pitch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,19 +247,29 @@ public final class LobbySystemsMod implements ModInitializer {
|
||||
ctx.getSource().getPlayerOrException(), ctx.getSource().getServer())));
|
||||
|
||||
dispatcher.register(Commands.literal("register")
|
||||
.then(Commands.argument("password", StringArgumentType.word())
|
||||
.then(Commands.argument("confirm", StringArgumentType.word())
|
||||
.then(Commands.argument("password", StringArgumentType.string())
|
||||
.then(Commands.argument("confirm", StringArgumentType.string())
|
||||
.executes(ctx -> AUTH.register(
|
||||
ctx.getSource().getPlayerOrException(),
|
||||
StringArgumentType.getString(ctx, "password"),
|
||||
StringArgumentType.getString(ctx, "confirm"))))));
|
||||
|
||||
dispatcher.register(Commands.literal("login")
|
||||
.then(Commands.argument("password", StringArgumentType.word())
|
||||
.then(Commands.argument("password", StringArgumentType.string())
|
||||
.executes(ctx -> AUTH.login(
|
||||
ctx.getSource().getPlayerOrException(),
|
||||
StringArgumentType.getString(ctx, "password")))));
|
||||
|
||||
// Internal command used by the vanilla dialog command-template action.
|
||||
// It intentionally has no operator requirement, but AuthManager only accepts it
|
||||
// while the calling player is in the RULES authentication stage.
|
||||
dispatcher.register(Commands.literal("lobbyauth")
|
||||
.then(Commands.literal("rules")
|
||||
.then(Commands.argument("accepted", com.mojang.brigadier.arguments.BoolArgumentType.bool())
|
||||
.executes(ctx -> AUTH.acceptRules(
|
||||
ctx.getSource().getPlayerOrException(),
|
||||
com.mojang.brigadier.arguments.BoolArgumentType.getBool(ctx, "accepted"))))));
|
||||
|
||||
dispatcher.register(Commands.literal("ls")
|
||||
.requires(Commands.hasPermission(Commands.LEVEL_ADMINS))
|
||||
.then(Commands.literal("reload")
|
||||
@@ -581,6 +591,13 @@ public final class LobbySystemsMod implements ModInitializer {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!AUTH.load()) {
|
||||
source.sendFailure(Component.literal("§eConfig utama direload, tetapi auth config/accounts bermasalah. Cek console."));
|
||||
LobbyLog.state("RELOAD FAILED", "Reason: auth config/accounts gagal dibaca");
|
||||
return 0;
|
||||
}
|
||||
AUTH.applyReload(server);
|
||||
|
||||
POSITIONS.setExcludedDimension(key);
|
||||
boolean portalOk = PORTALS.load();
|
||||
if (!portalOk) {
|
||||
|
||||
Reference in New Issue
Block a user