Finalize frontend of session management

This commit is contained in:
2025-06-26 19:45:20 +03:00
parent 169c4faa7d
commit 3da7f369a5
9 changed files with 235 additions and 149 deletions

View File

@@ -1,6 +1,7 @@
source "https://rubygems.org"
gem "base64"
gem "erb"
gem "json"
gem "net-http"
gem "sequel"

View File

@@ -3,6 +3,7 @@ GEM
specs:
base64 (0.3.0)
bigdecimal (3.2.2)
erb (5.0.1)
json (2.12.2)
logger (1.7.0)
mustermann (3.0.3)
@@ -38,6 +39,7 @@ PLATFORMS
DEPENDENCIES
base64
erb
json
net-http
sequel

View File

@@ -6,7 +6,8 @@
<meta
name="viewport"
content="initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
/>
/>
<meta name="signed_in" content="<%= @signed_in %>">
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
@@ -15,6 +16,7 @@
/>
<link rel="stylesheet" href="src/assets/style.css" />
<script type="module" src="src/js/accounts.js"></script>
<script type="module" src="src/js/game.js"></script>
</head>
<body>
<div class="header">
@@ -26,16 +28,16 @@
/>
</a>
<div class="header-right">
<button class="account-button" id="login-button">LOGIN</button>
<button class="account-button" id="signup-button">SIGN UP</button>
<button class="account-button" id="logout-button">LOGOUT</button>
<button class="account-button" id="forgot-button">
FORGOT - DEBUG
</button>
<div class="signed_in">
<button class="account-button" id="logout-button">LOGOUT</button>
</div>
<div class="not_signed_i">
<button class="account-button" id="login-button">LOGIN</button>
<button class="account-button" id="signup-button">SIGN UP</button>
</div>
</div>
</div>
<canvas id="main-canvas"></canvas>
<script type="module" src="src/js/game.js"></script>
<div class="popup" id="popup">
<span class="close" id="close">&times;</span>
<div class="popup-tab" id="login">
@@ -45,6 +47,7 @@
<label for="pass">PASSWORD</label>
<input type="password" name="pass" placeholder="Password" required />
<button type="submit">LOGIN</button>
<span class="link" id="forgot-link">Forgot password?</span>
<span class="info" id="login-info"></span>
</form>
</div>
@@ -53,7 +56,13 @@
<label for="username">USERNAME</label>
<input type="text" name="username" placeholder="Username" required />
<label for="email">EMAIL</label>
<input type="email" name="email" placeholder="Email" required />
<input
type="email"
name="email"
placeholder="Email"
required
autocapitalize="off"
/>
<label for="pass">PASSWORD</label>
<input type="password" name="pass" placeholder="Password" required />
<button type="submit">SIGN UP</button>
@@ -62,7 +71,14 @@
</div>
<div class="popup-tab" id="forgot-pass">
<form id="forgot-form" class="form">
<input type="email" name="email" placeholder="Email" />
<input
type="email"
name="email"
placeholder="Email"
required
autocapitalize="off"
autocomplete="off"
/>
<button type="submit">SUBMIT</button>
<span class="info" id="forgot-info"></span>
</form>

View File

@@ -1,7 +1,8 @@
# module for logging
module Logman
def self.log(log)
File.write("log/main.log", "[#{Time.now}] #{log}\n", mode: "a")
file, line = caller(1, 1)[0].split(":")
File.write("log/main.log", "[#{Time.now}] {#{file}:#{line}} #{log}\n", mode: "a")
end
def self.imp(log)

36
main.rb
View File

@@ -1,4 +1,5 @@
require "base64"
require "erb"
require "json"
require "net/http"
require "sequel"
@@ -19,7 +20,7 @@ env_data.each_line do |line|
ENV_HASH[match[1]] = match[2]
end
Logman.log ENV_HASH.inspect
# Logman.log ENV_HASH.inspect
CODE_ENV = :dev
@@ -36,8 +37,9 @@ set :public_folder, "public"
get "/" do
session = Sessions.new request, response
Logman.log session.message
send_file "index.html"
Logman.log session.message if session.message != ""
@signed_in = session.signed_in?.nil? ? false : true
ERB.new(File.read("index.erb")).result(binding)
end
get "/debug" do
@@ -48,7 +50,7 @@ end
post "/signup" do
session = Sessions.new request, response
uid = session["user"]
session.logout(uid) if uid.nil? || $active_users[uid].nil?
session.logout unless uid.nil? || $active_users[uid].nil?
data = JSON.parse(request.body.read)
if data["email"].nil? || data["pass"].nil? || data["username"].nil?
status 400
@@ -57,8 +59,13 @@ post "/signup" do
signup_status = Players.mk_player(data["username"], data["email"], data["pass"])
if signup_status[0] == 200
login_status = session.login(data["username"], data["pass"])
status login_status[0]
return { "message" => login_status[1] }.to_json
if login_status[0] == 200
status 200
return { "message" => login_status[1], "success" => "true" }.to_json
else
status login_status[0]
return { "message" => login_status[1] }.to_json
end
end
status signup_status[0]
return { "message" => signup_status[1] }.to_json
@@ -74,7 +81,7 @@ post "/login" do
data = JSON.parse(request.body.read)
session = Sessions.new request, response
uid = session["user"]
if $active_users[uid] && !session.logout(uid)
if $active_users[uid] && !session.logout
status 500
return { "message" => "Internal server error when signing the existing session out!" }.to_json
end
@@ -83,8 +90,13 @@ post "/login" do
return { "message" => "Bad request made!" }.to_json
end
login_status = session.login(data["username"], data["pass"])
status login_status[0]
return { "message" => login_status[1] }.to_json
if login_status[0] == 200
status 200
return { "message" => login_status[1], "success" => "true" }.to_json
else
status login_status[0]
return { "message" => login_status[1] }.to_json
end
end
post "/logout" do
@@ -94,7 +106,7 @@ post "/logout" do
status 400
return { "message" => "Not signed in!" }.to_json
end
unless session.logout(uid)
unless session.logout
status 500
return { "message" => "Internal server error when signing you out!" }.to_json
end
@@ -105,7 +117,7 @@ end
get "/logout" do
session = Sessions.new request, response
uid = session["user"]
session.logout(uid) unless $active_users[uid].nil?
session.logout unless $active_users[uid].nil?
redirect "/"
end
@@ -165,7 +177,7 @@ delete "/rm_player" do
status 400
return { "message" => "Not signed in!" }.to_json
end
if session.logout(uid) && Players.rm_player($active_users[uid])
if session.logout && Players.rm_player($active_users[uid])
status 200
return { "message" => "Sorry to see you go.." }.to_json
else

View File

@@ -46,7 +46,7 @@ module Players
code = Array.new(24) { ALPHANUM.sample }.join
DB["update Players set new_pass_code = ? where email = ?", code, email].update
Logman.log "Pass req: #{email} & #{code}\n"
Logman.log "Pass req: #{email} & #{code}"
true
@@ -76,7 +76,7 @@ module Players
def self.authorized?(username, pass)
digest = XXhash.xxh32(pass, ENV_HASH["SALT"])
Logman.log "Authorized: #{username} & #{digest}\n"
Logman.log "Authorized: #{username} & #{digest}"
player = self[username]
player && player[:digest].to_i == digest.to_i ? player : false
end
@@ -93,7 +93,7 @@ module Players
rm_player(player[:username]) if player[:created_at] + 24 * 60 * 60 < Time.now
end
rescue StandardError => e
Logman.log "Thread error: #{e.message}\n"
Logman.log "Thread error: #{e.message}"
ensure
sleep 60 * 60
end

View File

@@ -21,6 +21,27 @@ button {
font-size: 18px;
}
input {
outline: none;
font-family: "WDXL Lubrifont JP N", sans-serif;
font-size: 18px;
background-color: white;
color: #1b262c;
border: none;
}
.link {
color: #90bdd9;
text-decoration: none;
cursor: pointer;
font-family: "WDXL Lubrifont JP N", sans-serif;
}
.link:hover {
color: #adcee3;
text-decoration: underline;
}
.logo {
height: 50px;
position: relative;
@@ -72,9 +93,10 @@ button {
.info {
margin-bottom: 10px;
color: red;
color: white;
font-weight: bold;
opacity: none;
font-family: "WDXL Lubrifont JP N", sans-serif;
}
.info:empty::before {
@@ -132,6 +154,21 @@ button {
margin-right: 20px;
}
.account-button {
background: none;
border: none;
cursor: pointer;
color: #90bdd9;
font-size: 23px;
margin-right: 10px;
font-family: "WDXL Lubrifont JP N", sans-serif;
}
.account-button:hover {
color: #adcee3;
text-decoration: underline;
}
.pixelart {
image-rendering: pixelated;
image-rendering: crisp-edges;

View File

@@ -1,130 +1,145 @@
window.onload = async () => {
const login_form = document.getElementById("login-form");
const login_button = document.getElementById("login-button");
const signup_form = document.getElementById("signup-form");
const signup_button = document.getElementById("signup-button");
const logout_button = document.getElementById("logout-button");
const forgot_button = document.getElementById("forgot-button");
const forgot_form = document.getElementById("forgot-form");
const popup = document.getElementById("popup");
const loginSection = document.getElementById("login");
const signupSection = document.getElementById("signup");
const forgotPassSection = document.getElementById("forgot-pass");
const resetPassSection = document.getElementById("reset-pass");
forgot_button.onclick = () => {
document.getElementById("popup").classList.add("active");
document.getElementById("forgot-pass").classList.add("active");
document.getElementById("login").classList.remove("active");
document.getElementById("signup").classList.remove("active");
document.getElementById("reset-pass").classList.remove("active");
const loginForm = document.getElementById("login-form");
const signupForm = document.getElementById("signup-form");
const forgotForm = document.getElementById("forgot-form");
const resetForm = document.getElementById("reset-form");
const loginInfo = document.getElementById("login-info");
const signupInfo = document.getElementById("signup-info");
const forgotInfo = document.getElementById("forgot-info");
const resetInfo = document.getElementById("reset-info");
const loginButton = document.getElementById("login-button");
const signupButton = document.getElementById("signup-button");
const logoutButton = document.getElementById("logout-button");
const forgotButton = document.getElementById("forgot-link");
const closeButton = document.getElementById("close");
const signedInMeta = document.querySelector('meta[name="signed_in"]');
const isSignedIn = signedInMeta?.content === "true";
if (isSignedIn) {
loginButton.style.display = "none";
signupButton.style.display = "none";
} else {
logoutButton.style.display = "none";
}
const showPopup = (section) => {
popup.classList.add("active");
loginSection.classList.remove("active");
signupSection.classList.remove("active");
forgotPassSection.classList.remove("active");
resetPassSection.classList.remove("active");
if (section) section.classList.add("active");
};
forgot_form.onsubmit = async (e) => {
const hidePopup = () => {
popup.classList.remove("active");
loginSection.classList.remove("active");
signupSection.classList.remove("active");
forgotPassSection.classList.remove("active");
resetPassSection.classList.remove("active");
};
loginButton?.addEventListener("click", () => showPopup(loginSection));
signupButton?.addEventListener("click", () => showPopup(signupSection));
logoutButton?.addEventListener(
"click",
() => (window.location.href = "/logout"),
);
forgotButton?.addEventListener("click", () => showPopup(forgotPassSection));
closeButton?.addEventListener("click", hidePopup);
// TODO: if succesful hide form and popup success message
loginForm?.addEventListener("submit", async (e) => {
e.preventDefault();
const email = forgot_form.email.value;
const { username, pass } = loginForm;
const res = await fetch("/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: username.value, pass: pass.value }),
});
const data = await res.json();
loginInfo.innerText = data.message;
if (data.success == "true") {
loginButton.style.display = "none";
signupButton.style.display = "none";
logoutButton.style.display = "block";
}
});
// TODO: if succesful hide form and popup success message
signupForm?.addEventListener("submit", async (e) => {
e.preventDefault();
const { username, email, pass } = signupForm;
const res = await fetch("/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: username.value,
email: email.value,
pass: pass.value,
}),
});
const data = await res.json();
signupInfo.innerText = data.message;
if (data.success == "true") {
loginButton.style.display = "none";
signupButton.style.display = "none";
logoutButton.style.display = "block";
}
});
forgotForm?.addEventListener("submit", async (e) => {
e.preventDefault();
const email = forgotForm.email.value;
const res = await fetch("/forgot_password", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});
const data = await res.json();
document.getElementById("forgot-info").innerHTML = data.message;
};
logout_button.onclick = () => {
window.location.href = "/logout";
};
login_button.onclick = () => {
document.getElementById("popup").classList.add("active");
document.getElementById("login").classList.add("active");
document.getElementById("signup").classList.remove("active");
document.getElementById("forgot-pass").classList.remove("active");
document.getElementById("reset-pass").classList.remove("active");
};
signup_button.onclick = () => {
document.getElementById("popup").classList.add("active");
document.getElementById("signup").classList.add("active");
document.getElementById("login").classList.remove("active");
document.getElementById("forgot-pass").classList.remove("active");
document.getElementById("reset-pass").classList.remove("active");
};
document.getElementById("close").onclick = () => {
document.getElementById("popup").classList.remove("active");
document.getElementById("login").classList.remove("active");
document.getElementById("signup").classList.remove("active");
document.getElementById("forgot-pass").classList.remove("active");
document.getElementById("reset-pass").classList.remove("active");
};
login_form.onsubmit = async (e) => {
e.preventDefault();
const username = login_form.username.value;
const pass = login_form.pass.value;
let response = await fetch("/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username, pass }),
});
response = await response.json();
document.getElementById("login-info").innerText = response.message;
};
signup_form.onsubmit = async (e) => {
e.preventDefault();
const username = signup_form.username.value;
const email = signup_form.email.value;
const pass = signup_form.pass.value;
let response = await fetch("/signup", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username, email, pass }),
});
response = await response.json();
document.getElementById("signup-info").innerText = response.message;
};
forgotInfo.innerHTML = data.message;
});
// Handle reset code in URL
const params = new URLSearchParams(window.location.search);
const reset_code = params.get("reset_code");
if (reset_code) {
let response = await fetch("/pass_reset?", {
const resetCode = params.get("reset_code");
if (resetCode) {
const res = await fetch("/pass_reset?", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ code: reset_code }),
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code: resetCode }),
});
let status = response.status;
if (status == 200) {
document.getElementById("popup").classList.add("active");
document.getElementById("reset-pass").classList.add("active");
document.getElementById("login").classList.remove("active");
document.getElementById("signup").classList.remove("active");
document.getElementById("forgot-pass").classList.remove("active");
document.getElementById("reset-form").onsubmit = async (e) => {
if (res.status === 200) {
showPopup(resetPassSection);
resetForm?.addEventListener("submit", async (e) => {
e.preventDefault();
const pass = document.getElementById("reset-form").pass.value;
const pass_confirm =
document.getElementById("reset-form").pass_confirm.value;
if (pass != pass_confirm) {
document.getElementById("reset-info").innerText =
"Passwords do not match";
const pass = resetForm.pass.value;
const passConfirm = resetForm.pass_confirm.value;
if (pass !== passConfirm) {
resetInfo.innerText = "Passwords do not match";
return;
}
let response = await fetch("/reset_password/" + reset_code, {
const res = await fetch(`/reset_password/${resetCode}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ pass }),
});
response = await response.json();
document.getElementById("reset-info").innerText = response.message;
};
const data = await res.json();
resetInfo.innerText = data.message;
});
} else {
window.location.href = "/";
}

View File

@@ -5,32 +5,32 @@ class Sessions
@response = response
end
def signed_in?(code)
$active_users[code]
def signed_in?
$active_users[self["user"]]
end
def logout(uid)
def logout
uid = self["user"]
$active_users.delete(uid)
delete("user")
DB["delete from SignedInUsers where code = ?", uid].delete
true
rescue Sequel::Error => e
Logman.log "DB Error: #{e.message}\n"
Logman.log "DB Error: #{e.message}"
false
end
def login(username, pass)
Logman.log "Logging in: #{username} & #{pass} #{ENV_HASH["SALT"]}\n"
Logman.log "Logging in: #{username} & #{pass} #{ENV_HASH["SALT"]}"
player = Players.authorized?(username, pass)
if player
code = Array.new(24) { ALPHANUM.sample }.join
self["user", code] = code
self["user"] = code
$active_users[code] = username
begin
DB["insert into SignedInUsers (code, player) values (?, ?)", code, username].insert
rescue Sequel::Error => e
Logman.log "DB Error: #{e.message}\n"
Logman.log "DB Error: #{e.message}"
return [500, "Internal server error when signing you in!"]
end
return [200, "Remember to verify your email!"] unless Players.verified?(username)
@@ -40,7 +40,7 @@ class Sessions
end
end
def []=(key, uid, val)
def []=(key, val)
session = @request.cookies["session"]
session = session.nil? ? "{}" : Zlib::Inflate.inflate(Base64.decode64(session))
session = JSON.parse(session)
@@ -51,21 +51,23 @@ class Sessions
value: encoded,
path: "/",
expires: Time.now + 360 * 24 * 60 * 60)
uid = session["user"]
DB["UPDATE SignedInUsers SET last_used_at = CURRENT_TIMESTAMP WHERE code = ?", uid].update if uid
rescue JSON::ParserError, Zlib::Error
@response.delete_cookie("session")
rescue Sequel::Error => e
Logman.log "DB Error: #{e.message} when updating last_used_at for #{uid}\n"
Logman.log "DB Error: #{e.message} when updating last_used_at for #{uid}"
end
def [](key, uid: nil)
def [](key)
session = @request.cookies["session"]
session = session.nil? ? "{}" : Zlib::Inflate.inflate(Base64.decode64(session))
session = JSON.parse(session)
begin
uid = session["user"]
DB["UPDATE SignedInUsers SET last_used_at = CURRENT_TIMESTAMP WHERE code = ?", uid].update if uid
rescue Sequel::Error => e
Logman.log "DB Error: #{e.message} when updating last_used_at for #{uid}\n"
Logman.log "DB Error: #{e.message} when updating last_used_at for #{uid}"
end
session[key]
rescue JSON::ParserError, Zlib::Error
@@ -121,16 +123,16 @@ class Sessions
begin
DB[:SignedInUsers].where(code: session[:code]).delete
rescue StandardError => e
Logman.log "Thread DB error: #{e.message} on #{session[:code]} for #{session[:player]}\n"
Logman.log "Thread DB error: #{e.message} on #{session[:code]} for #{session[:player]}"
end
$active_users.delete(session[:code])
puts "Auto-logged out: #{session[:player]} (expired session)"
end
rescue StandardError => e
Logman.log "Thread error: #{e.message}\n"
Logman.log "Thread error: #{e.message}"
ensure
sleep 60 * 60 * 24
Logman.log "Thread sleeping\n"
Logman.log "Thread sleeping"
end
end
end