mirror of
https://github.com/hkalexling/Mango.git
synced 2025-08-03 11:25:29 -04:00
Merge branch 'dev'
This commit is contained in:
commit
14bf4da06c
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: apk add --no-cache yarn yaml sqlite-static
|
run: apk add --no-cache yarn yaml sqlite-static libarchive-dev libarchive-static acl-static expat-static zstd-static lz4-static bzip2-static
|
||||||
- name: Build
|
- name: Build
|
||||||
run: make static
|
run: make static
|
||||||
- name: Linter
|
- name: Linter
|
||||||
|
@ -4,7 +4,7 @@ WORKDIR /Mango
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
COPY package*.json .
|
COPY package*.json .
|
||||||
RUN apk add --no-cache yarn yaml sqlite-static \
|
RUN apk add --no-cache yarn yaml sqlite-static libarchive-dev libarchive-static acl-static expat-static zstd-static lz4-static bzip2-static \
|
||||||
&& make static
|
&& make static
|
||||||
|
|
||||||
FROM library/alpine
|
FROM library/alpine
|
||||||
|
2
Makefile
2
Makefile
@ -14,7 +14,7 @@ static: uglify | libs
|
|||||||
crystal build src/mango.cr --release --progress --static
|
crystal build src/mango.cr --release --progress --static
|
||||||
|
|
||||||
libs:
|
libs:
|
||||||
shards install
|
shards install --production
|
||||||
|
|
||||||
run:
|
run:
|
||||||
crystal run src/mango.cr --error-trace
|
crystal run src/mango.cr --error-trace
|
||||||
|
24
README.md
24
README.md
@ -11,7 +11,7 @@ Mango is a self-hosted manga server and reader. Its features include
|
|||||||
|
|
||||||
- Multi-user support
|
- Multi-user support
|
||||||
- Dark/light mode switch
|
- Dark/light mode switch
|
||||||
- Supports both `.zip` and `.cbz` formats
|
- Supported formats: `.cbz`, `.zip`, `.cbr` and `.rar`
|
||||||
- Supports nested folders in library
|
- Supports nested folders in library
|
||||||
- Automatically stores reading progress
|
- Automatically stores reading progress
|
||||||
- Built-in [MangaDex](https://mangadex.org/) downloader
|
- Built-in [MangaDex](https://mangadex.org/) downloader
|
||||||
@ -39,7 +39,7 @@ The official docker images are available on [Dockerhub](https://hub.docker.com/r
|
|||||||
|
|
||||||
### Build from source
|
### Build from source
|
||||||
|
|
||||||
1. Make sure you have `crystal`, `shards` and `yarn` installed. You might also need to install the development headers for `libsqlite3` and `libyaml`.
|
1. Make sure you have `crystal`, `shards` and `yarn` installed. You might also need to install the development headers of some libraries. Please see the [Dockerfile](https://github.com/hkalexling/Mango/blob/master/Dockerfile) for the full list of dependencies
|
||||||
2. Clone the repository
|
2. Clone the repository
|
||||||
3. `make && sudo make install`
|
3. `make && sudo make install`
|
||||||
4. Start Mango by running the command `mango`
|
4. Start Mango by running the command `mango`
|
||||||
@ -50,11 +50,21 @@ The official docker images are available on [Dockerhub](https://hub.docker.com/r
|
|||||||
### CLI
|
### CLI
|
||||||
|
|
||||||
```
|
```
|
||||||
Mango e-manga server/reader. Version 0.4.0
|
Mango - Manga Server and Web Reader. Version 0.5.0
|
||||||
|
|
||||||
-v, --version Show version
|
Usage:
|
||||||
-h, --help Show help
|
|
||||||
-c PATH, --config=PATH Path to the config file. Default is `~/.config/mango/config.yml`
|
mango [sub_command] [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
-c PATH, --config=PATH Path to the config file [type:String]
|
||||||
|
-h, --help Show this help.
|
||||||
|
-v, --version Show version.
|
||||||
|
|
||||||
|
Sub Commands:
|
||||||
|
|
||||||
|
admin Run admin tools
|
||||||
```
|
```
|
||||||
|
|
||||||
### Config
|
### Config
|
||||||
@ -84,7 +94,7 @@ mangadex:
|
|||||||
|
|
||||||
### Library Structure
|
### Library Structure
|
||||||
|
|
||||||
You can organize your `.cbz/.zip` files in nested folders in the library directory. Here's an example:
|
You can organize your archive files in nested folders in the library directory. Here's an example:
|
||||||
|
|
||||||
```
|
```
|
||||||
.
|
.
|
||||||
|
@ -242,7 +242,10 @@ const buildTable = () => {
|
|||||||
Object.entries(filters).forEach(([k, v]) => {
|
Object.entries(filters).forEach(([k, v]) => {
|
||||||
if (v === 'All') return;
|
if (v === 'All') return;
|
||||||
if (k === 'group') {
|
if (k === 'group') {
|
||||||
chapters = chapters.filter(c => v in c.groups);
|
chapters = chapters.filter(c => {
|
||||||
|
unescaped_groups = Object.entries(c.groups).map(([g, id]) => unescapeHTML(g));
|
||||||
|
return unescaped_groups.indexOf(v) >= 0;
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (k === 'lang') {
|
if (k === 'lang') {
|
||||||
@ -297,3 +300,9 @@ const buildTable = () => {
|
|||||||
});
|
});
|
||||||
$('#selection-controls').removeAttr('hidden');
|
$('#selection-controls').removeAttr('hidden');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const unescapeHTML = (str) => {
|
||||||
|
var elt = document.createElement("span");
|
||||||
|
elt.innerHTML = str;
|
||||||
|
return elt.innerText;
|
||||||
|
};
|
||||||
|
10
shard.lock
10
shard.lock
@ -2,12 +2,20 @@ version: 1.0
|
|||||||
shards:
|
shards:
|
||||||
ameba:
|
ameba:
|
||||||
github: crystal-ameba/ameba
|
github: crystal-ameba/ameba
|
||||||
version: 0.12.0
|
version: 0.12.1
|
||||||
|
|
||||||
|
archive:
|
||||||
|
github: hkalexling/archive.cr
|
||||||
|
version: 0.2.0
|
||||||
|
|
||||||
baked_file_system:
|
baked_file_system:
|
||||||
github: schovi/baked_file_system
|
github: schovi/baked_file_system
|
||||||
version: 0.9.8
|
version: 0.9.8
|
||||||
|
|
||||||
|
clim:
|
||||||
|
github: at-grandpa/clim
|
||||||
|
version: 0.12.0
|
||||||
|
|
||||||
db:
|
db:
|
||||||
github: crystal-lang/crystal-db
|
github: crystal-lang/crystal-db
|
||||||
version: 0.9.0
|
version: 0.9.0
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
name: mango
|
name: mango
|
||||||
version: 0.4.0
|
version: 0.5.0
|
||||||
|
|
||||||
authors:
|
authors:
|
||||||
- Alex Ling <hkalexling@gmail.com>
|
- Alex Ling <hkalexling@gmail.com>
|
||||||
@ -19,7 +19,9 @@ dependencies:
|
|||||||
github: crystal-lang/crystal-sqlite3
|
github: crystal-lang/crystal-sqlite3
|
||||||
baked_file_system:
|
baked_file_system:
|
||||||
github: schovi/baked_file_system
|
github: schovi/baked_file_system
|
||||||
|
archive:
|
||||||
development_dependencies:
|
github: hkalexling/archive.cr
|
||||||
ameba:
|
ameba:
|
||||||
github: crystal-ameba/ameba
|
github: crystal-ameba/ameba
|
||||||
|
clim:
|
||||||
|
github: at-grandpa/clim
|
||||||
|
@ -45,7 +45,7 @@ end
|
|||||||
def with_storage
|
def with_storage
|
||||||
with_default_config do
|
with_default_config do
|
||||||
temp_db = get_tempfile "mango-test-db"
|
temp_db = get_tempfile "mango-test-db"
|
||||||
storage = Storage.new temp_db.path
|
storage = Storage.new temp_db.path, false
|
||||||
clear = yield storage, temp_db.path
|
clear = yield storage, temp_db.path
|
||||||
if clear == true
|
if clear == true
|
||||||
temp_db.delete
|
temp_db.delete
|
||||||
|
59
src/archive.cr
Normal file
59
src/archive.cr
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
require "zip"
|
||||||
|
require "archive"
|
||||||
|
|
||||||
|
# A unified class to handle all supported archive formats. It uses the ::Zip
|
||||||
|
# module in crystal standard library if the target file is a zip archive.
|
||||||
|
# Otherwise it uses `archive.cr`.
|
||||||
|
class ArchiveFile
|
||||||
|
def initialize(@filename : String)
|
||||||
|
if [".cbz", ".zip"].includes? File.extname filename
|
||||||
|
@archive_file = Zip::File.new filename
|
||||||
|
else
|
||||||
|
@archive_file = Archive::File.new filename
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.open(filename : String, &)
|
||||||
|
s = self.new filename
|
||||||
|
yield s
|
||||||
|
s.close
|
||||||
|
end
|
||||||
|
|
||||||
|
def close
|
||||||
|
if @archive_file.is_a? Zip::File
|
||||||
|
@archive_file.as(Zip::File).close
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Lists all file entries
|
||||||
|
def entries
|
||||||
|
ary = [] of Zip::File::Entry | Archive::Entry
|
||||||
|
@archive_file.entries.map do |e|
|
||||||
|
if (e.is_a? Zip::File::Entry && e.file?) ||
|
||||||
|
(e.is_a? Archive::Entry && e.info.file?)
|
||||||
|
ary.push e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
ary
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_entry(e : Zip::File::Entry | Archive::Entry) : Bytes?
|
||||||
|
if e.is_a? Zip::File::Entry
|
||||||
|
data = nil
|
||||||
|
e.open do |io|
|
||||||
|
slice = Bytes.new e.uncompressed_size
|
||||||
|
bytes_read = io.read_fully? slice
|
||||||
|
data = slice if bytes_read
|
||||||
|
end
|
||||||
|
data
|
||||||
|
else
|
||||||
|
e.read
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def check
|
||||||
|
if @archive_file.is_a? Archive::File
|
||||||
|
@archive_file.as(Archive::File).check
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -9,7 +9,9 @@ class AuthHandler < Kemal::Handler
|
|||||||
def call(env)
|
def call(env)
|
||||||
return call_next(env) if request_path_startswith env, ["/login", "/logout"]
|
return call_next(env) if request_path_startswith env, ["/login", "/logout"]
|
||||||
|
|
||||||
cookie = env.request.cookies.find { |c| c.name == "token" }
|
cookie = env.request.cookies.find do |c|
|
||||||
|
c.name == "token-#{Config.current.port}"
|
||||||
|
end
|
||||||
if cookie.nil? || !@storage.verify_token cookie.value
|
if cookie.nil? || !@storage.verify_token cookie.value
|
||||||
return redirect env, "/login"
|
return redirect env, "/login"
|
||||||
end
|
end
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
require "zip"
|
|
||||||
require "mime"
|
require "mime"
|
||||||
require "json"
|
require "json"
|
||||||
require "uri"
|
require "uri"
|
||||||
require "./util"
|
require "./util"
|
||||||
|
require "./archive"
|
||||||
|
|
||||||
struct Image
|
struct Image
|
||||||
property data : Bytes
|
property data : Bytes
|
||||||
@ -25,7 +25,7 @@ class Entry
|
|||||||
@title = File.basename path, File.extname path
|
@title = File.basename path, File.extname path
|
||||||
@encoded_title = URI.encode @title
|
@encoded_title = URI.encode @title
|
||||||
@size = (File.size path).humanize_bytes
|
@size = (File.size path).humanize_bytes
|
||||||
file = Zip::File.new path
|
file = ArchiveFile.new path
|
||||||
@pages = file.entries.count do |e|
|
@pages = file.entries.count do |e|
|
||||||
["image/jpeg", "image/png"].includes? \
|
["image/jpeg", "image/png"].includes? \
|
||||||
MIME.from_filename? e.filename
|
MIME.from_filename? e.filename
|
||||||
@ -68,7 +68,8 @@ class Entry
|
|||||||
end
|
end
|
||||||
|
|
||||||
def read_page(page_num)
|
def read_page(page_num)
|
||||||
Zip::File.open @zip_path do |file|
|
img = nil
|
||||||
|
ArchiveFile.open @zip_path do |file|
|
||||||
page = file.entries
|
page = file.entries
|
||||||
.select { |e|
|
.select { |e|
|
||||||
["image/jpeg", "image/png"].includes? \
|
["image/jpeg", "image/png"].includes? \
|
||||||
@ -78,16 +79,13 @@ class Entry
|
|||||||
compare_alphanumerically a.filename, b.filename
|
compare_alphanumerically a.filename, b.filename
|
||||||
}
|
}
|
||||||
.[page_num - 1]
|
.[page_num - 1]
|
||||||
page.open do |io|
|
data = file.read_entry page
|
||||||
slice = Bytes.new page.uncompressed_size
|
if data
|
||||||
bytes_read = io.read_fully? slice
|
img = Image.new data, MIME.from_filename(page.filename), page.filename,
|
||||||
unless bytes_read
|
data.size
|
||||||
return nil
|
|
||||||
end
|
|
||||||
return Image.new slice, MIME.from_filename(page.filename),
|
|
||||||
page.filename, bytes_read
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
img
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -115,12 +113,16 @@ class Title
|
|||||||
@title_ids << title.id
|
@title_ids << title.id
|
||||||
next
|
next
|
||||||
end
|
end
|
||||||
if [".zip", ".cbz"].includes? File.extname path
|
if [".zip", ".cbz", ".rar", ".cbr"].includes? File.extname path
|
||||||
zip_exception = validate_zip path
|
unless File.readable? path
|
||||||
unless zip_exception.nil?
|
Logger.warn "File #{path} is not readable. Please make sure the " \
|
||||||
Logger.warn "File #{path} is corrupted or is not a valid zip " \
|
"file permission is configured correctly."
|
||||||
"archive. Ignoring it."
|
next
|
||||||
Logger.debug "Zip error: #{zip_exception}"
|
end
|
||||||
|
archive_exception = validate_archive path
|
||||||
|
unless archive_exception.nil?
|
||||||
|
Logger.warn "Unable to extract archive #{path}. Ignoring it. " \
|
||||||
|
"Archive error: #{archive_exception}"
|
||||||
next
|
next
|
||||||
end
|
end
|
||||||
entry = Entry.new path, self, @id, storage
|
entry = Entry.new path, self, @id, storage
|
||||||
|
@ -371,7 +371,7 @@ module MangaDex
|
|||||||
writer.close
|
writer.close
|
||||||
Logger.debug "cbz File created at #{zip_path}"
|
Logger.debug "cbz File created at #{zip_path}"
|
||||||
|
|
||||||
zip_exception = validate_zip zip_path
|
zip_exception = validate_archive zip_path
|
||||||
if !zip_exception.nil?
|
if !zip_exception.nil?
|
||||||
@queue.add_message "The downloaded archive is corrupted. " \
|
@queue.add_message "The downloaded archive is corrupted. " \
|
||||||
"Error: #{zip_exception}", job
|
"Error: #{zip_exception}", job
|
||||||
|
111
src/mango.cr
111
src/mango.cr
@ -2,31 +2,100 @@ require "./config"
|
|||||||
require "./server"
|
require "./server"
|
||||||
require "./mangadex/*"
|
require "./mangadex/*"
|
||||||
require "option_parser"
|
require "option_parser"
|
||||||
|
require "clim"
|
||||||
|
|
||||||
VERSION = "0.4.0"
|
MANGO_VERSION = "0.5.0"
|
||||||
|
|
||||||
config_path = nil
|
macro common_option
|
||||||
|
option "-c PATH", "--config=PATH", type: String,
|
||||||
|
desc: "Path to the config file"
|
||||||
|
end
|
||||||
|
|
||||||
OptionParser.parse do |parser|
|
macro throw(msg)
|
||||||
parser.banner = "Mango e-manga server/reader. Version #{VERSION}\n"
|
puts "ERROR: #{{{msg}}}"
|
||||||
|
puts
|
||||||
|
puts "Please see the `--help`."
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
parser.on "-v", "--version", "Show version" do
|
class CLI < Clim
|
||||||
puts "Version #{VERSION}"
|
main do
|
||||||
exit
|
desc "Mango - Manga Server and Web Reader. Version #{MANGO_VERSION}"
|
||||||
end
|
usage "mango [sub_command] [options]"
|
||||||
parser.on "-h", "--help", "Show help" do
|
help short: "-h"
|
||||||
puts parser
|
version "Version #{MANGO_VERSION}", short: "-v"
|
||||||
exit
|
common_option
|
||||||
end
|
run do |opts|
|
||||||
parser.on "-c PATH", "--config=PATH",
|
Config.load(opts.config).set_current
|
||||||
"Path to the config file. " \
|
MangaDex::Downloader.default
|
||||||
"Default is `~/.config/mango/config.yml`" do |path|
|
|
||||||
config_path = path
|
server = Server.new
|
||||||
|
server.start
|
||||||
|
end
|
||||||
|
|
||||||
|
sub "admin" do
|
||||||
|
desc "Run admin tools"
|
||||||
|
usage "mango admin [tool]"
|
||||||
|
help short: "-h"
|
||||||
|
run do |opts|
|
||||||
|
puts opts.help_string
|
||||||
|
end
|
||||||
|
sub "user" do
|
||||||
|
desc "User management tool"
|
||||||
|
usage "mango admin user [arguments] [options]"
|
||||||
|
help short: "-h"
|
||||||
|
argument "action", type: String,
|
||||||
|
desc: "Action to perform. Can be add/delete/update/list"
|
||||||
|
argument "username", type: String,
|
||||||
|
desc: "Username to update or delete"
|
||||||
|
option "-u USERNAME", "--username=USERNAME", type: String,
|
||||||
|
desc: "Username"
|
||||||
|
option "-p PASSWORD", "--password=PASSWORD", type: String,
|
||||||
|
desc: "Password"
|
||||||
|
option "-a", "--admin", desc: "Admin flag", type: Bool, default: false
|
||||||
|
common_option
|
||||||
|
run do |opts, args|
|
||||||
|
Config.load(opts.config).set_current
|
||||||
|
storage = Storage.new nil, false
|
||||||
|
|
||||||
|
case args.action
|
||||||
|
when "add"
|
||||||
|
throw "Options `-u` and `-p` required." if opts.username.nil? ||
|
||||||
|
opts.password.nil?
|
||||||
|
storage.new_user opts.username.not_nil!,
|
||||||
|
opts.password.not_nil!, opts.admin
|
||||||
|
when "delete"
|
||||||
|
throw "Argument `username` required." if args.username.nil?
|
||||||
|
storage.delete_user args.username
|
||||||
|
when "update"
|
||||||
|
throw "Argument `username` required." if args.username.nil?
|
||||||
|
username = opts.username || args.username
|
||||||
|
password = opts.password || ""
|
||||||
|
storage.update_user args.username, username.not_nil!,
|
||||||
|
password.not_nil!, opts.admin
|
||||||
|
when "list"
|
||||||
|
users = storage.list_users
|
||||||
|
name_length = users.map(&.[0].size).max
|
||||||
|
l_cell_width = ["username".size, name_length].max
|
||||||
|
r_cell_width = "admin access".size
|
||||||
|
header = " #{"username".ljust l_cell_width} | admin access "
|
||||||
|
puts "-" * header.size
|
||||||
|
puts header
|
||||||
|
puts "-" * header.size
|
||||||
|
users.each do |name, admin|
|
||||||
|
puts " #{name.ljust l_cell_width} | " \
|
||||||
|
"#{admin.to_s.ljust r_cell_width} "
|
||||||
|
end
|
||||||
|
puts "-" * header.size
|
||||||
|
when nil
|
||||||
|
puts opts.help_string
|
||||||
|
else
|
||||||
|
throw "Unknown action \"#{args.action}\"."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Config.load(config_path).set_current
|
CLI.start(ARGV)
|
||||||
MangaDex::Downloader.default
|
|
||||||
|
|
||||||
server = Server.new
|
|
||||||
server.start
|
|
||||||
|
@ -32,20 +32,6 @@ class AdminRouter < Router
|
|||||||
# would not contain `admin`
|
# would not contain `admin`
|
||||||
admin = !env.params.body["admin"]?.nil?
|
admin = !env.params.body["admin"]?.nil?
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
@context.storage.new_user username, password, admin
|
@context.storage.new_user username, password, admin
|
||||||
|
|
||||||
redirect env, "/admin/user"
|
redirect env, "/admin/user"
|
||||||
@ -65,23 +51,6 @@ class AdminRouter < Router
|
|||||||
admin = !env.params.body["admin"]?.nil?
|
admin = !env.params.body["admin"]?.nil?
|
||||||
original_username = env.params.url["original_username"]
|
original_username = env.params.url["original_username"]
|
||||||
|
|
||||||
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 != 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
|
|
||||||
|
|
||||||
@context.storage.update_user \
|
@context.storage.update_user \
|
||||||
original_username, username, password, admin
|
original_username, username, password, admin
|
||||||
|
|
||||||
|
@ -9,7 +9,9 @@ class MainRouter < Router
|
|||||||
|
|
||||||
get "/logout" do |env|
|
get "/logout" do |env|
|
||||||
begin
|
begin
|
||||||
cookie = env.request.cookies.find { |c| c.name == "token" }.not_nil!
|
cookie = env.request.cookies.find do |c|
|
||||||
|
c.name == "token-#{Config.current.port}"
|
||||||
|
end.not_nil!
|
||||||
@context.storage.logout cookie.value
|
@context.storage.logout cookie.value
|
||||||
rescue e
|
rescue e
|
||||||
@context.error "Error when attempting to log out: #{e}"
|
@context.error "Error when attempting to log out: #{e}"
|
||||||
@ -24,7 +26,8 @@ class MainRouter < Router
|
|||||||
password = env.params.body["password"]
|
password = env.params.body["password"]
|
||||||
token = @context.storage.verify_user(username, password).not_nil!
|
token = @context.storage.verify_user(username, password).not_nil!
|
||||||
|
|
||||||
cookie = HTTP::Cookie.new "token", token
|
cookie = HTTP::Cookie.new "token-#{Config.current.port}", token
|
||||||
|
cookie.path = Config.current.base_url
|
||||||
cookie.expires = Time.local.shift years: 1
|
cookie.expires = Time.local.shift years: 1
|
||||||
env.response.cookies << cookie
|
env.response.cookies << cookie
|
||||||
redirect env, "/"
|
redirect env, "/"
|
||||||
|
@ -22,7 +22,7 @@ class Storage
|
|||||||
@@default.not_nil!
|
@@default.not_nil!
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(db_path : String? = nil)
|
def initialize(db_path : String? = nil, init_user = true)
|
||||||
@path = db_path || Config.current.db_path
|
@path = db_path || Config.current.db_path
|
||||||
dir = File.dirname @path
|
dir = File.dirname @path
|
||||||
unless Dir.exists? dir
|
unless Dir.exists? dir
|
||||||
@ -51,12 +51,15 @@ class Storage
|
|||||||
Logger.debug "Creating DB file at #{@path}"
|
Logger.debug "Creating DB file at #{@path}"
|
||||||
db.exec "create unique index username_idx on users (username)"
|
db.exec "create unique index username_idx on users (username)"
|
||||||
db.exec "create unique index token_idx on users (token)"
|
db.exec "create unique index token_idx on users (token)"
|
||||||
random_pw = random_str
|
|
||||||
hash = hash_password random_pw
|
if init_user
|
||||||
db.exec "insert into users values (?, ?, ?, ?)",
|
random_pw = random_str
|
||||||
"admin", hash, nil, 1
|
hash = hash_password random_pw
|
||||||
Logger.log "Initial user created. You can log in with " \
|
db.exec "insert into users values (?, ?, ?, ?)",
|
||||||
"#{{"username" => "admin", "password" => random_pw}}"
|
"admin", hash, nil, 1
|
||||||
|
Logger.log "Initial user created. You can log in with " \
|
||||||
|
"#{{"username" => "admin", "password" => random_pw}}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -124,6 +127,8 @@ class Storage
|
|||||||
end
|
end
|
||||||
|
|
||||||
def new_user(username, password, admin)
|
def new_user(username, password, admin)
|
||||||
|
validate_username username
|
||||||
|
validate_password password
|
||||||
admin = (admin ? 1 : 0)
|
admin = (admin ? 1 : 0)
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
hash = hash_password password
|
hash = hash_password password
|
||||||
@ -134,8 +139,10 @@ class Storage
|
|||||||
|
|
||||||
def update_user(original_username, username, password, admin)
|
def update_user(original_username, username, password, admin)
|
||||||
admin = (admin ? 1 : 0)
|
admin = (admin ? 1 : 0)
|
||||||
|
validate_username username
|
||||||
|
validate_password password unless password.empty?
|
||||||
DB.open "sqlite3://#{@path}" do |db|
|
DB.open "sqlite3://#{@path}" do |db|
|
||||||
if password.size == 0
|
if password.empty?
|
||||||
db.exec "update users set username = (?), admin = (?) " \
|
db.exec "update users set username = (?), admin = (?) " \
|
||||||
"where username = (?)",
|
"where username = (?)",
|
||||||
username, admin, original_username
|
username, admin, original_username
|
||||||
|
36
src/util.cr
36
src/util.cr
@ -6,7 +6,9 @@ UPLOAD_URL_PREFIX = "/uploads"
|
|||||||
macro layout(name)
|
macro layout(name)
|
||||||
base_url = Config.current.base_url
|
base_url = Config.current.base_url
|
||||||
begin
|
begin
|
||||||
cookie = env.request.cookies.find { |c| c.name == "token" }
|
cookie = env.request.cookies.find do |c|
|
||||||
|
c.name == "token-#{Config.current.port}"
|
||||||
|
end
|
||||||
is_admin = false
|
is_admin = false
|
||||||
unless cookie.nil?
|
unless cookie.nil?
|
||||||
is_admin = @context.storage.verify_admin cookie.value
|
is_admin = @context.storage.verify_admin cookie.value
|
||||||
@ -26,7 +28,9 @@ end
|
|||||||
macro get_username(env)
|
macro get_username(env)
|
||||||
# if the request gets here, it has gone through the auth handler, and
|
# if the request gets here, it has gone through the auth handler, and
|
||||||
# we can be sure that a valid token exists, so we can use not_nil! here
|
# we can be sure that a valid token exists, so we can use not_nil! here
|
||||||
cookie = {{env}}.request.cookies.find { |c| c.name == "token" }.not_nil!
|
cookie = {{env}}.request.cookies.find do |c|
|
||||||
|
c.name == "token-#{Config.current.port}"
|
||||||
|
end.not_nil!
|
||||||
(@context.storage.verify_token cookie.value).not_nil!
|
(@context.storage.verify_token cookie.value).not_nil!
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -85,12 +89,9 @@ def compare_alphanumerically(a : String, b : String)
|
|||||||
compare_alphanumerically split_by_alphanumeric(a), split_by_alphanumeric(b)
|
compare_alphanumerically split_by_alphanumeric(a), split_by_alphanumeric(b)
|
||||||
end
|
end
|
||||||
|
|
||||||
# When downloading from MangaDex, the zip/cbz file would not be valid
|
def validate_archive(path : String) : Exception?
|
||||||
# before the download is completed. If we scan the zip file,
|
file = ArchiveFile.new path
|
||||||
# Entry.new would throw, so we use this method to check before
|
file.check
|
||||||
# constructing Entry
|
|
||||||
def validate_zip(path : String) : Exception?
|
|
||||||
file = Zip::File.new path
|
|
||||||
file.close
|
file.close
|
||||||
return
|
return
|
||||||
rescue e
|
rescue e
|
||||||
@ -105,3 +106,22 @@ def redirect(env, path)
|
|||||||
base = Config.current.base_url
|
base = Config.current.base_url
|
||||||
env.redirect File.join base, path
|
env.redirect File.join base, path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate_username(username)
|
||||||
|
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
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_password(password)
|
||||||
|
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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user