- basic admin panel and user management

This commit is contained in:
Alex Ling 2020-02-13 04:36:59 +00:00
parent f126dfb430
commit 83f6fc25f0
9 changed files with 307 additions and 17 deletions

View File

@ -8,3 +8,6 @@
.acard:hover {
text-decoration: none;
}
.uk-list li {
cursor: pointer;
}

View File

@ -13,13 +13,17 @@ class AuthHandler < Kemal::Handler
def call(env)
return call_next(env) if exclude_match?(env)
env.request.cookies.each do |c|
next if c.name != "token"
if @storage.verify_token c.value
return call_next env
cookie = env.request.cookies.find { |c| c.name == "token" }
if cookie.nil? || ! @storage.verify_token cookie.value
return env.redirect "/login"
end
if env.request.path.starts_with? "/admin"
unless storage.verify_admin cookie.value
env.response.status_code = 401
end
end
env.redirect "/login"
call_next env
end
end

View File

@ -17,6 +17,17 @@ macro send_img(env, img)
send_file {{env}}, {{img}}.data, {{img}}.mime
end
macro get_username(env)
cookie = {{env}}.request.cookies.find { |c| c.name == "token" }
next if cookie.nil?
storage.verify_token cookie.value
end
def hash_to_query(hash)
hash.map { |k, v| "#{k}=#{v}" }
.join("&")
end
get "/" do |env|
titles = library.titles
@ -32,6 +43,84 @@ get "/book/:title" do |env|
layout "title"
end
get "/admin" do |env|
layout "admin"
end
get "/admin/user" do |env|
users = storage.list_users
username = get_username env
layout "user"
end
get "/admin/user/edit" do |env|
username = env.params.query["username"]?
admin = env.params.query["admin"]?
if admin
admin = admin == "true"
end
error = env.params.query["error"]?
current_user = get_username env
new_user = username.nil? && admin.nil?
layout "user-edit"
end
post "/admin/user/edit" do |env|
# creating new user
begin
username = env.params.body["username"]
password = env.params.body["password"]
# if `admin` is unchecked, the body hash would not contain `admin`
admin = !env.params.body["admin"]?.nil?
if username.size < 3
raise "Username should contain at least 3 characters"
end
if password.size < 6
raise "Password should contain at least 6 characters"
end
storage.new_user username, password, admin
env.redirect "/admin/user"
rescue e
puts e.message
redirect_url = URI.new \
path: "/admin/user/edit",\
query: hash_to_query({"error" => e.message})
env.redirect redirect_url.to_s
end
end
post "/admin/user/edit/:original_username" do |env|
# editing existing user
begin
username = env.params.body["username"]
password = env.params.body["password"]
# if `admin` is unchecked, the body hash would not contain `admin`
admin = !env.params.body["admin"]?.nil?
original_username = env.params.url["original_username"]
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"
end
storage.update_user original_username, username, password, admin
env.redirect "/admin/user"
rescue e
puts e.message
redirect_url = URI.new \
path: "/admin/user/edit",\
query: hash_to_query({"username" => original_username, \
"admin" => admin, "error" => e.message})
env.redirect redirect_url.to_s
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
@ -72,18 +161,30 @@ get "/login" do |env|
render "src/views/login.ecr"
end
post "/login" do |env|
username = env.params.body["username"]
password = env.params.body["password"]
token = storage.verify_user username, password
if token.nil?
get "/logout" do |env|
begin
cookie = env.request.cookies.find { |c| c.name == "token" }
raise "" if cookie.nil?
storage.logout cookie.value
rescue
ensure
env.redirect "/login"
next
end
end
cookie = HTTP::Cookie.new "token", token
env.response.cookies << cookie
env.redirect "/"
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?
cookie = HTTP::Cookie.new "token", token
env.response.cookies << cookie
env.redirect "/"
rescue
env.redirect "/login"
end
end
get "/api/page/:title/:entry/:page" do |env|
@ -128,7 +229,6 @@ get "/api/book" do |env|
library.to_json
end
add_handler AuthHandler.new storage
Kemal.config.port = config.port

View File

@ -70,4 +70,62 @@ class Storage
end
end
end
def verify_admin(token)
DB.open "sqlite3://#{@path}" do |db|
begin
return db.query_one "select admin from users where " \
"token = (?)", token, as: Bool
rescue e : SQLite3::Exception | DB::Error
return false
end
end
end
def list_users()
results = Array(Tuple(String, Bool)).new
DB.open "sqlite3://#{@path}" do |db|
db.query "select username, admin from users" do |rs|
rs.each do
results << {rs.read(String), rs.read(Bool)}
end
end
end
results
end
def new_user(username, password, admin)
admin = (admin ? 1 : 0)
DB.open "sqlite3://#{@path}" do |db|
hash = hash_password password
db.exec "insert into users values (?, ?, ?, ?)",
username, hash, "", admin
end
end
def update_user(original_username, username, password, admin)
admin = (admin ? 1 : 0)
DB.open "sqlite3://#{@path}" do |db|
if password.size == 0
db.exec "update users set username = (?), admin = (?) "\
"where username = (?)",\
username, admin, original_username
else
hash = hash_password password
db.exec "update users set username = (?), admin = (?),"\
"password = (?) where username = (?)",\
username, admin, hash, original_username
end
end
end
def logout(token)
DB.open "sqlite3://#{@path}" do |db|
begin
db.exec "update users set token = (?) where token = (?)", \
"", token
rescue
end
end
end
end

24
src/views/admin.ecr Normal file
View File

@ -0,0 +1,24 @@
<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>
</ul>
<hr class="uk-divider-icon">
<a class="uk-button uk-button-danger" href="/logout">Log Out</a>
<% content_for "script" do %>
<script>
function scan() {
alert("scan");
}
$(function() {
$('li').click(function() {
url = $(this).attr('data-url');
if (url) {
$(location).attr('href', url)
}
});
});
</script>
<% end %>

View File

@ -6,7 +6,6 @@
<title>Mango</title>
<meta name="description" content="Mango Manga Server">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/css/uikit.min.css" />
<link rel="stylesheet" href="/css/mango.css" />
</head>
@ -23,5 +22,7 @@
<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>
<script src="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/js/uikit-icons.min.js"></script>
<%= yield_content "script" %>
</body>
</html>

View File

@ -6,7 +6,6 @@
<title>Mango</title>
<meta name="description" content="Mango Manga Server">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/css/uikit.min.css" />
<link rel="stylesheet" href="/css/mango.css" />
</head>

67
src/views/user-edit.ecr Normal file
View File

@ -0,0 +1,67 @@
<div id="alert"></div>
<form action="/admin/user/edit" method="post" accept-charset="utf-8">
<div class="uk-margin">
<label class="uk-form-label" for="form-stacked-text">Username</label>
<input class="uk-input" type="text" name="username"
<%- if username -%>
value=<%= username %>
<%- end -%>
>
</div>
<%- if new_user -%>
<div class="uk-margin">
<label class="uk-form-label" for="form-stacked-text">Password</label>
<input class="uk-input" type="password" name="password">
</div>
<%- end -%>
<div class="uk-margin">
<label class="uk-form-label" for="form-stacked-text">Admin Access</label>
<input class="uk-checkbox" type="checkbox" name="admin"
<%- if admin == true -%>
checked
<%- end -%>
>
</div>
<%- if !new_user -%>
<div>
<button class="uk-button uk-button-default" type="button" uk-toggle="target: #change-password">Change Password</button>
<div id="change-password" class="uk-margin" hidden>
<label class="uk-form-label" for="form-stacked-text">New Password</label>
<input class="uk-input" type="password" name="password">
</div>
</div>
<%- end -%>
<hr class="uk-divider-icon">
<input type="submit" value="Save" class="uk-button uk-button-primary">
</form>
<% content_for "script" do %>
<script>
$(function(){
var target = '/admin/user/edit';
<%- if !new_user -%>
target += '/<%= username %>';
<%- end -%>
$('form').attr('action', target);
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();
}
<%- if error -%>
alert('danger', '<%= error %>');
<%- end -%>
});
</script>
<% end %>

34
src/views/user.ecr Normal file
View File

@ -0,0 +1,34 @@
<table class="uk-table uk-table-divider">
<thead>
<tr>
<th>Username</th>
<th>Admin Access</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<%- users.each do |u| -%>
<tr>
<td><%= u[0] %></td>
<td><%= u[1] %></td>
<td>
<a href="/admin/user/edit?username=<%= u[0] %>&admin=<%= u[1] %>" uk-icon="file-edit"></a>
<%- if u[0] != username %>
<a href="#" onclick="remove('<%= u[0] %>');return false;" uk-icon="trash"></a>
<%- end %>
</td>
</tr>
<%- end -%>
</tbody>
</table>
<a href="/admin/user/edit" class="uk-button uk-button-primary">New User</a>
<% content_for "script" do %>
<script>
function remove(username) {
alert(username);
}
</script>
<% end %>