diff --git a/.gitignore b/.gitignore index 11d8cc2..4424173 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.db tmp.* *.log +.env diff --git a/.rubocop.yml b/.rubocop.yml index ece38e3..b15522c 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -16,6 +16,15 @@ Style/TrailingCommaInArrayLiteral: Style/FrozenStringLiteralComment: Enabled: false +Style/GlobalVars: + Enabled: false + +Style/MutableConstant: + Enabled: false + +Style/StringLiteralsInInterpolation: + Enabled: false + Layout/SpaceAroundOperators: Enabled: false diff --git a/Gemfile b/Gemfile index f1f87c4..c58a71b 100644 --- a/Gemfile +++ b/Gemfile @@ -11,3 +11,7 @@ gem "json" gem "base64" gem "zlib" + +gem "uri" + +gem "net-http" diff --git a/Gemfile.lock b/Gemfile.lock index b4f6a62..227a410 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,9 +3,12 @@ GEM specs: base64 (0.3.0) bigdecimal (3.2.2) + json (2.12.2) logger (1.7.0) mustermann (3.0.3) ruby2_keywords (~> 0.0.1) + net-http (0.6.0) + uri rack (3.1.16) rack-protection (4.1.1) base64 (>= 0.1.0) @@ -25,16 +28,23 @@ GEM rack-session (>= 2.0.0, < 3) tilt (~> 2.0) tilt (2.6.0) + uri (1.0.3) xxhash (0.6.0) + zlib (3.2.1) PLATFORMS ruby x86_64-linux DEPENDENCIES + base64 + json + net-http sequel sinatra + uri xxhash + zlib BUNDLED WITH 2.6.9 diff --git a/index.html b/index.html index 0eab6ad..50d74b5 100644 --- a/index.html +++ b/index.html @@ -1,16 +1,69 @@ - Infinsweeper + InfinSweeper + +
+ + + +
+ + +
+
- + + diff --git a/logman.rb b/logman.rb new file mode 100644 index 0000000..ee250fb --- /dev/null +++ b/logman.rb @@ -0,0 +1,10 @@ +# module for logging +module Logman + def self.log(log) + File.write("log/main.log", "[#{Time.now}] #{log}\n", mode: "a") + end + + def self.imp(log) + File.write("log/imp.log", "[#{Time.now}] #{log}\n", mode: "a") + end +end diff --git a/mailer.rb b/mailer.rb new file mode 100644 index 0000000..3632423 --- /dev/null +++ b/mailer.rb @@ -0,0 +1,31 @@ +# Mailer module +module Mail + def send(to, subject, body) + from_email = "noreply@infinsweeper.syedm.dev" + from_name = "InfinSweeper" + to = Array(to).map { |addr| { email_address: { address: addr, name: "" } } } + + payload = { + from: { + address: from_email, + name: from_name, + }, + to: to, + subject: subject, + htmlbody: body, + } + + uri = URI("https://api.zeptomail.com/v1.1/email") + req = Net::HTTP::Post.new(uri) + req["Authorization"] = "Zoho-enczapikey #{ENV_HASH["ZOHO_PASS"]}" + req["Content-Type"] = "application/json" + req.body = payload.to_json + + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true + response = http.request(req) + + return if response.is_a?(Net::HTTPSuccess) + Logman.imp "[ZeptoMail ERROR] #{response.body}" + end +end diff --git a/main.rb b/main.rb index 2448fca..47e4b13 100644 --- a/main.rb +++ b/main.rb @@ -1,123 +1,93 @@ require "sinatra" require "json" - -require_relative "players" -require_relative "session" +require "base64" +require "zlib" +require "sequel" +require "xxhash" +require "net/http" +require "uri" ALPHANUM = [*"0".."9", *"A".."Z", *"a".."z", "-", "_"].freeze +env_data = File.read(".env") +ENV_HASH = {} + +env_data.each_line do |line| + if (match = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/)) + _, key, val = match + ENV_HASH[key] = val + end +end + +CODE_ENV = :dev + db_file = File.expand_path("infinsweeper.db") DB = Sequel.connect("sqlite:///#{db_file}", single_threaded: false) DB.run("PRAGMA foreign_keys = ON;") +$active_users = DB[:SignedInUsers].all.map { |x| [x[:code], x[:player]] }.to_h -signed_in_users = DB[:SignedInUsers].all.map { |x| [x[:code], x[:player]] }.to_h - -Thread.new do - loop do - now = Time.now - fifteen_days_ago = now - (60 * 60 * 24 * 15) - six_days_ago = now - (60 * 60 * 24 * 6) - old_sessions = (DB[:SignedInUsers].where { created_at < fifteen_days_ago }.all + - DB[:SignedInUsers].where { last_used_at < six_days_ago }.all).uniq { |s| s[:code] } - old_sessions.each do |session| - begin - DB[:SignedInUsers].where(code: session[:code]).delete - rescue StandardError => e - File.write("log/main.log", - "[#{Time.now}] Thread DB error: #{e.message} on #{session[:code]} for #{session[:player]}\n", - mode: "a") - end - signed_in_users.delete(session[:code]) - puts "Auto-logged out: #{session[:player]} (expired session)" - end - rescue StandardError => e - File.write("log/main.log", "[#{Time.now}] Thread error: #{e.message}\n", mode: "a") - ensure - sleep 60 * 60 * 24 - end -end +load "logman.rb" +load "mailer.rb" +load "players.rb" +load "session.rb" set :public_folder, "public" get "/" do + session = Sessions.new request, response + Logman.log session["message"] send_file "index.html" end get "/debug" do - return get_session_all(request, response).inspect + content_type :json + (Sessions.new request, response).all.to_json end -post "/new_player" do +post "/signup" do + session = Sessions.new request, response data = JSON.parse(request.body.read) if data["email"].nil? || data["pass"].nil? || data["username"].nil? status 400 return { "message" => "Bad request made!" }.to_json end - player = Players.mk_player(data["username"], data["email"], data["pass"]) - case player - when "Successfully registered!" - code = Array.new(24) { ALPHANUM.sample }.join - set_session(request, response, "user", code, code) - signed_in_users[code] = data["email"] - begin - DB["insert into SignedInUsers (code, player) values (?, ?)", code, data["email"]].insert - rescue Sequel::Error => e - File.write("log/main.log", "DB Error: #{e.message}\n", mode: "a") - status 500 - return { "message" => "Internal server error when signing you in!" }.to_json - end - status 200 - else - status 400 + 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 end - return { "message" => player }.to_json + status signup_status[0] + return { "message" => signup_status[1] }.to_json end get "/verify/:code" do - if Players.verify(params[:code]) - status 200 - return { "message" => "Verified successfully!" }.to_json - else - status 400 - return { "message" => "Couldn't verify!" } - end + session = Sessions.new request, response + session["message"] = Players.verify(params[:code]) ? "Verified successfully!" : "Verification failed!" + redirect "/" end post "/login" do data = JSON.parse(request.body.read) - player = Players.authorized?(data["email"], data["pass"]) - if player - code = Array.new(24) { ALPHANUM.sample }.join - set_session(request, response, "user", code, code) - signed_in_users[code] = data["email"] - begin - DB["insert into SignedInUsers (code, player) values (?, ?)", code, data["email"]].insert - rescue Sequel::Error => e - File.write("log/main.log", "DB Error: #{e.message}\n", mode: "a") - status 500 - return { "message" => "Internal server error when signing you in!" }.to_json - end - status 200 - return { "message" => "Remember to verify your email!" }.to_json unless Players.verified?(data["email"]) - return { "message" => "Signed in successfully!" }.to_json - else - status 400 - return { "message" => "Couldn't sign you in!" }.to_json + session = Sessions.new request, response + uid = session["user"] + if $active_users[uid] && !session.logout(uid) + status 500 + return { "message" => "Internal server error when signing the existing session out!" }.to_json end + login_status = session.login(data["username"], data["pass"]) + status login_status[0] + return { "message" => login_status[1] }.to_json end post "/logout" do - uid = get_session(request, response, "user") - if signed_in_users[uid].nil? + session = Sessions.new request, response + uid = session["user"] + if $active_users[uid].nil? status 400 return { "message" => "Not signed in!" }.to_json end - signed_in_users.delete(uid) - rm_session(request, response, "user") - begin - DB["delete from SignedInUsers where code = ?", uid].delete - rescue Sequel::Error => e - File.write("log/main.log", "DB Error: #{e.message}\n", mode: "a") + unless session.logout(uid) status 500 return { "message" => "Internal server error when signing you out!" }.to_json end @@ -136,6 +106,10 @@ post "/forgot_password" do return { "message" => "Email sent successfully!" }.to_json end +get "/reset_password/:code" do + redirect "/?reset_code=#{params[:code]}" +end + post "/reset_password/:code" do data = JSON.parse(request.body.read) if data["pass"].nil? || params[:code].nil? @@ -152,14 +126,13 @@ post "/reset_password/:code" do end delete "/rm_player" do - uid = get_session(request, response, "user") - if uid.nil? || signed_in_users[uid].nil? + session = Sessions.new request, response + uid = session["user"] + if uid.nil? || $active_users[uid].nil? status 400 return { "message" => "Not signed in!" }.to_json end - if Players.rm_player(signed_in_users[uid]) - signed_in_users.delete(uid) - rm_session(request, response, "user") + if session.logout(uid) && Players.rm_player($active_users[uid]) status 200 return { "message" => "Sorry to see you go.." }.to_json else diff --git a/players.rb b/players.rb index 4b33596..4524a2c 100644 --- a/players.rb +++ b/players.rb @@ -1,21 +1,11 @@ -require "xxhash" -require "sequel" -require "json" - -ALPHANUM = [*"0".."9", *"A".."Z", *"a".."z", "-", "_"].freeze - # DataBase handler module module Players - db_file = File.expand_path("infinsweeper.db") - DB = Sequel.connect("sqlite:///#{db_file}", single_threaded: false) - DB.run("PRAGMA foreign_keys = ON;") - def self.list DB["select * from Players"].all end - def self.rm_player(email) - DB["delete from Players where email = ?", email].delete != 0 + def self.rm_player(username) + DB["delete from Players where username = ?", username].delete != 0 end def self.mk_player(username, email, pass) @@ -26,25 +16,24 @@ module Players pass.match?(/\A[a-zA-Z0-9_.!?@#$%^&*()+=-]+\z/) && pass.length >= 8 digest = XXhash.xxh32(pass, 1234) - - code = Array.new(24) { ALPHANUM.sample }.join + code = CODE_ENV == :prod ? Array.new(24) { ALPHANUM.sample }.join : "!" DB[ - "insert into Players (email, digest, username, activation_code) values (?, ?, ?, ?)", - email, digest, username, code + "insert into Players (username, digest, email, activation_code) values (?, ?, ?, ?)", + username, digest, email, code ].insert - send_email(:new, email, username, code) + send_email(:new, email, username, code) if CODE_ENV == :prod - "Successfully registered!" + [200, "Successfully signed up!"] rescue ArgumentError => e - e.message + [400, e.message] rescue Sequel::UniqueConstraintViolation - "Account already exists with this email or username!" + [400, "Account already exists with this username or username!"] end def self.verify(code) - DB["update Players set activation_code = ? where code = ?", "!", code].update != 0 + DB["update Players set activation_code = ? where activation_code = ?", "!", code].update != 0 end def self.unverified @@ -62,35 +51,36 @@ module Players def self.pass_reset(new_pass, code) digest = XXhash.xxh32(new_pass, 1234) - DB["update Players set digest = ? where new_pass_code = ?", digest, code].update != 0 + DB["update Players set digest = ?, new_pass_code = ? where new_pass_code = ?", digest, "", code].update != 0 end - def self.[](email) - DB["select * from Players where email = ?", email].first + def self.[](username) + DB["select * from Players where username = ?", username].first end - def self.[]=(email, data) - DB["update Players set data = ? where email = ?", data, email].update + def self.[]=(username, data) + DB["update Players set data = ? where username = ?", data, username].update end - def self.authorized?(email, pass) + def self.authorized?(username, pass) digest = XXhash.xxh32(pass, 1234) - player = self[email] + player = self[username] player && player[:digest].to_i == digest.to_i ? player : false end - def self.verified?(email) - player = self[email] - player && player[:code] == "!" + def self.verified?(username) + player = self[username] + Logger.log "Verified: #{player.inspect}\n" + player && player[:activation_code] == "!" end Thread.new do loop do unverified.each do |player| - rm_player(player[:email]) if player[:created_at] + 24 * 60 * 60 < Time.now + rm_player(player[:username]) if player[:created_at] + 24 * 60 * 60 < Time.now end rescue StandardError => e - File.write("log/main.log", "Thread error: #{e.message}\n", mode: "a") + Logger.log "Thread error: #{e.message}\n" ensure sleep 60 * 60 end diff --git a/public/src/assets/fonts/changa.woff b/public/src/assets/fonts/changa.woff new file mode 100644 index 0000000..e41b363 Binary files /dev/null and b/public/src/assets/fonts/changa.woff differ diff --git a/public/src/assets/img/logo_large.png b/public/src/assets/img/logo_lg.png similarity index 100% rename from public/src/assets/img/logo_large.png rename to public/src/assets/img/logo_lg.png diff --git a/public/src/assets/style.css b/public/src/assets/style.css index 4221350..4630f76 100644 --- a/public/src/assets/style.css +++ b/public/src/assets/style.css @@ -2,3 +2,59 @@ body { margin: 0; overflow: hidden; } + +@font-face { + font-family: "Changa"; + src: url("fonts/changa.woff") format("opentype"); + font-weight: normal; + font-style: normal; +} + +.header { + width: 100%; + height: 90px; + background-color: #1b262c; + border-bottom: 3px solid #90bdd9; + display: flex; + align-items: center; + justify-content: space-between; + font-family: "Changa"; +} + +.logo { + height: 50px; + position: relative; + top: 7px; +} + +.popup { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 500px; + height: 500px; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 9999; +} + +.popup-tab { + display: none; +} + +.header-right { + margin-right: 20px; +} + +.pixelart { + image-rendering: pixelated; + image-rendering: crisp-edges; + margin: 0 20px; +} + +#main-canvas { + height: calc(100vh - 90px); +} diff --git a/public/src/js/accounts.js b/public/src/js/accounts.js new file mode 100644 index 0000000..8c04f1b --- /dev/null +++ b/public/src/js/accounts.js @@ -0,0 +1,44 @@ +const login_form = document.getElementById("login-form"); +const login_button = document.getElementById("login-button"); + +login_button.onclick = () => { + document.getElementById("login").style.display = "block"; +}; + +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; +}; + +const signup_form = document.getElementById("signup-form"); +const signup_button = document.getElementById("signup-button"); + +signup_button.onclick = () => { + document.getElementById("signup").style.display = "block"; +}; + +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; +}; diff --git a/public/src/js/index.js b/public/src/js/game.js similarity index 100% rename from public/src/js/index.js rename to public/src/js/game.js diff --git a/public/src/js/game_renderer.js b/public/src/js/game_renderer.js index d78c89b..949f565 100644 --- a/public/src/js/game_renderer.js +++ b/public/src/js/game_renderer.js @@ -39,7 +39,7 @@ export default class GameRenderer { /** @type {HTMLCanvasElement} */ this.canvas = document.getElementById(CANVAS_ID); this.canvas.width = window.innerWidth; - this.canvas.height = window.innerHeight; + this.canvas.height = window.innerHeight - 90; /** @type {CanvasRenderingContext2D} */ this.ctx = this.canvas.getContext("2d"); this.ctx.imageSmoothingEnabled = false; diff --git a/public/src/js/ui_renderer.js b/public/src/js/ui_renderer.js index 2a2d310..f5ad28d 100644 --- a/public/src/js/ui_renderer.js +++ b/public/src/js/ui_renderer.js @@ -83,7 +83,7 @@ export default class UIRenderer extends GameRenderer { */ resize() { this.canvas.width = window.innerWidth; - this.canvas.height = window.innerHeight; + this.canvas.height = window.innerHeight - 90; this.ctx.imageSmoothingEnabled = false; } /** diff --git a/schema.sql b/schema.sql index e0e1cc6..8681d21 100644 --- a/schema.sql +++ b/schema.sql @@ -1,6 +1,6 @@ CREATE TABLE IF NOT EXISTS Players ( - email TEXT PRIMARY KEY, - username TEXT UNIQUE, + username TEXT PRIMARY KEY, + email TEXT UNIQUE, digest BLOB, data BLOB, activation_code TEXT, @@ -13,5 +13,5 @@ CREATE TABLE IF NOT EXISTS SignedInUsers ( player TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_used_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (player) REFERENCES Players (email) ON DELETE CASCADE + FOREIGN KEY (player) REFERENCES Players (username) ON DELETE CASCADE ); diff --git a/session.rb b/session.rb index f46e36e..d2ee380 100644 --- a/session.rb +++ b/session.rb @@ -1,62 +1,121 @@ -require "base64" -require "zlib" -require "json" - -def set_session(request, response, key, val, uid = nil) - session = request.cookies["session"] - session = session.nil? ? "{}" : Zlib::Inflate.inflate(Base64.decode64(session)) - session = JSON.parse(session) - session[key] = val - compressed = Zlib::Deflate.deflate(JSON.generate(session)) - encoded = Base64.encode64(compressed) - response.set_cookie("session", - value: encoded, - path: "/", - expires: Time.now + 360 * 24 * 60 * 60) - begin - DB["UPDATE SignedInUsers SET last_used_at = CURRENT_TIMESTAMP WHERE code = ?", uid].update if uid - rescue Sequel::Error => e - File.write("log/main.log", "DB Error: #{e.message} when updating last_used_at for #{uid}\n", mode: "a") +# class Sessions +class Sessions + def initialize(request, response) + @request = request + @response = response end -rescue JSON::ParserError, Zlib::Error - response.delete_cookie("session") -end -def get_session(request, response, key, uid = nil) - session = request.cookies["session"] - session = session.nil? ? "{}" : Zlib::Inflate.inflate(Base64.decode64(session)) - session = JSON.parse(session) - begin - DB["UPDATE SignedInUsers SET last_used_at = CURRENT_TIMESTAMP WHERE code = ?", uid].update if uid - rescue Sequel::Error => e - File.write("log/main.log", "DB Error: #{e.message} when updating last_used_at for #{uid}\n", mode: "a") + def signed_in?(code) + signed_in_users[code] end - session[key] -rescue JSON::ParserError, Zlib::Error - response.delete_cookie("session") - "" -end -def get_session_all(request, response) - session = request.cookies["session"] - session = session.nil? ? "{}" : Zlib::Inflate.inflate(Base64.decode64(session)) - JSON.parse(session) -rescue JSON::ParserError, Zlib::Error - response.delete_cookie("session") - "" -end + def logout(uid) + signed_in_users.delete(uid) + delete("user") + DB["delete from SignedInUsers where code = ?", uid].delete + true + rescue Sequel::Error => e + Logger.log "DB Error: #{e.message}\n" + false + end -def rm_session(request, response, key) - session = request.cookies["session"] - session = session.nil? ? "{}" : Zlib::Inflate.inflate(Base64.decode64(session)) - session = JSON.parse(session) - session.delete(key) - compressed = Zlib::Deflate.deflate(JSON.generate(session)) - encoded = Base64.encode64(compressed) - response.set_cookie("session", - value: encoded, - path: "/", - expires: Time.now + 360 * 24 * 60 * 60) -rescue JSON::ParserError, Zlib::Error - response.delete_cookie("session") + def login(username, pass) + player = Players.authorized?(username, pass) + if player + code = Array.new(24) { ALPHANUM.sample }.join + self["user", code] = code + signed_in_users[code] = username + begin + DB["insert into SignedInUsers (code, player) values (?, ?)", code, username].insert + rescue Sequel::Error => e + Logger.log "DB Error: #{e.message}\n" + return [500, "Internal server error when signing you in!"] + end + return [200, "Remember to verify your email!"] unless Players.verified?(username) + [200, "Signed in successfully!"] + else + [200, "Couldn't sign you in (Username or password incorrect)!"] + end + end + + def []=(key, uid, val) + session = @request.cookies["session"] + session = session.nil? ? "{}" : Zlib::Inflate.inflate(Base64.decode64(session)) + session = JSON.parse(session) + session[key] = val + compressed = Zlib::Deflate.deflate(JSON.generate(session)) + encoded = Base64.encode64(compressed) + @response.set_cookie("session", + value: encoded, + path: "/", + expires: Time.now + 360 * 24 * 60 * 60) + 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 + Logger.log "DB Error: #{e.message} when updating last_used_at for #{uid}\n" + end + + def [](key, uid: nil) + session = @request.cookies["session"] + session = session.nil? ? "{}" : Zlib::Inflate.inflate(Base64.decode64(session)) + session = JSON.parse(session) + begin + DB["UPDATE SignedInUsers SET last_used_at = CURRENT_TIMESTAMP WHERE code = ?", uid].update if uid + rescue Sequel::Error => e + Logger.log "DB Error: #{e.message} when updating last_used_at for #{uid}\n" + end + session[key] + rescue JSON::ParserError, Zlib::Error + @response.delete_cookie("session") + "" + end + + def all + session = @request.cookies["session"] + session = session.nil? ? "{}" : Zlib::Inflate.inflate(Base64.decode64(session)) + JSON.parse(session) + rescue JSON::ParserError, Zlib::Error + @response.delete_cookie("session") + {} + end + + def delete(key) + session = @request.cookies["session"] + session = session.nil? ? "{}" : Zlib::Inflate.inflate(Base64.decode64(session)) + session = JSON.parse(session) + session.delete(key) + compressed = Zlib::Deflate.deflate(JSON.generate(session)) + encoded = Base64.encode64(compressed) + @response.set_cookie("session", + value: encoded, + path: "/", + expires: Time.now + 360 * 24 * 60 * 60) + rescue JSON::ParserError, Zlib::Error + @response.delete_cookie("session") + end + + Thread.new do + loop do + now = Time.now + fifteen_days_ago = now - (60 * 60 * 24 * 15) + six_days_ago = now - (60 * 60 * 24 * 6) + old_sessions = (DB[:SignedInUsers].where { created_at < fifteen_days_ago }.all + + DB[:SignedInUsers].where { last_used_at < six_days_ago }.all).uniq { |s| s[:code] } + old_sessions.each do |session| + begin + DB[:SignedInUsers].where(code: session[:code]).delete + rescue StandardError => e + Logger.log "Thread DB error: #{e.message} on #{session[:code]} for #{session[:player]}\n" + end + signed_in_users.delete(session[:code]) + puts "Auto-logged out: #{session[:player]} (expired session)" + end + rescue StandardError => e + Logger.log "Thread error: #{e.message}\n" + ensure + sleep 60 * 60 * 24 + Logger.log "Thread sleeping\n" + end + end end