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 "./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
exclude ["/login"]
exclude ["/login"], "POST"
@ -18,9 +27,9 @@ class AuthHandler < Kemal::Handler
return env.redirect "/login"
end
if env.request.path.starts_with? "/admin"
if request_path_startswith env, ["/admin", "/api/admin"]
unless storage.verify_admin cookie.value
env.response.status_code = 401
return env.response.status_code = 401
end
end

View File

@ -9,10 +9,10 @@ class Config
property port = 9000
@[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")]
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
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." \
"Do you want mango to dump the default config there? [Y/n]"
input = gets
if !input.nil? && input.downcase == "n"
if input && input.downcase == "n"
abort "Aborting..."
end
default = self.allocate

View File

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

View File

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

View File

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

View File

@ -1,7 +1,13 @@
<ul class="uk-list uk-list-large uk-list-divider">
<li data-url="/admin/info">Server Info</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>
<hr class="uk-divider-icon">
@ -9,8 +15,22 @@
<% content_for "script" do %>
<script>
var scanning = false
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() {
$('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>
<%- titles.each do |t| -%>
<div>

View File

@ -11,13 +11,44 @@
</head>
<body>
<div class="uk-section uk-section-primary uk-section-small">
<p class="uk-align-right uk-margin-right">Hello !</p>
<div class="uk-offcanvas-content">
<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 class="uk-container uk-container-small">
<%= content %>
</div>
<div class="uk-section uk-section-default uk-section-small">
<div class="uk-container uk-container-small">
<%= content %>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/js/uikit.min.js"></script>

View File

@ -1,4 +1,5 @@
<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>
<%- title.entries.each do |e| -%>
<div>

View File

@ -1,3 +1,4 @@
<div id="alert"></div>
<table class="uk-table uk-table-divider">
<thead>
<tr>
@ -27,8 +28,24 @@
<% content_for "script" do %>
<script>
function remove(username) {
alert(username);
}
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) {
$.post('/api/admin/user/delete/' + username, function(data) {
if (data.success) {
location.reload();
}
else {
error = data.error;
alert('danger', error);
}
});
}
</script>
<% end %>