├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── profiles.clj ├── project.clj ├── resources ├── .catacumba.basedir ├── builtin.edn ├── config │ ├── default.edn │ └── test.edn ├── emails │ └── en │ │ └── register.mustache ├── migrations │ ├── 0000.main.up.sql │ ├── 0001.txlog.up.sql │ ├── 0002.auth.up.sql │ ├── 0003.projects.up.sql │ ├── 0004.pages.up.sql │ ├── 0005.kvstore.up.sql │ ├── 0006.emails.up.sql │ ├── 0007.images.up.sql │ ├── 0008.icons.up.sql │ └── XXXX.workers.up.sql ├── public │ └── static │ │ └── images │ │ └── email │ │ ├── facebook.png │ │ ├── img-header.jpg │ │ ├── linkedin.png │ │ ├── logo.png │ │ └── twitter.png └── sql │ ├── cli.sql │ ├── emails.sql │ ├── icons.sql │ ├── images.sql │ ├── kvstore.sql │ ├── pages.sql │ ├── projects.sql │ ├── users.sql │ └── workers.sql ├── scripts ├── fixtures.sh ├── run.sh └── smtpd.sh ├── src ├── data_readers.clj └── uxbox │ ├── cli │ ├── collimp.clj │ └── sql.clj │ ├── config.clj │ ├── db.clj │ ├── emails.clj │ ├── emails │ ├── core.clj │ ├── layouts.clj │ └── users.clj │ ├── fixtures.clj │ ├── frontend.clj │ ├── frontend │ ├── auth.clj │ ├── debug_emails.clj │ ├── errors.clj │ ├── icons.clj │ ├── images.clj │ ├── kvstore.clj │ ├── pages.clj │ ├── projects.clj │ ├── svgparse.clj │ └── users.clj │ ├── images.clj │ ├── locks.clj │ ├── main.clj │ ├── media.clj │ ├── migrations.clj │ ├── portation.clj │ ├── scheduled_jobs.clj │ ├── scheduled_jobs │ ├── emails.clj │ └── garbage.clj │ ├── services.clj │ ├── services │ ├── auth.clj │ ├── core.clj │ ├── icons.clj │ ├── images.clj │ ├── kvstore.clj │ ├── pages.clj │ ├── projects.clj │ ├── svgparse.clj │ └── users.clj │ ├── sql.clj │ └── util │ ├── blob.clj │ ├── cli.clj │ ├── closeable.clj │ ├── data.clj │ ├── exceptions.clj │ ├── images.clj │ ├── quartz.clj │ ├── response.clj │ ├── snappy.clj │ ├── spec.clj │ ├── tempfile.clj │ ├── template.clj │ ├── time.clj │ ├── token.clj │ ├── transit.clj │ ├── uuid.clj │ └── workers.clj ├── test ├── storages │ └── tests.clj └── uxbox │ └── tests │ ├── _files │ ├── sample.jpg │ ├── sample1.svg │ └── sample2.svg │ ├── helpers.clj │ ├── test_auth.clj │ ├── test_icons.clj │ ├── test_images.clj │ ├── test_kvstore.clj │ ├── test_pages.clj │ ├── test_projects.clj │ ├── test_svgparse.clj │ ├── test_txlog.clj │ └── test_users.clj └── vendor ├── executors └── core.clj └── storages ├── core.clj ├── fs ├── local.clj └── misc.clj ├── impl.clj ├── proto.clj └── util.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /resources/public/js/compiled/** 2 | figwheel_server.log 3 | pom.xml 4 | *jar 5 | /lib/ 6 | /classes/ 7 | /out/ 8 | /target/ 9 | .lein-deps-sum 10 | .lein-failures 11 | .lein-repl-history 12 | .lein-plugins/ 13 | .repl 14 | .nrepl-port 15 | /hicv/ 16 | node_modules 17 | /resources/public/css 18 | /resources/public/js 19 | /resources/public/media 20 | /*-init.clj -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | lein: lein 3 | sudo: false 4 | 5 | services: 6 | - postgresql 7 | 8 | before_script: 9 | - createdb test 10 | 11 | script: 12 | - lein test 13 | 14 | jdk: 15 | - oraclejdk8 16 | 17 | notifications: 18 | email: 19 | recipients: 20 | - niwi@niwi.nz 21 | on_success: change 22 | on_failure: change 23 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute to UXBox # 2 | 3 | Thank you for your interest in contributing to UXBox. This guide details how 4 | to contribute to UXBox in a way that is efficient for everyone. 5 | 6 | ## Contributor License Agreement ## 7 | 8 | By submitting code you are agree and can certify the below: 9 | 10 | Developer's Certificate of Origin 1.1 11 | 12 | By making a contribution to this project, I certify that: 13 | 14 | (a) The contribution was created in whole or in part by me and I 15 | have the right to submit it under the open source license 16 | indicated in the file; or 17 | 18 | (b) The contribution is based upon previous work that, to the best 19 | of my knowledge, is covered under an appropriate open source 20 | license and I have the right under that license to submit that 21 | work with modifications, whether created in whole or in part 22 | by me, under the same open source license (unless I am 23 | permitted to submit under a different license), as indicated 24 | in the file; or 25 | 26 | (c) The contribution was provided directly to me by some other 27 | person who certified (a), (b) or (c) and I have not modified 28 | it. 29 | 30 | (d) I understand and agree that this project and the contribution 31 | are public and that a record of the contribution (including all 32 | personal information I submit with it, including my sign-off) is 33 | maintained indefinitely and may be redistributed consistent with 34 | this project or the open source license(s) involved. 35 | 36 | Then, all your patches should contain a sign-off at the end of the patch/commit 37 | description body. It can be automatically added on adding `-s` parameter to 38 | `git commit`. 39 | 40 | This is an example of the aspect of the line: 41 | 42 | Signed-off-by: Andrey Antukh 43 | 44 | Please, use your real name (sorry, no pseudonyms or anonymous contributions 45 | are allowed). 46 | 47 | 48 | ## How to contribute ## 49 | 50 | > **WARNING** 51 | > 52 | > The project is still in a **design phase of the development** and 53 | > pull-requests with new functionality without previous chat are very 54 | > discouraged. 55 | 56 | If you have an idea and think that it would be awesome to have implemented in 57 | _uxbox_ or you have some time and you want contribute doing some bugfixing or 58 | implementing some small task, **please open an issue and discuss it**. 59 | 60 | No pull-request will be accepted without previous chat about the changes; 61 | independently if it is a new feature, already planned feature, small task or 62 | bug fix. The coordination is key for avoid doing double work. 63 | 64 | 65 | ## Code of conduct ## 66 | 67 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 68 | 69 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 70 | 71 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 72 | 73 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 74 | 75 | This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. 76 | 77 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 78 | 79 | This Code of Conduct is adapted from the Contributor Covenant, version 1.1.0, available from http://contributor-covenant.org/version/1/1/0/ 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UXBox Backend # 2 | 3 | **Merged into `uxbox` repository**, this repository is not going to be maintained. 4 | 5 | 6 | ## License ## 7 | 8 | ``` 9 | This Source Code Form is subject to the terms of the Mozilla Public 10 | License, v. 2.0. If a copy of the MPL was not distributed with this 11 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 12 | ``` 13 | 14 | [1]: https://github.com/uxbox/uxbox-docker 15 | -------------------------------------------------------------------------------- /profiles.clj: -------------------------------------------------------------------------------- 1 | {:dev 2 | {:plugins [[lein-ancient "0.6.10"]] 3 | :dependencies [[clj-http "2.1.0"]] 4 | :main ^:skip-aot uxbox.main} 5 | 6 | :prod 7 | {:jvm-opts ^:replace ["-Xms4g" "-Xmx4g" "-XX:+UseG1GC" 8 | "-XX:+AggressiveOpts" "-server"]}} 9 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject uxbox-backend "0.1.0-SNAPSHOT" 2 | :description "UXBox backend." 3 | :url "http://uxbox.github.io" 4 | :license {:name "MPL 2.0" :url "https://www.mozilla.org/en-US/MPL/2.0/"} 5 | :source-paths ["src" "vendor"] 6 | :javac-options ["-target" "1.8" "-source" "1.8" "-Xlint:-options"] 7 | :jvm-opts ["-Dclojure.compiler.direct-linking=true" 8 | ;; "-Dcom.sun.management.jmxremote.port=9090" 9 | ;; "-Dcom.sun.management.jmxremote.authenticate=false" 10 | ;; "-Dcom.sun.management.jmxremote.ssl=false" 11 | ;; "-Dcom.sun.management.jmxremote.rmi.port=9090" 12 | ;; "-Djava.rmi.server.hostname=0.0.0.0" 13 | "-Dclojure.spec.check-asserts=true" 14 | "-Dclojure.spec.compile-asserts=true" 15 | "-XX:+UseG1GC" "-Xms1g" "-Xmx1g"] 16 | :global-vars {*assert* true} 17 | :dependencies [[org.clojure/clojure "1.9.0-alpha14"] 18 | [org.clojure/tools.logging "0.3.1"] 19 | [funcool/struct "1.0.0"] 20 | [funcool/suricatta "1.2.0"] 21 | [funcool/promesa "1.6.0"] 22 | [funcool/catacumba "2.0.0-SNAPSHOT"] 23 | 24 | [org.clojure/data.xml "0.1.0-beta2"] 25 | [org.jsoup/jsoup "1.10.1"] 26 | 27 | [hiccup "1.0.5"] 28 | [org.im4java/im4java "1.4.0"] 29 | 30 | [org.slf4j/slf4j-simple "1.7.21"] 31 | [com.layerware/hugsql-core "0.4.7" 32 | :exclusions [org.clojure/tools.reader]] 33 | [niwinz/migrante "0.1.0"] 34 | 35 | [buddy/buddy-sign "1.3.0" :exclusions [org.clojure/tools.reader]] 36 | [buddy/buddy-hashers "1.1.0"] 37 | 38 | [org.xerial.snappy/snappy-java "1.1.2.6"] 39 | [com.github.spullara.mustache.java/compiler "0.9.4"] 40 | [org.postgresql/postgresql "9.4.1212"] 41 | [org.quartz-scheduler/quartz "2.2.3"] 42 | [org.quartz-scheduler/quartz-jobs "2.2.3"] 43 | [commons-io/commons-io "2.5"] 44 | [com.draines/postal "2.0.2"] 45 | 46 | [hikari-cp "1.7.5"] 47 | [mount "0.1.10"] 48 | [environ "1.1.0"]]) 49 | -------------------------------------------------------------------------------- /resources/.catacumba.basedir: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uxbox/uxbox-backend/036c42db8424be3ac34c38be80577ee279141681/resources/.catacumba.basedir -------------------------------------------------------------------------------- /resources/builtin.edn: -------------------------------------------------------------------------------- 1 | {:icons 2 | [{:name "Material Design (Action)" 3 | :path "./material/action/svg/production" 4 | :regex #"^.*_48px\.svg$"}] 5 | 6 | :images 7 | [{:name "Generic Collection 1" 8 | :path "./my-images/collection1/" 9 | :regex #"^.*\.(png|jpg|webp)$"}]} 10 | 11 | 12 | -------------------------------------------------------------------------------- /resources/config/default.edn: -------------------------------------------------------------------------------- 1 | {;; A secret key used for create tokens 2 | ;; WARNING: this is a default secret key and 3 | ;; it should be overwritten in production env. 4 | :secret "5qjiAn-QUpawUNqGP10UZKklSqbLKcdGY3sJpq0UUACpVXGg2HOFJCBejDWVHskhRyp7iHb4rjOLXX2ZjF-5cw" 5 | :smtp 6 | {:host "localhost" ;; Hostname of the desired SMTP server. 7 | :port 25 ;; Port of SMTP server. 8 | :user nil ;; Username to authenticate with (if authenticating). 9 | :pass nil ;; Password to authenticate with (if authenticating). 10 | :ssl false ;; Enables SSL encryption if value is truthy. 11 | :tls false ;; Enables TLS encryption if value is truthy. 12 | :noop true} 13 | 14 | :auth-options {:alg :a256kw :enc :a128cbc-hs256} 15 | 16 | :email {:reply-to "no-reply@uxbox.io" 17 | :from "no-reply@uxbox.io"} 18 | 19 | :http {:port 6060 20 | :max-body-size 52428800 21 | :debug true} 22 | 23 | :media 24 | {:basedir "resources/public/media" 25 | :baseuri "http://localhost:6060/media/"} 26 | 27 | :static 28 | {:basedir "resources/public/static" 29 | :baseuri "http://localhost:6060/static/"} 30 | 31 | :database 32 | {:adapter "postgresql" 33 | :username nil 34 | :password nil 35 | :database-name "uxbox" 36 | :server-name "localhost" 37 | :port-number 5432}} 38 | -------------------------------------------------------------------------------- /resources/config/test.edn: -------------------------------------------------------------------------------- 1 | {:migrations 2 | {:verbose false} 3 | 4 | :media 5 | {:basedir "/tmp/uxbox/media" 6 | :baseuri "http://localhost:6060/media/"} 7 | 8 | :static 9 | {:basedir "/tmp/uxbox/static" 10 | :baseuri "http://localhost:6060/static/"} 11 | 12 | :database 13 | {:adapter "postgresql" 14 | :username nil 15 | :password nil 16 | :database-name "test" 17 | :server-name "localhost" 18 | :port-number 5432}} 19 | -------------------------------------------------------------------------------- /resources/emails/en/register.mustache: -------------------------------------------------------------------------------- 1 | -- begin :subject 2 | Welcome to UXBOX. 3 | -- end 4 | 5 | -- begin :body-text 6 | Hello {{user}}! 7 | 8 | Welcome to UXBOX. 9 | 10 | UXBOX team. 11 | -- end 12 | 13 | -- begin :body-html 14 |

Hello {{user}}!

15 |

Welcome to UXBOX.

16 |

UXBOX team.

17 | -- end -------------------------------------------------------------------------------- /resources/migrations/0000.main.up.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 2 | CREATE EXTENSION IF NOT EXISTS "pgcrypto"; 3 | 4 | -- OCC 5 | 6 | CREATE OR REPLACE FUNCTION handle_occ() 7 | RETURNS TRIGGER AS $occ$ 8 | BEGIN 9 | IF (NEW.version != OLD.version) THEN 10 | RAISE EXCEPTION 'Version missmatch: expected % given %', 11 | OLD.version, NEW.version 12 | USING ERRCODE='P0002'; 13 | ELSE 14 | NEW.version := NEW.version + 1; 15 | END IF; 16 | RETURN NEW; 17 | END; 18 | $occ$ LANGUAGE plpgsql; 19 | 20 | -- Modified At 21 | 22 | CREATE OR REPLACE FUNCTION update_modified_at() 23 | RETURNS TRIGGER AS $updt$ 24 | BEGIN 25 | NEW.modified_at := clock_timestamp(); 26 | RETURN NEW; 27 | END; 28 | $updt$ LANGUAGE plpgsql; 29 | -------------------------------------------------------------------------------- /resources/migrations/0001.txlog.up.sql: -------------------------------------------------------------------------------- 1 | -- A table that will store the whole transaction log of the database. 2 | CREATE TABLE IF NOT EXISTS txlog ( 3 | id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 4 | created_at timestamptz NOT NULL DEFAULT clock_timestamp(), 5 | payload bytea NOT NULL 6 | ); 7 | 8 | CREATE OR REPLACE FUNCTION handle_txlog_notify() 9 | RETURNS TRIGGER AS $notify$ 10 | BEGIN 11 | PERFORM pg_notify('uxbox.transaction', (NEW.id)::text); 12 | RETURN NEW; 13 | END; 14 | $notify$ LANGUAGE plpgsql; 15 | 16 | CREATE TRIGGER txlog_notify_tgr AFTER INSERT ON txlog 17 | FOR EACH ROW EXECUTE PROCEDURE handle_txlog_notify(); 18 | -------------------------------------------------------------------------------- /resources/migrations/0002.auth.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 3 | 4 | created_at timestamptz NOT NULL DEFAULT clock_timestamp(), 5 | modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), 6 | deleted_at timestamptz DEFAULT NULL, 7 | 8 | fullname text NOT NULL DEFAULT '', 9 | username text NOT NULL, 10 | email text NOT NULL, 11 | photo text NOT NULL, 12 | password text NOT NULL, 13 | metadata bytea NOT NULL 14 | ); 15 | 16 | -- Insert a placeholder system user. 17 | INSERT INTO users (id, fullname, username, email, photo, password, metadata) 18 | VALUES ('00000000-0000-0000-0000-000000000000'::uuid, 19 | 'System User', 20 | '00000000-0000-0000-0000-000000000000', 21 | 'system@uxbox.io', 22 | '', 23 | '!', 24 | ''::bytea); 25 | 26 | CREATE UNIQUE INDEX users_username_idx 27 | ON users USING btree (username); 28 | 29 | CREATE UNIQUE INDEX users_email_idx 30 | ON users USING btree (email); 31 | 32 | CREATE TRIGGER users_modified_at_tgr BEFORE UPDATE ON users 33 | FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); 34 | 35 | CREATE TABLE user_pswd_recovery ( 36 | id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 37 | "user" uuid REFERENCES users(id) ON DELETE CASCADE, 38 | token text NOT NULL, 39 | 40 | created_at timestamptz NOT NULL DEFAULT clock_timestamp(), 41 | used_at timestamptz DEFAULT NULL 42 | ); 43 | 44 | CREATE INDEX user_pswd_recovery_user_idx 45 | ON user_pswd_recovery USING btree ("user"); 46 | 47 | CREATE UNIQUE INDEX user_pswd_recovery_token_idx 48 | ON user_pswd_recovery USING btree (token); 49 | 50 | -------------------------------------------------------------------------------- /resources/migrations/0003.projects.up.sql: -------------------------------------------------------------------------------- 1 | -- Table 2 | 3 | CREATE TABLE IF NOT EXISTS projects ( 4 | id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 5 | "user" uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, 6 | 7 | created_at timestamptz NOT NULL DEFAULT clock_timestamp(), 8 | modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), 9 | deleted_at timestamptz DEFAULT NULL, 10 | 11 | version bigint NOT NULL DEFAULT 0, 12 | name text NOT NULL 13 | ); 14 | 15 | CREATE TABLE IF NOT EXISTS project_shares ( 16 | project uuid PRIMARY KEY REFERENCES projects(id) ON DELETE CASCADE, 17 | 18 | created_at timestamptz NOT NULL DEFAULT clock_timestamp(), 19 | modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), 20 | token text 21 | ); 22 | 23 | -- Indexes 24 | 25 | CREATE INDEX projects_user_idx 26 | ON projects("user"); 27 | 28 | CREATE UNIQUE INDEX projects_shares_token_idx 29 | ON project_shares(token); 30 | 31 | -- Triggers 32 | 33 | CREATE OR REPLACE FUNCTION handle_project_create() 34 | RETURNS TRIGGER AS $$ 35 | DECLARE 36 | token text; 37 | BEGIN 38 | SELECT encode(digest(gen_random_bytes(128), 'sha256'), 'hex') 39 | INTO token; 40 | 41 | INSERT INTO project_shares (project, token) 42 | VALUES (NEW.id, token); 43 | 44 | RETURN NEW; 45 | END; 46 | $$ LANGUAGE plpgsql; 47 | 48 | CREATE TRIGGER project_on_create_tgr 49 | AFTER INSERT ON projects 50 | FOR EACH ROW EXECUTE PROCEDURE handle_project_create(); 51 | 52 | CREATE TRIGGER project_occ_tgr 53 | BEFORE UPDATE ON projects 54 | FOR EACH ROW EXECUTE PROCEDURE handle_occ(); 55 | 56 | CREATE TRIGGER projects_modified_at_tgr 57 | BEFORE UPDATE ON projects 58 | FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); 59 | 60 | CREATE TRIGGER project_shares_modified_at_tgr 61 | BEFORE UPDATE ON project_shares 62 | FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); 63 | -------------------------------------------------------------------------------- /resources/migrations/0004.pages.up.sql: -------------------------------------------------------------------------------- 1 | -- Tables 2 | 3 | CREATE TABLE IF NOT EXISTS pages ( 4 | id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 5 | 6 | "user" uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, 7 | project uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE, 8 | 9 | created_at timestamptz NOT NULL DEFAULT clock_timestamp(), 10 | modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), 11 | deleted_at timestamptz DEFAULT NULL, 12 | version bigint DEFAULT 0, 13 | 14 | name text NOT NULL, 15 | data bytea NOT NULL, 16 | metadata bytea NOT NULL 17 | ); 18 | 19 | CREATE TABLE IF NOT EXISTS pages_history ( 20 | id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 21 | 22 | "user" uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, 23 | page uuid NOT NULL REFERENCES pages(id) ON DELETE CASCADE, 24 | 25 | created_at timestamptz NOT NULL, 26 | modified_at timestamptz NOT NULL, 27 | version bigint NOT NULL DEFAULT 0, 28 | 29 | pinned bool NOT NULL DEFAULT false, 30 | label text NOT NULL DEFAULT '', 31 | data bytea NOT NULL 32 | ); 33 | 34 | -- Indexes 35 | 36 | CREATE INDEX pages_project_idx ON pages(project); 37 | CREATE INDEX pages_user_idx ON pages("user"); 38 | CREATE INDEX pages_history_page_idx ON pages_history(page); 39 | CREATE INDEX pages_history_user_idx ON pages_history("user"); 40 | 41 | -- Triggers 42 | 43 | CREATE OR REPLACE FUNCTION handle_page_update() 44 | RETURNS TRIGGER AS $pagechange$ 45 | BEGIN 46 | --- Update projects modified_at attribute when a 47 | --- page of that project is modified. 48 | UPDATE projects SET modified_at = clock_timestamp() 49 | WHERE id = OLD.project; 50 | 51 | --- Register a new history entry if the data 52 | --- property is changed. 53 | IF (OLD.data != NEW.data) THEN 54 | INSERT INTO pages_history (page, "user", created_at, 55 | modified_at, data, version) 56 | VALUES (OLD.id, OLD."user", OLD.modified_at, 57 | OLD.modified_at, OLD.data, OLD.version); 58 | END IF; 59 | 60 | RETURN NEW; 61 | END; 62 | $pagechange$ LANGUAGE plpgsql; 63 | 64 | CREATE TRIGGER page_on_update_tgr BEFORE UPDATE ON pages 65 | FOR EACH ROW EXECUTE PROCEDURE handle_page_update(); 66 | 67 | CREATE TRIGGER page_occ_tgr BEFORE UPDATE ON pages 68 | FOR EACH ROW EXECUTE PROCEDURE handle_occ(); 69 | 70 | CREATE TRIGGER pages_modified_at_tgr BEFORE UPDATE ON pages 71 | FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); 72 | 73 | CREATE TRIGGER pages_history_modified_at_tgr BEFORE UPDATE ON pages 74 | FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); 75 | -------------------------------------------------------------------------------- /resources/migrations/0005.kvstore.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS kvstore ( 2 | "user" uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, 3 | 4 | created_at timestamptz NOT NULL DEFAULT clock_timestamp(), 5 | modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), 6 | 7 | version bigint NOT NULL DEFAULT 0, 8 | 9 | key text NOT NULL, 10 | value bytea NOT NULL, 11 | 12 | PRIMARY KEY (key, "user") 13 | ); 14 | 15 | CREATE TRIGGER kvstore_occ_tgr BEFORE UPDATE ON kvstore 16 | FOR EACH ROW EXECUTE PROCEDURE handle_occ(); 17 | 18 | CREATE TRIGGER kvstore_modified_at_tgr BEFORE UPDATE ON kvstore 19 | FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); 20 | -------------------------------------------------------------------------------- /resources/migrations/0006.emails.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE email_status AS ENUM ('pending', 'ok', 'failed'); 2 | 3 | CREATE TABLE IF NOT EXISTS email_queue ( 4 | id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 5 | 6 | created_at timestamptz NOT NULL DEFAULT clock_timestamp(), 7 | modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), 8 | deleted_at timestamptz DEFAULT NULL, 9 | 10 | data bytea NOT NULL, 11 | 12 | priority smallint NOT NULL DEFAULT 10 13 | CHECK (priority BETWEEN 0 and 10), 14 | 15 | status email_status NOT NULL DEFAULT 'pending', 16 | retries integer NOT NULL DEFAULT -1 17 | ); 18 | 19 | -- Triggers 20 | 21 | CREATE TRIGGER email_queue_modified_at_tgr BEFORE UPDATE ON email_queue 22 | FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); 23 | 24 | -- Indexes 25 | 26 | CREATE INDEX email_status_idx 27 | ON email_queue (status); 28 | -------------------------------------------------------------------------------- /resources/migrations/0007.images.up.sql: -------------------------------------------------------------------------------- 1 | -- Tables 2 | 3 | CREATE TABLE IF NOT EXISTS images_collections ( 4 | id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 5 | "user" uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, 6 | 7 | created_at timestamptz NOT NULL DEFAULT clock_timestamp(), 8 | modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), 9 | deleted_at timestamptz DEFAULT NULL, 10 | version bigint NOT NULL DEFAULT 0, 11 | 12 | name text NOT NULL 13 | ); 14 | 15 | CREATE TABLE IF NOT EXISTS images ( 16 | id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 17 | "user" uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, 18 | 19 | created_at timestamptz NOT NULL DEFAULT clock_timestamp(), 20 | modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), 21 | deleted_at timestamptz DEFAULT NULL, 22 | 23 | version bigint NOT NULL DEFAULT 0, 24 | 25 | width int NOT NULL, 26 | height int NOT NULL, 27 | mimetype text NOT NULL, 28 | collection uuid REFERENCES images_collections(id) 29 | ON DELETE SET NULL 30 | DEFAULT NULL, 31 | name text NOT NULL, 32 | path text NOT NULL 33 | ); 34 | 35 | -- Indexes 36 | 37 | CREATE INDEX images_collections_user_idx 38 | ON images_collections ("user"); 39 | 40 | CREATE INDEX images_collection_idx 41 | ON images (collection); 42 | 43 | CREATE INDEX images_user_idx 44 | ON images ("user"); 45 | 46 | -- Triggers 47 | 48 | CREATE TRIGGER images_collections_occ_tgr BEFORE UPDATE ON images_collections 49 | FOR EACH ROW EXECUTE PROCEDURE handle_occ(); 50 | 51 | CREATE TRIGGER images_collections_modified_at_tgr BEFORE UPDATE ON images_collections 52 | FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); 53 | 54 | CREATE TRIGGER images_occ_tgr BEFORE UPDATE ON images 55 | FOR EACH ROW EXECUTE PROCEDURE handle_occ(); 56 | 57 | CREATE TRIGGER images_modified_at_tgr BEFORE UPDATE ON images 58 | FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); 59 | 60 | -------------------------------------------------------------------------------- /resources/migrations/0008.icons.up.sql: -------------------------------------------------------------------------------- 1 | -- Tables 2 | 3 | CREATE TABLE IF NOT EXISTS icons_collections ( 4 | id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 5 | "user" uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, 6 | 7 | created_at timestamptz NOT NULL DEFAULT clock_timestamp(), 8 | modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), 9 | deleted_at timestamptz DEFAULT NULL, 10 | version bigint NOT NULL DEFAULT 0, 11 | 12 | name text NOT NULL 13 | ); 14 | 15 | CREATE TABLE IF NOT EXISTS icons ( 16 | id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 17 | "user" uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, 18 | 19 | created_at timestamptz NOT NULL DEFAULT clock_timestamp(), 20 | modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), 21 | deleted_at timestamptz DEFAULT NULL, 22 | version bigint NOT NULL DEFAULT 0, 23 | 24 | name text NOT NULL, 25 | content text NOT NULL, 26 | metadata bytea NOT NULL, 27 | collection uuid REFERENCES icons_collections(id) 28 | ON DELETE SET NULL 29 | DEFAULT NULL 30 | ); 31 | 32 | -- Indexes 33 | 34 | CREATE INDEX icon_colections_user_idx 35 | ON icons_collections ("user"); 36 | 37 | CREATE INDEX icons_user_idx 38 | ON icons ("user"); 39 | 40 | CREATE INDEX icons_collection_idx 41 | ON icons (collection); 42 | 43 | -- Triggers 44 | 45 | CREATE TRIGGER icons_collections_occ_tgr BEFORE UPDATE ON icons_collections 46 | FOR EACH ROW EXECUTE PROCEDURE handle_occ(); 47 | 48 | CREATE TRIGGER icons_collections_modified_at_tgr BEFORE UPDATE ON icons_collections 49 | FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); 50 | 51 | CREATE TRIGGER icons_occ_tgr BEFORE UPDATE ON icons 52 | FOR EACH ROW EXECUTE PROCEDURE handle_occ(); 53 | 54 | CREATE TRIGGER icons_modified_at_tgr BEFORE UPDATE ON icons 55 | FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); 56 | 57 | -------------------------------------------------------------------------------- /resources/migrations/XXXX.workers.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE task_status 2 | AS ENUM ('pending', 'canceled', 'completed', 'failed'); 3 | 4 | CREATE TABLE task ( 5 | id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 6 | created_at timestamptz NOT NULL DEFAULT clock_timestamp(), 7 | completed_at timestamptz DEFAULT NULL, 8 | queue text NOT NULL DEFAULT '', 9 | status task_status NOT NULL DEFAULT 'pending', 10 | error text NOT NULL DEFAULT '' 11 | ) WITH (OIDS=FALSE); 12 | -------------------------------------------------------------------------------- /resources/public/static/images/email/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uxbox/uxbox-backend/036c42db8424be3ac34c38be80577ee279141681/resources/public/static/images/email/facebook.png -------------------------------------------------------------------------------- /resources/public/static/images/email/img-header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uxbox/uxbox-backend/036c42db8424be3ac34c38be80577ee279141681/resources/public/static/images/email/img-header.jpg -------------------------------------------------------------------------------- /resources/public/static/images/email/linkedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uxbox/uxbox-backend/036c42db8424be3ac34c38be80577ee279141681/resources/public/static/images/email/linkedin.png -------------------------------------------------------------------------------- /resources/public/static/images/email/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uxbox/uxbox-backend/036c42db8424be3ac34c38be80577ee279141681/resources/public/static/images/email/logo.png -------------------------------------------------------------------------------- /resources/public/static/images/email/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uxbox/uxbox-backend/036c42db8424be3ac34c38be80577ee279141681/resources/public/static/images/email/twitter.png -------------------------------------------------------------------------------- /resources/sql/cli.sql: -------------------------------------------------------------------------------- 1 | -- :name get-image-collection :? :1 2 | select * 3 | from images_collections as cc 4 | where cc.id = :id 5 | and cc."user" = '00000000-0000-0000-0000-000000000000'::uuid; 6 | 7 | -- :name create-image : (locked_tasks.created_at, locked_tasks.id) 23 | order by created_at, id 24 | limit 1 25 | ) as j 26 | from locked_tasks 27 | where locked_tasks.id is not null 28 | limit 1 29 | ) as t1 30 | ) 31 | ) 32 | select id, status, error, created_at 33 | from locked_tasks 34 | where locked 35 | limit 1; 36 | 37 | -- :name create-task :? :1 38 | insert into tasks (queue) 39 | values (:queue) 40 | returning *; 41 | 42 | -- :name mark-task-done 43 | update tasks 44 | set status = 'completed', 45 | completed_at = clock_timestamp() 46 | where id = :id; 47 | 48 | -- :name mark-task-failed 49 | update tasks 50 | set status = 'failed', 51 | error = :error, 52 | completed_at = clock_timestamp() 53 | where id = :id; 54 | -------------------------------------------------------------------------------- /scripts/fixtures.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | lein run -m uxbox.fixtures/init 3 | -------------------------------------------------------------------------------- /scripts/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | export PATH=/opt/img/bin:$PATH 3 | lein trampoline run -m uxbox.main 4 | -------------------------------------------------------------------------------- /scripts/smtpd.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | python -m smtpd -n -c DebuggingServer localhost:25 3 | -------------------------------------------------------------------------------- /src/data_readers.clj: -------------------------------------------------------------------------------- 1 | {instant uxbox.util.time/from-string} 2 | -------------------------------------------------------------------------------- /src/uxbox/cli/collimp.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.cli.collimp 8 | "Collection importer command line helper." 9 | (:require [clojure.spec :as s] 10 | [clojure.pprint :refer [pprint]] 11 | [clojure.java.io :as io] 12 | [mount.core :as mount] 13 | [cuerdas.core :as str] 14 | [suricatta.core :as sc] 15 | [storages.core :as st] 16 | [storages.util :as fs] 17 | [uxbox.config] 18 | [uxbox.db :as db] 19 | [uxbox.migrations] 20 | [uxbox.media :as media] 21 | [uxbox.cli.sql :as sql] 22 | [uxbox.util.spec :as us] 23 | [uxbox.util.cli :as cli] 24 | [uxbox.util.uuid :as uuid] 25 | [uxbox.util.data :as data]) 26 | (:import [java.io Reader PushbackReader] 27 | [javax.imageio ImageIO])) 28 | 29 | ;; --- Constants & Specs 30 | 31 | (def ^:const +imates-uuid-ns+ #uuid "3642a582-565f-4070-beba-af797ab27a6e") 32 | 33 | (s/def ::name string?) 34 | (s/def ::type keyword?) 35 | (s/def ::path string?) 36 | (s/def ::regex us/regex?) 37 | 38 | (s/def ::import-entry 39 | (s/keys :req-un [::name ::type ::path ::regex])) 40 | 41 | ;; --- CLI Helpers 42 | 43 | 44 | (defn printerr 45 | [& args] 46 | (binding [*out* *err*] 47 | (apply println args))) 48 | 49 | (defn pushback-reader 50 | [reader] 51 | (PushbackReader. ^Reader reader)) 52 | 53 | ;; --- Colors Collections Importer 54 | 55 | (def storage media/images-storage) 56 | 57 | (defn- create-image-collection 58 | "Create or replace image collection by its name." 59 | [conn {:keys [name] :as entry}] 60 | (let [id (uuid/namespaced +imates-uuid-ns+ name) 61 | sqlv (sql/create-image-collection {:id id :name name})] 62 | (sc/execute conn sqlv) 63 | id)) 64 | 65 | (defn- retrieve-image-size 66 | [path] 67 | (let [path (fs/path path) 68 | file (.toFile path) 69 | buff (ImageIO/read file)] 70 | [(.getWidth buff) 71 | (.getHeight buff)])) 72 | 73 | (defn- retrieve-image 74 | [conn id] 75 | {:pre [(uuid? id)]} 76 | (let [sqlv (sql/get-image {:id id})] 77 | (some->> (sc/fetch-one conn sqlv) 78 | (data/normalize-attrs)))) 79 | 80 | (defn- delete-image 81 | [conn {:keys [id path] :as image}] 82 | {:pre [(uuid? id) 83 | (fs/path? path)]} 84 | (let [sqlv (sql/delete-image {:id id})] 85 | @(st/delete storage path) 86 | (sc/execute conn sqlv))) 87 | 88 | (defn- create-image 89 | [conn collid imageid localpath] 90 | {:pre [(fs/path? localpath) 91 | (uuid? collid) 92 | (uuid? imageid)]} 93 | (let [filename (fs/base-name localpath) 94 | [width height] (retrieve-image-size localpath) 95 | extension (second (fs/split-ext filename)) 96 | path @(st/save storage filename localpath) 97 | params {:name filename 98 | :path (str path) 99 | :mimetype (case extension 100 | ".jpg" "image/jpeg" 101 | ".png" "image/png") 102 | :width width 103 | :height height 104 | :collection collid 105 | :id imageid} 106 | sqlv (sql/create-image params)] 107 | (sc/execute conn sqlv))) 108 | 109 | (defn- import-image 110 | [conn id fpath] 111 | {:pre [(uuid? id) (fs/path? fpath)]} 112 | (let [imageid (uuid/namespaced +imates-uuid-ns+ (str id fpath))] 113 | (if-let [image (retrieve-image conn imageid)] 114 | (do 115 | (delete-image conn image) 116 | (create-image conn id imageid fpath)) 117 | (create-image conn id imageid fpath)))) 118 | 119 | (defn- process-images-entry 120 | [conn {:keys [path regex] :as entry}] 121 | {:pre [(s/valid? ::import-entry entry)]} 122 | (let [id (create-image-collection conn entry)] 123 | (doseq [fpath (fs/list-files path)] 124 | (when (re-matches regex (str fpath)) 125 | (import-image conn id fpath))))) 126 | 127 | ;; --- Entry Point 128 | 129 | (defn- check-path! 130 | [path] 131 | (when-not path 132 | (cli/print-err! "No path is provided.") 133 | (cli/exit! -1)) 134 | (when-not (fs/exists? path) 135 | (cli/print-err! "Path does not exists.") 136 | (cli/exit! -1)) 137 | (when (fs/directory? path) 138 | (cli/print-err! "The provided path is a directory.") 139 | (cli/exit! -1)) 140 | (fs/path path)) 141 | 142 | (defn- read-import-file 143 | [path] 144 | (let [path (check-path! path) 145 | parent (fs/parent path) 146 | reader (pushback-reader (io/reader path))] 147 | [parent (read reader)])) 148 | 149 | (defn- start-system 150 | [] 151 | (-> (mount/only #{#'uxbox.config/config 152 | #'uxbox.db/datasource 153 | #'uxbox.migrations/migrations}) 154 | (mount/start))) 155 | 156 | (defn- stop-system 157 | [] 158 | (mount/stop)) 159 | 160 | (defn- run-importer 161 | [directory data] 162 | (println "Running importer on:") 163 | (pprint data)) 164 | 165 | (defn main 166 | [& [path]] 167 | (let [[directory data] (read-import-file path)] 168 | (start-system) 169 | (try 170 | (run-importer directory data) 171 | (finally 172 | (stop-system))))) 173 | -------------------------------------------------------------------------------- /src/uxbox/cli/sql.clj: -------------------------------------------------------------------------------- 1 | (ns uxbox.cli.sql 2 | (:require [hugsql.core :as hugsql])) 3 | 4 | (hugsql/def-sqlvec-fns "sql/cli.sql" {:quoting :ansi :fn-suffix ""}) 5 | -------------------------------------------------------------------------------- /src/uxbox/config.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.config 8 | "A configuration management." 9 | (:require [mount.core :refer [defstate]] 10 | [environ.core :refer (env)] 11 | [buddy.core.hash :as hash] 12 | [clojure.java.io :as io] 13 | [clojure.edn :as edn] 14 | [uxbox.util.exceptions :as ex] 15 | [uxbox.util.data :refer [deep-merge]])) 16 | 17 | ;; --- Configuration Loading & Parsing 18 | 19 | (def ^:dynamic *default-config-path* "config/default.edn") 20 | (def ^:dynamic *local-config-path* "config/local.edn") 21 | 22 | (defn read-config 23 | [] 24 | (let [builtin (io/resource *default-config-path*) 25 | local (io/resource *local-config-path*) 26 | external (io/file (:uxbox-config env))] 27 | (deep-merge (edn/read-string (slurp builtin)) 28 | (when local (edn/read-string (slurp local))) 29 | (when (and external (.exists external)) 30 | (edn/read-string (slurp external)))))) 31 | 32 | (defn read-test-config 33 | [] 34 | (binding [*local-config-path* "config/test.edn"] 35 | (read-config))) 36 | 37 | (defstate config 38 | :start (read-config)) 39 | 40 | ;; --- Secret Loading & Parsing 41 | 42 | (defn- initialize-secret 43 | [config] 44 | (let [secret (:secret config)] 45 | (when-not secret 46 | (ex/raise :code ::missing-secret-key 47 | :message "Missing `:secret` key in config.")) 48 | (hash/blake2b-256 secret))) 49 | 50 | (defstate secret 51 | :start (initialize-secret config)) 52 | 53 | -------------------------------------------------------------------------------- /src/uxbox/db.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.db 8 | "Database access layer for UXBOX." 9 | (:require [mount.core :as mount :refer (defstate)] 10 | [promesa.core :as p] 11 | [hikari-cp.core :as hikari] 12 | [executors.core :as exec] 13 | [suricatta.core :as sc] 14 | [suricatta.proto :as scp] 15 | [suricatta.types :as sct] 16 | [suricatta.transaction :as sctx] 17 | [uxbox.config :as cfg]) 18 | (:import org.jooq.TransactionContext 19 | org.jooq.TransactionProvider 20 | org.jooq.Configuration)) 21 | 22 | ;; --- State 23 | 24 | (def ^:const +defaults+ 25 | {:connection-timeout 30000 26 | :idle-timeout 600000 27 | :max-lifetime 1800000 28 | :minimum-idle 10 29 | :maximum-pool-size 10 30 | :adapter "postgresql" 31 | :username "" 32 | :password "" 33 | :database-name "" 34 | :server-name "localhost" 35 | :port-number 5432}) 36 | 37 | (defn create-datasource 38 | [config] 39 | (let [dbconf (merge +defaults+ config)] 40 | (hikari/make-datasource dbconf))) 41 | 42 | (defstate datasource 43 | :start (create-datasource (:database cfg/config)) 44 | :stop (hikari/close-datasource datasource)) 45 | 46 | ;; --- Suricatta Async Adapter 47 | 48 | (defn transaction 49 | "Asynchronous transaction handling." 50 | {:internal true} 51 | [ctx func] 52 | (let [^Configuration conf (.derive (scp/-config ctx)) 53 | ^TransactionContext txctx (sctx/transaction-context conf) 54 | ^TransactionProvider provider (.transactionProvider conf)] 55 | (doto conf 56 | (.data "suricatta.rollback" false) 57 | (.data "suricatta.transaction" true)) 58 | (try 59 | (.begin provider txctx) 60 | (->> (func (sct/context conf)) 61 | (p/map (fn [result] 62 | (if (.data conf "suricatta.rollback") 63 | (.rollback provider txctx) 64 | (.commit provider txctx)) 65 | result)) 66 | (p/error (fn [error] 67 | (.rollback provider (.cause txctx error)) 68 | (p/rejected error)))) 69 | (catch Exception cause 70 | (.rollback provider (.cause txctx cause)) 71 | (p/rejected cause))))) 72 | 73 | ;; --- Public Api 74 | 75 | (defmacro atomic 76 | [ctx & body] 77 | `(transaction ~ctx (fn [~ctx] ~@body))) 78 | 79 | (defn connection 80 | [] 81 | (sc/context datasource)) 82 | 83 | (defn fetch 84 | [& args] 85 | (exec/submit #(apply sc/fetch args))) 86 | 87 | (defn execute 88 | [& args] 89 | (exec/submit #(apply sc/execute args))) 90 | -------------------------------------------------------------------------------- /src/uxbox/emails.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.emails 8 | "Main api for send emails." 9 | (:require [uxbox.emails.core :as core])) 10 | 11 | (def send! core/send!) 12 | (def render core/render) 13 | 14 | (load "emails/users") 15 | -------------------------------------------------------------------------------- /src/uxbox/emails/core.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.emails.core 8 | (:require [hiccup.core :refer (html)] 9 | [hiccup.page :refer (html4)] 10 | [suricatta.core :as sc] 11 | [uxbox.db :as db] 12 | [uxbox.config :as cfg] 13 | [uxbox.sql :as sql] 14 | [uxbox.emails.layouts :as layouts] 15 | [uxbox.util.blob :as blob] 16 | [uxbox.util.transit :as t])) 17 | 18 | (def emails 19 | "A global state for registring emails." 20 | (atom {})) 21 | 22 | (defmacro defemail 23 | [type & args] 24 | (let [email (apply hash-map args)] 25 | `(do 26 | (swap! emails assoc ~type ~email) 27 | nil))) 28 | 29 | (defn- render-subject 30 | [{:keys [subject]} context] 31 | (cond 32 | (delay? subject) (deref subject) 33 | (ifn? subject) (subject context) 34 | (string? subject) subject 35 | :else (throw (ex-info "Invalid subject." {})))) 36 | 37 | (defn- render-body 38 | [[type bodyfn] layout context] 39 | (let [layoutfn (get layout type)] 40 | {:content (cond-> (bodyfn context) 41 | layoutfn (layoutfn context) 42 | (= type :text/html) (html4)) 43 | ::type type 44 | :type (subs (str type) 1)})) 45 | 46 | (defn- render-body-alternatives 47 | [{:keys [layout body] :as email} context] 48 | (reduce #(conj %1 (render-body %2 layout context)) [:alternatives] body)) 49 | 50 | (defn render-email 51 | [email context] 52 | (let [config (:email cfg/config) 53 | from (or (:email/from context) 54 | (:from config)) 55 | reply-to (or (:email/reply-to context) 56 | (:reply-to config) 57 | from)] 58 | {:subject (render-subject email context) 59 | :body (render-body-alternatives email context) 60 | :to (:email/to context) 61 | :from from 62 | :reply-to reply-to})) 63 | 64 | (def valid-priority? #{:high :low}) 65 | (def valid-email-identifier? #(contains? @emails %)) 66 | 67 | (defn render 68 | "Render a email as data structure." 69 | [{name :email/name :as context}] 70 | {:pre [(valid-email-identifier? name)]} 71 | (let [email (get @emails name)] 72 | (render-email email context))) 73 | 74 | (defn send! 75 | "Schedule the email for sending." 76 | [{name :email/name 77 | priority :email/priority 78 | :or {priority :high} 79 | :as context}] 80 | {:pre [(valid-priority? priority) 81 | (valid-email-identifier? name)]} 82 | (let [email (get @emails name) 83 | email (render-email email context) 84 | data (-> email t/encode blob/encode) 85 | priority (case priority :low 1 :high 10) 86 | sqlv (sql/insert-email {:data data :priority priority})] 87 | (with-open [conn (db/connection)] 88 | (sc/atomic conn 89 | (sc/execute conn sqlv))))) 90 | -------------------------------------------------------------------------------- /src/uxbox/emails/layouts.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.emails.layouts 8 | (:require [uxbox.media :as md])) 9 | 10 | (def default-embedded-styles 11 | "/* GLOBAL */ 12 | * { 13 | margin:0; 14 | padding:0; 15 | font-family: Arial, sans-serif; 16 | font-size: 100%; 17 | line-height: 1.6; 18 | } 19 | 20 | img { 21 | max-width: 100%; 22 | width: 100%; 23 | } 24 | 25 | .img-header { 26 | border-top-left-radius: 5px; 27 | border-top-right-radius: 5px; 28 | } 29 | 30 | body { 31 | -webkit-font-smoothing:antialiased; 32 | -webkit-text-size-adjust:none; 33 | width: 100%!important; 34 | height: 100%; 35 | } 36 | 37 | /* ELEMENTS */ 38 | a { 39 | color: #78dbbe; 40 | text-decoration:none; 41 | font-weight: bold; 42 | } 43 | 44 | .btn-primary { 45 | text-decoration:none; 46 | color: #fff; 47 | background-color: #78dbbe; 48 | padding: 10px 30px; 49 | font-weight: bold; 50 | margin: 20px 0; 51 | text-align: center; 52 | cursor: pointer; 53 | display: inline-block; 54 | border-radius: 4px; 55 | } 56 | 57 | .btn-primary:hover { 58 | color: #FFF; 59 | background-color: #8eefcf; 60 | } 61 | 62 | .last { 63 | margin-bottom: 0; 64 | } 65 | 66 | .first{ 67 | margin-top: 0; 68 | } 69 | 70 | .logo { 71 | background-color: #f6f6f6; 72 | padding: 10px; 73 | text-align: center; 74 | padding-bottom: 25px; 75 | } 76 | .logo h2 { 77 | color: #777; 78 | font-size: 20px; 79 | font-weight: bold; 80 | margin-top: 15px; 81 | } 82 | .logo img { 83 | max-width: 150px; 84 | } 85 | 86 | /* BODY */ 87 | table.body-wrap { 88 | width: 100%; 89 | padding: 20px; 90 | } 91 | 92 | table.body-wrap .container{ 93 | border-radius: 5px; 94 | color: #ababab; 95 | } 96 | 97 | 98 | /* FOOTER */ 99 | table.footer-wrap { 100 | width: 100%; 101 | clear:both!important; 102 | } 103 | 104 | .footer-wrap .container p { 105 | font-size: 12px; 106 | color:#666; 107 | 108 | } 109 | 110 | table.footer-wrap a{ 111 | color: #999; 112 | } 113 | 114 | 115 | /* TYPOGRAPHY */ 116 | h1,h2,h3{ 117 | font-family: Arial, sans-serif; 118 | line-height: 1.1; 119 | margin-bottom:15px; 120 | color:#000; 121 | margin: 40px 0 10px; 122 | line-height: 1.2; 123 | font-weight:200; 124 | } 125 | 126 | h1 { 127 | color: #777; 128 | font-size: 28px; 129 | font-weight: bold; 130 | } 131 | h2 { 132 | font-size: 24px; 133 | } 134 | h3 { 135 | font-size: 18px; 136 | } 137 | 138 | p, ul { 139 | margin-bottom: 10px; 140 | font-weight: normal; 141 | } 142 | 143 | ul li { 144 | margin-left:5px; 145 | list-style-position: inside; 146 | } 147 | 148 | /* RESPONSIVE */ 149 | 150 | /* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */ 151 | .container { 152 | display: block !important; 153 | max-width: 620px !important; 154 | margin: 0 auto !important; /* makes it centered */ 155 | clear: both !important; 156 | } 157 | 158 | /* This should also be a block element, so that it will fill 100% of the .container */ 159 | .content { 160 | padding: 20px; 161 | max-width: 620px; 162 | margin: 0 auto; 163 | display: block; 164 | } 165 | 166 | /* Let's make sure tables in the content area are 100% wide */ 167 | .content table { 168 | width: 100%; 169 | }") 170 | 171 | (defn- default-html 172 | [body context] 173 | [:html 174 | [:head 175 | [:meta {:http-equiv "Content-Type" 176 | :content "text/html; charset=UTF-8"}] 177 | [:meta {:name "viewport" 178 | :content "width=device-width"}] 179 | [:title "title"] 180 | [:style default-embedded-styles]] 181 | [:body {:bgcolor "#f6f6f6" 182 | :cz-shortcut-listen "true"} 183 | [:table.body-wrap 184 | [:tbody 185 | [:tr 186 | [:td] 187 | [:td.container {:bgcolor "#FFFFFF"} 188 | [:div.logo 189 | [:img {:src (md/resolve-asset "images/email/logo.png") 190 | :alt "UXBOX"}]] 191 | body] 192 | [:td]]]] 193 | [:table.footer-wrap 194 | [:tbody 195 | [:tr 196 | [:td] 197 | [:td.container 198 | [:div.content 199 | [:table 200 | [:tbody 201 | [:tr 202 | [:td 203 | [:div {:style "text-align: center;"} 204 | [:a {:href "#" :target "_blank"} 205 | [:img {:style "display: inline-block; width: 25px; margin-right: 5px;" 206 | :src (md/resolve-asset "images/email/twitter.png")}]] 207 | [:a {:href "#" :target "_blank"} 208 | [:img {:style "display: inline-block; width: 25px; margin-right: 5px;" 209 | :src (md/resolve-asset "images/email/facebook.png")}]] 210 | [:a {:href "#" :target "_blank"} 211 | [:img {:style "display: inline-block; width: 25px; margin-right: 5px;" 212 | :src (md/resolve-asset "images/email/linkedin.png")}]]]]] 213 | [:tr 214 | [:td {:align "center"} 215 | [:p 216 | [:span "Sent from UXBOX | "] 217 | [:a {:href "#" :target "_blank"} 218 | [:unsubscribe "Email preferences"]]]]]]]]] 219 | [:td]]]]]]) 220 | 221 | (defn default-text 222 | [body context] 223 | body) 224 | 225 | (def default 226 | "Default layout instance." 227 | {:text/html default-html 228 | :text/plain default-text}) 229 | -------------------------------------------------------------------------------- /src/uxbox/emails/users.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.emails.users 8 | (:require [uxbox.media :as md] 9 | [uxbox.emails.core :refer (defemail)] 10 | [uxbox.emails.layouts :as layouts])) 11 | 12 | ;; --- User Register 13 | 14 | (defn- register-body-html 15 | [{:keys [name] :as ctx}] 16 | [:div 17 | [:img.img-header {:src (md/resolve-asset "images/email/img-header.jpg") 18 | :alt "UXBOX"}] 19 | [:div.content 20 | [:table 21 | [:tbody 22 | [:tr 23 | [:td 24 | [:h1 "Hi " name] 25 | [:p "Welcome to uxbox."] 26 | [:p 27 | [:a.btn-primary {:href "#"} "Sign in"]] 28 | [:p "Sincerely," [:br] [:strong "The UXBOX Team"]] 29 | #_[:p "P.S. Having trouble signing up? please contact " 30 | [:a {:href "#"} "Support email"]]]]]]]]) 31 | 32 | (defn- register-body-text 33 | [{:keys [name] :as ctx}] 34 | (str "Hi " name "\n\n" 35 | "Welcome to uxbox!\n\n" 36 | "Sincerely, the UXBOX team.\n")) 37 | 38 | (defemail :users/register 39 | :layout layouts/default 40 | :subject "UXBOX: Welcome!" 41 | :body {:text/html register-body-html 42 | :text/plain register-body-text}) 43 | 44 | ;; --- Password Recovery 45 | 46 | (defn- password-recovery-body-html 47 | [{:keys [name token] :as ctx}] 48 | [:div 49 | [:img.img-header {:src (md/resolve-asset "images/img-header.jpg") 50 | :alt "UXBOX"}] 51 | [:div.content 52 | [:table 53 | [:tbody 54 | [:tr 55 | [:td 56 | [:h1 "Hi " name] 57 | [:p "A password recovery is requested."] 58 | [:p 59 | "Please, follow the following url in order to" 60 | "change your password." 61 | [:a {:href "#"} "http://uxbox.io/..."]] 62 | [:p "Sincerely," [:br] [:strong "The UXBOX Team"]]]]]]]]) 63 | 64 | (defn- password-recovery-body-text 65 | [{:keys [name token] :as ctx}] 66 | (str "Hi " name "\n\n" 67 | "A password recovery is requested.\n\n" 68 | "Please follow the following url in order to change the password:\n\n" 69 | " http://uxbox.io/recovery/" token "\n\n\n" 70 | "Sincerely, the UXBOX team.\n")) 71 | 72 | (defemail :users/password-recovery 73 | :layout layouts/default 74 | :subject "Password recovery requested." 75 | :body {:text/html password-recovery-body-html 76 | :text/plain password-recovery-body-text}) 77 | 78 | -------------------------------------------------------------------------------- /src/uxbox/fixtures.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.fixtures 8 | "A initial fixtures." 9 | (:require [buddy.hashers :as hashers] 10 | [buddy.core.codecs :as codecs] 11 | [catacumba.serializers :as sz] 12 | [mount.core :as mount] 13 | [clj-uuid :as uuid] 14 | [suricatta.core :as sc] 15 | [uxbox.config :as cfg] 16 | [uxbox.db :as db] 17 | [uxbox.migrations] 18 | [uxbox.util.transit :as t] 19 | [uxbox.services.users :as susers] 20 | [uxbox.services.projects :as sproj] 21 | [uxbox.services.pages :as spag])) 22 | 23 | (defn- mk-uuid 24 | [prefix i] 25 | (uuid/v5 uuid/+namespace-oid+ (str prefix i))) 26 | 27 | (defn- data-encode 28 | [data] 29 | (-> (t/encode data) 30 | (codecs/bytes->str))) 31 | 32 | (defn- create-user 33 | [conn i] 34 | (println "create user" i) 35 | (susers/create-user conn 36 | {:username (str "user" i) 37 | :id (mk-uuid "user" i) 38 | :fullname (str "User " i) 39 | :metadata (data-encode {}) 40 | :password "123123" 41 | :email (str "user" i ".test@uxbox.io")})) 42 | 43 | (defn- create-project 44 | [conn i ui] 45 | ;; (Thread/sleep 20) 46 | (println "create project" i "for user" ui) 47 | (sproj/create-project conn 48 | {:id (mk-uuid "project" i) 49 | :user (mk-uuid "user" ui) 50 | :name (str "project " i)})) 51 | 52 | (defn- create-page 53 | [conn i pi ui] 54 | ;; (Thread/sleep 1) 55 | (println "create page" i "for user" ui "for project" pi) 56 | (spag/create-page conn 57 | {:id (mk-uuid "page" i) 58 | :user (mk-uuid "user" ui) 59 | :project (mk-uuid "project" pi) 60 | :data nil 61 | :metadata {:width 1024 62 | :height 768 63 | :layout "tablet"} 64 | :name (str "page " i)})) 65 | 66 | (def num-users 50) 67 | (def num-projects 5) 68 | (def num-pages 5) 69 | 70 | (defn init 71 | [] 72 | (mount/start) 73 | (with-open [conn (db/connection)] 74 | (sc/atomic conn 75 | (doseq [i (range num-users)] 76 | (create-user conn i)) 77 | 78 | (doseq [ui (range num-users)] 79 | (doseq [i (range num-projects)] 80 | (create-project conn (str ui i) ui))) 81 | 82 | (doseq [pi (range num-projects)] 83 | (doseq [ui (range num-users)] 84 | (doseq [i (range num-pages)] 85 | (create-page conn (str pi ui i) (str ui pi) ui)))))) 86 | (mount/stop)) 87 | -------------------------------------------------------------------------------- /src/uxbox/frontend.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.frontend 8 | (:require [mount.core :refer [defstate]] 9 | [catacumba.core :as ct] 10 | [catacumba.http :as http] 11 | [catacumba.serializers :as sz] 12 | [catacumba.handlers.auth :as cauth] 13 | [catacumba.handlers.parse :as cparse] 14 | [catacumba.handlers.misc :as cmisc] 15 | [uxbox.config :as cfg] 16 | [uxbox.frontend.auth :as auth] 17 | [uxbox.frontend.users :as users] 18 | [uxbox.frontend.errors :as errors] 19 | [uxbox.frontend.projects :as projects] 20 | [uxbox.frontend.pages :as pages] 21 | [uxbox.frontend.images :as images] 22 | [uxbox.frontend.icons :as icons] 23 | [uxbox.frontend.kvstore :as kvstore] 24 | [uxbox.frontend.svgparse :as svgparse] 25 | [uxbox.frontend.debug-emails :as dbgemails] 26 | [uxbox.util.response :refer [rsp]] 27 | [uxbox.util.uuid :as uuid])) 28 | 29 | ;; --- Top Level Handlers 30 | 31 | (defn- welcome-api 32 | "A GET entry point for the api that shows 33 | a welcome message." 34 | [context] 35 | (let [body {:message "Welcome to UXBox api."}] 36 | (-> (sz/encode body :json) 37 | (http/ok {:content-type "application/json"})))) 38 | 39 | (defn- debug-only 40 | [context] 41 | (if (-> cfg/config :server :debug) 42 | (ct/delegate) 43 | (http/not-found ""))) 44 | 45 | ;; --- Config 46 | 47 | (def cors-conf 48 | {:origin "*" 49 | :max-age 3600 50 | :allow-methods #{:post :put :get :delete :trace} 51 | :allow-headers #{:x-requested-with :content-type :authorization}}) 52 | 53 | ;; --- Routes 54 | 55 | (defn routes 56 | ([] (routes cfg/config)) 57 | ([config] 58 | (let [auth-opts {:secret cfg/secret 59 | :options (:auth-options cfg/config)}] 60 | (ct/routes 61 | [[:any (cauth/auth (cauth/jwe-backend auth-opts))] 62 | [:any (cmisc/autoreloader)] 63 | 64 | [:get "api" #'welcome-api] 65 | [:assets "media" {:dir "public/media"}] 66 | [:assets "static" {:dir "public/static"}] 67 | 68 | [:prefix "debug" 69 | [:any debug-only] 70 | [:get "emails" #'dbgemails/list-emails] 71 | [:get "emails/email" #'dbgemails/show-email]] 72 | 73 | [:prefix "api" 74 | [:any (cmisc/cors cors-conf)] 75 | [:any (cparse/body-params)] 76 | [:error #'errors/handler] 77 | 78 | [:post "auth/token" #'auth/login] 79 | [:post "auth/register" #'users/register-user] 80 | [:get "auth/recovery/:token" #'users/validate-recovery-token] 81 | [:post "auth/recovery" #'users/request-recovery] 82 | [:put "auth/recovery" #'users/recover-password] 83 | 84 | [:get "projects-by-token/:token" #'projects/retrieve-project-by-share-token] 85 | 86 | ;; SVG Parse 87 | [:post "svg/parse" #'svgparse/parse] 88 | 89 | [:any #'auth/authorization] 90 | 91 | ;; KVStore 92 | [:put "kvstore" #'kvstore/update] 93 | [:get "kvstore/:key" #'kvstore/retrieve] 94 | [:delete "kvstore/:key" #'kvstore/delete] 95 | 96 | ;; Projects 97 | [:get "projects/:id/pages" #'pages/list-pages-by-project] 98 | [:put "projects/:id" #'projects/update-project] 99 | [:delete "projects/:id" #'projects/delete-project] 100 | [:post "projects" #'projects/create-project] 101 | [:get "projects" #'projects/list-projects] 102 | 103 | ;; Image Collections 104 | [:put "library/image-collections/:id" #'images/update-collection] 105 | [:delete "library/image-collections/:id" #'images/delete-collection] 106 | [:get "library/image-collections" #'images/list-collections] 107 | [:post "library/image-collections" #'images/create-collection] 108 | [:get "library/image-collections/:id/images" #'images/list-images] 109 | [:get "library/image-collections/images" #'images/list-images] 110 | 111 | ;; Images 112 | [:put "library/images/copy" #'images/copy-image] 113 | [:delete "library/images/:id" #'images/delete-image] 114 | [:get "library/images/:id" #'images/retrieve-image] 115 | [:put "library/images/:id" #'images/update-image] 116 | [:post "library/images" #'images/create-image] 117 | 118 | ;; Icon Collections 119 | [:put "library/icon-collections/:id" #'icons/update-collection] 120 | [:delete "library/icon-collections/:id" #'icons/delete-collection] 121 | [:get "library/icon-collections" #'icons/list-collections] 122 | [:post "library/icon-collections" #'icons/create-collection] 123 | [:get "library/icon-collections/:id/icons" #'icons/list-icons] 124 | [:get "library/icon-collections/icons" #'icons/list-icons] 125 | 126 | ;; Icons 127 | [:put "library/icons/copy" #'icons/copy-icon] 128 | [:delete "library/icons/:id" #'icons/delete-icon] 129 | [:put "library/icons/:id" #'icons/update-icon] 130 | [:post "library/icons" #'icons/create-icon] 131 | 132 | ;; Pages 133 | [:put "pages/:id/metadata" #'pages/update-page-metadata] 134 | [:get "pages/:id/history" #'pages/retrieve-page-history] 135 | [:put "pages/:id/history/:hid" #'pages/update-page-history] 136 | [:put "pages/:id" #'pages/update-page] 137 | [:delete "pages/:id" #'pages/delete-page] 138 | [:post "pages" #'pages/create-page] 139 | 140 | ;; Profile 141 | [:get "profile/me" #'users/retrieve-profile] 142 | [:put "profile/me" #'users/update-profile] 143 | [:put "profile/me/password" #'users/update-password] 144 | [:post "profile/me/photo" #'users/update-photo]]])))) 145 | 146 | ;; --- State Initialization 147 | 148 | (defn- start-server 149 | [config] 150 | (let [config (:http config)] 151 | (ct/run-server (routes config) config))) 152 | 153 | (defstate server 154 | :start (start-server cfg/config) 155 | :stop (.stop server)) 156 | -------------------------------------------------------------------------------- /src/uxbox/frontend/auth.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.frontend.auth 8 | (:require [clojure.spec :as s] 9 | [catacumba.core :as ct] 10 | [catacumba.http :as http] 11 | [promesa.core :as p] 12 | [uxbox.util.spec :as us] 13 | [uxbox.services :as sv] 14 | [uxbox.util.uuid :as uuid] 15 | [uxbox.util.response :refer (rsp)])) 16 | 17 | (s/def ::scope string?) 18 | (s/def ::login (s/keys :req-un [::us/username ::us/password ::scope])) 19 | 20 | (defn login 21 | [{data :data}] 22 | (let [data (us/conform ::login data) 23 | message (assoc data :type :login)] 24 | (->> (sv/novelty message) 25 | (p/map #(http/ok (rsp %)))))) 26 | 27 | ;; TODO: improve authorization 28 | 29 | (defn authorization 30 | [{:keys [identity] :as context}] 31 | (if identity 32 | (ct/delegate {:identity (uuid/from-string (:id identity))}) 33 | (http/forbidden (rsp nil)))) 34 | -------------------------------------------------------------------------------- /src/uxbox/frontend/debug_emails.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.frontend.debug-emails 8 | "A helper namespace for just render emails." 9 | (:require [clojure.edn :as edn] 10 | [catacumba.http :as http] 11 | [hiccup.page :refer (html5)] 12 | [uxbox.emails :as emails] 13 | [uxbox.emails.core :as emails-core])) 14 | 15 | (def +available-emails+ 16 | {:users/register 17 | {:name "Cirilla"} 18 | :users/password-recovery 19 | {:name "Cirilla" 20 | :token "agNFhA6SolcFb4Us2NOTNWh0cfFDquVLAav400xQPjw"}}) 21 | 22 | (defn- render-emails-list 23 | [] 24 | (html5 25 | [:section {:style "font-family: Monoid, monospace; font-size: 14px;"} 26 | [:h1 "Available emails"] 27 | [:table {:style "width: 500px;"} 28 | [:tbody 29 | [:tr 30 | (for [[type email] @emails-core/emails] 31 | [:tr 32 | [:td (pr-str type)] 33 | [:td 34 | [:a {:href (str "/debug/emails/email?id=" 35 | (pr-str type) 36 | "&type=:text/html")} 37 | "(html)"]] 38 | [:td 39 | [:a {:href (str "/debug/emails/email?id=" 40 | (pr-str type) 41 | "&type=:text/plain")} 42 | "(text)"]]])]]]])) 43 | 44 | (defn list-emails 45 | [context] 46 | (http/ok (render-emails-list) 47 | {:content-type "text/html; charset=utf-8"})) 48 | 49 | (defn- render-email 50 | [type content] 51 | (if (= type :text/html) 52 | content 53 | (html5 54 | [:pre content]))) 55 | 56 | (defn show-email 57 | [{params :query-params}] 58 | (let [id (edn/read-string (:id params)) 59 | type (or (edn/read-string (:type params)) :text/html) 60 | params (-> (get +available-emails+ id) 61 | (assoc :email/name id)) 62 | email (emails/render params) 63 | content (->> (:body email) 64 | (filter #(= (:uxbox.emails.core/type %) type)) 65 | (first) 66 | (:content))] 67 | (-> (render-email type content) 68 | (http/ok {:content-type "text/html; charset=utf-8"})))) 69 | -------------------------------------------------------------------------------- /src/uxbox/frontend/errors.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.frontend.errors 8 | "A errors handling for frontend api." 9 | (:require [catacumba.core :as ct] 10 | [catacumba.http :as http] 11 | [uxbox.util.response :refer (rsp)])) 12 | 13 | (defmulti handle-exception #(:type (ex-data %))) 14 | 15 | (defmethod handle-exception :validation 16 | [err] 17 | (println "\n*********** stack trace ***********") 18 | (.printStackTrace err) 19 | (println "\n********* end stack trace *********") 20 | (let [response (ex-data err)] 21 | (http/bad-request (rsp response)))) 22 | 23 | (defmethod handle-exception :default 24 | [err] 25 | (println "\n*********** stack trace ***********") 26 | (.printStackTrace err) 27 | (println "\n********* end stack trace *********") 28 | (let [response (ex-data err)] 29 | (http/internal-server-error (rsp response)))) 30 | 31 | ;; --- Entry Point 32 | 33 | (defn- handle-data-access-exception 34 | [err] 35 | (let [err (.getCause err) 36 | state (.getSQLState err) 37 | message (.getMessage err)] 38 | (case state 39 | "P0002" 40 | (-> (rsp {:message message 41 | :payload nil 42 | :type :occ}) 43 | (http/precondition-failed)) 44 | 45 | (do 46 | (.printStackTrace err) 47 | (-> (rsp {:message message 48 | :type :unexpected 49 | :payload nil}) 50 | (http/internal-server-error)))))) 51 | 52 | (defn- handle-unexpected-exception 53 | [err] 54 | (.printStackTrace err) 55 | (let [message (.getMessage err)] 56 | (-> (rsp {:message message 57 | :type :unexpected 58 | :payload nil}) 59 | (http/internal-server-error)))) 60 | 61 | (defn handler 62 | [context err] 63 | (cond 64 | (instance? clojure.lang.ExceptionInfo err) 65 | (handle-exception err) 66 | 67 | (instance? java.util.concurrent.CompletionException err) 68 | (handler context (.getCause err)) 69 | 70 | (instance? org.jooq.exception.DataAccessException err) 71 | (handle-data-access-exception err) 72 | 73 | :else 74 | (handle-unexpected-exception err))) 75 | -------------------------------------------------------------------------------- /src/uxbox/frontend/icons.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.frontend.icons 8 | (:require [clojure.spec :as s] 9 | [promesa.core :as p] 10 | [catacumba.http :as http] 11 | [storages.core :as st] 12 | [uxbox.util.spec :as us] 13 | [uxbox.services :as sv] 14 | [uxbox.util.response :refer (rsp)] 15 | [uxbox.util.uuid :as uuid])) 16 | 17 | ;; --- Constants & Config 18 | 19 | (s/def ::collection (s/nilable ::us/uuid-string)) 20 | 21 | (s/def ::width (s/and number? pos?)) 22 | (s/def ::height (s/and number? pos?)) 23 | (s/def ::view-box (s/and (s/coll-of number?) 24 | #(= 4 (count %)) 25 | vector?)) 26 | 27 | (s/def ::mimetype string?) 28 | (s/def ::metadata 29 | (s/keys :opt-un [::width ::height ::view-box ::mimetype])) 30 | 31 | (s/def ::content string?) 32 | 33 | ;; --- Create Collection 34 | 35 | (s/def ::create-collection 36 | (s/keys :req-un [::us/name] :opt-un [::us/id])) 37 | 38 | (defn create-collection 39 | [{user :identity data :data}] 40 | (let [data (us/conform ::create-collection data) 41 | message (assoc data 42 | :type :create-icon-collection 43 | :user user)] 44 | (->> (sv/novelty message) 45 | (p/map (fn [result] 46 | (let [loc (str "/api/library/icons/" (:id result))] 47 | (http/created loc (rsp result)))))))) 48 | 49 | ;; --- Update Collection 50 | 51 | (s/def ::update-collection 52 | (s/merge ::create-collection (s/keys :req-un [::us/version]))) 53 | 54 | (defn update-collection 55 | [{user :identity params :route-params data :data}] 56 | (let [data (us/conform ::update-collection data) 57 | message (assoc data 58 | :id (uuid/from-string (:id params)) 59 | :type :update-icon-collection 60 | :user user)] 61 | (-> (sv/novelty message) 62 | (p/then #(http/ok (rsp %)))))) 63 | 64 | ;; --- Delete Collection 65 | 66 | (defn delete-collection 67 | [{user :identity params :route-params}] 68 | (let [message {:id (uuid/from-string (:id params)) 69 | :type :delete-icon-collection 70 | :user user}] 71 | (-> (sv/novelty message) 72 | (p/then (fn [v] (http/no-content)))))) 73 | 74 | ;; --- List collections 75 | 76 | (defn list-collections 77 | [{user :identity}] 78 | (let [params {:user user 79 | :type :list-icon-collections}] 80 | (-> (sv/query params) 81 | (p/then #(http/ok (rsp %)))))) 82 | 83 | ;; --- Create Icon 84 | 85 | (s/def ::create-icon 86 | (s/keys :req-un [::metadata ::us/name ::metadata ::content] 87 | :opt-un [::us/id ::collection])) 88 | 89 | (defn create-icon 90 | [{user :identity data :data :as request}] 91 | (let [{:keys [id name content metadata collection]} (us/conform ::create-icon data) 92 | id (or id (uuid/random))] 93 | (->> (sv/novelty {:id id 94 | :type :create-icon 95 | :user user 96 | :name name 97 | :collection collection 98 | :metadata metadata 99 | :content content}) 100 | (p/map (fn [entry] 101 | (let [loc (str "/api/library/icons/" (:id entry))] 102 | (http/created loc (rsp entry)))))))) 103 | 104 | ;; --- Update Icon 105 | 106 | (s/def ::update-icon 107 | (s/keys :req-un [::us/name ::us/version ::collection] :opt-un [::us/id])) 108 | 109 | (defn update-icon 110 | [{user :identity params :route-params data :data}] 111 | (let [data (us/conform ::update-icon data) 112 | message (assoc data 113 | :id (uuid/from-string (:id params)) 114 | :type :update-icon 115 | :user user)] 116 | (->> (sv/novelty message) 117 | (p/map #(http/ok (rsp %)))))) 118 | 119 | ;; --- Copy Icon 120 | 121 | (s/def ::copy-icon 122 | (s/keys :req-un [:us/id ::collection])) 123 | 124 | (defn copy-icon 125 | [{user :identity data :data}] 126 | (let [data (us/conform ::copy-icon data) 127 | message (assoc data 128 | :user user 129 | :type :copy-icon)] 130 | (->> (sv/novelty message) 131 | (p/map #(http/ok (rsp %)))))) 132 | 133 | ;; --- Delete Icon 134 | 135 | (defn delete-icon 136 | [{user :identity params :route-params}] 137 | (let [message {:id (uuid/from-string (:id params)) 138 | :type :delete-icon 139 | :user user}] 140 | (->> (sv/novelty message) 141 | (p/map (fn [v] (http/no-content)))))) 142 | 143 | ;; --- List collections 144 | 145 | (s/def ::list-icons 146 | (s/keys :opt-un [::us/id])) 147 | 148 | (defn list-icons 149 | [{user :identity route-params :route-params}] 150 | (let [{:keys [id]} (us/conform ::list-icons route-params) 151 | params {:collection id 152 | :type :list-icons 153 | :user user}] 154 | (->> (sv/query params) 155 | (p/map rsp) 156 | (p/map http/ok)))) 157 | -------------------------------------------------------------------------------- /src/uxbox/frontend/kvstore.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.frontend.kvstore 8 | (:refer-clojure :exclude [update]) 9 | (:require [clojure.spec :as s] 10 | [promesa.core :as p] 11 | [catacumba.http :as http] 12 | [uxbox.media :as media] 13 | [uxbox.util.spec :as us] 14 | [uxbox.services :as sv] 15 | [uxbox.util.response :refer (rsp)] 16 | [uxbox.util.uuid :as uuid])) 17 | 18 | (s/def ::version integer?) 19 | (s/def ::key string?) 20 | (s/def ::value any?) 21 | 22 | ;; --- Retrieve 23 | 24 | (s/def ::retrieve (s/keys :req-un [::key])) 25 | 26 | (defn retrieve 27 | [{user :identity params :route-params}] 28 | (let [data (us/conform ::retrieve params) 29 | params (assoc data 30 | :type :retrieve-kvstore 31 | :user user)] 32 | (->> (sv/query params) 33 | (p/map #(http/ok (rsp %)))))) 34 | 35 | ;; --- Update (or Create) 36 | 37 | (s/def ::update (s/keys :req-un [::key ::value] 38 | :opt-un [::version])) 39 | 40 | (defn update 41 | [{user :identity data :data}] 42 | (let [data (us/conform ::update data) 43 | params (assoc data 44 | :type :update-kvstore 45 | :user user)] 46 | (->> (sv/novelty params) 47 | (p/map #(http/ok (rsp %)))))) 48 | 49 | ;; --- Delete 50 | 51 | (s/def ::delete (s/keys :req-un [::key])) 52 | 53 | (defn delete 54 | [{user :identity params :route-params}] 55 | (let [data (us/conform ::delete params) 56 | params (assoc data 57 | :type :delete-kvstore 58 | :user user)] 59 | (->> (sv/novelty params) 60 | (p/map (fn [_] (http/no-content)))))) 61 | -------------------------------------------------------------------------------- /src/uxbox/frontend/pages.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.frontend.pages 8 | (:require [clojure.spec :as s] 9 | [promesa.core :as p] 10 | [catacumba.http :as http] 11 | [uxbox.util.spec :as us] 12 | [uxbox.services :as sv] 13 | [uxbox.util.response :refer (rsp)] 14 | [uxbox.util.uuid :as uuid])) 15 | 16 | ;; --- List Pages 17 | 18 | (defn list-pages-by-project 19 | [{user :identity params :route-params}] 20 | (let [params {:user user 21 | :project (uuid/from-string (:id params)) 22 | :type :list-pages-by-project}] 23 | (-> (sv/query params) 24 | (p/then #(http/ok (rsp %)))))) 25 | 26 | ;; --- Create Page 27 | 28 | (s/def ::data any?) 29 | (s/def ::metadata any?) 30 | (s/def ::project ::us/id) 31 | (s/def ::create-page 32 | (s/keys :req-un [::data ::metadata ::project ::us/name] 33 | :opt-un [::us/id])) 34 | 35 | (defn create-page 36 | [{user :identity data :data}] 37 | (let [data (us/conform ::create-page data) 38 | message (assoc data 39 | :type :create-page 40 | :user user)] 41 | (->> (sv/novelty message) 42 | (p/map (fn [result] 43 | (let [loc (str "/api/pages/" (:id result))] 44 | (http/created loc (rsp result)))))))) 45 | 46 | ;; --- Update Page 47 | 48 | (s/def ::update-page 49 | (s/merge ::create-page (s/keys :req-un [::us/version]))) 50 | 51 | (defn update-page 52 | [{user :identity params :route-params data :data}] 53 | (let [data (us/conform ::update-page data) 54 | message (assoc data 55 | :id (uuid/from-string (:id params)) 56 | :type :update-page 57 | :user user)] 58 | (->> (sv/novelty message) 59 | (p/map #(http/ok (rsp %)))))) 60 | 61 | ;; --- Update Page Metadata 62 | 63 | (s/def ::update-page-metadata 64 | (s/keys :req-un [::us/id ::metadata ::project ::us/name])) 65 | 66 | (defn update-page-metadata 67 | [{user :identity params :route-params data :data}] 68 | (let [data (us/conform ::update-page-metadata data) 69 | message (assoc data 70 | :id (uuid/from-string (:id params)) 71 | :type :update-page-metadata 72 | :user user)] 73 | (->> (sv/novelty message) 74 | (p/map #(http/ok (rsp %)))))) 75 | 76 | ;; --- Delete Page 77 | 78 | (defn delete-page 79 | [{user :identity params :route-params}] 80 | (let [message {:id (uuid/from-string (:id params)) 81 | :type :delete-page 82 | :user user}] 83 | (-> (sv/novelty message) 84 | (p/then (fn [v] (http/no-content)))))) 85 | 86 | ;; --- Retrieve Page History 87 | 88 | (s/def ::max (s/and ::us/integer-string ::us/positive-integer)) 89 | (s/def ::since ::us/integer-string) 90 | (s/def ::pinned ::us/boolean-string) 91 | 92 | (s/def ::retrieve-page-history 93 | (s/keys :opt-un [::max ::since ::pinned])) 94 | 95 | (defn retrieve-page-history 96 | [{user :identity params :route-params query :query-params}] 97 | (let [query (us/conform ::retrieve-page-history query) 98 | message (assoc query 99 | :id (uuid/from-string (:id params)) 100 | :type :list-page-history 101 | :user user)] 102 | (->> (sv/query message) 103 | (p/map #(http/ok (rsp %)))))) 104 | 105 | ;; --- Update Page History 106 | 107 | (s/def ::label string?) 108 | (s/def ::update-page-history 109 | (s/keys :req-un [::label ::pinned])) 110 | 111 | (defn update-page-history 112 | [{user :identity params :route-params data :data}] 113 | (let [data (us/conform ::update-page-history data) 114 | message (assoc data 115 | :type :update-page-history 116 | :id (uuid/from-string (:hid params)) 117 | :user user)] 118 | (->> (sv/novelty message) 119 | (p/map #(http/ok (rsp %)))))) 120 | -------------------------------------------------------------------------------- /src/uxbox/frontend/projects.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.frontend.projects 8 | (:require [clojure.spec :as s] 9 | [promesa.core :as p] 10 | [catacumba.http :as http] 11 | [uxbox.util.spec :as us] 12 | [uxbox.services :as sv] 13 | [uxbox.util.response :refer (rsp)] 14 | [uxbox.util.uuid :as uuid])) 15 | 16 | ;; --- List Projects 17 | 18 | (defn list-projects 19 | [{user :identity}] 20 | (let [message {:user user :type :list-projects}] 21 | (->> (sv/query message) 22 | (p/map #(http/ok (rsp %)))))) 23 | 24 | ;; --- Create Projects 25 | 26 | (s/def ::create-project 27 | (s/keys :req-un [::us/name] :opt-un [::us/id])) 28 | 29 | (defn create-project 30 | [{user :identity data :data}] 31 | (let [data (us/conform ::create-project data) 32 | message (assoc data 33 | :type :create-project 34 | :user user)] 35 | (->> (sv/novelty message) 36 | (p/map (fn [result] 37 | (let [loc (str "/api/projects/" (:id result))] 38 | (http/created loc (rsp result)))))))) 39 | 40 | ;; --- Update Project 41 | 42 | (s/def ::update-project 43 | (s/keys :req-un [::us/name ::us/version])) 44 | 45 | (defn update-project 46 | [{user :identity params :route-params data :data}] 47 | (let [data (us/conform ::update-project data) 48 | message (assoc data 49 | :id (uuid/from-string (:id params)) 50 | :type :update-project 51 | :user user)] 52 | (-> (sv/novelty message) 53 | (p/then #(http/ok (rsp %)))))) 54 | 55 | ;; --- Delete Project 56 | 57 | (defn delete-project 58 | [{user :identity params :route-params}] 59 | (let [message {:id (uuid/from-string (:id params)) 60 | :type :delete-project 61 | :user user}] 62 | (-> (sv/novelty message) 63 | (p/then (fn [v] (http/no-content)))))) 64 | 65 | 66 | ;; --- Retrieve project 67 | 68 | (defn retrieve-project-by-share-token 69 | [{params :route-params}] 70 | (let [message {:token (:token params) 71 | :type :retrieve-project-by-share-token}] 72 | (->> (sv/query message) 73 | (p/map #(http/ok (rsp %)))))) 74 | -------------------------------------------------------------------------------- /src/uxbox/frontend/svgparse.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.frontend.svgparse 8 | "A frontend exposed endpoints for svgparse functionality." 9 | (:require [clojure.spec :as s] 10 | [promesa.core :as p] 11 | [catacumba.http :as http] 12 | [uxbox.util.spec :as us] 13 | [uxbox.services :as sv] 14 | [uxbox.util.response :refer (rsp)] 15 | [uxbox.util.uuid :as uuid])) 16 | 17 | (defn parse 18 | [{body :body :as context}] 19 | (let [message {:data (slurp body) 20 | :type :parse-svg}] 21 | (->> (sv/query message) 22 | (p/map #(http/ok (rsp %)))))) 23 | -------------------------------------------------------------------------------- /src/uxbox/frontend/users.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.frontend.users 8 | (:require [clojure.spec :as s] 9 | [promesa.core :as p] 10 | [catacumba.http :as http] 11 | [storages.core :as st] 12 | [storages.util :as path] 13 | [uxbox.media :as media] 14 | [uxbox.images :as images] 15 | [uxbox.util.spec :as us] 16 | [uxbox.services :as sv] 17 | [uxbox.services.users :as svu] 18 | [uxbox.util.response :refer (rsp)] 19 | [uxbox.util.uuid :as uuid])) 20 | 21 | ;; --- Helpers 22 | 23 | (defn- resolve-thumbnail 24 | [user] 25 | (let [opts {:src :photo 26 | :dst :photo 27 | :size [100 100] 28 | :quality 90 29 | :format "jpg"}] 30 | (images/populate-thumbnails user opts))) 31 | 32 | ;; --- Retrieve Profile 33 | 34 | (defn retrieve-profile 35 | [{user :identity}] 36 | (let [message {:user user 37 | :type :retrieve-profile}] 38 | (->> (sv/query message) 39 | (p/map resolve-thumbnail) 40 | (p/map #(http/ok (rsp %)))))) 41 | 42 | ;; --- Update Profile 43 | 44 | (s/def ::fullname string?) 45 | (s/def ::metadata any?) 46 | (s/def ::update-profile 47 | (s/keys :req-un [::us/id ::us/username ::us/email 48 | ::fullname ::metadata])) 49 | 50 | (defn update-profile 51 | [{user :identity data :data}] 52 | (let [data (us/conform ::update-profile data) 53 | message (assoc data 54 | :type :update-profile 55 | :user user)] 56 | (->> (sv/novelty message) 57 | (p/map resolve-thumbnail) 58 | (p/map #(http/ok (rsp %)))))) 59 | 60 | ;; --- Update Password 61 | 62 | (s/def ::old-password ::us/password) 63 | (s/def ::update-password 64 | (s/keys :req-un [::us/password ::old-password])) 65 | 66 | (defn update-password 67 | [{user :identity data :data}] 68 | (let [data (us/conform ::update-password data) 69 | message (assoc data 70 | :type :update-profile-password 71 | :user user)] 72 | (-> (sv/novelty message) 73 | (p/then #(http/ok (rsp %)))))) 74 | 75 | ;; --- Update Profile Photo 76 | 77 | (s/def ::file ::us/uploaded-file) 78 | (s/def ::update-photo (s/keys :req-un [::file])) 79 | 80 | (defn update-photo 81 | [{user :identity data :data}] 82 | (letfn [(store-photo [file] 83 | (let [filename (path/base-name file) 84 | storage media/images-storage] 85 | (st/save storage filename file))) 86 | (assign-photo [path] 87 | (sv/novelty {:user user 88 | :path (str path) 89 | :type :update-profile-photo})) 90 | (create-response [_] 91 | (http/no-content))] 92 | (let [{:keys [file]} (us/conform ::update-photo data)] 93 | (->> (store-photo file) 94 | (p/mapcat assign-photo) 95 | (p/map create-response))))) 96 | 97 | ;; --- Register User 98 | 99 | (s/def ::register 100 | (s/keys :req-un [::us/username ::us/email ::us/password ::fullname])) 101 | 102 | (defn register-user 103 | [{data :data}] 104 | (let [data (us/conform ::register data) 105 | message (assoc data :type :register-profile)] 106 | (->> (sv/novelty message) 107 | (p/map #(http/ok (rsp %)))))) 108 | 109 | 110 | ;; --- Request Password Recovery 111 | 112 | ;; FIXME: rename for consistency 113 | 114 | (s/def ::request-recovery 115 | (s/keys :req-un [::us/username])) 116 | 117 | (defn request-recovery 118 | [{data :data}] 119 | (let [data (us/conform ::request-recovery data) 120 | message (assoc data :type :request-profile-password-recovery)] 121 | (->> (sv/novelty message) 122 | (p/map (fn [_] (http/no-content)))))) 123 | 124 | ;; --- Password Recovery 125 | 126 | ;; FIXME: rename for consistency 127 | 128 | (s/def ::token string?) 129 | (s/def ::password-recovery 130 | (s/keys :req-un [::token ::us/password])) 131 | 132 | (defn recover-password 133 | [{data :data}] 134 | (let [data (us/conform ::password-recovery data) 135 | message (assoc data :type :recover-profile-password)] 136 | (->> (sv/novelty message) 137 | (p/map (fn [_] (http/no-content)))))) 138 | 139 | ;; --- Valiadate Recovery Token 140 | 141 | (defn validate-recovery-token 142 | [{params :route-params}] 143 | (let [message {:type :validate-profile-password-recovery-token 144 | :token (:token params)}] 145 | (->> (sv/query message) 146 | (p/map (fn [v] 147 | (if v 148 | (http/no-content) 149 | (http/not-found ""))))))) 150 | -------------------------------------------------------------------------------- /src/uxbox/images.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.images 8 | "Image postprocessing." 9 | (:require [storages.core :as st] 10 | [storages.util :as path] 11 | [clojure.spec :as s] 12 | [uxbox.util.spec :as us] 13 | [uxbox.media :as media] 14 | [uxbox.util.images :as images] 15 | [uxbox.util.data :refer (dissoc-in)])) 16 | 17 | ;; FIXME: add spec for thumbnail config 18 | 19 | (defn make-thumbnail 20 | [path {:keys [size format quality] :as cfg}] 21 | (let [parent (path/parent path) 22 | [filename ext] (path/split-ext path) 23 | 24 | suffix-parts [(nth size 0) (nth size 1) quality format] 25 | final-name (apply str filename "-" (interpose "." suffix-parts)) 26 | final-path (path/path parent final-name) 27 | 28 | images-storage media/images-storage 29 | thumbs-storage media/thumbnails-storage] 30 | (if @(st/exists? thumbs-storage final-path) 31 | (str (st/public-url thumbs-storage final-path)) 32 | (if @(st/exists? images-storage path) 33 | (let [datapath @(st/lookup images-storage path) 34 | content (images/thumbnail datapath cfg) 35 | path @(st/save thumbs-storage final-path content)] 36 | (str (st/public-url thumbs-storage path))) 37 | nil)))) 38 | 39 | (defn populate-thumbnail 40 | [entry {:keys [src dst] :as cfg}] 41 | (assert (map? entry) "`entry` should be map") 42 | 43 | (let [src (if (vector? src) src [src]) 44 | dst (if (vector? dst) dst [dst]) 45 | src (get-in entry src)] 46 | (if (empty? src) 47 | entry 48 | (assoc-in entry dst (make-thumbnail src cfg))))) 49 | 50 | (defn populate-thumbnails 51 | [entry & settings] 52 | (reduce populate-thumbnail entry settings)) 53 | 54 | (defn populate-urls 55 | [entry storage src dst] 56 | (assert (map? entry) "`entry` should be map") 57 | (assert (st/storage? storage) "`storage` should be a valid storage instance.") 58 | (let [src (if (vector? src) src [src]) 59 | dst (if (vector? dst) dst [dst]) 60 | value (get-in entry src)] 61 | (if (empty? value) 62 | entry 63 | (let [url (str (st/public-url storage value))] 64 | (-> entry 65 | (dissoc-in src) 66 | (assoc-in dst url)))))) 67 | -------------------------------------------------------------------------------- /src/uxbox/locks.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.locks 8 | "Advirsory locks for specific handling concurrent modifications 9 | on particular objects in the database." 10 | (:require [suricatta.core :as sc]) 11 | (:import clojure.lang.Murmur3)) 12 | 13 | (defn- uuid->long 14 | [v] 15 | (Murmur3/hashUnencodedChars (str v))) 16 | 17 | (defn acquire! 18 | [conn v] 19 | (let [id (uuid->long v)] 20 | (sc/execute conn ["select pg_advisory_xact_lock(?);" id]) 21 | nil)) 22 | -------------------------------------------------------------------------------- /src/uxbox/main.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.main 8 | (:require [clojure.tools.namespace.repl :as repl] 9 | [clojure.walk :refer [macroexpand-all]] 10 | [clojure.pprint :refer [pprint]] 11 | [clojure.test :as test] 12 | [clojure.java.io :as io] 13 | [mount.core :as mount] 14 | [buddy.core.codecs :as codecs] 15 | [buddy.core.codecs.base64 :as b64] 16 | [buddy.core.nonce :as nonce] 17 | [uxbox.config :as cfg] 18 | [uxbox.migrations] 19 | [uxbox.db] 20 | [uxbox.frontend] 21 | [uxbox.scheduled-jobs]) 22 | (:gen-class)) 23 | 24 | ;; --- Development Stuff 25 | 26 | (defn- start 27 | [] 28 | (mount/start)) 29 | 30 | (defn- start-minimal 31 | [] 32 | (-> (mount/only #{#'uxbox.config/config 33 | #'uxbox.db/datasource 34 | #'uxbox.migrations/migrations}) 35 | (mount/start))) 36 | 37 | (defn- stop 38 | [] 39 | (mount/stop)) 40 | 41 | (defn- refresh 42 | [] 43 | (stop) 44 | (repl/refresh)) 45 | 46 | (defn- refresh-all 47 | [] 48 | (stop) 49 | (repl/refresh-all)) 50 | 51 | (defn- go 52 | "starts all states defined by defstate" 53 | [] 54 | (start) 55 | :ready) 56 | 57 | (defn- reset 58 | [] 59 | (stop) 60 | (repl/refresh :after 'uxbox.main/start)) 61 | 62 | (defn make-secret 63 | [] 64 | (-> (nonce/random-bytes 64) 65 | (b64/encode true) 66 | (codecs/bytes->str))) 67 | 68 | ;; --- Entry point (only for uberjar) 69 | 70 | (defn test-vars 71 | [& vars] 72 | (repl/refresh) 73 | (test/test-vars 74 | (map (fn [sym] 75 | (require (symbol (namespace sym))) 76 | (resolve sym)) 77 | vars))) 78 | 79 | (defn test-ns 80 | [ns] 81 | (repl/refresh) 82 | (test/test-ns ns)) 83 | 84 | (defn test-all 85 | ([] (test/run-all-tests #"^uxbox.tests.*")) 86 | ([re] (test/run-all-tests re))) 87 | 88 | ;; --- Entry point (only for uberjar) 89 | 90 | (defn -main 91 | [& args] 92 | (mount/start)) 93 | -------------------------------------------------------------------------------- /src/uxbox/media.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.media 8 | "A media storage impl for uxbox." 9 | (:require [mount.core :as mount :refer (defstate)] 10 | [clojure.java.io :as io] 11 | [cuerdas.core :as str] 12 | [storages.core :as st] 13 | [storages.fs.local :refer (filesystem)] 14 | [storages.fs.misc :refer (hashed scoped)] 15 | [uxbox.config :refer (config)])) 16 | 17 | ;; --- State 18 | 19 | (defstate static-storage 20 | :start (let [{:keys [basedir baseuri]} (:static config)] 21 | (filesystem {:basedir basedir :baseuri baseuri}))) 22 | 23 | (defstate media-storage 24 | :start (let [{:keys [basedir baseuri]} (:media config)] 25 | (filesystem {:basedir basedir :baseuri baseuri}))) 26 | 27 | (defstate images-storage 28 | :start (-> media-storage 29 | (scoped "images") 30 | (hashed))) 31 | 32 | (defstate thumbnails-storage 33 | :start (scoped media-storage "thumbs")) 34 | 35 | ;; --- Public Api 36 | 37 | (defn resolve-asset 38 | [path] 39 | (str (st/public-url static-storage path))) 40 | -------------------------------------------------------------------------------- /src/uxbox/migrations.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.migrations 8 | (:require [mount.core :as mount :refer (defstate)] 9 | [migrante.core :as mg :refer (defmigration)] 10 | [uxbox.db :as db] 11 | [uxbox.config :as cfg] 12 | [uxbox.util.template :as tmpl])) 13 | 14 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 15 | ;; Migrations 16 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 17 | 18 | (defmigration utils-0000 19 | "Create a initial version of txlog table." 20 | :up (mg/resource "migrations/0000.main.up.sql")) 21 | 22 | (defmigration txlog-0001 23 | "Create a initial version of txlog table." 24 | :up (mg/resource "migrations/0001.txlog.up.sql")) 25 | 26 | (defmigration auth-0002 27 | "Create initial auth related tables." 28 | :up (mg/resource "migrations/0002.auth.up.sql")) 29 | 30 | (defmigration projects-0003 31 | "Create initial tables for projects." 32 | :up (mg/resource "migrations/0003.projects.up.sql")) 33 | 34 | (defmigration pages-0004 35 | "Create initial tables for pages." 36 | :up (mg/resource "migrations/0004.pages.up.sql")) 37 | 38 | (defmigration kvstore-0005 39 | "Create initial tables for kvstore." 40 | :up (mg/resource "migrations/0005.kvstore.up.sql")) 41 | 42 | (defmigration emails-queue-0006 43 | "Create initial tables for emails queue." 44 | :up (mg/resource "migrations/0006.emails.up.sql")) 45 | 46 | (defmigration images-0007 47 | "Create initial tables for image collections." 48 | :up (mg/resource "migrations/0007.images.up.sql")) 49 | 50 | (defmigration icons-0008 51 | "Create initial tables for image collections." 52 | :up (mg/resource "migrations/0008.icons.up.sql")) 53 | 54 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 55 | ;; Entry point 56 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 57 | 58 | (def +migrations+ 59 | {:name :uxbox-main 60 | :steps [[:0000 utils-0000] 61 | [:0001 txlog-0001] 62 | [:0002 auth-0002] 63 | [:0003 projects-0003] 64 | [:0004 pages-0004] 65 | [:0005 kvstore-0005] 66 | [:0006 emails-queue-0006] 67 | [:0007 images-0007] 68 | [:0008 icons-0008]]}) 69 | 70 | (defn- migrate 71 | [] 72 | (let [options (:migrations cfg/config {})] 73 | (with-open [mctx (mg/context db/datasource options)] 74 | (mg/migrate mctx +migrations+) 75 | nil))) 76 | 77 | (defstate migrations 78 | :start (migrate)) 79 | -------------------------------------------------------------------------------- /src/uxbox/portation.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.portation 8 | "Support for export/import operations of projects." 9 | (:refer-clojure :exclude [with-open]) 10 | (:require [clojure.java.io :as io] 11 | [suricatta.core :as sc] 12 | [storages.util :as path] 13 | [uxbox.db :as db] 14 | [uxbox.sql :as sql] 15 | [uxbox.util.uuid :as uuid] 16 | [uxbox.util.closeable :refer (with-open)] 17 | [uxbox.util.tempfile :as tmpfile] 18 | [uxbox.util.transit :as t] 19 | [uxbox.util.snappy :as snappy])) 20 | 21 | ;; --- Export 22 | 23 | (defn- write-project 24 | [conn writer id] 25 | (let [sql (sql/get-project-by-id {:id id}) 26 | result (sc/fetch-one conn sql)] 27 | (when-not result 28 | (ex-info "No project found with specified id" {:id id})) 29 | (t/write! writer {::type ::project ::payload result}))) 30 | 31 | (defn- write-pages 32 | [conn writer id] 33 | (let [sql (sql/get-pages-for-project {:project id}) 34 | results (sc/fetch conn sql)] 35 | (run! #(t/write! writer {::type ::page ::payload %}) results))) 36 | 37 | (defn- write-pages-history 38 | [conn writer id] 39 | (let [sql (sql/get-page-history-for-project {:project id}) 40 | results (sc/fetch conn sql)] 41 | (run! #(t/write! writer {::type ::page-history ::payload %}) results))) 42 | 43 | (defn- write-data 44 | [path id] 45 | (with-open [ostream (io/output-stream path) 46 | zstream (snappy/output-stream ostream) 47 | conn (db/connection)] 48 | (let [writer (t/writer zstream {:type :msgpack})] 49 | (sc/atomic conn 50 | (write-project conn writer id) 51 | (write-pages conn writer id) 52 | (write-pages-history conn writer id))))) 53 | 54 | (defn export 55 | "Given an id, returns a path to a temporal file with the exported 56 | bundle of the specified project." 57 | [id] 58 | (let [path (tmpfile/create)] 59 | (write-data path id) 60 | path)) 61 | 62 | ;; --- Import 63 | 64 | (defn- read-entry 65 | [reader] 66 | (try 67 | (t/read! reader) 68 | (catch RuntimeException e 69 | (let [cause (.getCause e)] 70 | (if (instance? java.io.EOFException cause) 71 | ::eof 72 | (throw e)))))) 73 | 74 | (defn- persist-project 75 | [conn project] 76 | (let [sql (sql/create-project project)] 77 | (sc/execute conn sql))) 78 | 79 | (defn- persist-page 80 | [conn page] 81 | (let [sql (sql/create-page page)] 82 | (sc/execute conn sql))) 83 | 84 | (defn- persist-page-history 85 | [conn history] 86 | (let [sql (sql/create-page-history history)] 87 | (sc/execute conn sql))) 88 | 89 | (defn- persist-entry 90 | [conn entry] 91 | (let [payload (::payload entry) 92 | type (::type entry)] 93 | (case type 94 | ::project (persist-project conn payload) 95 | ::page (persist-page conn payload) 96 | ::page-history (persist-page-history conn payload)))) 97 | 98 | (defn- read-data 99 | [conn reader] 100 | (loop [entry (read-entry reader)] 101 | (when (not= entry ::eof) 102 | (persist-entry conn entry) 103 | (recur (read-entry reader))))) 104 | 105 | (defn import! 106 | "Given a path to the previously exported bundle, try to import it." 107 | [path] 108 | (with-open [istream (io/input-stream (path/path path)) 109 | zstream (snappy/input-stream istream) 110 | conn (db/connection)] 111 | (let [reader (t/reader zstream {:type :msgpack})] 112 | (sc/atomic conn 113 | (read-data conn reader) 114 | nil)))) 115 | -------------------------------------------------------------------------------- /src/uxbox/scheduled_jobs.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.scheduled-jobs 8 | "Time-based scheduled jobs." 9 | (:require [mount.core :as mount :refer (defstate)] 10 | [uxbox.config :as cfg] 11 | [uxbox.db] 12 | [uxbox.util.quartz :as qtz])) 13 | 14 | (defn- initialize 15 | [] 16 | (let [nss #{'uxbox.scheduled-jobs.garbage 17 | 'uxbox.scheduled-jobs.emails}] 18 | (-> (qtz/scheduler) 19 | (qtz/start! {:search-on nss})))) 20 | 21 | (defstate scheduler 22 | :start (initialize) 23 | :stop (qtz/stop! scheduler)) 24 | -------------------------------------------------------------------------------- /src/uxbox/scheduled_jobs/emails.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.scheduled-jobs.emails 8 | "Email sending async tasks." 9 | (:require [clojure.tools.logging :as log] 10 | [suricatta.core :as sc] 11 | [postal.core :as postal] 12 | [uxbox.db :as db] 13 | [uxbox.config :as cfg] 14 | [uxbox.sql :as sql] 15 | [uxbox.util.quartz :as qtz] 16 | [uxbox.util.blob :as blob] 17 | [uxbox.util.transit :as t] 18 | [uxbox.util.data :as data])) 19 | 20 | ;; --- Impl details 21 | 22 | (defn- decode-email-data 23 | [{:keys [data] :as result}] 24 | (merge result (when data 25 | {:data (-> data blob/decode t/decode)}))) 26 | 27 | (defn- fetch-pending-emails 28 | [conn] 29 | (let [sqlv (sql/get-pending-emails)] 30 | (->> (sc/fetch conn sqlv) 31 | (map data/normalize-attrs) 32 | (map decode-email-data)))) 33 | 34 | (defn- fetch-immediate-emails 35 | [conn] 36 | (let [sqlv (sql/get-immediate-emails)] 37 | (->> (sc/fetch conn sqlv) 38 | (map data/normalize-attrs) 39 | (map decode-email-data)))) 40 | 41 | (defn- fetch-failed-emails 42 | [conn] 43 | (let [sqlv (sql/get-pending-emails)] 44 | (->> (sc/fetch conn sqlv) 45 | (map data/normalize-attrs) 46 | (map decode-email-data)))) 47 | 48 | (defn- mark-email-as-sent 49 | [conn id] 50 | (let [sqlv (sql/mark-email-as-sent {:id id})] 51 | (sc/execute conn sqlv))) 52 | 53 | (defn- mark-email-as-failed 54 | [conn id] 55 | (let [sqlv (sql/mark-email-as-failed {:id id})] 56 | (sc/execute conn sqlv))) 57 | 58 | (defn- send-email-to-console 59 | [{:keys [id data] :as entry}] 60 | (println "******** start email:" id "**********") 61 | (println (->> (:body data) 62 | (filter #(= (:uxbox.emails.core/type %) :text/plain)) 63 | (first) 64 | (:content))) 65 | (println "********** end email:" id "**********") 66 | {:error :SUCCESS}) 67 | 68 | (defn- send-email 69 | [{:keys [id data] :as entry}] 70 | (let [config (:smtp cfg/config) 71 | result (if (:noop config) 72 | (send-email-to-console entry) 73 | (postal/send-message config data))] 74 | (if (= (:error result) :SUCCESS) 75 | (log/debug "Message" id "sent successfully.") 76 | (log/warn "Message" id "failed with:" (:message result))) 77 | (if (= (:error result) :SUCCESS) 78 | true 79 | false))) 80 | 81 | (defn- send-emails 82 | [conn entries] 83 | (loop [entries entries] 84 | (if-let [entry (first entries)] 85 | (do (if (send-email entry) 86 | (mark-email-as-sent conn (:id entry)) 87 | (mark-email-as-failed conn (:id entry))) 88 | (recur (rest entries)))))) 89 | 90 | ;; --- Jobs 91 | 92 | (defn send-immediate-emails 93 | {::qtz/interval (* 60 1 1000) ;; every 1min 94 | ::qtz/repeat? true 95 | ::qtz/job true} 96 | [] 97 | (log/info "task-send-immediate-emails...") 98 | (with-open [conn (db/connection)] 99 | (sc/atomic conn 100 | (->> (fetch-immediate-emails conn) 101 | (send-emails conn))))) 102 | 103 | (defn send-pending-emails 104 | {::qtz/interval (* 60 5 1000) ;; every 5min 105 | ::qtz/repeat? true 106 | ::qtz/job true} 107 | [] 108 | (with-open [conn (db/connection)] 109 | (sc/atomic conn 110 | (->> (fetch-pending-emails conn) 111 | (send-emails conn))))) 112 | 113 | (defn send-failed-emails 114 | "Job that resends failed to send messages." 115 | {::qtz/interval (* 60 5 1000) ;; every 5min 116 | ::qtz/repeat? true 117 | ::qtz/job true} 118 | [] 119 | (log/info "task-send-failed-emails...") 120 | (with-open [conn (db/connection)] 121 | (sc/atomic conn 122 | (->> (fetch-failed-emails conn) 123 | (send-emails conn))))) 124 | -------------------------------------------------------------------------------- /src/uxbox/scheduled_jobs/garbage.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.scheduled-jobs.garbage 8 | "Garbage Collector related tasks." 9 | (:require [suricatta.core :as sc] 10 | [uxbox.db :as db] 11 | [uxbox.util.quartz :as qtz])) 12 | 13 | ;; --- Delete projects 14 | 15 | ;; TODO: move inline sql into resources/sql directory 16 | 17 | (defn clean-deleted-projects 18 | "Task that cleans the deleted projects." 19 | {::qtz/repeat? true 20 | ::qtz/interval (* 1000 3600 24) 21 | ::qtz/job true} 22 | [] 23 | (with-open [conn (db/connection)] 24 | (sc/atomic conn 25 | (let [sql (str "DELETE FROM projects " 26 | " WHERE deleted_at is not null AND " 27 | " (now()-deleted_at)::interval > '10 day'::interval;")] 28 | (sc/execute conn sql))))) 29 | -------------------------------------------------------------------------------- /src/uxbox/services.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.services 8 | "Main namespace for access to all uxbox services." 9 | (:require [suricatta.core :as sc] 10 | [executors.core :as exec] 11 | [promesa.core :as p] 12 | [uxbox.db :as db] 13 | [uxbox.services.core :as core] 14 | [uxbox.util.transit :as t] 15 | [uxbox.util.blob :as blob])) 16 | 17 | ;; Load relevant subnamespaces with the implementation 18 | (load "services/auth") 19 | (load "services/projects") 20 | (load "services/pages") 21 | (load "services/images") 22 | (load "services/icons") 23 | (load "services/kvstore") 24 | 25 | ;; --- Implementation 26 | 27 | (def ^:private encode (comp blob/encode t/encode)) 28 | 29 | (defn- insert-txlog 30 | [data] 31 | (with-open [conn (db/connection)] 32 | (let [sql (str "INSERT INTO txlog (payload) VALUES (?)") 33 | sqlv [sql (encode data)]] 34 | (sc/execute conn sqlv)))) 35 | 36 | (defn- handle-novelty 37 | [data] 38 | (let [rs (core/novelty data) 39 | rs (if (p/promise? rs) rs (p/resolved rs))] 40 | (p/map (fn [v] 41 | (insert-txlog data) 42 | v) rs))) 43 | 44 | (defn- handle-query 45 | [data] 46 | (let [result (core/query data)] 47 | (if (p/promise? result) 48 | result 49 | (p/resolved result)))) 50 | 51 | ;; --- Public Api 52 | 53 | (defn novelty 54 | [data] 55 | (->> (exec/submit (partial handle-novelty data)) 56 | (p/mapcat identity))) 57 | 58 | (defn query 59 | [data] 60 | (->> (exec/submit (partial handle-query data)) 61 | (p/mapcat identity))) 62 | -------------------------------------------------------------------------------- /src/uxbox/services/auth.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.services.auth 8 | (:require [clojure.spec :as s] 9 | [suricatta.core :as sc] 10 | [buddy.hashers :as hashers] 11 | [buddy.sign.jwt :as jwt] 12 | [buddy.core.hash :as hash] 13 | [uxbox.config :as cfg] 14 | [uxbox.util.spec :as us] 15 | [uxbox.db :as db] 16 | [uxbox.services.core :as core] 17 | [uxbox.services.users :as users] 18 | [uxbox.util.exceptions :as ex])) 19 | 20 | ;; --- Login 21 | 22 | (defn- check-user-password 23 | [user password] 24 | (hashers/check password (:password user))) 25 | 26 | (defn generate-token 27 | [user] 28 | (let [data {:id (:id user)} 29 | opts (:auth-options cfg/config)] 30 | (jwt/encrypt data cfg/secret opts))) 31 | 32 | (s/def ::scope string?) 33 | (s/def ::login 34 | (s/keys :req-un [::us/username ::us/password ::scope])) 35 | 36 | (defmethod core/novelty :login 37 | [{:keys [username password scope] :as params}] 38 | (s/assert ::login params) 39 | (with-open [conn (db/connection)] 40 | (let [user (users/find-user-by-username-or-email conn username)] 41 | (when-not user 42 | (ex/raise :type :validation 43 | :code ::wrong-credentials)) 44 | (if (check-user-password user password) 45 | {:token (generate-token user)} 46 | (ex/raise :type :validation 47 | :code ::wrong-credentials))))) 48 | -------------------------------------------------------------------------------- /src/uxbox/services/core.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.services.core 8 | (:require [clojure.walk :as walk] 9 | [cuerdas.core :as str] 10 | [uxbox.util.exceptions :as ex])) 11 | 12 | (defmulti novelty :type) 13 | 14 | (defmulti query :type) 15 | 16 | (defmethod novelty :default 17 | [{:keys [type] :as data}] 18 | (ex/raise :code ::not-implemented 19 | :message-category :novelty 20 | :message-type type)) 21 | 22 | (defmethod query :default 23 | [{:keys [type] :as data}] 24 | (ex/raise :code ::not-implemented 25 | :message-category :query 26 | :message-type type)) 27 | 28 | -------------------------------------------------------------------------------- /src/uxbox/services/kvstore.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.services.kvstore 8 | (:require [clojure.spec :as s] 9 | [suricatta.core :as sc] 10 | [buddy.core.codecs :as codecs] 11 | [uxbox.config :as ucfg] 12 | [uxbox.sql :as sql] 13 | [uxbox.db :as db] 14 | [uxbox.util.spec :as us] 15 | [uxbox.services.core :as core] 16 | [uxbox.util.time :as dt] 17 | [uxbox.util.data :as data] 18 | [uxbox.util.transit :as t] 19 | [uxbox.util.blob :as blob] 20 | [uxbox.util.uuid :as uuid])) 21 | 22 | (s/def ::version integer?) 23 | (s/def ::key string?) 24 | (s/def ::value any?) 25 | (s/def ::user uuid?) 26 | 27 | (defn decode-value 28 | [{:keys [value] :as data}] 29 | (if value 30 | (assoc data :value (-> value blob/decode t/decode)) 31 | data)) 32 | 33 | ;; --- Update KVStore 34 | 35 | (s/def ::update-kvstore 36 | (s/keys :req-un [::key ::value ::user ::version])) 37 | 38 | (defn update-kvstore 39 | [conn {:keys [user key value version] :as data}] 40 | (let [opts {:user user 41 | :key key 42 | :version version 43 | :value (-> value t/encode blob/encode)} 44 | sqlv (sql/update-kvstore opts)] 45 | (some->> (sc/fetch-one conn sqlv) 46 | (data/normalize-attrs) 47 | (decode-value)))) 48 | 49 | (defmethod core/novelty :update-kvstore 50 | [params] 51 | (s/assert ::update-kvstore params) 52 | (with-open [conn (db/connection)] 53 | (sc/apply-atomic conn update-kvstore params))) 54 | 55 | ;; --- Retrieve KVStore 56 | 57 | (s/def ::retrieve-kvstore 58 | (s/keys :req-un [::key ::user])) 59 | 60 | (defn retrieve-kvstore 61 | [conn {:keys [user key] :as params}] 62 | (let [sqlv (sql/retrieve-kvstore params)] 63 | (some->> (sc/fetch-one conn sqlv) 64 | (data/normalize-attrs) 65 | (decode-value)))) 66 | 67 | (defmethod core/query :retrieve-kvstore 68 | [params] 69 | (s/assert ::retrieve-kvstore params) 70 | (with-open [conn (db/connection)] 71 | (retrieve-kvstore conn params))) 72 | 73 | ;; --- Delete KVStore 74 | 75 | (s/def ::delete-kvstore 76 | (s/keys :req-un [::key ::user])) 77 | 78 | (defn delete-kvstore 79 | [conn {:keys [user key] :as params}] 80 | (let [sqlv (sql/delete-kvstore params)] 81 | (pos? (sc/execute conn sqlv)))) 82 | 83 | (defmethod core/novelty :delete-kvstore 84 | [params] 85 | (s/assert ::delete-kvstore params) 86 | (with-open [conn (db/connection)] 87 | (sc/apply-atomic conn delete-kvstore params))) 88 | -------------------------------------------------------------------------------- /src/uxbox/services/projects.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.services.projects 8 | (:require [clojure.spec :as s] 9 | [suricatta.core :as sc] 10 | [buddy.core.codecs :as codecs] 11 | [uxbox.config :as ucfg] 12 | [uxbox.sql :as sql] 13 | [uxbox.db :as db] 14 | [uxbox.util.spec :as us] 15 | [uxbox.services.core :as core] 16 | [uxbox.services.pages :as pages] 17 | [uxbox.util.data :as data] 18 | [uxbox.util.transit :as t] 19 | [uxbox.util.blob :as blob] 20 | [uxbox.util.uuid :as uuid])) 21 | 22 | (s/def ::token string?) 23 | (s/def ::data string?) 24 | (s/def ::user uuid?) 25 | (s/def ::project uuid?) 26 | 27 | ;; --- Create Project 28 | 29 | (defn create-project 30 | [conn {:keys [id user name] :as data}] 31 | (let [id (or id (uuid/random)) 32 | sqlv (sql/create-project {:id id :user user :name name})] 33 | (some-> (sc/fetch-one conn sqlv) 34 | (data/normalize)))) 35 | 36 | (s/def ::create-project 37 | (s/keys :req-un [::user ::us/name] 38 | :opt-un [::us/id])) 39 | 40 | (defmethod core/novelty :create-project 41 | [params] 42 | (s/assert ::create-project params) 43 | (with-open [conn (db/connection)] 44 | (create-project conn params))) 45 | 46 | ;; --- Update Project 47 | 48 | (defn- update-project 49 | [conn {:keys [name version id user] :as data}] 50 | (let [sqlv (sql/update-project {:name name 51 | :version version 52 | :id id 53 | :user user})] 54 | (some-> (sc/fetch-one conn sqlv) 55 | (data/normalize)))) 56 | 57 | (s/def ::update-project 58 | (s/merge ::create-project (s/keys :req-un [::us/version]))) 59 | 60 | (defmethod core/novelty :update-project 61 | [params] 62 | (s/assert ::update-project params) 63 | (with-open [conn (db/connection)] 64 | (update-project conn params))) 65 | 66 | ;; --- Delete Project 67 | 68 | (defn- delete-project 69 | [conn {:keys [id user] :as data}] 70 | (let [sqlv (sql/delete-project {:id id :user user})] 71 | (pos? (sc/execute conn sqlv)))) 72 | 73 | (s/def ::delete-project 74 | (s/keys :req-un [::us/id ::user])) 75 | 76 | (defmethod core/novelty :delete-project 77 | [params] 78 | (s/assert ::delete-project params) 79 | (with-open [conn (db/connection)] 80 | (delete-project conn params))) 81 | 82 | ;; --- List Projects 83 | 84 | (declare decode-page-metadata) 85 | (declare decode-page-data) 86 | 87 | (defn get-projects 88 | [conn user] 89 | (let [sqlv (sql/get-projects {:user user})] 90 | (->> (sc/fetch conn sqlv) 91 | (map data/normalize) 92 | 93 | ;; This is because the project comes with 94 | ;; the first page preloaded and it need 95 | ;; to be decoded. 96 | (map decode-page-metadata) 97 | (map decode-page-data)))) 98 | 99 | (defmethod core/query :list-projects 100 | [{:keys [user] :as params}] 101 | (s/assert ::user user) 102 | (with-open [conn (db/connection)] 103 | (get-projects conn user))) 104 | 105 | ;; --- Retrieve Project by share token 106 | 107 | (defn- get-project-by-share-token 108 | [conn token] 109 | (let [sqlv (sql/get-project-by-share-token {:token token}) 110 | project (some-> (sc/fetch-one conn sqlv) 111 | (data/normalize))] 112 | (when-let [id (:id project)] 113 | (let [pages (vec (pages/get-pages-for-project conn id))] 114 | (assoc project :pages pages))))) 115 | 116 | (defmethod core/query :retrieve-project-by-share-token 117 | [{:keys [token]}] 118 | (s/assert ::token token) 119 | (with-open [conn (db/connection)] 120 | (get-project-by-share-token conn token))) 121 | 122 | ;; --- Retrieve share tokens 123 | 124 | (defn get-share-tokens-for-project 125 | [conn project] 126 | (s/assert ::project project) 127 | (let [sqlv (sql/get-share-tokens-for-project {:project project})] 128 | (->> (sc/fetch conn sqlv) 129 | (map data/normalize)))) 130 | 131 | ;; Helpers 132 | 133 | (defn- decode-page-metadata 134 | [{:keys [page-metadata] :as result}] 135 | (merge result (when page-metadata 136 | {:page-metadata (-> page-metadata blob/decode t/decode)}))) 137 | 138 | (defn- decode-page-data 139 | [{:keys [page-data] :as result}] 140 | (merge result (when page-data 141 | {:page-data (-> page-data blob/decode t/decode)}))) 142 | 143 | 144 | -------------------------------------------------------------------------------- /src/uxbox/services/svgparse.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.services.svgparse 8 | (:require [clojure.spec :as s] 9 | [cuerdas.core :as str] 10 | [uxbox.util.spec :as us] 11 | [uxbox.services.core :as core] 12 | [uxbox.util.exceptions :as ex]) 13 | (:import org.jsoup.Jsoup 14 | java.io.InputStream)) 15 | 16 | (s/def ::content string?) 17 | (s/def ::width number?) 18 | (s/def ::height number?) 19 | (s/def ::name string?) 20 | (s/def ::view-box (s/coll-of number? :min-count 4 :max-count 4)) 21 | (s/def ::svg-entity (s/keys :req-un [::content ::width ::height ::view-box] 22 | :opt-un [::name])) 23 | 24 | ;; --- Implementation 25 | 26 | (defn- parse-double 27 | [data] 28 | {:pre [(string? data)]} 29 | (Double/parseDouble data)) 30 | 31 | (defn- parse-viewbox 32 | [data] 33 | {:pre [(string? data)]} 34 | (mapv parse-double (str/split data #"\s+"))) 35 | 36 | (defn- assoc-attr 37 | [acc attr] 38 | (let [key (.getKey attr) 39 | val (.getValue attr)] 40 | (case key 41 | "width" (assoc acc :width (parse-double val)) 42 | "height" (assoc acc :height (parse-double val)) 43 | "viewbox" (assoc acc :view-box (parse-viewbox val)) 44 | "sodipodi:docname" (assoc acc :name val) 45 | acc))) 46 | 47 | (defn- parse-attrs 48 | [element] 49 | (let [attrs (.attributes element)] 50 | (reduce assoc-attr {} attrs))) 51 | 52 | (defn- parse-svg 53 | [data] 54 | (try 55 | (let [document (Jsoup/parse data) 56 | svgelement (some-> (.body document) 57 | (.getElementsByTag "svg") 58 | (first)) 59 | innerxml (.html svgelement) 60 | attrs (parse-attrs svgelement)] 61 | (merge {:content innerxml} attrs)) 62 | (catch java.lang.IllegalArgumentException e 63 | (ex/raise :type :validation 64 | :code ::invalid-input 65 | :message "Input does not seems to be a valid svg.")) 66 | (catch java.lang.NullPointerException e 67 | (ex/raise :type :validation 68 | :code ::invalid-input 69 | :message "Input does not seems to be a valid svg.")) 70 | (catch Exception e 71 | (.printStackTrace e) 72 | (ex/raise :code ::unexpected)))) 73 | 74 | ;; --- Public Api 75 | 76 | (defn parse-string 77 | "Parse SVG from a string." 78 | [data] 79 | {:pre [(string? data)]} 80 | (let [result (parse-svg data)] 81 | (if (s/valid? ::svg-entity result) 82 | result 83 | (ex/raise :type :validation 84 | :code ::invalid-result 85 | :message "The result does not conform valid svg entity.")))) 86 | 87 | (defn parse 88 | [data] 89 | {:pre [(instance? InputStream data)]} 90 | (parse-string (slurp data))) 91 | 92 | (defmethod core/query :parse-svg 93 | [{:keys [data] :as params}] 94 | {:pre [(string? data)]} 95 | (parse-string data)) 96 | -------------------------------------------------------------------------------- /src/uxbox/sql.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.sql 8 | (:require [hugsql.core :as hugsql])) 9 | 10 | (hugsql/def-sqlvec-fns "sql/projects.sql" {:quoting :ansi :fn-suffix ""}) 11 | (hugsql/def-sqlvec-fns "sql/pages.sql" {:quoting :ansi :fn-suffix ""}) 12 | (hugsql/def-sqlvec-fns "sql/users.sql" {:quoting :ansi :fn-suffix ""}) 13 | (hugsql/def-sqlvec-fns "sql/emails.sql" {:quoting :ansi :fn-suffix ""}) 14 | (hugsql/def-sqlvec-fns "sql/images.sql" {:quoting :ansi :fn-suffix ""}) 15 | (hugsql/def-sqlvec-fns "sql/icons.sql" {:quoting :ansi :fn-suffix ""}) 16 | (hugsql/def-sqlvec-fns "sql/kvstore.sql" {:quoting :ansi :fn-suffix ""}) 17 | (hugsql/def-sqlvec-fns "sql/workers.sql" {:quoting :ansi :fn-suffix ""}) 18 | 19 | -------------------------------------------------------------------------------- /src/uxbox/util/blob.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.util.blob 8 | "A generic blob storage encoding. Mainly used for 9 | page data, page options and txlog payload storage." 10 | (:require [uxbox.util.snappy :as snappy])) 11 | 12 | (defn encode 13 | "Encode data into compressed blob." 14 | [data] 15 | (snappy/compress data)) 16 | 17 | (defn decode 18 | "Decode blob into string." 19 | [^bytes data] 20 | (snappy/uncompress data)) 21 | 22 | -------------------------------------------------------------------------------- /src/uxbox/util/cli.clj: -------------------------------------------------------------------------------- 1 | (ns uxbox.util.cli 2 | "Command line interface helpers.") 3 | 4 | (defn exit! 5 | ([] (exit! 0)) 6 | ([code] 7 | (System/exit code))) 8 | 9 | (defmacro print-err! 10 | [& args] 11 | `(binding [*out* *err*] 12 | (println ~@args))) 13 | -------------------------------------------------------------------------------- /src/uxbox/util/closeable.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.util.closeable 8 | "A closeable abstraction. A drop in replacement for 9 | clojure builtin `with-open` syntax abstraction." 10 | (:refer-clojure :exclude [with-open])) 11 | 12 | (defprotocol ICloseable 13 | (-close [_] "Close the resource.")) 14 | 15 | (defmacro with-open 16 | [bindings & body] 17 | {:pre [(vector? bindings) 18 | (even? (count bindings)) 19 | (pos? (count bindings))]} 20 | (reduce (fn [acc bindings] 21 | `(let ~(vec bindings) 22 | (try 23 | ~acc 24 | (finally 25 | (-close ~(first bindings)))))) 26 | `(do ~@body) 27 | (reverse (partition 2 bindings)))) 28 | 29 | (extend-protocol ICloseable 30 | java.lang.AutoCloseable 31 | (-close [this] (.close this))) 32 | -------------------------------------------------------------------------------- /src/uxbox/util/data.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.util.data 8 | "Data transformations utils." 9 | (:require [clojure.walk :as walk] 10 | [cuerdas.core :as str])) 11 | 12 | (defn dissoc-in 13 | [m [k & ks :as keys]] 14 | (if ks 15 | (if-let [nextmap (get m k)] 16 | (let [newmap (dissoc-in nextmap ks)] 17 | (if (seq newmap) 18 | (assoc m k newmap) 19 | (dissoc m k))) 20 | m) 21 | (dissoc m k))) 22 | 23 | (defn normalize-attrs 24 | "Recursively transforms all map keys from strings to keywords." 25 | [m] 26 | (letfn [(tf [[k v]] 27 | (let [ks (-> (name k) 28 | (str/replace "_" "-"))] 29 | [(keyword ks) v])) 30 | (walker [x] 31 | (if (map? x) 32 | (into {} (map tf) x) 33 | x))] 34 | (walk/postwalk walker m))) 35 | 36 | (defn strip-delete-attrs 37 | [m] 38 | (dissoc m :deleted-at)) 39 | 40 | (defn normalize 41 | "Perform a common normalization transformation 42 | for a entity (database retrieved) data structure." 43 | [m] 44 | (-> m normalize-attrs strip-delete-attrs)) 45 | 46 | (defn deep-merge 47 | [& maps] 48 | (letfn [(merge' [& maps] 49 | (if (every? map? maps) 50 | (apply merge-with merge' maps) 51 | (last maps)))] 52 | (apply merge' (remove nil? maps)))) 53 | -------------------------------------------------------------------------------- /src/uxbox/util/exceptions.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.util.exceptions 8 | "A helpers for work with exceptions.") 9 | 10 | (defn error 11 | [& {:keys [type code message] :or {type :unexpected} :as payload}] 12 | {:pre [(keyword? type) (keyword? code)]} 13 | (let [message (if message 14 | (str message " / " (pr-str code) "") 15 | (pr-str code)) 16 | payload (assoc payload :type type)] 17 | (ex-info message payload))) 18 | 19 | (defmacro raise 20 | [& args] 21 | `(throw (error ~@args))) 22 | -------------------------------------------------------------------------------- /src/uxbox/util/images.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.util.images 8 | "Images transformation utils." 9 | (:require [clojure.java.io :as io]) 10 | (:import org.im4java.core.IMOperation 11 | org.im4java.core.ConvertCmd 12 | org.im4java.process.Pipe 13 | java.io.ByteArrayInputStream 14 | java.io.ByteArrayOutputStream)) 15 | 16 | ;; Related info on how thumbnails generation 17 | ;; http://www.imagemagick.org/Usage/thumbnails/ 18 | 19 | (defn thumbnail 20 | ([input] (thumbnail input nil)) 21 | ([input {:keys [size quality format] 22 | :or {format "jpg" 23 | quality 92 24 | size [200 200]} 25 | :as opts}] 26 | {:pre [(vector? size)]} 27 | (with-open [out (ByteArrayOutputStream.) 28 | in (io/input-stream input)] 29 | (let [[width height] size 30 | pipe (Pipe. in out) 31 | op (doto (IMOperation.) 32 | (.addRawArgs ^java.util.List ["-"]) 33 | (.autoOrient) 34 | ;; (.thumbnail (int width) (int height) "^") 35 | ;; (.gravity "center") 36 | ;; (.extent (int width) (int height)) 37 | (.resize (int width) (int height) "^") 38 | (.quality (double quality)) 39 | (.addRawArgs ^java.util.List [(str format ":-")])) 40 | cmd (doto (ConvertCmd.) 41 | (.setInputProvider pipe) 42 | (.setOutputConsumer pipe))] 43 | (.run cmd op (make-array Object 0)) 44 | (ByteArrayInputStream. (.toByteArray out)))))) 45 | -------------------------------------------------------------------------------- /src/uxbox/util/quartz.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.util.quartz 8 | "A lightweight abstraction layer for quartz job scheduling library." 9 | (:import java.util.Properties 10 | org.quartz.Scheduler 11 | org.quartz.SchedulerException 12 | org.quartz.impl.StdSchedulerFactory 13 | org.quartz.Job 14 | org.quartz.JobBuilder 15 | org.quartz.JobDataMap 16 | org.quartz.JobExecutionContext 17 | org.quartz.TriggerBuilder 18 | org.quartz.CronScheduleBuilder 19 | org.quartz.SimpleScheduleBuilder 20 | org.quartz.PersistJobDataAfterExecution 21 | org.quartz.DisallowConcurrentExecution)) 22 | 23 | ;; --- Implementation 24 | 25 | (defn- map->props 26 | [data] 27 | (let [p (Properties.)] 28 | (run! (fn [[k v]] (.setProperty p (name k) (str v))) (seq data)) 29 | p)) 30 | 31 | (deftype JobImpl [] 32 | Job 33 | (execute [_ context] 34 | (let [^JobDataMap data (.. context getJobDetail getJobDataMap) 35 | args (.get data "arguments") 36 | state (.get data "state") 37 | callable (.get data "callable")] 38 | (if state 39 | (apply callable state args) 40 | (apply callable args))))) 41 | 42 | (defn- resolve-var 43 | [sym] 44 | (let [ns (symbol (namespace sym)) 45 | func (symbol (name sym))] 46 | (require ns) 47 | (resolve func))) 48 | 49 | (defn- build-trigger 50 | [opts] 51 | (let [repeat? (::repeat? opts true) 52 | interval (::interval opts 1000) 53 | cron (::cron opts) 54 | group (::group opts "uxbox") 55 | schdl (if cron 56 | (CronScheduleBuilder/cronSchedule cron) 57 | (let [schdl (SimpleScheduleBuilder/simpleSchedule) 58 | schdl (if (number? repeat?) 59 | (.withRepeatCount schdl repeat?) 60 | (.repeatForever schdl))] 61 | (.withIntervalInMilliseconds schdl interval))) 62 | name (str (:name opts) "-trigger") 63 | bldr (doto (TriggerBuilder/newTrigger) 64 | (.startNow) 65 | (.withIdentity name group) 66 | (.withSchedule schdl))] 67 | (.build bldr))) 68 | 69 | (defn- build-job-detail 70 | [fvar args] 71 | (let [opts (meta fvar) 72 | state (::state opts) 73 | group (::group opts "uxbox") 74 | name (str (:name opts)) 75 | data {"callable" @fvar 76 | "arguments" (into [] args) 77 | "state" (if state (atom state) nil)} 78 | bldr (doto (JobBuilder/newJob JobImpl) 79 | (.storeDurably false) 80 | (.usingJobData (JobDataMap. data)) 81 | (.withIdentity name group))] 82 | (.build bldr))) 83 | 84 | (defn- make-scheduler-props 85 | [{:keys [name daemon? threads thread-priority] 86 | :or {name "uxbox-scheduler" 87 | daemon? true 88 | threads 1 89 | thread-priority Thread/MIN_PRIORITY}}] 90 | (map->props 91 | {"org.quartz.threadPool.threadCount" threads 92 | "org.quartz.threadPool.threadPriority" thread-priority 93 | "org.quartz.threadPool.makeThreadsDaemons" (if daemon? "true" "false") 94 | "org.quartz.scheduler.instanceName" name 95 | "org.quartz.scheduler.makeSchedulerThreadDaemon" (if daemon? "true" "false")})) 96 | 97 | ;; --- Public Api 98 | 99 | (defn scheduler 100 | "Create a new scheduler instance." 101 | ([] (scheduler nil)) 102 | ([opts] 103 | (let [props (make-scheduler-props opts) 104 | factory (StdSchedulerFactory. props)] 105 | (.getScheduler factory)))) 106 | 107 | (declare schedule!) 108 | 109 | (defn start! 110 | ([schd] 111 | (start! schd nil)) 112 | ([schd {:keys [delay search-on]}] 113 | ;; Start the scheduler 114 | (if (number? delay) 115 | (.startDelayed schd (int delay)) 116 | (.start schd)) 117 | 118 | (when (coll? search-on) 119 | (run! (fn [ns] 120 | (require ns) 121 | (doseq [v (vals (ns-publics ns))] 122 | (when (::job (meta v)) 123 | (schedule! schd v)))) 124 | search-on)) 125 | schd)) 126 | 127 | (defn stop! 128 | [scheduler] 129 | (.shutdown ^Scheduler scheduler true)) 130 | 131 | ;; TODO: add proper handling of `:delay` option that should allow 132 | ;; execute a task firstly delayed until some milliseconds or at certain time. 133 | 134 | (defn schedule! 135 | [schd f & args] 136 | (let [vf (if (symbol? f) (resolve-var f) f) 137 | job (build-job-detail vf args) 138 | trigger (build-trigger (meta vf))] 139 | (.scheduleJob ^Scheduler schd job trigger))) 140 | -------------------------------------------------------------------------------- /src/uxbox/util/response.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.util.response 8 | "A lightweigt reponse type definition. 9 | 10 | At first instance it allows set the appropriate 11 | content-type headers and encode the body using 12 | the builtin transit abstraction. 13 | 14 | In future it will allow easy adapt for the content 15 | negotiation that is coming to catacumba." 16 | (:require [catacumba.impl.handlers :as ch] 17 | [catacumba.impl.context :as ctx] 18 | [buddy.core.hash :as hash] 19 | [buddy.core.codecs :as codecs] 20 | [buddy.core.codecs.base64 :as b64] 21 | [uxbox.util.transit :as t]) 22 | (:import ratpack.handling.Context 23 | ratpack.http.Response 24 | ratpack.http.Request 25 | ratpack.http.Headers 26 | ratpack.http.MutableHeaders)) 27 | 28 | (defn digest 29 | [^bytes data] 30 | (-> (hash/blake2b-256 data) 31 | (b64/encode true) 32 | (codecs/bytes->str))) 33 | 34 | (defn- etag-match? 35 | [^Request request ^String new-tag] 36 | (let [^Headers headers (.getHeaders request)] 37 | (when-let [etag (.get headers "if-none-match")] 38 | (= etag new-tag)))) 39 | 40 | (deftype Rsp [data] 41 | ch/ISend 42 | (-send [_ ctx] 43 | (let [^Response response (ctx/get-response* ctx) 44 | ^Request request (ctx/get-request* ctx) 45 | ^MutableHeaders headers (.getHeaders response) 46 | ^String method (.. request getMethod getName toLowerCase) 47 | data (t/encode data)] 48 | (if (= method "get") 49 | (let [etag (digest data)] 50 | (if (etag-match? request etag) 51 | (do 52 | (.set headers "etag" etag) 53 | (.status response 304) 54 | (.send response)) 55 | (do 56 | (.set headers "content-type" "application/transit+json") 57 | (.set headers "etag" etag) 58 | (ch/-send data ctx)))) 59 | (do 60 | (.set headers "content-type" "application/transit+json") 61 | (ch/-send data ctx)))))) 62 | 63 | (defn rsp 64 | "A shortcut for create a response instance." 65 | [data] 66 | (Rsp. data)) 67 | -------------------------------------------------------------------------------- /src/uxbox/util/snappy.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.util.snappy 8 | "A lightweight abstraction layer for snappy compression library." 9 | (:require [buddy.core.codecs :as codecs]) 10 | (:import org.xerial.snappy.Snappy 11 | org.xerial.snappy.SnappyFramedInputStream 12 | org.xerial.snappy.SnappyFramedOutputStream 13 | 14 | java.io.OutputStream 15 | java.io.InputStream)) 16 | 17 | 18 | (defn compress 19 | "Compress data unsing snappy compression algorithm." 20 | [data] 21 | (-> (codecs/to-bytes data) 22 | (Snappy/compress))) 23 | 24 | (defn uncompress 25 | "Uncompress data using snappy compression algorithm." 26 | [data] 27 | (-> (codecs/to-bytes data) 28 | (Snappy/uncompress))) 29 | 30 | (defn input-stream 31 | "Create a Snappy framed input stream." 32 | [^InputStream istream] 33 | (SnappyFramedInputStream. istream)) 34 | 35 | (defn output-stream 36 | "Create a Snappy framed output stream." 37 | ([ostream] 38 | (output-stream ostream nil)) 39 | ([^OutputStream ostream {:keys [block-size] :or {block-size 65536}}] 40 | (SnappyFramedOutputStream. ostream (int block-size) 1.0))) 41 | -------------------------------------------------------------------------------- /src/uxbox/util/spec.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.util.spec 8 | (:refer-clojure :exclude [keyword uuid vector boolean map set]) 9 | (:require [clojure.spec :as s] 10 | [cuerdas.core :as str] 11 | [uxbox.util.exceptions :as ex]) 12 | (:import java.time.Instant)) 13 | 14 | ;; --- Constants 15 | 16 | (def email-rx 17 | #"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$") 18 | 19 | (def uuid-rx 20 | #"^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$") 21 | 22 | ;; --- Public Api 23 | 24 | (defn conform 25 | [spec data] 26 | (let [result (s/conform spec data)] 27 | (if (= result ::s/invalid) 28 | (ex/raise :type :validation 29 | :code ::invalid 30 | :message (s/explain-str spec data) 31 | :context (s/explain-data spec data)) 32 | result))) 33 | 34 | ;; --- Predicates 35 | 36 | (defn email? 37 | [v] 38 | (and string? 39 | (re-matches email-rx v))) 40 | 41 | (defn instant? 42 | [v] 43 | (instance? Instant v)) 44 | 45 | (defn path? 46 | [v] 47 | (instance? java.nio.file.Path v)) 48 | 49 | (defn regex? 50 | [v] 51 | (instance? java.util.regex.Pattern v)) 52 | 53 | ;; --- Conformers 54 | 55 | (defn- uuid-conformer 56 | [v] 57 | (cond 58 | (uuid? v) v 59 | (string? v) 60 | (cond 61 | (re-matches uuid-rx v) 62 | (java.util.UUID/fromString v) 63 | 64 | (str/empty? v) 65 | nil 66 | 67 | :else 68 | ::s/invalid) 69 | :else ::s/invalid)) 70 | 71 | (defn- integer-conformer 72 | [v] 73 | (cond 74 | (integer? v) v 75 | (string? v) 76 | (if (re-matches #"^[-+]?\d+$" v) 77 | (Long/parseLong v) 78 | ::s/invalid) 79 | :else ::s/invalid)) 80 | 81 | (defn boolean-conformer 82 | [v] 83 | (cond 84 | (boolean? v) v 85 | (string? v) 86 | (if (re-matches #"^(?:t|true|false|f|0|1)$" v) 87 | (contains? #{"t" "true" "1"} v) 88 | ::s/invalid) 89 | :else ::s/invalid)) 90 | 91 | (defn boolean-unformer 92 | [v] 93 | (if v "true" "false")) 94 | 95 | ;; --- Default Specs 96 | 97 | (s/def ::integer-string (s/conformer integer-conformer str)) 98 | (s/def ::uuid-string (s/conformer uuid-conformer str)) 99 | (s/def ::boolean-string (s/conformer boolean-conformer boolean-unformer)) 100 | (s/def ::positive-integer #(< 0 % Long/MAX_VALUE)) 101 | (s/def ::uploaded-file #(instance? ratpack.form.UploadedFile %)) 102 | (s/def ::uuid uuid?) 103 | (s/def ::bytes bytes?) 104 | (s/def ::path path?) 105 | 106 | (s/def ::id ::uuid-string) 107 | (s/def ::name string?) 108 | (s/def ::username string?) 109 | (s/def ::password string?) 110 | (s/def ::version integer?) 111 | (s/def ::email email?) 112 | (s/def ::token string?) 113 | 114 | -------------------------------------------------------------------------------- /src/uxbox/util/tempfile.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.util.tempfile 8 | "A temporal file abstractions." 9 | (:require [storages.core :as st] 10 | [storages.util :as path]) 11 | (:import [java.nio.file Files])) 12 | 13 | (defn create 14 | "Create a temporal file." 15 | [& {:keys [suffix prefix]}] 16 | (->> (path/make-file-attrs "rwxr-xr-x") 17 | (Files/createTempFile prefix suffix))) 18 | -------------------------------------------------------------------------------- /src/uxbox/util/template.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.util.template 8 | "A lightweight abstraction over mustache.java template engine. 9 | The documentation can be found: http://mustache.github.io/mustache.5.html" 10 | (:require [clojure.walk :as walk] 11 | [clojure.java.io :as io]) 12 | (:import java.io.StringReader 13 | java.io.StringWriter 14 | java.util.HashMap 15 | com.github.mustachejava.DefaultMustacheFactory 16 | com.github.mustachejava.Mustache)) 17 | 18 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 19 | ;; Impl 20 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 21 | 22 | (def ^:private 23 | ^DefaultMustacheFactory 24 | +mustache-factory+ (DefaultMustacheFactory.)) 25 | 26 | (defprotocol ITemplate 27 | "A basic template rendering abstraction." 28 | (-render [template context])) 29 | 30 | (extend-type Mustache 31 | ITemplate 32 | (-render [template context] 33 | (with-out-str 34 | (let [scope (HashMap. (walk/stringify-keys context))] 35 | (.execute template *out* scope))))) 36 | 37 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 38 | ;; Public Api 39 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 40 | 41 | (defn render-string 42 | "Render string as mustache template." 43 | ([^String template] 44 | (render-string template {})) 45 | ([^String template context] 46 | (let [reader (StringReader. template) 47 | template (.compile +mustache-factory+ reader "example")] 48 | (-render template context)))) 49 | 50 | (defn render 51 | "Load a file from the class path and render 52 | it using mustache template." 53 | ([^String path] 54 | (render path {})) 55 | ([^String path context] 56 | (render-string (slurp (io/resource path)) context))) 57 | -------------------------------------------------------------------------------- /src/uxbox/util/time.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.util.time 8 | (:require [suricatta.proto :as proto] 9 | [cognitect.transit :as t]) 10 | (:import java.time.Instant 11 | java.sql.Timestamp)) 12 | 13 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 14 | ;; Serialization Layer conversions 15 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 16 | 17 | (declare from-string) 18 | 19 | (def ^:private write-handler 20 | (t/write-handler 21 | (constantly "m") 22 | (fn [v] (str (.toEpochMilli v))))) 23 | 24 | (def ^:private read-handler 25 | (t/read-handler 26 | (fn [v] (-> (Long/parseLong v) 27 | (Instant/ofEpochMilli))))) 28 | 29 | (def +read-handlers+ 30 | {"m" read-handler}) 31 | 32 | (def +write-handlers+ 33 | {Instant write-handler}) 34 | 35 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 36 | ;; Persistence Layer Conversions 37 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 38 | 39 | (extend-protocol proto/IParamType 40 | Instant 41 | (-render [self ctx] 42 | (if (proto/-inline? ctx) 43 | (str "'" (.toString self) "'::timestamptz") 44 | "?::timestamptz")) 45 | 46 | (-bind [self ctx] 47 | (when-not (proto/-inline? ctx) 48 | (let [stmt (proto/-statement ctx) 49 | idx (proto/-next-bind-index ctx) 50 | obj (Timestamp/from self)] 51 | (.setTimestamp stmt idx obj))))) 52 | 53 | (extend-protocol proto/ISQLType 54 | Timestamp 55 | (-convert [self] 56 | (.toInstant self))) 57 | 58 | (defmethod print-method Instant 59 | [mv ^java.io.Writer writer] 60 | (.write writer (str "#instant \"" (.toString mv) "\""))) 61 | 62 | (defmethod print-dup Instant [o w] 63 | (print-method o w)) 64 | 65 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 66 | ;; Helpers 67 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 68 | 69 | (defn from-string 70 | [s] 71 | {:pre [(string? s)]} 72 | (Instant/parse s)) 73 | 74 | (defn now 75 | [] 76 | (Instant/now)) 77 | -------------------------------------------------------------------------------- /src/uxbox/util/token.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.util.token 8 | "Facilities for generate random tokens." 9 | (:require [buddy.core.nonce :as nonce] 10 | [buddy.core.hash :as hash] 11 | [buddy.core.codecs :as codecs] 12 | [buddy.core.codecs.base64 :as b64])) 13 | 14 | (defn random 15 | "Returns a 32 bytes randomly generated token 16 | with 1024 random seed. The output is encoded 17 | using urlsafe variant of base64." 18 | [] 19 | (-> (nonce/random-bytes 1024) 20 | (hash/blake2b-256) 21 | (b64/encode true) 22 | (codecs/bytes->str))) 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/uxbox/util/transit.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.util.transit 8 | (:require [cognitect.transit :as t] 9 | [catacumba.handlers.parse :as cparse] 10 | [uxbox.util.time :as dt]) 11 | (:import ratpack.http.TypedData 12 | ratpack.handling.Context 13 | java.io.ByteArrayInputStream 14 | java.io.ByteArrayOutputStream)) 15 | 16 | ;; --- Handlers 17 | 18 | (def ^:private +reader-handlers+ 19 | dt/+read-handlers+) 20 | 21 | (def ^:private +write-handlers+ 22 | dt/+write-handlers+) 23 | 24 | ;; --- Low-Level Api 25 | 26 | (defn reader 27 | ([istream] 28 | (reader istream nil)) 29 | ([istream {:keys [type] :or {type :json}}] 30 | (t/reader istream type {:handlers +reader-handlers+}))) 31 | 32 | (defn read! 33 | "Read value from streamed transit reader." 34 | [reader] 35 | (t/read reader)) 36 | 37 | (defn writer 38 | ([ostream] 39 | (writer ostream nil)) 40 | ([ostream {:keys [type] :or {type :json}}] 41 | (t/writer ostream type {:handlers +write-handlers+}))) 42 | 43 | (defn write! 44 | [writer data] 45 | (t/write writer data)) 46 | 47 | 48 | ;; --- Catacumba Extension 49 | 50 | (defmethod cparse/parse-body :application/transit+json 51 | [^Context ctx ^TypedData body] 52 | (let [reader (reader (.getInputStream body) {:type :json})] 53 | (read! reader))) 54 | 55 | ;; --- High-Level Api 56 | 57 | (defn decode 58 | ([data] 59 | (decode data nil)) 60 | ([data opts] 61 | (with-open [input (ByteArrayInputStream. data)] 62 | (read! (reader input opts))))) 63 | 64 | (defn encode 65 | ([data] 66 | (encode data nil)) 67 | ([data opts] 68 | (with-open [out (ByteArrayOutputStream.)] 69 | (let [w (writer out opts)] 70 | (write! w data) 71 | (.toByteArray out))))) 72 | -------------------------------------------------------------------------------- /src/uxbox/util/uuid.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.util.uuid 8 | (:require [clj-uuid :as uuid]) 9 | (:import java.util.UUID)) 10 | 11 | (def ^:const zero uuid/+null+) 12 | 13 | (def random 14 | "Alias for clj-uuid/v4." 15 | uuid/v4) 16 | 17 | (defn namespaced 18 | [ns data] 19 | (uuid/v5 ns data)) 20 | 21 | (defn from-string 22 | "Parse string uuid representation into proper UUID instance." 23 | [s] 24 | (UUID/fromString s)) 25 | 26 | -------------------------------------------------------------------------------- /src/uxbox/util/workers.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns uxbox.util.workers 8 | "A distributed asynchronous tasks queue implementation on top 9 | of PostgreSQL reliable advirsory locking mechanism." 10 | (:require [suricatta.core :as sc] 11 | [uxbox.db :as db] 12 | [uxbox.sql :as sql])) 13 | 14 | (defn- poll-for-task 15 | [conn queue] 16 | (let [sql (sql/acquire-task {:queue queue})] 17 | (sc/fetch-one conn sql))) 18 | 19 | (defn- mark-task-done 20 | [conn {:keys [id]}] 21 | (let [sql (sql/mark-task-done {:id id})] 22 | (sc/execute conn sql))) 23 | 24 | (defn- mark-task-failed 25 | [conn {:keys [id]} error] 26 | (let [sql (sql/mark-task-done {:id id :error (.getMessage error)})] 27 | (sc/execute conn sql))) 28 | 29 | (defn- watch-unit 30 | [conn queue callback] 31 | (let [task (poll-for-task conn queue)] 32 | (if (nil? task) 33 | (Thread/sleep 1000) 34 | (try 35 | (sc/atomic conn 36 | (callback conn task) 37 | (mark-task-done conn task)) 38 | (catch Exception e 39 | (mark-task-failed conn task e)))))) 40 | 41 | (defn- watch-loop 42 | "Watch tasks on the specified queue and executes a 43 | callback for each task is received. 44 | NOTE: This function blocks the current thread." 45 | [queue callback] 46 | (try 47 | (loop [] 48 | (with-open [conn (db/connection)] 49 | (sc/atomic conn (watch-unit conn queue callback))) 50 | (recur)) 51 | (catch InterruptedException e 52 | ;; just ignoring 53 | ))) 54 | 55 | (defn watch! 56 | [queue callback] 57 | (let [runnable #(watch-loop queue callback) 58 | thread (Thread. ^Runnable runnable)] 59 | (.setDaemon thread true) 60 | (.start thread) 61 | (reify 62 | java.lang.AutoCloseable 63 | (close [_] 64 | (.interrupt thread) 65 | (.join thread 2000)) 66 | 67 | clojure.lang.IDeref 68 | (deref [_] 69 | (.join thread)) 70 | 71 | clojure.lang.IBlockingDeref 72 | (deref [_ ms default] 73 | (.join thread ms) 74 | default)))) 75 | -------------------------------------------------------------------------------- /test/storages/tests.clj: -------------------------------------------------------------------------------- 1 | (ns storages.tests 2 | (:require [clojure.test :as t] 3 | [storages.core :as st] 4 | [storages.fs.local :as fs] 5 | [storages.fs.misc :as misc]) 6 | (:import java.io.File 7 | org.apache.commons.io.FileUtils)) 8 | 9 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 10 | ;; Test Fixtures 11 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 12 | 13 | (defn- clean-temp-directory 14 | [next] 15 | (next) 16 | (let [directory (File. "/tmp/catacumba/")] 17 | (FileUtils/deleteDirectory directory))) 18 | 19 | (t/use-fixtures :each clean-temp-directory) 20 | 21 | ;; --- Tests: FileSystemStorage 22 | 23 | (t/deftest test-localfs-store-and-lookup 24 | (let [storage (fs/filesystem {:basedir "/tmp/catacumba/test" 25 | :baseuri "http://localhost:5050/"}) 26 | rpath @(st/save storage "test.txt" "my content") 27 | fpath @(st/lookup storage rpath) 28 | fdata (slurp fpath)] 29 | (t/is (= (str fpath) "/tmp/catacumba/test/test.txt")) 30 | (t/is (= "my content" fdata)))) 31 | 32 | (t/deftest test-localfs-store-and-get-public-url 33 | (let [storage (fs/filesystem {:basedir "/tmp/catacumba/test" 34 | :baseuri "http://localhost:5050/"}) 35 | rpath @(st/save storage "test.txt" "my content") 36 | ruri (st/public-url storage rpath)] 37 | (t/is (= (str ruri) "http://localhost:5050/test.txt")))) 38 | 39 | (t/deftest test-localfs-store-and-lookup-with-subdirs 40 | (let [storage (fs/filesystem {:basedir "/tmp/catacumba/test" 41 | :baseuri "http://localhost:5050/"}) 42 | rpath @(st/save storage "somepath/test.txt" "my content") 43 | fpath @(st/lookup storage rpath) 44 | fdata (slurp fpath)] 45 | (t/is (= (str fpath) "/tmp/catacumba/test/somepath/test.txt")) 46 | (t/is (= "my content" fdata)))) 47 | 48 | (t/deftest test-localfs-store-and-delete-and-check 49 | (let [storage (fs/filesystem {:basedir "/tmp/catacumba/test" 50 | :baseuri "http://localhost:5050/"}) 51 | rpath @(st/save storage "test.txt" "my content")] 52 | (t/is @(st/delete storage rpath)) 53 | (t/is (not @(st/exists? storage rpath))))) 54 | 55 | (t/deftest test-localfs-store-duplicate-file-raises-exception 56 | (let [storage (fs/filesystem {:basedir "/tmp/catacumba/test" 57 | :baseuri "http://localhost:5050/"})] 58 | (t/is @(st/save storage "test.txt" "my content")) 59 | (t/is (thrown? java.util.concurrent.ExecutionException 60 | @(st/save storage "test.txt" "my content"))))) 61 | 62 | (t/deftest test-localfs-access-unauthorized-path 63 | (let [storage (fs/filesystem {:basedir "/tmp/catacumba/test" 64 | :baseuri "http://localhost:5050/"})] 65 | (t/is (thrown? java.util.concurrent.ExecutionException 66 | @(st/lookup storage "../test.txt"))) 67 | (t/is (thrown? java.util.concurrent.ExecutionException 68 | @(st/lookup storage "/test.txt"))))) 69 | 70 | ;; --- Tests: ScopedPathStorage 71 | 72 | (t/deftest test-localfs-scoped-store-and-lookup 73 | (let [storage (fs/filesystem {:basedir "/tmp/catacumba/test" 74 | :baseuri "http://localhost:5050/"}) 75 | storage (misc/scoped storage "some/prefix") 76 | rpath @(st/save storage "test.txt" "my content") 77 | fpath @(st/lookup storage rpath) 78 | fdata (slurp fpath)] 79 | (t/is (= (str fpath) "/tmp/catacumba/test/some/prefix/test.txt")) 80 | (t/is (= "my content" fdata)))) 81 | 82 | (t/deftest test-localfs-scoped-store-and-delete-and-check 83 | (let [storage (fs/filesystem {:basedir "/tmp/catacumba/test" 84 | :baseuri "http://localhost:5050/"}) 85 | storage (misc/scoped storage "some/prefix") 86 | rpath @(st/save storage "test.txt" "my content")] 87 | (t/is @(st/delete storage rpath)) 88 | (t/is (not @(st/exists? storage rpath))))) 89 | 90 | (t/deftest test-localfs-scoped-store-duplicate-file-raises-exception 91 | (let [storage (fs/filesystem {:basedir "/tmp/catacumba/test" 92 | :baseuri "http://localhost:5050/"}) 93 | storage (misc/scoped storage "some/prefix")] 94 | (t/is @(st/save storage "test.txt" "my content")) 95 | (t/is (thrown? java.util.concurrent.ExecutionException 96 | @(st/save storage "test.txt" "my content"))))) 97 | 98 | (t/deftest test-localfs-scoped-access-unauthorized-path 99 | (let [storage (fs/filesystem {:basedir "/tmp/catacumba/test" 100 | :baseuri "http://localhost:5050/"}) 101 | storage (misc/scoped storage "some/prefix")] 102 | (t/is (thrown? java.util.concurrent.ExecutionException 103 | @(st/lookup storage "../test.txt"))) 104 | (t/is (thrown? java.util.concurrent.ExecutionException 105 | @(st/lookup storage "/test.txt"))))) 106 | 107 | -------------------------------------------------------------------------------- /test/uxbox/tests/_files/sample.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uxbox/uxbox-backend/036c42db8424be3ac34c38be80577ee279141681/test/uxbox/tests/_files/sample.jpg -------------------------------------------------------------------------------- /test/uxbox/tests/_files/sample1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | image/svg+xml 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/uxbox/tests/_files/sample2.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 30 | 50 | 54 | 55 | -------------------------------------------------------------------------------- /test/uxbox/tests/test_auth.clj: -------------------------------------------------------------------------------- 1 | (ns uxbox.tests.test-auth 2 | (:require [clojure.test :as t] 3 | [promesa.core :as p] 4 | [clj-http.client :as http] 5 | [catacumba.testing :refer (with-server)] 6 | [buddy.hashers :as hashers] 7 | [uxbox.db :as db] 8 | [uxbox.frontend :as uft] 9 | [uxbox.services.users :as usu] 10 | [uxbox.services :as usv] 11 | [uxbox.tests.helpers :as th])) 12 | 13 | (t/use-fixtures :each th/database-reset) 14 | 15 | (t/deftest test-http-success-auth 16 | (let [data {:username "user1" 17 | :fullname "user 1" 18 | :metadata "1" 19 | :password "user1" 20 | :email "user1@uxbox.io"} 21 | user (with-open [conn (db/connection)] 22 | (usu/create-user conn data))] 23 | (with-server {:handler (uft/routes)} 24 | (let [data {:username "user1" 25 | :password "user1" 26 | :metadata "1" 27 | :scope "foobar"} 28 | uri (str th/+base-url+ "/api/auth/token") 29 | [status data] (th/http-post uri {:body data})] 30 | ;; (println "RESPONSE:" status data) 31 | (t/is (= status 200)) 32 | (t/is (contains? data :token)))))) 33 | 34 | (t/deftest test-http-failed-auth 35 | (let [data {:username "user1" 36 | :fullname "user 1" 37 | :metadata "1" 38 | :password (hashers/encrypt "user1") 39 | :email "user1@uxbox.io"} 40 | user (with-open [conn (db/connection)] 41 | (usu/create-user conn data))] 42 | (with-server {:handler (uft/routes)} 43 | (let [data {:username "user1" 44 | :password "user2" 45 | :metadata "2" 46 | :scope "foobar"} 47 | uri (str th/+base-url+ "/api/auth/token") 48 | [status data] (th/http-post uri {:body data})] 49 | ;; (println "RESPONSE:" status data) 50 | (t/is (= 400 status)) 51 | (t/is (= (:type data) :validation)) 52 | (t/is (= (:code data) :uxbox.services.auth/wrong-credentials)))))) 53 | 54 | -------------------------------------------------------------------------------- /test/uxbox/tests/test_kvstore.clj: -------------------------------------------------------------------------------- 1 | (ns uxbox.tests.test-kvstore 2 | (:require [clojure.test :as t] 3 | [promesa.core :as p] 4 | [suricatta.core :as sc] 5 | [catacumba.testing :refer (with-server)] 6 | [buddy.core.codecs :as codecs] 7 | [uxbox.db :as db] 8 | [uxbox.util.uuid :as uuid] 9 | [uxbox.frontend :as uft] 10 | [uxbox.services.kvstore :as kvs] 11 | [uxbox.tests.helpers :as th])) 12 | 13 | (t/use-fixtures :each th/database-reset) 14 | 15 | (t/deftest test-http-kvstore 16 | (with-open [conn (db/connection)] 17 | (let [{:keys [id] :as user} (th/create-user conn 1)] 18 | 19 | ;; Not exists at this moment 20 | (t/is (nil? (kvs/retrieve-kvstore conn {:user id :key "foo" :version -1}))) 21 | 22 | ;; Creating new one should work as expected 23 | (with-server {:handler (uft/routes)} 24 | (let [uri (str th/+base-url+ "/api/kvstore") 25 | body {:key "foo" :value "bar" :version -1} 26 | params {:body body} 27 | [status data] (th/http-put user uri params)] 28 | (println "RESPONSE:" status data) 29 | (t/is (= 200 status)) 30 | (t/is (= (:key data) "foo")) 31 | (t/is (= (:value data) "bar")))) 32 | 33 | ;; Should exists 34 | (let [data (kvs/retrieve-kvstore conn {:user id :key "foo"})] 35 | (t/is (= (:key data) "foo")) 36 | (t/is (= (:value data) "bar")) 37 | 38 | ;; Overwriting should work 39 | (with-server {:handler (uft/routes)} 40 | (let [uri (str th/+base-url+ "/api/kvstore") 41 | body (assoc data :key "foo" :value "baz") 42 | params {:body body} 43 | [status data] (th/http-put user uri params)] 44 | (println "RESPONSE:" status data) 45 | (t/is (= 200 status)) 46 | (t/is (= (:key data) "foo")) 47 | (t/is (= (:value data) "baz"))))) 48 | 49 | ;; Should exists and match the overwritten value 50 | (let [data (kvs/retrieve-kvstore conn {:user id :key "foo"})] 51 | (t/is (= (:key data) "foo")) 52 | (t/is (= (:value data) "baz"))) 53 | 54 | ;; Delete should work 55 | (with-server {:handler (uft/routes)} 56 | (let [uri (str th/+base-url+ "/api/kvstore/foo") 57 | [status data] (th/http-delete user uri)] 58 | (println "RESPONSE:" status data) 59 | (t/is (= 204 status)))) 60 | 61 | ;; Not exists at this moment 62 | (t/is (nil? (kvs/retrieve-kvstore conn {:user id :key "foo"}))) 63 | 64 | ))) 65 | 66 | -------------------------------------------------------------------------------- /test/uxbox/tests/test_projects.clj: -------------------------------------------------------------------------------- 1 | (ns uxbox.tests.test-projects 2 | (:require [clojure.test :as t] 3 | [promesa.core :as p] 4 | [suricatta.core :as sc] 5 | [clj-uuid :as uuid] 6 | [catacumba.testing :refer (with-server)] 7 | [catacumba.serializers :as sz] 8 | [uxbox.db :as db] 9 | [uxbox.frontend :as uft] 10 | [uxbox.services.projects :as uspr] 11 | [uxbox.services.pages :as uspg] 12 | [uxbox.services :as usv] 13 | [uxbox.tests.helpers :as th])) 14 | 15 | (t/use-fixtures :each th/database-reset) 16 | 17 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 18 | ;; Frontend Test 19 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 20 | 21 | (t/deftest test-http-project-list 22 | (with-open [conn (db/connection)] 23 | (let [user (th/create-user conn 1) 24 | proj (uspr/create-project conn {:user (:id user) :name "proj1"})] 25 | (with-server {:handler (uft/routes)} 26 | (let [uri (str th/+base-url+ "/api/projects") 27 | [status data] (th/http-get user uri)] 28 | ;; (println "RESPONSE:" status data) 29 | (t/is (= 200 status)) 30 | (t/is (= 1 (count data)))))))) 31 | 32 | (t/deftest test-http-project-create 33 | (with-open [conn (db/connection)] 34 | (let [user (th/create-user conn 1)] 35 | (with-server {:handler (uft/routes)} 36 | (let [uri (str th/+base-url+ "/api/projects") 37 | params {:body {:name "proj1"}} 38 | [status data] (th/http-post user uri params)] 39 | ;; (println "RESPONSE:" status data) 40 | (t/is (= 201 status)) 41 | (t/is (= (:user data) (:id user))) 42 | (t/is (= (:name data) "proj1"))))))) 43 | 44 | (t/deftest test-http-project-update 45 | (with-open [conn (db/connection)] 46 | (let [user (th/create-user conn 1) 47 | proj (uspr/create-project conn {:user (:id user) :name "proj1"})] 48 | (with-server {:handler (uft/routes)} 49 | (let [uri (str th/+base-url+ "/api/projects/" (:id proj)) 50 | params {:body (assoc proj :name "proj2")} 51 | [status data] (th/http-put user uri params)] 52 | ;; (println "RESPONSE:" status data) 53 | (t/is (= 200 status)) 54 | (t/is (= (:user data) (:id user))) 55 | (t/is (= (:name data) "proj2"))))))) 56 | 57 | (t/deftest test-http-project-delete 58 | (with-open [conn (db/connection)] 59 | (let [user (th/create-user conn 1) 60 | proj (uspr/create-project conn {:user (:id user) :name "proj1"})] 61 | (with-server {:handler (uft/routes)} 62 | (let [uri (str th/+base-url+ "/api/projects/" (:id proj)) 63 | [status data] (th/http-delete user uri)] 64 | (t/is (= 204 status)) 65 | (let [sqlv ["SELECT * FROM projects WHERE \"user\"=? AND deleted_at is null" 66 | (:id user)] 67 | result (sc/fetch conn sqlv)] 68 | (t/is (empty? result)))))))) 69 | 70 | (t/deftest test-http-project-retrieve-by-share-token 71 | (with-open [conn (db/connection)] 72 | (let [user (th/create-user conn 1) 73 | proj (uspr/create-project conn {:user (:id user) :name "proj1"}) 74 | page (uspg/create-page conn {:id (uuid/v4) 75 | :user (:id user) 76 | :project (:id proj) 77 | :version 0 78 | :data "1" 79 | :options "2" 80 | :name "page1" 81 | :width 200 82 | :height 200 83 | :layout "mobil"}) 84 | shares (uspr/get-share-tokens-for-project conn (:id proj))] 85 | (with-server {:handler (uft/routes)} 86 | (let [token (:token (first shares)) 87 | uri (str th/+base-url+ "/api/projects-by-token/" token) 88 | [status data] (th/http-get user uri)] 89 | ;; (println "RESPONSE:" status data) 90 | (t/is (= status 200)) 91 | (t/is (vector? (:pages data))) 92 | (t/is (= 1 (count (:pages data))))))))) 93 | -------------------------------------------------------------------------------- /test/uxbox/tests/test_svgparse.clj: -------------------------------------------------------------------------------- 1 | (ns uxbox.tests.test-svgparse 2 | (:require [clojure.test :as t] 3 | [clojure.java.io :as io] 4 | [catacumba.testing :refer [with-server]] 5 | [uxbox.frontend :as uft] 6 | [uxbox.services :as usv] 7 | [uxbox.services.svgparse :as svg] 8 | [uxbox.tests.helpers :as th])) 9 | 10 | (t/use-fixtures :each th/state-init) 11 | 12 | (t/deftest parse-svg-test 13 | (t/testing "parsing valid svg 1" 14 | (let [image (slurp (io/resource "uxbox/tests/_files/sample1.svg")) 15 | result (svg/parse-string image)] 16 | (t/is (contains? result :width)) 17 | (t/is (contains? result :height)) 18 | (t/is (contains? result :view-box)) 19 | (t/is (contains? result :name)) 20 | (t/is (contains? result :content)) 21 | (t/is (= 500.0 (:width result))) 22 | (t/is (= 500.0 (:height result))) 23 | (t/is (= [0.0 0.0 500.00001 500.00001] (:view-box result))) 24 | (t/is (= "lock.svg" (:name result))))) 25 | 26 | (t/testing "parsing valid svg 2" 27 | (let [image (slurp (io/resource "uxbox/tests/_files/sample2.svg")) 28 | result (svg/parse-string image)] 29 | (t/is (contains? result :width)) 30 | (t/is (contains? result :height)) 31 | (t/is (contains? result :view-box)) 32 | (t/is (contains? result :name)) 33 | (t/is (contains? result :content)) 34 | (t/is (= 500.0 (:width result))) 35 | (t/is (= 500.0 (:height result))) 36 | (t/is (= [0.0 0.0 500.0 500.00001] (:view-box result))) 37 | (t/is (= "play.svg" (:name result))))) 38 | 39 | (t/testing "parsing invalid data 1" 40 | (let [image (slurp (io/resource "uxbox/tests/_files/sample.jpg")) 41 | [e result] (th/try-on (svg/parse-string image))] 42 | (t/is (th/exception? e)) 43 | (t/is (th/ex-info? e)) 44 | (t/is (th/ex-with-code? e :uxbox.services.svgparse/invalid-input)))) 45 | 46 | (t/testing "parsing invalid data 2" 47 | (let [[e result] (th/try-on (svg/parse-string ""))] 48 | (t/is (th/exception? e)) 49 | (t/is (th/ex-info? e)) 50 | (t/is (th/ex-with-code? e :uxbox.services.svgparse/invalid-input)))) 51 | 52 | (t/testing "parsing invalid data 3" 53 | (let [[e result] (th/try-on (svg/parse-string ""))] 54 | (t/is (th/exception? e)) 55 | (t/is (th/ex-info? e)) 56 | (t/is (th/ex-with-code? e :uxbox.services.svgparse/invalid-result)))) 57 | 58 | (t/testing "valid http request" 59 | (let [image (slurp (io/resource "uxbox/tests/_files/sample2.svg")) 60 | path "/api/svg/parse"] 61 | (with-server {:handler (uft/routes)} 62 | (let [rsp (th/request {:method :post 63 | :path path 64 | :body image 65 | :raw? true})] 66 | (t/is (= 200 (:status rsp))) 67 | (t/is (contains? (:body rsp) :width)) 68 | (t/is (contains? (:body rsp) :height)) 69 | (t/is (contains? (:body rsp) :view-box)) 70 | (t/is (contains? (:body rsp) :name)) 71 | (t/is (contains? (:body rsp) :content)) 72 | (t/is (= 500.0 (:width (:body rsp)))) 73 | (t/is (= 500.0 (:height (:body rsp)))) 74 | (t/is (= [0.0 0.0 500.0 500.00001] (:view-box (:body rsp)))) 75 | (t/is (= "play.svg" (:name (:body rsp)))))))) 76 | 77 | (t/testing "invalid http request" 78 | (let [path "/api/svg/parse" 79 | image ""] 80 | (with-server {:handler (uft/routes)} 81 | (let [rsp (th/request {:method :post 82 | :path path 83 | :body image 84 | :raw? true})] 85 | (t/is (= 400 (:status rsp))) 86 | (t/is (= :validation (get-in rsp [:body :type]))) 87 | (t/is (= ::svg/invalid-result (get-in rsp [:body :code]))))))) 88 | 89 | ) 90 | -------------------------------------------------------------------------------- /test/uxbox/tests/test_txlog.clj: -------------------------------------------------------------------------------- 1 | (ns uxbox.tests.test-txlog 2 | "A txlog and services abstraction generic tests." 3 | (:require [clojure.test :as t] 4 | [promesa.core :as p] 5 | [uxbox.services.core :as usc] 6 | [uxbox.services :as usv] 7 | [uxbox.tests.helpers :as th])) 8 | 9 | (t/use-fixtures :each th/database-reset) 10 | 11 | (defmethod usc/novelty ::testype1 12 | [data] 13 | true) 14 | 15 | (t/deftest txlog-spec1 16 | (let [data {:type ::testype1 :foo 1 :bar "baz"} 17 | response (usv/novelty data)] 18 | (t/is (p/promise? response)) 19 | (t/is (= true @response)))) 20 | -------------------------------------------------------------------------------- /test/uxbox/tests/test_users.clj: -------------------------------------------------------------------------------- 1 | (ns uxbox.tests.test-users 2 | (:require [clojure.test :as t] 3 | [clojure.java.io :as io] 4 | [promesa.core :as p] 5 | [buddy.hashers :as hashers] 6 | [clj-http.client :as http] 7 | [suricatta.core :as sc] 8 | [catacumba.testing :refer (with-server)] 9 | [uxbox.db :as db] 10 | [uxbox.frontend :as uft] 11 | [uxbox.services.users :as usu] 12 | [uxbox.services :as usv] 13 | [uxbox.tests.helpers :as th])) 14 | 15 | (t/use-fixtures :each th/database-reset) 16 | 17 | (t/deftest test-http-retrieve-profile 18 | (with-open [conn (db/connection)] 19 | (let [user (th/create-user conn 1)] 20 | (with-server {:handler (uft/routes)} 21 | (let [uri (str th/+base-url+ "/api/profile/me") 22 | [status data] (th/http-get user uri)] 23 | ;; (println "RESPONSE:" status data) 24 | (t/is (= 200 status)) 25 | (t/is (= (:fullname data) "User 1")) 26 | (t/is (= (:username data) "user1")) 27 | (t/is (= (:metadata data) "1")) 28 | (t/is (= (:email data) "user1@uxbox.io")) 29 | (t/is (not (contains? data :password)))))))) 30 | 31 | (t/deftest test-http-update-profile 32 | (with-open [conn (db/connection)] 33 | (let [user (th/create-user conn 1)] 34 | (with-server {:handler (uft/routes)} 35 | (let [uri (str th/+base-url+ "/api/profile/me") 36 | data (assoc user 37 | :fullname "Full Name" 38 | :username "user222" 39 | :metadata "222" 40 | :email "user222@uxbox.io") 41 | [status data] (th/http-put user uri {:body data})] 42 | ;; (println "RESPONSE:" status data) 43 | (t/is (= 200 status)) 44 | (t/is (= (:fullname data) "Full Name")) 45 | (t/is (= (:username data) "user222")) 46 | (t/is (= (:metadata data) "222")) 47 | (t/is (= (:email data) "user222@uxbox.io")) 48 | (t/is (not (contains? data :password)))))))) 49 | 50 | (t/deftest test-http-update-profile-photo 51 | (with-open [conn (db/connection)] 52 | (let [user (th/create-user conn 1)] 53 | (with-server {:handler (uft/routes)} 54 | (let [uri (str th/+base-url+ "/api/profile/me/photo") 55 | params [{:name "sample.jpg" 56 | :part-name "file" 57 | :content (io/input-stream 58 | (io/resource "uxbox/tests/_files/sample.jpg"))}] 59 | [status data] (th/http-multipart user uri params)] 60 | ;; (println "RESPONSE:" status data) 61 | (t/is (= 204 status))))))) 62 | 63 | (t/deftest test-http-register-user 64 | (with-server {:handler (uft/routes)} 65 | (let [uri (str th/+base-url+ "/api/auth/register") 66 | data {:fullname "Full Name" 67 | :username "user222" 68 | :email "user222@uxbox.io" 69 | :password "user222"} 70 | [status data] (th/http-post uri {:body data})] 71 | ;; (println "RESPONSE:" status data) 72 | (t/is (= 200 status))))) 73 | 74 | (t/deftest test-http-validate-recovery-token 75 | (with-open [conn (db/connection)] 76 | (let [user (th/create-user conn 1)] 77 | (with-server {:handler (uft/routes)} 78 | (let [token (#'usu/request-password-recovery conn "user1") 79 | uri1 (str th/+base-url+ "/api/auth/recovery/not-existing") 80 | uri2 (str th/+base-url+ "/api/auth/recovery/" token) 81 | [status1 data1] (th/http-get user uri1) 82 | [status2 data2] (th/http-get user uri2)] 83 | ;; (println "RESPONSE:" status1 data1) 84 | ;; (println "RESPONSE:" status2 data2) 85 | (t/is (= 404 status1)) 86 | (t/is (= 204 status2))))))) 87 | 88 | (t/deftest test-http-request-password-recovery 89 | (with-open [conn (db/connection)] 90 | (let [user (th/create-user conn 1) 91 | sql "select * from user_pswd_recovery" 92 | res (sc/fetch-one conn sql)] 93 | 94 | ;; Initially no tokens exists 95 | (t/is (nil? res)) 96 | 97 | (with-server {:handler (uft/routes)} 98 | (let [uri (str th/+base-url+ "/api/auth/recovery") 99 | data {:username "user1"} 100 | [status data] (th/http-post user uri {:body data})] 101 | ;; (println "RESPONSE:" status data) 102 | (t/is (= 204 status))) 103 | 104 | (let [res (sc/fetch-one conn sql)] 105 | (t/is (not (nil? res))) 106 | (t/is (= (:user res) (:id user)))))))) 107 | 108 | (t/deftest test-http-validate-recovery-token 109 | (with-open [conn (db/connection)] 110 | (let [user (th/create-user conn 1)] 111 | (with-server {:handler (uft/routes)} 112 | (let [token (#'usu/request-password-recovery conn (:username user)) 113 | uri (str th/+base-url+ "/api/auth/recovery") 114 | data {:token token :password "mytestpassword"} 115 | [status data] (th/http-put user uri {:body data}) 116 | 117 | user' (usu/find-full-user-by-id conn (:id user))] 118 | (t/is (= status 204)) 119 | (t/is (hashers/check "mytestpassword" (:password user')))))))) 120 | 121 | -------------------------------------------------------------------------------- /vendor/executors/core.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns executors.core 8 | "A executos service abstraction layer." 9 | (:import java.util.function.Supplier 10 | java.util.concurrent.ForkJoinPool 11 | java.util.concurrent.Future 12 | java.util.concurrent.CompletableFuture 13 | java.util.concurrent.ExecutorService 14 | java.util.concurrent.TimeoutException 15 | java.util.concurrent.ThreadFactory 16 | java.util.concurrent.TimeUnit 17 | java.util.concurrent.ScheduledExecutorService 18 | java.util.concurrent.Executors)) 19 | 20 | (def ^:const +max-priority+ Thread/MAX_PRIORITY) 21 | (def ^:const +min-priority+ Thread/MIN_PRIORITY) 22 | (def ^:const +norm-priority+ Thread/NORM_PRIORITY) 23 | 24 | ;; --- Protocols 25 | 26 | (defprotocol IExecutor 27 | (^:private -execute [_ task] "Execute a task in a executor.") 28 | (^:private -submit [_ task] "Submit a task and return a promise.")) 29 | 30 | (defprotocol IScheduledExecutor 31 | (^:provate -schedule [_ ms task] "Schedule a task to execute in a future.")) 32 | 33 | (defprotocol IScheduledTask 34 | "A cancellation abstraction." 35 | (-cancel [_]) 36 | (-cancelled? [_])) 37 | 38 | ;; --- Implementation 39 | 40 | (defn- thread-factory-adapter 41 | "Adapt a simple clojure function into a 42 | ThreadFactory instance." 43 | [func] 44 | (reify ThreadFactory 45 | (^Thread newThread [_ ^Runnable runnable] 46 | (func runnable)))) 47 | 48 | (defn- thread-factory 49 | [{:keys [daemon priority] 50 | :or {daemon true 51 | priority Thread/NORM_PRIORITY}}] 52 | (thread-factory-adapter 53 | (fn [runnable] 54 | (let [thread (Thread. ^Runnable runnable)] 55 | (.setDaemon thread daemon) 56 | (.setPriority thread priority) 57 | thread)))) 58 | 59 | (defn- resolve-thread-factory 60 | [opts] 61 | (cond 62 | (map? opts) (thread-factory opts) 63 | (fn? opts) (thread-factory-adapter opts) 64 | (instance? ThreadFactory opts) opts 65 | :else (throw (ex-info "Invalid thread factory" {})))) 66 | 67 | (deftype ScheduledTask [^Future fut] 68 | clojure.lang.IDeref 69 | (deref [_] 70 | (.get fut)) 71 | 72 | clojure.lang.IBlockingDeref 73 | (deref [_ ms default] 74 | (try 75 | (.get fut ms TimeUnit/MILLISECONDS) 76 | (catch TimeoutException e 77 | default))) 78 | 79 | clojure.lang.IPending 80 | (isRealized [_] (and (.isDone fut) 81 | (not (.isCancelled fut)))) 82 | 83 | IScheduledTask 84 | (-cancelled? [_] 85 | (.isCancelled fut)) 86 | 87 | (-cancel [_] 88 | (when-not (.isCancelled fut) 89 | (.cancel fut true)))) 90 | 91 | (extend-type ExecutorService 92 | IExecutor 93 | (-execute [this task] 94 | (CompletableFuture/runAsync ^Runnable task this)) 95 | 96 | (-submit [this task] 97 | (let [supplier (reify Supplier (get [_] (task)))] 98 | (CompletableFuture/supplyAsync supplier this)))) 99 | 100 | (extend-type ScheduledExecutorService 101 | IScheduledExecutor 102 | (-schedule [this ms func] 103 | (let [fut (.schedule this func ms TimeUnit/MILLISECONDS)] 104 | (ScheduledTask. fut)))) 105 | 106 | ;; --- Public Api (Pool Constructors) 107 | 108 | (defn common-pool 109 | "Get the common pool." 110 | [] 111 | (ForkJoinPool/commonPool)) 112 | 113 | (defn cached 114 | "A cached thread pool constructor." 115 | ([] 116 | (Executors/newCachedThreadPool)) 117 | ([opts] 118 | (let [factory (resolve-thread-factory opts)] 119 | (Executors/newCachedThreadPool factory)))) 120 | 121 | (defn fixed 122 | "A fixed thread pool constructor." 123 | ([n] 124 | (Executors/newFixedThreadPool (int n))) 125 | ([n opts] 126 | (let [factory (resolve-thread-factory opts)] 127 | (Executors/newFixedThreadPool (int n) factory)))) 128 | 129 | (defn single-thread 130 | "A single thread pool constructor." 131 | ([] 132 | (Executors/newSingleThreadExecutor)) 133 | ([opts] 134 | (let [factory (resolve-thread-factory opts)] 135 | (Executors/newSingleThreadExecutor factory)))) 136 | 137 | (defn scheduled 138 | "A scheduled thread pool constructo." 139 | ([] (Executors/newScheduledThreadPool (int 1))) 140 | ([n] (Executors/newScheduledThreadPool (int n))) 141 | ([n opts] 142 | (let [factory (resolve-thread-factory opts)] 143 | (Executors/newScheduledThreadPool (int n) factory)))) 144 | 145 | ;; --- Public Api (Task Execution) 146 | 147 | (defn execute 148 | "Execute a task in a provided executor. 149 | 150 | A task is a plain clojure function or 151 | jvm Runnable instance." 152 | ([task] 153 | (-> (common-pool) 154 | (-execute task))) 155 | ([executor task] 156 | (-execute executor task))) 157 | 158 | (defn submit 159 | "Submit a task to be executed in a provided executor 160 | and return a promise that will be completed with 161 | the return value of a task. 162 | 163 | A task is a plain clojure function." 164 | ([task] 165 | (-> (common-pool) 166 | (-submit task))) 167 | ([executor task] 168 | (-submit executor task))) 169 | 170 | (defn schedule 171 | "Schedule task exection for some time in the future." 172 | ([ms task] 173 | (-> (common-pool) 174 | (-schedule ms task))) 175 | ([executor ms task] 176 | (-schedule executor ms task))) 177 | -------------------------------------------------------------------------------- /vendor/storages/core.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns storages.core 8 | "A storages abstraction layer." 9 | (:require [storages.proto :as pt] 10 | [storages.impl])) 11 | 12 | (defn save 13 | "Perists a file or bytes in the storage. This function 14 | returns a relative path where file is saved. 15 | 16 | The final file path can be different to the one provided 17 | to this function and the behavior is totally dependen on 18 | the storage implementation." 19 | [storage path content] 20 | (pt/-save storage path content)) 21 | 22 | (defn lookup 23 | "Resolve provided relative path in the storage and return 24 | the local filesystem absolute path to it. 25 | This method may be not implemented in all storages." 26 | [storage path] 27 | {:pre [(satisfies? pt/ILocalStorage storage)]} 28 | (pt/-lookup storage path)) 29 | 30 | (defn exists? 31 | "Check if a relative `path` exists in the storage." 32 | [storage path] 33 | (pt/-exists? storage path)) 34 | 35 | (defn delete 36 | "Delete a file from the storage." 37 | [storage path] 38 | (pt/-delete storage path)) 39 | 40 | (defn clear! 41 | "Clear all contents of the storage." 42 | [storage] 43 | (pt/-clear storage)) 44 | 45 | (defn path 46 | "Create path from string or more than one string." 47 | ([fst] 48 | (pt/-path fst)) 49 | ([fst & more] 50 | (pt/-path (cons fst more)))) 51 | 52 | (defn public-url 53 | [storage path] 54 | (pt/-public-uri storage path)) 55 | 56 | (defn storage? 57 | "Return `true` if `v` implements IStorage protocol" 58 | [v] 59 | (satisfies? pt/IStorage v)) 60 | -------------------------------------------------------------------------------- /vendor/storages/fs/local.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns storages.fs.local 8 | "A local filesystem storage implementation." 9 | (:require [promesa.core :as p] 10 | [clojure.java.io :as io] 11 | [executors.core :as exec] 12 | [storages.proto :as pt] 13 | [storages.impl :as impl] 14 | [storages.util :as util]) 15 | (:import java.io.InputStream 16 | java.io.OutputStream 17 | java.net.URI 18 | java.nio.file.Path 19 | java.nio.file.Files)) 20 | 21 | (defn normalize-path 22 | [^Path base ^Path path] 23 | (if (util/absolute? path) 24 | (throw (ex-info "Suspicios operation: absolute path not allowed." 25 | {:path (str path)})) 26 | (let [^Path fullpath (.resolve base path) 27 | ^Path fullpath (.normalize fullpath)] 28 | (when-not (.startsWith fullpath base) 29 | (throw (ex-info "Suspicios operation: go to parent dir is not allowed." 30 | {:path (str path)}))) 31 | fullpath))) 32 | 33 | (defn- save 34 | [base path content] 35 | (let [^Path path (pt/-path path) 36 | ^Path fullpath (normalize-path base path)] 37 | (when-not (util/exists? (.getParent fullpath)) 38 | (util/create-dir! (.getParent fullpath))) 39 | (with-open [^InputStream source (pt/-input-stream content) 40 | ^OutputStream dest (Files/newOutputStream 41 | fullpath util/write-open-opts)] 42 | (io/copy source dest) 43 | path))) 44 | 45 | (defn- delete 46 | [base path] 47 | (let [path (->> (pt/-path path) 48 | (normalize-path base))] 49 | (Files/deleteIfExists ^Path path))) 50 | 51 | (defrecord FileSystemStorage [^Path base ^URI baseuri] 52 | pt/IPublicStorage 53 | (-public-uri [_ path] 54 | (.resolve baseuri (str path))) 55 | 56 | pt/IStorage 57 | (-save [_ path content] 58 | (exec/submit (partial save base path content))) 59 | 60 | (-delete [_ path] 61 | (exec/submit (partial delete base path))) 62 | 63 | (-exists? [this path] 64 | (try 65 | (p/resolved 66 | (let [path (->> (pt/-path path) 67 | (normalize-path base))] 68 | (util/exists? path))) 69 | (catch Exception e 70 | (p/rejected e)))) 71 | 72 | pt/IClearableStorage 73 | (-clear [_] 74 | (util/delete-dir! base) 75 | (util/create-dir! base)) 76 | 77 | pt/ILocalStorage 78 | (-lookup [_ path'] 79 | (try 80 | (p/resolved 81 | (->> (pt/-path path') 82 | (normalize-path base))) 83 | (catch Exception e 84 | (p/rejected e))))) 85 | 86 | (defn filesystem 87 | "Create an instance of local FileSystem storage providing an 88 | absolute base path. 89 | 90 | If that path does not exists it will be automatically created, 91 | if it exists but is not a directory, an exception will be 92 | raised." 93 | [{:keys [basedir baseuri] :as keys}] 94 | (let [^Path basepath (pt/-path basedir) 95 | ^URI baseuri (pt/-uri baseuri)] 96 | (when (and (util/exists? basepath) 97 | (not (util/directory? basepath))) 98 | (throw (ex-info "File already exists." {}))) 99 | 100 | (when-not (util/exists? basepath) 101 | (util/create-dir! basepath)) 102 | 103 | (->FileSystemStorage basepath baseuri))) 104 | 105 | -------------------------------------------------------------------------------- /vendor/storages/fs/misc.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns storages.fs.misc 8 | "A local filesystem storage implementation." 9 | (:require [promesa.core :as p] 10 | [cuerdas.core :as str] 11 | [buddy.core.codecs :as codecs] 12 | [buddy.core.codecs.base64 :as b64] 13 | [buddy.core.nonce :as nonce] 14 | [buddy.core.hash :as hash] 15 | [storages.proto :as pt] 16 | [storages.impl :as impl] 17 | [storages.fs.local :as localfs]) 18 | (:import java.io.InputStream 19 | java.io.OutputStream 20 | java.nio.file.Path 21 | java.nio.file.Files)) 22 | 23 | ;; --- Scoped Storage 24 | 25 | (defrecord ScopedPathStorage [storage ^Path prefix] 26 | pt/IPublicStorage 27 | (-public-uri [_ path] 28 | (let [^Path path (pt/-path [prefix path])] 29 | (pt/-public-uri storage path))) 30 | 31 | pt/IStorage 32 | (-save [_ path content] 33 | (let [^Path path (pt/-path [prefix path])] 34 | (->> (pt/-save storage path content) 35 | (p/map (fn [^Path path] 36 | (.relativize prefix path)))))) 37 | 38 | (-delete [_ path] 39 | (let [^Path path (pt/-path [prefix path])] 40 | (pt/-delete storage path))) 41 | 42 | (-exists? [this path] 43 | (let [^Path path (pt/-path [prefix path])] 44 | (pt/-exists? storage path))) 45 | 46 | pt/ILocalStorage 47 | (-lookup [_ path] 48 | (->> (pt/-lookup storage "") 49 | (p/map (fn [^Path base] 50 | (let [base (pt/-path [base prefix])] 51 | (->> (pt/-path path) 52 | (localfs/normalize-path base)))))))) 53 | 54 | (defn scoped 55 | "Create a composed storage instance that automatically prefixes 56 | the path when content is saved. For the rest of methods it just 57 | relies to the underlying storage. 58 | 59 | This is usefull for atomatically add sertain prefix to some 60 | uploads." 61 | [storage prefix] 62 | (let [prefix (pt/-path prefix)] 63 | (->ScopedPathStorage storage prefix))) 64 | 65 | ;; --- Hashed Storage 66 | 67 | (defn- generate-path 68 | [^Path path] 69 | (let [name (str (.getFileName path)) 70 | hash (-> (nonce/random-nonce 128) 71 | (hash/blake2b-256) 72 | (b64/encode true) 73 | (codecs/bytes->str)) 74 | tokens (re-seq #"[\w\d\-\_]{3}" hash) 75 | path-tokens (take 6 tokens) 76 | rest-tokens (drop 6 tokens) 77 | path (pt/-path path-tokens) 78 | frest (apply str rest-tokens)] 79 | (pt/-path (list path frest name)))) 80 | 81 | (defrecord HashedStorage [storage] 82 | pt/IPublicStorage 83 | (-public-uri [_ path] 84 | (pt/-public-uri storage path)) 85 | 86 | pt/IStorage 87 | (-save [_ path content] 88 | (let [^Path path (pt/-path path) 89 | ^Path path (generate-path path)] 90 | (pt/-save storage path content))) 91 | 92 | (-delete [_ path] 93 | (pt/-delete storage path)) 94 | 95 | (-exists? [this path] 96 | (pt/-exists? storage path)) 97 | 98 | pt/ILocalStorage 99 | (-lookup [_ path] 100 | (pt/-lookup storage path))) 101 | 102 | (defn hashed 103 | "Create a composed storage instance that uses random 104 | hash based directory tree distribution for the final 105 | file path. 106 | 107 | This is usefull when you want to store files with 108 | not predictable uris." 109 | [storage] 110 | (->HashedStorage storage)) 111 | 112 | -------------------------------------------------------------------------------- /vendor/storages/impl.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns storages.impl 8 | "Implementation details and helpers." 9 | (:require [storages.proto :as pt] 10 | [storages.util :as util] 11 | [buddy.core.codecs :as codecs] 12 | [clojure.java.io :as io]) 13 | (:import java.io.File 14 | java.io.ByteArrayInputStream 15 | java.io.InputStream 16 | java.net.URL 17 | java.net.URI 18 | java.nio.file.Path 19 | java.nio.file.Paths 20 | java.nio.file.Files)) 21 | 22 | (extend-protocol pt/IContent 23 | String 24 | (-input-stream [v] 25 | (ByteArrayInputStream. (codecs/str->bytes v))) 26 | 27 | Path 28 | (-input-stream [v] 29 | (io/input-stream v)) 30 | 31 | File 32 | (-input-stream [v] 33 | (io/input-stream v)) 34 | 35 | URI 36 | (-input-stream [v] 37 | (io/input-stream v)) 38 | 39 | URL 40 | (-input-stream [v] 41 | (io/input-stream v)) 42 | 43 | InputStream 44 | (-input-stream [v] 45 | v) 46 | 47 | ratpack.http.TypedData 48 | (-input-stream [this] 49 | (.getInputStream this))) 50 | 51 | (extend-protocol pt/IUri 52 | URI 53 | (-uri [v] v) 54 | 55 | String 56 | (-uri [v] (URI. v))) 57 | 58 | (def ^:private empty-string-array 59 | (make-array String 0)) 60 | 61 | (extend-protocol pt/IPath 62 | Path 63 | (-path [v] v) 64 | 65 | URI 66 | (-path [v] (Paths/get v)) 67 | 68 | URL 69 | (-path [v] (Paths/get (.toURI v))) 70 | 71 | String 72 | (-path [v] (Paths/get v empty-string-array)) 73 | 74 | clojure.lang.Sequential 75 | (-path [v] 76 | (reduce #(.resolve %1 %2) 77 | (pt/-path (first v)) 78 | (map pt/-path (rest v))))) 79 | 80 | (defn- path->input-stream 81 | [^Path path] 82 | (Files/newInputStream path util/read-open-opts)) 83 | 84 | (defn- path->output-stream 85 | [^Path path] 86 | (Files/newOutputStream path util/write-open-opts)) 87 | 88 | (extend-type Path 89 | io/IOFactory 90 | (make-reader [path opts] 91 | (let [^InputStream is (path->input-stream path)] 92 | (io/make-reader is opts))) 93 | (make-writer [path opts] 94 | (let [^OutputStream os (path->output-stream path)] 95 | (io/make-writer os opts))) 96 | (make-input-stream [path opts] 97 | (let [^InputStream is (path->input-stream path)] 98 | (io/make-input-stream is opts))) 99 | (make-output-stream [path opts] 100 | (let [^OutputStream os (path->output-stream path)] 101 | (io/make-output-stream os opts)))) 102 | 103 | (extend-type ratpack.http.TypedData 104 | io/IOFactory 105 | (make-reader [td opts] 106 | (let [^InputStream is (.getInputStream td)] 107 | (io/make-reader is opts))) 108 | (make-writer [path opts] 109 | (throw (UnsupportedOperationException. "read only object"))) 110 | (make-input-stream [td opts] 111 | (let [^InputStream is (.getInputStream td)] 112 | (io/make-input-stream is opts))) 113 | (make-output-stream [path opts] 114 | (throw (UnsupportedOperationException. "read only object")))) 115 | 116 | -------------------------------------------------------------------------------- /vendor/storages/proto.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns storages.proto 8 | "A storage abstraction definition.") 9 | 10 | (defprotocol IUri 11 | (-uri [_] "Coerce to uri.")) 12 | 13 | (defprotocol IPath 14 | (-path [_] "Coerce to path.")) 15 | 16 | (defprotocol IContent 17 | (-input-stream [_] "Coerce to input stream.")) 18 | 19 | (defprotocol IStorage 20 | "A basic abstraction for storage access." 21 | (-save [_ path content] "Persist the content under specified path.") 22 | (-delete [_ path] "Delete the file by its path.") 23 | (-exists? [_ path] "Check if file exists by path.")) 24 | 25 | (defprotocol IClearableStorage 26 | (-clear [_] "clear all contents of the storage")) 27 | 28 | (defprotocol IPublicStorage 29 | (-public-uri [_ path] "Get a public accessible uri for path.")) 30 | 31 | (defprotocol ILocalStorage 32 | (-lookup [_ path] "Resolves the path to the local filesystem.")) 33 | 34 | (defprotocol IStorageIntrospection 35 | (-accessed-time [_ path] "Return the last accessed time of the file.") 36 | (-created-time [_ path] "Return the creation time of the file.") 37 | (-modified-time [_ path] "Return the last modified time of the file.")) 38 | 39 | -------------------------------------------------------------------------------- /vendor/storages/util.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016 Andrey Antukh 6 | 7 | (ns storages.util 8 | "FileSystem related utils." 9 | (:refer-clojure :exclude [name]) 10 | (:require [storages.proto :as pt]) 11 | (:import java.nio.file.Path 12 | java.nio.file.Files 13 | java.nio.file.LinkOption 14 | java.nio.file.OpenOption 15 | java.nio.file.StandardOpenOption 16 | java.nio.file.SimpleFileVisitor 17 | java.nio.file.FileVisitResult 18 | java.nio.file.attribute.FileAttribute 19 | java.nio.file.attribute.PosixFilePermissions 20 | ratpack.form.UploadedFile)) 21 | 22 | ;; --- Constants 23 | 24 | (def write-open-opts 25 | (->> [#_StandardOpenOption/CREATE_NEW 26 | StandardOpenOption/CREATE 27 | StandardOpenOption/WRITE] 28 | (into-array OpenOption))) 29 | 30 | (def read-open-opts 31 | (->> [StandardOpenOption/READ] 32 | (into-array OpenOption))) 33 | 34 | (def follow-link-opts 35 | (into-array LinkOption [LinkOption/NOFOLLOW_LINKS])) 36 | 37 | ;; --- Path Helpers 38 | 39 | (defn path 40 | "Create path from string or more than one string." 41 | ([fst] 42 | (pt/-path fst)) 43 | ([fst & more] 44 | (pt/-path (cons fst more)))) 45 | 46 | (defn make-file-attrs 47 | "Generate a array of `FileAttribute` instances 48 | generated from `rwxr-xr-x` kind of expressions." 49 | [^String expr] 50 | (let [perms (PosixFilePermissions/fromString expr) 51 | attr (PosixFilePermissions/asFileAttribute perms)] 52 | (into-array FileAttribute [attr]))) 53 | 54 | (defn path? 55 | "Return `true` if provided value is an instance of Path." 56 | [v] 57 | (instance? Path v)) 58 | 59 | (defn absolute? 60 | "Return `true` if the provided path is absolute, `else` in case contrary. 61 | The `path` parameter can be anything convertible to path instance." 62 | [path] 63 | (let [^Path path (pt/-path path)] 64 | (.isAbsolute path))) 65 | 66 | (defn exists? 67 | "Return `true` if the provided path exists, `else` in case contrary. 68 | The `path` parameter can be anything convertible to path instance." 69 | [path] 70 | (let [^Path path (pt/-path path)] 71 | (Files/exists path follow-link-opts))) 72 | 73 | (defn directory? 74 | "Return `true` if the provided path is a directory, `else` in case contrary. 75 | The `path` parameter can be anything convertible to path instance." 76 | [path] 77 | (let [^Path path (pt/-path path)] 78 | (Files/isDirectory path follow-link-opts))) 79 | 80 | (defn parent 81 | "Get parent path if it exists." 82 | [path] 83 | (.getParent ^Path (pt/-path path))) 84 | 85 | (defn base-name 86 | "Get the file name." 87 | [path] 88 | (if (instance? UploadedFile path) 89 | (.getFileName ^UploadedFile path) 90 | (str (.getFileName ^Path (pt/-path path))))) 91 | 92 | (defn split-ext 93 | "Returns a vector of `[name extension]`." 94 | [path] 95 | (let [base (base-name path) 96 | i (.lastIndexOf base ".")] 97 | (if (pos? i) 98 | [(subs base 0 i) (subs base i)] 99 | [base nil]))) 100 | 101 | (defn extension 102 | "Return the extension part of a file." 103 | [path] 104 | (last (split-ext path))) 105 | 106 | (defn name 107 | "Return the name part of a file." 108 | [path] 109 | (first (split-ext path))) 110 | 111 | (defn list-directory 112 | [path] 113 | (let [path (pt/-path path)] 114 | (with-open [stream (Files/newDirectoryStream path)] 115 | (vec stream)))) 116 | 117 | (defn list-files 118 | [path] 119 | (filter (complement directory?) (list-directory path))) 120 | 121 | ;; --- Side-Effectfull Operations 122 | 123 | (defn create-dir! 124 | "Create a new directory." 125 | [path] 126 | (let [^Path path (pt/-path path) 127 | attrs (make-file-attrs "rwxr-xr-x")] 128 | (Files/createDirectories path attrs))) 129 | 130 | (defn delete-dir! 131 | [path] 132 | (let [path (pt/-path path) 133 | visitor (proxy [SimpleFileVisitor] [] 134 | (visitFile [file attrs] 135 | (Files/delete file) 136 | FileVisitResult/CONTINUE) 137 | (postVisitDirectory [dir exc] 138 | (Files/delete dir) 139 | FileVisitResult/CONTINUE))] 140 | (Files/walkFileTree path visitor))) 141 | --------------------------------------------------------------------------------