From 7a5bb92cc8b916da98ba0a26548fefc3201bec6e Mon Sep 17 00:00:00 2001 From: "Jill \"oatmealine\" Monoids" Date: Wed, 11 Jan 2023 16:51:14 +0300 Subject: [PATCH] account management; change username & password --- public/style.css | 4 + public/template/account_management.ecr | 99 ++++++++++++++++++-- public/template/account_settings.ecr | 56 +++++++++++ public/template/index.ecr | 1 + public/template/login.ecr | 5 +- src/lib/templates.cr | 24 ++++- src/template_endpoints/account_management.cr | 56 +---------- src/template_endpoints/account_settings.cr | 73 +++++++++++++++ src/template_endpoints/login.cr | 59 ++++++++++++ 9 files changed, 314 insertions(+), 63 deletions(-) create mode 100644 public/template/account_settings.ecr create mode 100644 src/template_endpoints/account_settings.cr create mode 100644 src/template_endpoints/login.cr diff --git a/public/style.css b/public/style.css index 8b7e8f1..ba3c47b 100644 --- a/public/style.css +++ b/public/style.css @@ -108,4 +108,8 @@ pre { .fancy-button:hover { background-color: var(--accent-color-bri); +} + +.error { + color: #f33; } \ No newline at end of file diff --git a/public/template/account_management.ecr b/public/template/account_management.ecr index 5ada715..46da95c 100644 --- a/public/template/account_management.ecr +++ b/public/template/account_management.ecr @@ -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; + } - hewwo, <%= username %>!

- -
  • Nothing is implemented right now for account management
  • -
  • This is just a test list
  • -
  • Did you know your account ID is <%= account_id %>?
  • -
    +
    + +
    + +
    +
    +
    + hewwo, <%= username %>! +
    +
    + <%= stars %> <%= diamonds %> <%= coins %> <%= user_coins %> <%= demons %> +
    +
    +
    + Settings ยท Log out +
    +
    +
    +

    + blablabla lorem ipsum whatever. put stuff here later diff --git a/public/template/account_settings.ecr b/public/template/account_settings.ecr new file mode 100644 index 0000000..f6936b0 --- /dev/null +++ b/public/template/account_settings.ecr @@ -0,0 +1,56 @@ + + + + + + + + Account Management + + + + <%= Templates.dir_header %>
    + + <%- if error -%> +
    <%= error %>
    +
    + <%- elsif result -%> + <%= result %> +

    + <%- end -%> + +
    +
    +
    +
    + +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    + +
    + + \ No newline at end of file diff --git a/public/template/index.ecr b/public/template/index.ecr index b62036c..cf0a0ac 100644 --- a/public/template/index.ecr +++ b/public/template/index.ecr @@ -36,6 +36,7 @@ But you probably already knew that. You may be looking for:
  • The Git repository
  • +
  • Account stuff
  • Song reuploading and searching
  • Levels and level reuploading
  • <%- if config_get("sessions.allow").as(Bool | Nil) -%> diff --git a/public/template/login.ecr b/public/template/login.ecr index 7b3ac50..4cf7f95 100644 --- a/public/template/login.ecr +++ b/public/template/login.ecr @@ -27,13 +27,10 @@ height: 100%; padding: 1em; } - .error { - color: #f33; - } -
    +
    diff --git a/src/lib/templates.cr b/src/lib/templates.cr index 2a3c99c..ed8446a 100644 --- a/src/lib/templates.cr +++ b/src/lib/templates.cr @@ -4,8 +4,8 @@ module CrystalGauntlet::Templates extend self macro dir_header() - path_split = context.request.path.split('/') - "
    " + path_split.map_with_index { |v, i| "#{i == 0 ? "crystal-gauntlet" : v}"}.join(" / ") + "
    " + %path_split = context.request.path.split('/') + "
    " + %path_split.map_with_index { |v, i| "#{i == 0 ? "crystal-gauntlet" : v}"}.join(" / ") + "
    " end def footer() @@ -15,6 +15,26 @@ module CrystalGauntlet::Templates ) 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 diff --git a/src/template_endpoints/account_management.cr b/src/template_endpoints/account_management.cr index 3b62329..8b26505 100644 --- a/src/template_endpoints/account_management.cr +++ b/src/template_endpoints/account_management.cr @@ -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) } diff --git a/src/template_endpoints/account_settings.cr b/src/template_endpoints/account_settings.cr new file mode 100644 index 0000000..5996c1d --- /dev/null +++ b/src/template_endpoints/account_settings.cr @@ -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) +} diff --git a/src/template_endpoints/login.cr b/src/template_endpoints/login.cr new file mode 100644 index 0000000..11d6b5f --- /dev/null +++ b/src/template_endpoints/login.cr @@ -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 +}