commit 3c2b054ec8979d164f735c35e7355d313d84f28c Author: Alex Ling Date: Tue Feb 11 22:06:17 2020 +0000 - initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..163eb75 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*.cr] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0bb75ea --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/docs/ +/lib/ +/bin/ +/.shards/ +*.dwarf diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..765f0e9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +language: crystal + +# Uncomment the following if you'd like Travis to run specs and check code formatting +# script: +# - crystal spec +# - crystal tool format --check diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8f009b5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2020 Alex Ling + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..140a971 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# mango + +TODO: Write a description here + +## Installation + +TODO: Write installation instructions here + +## Usage + +TODO: Write usage instructions here + +## Development + +TODO: Write development instructions here + +## Contributing + +1. Fork it () +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Add some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create a new Pull Request + +## Contributors + +- [Alex Ling](https://github.com/your-github-user) - creator and maintainer diff --git a/mango b/mango new file mode 100755 index 0000000..fd8dd8f Binary files /dev/null and b/mango differ diff --git a/shard.lock b/shard.lock new file mode 100644 index 0000000..1f86c47 --- /dev/null +++ b/shard.lock @@ -0,0 +1,30 @@ +version: 1.0 +shards: + db: + github: crystal-lang/crystal-db + version: 0.8.0 + + exception_page: + github: crystal-loot/exception_page + version: 0.1.2 + + kemal: + github: kemalcr/kemal + version: 0.26.1 + + kemal-basic-auth: + github: kemalcr/kemal-basic-auth + version: 0.2.0 + + kilt: + github: jeromegn/kilt + version: 0.4.0 + + radix: + github: luislavena/radix + version: 0.3.9 + + sqlite3: + github: crystal-lang/crystal-sqlite3 + version: 0.15.0 + diff --git a/shard.yml b/shard.yml new file mode 100644 index 0000000..c26e0a9 --- /dev/null +++ b/shard.yml @@ -0,0 +1,21 @@ +name: mango +version: 0.1.0 + +authors: + - Alex Ling + +targets: + mango: + main: src/mango.cr + +crystal: 0.32.1 + +license: MIT + +dependencies: + kemal: + github: kemalcr/kemal + kemal-basic-auth: + github: kemalcr/kemal-basic-auth + sqlite3: + github: crystal-lang/crystal-sqlite3 diff --git a/spec/mango_spec.cr b/spec/mango_spec.cr new file mode 100644 index 0000000..b83fc66 --- /dev/null +++ b/spec/mango_spec.cr @@ -0,0 +1,9 @@ +require "./spec_helper" + +describe Mango do + # TODO: Write tests + + it "works" do + false.should eq(true) + end +end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr new file mode 100644 index 0000000..3f10940 --- /dev/null +++ b/spec/spec_helper.cr @@ -0,0 +1,2 @@ +require "spec" +require "../src/mango" diff --git a/src/auth_handler.cr b/src/auth_handler.cr new file mode 100644 index 0000000..3e2c68b --- /dev/null +++ b/src/auth_handler.cr @@ -0,0 +1,15 @@ +require "kemal" + +class AuthHandler < Kemal::Handler + exclude ["/login"] + def call(env) + return call_next(env) if exclude_match?(env) + my_cookie = HTTP::Cookie.new( + name: "Example", + value: "KemalCR" + ) + env.response.cookies << my_cookie + + pp env.request.cookies + end +end diff --git a/src/config.cr b/src/config.cr new file mode 100644 index 0000000..aad2bc3 --- /dev/null +++ b/src/config.cr @@ -0,0 +1,37 @@ +require "yaml" +require "uuid" +require "base64" + +class Config + include YAML::Serializable + + @[YAML::Field(key: "port")] + property port = 9000 + + @[YAML::Field(key: "library_path")] + 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 + + def self.load + cfg_path = File.expand_path "~/.config/mango/config.yml", home: true + if File.exists? cfg_path + return self.from_yaml File.read cfg_path + end + 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" + abort "Aborting..." + end + default = self.allocate + cfg_dir = File.dirname cfg_path + unless Dir.exists? cfg_dir + Dir.mkdir_p cfg_dir + end + File.write cfg_path, default.to_yaml + puts "The config file has been created at #{cfg_path}." + default + end +end diff --git a/src/library.cr b/src/library.cr new file mode 100644 index 0000000..b4ed5de --- /dev/null +++ b/src/library.cr @@ -0,0 +1,44 @@ +require "zip" + +class Entry + property zip_path : String + property title : String + property size : String + + def initialize(path : String) + @zip_path = path + @title = File.basename path, ".zip" + @size = (File.size path).humanize_bytes + end +end + +class Title + property dir : String + property entries : Array(Entry) + property title : String + + def initialize(dir : String) + @dir = dir + @title = File.basename dir + @entries = (Dir.entries dir) + .select! { |path| (File.extname path) == ".zip" } + .map { |path| Entry.new File.join dir, path } + .sort { |a, b| a.title <=> b.title } + end +end + +class Library + property dir : String + property titles : Array(Title) + + def initialize(dir : String) + @dir = dir + unless Dir.exists? dir + abort "ERROR: The library directory #{dir} does not exist" + end + @titles = (Dir.entries dir) + .select! { |path| File.directory? File.join dir, path } + .map { |path| Title.new File.join dir, path } + .select! { |title| !title.entries.empty? } + end +end diff --git a/src/mango.cr b/src/mango.cr new file mode 100644 index 0000000..d992839 --- /dev/null +++ b/src/mango.cr @@ -0,0 +1,25 @@ +require "kemal" +require "./config" +require "./library" +require "./storage" +require "./auth_handler" + +config = Config.load + +library = Library.new config.library_path + +storage = Storage.new config.db_path + +get "/" do + "Hello World!" +end + +# APIs +get "/api/test" do |env| + "Hello!" +end + +add_handler AuthHandler.new + +Kemal.config.port = config.port +Kemal.run diff --git a/src/storage.cr b/src/storage.cr new file mode 100644 index 0000000..bfe6712 --- /dev/null +++ b/src/storage.cr @@ -0,0 +1,73 @@ +require "sqlite3" +require "crypto/bcrypt" +require "uuid" +require "base64" + +def hash_password(pw) + Crypto::Bcrypt::Password.create(pw).to_s +end + +def verify_password(hash, pw) + (Crypto::Bcrypt::Password.new hash).verify pw +end + +def random_str() + Base64.strict_encode UUID.random().to_s +end + +class Storage + property path : String + + def initialize(path) + @path = path + DB.open "sqlite3://#{path}" do |db| + begin + db.exec "create table users" \ + "(username text, password text, token text, admin integer)" + rescue e : SQLite3::Exception | DB::Error + unless e.message == "table users already exists" + raise e + end + else + db.exec "create unique index username_idx on users (username)" + db.exec "create unique index token_idx on users (token)" + random_pw = random_str + hash = hash_password random_pw + db.exec "insert into users values (?, ?, ?, ?)", + "admin", hash, "", 1 + puts "Initial user created. You can log in with " \ + "#{{"username" => "admin", "password" => random_pw}}" + end + end + end + + def verify_user(username, password) + DB.open "sqlite3://#{@path}" do |db| + begin + hash = db.query_one "select password from users where " \ + "username = (?)", username, as: String + unless verify_password hash, password + return nil + end + token = random_str + db.exec "update users set token = (?) where username = (?)", + token, username + return token + rescue e : SQLite3::Exception | DB::Error + return nil + end + end + end + + def verify_token(token) + DB.open "sqlite3://#{@path}" do |db| + begin + username = db.query_one "select username from users where " \ + "token = (?)", token, as: String + return username + rescue e : SQLite3::Exception | DB::Error + return nil + end + end + end +end