account management; change username & password

This commit is contained in:
Jill 2023-01-11 16:51:14 +03:00
parent 8aef039d01
commit 7a5bb92cc8
9 changed files with 314 additions and 63 deletions

View File

@ -108,4 +108,8 @@ pre {
.fancy-button:hover {
background-color: var(--accent-color-bri);
}
.error {
color: #f33;
}

View File

@ -12,14 +12,101 @@
margin: auto;
padding: 1em;
}
.greeting ::selection {
background-color: #000;
color: #fff;
}
.greeting {
background-color: var(--accent-color);
color: #000;
border-radius: 1.5em;
padding: 1em;
margin: 1em;
display: flex;
flex-direction: row;
gap: 1rem;
}
.greeting-l {
flex: 0 0 auto;
max-width: 100%;
width: auto;
height: 100%;
max-height: 3rem;
object-fit: contain;
display: block;
}
.greeting-r {
flex: 1 1 0px;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.greeting-top {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.greeting-top-left {
font-size: 1.4rem;
}
.greeting-stats {
display: flex;
flex-direction: row;
align-items: stretch;
gap: 0.5ex;
}
.greeting-stats img {
width: auto;
height: 1em;
}
.greeting a {
color: #000;
}
@media (max-width: 650px) {
.greeting-top {
flex-direction: column;
gap: 0.2em;
align-items: start;
}
.greeting-bottom {
text-align: right;
}
.greeting-r {
gap: 0.2em;
}
}
.favicon {
margin: auto;
display: block;
}
</style>
</head>
<body>
hewwo, <b><%= username %></b>!<br><br>
<el>
<li>Nothing is implemented right now for account management</li>
<li>This is just a test list</li>
<li>Did you know your account ID is <%= account_id %>?</li>
</el>
<a href="/"><img src="/favicon.png" width="64" height="auto" class="spinny favicon"></a><br>
<div class="greeting">
<img src="https://cdn.discordapp.com/attachments/902195395264905217/1062706739969019984/ball_35.png" width="150" height="150" class="greeting-l">
<div class="greeting-r">
<div class="greeting-top">
<div class="greeting-top-left">
hewwo, <b><%= username %></b>!
</div>
<div class="greeting-stats">
<%= stars %> <img src="https://gdbrowser.com/assets/star.png"> <%= diamonds %> <img src="https://gdbrowser.com/assets/diamond.png"> <%= coins %> <img src="https://gdbrowser.com/assets/coin.png"> <%= user_coins %> <img src="https://gdbrowser.com/assets/silvercoin.png"> <%= demons %> <img src="https://gdbrowser.com/assets/demon.png">
</div>
</div>
<div class="greeting-bottom">
<a href="/accounts/settings">Settings</a> · <a>Log out</a>
</div>
</div>
</div>
<br><br>
blablabla lorem ipsum whatever. put stuff here later
</body>
</html>

View File

@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/style.css" />
<title>Account Management</title>
<style>
body {
max-width: 800px;
margin: auto;
padding: 1em;
}
form {
margin: auto;
max-width: 700px;
background-color: rgba(150, 150, 150, 0.15);
padding: 1em;
border-radius: 1em;
line-height: 1.5;
}
</style>
</head>
<body>
<%= Templates.dir_header %><br>
<%- if error -%>
<div class="error"><%= error %></div>
<br>
<%- elsif result -%>
<%= result %>
<br><br>
<%- end -%>
<form action="/accounts/settings" method="post">
<label for="username">Username</label><br>
<input type="text" id="username" name="username" minlength="3" maxlength="16" required value="<%= username %>" /><br>
<hr>
<input type="submit" value="Update Account" />
</form>
<br>
<form action="/accounts/settings" method="post">
<label for="old_password">Old password</label><br>
<input type="password" id="old_password" name="old_password" minlength="3" maxlength="16" required/><br>
<label for="new_password">New password</label><br>
<input type="password" id="new_password" name="new_password" minlength="3" maxlength="16" required/><br>
<label for="repeat_new_password">Repeat new password</label><br>
<input type="password" id="repeat_new_password" name="repeat_new_password" minlength="3" maxlength="16" required/><br>
<hr>
<input type="submit" value="Update Password" />
</form>
</body>
</html>

View File

@ -36,6 +36,7 @@
But you probably already knew that. You may be looking for:<br>
<el>
<li>The <a href="https://git.oat.zone/oat/crystal-gauntlet">Git repository</a></li>
<li><a href="/accounts">Account stuff</a></li>
<li><a href="/tools/song_upload">Song reuploading</a> and <a href="/tools/song_search">searching</a></li>
<li><a href="/tools/levels">Levels</a> and <a href="/tools/reupload">level reuploading</a></li>
<%- if config_get("sessions.allow").as(Bool | Nil) -%>

View File

@ -27,13 +27,10 @@
height: 100%;
padding: 1em;
}
.error {
color: #f33;
}
</style>
</head>
<body>
<form action="/accounts/" method="post">
<form method="post">
<img src="/favicon.png" width="64" height="auto" class="spinny">
<br>
<label for="username">Username</label>

View File

@ -4,8 +4,8 @@ module CrystalGauntlet::Templates
extend self
macro dir_header()
path_split = context.request.path.split('/')
"<div class='dir-header'>" + path_split.map_with_index { |v, i| "<a href='/#{path_split[1..i].join('/')}'>#{i == 0 ? "crystal-gauntlet" : v}</a>"}.join(" / ") + "</div>"
%path_split = context.request.path.split('/')
"<div class='dir-header'>" + %path_split.map_with_index { |v, i| "<a href='/#{%path_split[1..i].join('/')}'>#{i == 0 ? "crystal-gauntlet" : v}</a>"}.join(" / ") + "</div>"
end
def footer()
@ -15,6 +15,26 @@ module CrystalGauntlet::Templates
</div>
)
end
macro auth()
if session = CrystalGauntlet.sessions.get(context)
logged_in = true
account_id = session.account_id
user_id = session.user_id
username = session.username
else
logged_in = false
account_id = nil
user_id = nil
username = nil
end
if !logged_in
context.response.headers.add("Location", "/login?#{URI::Params.encode({"redir" => context.request.path})}")
context.response.status = HTTP::Status::SEE_OTHER
return
end
end
end
module CrystalGauntlet

View File

@ -1,6 +1,3 @@
require "uri"
require "http-session"
include CrystalGauntlet
CrystalGauntlet.template_endpoints["/#{config_get("general.append_path").as(String | Nil) || ""}accounts/accountManagement.php"] = ->(context : HTTP::Server::Context) {
@ -11,54 +8,11 @@ CrystalGauntlet.template_endpoints["/#{config_get("general.append_path").as(Stri
CrystalGauntlet.template_endpoints["/accounts"] = ->(context : HTTP::Server::Context) {
context.response.content_type = "text/html"
if session = CrystalGauntlet.sessions.get(context)
logged_in = true
account_id = session.account_id
user_id = session.user_id
username = session.username
else
logged_in = false
account_id = nil
user_id = nil
username = nil
end
user_id = nil
username = nil
Templates.auth()
body = context.request.body
if body
begin
params = URI::Params.parse(body.gets_to_end)
username = params["username"].strip
password = params["password"].strip
stars, demons, coins, user_coins, diamonds, creator_points = DATABASE.query_one("select stars, demons, coins, user_coins, diamonds, creator_points from users where id = ?", user_id, as: {Int32, Int32, Int32, Int32, Int32, Int32})
if username.empty? || password.empty?
raise "Invalid username or password"
end
# todo: dedup this code with the login account endpoint maybe
result = DATABASE.query_all("select id, password from accounts where username = ?", username, as: {Int32, String})
if result.size > 0
account_id, hash = result[0]
bcrypt = Crypto::Bcrypt::Password.new(hash)
if bcrypt.verify(password)
user_id = Accounts.get_user_id(account_id)
logged_in = true
LOG.debug { "#{username} logged in" }
CrystalGauntlet.sessions.set(context, UserSession.new(username, account_id, user_id))
else
raise "Invalid password"
end
else
raise "No such user exists"
end
rescue error
LOG.error(exception: error) {"whar...."}
end
end
if logged_in
ECR.embed("./public/template/account_management.ecr", context.response)
else
ECR.embed("./public/template/login.ecr", context.response)
end
ECR.embed("./public/template/account_management.ecr", context.response)
}

View File

@ -0,0 +1,73 @@
require "uri"
include CrystalGauntlet
CrystalGauntlet.template_endpoints["/accounts/settings"] = ->(context : HTTP::Server::Context) {
context.response.content_type = "text/html"
account_id = nil
user_id = nil
username = nil
Templates.auth()
result = nil
params = context.request.body.try { |b| URI::Params.parse(b.gets_to_end) }
if params
begin
if params["username"]?
# todo: dedup this and the gd register endpoint
username = params["username"].strip
if username.size < 3
raise "Username must at least be 3 characters long"
end
if username.size > 16
raise "Username must at most be 16 characters long"
end
if DATABASE.scalar("select count(*) from accounts where username = ?", username).as(Int64) > 0
raise "Username already taken"
end
DATABASE.exec("update accounts set username = ? where id = ?", username, account_id)
DATABASE.exec("update users set username = ? where id = ?", username, user_id)
# refresh session
CrystalGauntlet.sessions.set(context, UserSession.new(username, account_id.not_nil!, user_id.not_nil!))
result = "Changed username successfully"
end
if params["old_password"]? && params["new_password"]? && params["repeat_new_password"]?
if params["repeat_new_password"] != params["new_password"]
raise "New password and repeated password do not match"
end
new_password = params["new_password"].strip
# todo: dedup this and gd register endpoint
if new_password.size < 6
raise "New password must be at least 6 characters long"
end
old_hash = DATABASE.query_one("select password from accounts where username = ?", username, as: {String})
bcrypt = Crypto::Bcrypt::Password.new(old_hash)
if !bcrypt.verify(params["old_password"])
raise "Invalid old password"
end
password_hash = Crypto::Bcrypt::Password.create(new_password, cost: 10).to_s
gjp2 = CrystalGauntlet::GJP.hash(new_password)
DATABASE.exec("update accounts set password = ?, gjp2 = ? where id = ?", password_hash, gjp2, account_id)
result = "Changed password successfully"
end
rescue error
LOG.error {"whar.... #{error}"}
end
end
ECR.embed("./public/template/account_settings.ecr", context.response)
}

View File

@ -0,0 +1,59 @@
require "uri"
require "http-session"
include CrystalGauntlet
CrystalGauntlet.template_endpoints["/login"] = ->(context : HTTP::Server::Context) {
if session = CrystalGauntlet.sessions.get(context)
logged_in = true
account_id = session.account_id
user_id = session.user_id
username = session.username
else
logged_in = false
account_id = nil
user_id = nil
username = nil
end
body = context.request.body
if body
begin
params = URI::Params.parse(body.gets_to_end)
username = params["username"].strip
password = params["password"].strip
if username.empty? || password.empty?
raise "Invalid username or password"
end
# todo: dedup this code with the login account endpoint maybe
result = DATABASE.query_all("select id, password from accounts where username = ?", username, as: {Int32, String})
if result.size > 0
account_id, hash = result[0]
bcrypt = Crypto::Bcrypt::Password.new(hash)
if bcrypt.verify(password)
user_id = Accounts.get_user_id(account_id)
logged_in = true
LOG.debug { "#{username} logged in" }
CrystalGauntlet.sessions.set(context, UserSession.new(username, account_id, user_id))
else
raise "Invalid password"
end
else
raise "No such user exists"
end
rescue error
LOG.error(exception: error) {"whar...."}
end
end
if logged_in
context.response.headers.add("Location", "#{context.request.query_params["redir"]? || "/accounts"}")
context.response.status = HTTP::Status::SEE_OTHER
return
else
ECR.embed("./public/template/login.ecr", context.response)
end
}