mirror of
https://github.com/hkalexling/Mango.git
synced 2025-08-03 03:15:31 -04:00
-
This commit is contained in:
parent
83f6fc25f0
commit
042df2bf1f
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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 }
|
||||||
|
74
src/mango.cr
74
src/mango.cr
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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() {
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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 %>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user