From 8b07b17e5fbe721d40d4d35ca56638eedb0e9cec Mon Sep 17 00:00:00 2001 From: fretuks Date: Thu, 9 Jul 2026 08:43:39 +0200 Subject: [PATCH] implement authentication and register correctly --- pom.xml | 5 + .../frederik/promotion/DatabaseService.java | 22 +-- .../resources/META-INF/resources/auth.html | 79 +++++++++++ src/main/resources/META-INF/resources/auth.js | 132 ++++++++++++++++++ .../resources/META-INF/resources/index.html | 2 +- .../META-INF/resources/register.html | 78 +++++++++++ .../resources/META-INF/resources/register.js | 105 ++++++++++++++ 7 files changed, 414 insertions(+), 9 deletions(-) create mode 100644 src/main/resources/META-INF/resources/auth.html create mode 100644 src/main/resources/META-INF/resources/auth.js create mode 100644 src/main/resources/META-INF/resources/register.html create mode 100644 src/main/resources/META-INF/resources/register.js diff --git a/pom.xml b/pom.xml index 8e4663b..f5ac8c8 100644 --- a/pom.xml +++ b/pom.xml @@ -67,6 +67,11 @@ rest-assured test + + io.quarkus + quarkus-jdbc-h2 + test + diff --git a/src/main/java/dev/frederik/promotion/DatabaseService.java b/src/main/java/dev/frederik/promotion/DatabaseService.java index a576bb0..fbbc340 100644 --- a/src/main/java/dev/frederik/promotion/DatabaseService.java +++ b/src/main/java/dev/frederik/promotion/DatabaseService.java @@ -79,10 +79,9 @@ public class DatabaseService { private void seed(Connection connection) throws SQLException { List platforms = List.of("Windows", "Linux", "Mobile (Android)"); - try (PreparedStatement insert = connection.prepareStatement("INSERT INTO platforms (name) VALUES (?) ON CONFLICT (name) DO NOTHING")) { - for (String platform : platforms) { - insert.setString(1, platform); - insert.executeUpdate(); + for (String platform : platforms) { + if (findOne("SELECT id FROM platforms WHERE name = ?", platform) == null) { + execute(connection, "INSERT INTO platforms (name) VALUES (?)", platform); } } @@ -175,9 +174,12 @@ public class DatabaseService { } private static long insertAndReturnId(Connection connection, String sql, Object... params) throws SQLException { - try (PreparedStatement statement = prepare(connection, sql + " RETURNING id", params); - ResultSet resultSet = statement.executeQuery()) { - return resultSet.next() ? resultSet.getLong(1) : 0; + try (PreparedStatement statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + bind(statement, params); + statement.executeUpdate(); + try (ResultSet resultSet = statement.getGeneratedKeys()) { + return resultSet.next() ? resultSet.getLong(1) : 0; + } } } @@ -189,9 +191,13 @@ public class DatabaseService { private static PreparedStatement prepare(Connection connection, String sql, Object... params) throws SQLException { PreparedStatement statement = connection.prepareStatement(sql); + bind(statement, params); + return statement; + } + + private static void bind(PreparedStatement statement, Object... params) throws SQLException { for (int i = 0; i < params.length; i++) { statement.setObject(i + 1, params[i]); } - return statement; } } diff --git a/src/main/resources/META-INF/resources/auth.html b/src/main/resources/META-INF/resources/auth.html new file mode 100644 index 0000000..d303f1f --- /dev/null +++ b/src/main/resources/META-INF/resources/auth.html @@ -0,0 +1,79 @@ + + + + + + Kernel Panic - Auth + + + + + +
+
+
+

registered player auth

+

Login to the private boot log

+

+ Use the same username and email from pre-registration. Authenticated + players can inspect extra game intel before launch. +

+
+ +
+
+ + +
+ + +
+

+
+ +
+ session + not authenticated + role: none +
+
+
+ +
+
+

authenticated intel

+

Game details unlocked after login

+
+
+
+

Access required

+

Log in with your registered shell account to reveal private build notes.

+
+
+
+
+ +
+ auth mode: bearer jwt + registered players only +
+ + + + diff --git a/src/main/resources/META-INF/resources/auth.js b/src/main/resources/META-INF/resources/auth.js new file mode 100644 index 0000000..ce41265 --- /dev/null +++ b/src/main/resources/META-INF/resources/auth.js @@ -0,0 +1,132 @@ +const authForm = document.querySelector("#auth-form"); +const authMessage = document.querySelector("#auth-message"); +const authSession = document.querySelector("#auth-session"); +const authLogout = document.querySelector("#auth-logout"); +const privateIntel = document.querySelector("#private-intel"); +const tokenStorageKey = "kernel-panic-player-token"; + +function setAuthMessage(message, isError = false) { + authMessage.textContent = message; + authMessage.classList.toggle("error", isError); +} + +async function requestJson(url, options = {}) { + const response = await fetch(url, { + headers: { + "Content-Type": "application/json", + ...(options.headers || {}) + }, + ...options + }); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || "Request failed."); + } + + return data; +} + +function authHeaders() { + const token = localStorage.getItem(tokenStorageKey); + return token ? { Authorization: `Bearer ${token}` } : {}; +} + +function renderAuthSession(username, groups = []) { + const groupText = groups.length > 0 ? groups.join(", ") : "none"; + authSession.innerHTML = ` + session + ${username || "not authenticated"} + role: ${groupText} + `; +} + +function renderLockedIntel() { + privateIntel.innerHTML = ` +
+

Access required

+

Log in with your registered shell account to reveal private build notes.

+
+ `; +} + +function renderGameDetails(details) { + privateIntel.innerHTML = ` +
+

Starting distros

+

${details.available_starting_distros.join(", ")}

+
+
+

Language pool

+

${details.starting_languages.join(", ")}

+
+
+

Hidden enemies

+

${details.hidden_enemies.join(", ")}

+
+
+

Economy

+

${Object.entries(details.economy).map(([name, text]) => `${name}: ${text}`).join(" / ")}

+
+
+

${details.title}

+

${details.developer_note}

+
+ `; +} + +async function refreshAuthSession() { + const token = localStorage.getItem(tokenStorageKey); + if (!token) { + renderAuthSession(null); + renderLockedIntel(); + return; + } + + try { + const [session, details] = await Promise.all([ + requestJson("/auth/me", { headers: authHeaders() }), + requestJson("/auth/game-details", { headers: authHeaders() }) + ]); + renderAuthSession(session.username, session.groups || []); + renderGameDetails(details); + } catch (error) { + localStorage.removeItem(tokenStorageKey); + renderAuthSession(null); + renderLockedIntel(); + } +} + +authForm.addEventListener("submit", async (event) => { + event.preventDefault(); + setAuthMessage("Checking registered shell account..."); + + const formData = new FormData(authForm); + try { + const result = await requestJson("/auth/login", { + method: "POST", + body: JSON.stringify({ + username: formData.get("username"), + email: formData.get("email") + }) + }); + localStorage.setItem(tokenStorageKey, result.access_token); + authForm.reset(); + setAuthMessage("Authenticated. Private game intel unlocked."); + await refreshAuthSession(); + } catch (error) { + localStorage.removeItem(tokenStorageKey); + renderAuthSession(null); + renderLockedIntel(); + setAuthMessage(error.message, true); + } +}); + +authLogout.addEventListener("click", () => { + localStorage.removeItem(tokenStorageKey); + renderAuthSession(null); + renderLockedIntel(); + setAuthMessage("Session cleared."); +}); + +refreshAuthSession(); diff --git a/src/main/resources/META-INF/resources/index.html b/src/main/resources/META-INF/resources/index.html index 6eebd05..b019a5e 100644 --- a/src/main/resources/META-INF/resources/index.html +++ b/src/main/resources/META-INF/resources/index.html @@ -182,7 +182,7 @@
-

Rott Credits

+

Root Credits

Premium currency converts to pull currency for banners and cosmetics, not direct stat purchases.

diff --git a/src/main/resources/META-INF/resources/register.html b/src/main/resources/META-INF/resources/register.html new file mode 100644 index 0000000..9279654 --- /dev/null +++ b/src/main/resources/META-INF/resources/register.html @@ -0,0 +1,78 @@ + + + + + + Kernel Panic - Register + + + + + +
+
+
+

pre-register

+

Reserve your shell account

+

+ Choose your launch platform, create a shell account, then use the same + username and email on the auth page to reveal private game details. +

+
+ +
+ + +
+ Platforms +
+
+ + +

+
+
+ +
+
+ 0 + registered platform installs +
+
+ 3 + launch platforms +
+
+ 2 + known rewards +
+
+
+ +
+ supported targets: windows / linux / android + auth unlocks after registration +
+ + + + diff --git a/src/main/resources/META-INF/resources/register.js b/src/main/resources/META-INF/resources/register.js new file mode 100644 index 0000000..2ca4e93 --- /dev/null +++ b/src/main/resources/META-INF/resources/register.js @@ -0,0 +1,105 @@ +const platformList = document.querySelector("#platform-list"); +const registerForm = document.querySelector("#register-form"); +const formMessage = document.querySelector("#form-message"); +const registrationCount = document.querySelector("#registration-count"); +const platformCount = document.querySelector("#platform-count"); +const rewardCount = document.querySelector("#reward-count"); + +function setMessage(message, isError = false) { + formMessage.textContent = message; + formMessage.classList.toggle("error", isError); +} + +async function requestJson(url, options = {}) { + const response = await fetch(url, { + headers: { + "Content-Type": "application/json", + ...(options.headers || {}) + }, + ...options + }); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || "Request failed."); + } + + return data; +} + +function renderPlatforms(platforms) { + platformList.innerHTML = ""; + platforms.forEach((platform) => { + const label = document.createElement("label"); + label.className = "platform-option"; + label.innerHTML = ` + + ${platform.name} + `; + platformList.appendChild(label); + }); + platformCount.textContent = platforms.length; +} + +async function refreshStats() { + const [count, rewards] = await Promise.all([ + requestJson("/registrations/count"), + requestJson("/rewards") + ]); + registrationCount.textContent = count.count; + rewardCount.textContent = rewards.length; +} + +async function loadPageData() { + try { + const platforms = await requestJson("/platforms"); + renderPlatforms(platforms); + await refreshStats(); + } catch (error) { + setMessage(error.message, true); + } +} + +registerForm.addEventListener("submit", async (event) => { + event.preventDefault(); + setMessage("Opening shell account..."); + + const formData = new FormData(registerForm); + const selectedPlatforms = formData.getAll("platforms").map(Number); + + if (selectedPlatforms.length === 0) { + setMessage("Choose at least one platform.", true); + return; + } + + try { + const user = await requestJson("/users", { + method: "POST", + body: JSON.stringify({ + username: formData.get("username"), + email: formData.get("email"), + newsletter_optin: formData.get("newsletter_optin") === "on" + }) + }); + + await Promise.all( + selectedPlatforms.map((platformId) => + requestJson("/registrations", { + method: "POST", + body: JSON.stringify({ + user_id: user.id, + platform_id: platformId + }) + }) + ) + ); + + registerForm.reset(); + setMessage("Pre-registration complete. Use Auth to unlock private game intel."); + await refreshStats(); + } catch (error) { + setMessage(error.message, true); + } +}); + +loadPageData();