This commit is contained in:
Alex Ling 2020-02-14 00:57:39 +00:00
parent 83f6fc25f0
commit 042df2bf1f
10 changed files with 162 additions and 40 deletions

View File

@ -1,6 +1,15 @@
require "kemal" require "kemal"
require "./storage" require "./storage"
def request_path_startswith(env, ary)
ary.each do |prefix|
if env.request.path.starts_with? prefix
return true
end
end
return false
end
class AuthHandler < Kemal::Handler class AuthHandler < Kemal::Handler
exclude ["/login"] exclude ["/login"]
exclude ["/login"], "POST" exclude ["/login"], "POST"
@ -18,9 +27,9 @@ class AuthHandler < Kemal::Handler
return env.redirect "/login" return env.redirect "/login"
end end
if env.request.path.starts_with? "/admin" if request_path_startswith env, ["/admin", "/api/admin"]
unless storage.verify_admin cookie.value unless storage.verify_admin cookie.value
env.response.status_code = 401 return env.response.status_code = 401
end end
end end

View File

@ -9,10 +9,10 @@ class Config
property port = 9000 property port = 9000
@[YAML::Field(key: "library_path")] @[YAML::Field(key: "library_path")]
property library_path = File.expand_path "~/mango-library", home: true property library_path = File.expand_path "~/mango/library", home: true
@[YAML::Field(key: "db_path")] @[YAML::Field(key: "db_path")]
property db_path = File.expand_path "~/mango-library/mango.db", home: true property db_path = File.expand_path "~/mango/mango.db", home: true
def self.load def self.load
cfg_path = File.expand_path "~/.config/mango/config.yml", home: true cfg_path = File.expand_path "~/.config/mango/config.yml", home: true
@ -22,7 +22,7 @@ class Config
puts "The config file #{cfg_path} does not exist." \ puts "The config file #{cfg_path} does not exist." \
"Do you want mango to dump the default config there? [Y/n]" "Do you want mango to dump the default config there? [Y/n]"
input = gets input = gets
if !input.nil? && input.downcase == "n" if input && input.downcase == "n"
abort "Aborting..." abort "Aborting..."
end end
default = self.allocate default = self.allocate

View File

@ -79,7 +79,7 @@ class Library
def initialize(dir : String) def initialize(dir : String)
@dir = dir @dir = dir
unless Dir.exists? dir unless Dir.exists? dir
abort "ERROR: The library directory #{dir} does not exist" Dir.mkdir_p dir
end end
@titles = (Dir.entries dir) @titles = (Dir.entries dir)
.select! { |path| File.directory? File.join dir, path } .select! { |path| File.directory? File.join dir, path }

View File

@ -8,7 +8,6 @@ config = Config.load
library = Library.new config.library_path library = Library.new config.library_path
storage = Storage.new config.db_path storage = Storage.new config.db_path
macro layout(name) macro layout(name)
render "src/views/#{{{name}}}.ecr", "src/views/layout.ecr" render "src/views/#{{{name}}}.ecr", "src/views/layout.ecr"
end end
@ -23,11 +22,14 @@ macro get_username(env)
storage.verify_token cookie.value storage.verify_token cookie.value
end end
def hash_to_query(hash) macro send_json(env, json)
hash.map { |k, v| "#{k}=#{v}" } {{env}}.response.content_type = "application/json"
.join("&") {{json}}
end end
def hash_to_query(hash)
hash.map { |k, v| "#{k}=#{v}" }.join("&")
end
get "/" do |env| get "/" do |env|
titles = library.titles titles = library.titles
@ -53,6 +55,7 @@ get "/admin/user" do |env|
layout "user" layout "user"
end end
get "/admin/user/edit" do |env| get "/admin/user/edit" do |env|
username = env.params.query["username"]? username = env.params.query["username"]?
admin = env.params.query["admin"]? admin = env.params.query["admin"]?
@ -76,9 +79,16 @@ post "/admin/user/edit" do |env|
if username.size < 3 if username.size < 3
raise "Username should contain at least 3 characters" raise "Username should contain at least 3 characters"
end end
if (username =~ /^[A-Za-z0-9_]+$/).nil?
raise "Username should contain alphanumeric characters "\
"and underscores only"
end
if password.size < 6 if password.size < 6
raise "Password should contain at least 6 characters" raise "Password should contain at least 6 characters"
end end
if (password =~ /^[[:ascii:]]+$/).nil?
raise "password should contain ASCII characters only"
end
storage.new_user username, password, admin storage.new_user username, password, admin
@ -104,9 +114,19 @@ post "/admin/user/edit/:original_username" do |env|
if username.size < 3 if username.size < 3
raise "Username should contain at least 3 characters" raise "Username should contain at least 3 characters"
end end
if password.size != 0 && password.size < 6 if (username =~ /^[A-Za-z0-9_]+$/).nil?
raise "Username should contain alphanumeric characters "\
"and underscores only"
end
if password.size != 0
if password.size < 6
raise "Password should contain at least 6 characters" raise "Password should contain at least 6 characters"
end end
if (password =~ /^[[:ascii:]]+$/).nil?
raise "password should contain ASCII characters only"
end
end
storage.update_user original_username, username, password, admin storage.update_user original_username, username, password, admin
@ -121,14 +141,13 @@ post "/admin/user/edit/:original_username" do |env|
end end
end end
get "/reader/:title/:entry" do |env| get "/reader/:title/:entry" do |env|
# We should save the reading progress, and ask the user if she wants to # We should save the reading progress, and ask the user if she wants to
# start over or resume. For now we just start from page 0 # start over or resume. For now we just start from page 0
begin begin
title = library.get_title env.params.url["title"] title = (library.get_title env.params.url["title"]).not_nil!
raise "" if title.nil? entry = (title.get_entry env.params.url["entry"]).not_nil!
entry = title.get_entry env.params.url["entry"]
raise "" if entry.nil?
env.redirect "/reader/#{title.title}/#{entry.title}/0" env.redirect "/reader/#{title.title}/#{entry.title}/0"
rescue rescue
env.response.status_code = 404 env.response.status_code = 404
@ -139,10 +158,8 @@ get "/reader/:title/:entry/:page" do |env|
imgs_each_page = 5 imgs_each_page = 5
# here each :page contains `imgs_each_page` images # here each :page contains `imgs_each_page` images
begin begin
title = library.get_title env.params.url["title"] title = (library.get_title env.params.url["title"]).not_nil!
raise "" if title.nil? entry = (title.get_entry env.params.url["entry"]).not_nil!
entry = title.get_entry env.params.url["entry"]
raise "" if entry.nil?
page = env.params.url["page"].to_i page = env.params.url["page"].to_i
raise "" if page * imgs_each_page >= entry.pages raise "" if page * imgs_each_page >= entry.pages
@ -163,8 +180,7 @@ end
get "/logout" do |env| get "/logout" do |env|
begin begin
cookie = env.request.cookies.find { |c| c.name == "token" } cookie = env.request.cookies.find { |c| c.name == "token" }.not_nil!
raise "" if cookie.nil?
storage.logout cookie.value storage.logout cookie.value
rescue rescue
ensure ensure
@ -176,8 +192,7 @@ post "/login" do |env|
begin begin
username = env.params.body["username"] username = env.params.body["username"]
password = env.params.body["password"] password = env.params.body["password"]
token = storage.verify_user username, password token = storage.verify_user(username, password).not_nil!
raise "" if token.nil?
cookie = HTTP::Cookie.new "token", token cookie = HTTP::Cookie.new "token", token
env.response.cookies << cookie env.response.cookies << cookie
@ -215,8 +230,7 @@ get "/api/book/:title" do |env|
t = library.get_title title t = library.get_title title
raise "Title `#{title}` not found" if t.nil? raise "Title `#{title}` not found" if t.nil?
env.response.content_type = "application/json" send_json env, t.to_json
t.to_json
rescue e rescue e
STDERR.puts e STDERR.puts e
env.response.status_code = 500 env.response.status_code = 500
@ -225,8 +239,26 @@ get "/api/book/:title" do |env|
end end
get "/api/book" do |env| get "/api/book" do |env|
env.response.content_type = "application/json" send_json env, library.to_json
library.to_json end
post "/api/admin/scan" do |env|
start = Time.utc
library = Library.new config.library_path
ms = (Time.utc - start).total_milliseconds
send_json env, \
{"milliseconds" => ms, "titles" => library.titles.size}.to_json
end
post "/api/admin/user/delete/:username" do |env|
begin
username = env.params.url["username"]
storage.delete_user username
rescue e
send_json env, {"success" => false, "error" => e.message}.to_json
else
send_json env, {"success" => true}.to_json
end
end end
add_handler AuthHandler.new storage add_handler AuthHandler.new storage

View File

@ -20,6 +20,10 @@ class Storage
def initialize(path) def initialize(path)
@path = path @path = path
dir = File.dirname path
unless Dir.exists? dir
Dir.mkdir_p dir
end
DB.open "sqlite3://#{path}" do |db| DB.open "sqlite3://#{path}" do |db|
begin begin
db.exec "create table users" \ db.exec "create table users" \
@ -34,7 +38,7 @@ class Storage
random_pw = random_str random_pw = random_str
hash = hash_password random_pw hash = hash_password random_pw
db.exec "insert into users values (?, ?, ?, ?)", db.exec "insert into users values (?, ?, ?, ?)",
"admin", hash, "", 1 "admin", hash, nil, 1
puts "Initial user created. You can log in with " \ puts "Initial user created. You can log in with " \
"#{{"username" => "admin", "password" => random_pw}}" "#{{"username" => "admin", "password" => random_pw}}"
end end
@ -99,7 +103,7 @@ class Storage
DB.open "sqlite3://#{@path}" do |db| DB.open "sqlite3://#{@path}" do |db|
hash = hash_password password hash = hash_password password
db.exec "insert into users values (?, ?, ?, ?)", db.exec "insert into users values (?, ?, ?, ?)",
username, hash, "", admin username, hash, nil, admin
end end
end end
@ -119,11 +123,17 @@ class Storage
end end
end end
def delete_user(username)
DB.open "sqlite3://#{@path}" do |db|
db.exec "delete from users where username = (?)", username
end
end
def logout(token) def logout(token)
DB.open "sqlite3://#{@path}" do |db| DB.open "sqlite3://#{@path}" do |db|
begin begin
db.exec "update users set token = (?) where token = (?)", \ db.exec "update users set token = (?) where token = (?)", \
"", token nil, token
rescue rescue
end end
end end

View File

@ -1,7 +1,13 @@
<ul class="uk-list uk-list-large uk-list-divider"> <ul class="uk-list uk-list-large uk-list-divider">
<li data-url="/admin/info">Server Info</li> <li data-url="/admin/info">Server Info</li>
<li data-url="/admin/user">User Managerment</li> <li data-url="/admin/user">User Managerment</li>
<li onclick="scan()">Scan Library Files</li> <li onclick="if(!scanning){scan()}">
<span id="scan">Scan Library Files</span>
<span id="scan-status" style="float:right;">
<div uk-spinner hidden></div>
<span hidden style="cursor:auto;"></span>
</span>
</li>
</ul> </ul>
<hr class="uk-divider-icon"> <hr class="uk-divider-icon">
@ -9,8 +15,22 @@
<% content_for "script" do %> <% content_for "script" do %>
<script> <script>
var scanning = false
function scan() { function scan() {
alert("scan"); scanning = true
$('#scan-status > div').removeAttr('hidden');
$('#scan-status > span').attr('hidden', '');
var color = $('#scan').css('color');
$('#scan').css('color', 'gray');
$.post('/api/admin/scan', function (data) {
var ms = data.milliseconds;
var titles = data.titles;
$('#scan-status > span').text('Scanned ' + titles + ' titles in ' + ms + 'ms');
$('#scan-status > span').removeAttr('hidden');
$('#scan').css('color', color);
$('#scan-status > div').attr('hidden', '');
scanning = false;
});
} }
$(function() { $(function() {
$('li').click(function() { $('li').click(function() {

View File

@ -1,3 +1,5 @@
<h2 class=uk-title>Library</h2>
<p class="uk-text-meta"><%= titles.size %> titles found</p>
<div class="uk-child-width-1-4@m" uk-grid> <div class="uk-child-width-1-4@m" uk-grid>
<%- titles.each do |t| -%> <%- titles.each do |t| -%>
<div> <div>

View File

@ -11,8 +11,39 @@
</head> </head>
<body> <body>
<div class="uk-section uk-section-primary uk-section-small"> <div class="uk-offcanvas-content">
<p class="uk-align-right uk-margin-right">Hello !</p> <div class="uk-navbar-container uk-navbar-transparent" uk-navbar="uk-navbar">
<div id="mobile-nav" uk-offcanvas="overlay: true">
<div class="uk-offcanvas-bar uk-flex uk-flex-column">
<ul class="uk-nav uk-nav-primary uk-nav-center uk-margin-auto-vertical">
<li><a href="/">Home</a></li>
<li><a href="/admin">Admin</a></li>
<hr uk-divider>
<li><a href="/logout">Logout</a></li>
</ul>
</div>
</div>
</div>
</div>
<div class="uk-position-top">
<div class="uk-navbar-container uk-navbar-transparent" uk-navbar="uk-navbar">
<div class="uk-navbar-left uk-hidden@s">
<div class="uk-navbar-toggle" uk-navbar-toggle-icon="uk-navbar-toggle-icon" uk-toggle="target: #mobile-nav"></div>
</div>
<div class="uk-navbar-left uk-visible@s">
<ul class="uk-navbar-nav">
<li><a href="/">Home</a></li>
<li><a href="/admin">Admin</a></li>
</ul>
</div>
<div class="uk-navbar-right uk-visible@s">
<ul class="uk-navbar-nav">
<li><a href="/logout">Logout</a></li>
</ul>
</div>
</div>
</div>
<div class="uk-section uk-section-default uk-section-small">
</div> </div>
<div class="uk-section uk-section-default uk-section-small"> <div class="uk-section uk-section-default uk-section-small">
<div class="uk-container uk-container-small"> <div class="uk-container uk-container-small">

View File

@ -1,4 +1,5 @@
<h2 class=uk-title><%= title.title %></h2> <h2 class=uk-title><%= title.title %></h2>
<p class="uk-text-meta"><%= title.entries.size %> entries found</p>
<div class="uk-child-width-1-4@m" uk-grid> <div class="uk-child-width-1-4@m" uk-grid>
<%- title.entries.each do |e| -%> <%- title.entries.each do |e| -%>
<div> <div>

View File

@ -1,3 +1,4 @@
<div id="alert"></div>
<table class="uk-table uk-table-divider"> <table class="uk-table uk-table-divider">
<thead> <thead>
<tr> <tr>
@ -27,8 +28,24 @@
<% content_for "script" do %> <% content_for "script" do %>
<script> <script>
function alert(level, text) {
hideAlert();
var html = '<div class="uk-alert-' + level + '" uk-alert><a class="uk-alert-close" uk-close></a><p>' + text + '</p></div>';
$('#alert').append(html);
}
function hideAlert() {
$('#alert').empty();
}
function remove(username) { function remove(username) {
alert(username); $.post('/api/admin/user/delete/' + username, function(data) {
if (data.success) {
location.reload();
}
else {
error = data.error;
alert('danger', error);
}
});
} }
</script> </script>
<% end %> <% end %>