├── .gitignore ├── .iex.exs ├── .travis.yml ├── LICENSE ├── README.md ├── assets ├── brunch-config.js ├── css │ └── app.scss ├── js │ └── app.js ├── package.json └── static │ ├── favicon.ico │ ├── images │ └── phoenix.png │ └── robots.txt ├── config ├── config.exs ├── dev.exs ├── prod.exs └── test.exs ├── etc └── emqttd │ ├── emq.conf │ └── plugins │ └── emq_auth_pgsql.conf ├── lib ├── iris.ex ├── iris │ ├── accounts │ │ ├── accounts.ex │ │ ├── device.ex │ │ ├── role.ex │ │ └── user.ex │ ├── application.ex │ ├── ecto_enums.ex │ ├── mailer.ex │ └── repo.ex ├── iris_web.ex ├── iris_web │ ├── channels │ │ └── user_socket.ex │ ├── controllers │ │ ├── device_controller.ex │ │ ├── message_controller.ex │ │ ├── page_controller.ex │ │ ├── registration_controller.ex │ │ ├── service_controller.ex │ │ ├── session_controller.ex │ │ └── user_controller.ex │ ├── email.ex │ ├── endpoint.ex │ ├── gettext.ex │ ├── models │ │ ├── message.ex │ │ ├── service.ex │ │ └── token.ex │ ├── plugs │ │ ├── authedicate_admin.ex │ │ └── authedicate_user.ex │ ├── router.ex │ ├── support │ │ └── AuthedicationToken.ex │ ├── templates │ │ ├── device │ │ │ ├── edit.html.eex │ │ │ ├── form.html.eex │ │ │ ├── index.html.eex │ │ │ ├── new.html.eex │ │ │ └── show.html.eex │ │ ├── email │ │ │ ├── login.html.eex │ │ │ ├── login.text.eex │ │ │ ├── welcome.html.eex │ │ │ └── welcome.text.eex │ │ ├── layout │ │ │ ├── app.html.eex │ │ │ ├── email.html.eex │ │ │ ├── email.text.eex │ │ │ ├── footer.html.eex │ │ │ ├── messages.html.eex │ │ │ └── nav.html.eex │ │ ├── page │ │ │ ├── dashboard.html.eex │ │ │ ├── devices.html.eex │ │ │ ├── index.html.eex │ │ │ ├── notification_form.html.eex │ │ │ ├── services.html.eex │ │ │ └── settings.html.eex │ │ ├── registration │ │ │ └── signup.html.eex │ │ ├── session │ │ │ └── login.html.eex │ │ └── user │ │ │ ├── edit.html.eex │ │ │ ├── form.html.eex │ │ │ ├── index.html.eex │ │ │ ├── new.html.eex │ │ │ └── show.html.eex │ └── views │ │ ├── changeset_view.ex │ │ ├── device_view.ex │ │ ├── email_view.ex │ │ ├── error_helpers.ex │ │ ├── error_view.ex │ │ ├── layout_view.ex │ │ ├── message_view.ex │ │ ├── page_view.ex │ │ ├── registration_view.ex │ │ ├── service_view.ex │ │ ├── session_view.ex │ │ └── user_view.ex └── mqtt │ ├── messenger.ex │ └── supervisor.ex ├── mix.exs ├── mix.lock ├── priv ├── gettext │ ├── en │ │ └── LC_MESSAGES │ │ │ └── errors.po │ └── errors.pot └── repo │ ├── migrations │ ├── 20170629190008_create_service.exs │ ├── 20170629195901_create_message.exs │ ├── 20170702202723_create_role.exs │ ├── 20170702202724_create_user.exs │ ├── 20170704174839_create_token.exs │ └── 20170719054344_create_device.exs │ └── seeds.exs └── test ├── iris_web ├── controllers │ ├── device_controller_test.exs │ ├── message_controller_test.exs │ ├── page_controller_test.exs │ ├── registration_controller_test.exs │ ├── service_controller_test.exs │ ├── session_controller_test.exs │ └── user_controller_test.exs ├── models │ ├── device_test.exs │ ├── message_test.exs │ ├── role_test.exs │ ├── service_test.exs │ ├── token_test.exs │ └── user_test.exs └── views │ ├── error_view_test.exs │ ├── layout_view_test.exs │ └── page_view_test.exs ├── support ├── channel_case.ex ├── conn_case.ex ├── data_case.ex └── test_helper.ex └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | # App artifacts 2 | /_build 3 | /db 4 | /deps 5 | /*.ez 6 | /cover 7 | 8 | # Generated on crash by the VM 9 | erl_crash.dump 10 | 11 | # Static artifacts 12 | /assets/node_modules 13 | 14 | # Since we are building assets from web/static, 15 | # we ignore priv/static. You may want to comment 16 | # this depending on your deployment strategy. 17 | /priv/static/ 18 | 19 | # The config/prod.secret.exs file by default contains sensitive 20 | # data and you should not commit it into version control. 21 | # 22 | # Alternatively, you may comment the line below and commit the 23 | # secrets file as long as you replace its contents by environment 24 | # variables. 25 | /config/prod.secret.exs 26 | -------------------------------------------------------------------------------- /.iex.exs: -------------------------------------------------------------------------------- 1 | alias Iris.{Role, User, Endpoint} 2 | alias IrisWeb.Router.Helpers 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: required 3 | 4 | language: elixir 5 | elixir: 6 | - 1.5 7 | otp_release: 8 | - 20.0 9 | 10 | addons: 11 | postgresql: '9.4' 12 | 13 | services: 14 | - postgresql 15 | 16 | before_install: 17 | - wget http://emqtt.io/downloads/stable/ubuntu14_04-deb -O emqtt.deb 18 | - sudo dpkg -i emqtt.deb 19 | - sudo service emqttd start 20 | 21 | before_script: 22 | - mix do ecto.create, ecto.migrate 23 | 24 | env: 25 | - MIX_ENV=test 26 | 27 | script: 28 | - mix coveralls.travis 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Stelios Joseph Karras 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Iris 2 | 3 | A self hosted notification server without google cloud messaging, firebase cloud messaging or apple push notification service. 4 | 5 | ## Motivation 6 | The driving force behind this project is my need for a tool that: 7 | - Notifies the user when something is happening in a service/application. 8 | - The user can deploy it in her/his premises. 9 | - Uses a self hosted and open source message broker and not an external one. 10 | 11 | ## Project Status 12 | 13 | [![Build Status](https://api.travis-ci.org/7-iris/iris_server.svg?branch=master)](https://travis-ci.org/7-iris/iris_server) 14 | [![Coverage Status](https://coveralls.io/repos/github/7-iris/iris_server/badge.svg?branch=master)](https://coveralls.io/github/7-iris/iris_server?branch=master) 15 | 16 | This project is under development, it is not ready for production use yet. 17 | 18 | ## Usage example 19 | 20 | To send a notification from bash: 21 | 22 | ``` 23 | echo '{"service_token":"xyz","title":"Testing","text":"notification message"}' | curl -H "Content-Type: application/json" -XPOST -d @- http://localhost:4000/api/messages 24 | ``` 25 | 26 | ## Installation 27 | 28 | ### Prerequisites 29 | In order to get iris to work you need to install the emqttd server first. 30 | You can find the instructions [here](http://emqtt.io/docs/v2/install.html). 31 | 32 | ### Installing via DEB package 33 | TODO 34 | 35 | ### Installing via docker image 36 | TODO 37 | 38 | ## Built With 39 | 40 | * [Phoenix](https://github.com/phoenixframework/phoenix) 41 | * [Hulaaki](https://github.com/suvash/hulaaki) 42 | * [Emqttd](https://github.com/emqtt/emqttd) 43 | * [Bootstrap](https://github.com/twbs/bootstrap) 44 | * [Bootswatch](https://github.com/thomaspark/bootswatch) 45 | * And many more... 46 | 47 | ## Resources 48 | 49 | * [Project roadmap](https://github.com/7-iris/iris_server/wiki/Roadmap) 50 | * [What is mqtt?](http://mqtt.org/faq) 51 | * [What is emqttd?](http://emqtt.io/about) 52 | 53 | ## Naming 54 | Iris in Greek mythology was one of the messengers of the Olympian gods. 55 | 56 | ## Authors 57 | 58 | * Stelios Joseph Karras [stylkarr@gmail.com](mailto://stylkarr@gmail.com) 59 | 60 | ## License 61 | 62 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE) file for details 63 | 64 | ## Acknowledgments 65 | 66 | * Inspired by [pushbullet](http://pushbullet.com/), [pushover](http://pushover.net/) and [pushjet](http://pushjet.io/) 67 | -------------------------------------------------------------------------------- /assets/brunch-config.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | // See http://brunch.io/#documentation for docs. 3 | files: { 4 | javascripts: { 5 | joinTo: "js/app.js" 6 | }, 7 | stylesheets: { 8 | joinTo: "css/app.css", 9 | order: { 10 | after: ["web/static/css/app.css"] 11 | } 12 | }, 13 | templates: { 14 | joinTo: "js/app.js" 15 | } 16 | }, 17 | 18 | conventions: { 19 | assets: /^(static)/ 20 | }, 21 | 22 | paths: { 23 | watched: ["static", "css", "js", "vendor"], 24 | 25 | // Where to compile files to 26 | public: "../priv/static" 27 | }, 28 | 29 | // Configure your plugins 30 | plugins: { 31 | babel: { 32 | // Do not use ES6 compiler in vendor code 33 | ignore: [/vendor/] 34 | }, 35 | sass: { 36 | options: { 37 | includePaths: ["node_modules/bootstrap/scss"], 38 | precision: 8 // Minimum precision required by bootstrap-sass 39 | } 40 | } 41 | }, 42 | 43 | modules: { 44 | autoRequire: { 45 | "js/app.js": ["js/app"] 46 | } 47 | }, 48 | 49 | npm: { 50 | enabled: true, 51 | globals: { 52 | $: "jquery", 53 | jQuery: "jquery", 54 | bootstrap: "bootstrap" 55 | } 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /assets/css/app.scss: -------------------------------------------------------------------------------- 1 | /* This file is for your main application css. */ 2 | @import "bootswatch/_variables.scss"; 3 | @import "bootstrap"; 4 | @import "bootswatch/_bootswatch.scss"; 5 | -------------------------------------------------------------------------------- /assets/js/app.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7-iris/iris_server/8faa370f506a003ae3c06b23d6c827bd746368f3/assets/js/app.js -------------------------------------------------------------------------------- /assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": {}, 3 | "license": "MIT", 4 | "scripts": { 5 | "deploy": "brunch build --production", 6 | "watch": "brunch watch --stdin" 7 | }, 8 | "dependencies": { 9 | "bootstrap": "^4.0.0-alpha.6", 10 | "bootswatch": "https://github.com/thomaspark/bootswatch/archive/v4.0.0-alpha.6.tar.gz", 11 | "jquery": "^3.2.1", 12 | "phoenix": "file:../deps/phoenix", 13 | "phoenix_html": "file:../deps/phoenix_html" 14 | }, 15 | "devDependencies": { 16 | "babel-brunch": "~6.0.0", 17 | "brunch": "2.7.4", 18 | "clean-css-brunch": "~2.0.0", 19 | "copycat-brunch": "^1.1.0", 20 | "css-brunch": "~2.0.0", 21 | "javascript-brunch": "~2.0.0", 22 | "sass-brunch": "^2.10.4", 23 | "uglify-js-brunch": "~2.0.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /assets/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7-iris/iris_server/8faa370f506a003ae3c06b23d6c827bd746368f3/assets/static/favicon.ico -------------------------------------------------------------------------------- /assets/static/images/phoenix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7-iris/iris_server/8faa370f506a003ae3c06b23d6c827bd746368f3/assets/static/images/phoenix.png -------------------------------------------------------------------------------- /assets/static/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | use Mix.Config 7 | 8 | # General application configuration 9 | config :iris, 10 | ecto_repos: [Iris.Repo] 11 | 12 | # Configures the endpoint 13 | config :iris, IrisWeb.Endpoint, 14 | url: [host: "localhost"], 15 | secret_key_base: "kdl17scWn5kPZxPtO8n6NE0eMwC7c2PIcFLhG+LQTp2jUDCNI5N6tHwXC3dCFLRq", 16 | render_errors: [view: IrisWeb.ErrorView, accepts: ~w(html json)], 17 | pubsub: [name: Iris.PubSub, 18 | adapter: Phoenix.PubSub.PG2] 19 | 20 | # Configures Elixir's Logger 21 | config :logger, :console, 22 | format: "$time $metadata[$level] $message\n", 23 | metadata: [:request_id] 24 | 25 | # Import environment specific config. This must remain at the bottom 26 | # of this file so it overrides the configuration defined above. 27 | import_config "#{Mix.env}.exs" 28 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For development, we disable any cache and enable 4 | # debugging and code reloading. 5 | # 6 | # The watchers configuration can be used to run external 7 | # watchers to your application. For example, we use it 8 | # with brunch.io to recompile .js and .css sources. 9 | config :iris, IrisWeb.Endpoint, 10 | http: [port: 4000], 11 | debug_errors: true, 12 | code_reloader: true, 13 | check_origin: false, 14 | watchers: [node: ["node_modules/brunch/bin/brunch", "watch", "--stdin", 15 | cd: Path.expand("../assets", __DIR__)]] 16 | 17 | 18 | # Watch static and templates for browser reloading. 19 | config :iris, IrisWeb.Endpoint, 20 | live_reload: [ 21 | patterns: [ 22 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$}, 23 | ~r{priv/gettext/.*(po)$}, 24 | ~r{lib/iris_web/views/.*(ex)$}, 25 | ~r{lib/iris_web/templates/.*(eex)$} 26 | ] 27 | ] 28 | 29 | # Do not include metadata nor timestamps in development logs 30 | config :logger, 31 | :console, format: "[$level] $message\n", 32 | level: :debug, 33 | truncate: 4096 34 | 35 | # Set a higher stacktrace during development. Avoid configuring such 36 | # in production as building large stacktraces may be expensive. 37 | config :phoenix, :stacktrace_depth, 20 38 | 39 | # Configure your database 40 | config :iris, Iris.Repo, 41 | adapter: Ecto.Adapters.Postgres, 42 | username: "postgres", 43 | password: "postgres", 44 | database: "iris_dev", 45 | hostname: "localhost", 46 | pool_size: 10 47 | 48 | config :iris, Mqtt.Messenger, 49 | client_id: "iris_server", 50 | host: "localhost", 51 | username: "admin@dev.com", 52 | password: "sagapo", 53 | port: 1883 54 | 55 | config :iris, Iris.Mailer, 56 | adapter: Bamboo.LocalAdapter 57 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For production, we configure the host to read the PORT 4 | # from the system environment. Therefore, you will need 5 | # to set PORT=80 before running your server. 6 | # 7 | # You should also configure the url host to something 8 | # meaningful, we use this information when generating URLs. 9 | # 10 | # Finally, we also include the path to a manifest 11 | # containing the digested version of static files. This 12 | # manifest is generated by the mix phoenix.digest task 13 | # which you typically run after static files are built. 14 | config :iris, IrisWeb.Endpoint, 15 | http: [port: {:system, "PORT"}], 16 | url: [host: "example.com", port: 80], 17 | # cache_static_manifest: "priv/static/manifest.json" 18 | 19 | # Do not print debug messages in production 20 | config :logger, level: :info 21 | 22 | # ## SSL Support 23 | # 24 | # To get SSL working, you will need to add the `https` key 25 | # to the previous section and set your `:url` port to 443: 26 | # 27 | # config :iris, IrisWeb.Endpoint, 28 | # ... 29 | # url: [host: "example.com", port: 443], 30 | # https: [port: 443, 31 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 32 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")] 33 | # 34 | # Where those two env variables return an absolute path to 35 | # the key and cert in disk or a relative path inside priv, 36 | # for example "priv/ssl/server.key". 37 | # 38 | # We also recommend setting `force_ssl`, ensuring no data is 39 | # ever sent via http, always redirecting to https: 40 | # 41 | # config :iris, IrisWeb.Endpoint, 42 | # force_ssl: [hsts: true] 43 | # 44 | # Check `Plug.SSL` for all available options in `force_ssl`. 45 | 46 | # ## Using releases 47 | # 48 | # If you are doing OTP releases, you need to instruct Phoenix 49 | # to start the server for all endpoints: 50 | # 51 | # config :phoenix, :serve_endpoints, true 52 | # 53 | # Alternatively, you can configure exactly which server to 54 | # start per endpoint: 55 | # 56 | # config :iris, IrisWeb.Endpoint, server: true 57 | # 58 | 59 | # Finally import the config/prod.secret.exs 60 | # which should be versioned separately. 61 | import_config "prod.secret.exs" 62 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # We don't run a server during test. If one is required, 4 | # you can enable the server option below. 5 | config :iris, IrisWeb.Endpoint, 6 | http: [port: 4001], 7 | server: false 8 | 9 | # Print only warnings and errors during test 10 | config :logger, level: :warn 11 | 12 | # Configure your database 13 | config :iris, Iris.Repo, 14 | adapter: Ecto.Adapters.Postgres, 15 | username: "postgres", 16 | password: "postgres", 17 | database: "iris_test", 18 | hostname: "localhost", 19 | pool: Ecto.Adapters.SQL.Sandbox 20 | 21 | config :iris, Mqtt.Messenger, 22 | client_id: "iris_server", 23 | host: "localhost", 24 | username: "admin@dev.com", 25 | password: "sagapo", 26 | port: 1883 27 | 28 | config :iris, Iris.Mailer, 29 | adapter: Bamboo.LocalAdapter 30 | -------------------------------------------------------------------------------- /etc/emqttd/emq.conf: -------------------------------------------------------------------------------- 1 | ##=================================================================== 2 | ## EMQ Configuration R2.3 3 | ##=================================================================== 4 | 5 | ##-------------------------------------------------------------------- 6 | ## Cluster 7 | ##-------------------------------------------------------------------- 8 | 9 | ## Cluster name 10 | cluster.name = emqcl 11 | 12 | ## Cluster discovery strategy: manual | static | mcast | dns | etcd | k8s 13 | cluster.discovery = manual 14 | 15 | ## Cluster Autoheal: on | off 16 | cluster.autoheal = on 17 | 18 | ## Clean down node of the cluster 19 | cluster.autoclean = 5m 20 | 21 | ##-------------------------------------------------------------------- 22 | ## Node Args 23 | ##-------------------------------------------------------------------- 24 | 25 | ## Node name 26 | node.name = emq@127.0.0.1 27 | 28 | ## Cookie for distributed node 29 | node.cookie = emqsecretcookie 30 | 31 | ## SMP support: enable, auto, disable 32 | node.smp = enable 33 | 34 | ## Enable kernel poll 35 | node.kernel_poll = on 36 | 37 | ## async thread pool 38 | node.async_threads = 32 39 | 40 | ## Erlang Process Limit 41 | node.process_limit = 256000 42 | 43 | ## Sets the maximum number of simultaneously existing ports for this system 44 | node.max_ports = 65536 45 | 46 | ## Set the distribution buffer busy limit (dist_buf_busy_limit) 47 | node.dist_buffer_size = 32MB 48 | 49 | ## Max ETS Tables. 50 | ## Note that mnesia and SSL will create temporary ets tables. 51 | node.max_ets_tables = 256000 52 | 53 | ## Tweak GC to run more often 54 | node.fullsweep_after = 1000 55 | 56 | ## Crash dump 57 | node.crash_dump = /var/log/emqttd/crash.dump 58 | 59 | ## Distributed node ticktime 60 | node.dist_net_ticktime = 60 61 | 62 | ## Distributed node port range 63 | node.dist_listen_min = 6369 64 | node.dist_listen_max = 6379 65 | 66 | ##-------------------------------------------------------------------- 67 | ## Log 68 | ##-------------------------------------------------------------------- 69 | 70 | ## Set the log dir 71 | log.dir = /var/log/emqttd 72 | 73 | ## Console log. Enum: off, file, console, both 74 | log.console = file 75 | 76 | ## Console log level. Enum: debug, info, notice, warning, error, critical, alert, emergency 77 | log.console.level = info 78 | 79 | ## Syslog. Enum: on, off 80 | log.syslog = on 81 | 82 | ## syslog level. Enum: debug, info, notice, warning, error, critical, alert, emergency 83 | log.syslog.level = info 84 | 85 | ## Console log file 86 | log.console.file = /var/log/emqttd/console.log 87 | 88 | ## Error log file 89 | log.error.file = /var/log/emqttd/error.log 90 | 91 | ## Enable the crash log. Enum: on, off 92 | log.crash = on 93 | 94 | log.crash.file = /var/log/emqttd/crash.log 95 | 96 | ##-------------------------------------------------------------------- 97 | ## Allow Anonymous and Default ACL 98 | ##-------------------------------------------------------------------- 99 | 100 | ## Allow Anonymous authentication 101 | mqtt.allow_anonymous = false 102 | 103 | ## ACL nomatch 104 | mqtt.acl_nomatch = allow 105 | 106 | ## Default ACL File 107 | mqtt.acl_file = /etc/emqttd/acl.conf 108 | 109 | ## Cache ACL for PUBLISH 110 | mqtt.cache_acl = true 111 | 112 | ##-------------------------------------------------------------------- 113 | ## MQTT Protocol 114 | ##-------------------------------------------------------------------- 115 | 116 | ## Max ClientId Length Allowed. 117 | mqtt.max_clientid_len = 1024 118 | 119 | ## Max Packet Size Allowed, 64K by default. 120 | mqtt.max_packet_size = 64KB 121 | 122 | ## Check Websocket Protocol Header. Enum: on, off 123 | mqtt.websocket_protocol_header = on 124 | 125 | ##-------------------------------------------------------------------- 126 | ## MQTT Connection 127 | ##-------------------------------------------------------------------- 128 | 129 | ## Force GC: integer. Value 0 disabled the Force GC. 130 | mqtt.conn.force_gc_count = 100 131 | 132 | ##-------------------------------------------------------------------- 133 | ## MQTT Client 134 | ##-------------------------------------------------------------------- 135 | 136 | ## Client Idle Timeout (Second) 137 | mqtt.client.idle_timeout = 30s 138 | 139 | ## Enable client Stats: on | off 140 | mqtt.client.enable_stats = off 141 | 142 | ##-------------------------------------------------------------------- 143 | ## MQTT Session 144 | ##-------------------------------------------------------------------- 145 | 146 | ## Max Number of Subscriptions, 0 means no limit. 147 | mqtt.session.max_subscriptions = 0 148 | 149 | ## Upgrade QoS? 150 | mqtt.session.upgrade_qos = off 151 | 152 | ## Max Size of the Inflight Window for QoS1 and QoS2 messages 153 | ## 0 means no limit 154 | mqtt.session.max_inflight = 32 155 | 156 | ## Retry Interval for redelivering QoS1/2 messages. 157 | mqtt.session.retry_interval = 20s 158 | 159 | ## Client -> Broker: Max Packets Awaiting PUBREL, 0 means no limit 160 | mqtt.session.max_awaiting_rel = 100 161 | 162 | ## Awaiting PUBREL Timeout 163 | mqtt.session.await_rel_timeout = 20s 164 | 165 | ## Enable Statistics: on | off 166 | mqtt.session.enable_stats = off 167 | 168 | ## Expired after 1 day: 169 | ## w - week 170 | ## d - day 171 | ## h - hour 172 | ## m - minute 173 | ## s - second 174 | mqtt.session.expiry_interval = 2h 175 | 176 | ## Ignore message from self publish 177 | mqtt.session.ignore_loop_deliver = false 178 | 179 | ##-------------------------------------------------------------------- 180 | ## MQTT Message Queue 181 | ##-------------------------------------------------------------------- 182 | 183 | ## Type: simple | priority 184 | mqtt.mqueue.type = simple 185 | 186 | ## Topic Priority: 0~255, Default is 0 187 | ## mqtt.mqueue.priority = topic/1=10,topic/2=8 188 | 189 | ## Max queue length. Enqueued messages when persistent client disconnected, 190 | ## or inflight window is full. 0 means no limit. 191 | mqtt.mqueue.max_length = 1000 192 | 193 | ## Low-water mark of queued messages 194 | mqtt.mqueue.low_watermark = 20% 195 | 196 | ## High-water mark of queued messages 197 | mqtt.mqueue.high_watermark = 60% 198 | 199 | ## Queue Qos0 messages? 200 | mqtt.mqueue.store_qos0 = true 201 | 202 | ##-------------------------------------------------------------------- 203 | ## MQTT Broker and PubSub 204 | ##-------------------------------------------------------------------- 205 | 206 | ## System Interval of publishing broker $SYS Messages 207 | mqtt.broker.sys_interval = 60 208 | 209 | ## PubSub Pool Size. Default should be scheduler numbers. 210 | mqtt.pubsub.pool_size = 8 211 | mqtt.pubsub.by_clientid = true 212 | 213 | ## Subscribe Asynchronously 214 | mqtt.pubsub.async = true 215 | 216 | ##-------------------------------------------------------------------- 217 | ## MQTT Bridge 218 | ##-------------------------------------------------------------------- 219 | 220 | ## Bridge Queue Size 221 | mqtt.bridge.max_queue_len = 10000 222 | 223 | ## Ping Interval of bridge node. Unit: Second 224 | mqtt.bridge.ping_down_interval = 1 225 | 226 | ##------------------------------------------------------------------- 227 | ## MQTT Plugins 228 | ##------------------------------------------------------------------- 229 | 230 | ## Dir of plugins' config 231 | mqtt.plugins.etc_dir =/etc/emqttd/plugins/ 232 | 233 | ## File to store loaded plugin names. 234 | mqtt.plugins.loaded_file = /var/lib/emqttd/loaded_plugins 235 | 236 | ##-------------------------------------------------------------------- 237 | ## MQTT Listeners 238 | ##-------------------------------------------------------------------- 239 | 240 | ##-------------------------------------------------------------------- 241 | ## External TCP Listener 242 | 243 | ## External TCP Listener: 1883, 127.0.0.1:1883, ::1:1883 244 | listener.tcp.external = 0.0.0.0:1883 245 | 246 | ## Size of acceptor pool 247 | listener.tcp.external.acceptors = 16 248 | 249 | ## Maximum number of concurrent clients 250 | listener.tcp.external.max_clients = 102400 251 | 252 | ## Rate Limit. Format is 'burst,rate', Unit is KB/Sec 253 | listener.tcp.external.access.2 = allow all 254 | 255 | ## TCP Socket Options 256 | listener.tcp.external.backlog = 1024 257 | listener.tcp.external.buffer = 4KB 258 | listener.tcp.external.nodelay = true 259 | 260 | ##-------------------------------------------------------------------- 261 | ## Internal TCP Listener 262 | 263 | ## Internal TCP Listener: 11883, 127.0.0.1:11883, ::1:11883 264 | listener.tcp.internal = 127.0.0.1:11883 265 | 266 | ## Size of acceptor pool 267 | listener.tcp.internal.acceptors = 16 268 | 269 | ## Maximum number of concurrent clients 270 | listener.tcp.internal.max_clients = 102400 271 | 272 | ## TCP Socket Options 273 | listener.tcp.internal.backlog = 512 274 | listener.tcp.internal.tune_buffer = on 275 | listener.tcp.internal.buffer = 1MB 276 | listener.tcp.internal.recbuf = 4KB 277 | listener.tcp.internal.sndbuf = 1MB 278 | listener.tcp.internal.nodelay = true 279 | 280 | ##-------------------------------------------------------------------- 281 | ## External SSL Listener 282 | 283 | ## SSL Listener: 8883, 127.0.0.1:8883, ::1:8883 284 | listener.ssl.external = 8883 285 | 286 | ## Size of acceptor pool 287 | listener.ssl.external.acceptors = 16 288 | 289 | ## Maximum number of concurrent clients 290 | listener.ssl.external.max_clients = 1024 291 | 292 | ## Proxy Protocol V1/2 293 | 294 | listener.ssl.external.access.1 = allow all 295 | 296 | ### SSL Options. See http://erlang.org/doc/man/ssl.html 297 | 298 | listener.ssl.external.handshake_timeout = 15s 299 | listener.ssl.external.keyfile = /etc/emqttd/certs/key.pem 300 | listener.ssl.external.certfile = /etc/emqttd/certs/cert.pem 301 | 302 | ## listener.ssl.external.ciphers = ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA 303 | 304 | ##-------------------------------------------------------------------- 305 | ## External MQTT/WebSocket Listener 306 | 307 | listener.ws.external = 8083 308 | listener.ws.external.acceptors = 4 309 | listener.ws.external.max_clients = 64 310 | listener.ws.external.access.1 = allow all 311 | 312 | ## TCP Options 313 | listener.ws.external.backlog = 1024 314 | listener.ws.external.recbuf = 4KB 315 | listener.ws.external.sndbuf = 4KB 316 | listener.ws.external.buffer = 4KB 317 | listener.ws.external.nodelay = true 318 | 319 | ##-------------------------------------------------------------------- 320 | ## External MQTT/WebSocket/SSL Listener 321 | 322 | listener.wss.external = 8084 323 | listener.wss.external.acceptors = 4 324 | listener.wss.external.max_clients = 64 325 | listener.wss.external.access.1 = allow all 326 | 327 | ## SSL Options 328 | listener.wss.external.handshake_timeout = 15s 329 | listener.wss.external.keyfile = /etc/emqttd/certs/key.pem 330 | listener.wss.external.certfile = /etc/emqttd/certs/cert.pem 331 | 332 | ##-------------------------------------------------------------------- 333 | ## HTTP Management API Listener 334 | 335 | listener.api.mgmt = 127.0.0.1:8080 336 | 337 | listener.api.mgmt.acceptors = 4 338 | 339 | listener.api.mgmt.max_clients = 64 340 | 341 | listener.api.mgmt.access.1 = allow all 342 | 343 | ##------------------------------------------------------------------- 344 | ## System Monitor 345 | ##------------------------------------------------------------------- 346 | 347 | ## Long GC, don't monitor in production mode for: 348 | ## https://github.com/erlang/otp/blob/feb45017da36be78d4c5784d758ede619fa7bfd3/erts/emulator/beam/erl_gc.c#L421 349 | sysmon.long_gc = false 350 | 351 | ## Long Schedule(ms) 352 | sysmon.long_schedule = 240 353 | 354 | ## 8M words. 32MB on 32-bit VM, 64MB on 64-bit VM. 355 | sysmon.large_heap = 8MB 356 | 357 | ## Busy Port 358 | sysmon.busy_port = false 359 | 360 | ## Busy Dist Port 361 | sysmon.busy_dist_port = true 362 | -------------------------------------------------------------------------------- /etc/emqttd/plugins/emq_auth_pgsql.conf: -------------------------------------------------------------------------------- 1 | ## Postgre Server 2 | auth.pgsql.server = 127.0.0.1:5432 3 | auth.pgsql.pool = 1 4 | auth.pgsql.username = postgres 5 | auth.pgsql.password = postgres 6 | auth.pgsql.database = iris_dev 7 | auth.pgsql.encoding = utf8 8 | auth.pgsql.ssl = false 9 | 10 | ## Variables: %u = username, %c = clientid, %a = ipaddress 11 | auth.pgsql.auth_query = SELECT devices.password FROM public.devices, public.users WHERE devices.client_id = '%c' AND users.email = '%u' AND devices.user_id = users.id AND users.disabled_at IS NULL LIMIT 1 12 | 13 | auth.pgsql.password_hash = plain 14 | 15 | ## Superuser Query 16 | auth.pgsql.super_query = SELECT roles.admin FROM public.users, public.roles WHERE roles.id = users.role_id AND users.email = '%u' LIMIT 1 17 | #auth.pgsql.acl_query = SELECT allow, ipaddr, username, clientid, access, topic FROM mqtt_acl WHERE ipaddr = '%a' OR username = '%u' OR username = '$all' OR clientid = '%c' 18 | #auth.pgsql.acl_nomatch = deny 19 | -------------------------------------------------------------------------------- /lib/iris.ex: -------------------------------------------------------------------------------- 1 | defmodule Iris do 2 | @moduledoc """ 3 | Iris keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /lib/iris/accounts/accounts.ex: -------------------------------------------------------------------------------- 1 | defmodule Iris.Accounts do 2 | @moduledoc """ 3 | The Accounts context. 4 | """ 5 | import Ecto.Query, warn: false 6 | import Ecto 7 | alias Iris.{Role, User, Repo, Device} 8 | 9 | #### Users 10 | 11 | @doc """ 12 | Returns the list of users. 13 | """ 14 | def list_users do 15 | Repo.all(User) 16 | end 17 | 18 | @doc """ 19 | Gets a single user. 20 | 21 | Raises `Ecto.NoResultsError` if the User does not exist. 22 | """ 23 | def get_user!(id), do: Repo.get!(User, id) 24 | 25 | def get_user_by_email(email) do 26 | Repo.get_by(User, email: email) 27 | end 28 | 29 | @doc """ 30 | Creates a user. 31 | """ 32 | def create_user(attributes \\ %{}) do 33 | attributes = check_user_attributes(attributes) 34 | %User{} 35 | |> User.changeset(attributes) 36 | |> Repo.insert() 37 | end 38 | 39 | defp check_user_attributes(attributes) do 40 | case Map.get(attributes, :role_id) do 41 | nil -> 42 | [role|_] = Repo.all(from role in Role, where: [admin: false]) 43 | Map.put(attributes, :role_id, role.id) 44 | |> AtomicMap.convert(safe: true); 45 | _ -> attributes 46 | end 47 | end 48 | 49 | @doc """ 50 | Updates a user. 51 | """ 52 | def update_user(%User{} = user, attributes) do 53 | user 54 | |> User.changeset(attributes) 55 | |> Repo.update() 56 | end 57 | 58 | @doc """ 59 | Deletes a User. 60 | """ 61 | def delete_user(%User{} = user) do 62 | Repo.delete(user) 63 | end 64 | 65 | @doc """ 66 | Returns an `%Ecto.Changeset{}` for tracking user changes. 67 | """ 68 | def change_user(%User{} = user) do 69 | User.changeset(user, %{}) 70 | end 71 | 72 | #### Devices 73 | 74 | @doc """ 75 | Returns a list of the devices that the user owns. 76 | """ 77 | def list_devices_by_user(user) do 78 | Repo.all(assoc(user, :devices)) 79 | end 80 | 81 | @doc """ 82 | Gets a signle device 83 | """ 84 | def get_device!(id, user) do 85 | Repo.get!(assoc(user, :devices), id) 86 | end 87 | 88 | @doc """ 89 | Creates a device for the specified user. 90 | """ 91 | def create_device(attributes \\ %{}, user) do 92 | build_assoc(user, :devices) 93 | |> struct(%{client_id: random_string(20), access_token: random_string(30), password: random_string(5) }) 94 | |> Device.changeset(attributes) 95 | |> Repo.insert() 96 | end 97 | 98 | @doc """ 99 | Updates a device. 100 | """ 101 | def update_device(%Device{} = device, attributes) do 102 | device 103 | |> Device.changeset(attributes) 104 | |> Repo.update() 105 | end 106 | 107 | @doc """ 108 | Deletes a device. 109 | """ 110 | def delete_device(%Device{} = device) do 111 | Repo.delete(device) 112 | end 113 | 114 | @doc """ 115 | Returns an `%Ecto.Changeset{}` for tracking user changes. 116 | """ 117 | def change_device(%Device{} = device) do 118 | Device.changeset(device, %{}) 119 | end 120 | 121 | @doc false 122 | defp random_string(length) do 123 | :crypto.strong_rand_bytes(length) |> Base.encode32 |> binary_part(0, length) 124 | end 125 | 126 | end 127 | -------------------------------------------------------------------------------- /lib/iris/accounts/device.ex: -------------------------------------------------------------------------------- 1 | defmodule Iris.Device do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | schema "devices" do 6 | field :name, :string 7 | field :password, :string 8 | field :access_token, :string 9 | field :client_id, :string 10 | field :status, :boolean, default: false 11 | field :last_synced, Ecto.DateTime 12 | 13 | belongs_to :user, Iris.User 14 | 15 | timestamps() 16 | end 17 | 18 | @doc """ 19 | Builds a changeset based on the `struct` and `params`. 20 | """ 21 | def changeset(struct, params \\ %{}) do 22 | struct 23 | |> cast(params, [:name, :password, :client_id, :access_token, :status, :last_synced, :user_id]) 24 | |> validate_required([:name, :password, :client_id, :access_token, :user_id]) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/iris/accounts/role.ex: -------------------------------------------------------------------------------- 1 | defmodule Iris.Role do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | schema "roles" do 6 | field :title, :string 7 | field :admin, :boolean, default: false 8 | 9 | timestamps() 10 | end 11 | 12 | @doc """ 13 | Builds a changeset based on the `struct` and `params`. 14 | """ 15 | def changeset(struct, params \\ %{}) do 16 | struct 17 | |> cast(params, [:title, :admin]) 18 | |> validate_required([:title, :admin]) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/iris/accounts/user.ex: -------------------------------------------------------------------------------- 1 | defmodule Iris.User do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | alias Iris.{Repo, Role, User, Device} 6 | 7 | schema "users" do 8 | field :email, :string 9 | field :access_token, :string 10 | field :disabled_at, Ecto.DateTime 11 | field :disabled, :boolean, virtual: true 12 | 13 | belongs_to :role, Role 14 | has_many :devices, Device 15 | 16 | timestamps() 17 | end 18 | 19 | @doc """ 20 | Builds a changeset based on the `struct` and `params`. 21 | """ 22 | def changeset(%User{} = user, attributes) do 23 | user 24 | |> cast(attributes, [:email, :role_id]) 25 | |> validate_required([:email, :role_id]) 26 | |> validate_format(:email, ~r/@/) 27 | |> unique_constraint(:email) 28 | end 29 | 30 | @doc """ 31 | Check if the user role is admin. 32 | """ 33 | def is_admin?(user) do 34 | (role = Repo.get(Role, user.role_id)) && role.admin 35 | end 36 | 37 | @doc """ 38 | If the disabled_at is not empty then the user is not active. 39 | """ 40 | def is_active?(user) do 41 | user.disabled_at == nil 42 | end 43 | 44 | end 45 | -------------------------------------------------------------------------------- /lib/iris/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Iris.Application do 2 | use Application 3 | 4 | def start(_type, _args) do 5 | import Supervisor.Spec 6 | 7 | children = [ 8 | supervisor(Iris.Repo, []), 9 | supervisor(IrisWeb.Endpoint, []), 10 | supervisor(Mqtt.Supervisor, []) 11 | ] 12 | 13 | opts = [strategy: :one_for_one, name: Iris.Supervisor] 14 | Supervisor.start_link(children, opts) 15 | end 16 | 17 | def config_change(changed, _new, removed) do 18 | IrisWeb.Endpoint.config_change(changed, removed) 19 | :ok 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/iris/ecto_enums.ex: -------------------------------------------------------------------------------- 1 | import EctoEnum 2 | defenum ServiceTypeEnum, :service_type, [:P, :W, :A, :S, :O] 3 | defenum TokenTypeEnum, :token_type, [:auth, :deviceActivation] 4 | -------------------------------------------------------------------------------- /lib/iris/mailer.ex: -------------------------------------------------------------------------------- 1 | defmodule Iris.Mailer do 2 | use Bamboo.Mailer, otp_app: :iris 3 | end 4 | -------------------------------------------------------------------------------- /lib/iris/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule Iris.Repo do 2 | use Ecto.Repo, otp_app: :iris 3 | end 4 | -------------------------------------------------------------------------------- /lib/iris_web.ex: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, views, channels and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use IrisWeb, :controller 9 | use IrisWeb, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. Instead, define any helper function in modules 17 | and import those modules here. 18 | """ 19 | 20 | def controller do 21 | quote do 22 | use Phoenix.Controller, namespace: IrisWeb 23 | import Plug.Conn 24 | import IrisWeb.Router.Helpers 25 | import IrisWeb.Gettext 26 | end 27 | end 28 | 29 | def view do 30 | quote do 31 | use Phoenix.View, root: "lib/iris_web/templates", namespace: IrisWeb 32 | 33 | # Import convenience functions from controllers 34 | import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1] 35 | 36 | # Use all HTML functionality (forms, tags, etc) 37 | use Phoenix.HTML 38 | 39 | import IrisWeb.Router.Helpers 40 | import IrisWeb.ErrorHelpers 41 | import IrisWeb.Gettext 42 | end 43 | end 44 | 45 | def router do 46 | quote do 47 | use Phoenix.Router 48 | import Plug.Conn 49 | import Phoenix.Controller 50 | end 51 | end 52 | 53 | def channel do 54 | quote do 55 | use Phoenix.Channel 56 | import IrisWeb.Gettext 57 | end 58 | end 59 | 60 | @doc """ 61 | When used, dispatch to the appropriate controller/view/etc. 62 | """ 63 | defmacro __using__(which) when is_atom(which) do 64 | apply(__MODULE__, which, []) 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/iris_web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | # channel "room:*", Iris.RoomChannel 6 | 7 | ## Transports 8 | transport :websocket, Phoenix.Transports.WebSocket 9 | # transport :longpoll, Phoenix.Transports.LongPoll 10 | 11 | # Socket params are passed from the client and can 12 | # be used to verify and authenticate a user. After 13 | # verification, you can put default assigns into 14 | # the socket that will be set for all channels, ie 15 | # 16 | # {:ok, assign(socket, :user_id, verified_user_id)} 17 | # 18 | # To deny connection, return `:error`. 19 | # 20 | # See `Phoenix.Token` documentation for examples in 21 | # performing token verification on connect. 22 | def connect(_params, socket) do 23 | {:ok, socket} 24 | end 25 | 26 | # Socket id's are topics that allow you to identify all sockets for a given user: 27 | # 28 | # def id(socket), do: "users_socket:#{socket.assigns.user_id}" 29 | # 30 | # Would allow you to broadcast a "disconnect" event and terminate 31 | # all active sockets and channels for a given user: 32 | # 33 | # IrisWeb.Endpoint.broadcast("users_socket:#{user.id}", "disconnect", %{}) 34 | # 35 | # Returning `nil` makes this socket anonymous. 36 | def id(_socket), do: nil 37 | end 38 | -------------------------------------------------------------------------------- /lib/iris_web/controllers/device_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.DeviceController do 2 | use IrisWeb, :controller 3 | 4 | alias Iris.{Accounts, Device} 5 | 6 | plug Iris.Plugs.AuthedicateUser 7 | 8 | def index(conn, _params) do 9 | user = get_session(conn, :current_user) 10 | devices = Accounts.list_devices_by_user(user) 11 | render(conn, "index.html", devices: devices) 12 | end 13 | 14 | def new(conn, _params) do 15 | changeset = Accounts.change_device(%Device{status: false}) 16 | render(conn, "new.html", changeset: changeset) 17 | end 18 | 19 | def create(conn, %{"device" => device_params}) do 20 | user = get_session(conn, :current_user) 21 | case Accounts.create_device(device_params, user) do 22 | {:ok, _device} -> 23 | conn 24 | |> put_flash(:info, "Device created successfully.") 25 | |> redirect(to: device_path(conn, :index)) 26 | {:error, changeset} -> 27 | render(conn, "new.html", changeset: changeset) 28 | end 29 | end 30 | 31 | def show(conn, %{"id" => id}) do 32 | user = get_session(conn, :current_user) 33 | device = Accounts.get_device!(id, user) 34 | render(conn, "show.html", device: device) 35 | end 36 | 37 | def edit(conn, %{"id" => id}) do 38 | user = get_session(conn, :current_user) 39 | device = Accounts.get_device!(id, user) 40 | changeset = Accounts.change_device(device) 41 | render(conn, "edit.html", device: device, changeset: changeset) 42 | end 43 | 44 | def update(conn, %{"id" => id, "device" => device_params}) do 45 | user = get_session(conn, :current_user) 46 | device = Accounts.get_device!(id, user) 47 | case Accounts.update_device(device, device_params) do 48 | {:ok, device} -> 49 | conn 50 | |> put_flash(:info, "Device updated successfully.") 51 | |> redirect(to: device_path(conn, :show, device)) 52 | {:error, changeset} -> 53 | render(conn, "edit.html", device: device, changeset: changeset) 54 | end 55 | end 56 | 57 | def delete(conn, %{"id" => id}) do 58 | user = get_session(conn, :current_user) 59 | device = Accounts.get_device!(id, user) 60 | case Accounts.delete_device(device) do 61 | {:ok, _device} -> 62 | conn 63 | |> put_flash(:info, "Device deleted successfully.") 64 | |> redirect(to: device_path(conn, :index)) 65 | {:error, _} -> 66 | redirect(conn, to: device_path(conn, :index)) 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/iris_web/controllers/message_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.MessageController do 2 | use IrisWeb, :controller 3 | 4 | alias Iris.{Message, Repo} 5 | 6 | def index(conn, _params) do 7 | messages = Repo.all(Message) 8 | render(conn, "index.json", messages: messages) 9 | end 10 | 11 | def create(conn, message_params) do 12 | changeset = Message.changeset(%Message{}, message_params) 13 | case Repo.insert(changeset) do 14 | {:ok, message} -> 15 | conn 16 | |> put_status(:created) 17 | |> put_resp_header("location", message_path(conn, :show, message)) 18 | |> render("show.json", message: message) 19 | {:error, changeset} -> 20 | conn 21 | |> put_status(:unprocessable_entity) 22 | |> render(IrisWeb.ChangesetView, "error.json", changeset: changeset) 23 | end 24 | end 25 | 26 | def show(conn, %{"id" => id}) do 27 | message = Repo.get!(Message, id) 28 | render(conn, "show.json", message: message) 29 | end 30 | 31 | def update(conn, message_params) do 32 | message = Repo.get!(Message, message_params["id"]) 33 | changeset = Message.changeset(message, message_params) 34 | 35 | case Repo.update(changeset) do 36 | {:ok, message} -> 37 | render(conn, "show.json", message: message) 38 | {:error, changeset} -> 39 | conn 40 | |> put_status(:unprocessable_entity) 41 | |> render(IrisWeb.ChangesetView, "error.json", changeset: changeset) 42 | end 43 | end 44 | 45 | def delete(conn, %{"id" => id}) do 46 | message = Repo.get!(Message, id) 47 | Repo.delete!(message) 48 | send_resp(conn, :no_content, "") 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/iris_web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.PageController do 2 | use IrisWeb, :controller 3 | 4 | plug Iris.Plugs.AuthedicateUser when action != :index 5 | 6 | def index(conn, _) do 7 | render conn, "index.html" 8 | end 9 | 10 | def dashboard(conn, _) do 11 | render conn, "dashboard.html" 12 | end 13 | 14 | def settings(conn, _) do 15 | render conn, "settings.html" 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /lib/iris_web/controllers/registration_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.RegistrationController do 2 | use IrisWeb, :controller 3 | 4 | alias Iris.{Accounts, User, Mailer} 5 | alias IrisWeb.Email 6 | 7 | def signup(%{method: "GET"} = conn, _params) do 8 | render conn, "signup.html", changeset: Accounts.change_user(%User{}) 9 | end 10 | 11 | def signup(%{method: "POST"} = conn, %{"user" => %{"email" => email}}) when is_nil(email) do 12 | conn 13 | |> put_flash(:info, "You have to provide an email address.") 14 | |> redirect(to: registration_path(conn, :signup)) 15 | end 16 | 17 | def signup(%{method: "POST"} = conn, %{"user" => user_params}) do 18 | case Accounts.get_user_by_email(user_params["email"]) do 19 | nil -> create_user(conn, user_params) 20 | _ -> succes_signup(conn) # Don't leak which email address is already used 21 | end 22 | end 23 | 24 | defp create_user(conn, user_params) do 25 | case Accounts.create_user(user_params) do 26 | {:ok, user} -> 27 | Email.welcome(user) 28 | |> Mailer.deliver_later 29 | succes_signup(conn) 30 | {:error, changeset} -> 31 | render conn, "signup.html", changeset: changeset 32 | end 33 | end 34 | 35 | defp succes_signup(conn) do 36 | conn 37 | |> put_flash(:info, "You signed up successfully.") 38 | |> redirect(to: session_path(conn, :login)) 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /lib/iris_web/controllers/service_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.ServiceController do 2 | use IrisWeb, :controller 3 | 4 | alias Iris.{Service, Repo} 5 | 6 | plug Iris.Plugs.AuthedicateUser 7 | 8 | def index(conn, _params) do 9 | services = Repo.all(Service) 10 | render(conn, "index.json", services: services) 11 | end 12 | 13 | def create(conn, service_params) do 14 | changeset = Service.changeset(%Service{}, service_params) 15 | 16 | case Repo.insert(changeset) do 17 | {:ok, service} -> 18 | conn 19 | |> put_status(:created) 20 | |> put_resp_header("location", service_path(conn, :show, service)) 21 | |> render("show.json", service: service) 22 | {:error, changeset} -> 23 | conn 24 | |> put_status(:unprocessable_entity) 25 | |> render(IrisWeb.ChangesetView, "error.json", changeset: changeset) 26 | end 27 | end 28 | 29 | def show(conn, %{"id" => id}) do 30 | service = Repo.get!(Service, id) 31 | render(conn, "show.json", service: service) 32 | end 33 | 34 | def update(conn, service_params) do 35 | service = Repo.get!(Service, service_params["id"]) 36 | changeset = Service.changeset(service, service_params) 37 | 38 | case Repo.update(changeset) do 39 | {:ok, service} -> 40 | render(conn, "show.json", service: service) 41 | {:error, changeset} -> 42 | conn 43 | |> put_status(:unprocessable_entity) 44 | |> render(IrisWeb.ChangesetView, "error.json", changeset: changeset) 45 | end 46 | end 47 | 48 | def delete(conn, %{"id" => id}) do 49 | service = Repo.get!(Service, id) 50 | Repo.delete!(service) 51 | send_resp(conn, :no_content, "") 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/iris_web/controllers/session_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.SessionController do 2 | use IrisWeb, :controller 3 | 4 | alias Iris.{Accounts, User, Support.AuthenticationToken, Repo} 5 | 6 | def login(%{method: "GET"} = conn, %{"t" => token}) do 7 | case AuthenticationToken.verify_token_value(token) do 8 | {:ok, user} -> 9 | user = Iris.User 10 | |> Repo.get(user.id) 11 | |> Repo.preload(:role) 12 | conn 13 | |> assign(:current_user, user) 14 | |> put_session(:current_user, user) 15 | |> configure_session(renew: true) 16 | |> put_flash(:info, "You signed in successfully.") 17 | |> redirect(to: page_path(conn, :dashboard)) 18 | 19 | {:error, _reason} -> 20 | conn 21 | |> put_flash(:error, "Invalid token.") 22 | |> redirect(to: session_path(conn, :login)) 23 | end 24 | end 25 | 26 | def login(%{method: "GET"} = conn, _) do 27 | render conn, "login.html", changeset: Accounts.change_user(%User{}) 28 | end 29 | 30 | def login(%{method: "POST"} = conn, %{"user" => %{"email" => email}}) when is_nil(email) do 31 | conn 32 | |> put_flash(:info, "You have to provide an email address.") 33 | |> redirect(to: session_path(conn, :login)) 34 | end 35 | 36 | def login(%{method: "POST"} = conn, %{"user" => %{"email" => email}}) when not is_nil(email) do 37 | AuthenticationToken.send_token(email) 38 | conn 39 | |> put_flash(:info, "Check your inbox.") 40 | |> redirect(to: page_path(conn, :index)) 41 | end 42 | 43 | def logout(conn, _) do 44 | conn 45 | |> delete_session(:current_user) 46 | |> put_flash(:info, "Logout successfully!") 47 | |> redirect(to: page_path(conn, :index)) 48 | end 49 | 50 | end 51 | -------------------------------------------------------------------------------- /lib/iris_web/controllers/user_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.UserController do 2 | use IrisWeb, :controller 3 | 4 | alias Iris.{User, Accounts} 5 | 6 | plug Iris.Plugs.AuthedicateUser 7 | plug Iris.Plugs.AuthedicateAdmin 8 | 9 | def index(conn, _params) do 10 | users = Accounts.list_users() 11 | render(conn, "index.html", users: users) 12 | end 13 | 14 | def new(conn, _params) do 15 | changeset = Accounts.change_user(%User{}) 16 | render(conn, "new.html", changeset: changeset) 17 | end 18 | 19 | def create(conn, %{"user" => user_params}) do 20 | case Accounts.create_user(user_params) do 21 | {:ok, _user} -> 22 | conn 23 | |> put_flash(:info, "User created successfully.") 24 | |> redirect(to: user_path(conn, :index)) 25 | {:error, changeset} -> 26 | render(conn, "new.html", changeset: changeset) 27 | end 28 | end 29 | 30 | def show(conn, %{"id" => id}) do 31 | user = Accounts.get_user!(id) 32 | render(conn, "show.html", user: user) 33 | end 34 | 35 | def edit(conn, %{"id" => id}) do 36 | user = Accounts.get_user!(id) 37 | changeset = Accounts.change_user(user) 38 | render(conn, "edit.html", user: user, changeset: changeset) 39 | end 40 | 41 | def update(conn, %{"id" => id, "user" => user_params}) do 42 | user = Accounts.get_user!(id) 43 | case Accounts.update_user(user, user_params) do 44 | {:ok, user} -> 45 | conn 46 | |> put_flash(:info, "User updated successfully.") 47 | |> redirect(to: user_path(conn, :show, user)) 48 | {:error, changeset} -> 49 | render(conn, "edit.html", user: user, changeset: changeset) 50 | end 51 | end 52 | 53 | def delete(conn, %{"id" => id}) do 54 | user = Accounts.get_user!(id) 55 | {:ok, _user} = Accounts.delete_user(user) 56 | 57 | conn 58 | |> put_flash(:info, "User deleted successfully.") 59 | |> redirect(to: user_path(conn, :index)) 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/iris_web/email.ex: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.Email do 2 | 3 | use Bamboo.Phoenix, view: IrisWeb.EmailView 4 | 5 | @doc """ 6 | Email containing the magic login. 7 | """ 8 | def login_link(auth_token, user) do 9 | hostname = get_host_name() 10 | new_email() 11 | |> to(user.email) 12 | |> from("no-reply@#{hostname}") 13 | |> subject("Login link") 14 | |> put_html_layout({IrisWeb.LayoutView, "email.html"}) 15 | |> render(:login, token: auth_token) 16 | |> put_text_layout({IrisWeb.LayoutView, "email.text"}) 17 | |> render(:login, token: auth_token) 18 | end 19 | 20 | @doc """ 21 | Welcoming email. 22 | """ 23 | def welcome(user) do 24 | hostname = get_host_name() 25 | new_email() 26 | |> to(user.email) 27 | |> from("no-reply@#{hostname}") 28 | |> subject("Welcome") 29 | |> put_html_layout({IrisWeb.LayoutView, "email.html"}) 30 | |> render(:welcome, user: user) 31 | |> put_text_layout({IrisWeb.LayoutView, "email.text"}) 32 | |> render(:welcome, user: user) 33 | end 34 | 35 | @doc false 36 | defp get_host_name() do 37 | [{:url, [{:host, hostname} | _]} | _] = Application.get_env(:iris, IrisWeb.Endpoint) 38 | hostname 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /lib/iris_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :iris 3 | 4 | socket "/socket", IrisWeb.UserSocket 5 | 6 | # Serve at "/" the static files from "priv/static" directory. 7 | # 8 | # You should set gzip to true if you are running phoenix.digest 9 | # when deploying your static files in production. 10 | plug Plug.Static, 11 | at: "/", from: :iris, gzip: false, 12 | only: ~w(css fonts images js favicon.ico robots.txt) 13 | 14 | # Code reloading can be explicitly enabled under the 15 | # :code_reloader configuration of your endpoint. 16 | if code_reloading? do 17 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 18 | plug Phoenix.LiveReloader 19 | plug Phoenix.CodeReloader 20 | end 21 | 22 | plug Plug.RequestId 23 | plug Plug.Logger 24 | 25 | plug Plug.Parsers, 26 | parsers: [:urlencoded, :multipart, :json], 27 | pass: ["*/*"], 28 | json_decoder: Poison 29 | 30 | plug Plug.MethodOverride 31 | plug Plug.Head 32 | 33 | # The session will be stored in the cookie and signed, 34 | # this means its contents can be read but not tampered with. 35 | # Set :encryption_salt if you would also like to encrypt it. 36 | plug Plug.Session, 37 | store: :cookie, 38 | key: "_iris_key", 39 | signing_salt: "gjqVZk90" 40 | 41 | plug IrisWeb.Router 42 | 43 | @doc """ 44 | Callback invoked for dynamically configuring the endpoint. 45 | 46 | It receives the endpoint configuration and checks if 47 | configuration should be loaded from the system environment. 48 | """ 49 | def init(_key, config) do 50 | if config[:load_from_system_env] do 51 | port = System.get_env("PORT") || raise "expected the PORT environment variable to be set" 52 | {:ok, Keyword.put(config, :http, [:inet6, port: port])} 53 | else 54 | {:ok, config} 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/iris_web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](https://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import Iris.Gettext 9 | 10 | # Simple translation 11 | gettext "Here is the string to translate" 12 | 13 | # Plural translation 14 | ngettext "Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3 17 | 18 | # Domain-based translation 19 | dgettext "errors", "Here is the error message to translate" 20 | 21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :iris 24 | end 25 | -------------------------------------------------------------------------------- /lib/iris_web/models/message.ex: -------------------------------------------------------------------------------- 1 | defmodule Iris.Message do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | schema "messages" do 6 | field :title, :string 7 | field :text, :string 8 | field :priority, :integer 9 | field :link, :string 10 | field :service_token, :string 11 | 12 | timestamps() 13 | end 14 | 15 | @doc """ 16 | Builds a changeset based on the `struct` and `params`. 17 | """ 18 | def changeset(struct, params \\ %{}) do 19 | struct 20 | |> cast(params, [:title, :text, :priority, :link, :service_token]) 21 | |> validate_required([:title, :text, :priority, :link, :service_token]) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/iris_web/models/service.ex: -------------------------------------------------------------------------------- 1 | defmodule Iris.Service do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | schema "services" do 6 | field :name, :string 7 | field :description, :string 8 | field :icon, :string 9 | field :token, :string 10 | field :type, ServiceTypeEnum 11 | 12 | timestamps() 13 | end 14 | 15 | @doc """ 16 | Builds a changeset based on the `struct` and `params`. 17 | """ 18 | def changeset(struct, params \\ %{}) do 19 | struct 20 | |> cast(params, [:name, :description, :icon, :token, :type]) 21 | |> validate_required([:name, :description, :icon, :token, :type]) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/iris_web/models/token.ex: -------------------------------------------------------------------------------- 1 | defmodule Iris.Token do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | alias Iris.User 6 | alias IrisWeb.Endpoint 7 | 8 | schema "tokens" do 9 | field :value, :string 10 | field :type, TokenTypeEnum 11 | 12 | belongs_to :user, User 13 | 14 | timestamps() 15 | end 16 | 17 | 18 | 19 | @doc """ 20 | Creates an token for the specificed user 21 | """ 22 | def create_auth_token(struct, user) do 23 | struct 24 | |> cast(%{}, []) 25 | |> put_assoc(:user, user) 26 | |> put_change(:value, generate_auth_token(user)) 27 | |> put_change(:type, "auth") 28 | |> changeset 29 | end 30 | 31 | @doc """ 32 | Creates an device token for the specificed user 33 | """ 34 | def create_device_token(struct, user) do 35 | struct 36 | |> cast(%{}, []) 37 | |> put_assoc(:user, user) 38 | |> put_change(:value, generate_device_token()) 39 | |> put_change(:type, "device") 40 | |> changeset 41 | end 42 | 43 | def changeset(struct, params \\ %{}) do 44 | struct 45 | |> validate_required([:value, :user, :type]) 46 | |> unique_constraint(:value) 47 | end 48 | 49 | defp generate_device_token() do 50 | :crypto.strong_rand_bytes(8) |> Base.url_encode64 |> binary_part(0, 8) 51 | end 52 | 53 | defp generate_auth_token(nil), do: nil 54 | defp generate_auth_token(user) do 55 | Phoenix.Token.sign(Endpoint, "user", user.id) 56 | end 57 | 58 | end 59 | -------------------------------------------------------------------------------- /lib/iris_web/plugs/authedicate_admin.ex: -------------------------------------------------------------------------------- 1 | defmodule Iris.Plugs.AuthedicateAdmin do 2 | import Plug.Conn 3 | 4 | alias Iris.User 5 | 6 | def init(default), do: default 7 | 8 | def call(conn, _default) do 9 | user = get_session(conn, :current_user) 10 | cond do 11 | user != nil && User.is_admin?(user) -> conn 12 | user != nil -> conn |> send_resp(403, "Forbidden") |> halt() 13 | true -> conn |> send_resp(401, "Unauthorized") |> halt() 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/iris_web/plugs/authedicate_user.ex: -------------------------------------------------------------------------------- 1 | defmodule Iris.Plugs.AuthedicateUser do 2 | import Plug.Conn 3 | 4 | alias Iris.User 5 | 6 | def init(default), do: default 7 | 8 | def call(conn, _default) do 9 | user = get_session(conn, :current_user) 10 | cond do 11 | user && User.is_active?(user) -> conn 12 | user != nil -> conn |> send_resp(403, "Forbidden") |> halt() 13 | true -> conn |> send_resp(401, "Unauthorized") |> halt() 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/iris_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.Router do 2 | use IrisWeb, :router 3 | 4 | pipeline :browser do 5 | plug :accepts, ["html"] 6 | plug :fetch_session 7 | plug :fetch_flash 8 | plug :protect_from_forgery 9 | plug :put_secure_browser_headers 10 | end 11 | 12 | pipeline :api do 13 | plug :accepts, ["json"] 14 | plug :fetch_session 15 | end 16 | 17 | if Mix.env == :dev do 18 | forward "/sent_emails", Bamboo.EmailPreviewPlug 19 | end 20 | 21 | scope "/", IrisWeb do 22 | pipe_through :browser 23 | 24 | get "/", PageController, :index 25 | get "/dashboard", PageController, :dashboard 26 | get "/settings", PageController, :settings 27 | resources "/users", UserController 28 | resources "/devices", DeviceController 29 | 30 | get "/signup", RegistrationController, :signup 31 | post "/signup", RegistrationController, :signup 32 | 33 | get "/login", SessionController, :login 34 | post "/login", SessionController, :login 35 | get "/logout", SessionController, :logout 36 | end 37 | 38 | scope "/api", IrisWeb do 39 | pipe_through :api 40 | 41 | resources "/services", ServiceController, except: [:new, :edit] 42 | resources "/messages", MessageController 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/iris_web/support/AuthedicationToken.ex: -------------------------------------------------------------------------------- 1 | defmodule Iris.Support.AuthenticationToken do 2 | 3 | import Ecto.Query, only: [where: 3] 4 | 5 | alias Iris.{Token, Mailer, Repo, User} 6 | alias IrisWeb.{Endpoint, Email} 7 | 8 | @token_max_age 1800 9 | 10 | def create_token(user) do 11 | changeset = Token.create_auth_token(%Token{}, user) 12 | auth_token = Repo.insert!(changeset) 13 | auth_token.value 14 | end 15 | 16 | def verify_token_value(value) do 17 | Token 18 | |> where([t], t.value == ^value) 19 | |> where([t], t.inserted_at > datetime_add(^Ecto.DateTime.utc, ^(@token_max_age * -1), "second")) 20 | |> where([t], t.type == "auth") 21 | |> Repo.one() 22 | |> verify_token() 23 | end 24 | 25 | defp verify_token(nil), do: {:error, :invalid} 26 | defp verify_token(token) do 27 | token = 28 | token 29 | |> Repo.preload(:user) 30 | |> Repo.delete! 31 | 32 | user_id = token.user.id 33 | case Phoenix.Token.verify(Endpoint, "user", token.value, max_age: @token_max_age) do 34 | {:ok, ^user_id} -> {:ok, token.user} 35 | {:error, reason} -> {:error, reason} 36 | end 37 | end 38 | 39 | def send_token(nil), do: {:error, :not_found} 40 | 41 | def send_token(email) when is_binary(email) do 42 | User 43 | |> Repo.get_by(email: email) 44 | |> send_token() 45 | end 46 | 47 | def send_token(user = %User{}) do 48 | mail_token(user) 49 | end 50 | 51 | def mail_token(nil), do: {:error, :not_found} 52 | 53 | def mail_token(user) do 54 | user 55 | |> create_token() 56 | |> Email.login_link(user) 57 | |> Mailer.deliver_now() 58 | :ok 59 | end 60 | 61 | end 62 | -------------------------------------------------------------------------------- /lib/iris_web/templates/device/edit.html.eex: -------------------------------------------------------------------------------- 1 |

Edit device

2 | 3 | <%= render "form.html", changeset: @changeset, 4 | action: device_path(@conn, :update, @device) %> 5 | 6 | <%= link "Back", to: device_path(@conn, :index) %> 7 | -------------------------------------------------------------------------------- /lib/iris_web/templates/device/form.html.eex: -------------------------------------------------------------------------------- 1 | <%= form_for @changeset, @action, fn f -> %> 2 | <%= if @changeset.action do %> 3 |
4 |

Oops, something went wrong! Please check the errors below.

5 |
6 | <% end %> 7 | 8 |
9 | <%= label f, :name, class: "control-label" %> 10 | <%= text_input f, :name, class: "form-control" %> 11 | <%= error_tag f, :name %> 12 |
13 | 14 |
15 | <%= label f, :status, class: "control-label" %> 16 | <%= checkbox f, :status, class: "form-control" %> 17 | <%= error_tag f, :status %> 18 |
19 | 20 |
21 | <%= submit "Submit", class: "btn btn-primary" %> 22 |
23 | <% end %> 24 | -------------------------------------------------------------------------------- /lib/iris_web/templates/device/index.html.eex: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | <%= for device <- @devices do %> 19 | 20 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | <% end %> 32 | 33 |
NameClient IdStatusLast synced
<%= device.name %><%= device.client_id %><%= device.status %><%= device.last_synced %> 26 | <%= link "Show", to: device_path(@conn, :show, device), class: "btn btn-default btn-xs" %> 27 | <%= link "Edit", to: device_path(@conn, :edit, device), class: "btn btn-default btn-xs" %> 28 | <%= link "Delete", to: device_path(@conn, :delete, device), method: :delete, class: "btn btn-danger btn-xs" %> 29 |
34 | <%= link "New device", to: device_path(@conn, :new) %> 35 |
36 | -------------------------------------------------------------------------------- /lib/iris_web/templates/device/new.html.eex: -------------------------------------------------------------------------------- 1 |

New device

2 | 3 | <%= render "form.html", changeset: @changeset, 4 | action: device_path(@conn, :create) %> 5 | 6 | <%= link "Back", to: device_path(@conn, :index) %> 7 | -------------------------------------------------------------------------------- /lib/iris_web/templates/device/show.html.eex: -------------------------------------------------------------------------------- 1 |

Show device

2 | 3 | 31 | 32 | <%= link "Edit", to: device_path(@conn, :edit, @device) %> 33 | <%= link "Back", to: device_path(@conn, :index) %> 34 | -------------------------------------------------------------------------------- /lib/iris_web/templates/email/login.html.eex: -------------------------------------------------------------------------------- 1 |

Follow this link <%= link "link", to: session_url(IrisWeb.Endpoint, :login, t: @token) %> to login to your account.

2 | -------------------------------------------------------------------------------- /lib/iris_web/templates/email/login.text.eex: -------------------------------------------------------------------------------- 1 | Follow this link <%= session_url(IrisWeb.Endpoint, :login, t: @token) %> to login to your account. 2 | -------------------------------------------------------------------------------- /lib/iris_web/templates/email/welcome.html.eex: -------------------------------------------------------------------------------- 1 |

2 | Welcome to Iris! 3 |

4 | -------------------------------------------------------------------------------- /lib/iris_web/templates/email/welcome.text.eex: -------------------------------------------------------------------------------- 1 | Welcome to Iris! 2 | -------------------------------------------------------------------------------- /lib/iris_web/templates/layout/app.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Iris 11 | "> 12 | 13 | 14 | <%= render "nav.html", assigns %> 15 |
16 | <%= render "messages.html", assigns %> 17 |
18 | <%= render @view_module, @view_template, assigns %> 19 |
20 | <%= render "footer.html", assigns %> 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /lib/iris_web/templates/layout/email.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

Hello!

9 | <%= render @view_module, @view_template, assigns %> 10 |

- The Iris Team.

11 | 12 | 13 | -------------------------------------------------------------------------------- /lib/iris_web/templates/layout/email.text.eex: -------------------------------------------------------------------------------- 1 | Hello! 2 | 3 | <%= render @view_module, @view_template, assigns %> 4 | 5 | - The Iris Team. 6 | -------------------------------------------------------------------------------- /lib/iris_web/templates/layout/footer.html.eex: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /lib/iris_web/templates/layout/messages.html.eex: -------------------------------------------------------------------------------- 1 |
2 | <%= if info = get_flash(@conn, :info) do %> 3 |
4 |
5 | 6 |

Success!

7 |

<%= info %>

8 |
9 |
10 | <% end %> 11 | <% if error = get_flash(@conn, :error) do %> 12 |
13 |
14 | 15 |

Error!

16 |

<%= error %>

17 |
18 |
19 | <% end %> 20 |
21 | -------------------------------------------------------------------------------- /lib/iris_web/templates/layout/nav.html.eex: -------------------------------------------------------------------------------- 1 | 47 | -------------------------------------------------------------------------------- /lib/iris_web/templates/page/dashboard.html.eex: -------------------------------------------------------------------------------- 1 |
2 | <%= render "notification_form.html" %> 3 |
4 |
5 | -------------------------------------------------------------------------------- /lib/iris_web/templates/page/devices.html.eex: -------------------------------------------------------------------------------- 1 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
#NameStatusLast Synced
1blabladisabledabout 13 hours ago
23 |
24 | -------------------------------------------------------------------------------- /lib/iris_web/templates/page/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 | 6 |
7 |
8 | Download the android client here... 9 |
10 | -------------------------------------------------------------------------------- /lib/iris_web/templates/page/notification_form.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 | Send notification 4 |
5 | 6 | 10 |
11 | 12 |
13 | 14 | 18 |
19 | 20 |
21 | 22 | 23 |
24 | 25 |
26 | 27 | 28 |
29 | 30 |
31 | 32 | 33 |
34 | 35 |
36 |
37 | -------------------------------------------------------------------------------- /lib/iris_web/templates/page/services.html.eex: -------------------------------------------------------------------------------- 1 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
#NameDescription
1SSHopen ssh
21 |
22 | -------------------------------------------------------------------------------- /lib/iris_web/templates/page/settings.html.eex: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 | -------------------------------------------------------------------------------- /lib/iris_web/templates/registration/signup.html.eex: -------------------------------------------------------------------------------- 1 |

Signup

2 | 3 | <%= form_for @conn, registration_path(@conn, :signup), [as: :user], fn f -> %> 4 |
5 | <%= label f, :email, class: "control-label" %> 6 | <%= text_input f, :email, class: "form-control", autofocus: true %> 7 | <%= error_tag f, :email %> 8 |
9 | 10 |
11 | <%= submit "Signup", class: "btn btn-primary" %> 12 |
13 | <% end %> 14 | -------------------------------------------------------------------------------- /lib/iris_web/templates/session/login.html.eex: -------------------------------------------------------------------------------- 1 |

Login

2 | 3 | <%= form_for @conn, session_path(@conn, :login), [as: :user], fn f -> %> 4 |
5 | <%= label f, :email, class: "control-label" %> 6 | <%= text_input f, :email, class: "form-control", autofocus: true %> 7 | <%= error_tag f, :email %> 8 |
9 | 10 |
11 | <%= submit "Login", class: "btn btn-primary" %> 12 |
13 | <% end %> 14 | -------------------------------------------------------------------------------- /lib/iris_web/templates/user/edit.html.eex: -------------------------------------------------------------------------------- 1 |

Edit user

2 | 3 | <%= render "form.html", changeset: @changeset, 4 | action: user_path(@conn, :update, @user) %> 5 | 6 | <%= link "Back", to: user_path(@conn, :index) %> 7 | -------------------------------------------------------------------------------- /lib/iris_web/templates/user/form.html.eex: -------------------------------------------------------------------------------- 1 | <%= form_for @changeset, @action, fn f -> %> 2 | <%= if @changeset.action do %> 3 |
4 |

Oops, something went wrong! Please check the errors below.

5 |
6 | <% end %> 7 | 8 |
9 | <%= label f, :email, class: "control-label" %> 10 | <%= text_input f, :email, class: "form-control" %> 11 | <%= error_tag f, :email %> 12 |
13 | 14 |
15 | <%= label f, :disabled_at, class: "control-label" %> 16 | <%= datetime_select f, :disabled_at, class: "form-control" %> 17 | <%= error_tag f, :disabled_at %> 18 |
19 | 20 |
21 | <%= submit "Submit", class: "btn btn-primary" %> 22 |
23 | <% end %> 24 | -------------------------------------------------------------------------------- /lib/iris_web/templates/user/index.html.eex: -------------------------------------------------------------------------------- 1 |

Listing users

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | <%= for user <- @users do %> 14 | 15 | 16 | 17 | 18 | 23 | 24 | <% end %> 25 | 26 |
EmailDisabled at
<%= user.email %><%= user.disabled_at %> 19 | <%= link "Show", to: user_path(@conn, :show, user), class: "btn btn-default btn-xs" %> 20 | <%= link "Edit", to: user_path(@conn, :edit, user), class: "btn btn-default btn-xs" %> 21 | <%= link "Delete", to: user_path(@conn, :delete, user), method: :delete, data: [confirm: "Are you sure?"], class: "btn btn-danger btn-xs" %> 22 |
27 | 28 | <%= link "New user", to: user_path(@conn, :new) %> 29 | -------------------------------------------------------------------------------- /lib/iris_web/templates/user/new.html.eex: -------------------------------------------------------------------------------- 1 |

New user

2 | 3 | <%= render "form.html", changeset: @changeset, 4 | action: user_path(@conn, :create) %> 5 | 6 | <%= link "Back", to: user_path(@conn, :index) %> 7 | -------------------------------------------------------------------------------- /lib/iris_web/templates/user/show.html.eex: -------------------------------------------------------------------------------- 1 |

Show user

2 | 3 | 16 | 17 | <%= link "Edit", to: user_path(@conn, :edit, @user) %> 18 | <%= link "Back", to: user_path(@conn, :index) %> 19 | -------------------------------------------------------------------------------- /lib/iris_web/views/changeset_view.ex: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.ChangesetView do 2 | use IrisWeb, :view 3 | 4 | @doc """ 5 | Traverses and translates changeset errors. 6 | 7 | See `Ecto.Changeset.traverse_errors/2` and 8 | `Iris.ErrorHelpers.translate_error/1` for more details. 9 | """ 10 | def translate_errors(changeset) do 11 | Ecto.Changeset.traverse_errors(changeset, &translate_error/1) 12 | end 13 | 14 | def render("error.json", %{changeset: changeset}) do 15 | # When encoded, the changeset returns its errors 16 | # as a JSON object. So we just pass it forward. 17 | %{errors: translate_errors(changeset)} 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/iris_web/views/device_view.ex: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.DeviceView do 2 | use IrisWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /lib/iris_web/views/email_view.ex: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.EmailView do 2 | use IrisWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /lib/iris_web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | use Phoenix.HTML 7 | 8 | @doc """ 9 | Generates tag for inlined form input errors. 10 | """ 11 | def error_tag(form, field) do 12 | if error = form.errors[field] do 13 | content_tag :span, translate_error(error), class: "help-block" 14 | end 15 | end 16 | 17 | @doc """ 18 | Translates an error message using gettext. 19 | """ 20 | def translate_error({msg, opts}) do 21 | # Because error messages were defined within Ecto, we must 22 | # call the Gettext module passing our Gettext backend. We 23 | # also use the "errors" domain as translations are placed 24 | # in the errors.po file. 25 | # Ecto will pass the :count keyword if the error message is 26 | # meant to be pluralized. 27 | # On your own code and templates, depending on whether you 28 | # need the message to be pluralized or not, this could be 29 | # written simply as: 30 | # 31 | # dngettext "errors", "1 file", "%{count} files", count 32 | # dgettext "errors", "is invalid" 33 | # 34 | if count = opts[:count] do 35 | Gettext.dngettext(IrisWeb.Gettext, "errors", msg, msg, count, opts) 36 | else 37 | Gettext.dgettext(IrisWeb.Gettext, "errors", msg, opts) 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/iris_web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.ErrorView do 2 | use IrisWeb, :view 3 | 4 | def render("404.html", _assigns) do 5 | "Page not found" 6 | end 7 | 8 | def render("500.html", _assigns) do 9 | "Internal server error" 10 | end 11 | 12 | # In case no render clause matches or no 13 | # template is found, let's render it as 500 14 | def template_not_found(_template, assigns) do 15 | render "500.html", assigns 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/iris_web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.LayoutView do 2 | use IrisWeb, :view 3 | 4 | alias Iris.User 5 | 6 | def current_user(conn) do 7 | Plug.Conn.get_session(conn, :current_user) 8 | end 9 | 10 | def current_user_is_admin?(conn) do 11 | user = current_user(conn) 12 | User.is_admin?(user) 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /lib/iris_web/views/message_view.ex: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.MessageView do 2 | use IrisWeb, :view 3 | 4 | def render("index.json", %{messages: messages}) do 5 | %{data: render_many(messages, IrisWeb.MessageView, "message.json")} 6 | end 7 | 8 | def render("show.json", %{message: message}) do 9 | render_one(message, IrisWeb.MessageView, "message.json") 10 | end 11 | 12 | def render("message.json", %{message: message}) do 13 | %{id: message.id, 14 | title: message.title, 15 | text: message.text, 16 | priority: message.priority, 17 | link: message.link, 18 | service_token: message.service_token} 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/iris_web/views/page_view.ex: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.PageView do 2 | use IrisWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /lib/iris_web/views/registration_view.ex: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.RegistrationView do 2 | use IrisWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /lib/iris_web/views/service_view.ex: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.ServiceView do 2 | use IrisWeb, :view 3 | 4 | def render("index.json", %{services: services}) do 5 | %{data: render_many(services, IrisWeb.ServiceView, "service.json")} 6 | end 7 | 8 | def render("show.json", %{service: service}) do 9 | render_one(service, IrisWeb.ServiceView, "service.json") 10 | end 11 | 12 | def render("service.json", %{service: service}) do 13 | %{id: service.id, 14 | name: service.name, 15 | description: service.description, 16 | icon: service.icon, 17 | token: service.token, 18 | type: service.type} 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/iris_web/views/session_view.ex: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.SessionView do 2 | use IrisWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /lib/iris_web/views/user_view.ex: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.UserView do 2 | use IrisWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /lib/mqtt/messenger.ex: -------------------------------------------------------------------------------- 1 | defmodule Mqtt.Messenger do 2 | use GenServer 3 | alias Hulaaki.Connection 4 | alias Hulaaki.Message 5 | require Logger 6 | 7 | # Public API 8 | 9 | def start_link(state \\ %{}) do 10 | GenServer.start_link(__MODULE__, state, name: __MODULE__) 11 | end 12 | 13 | def ping, do: GenServer.call(__MODULE__, :ping) 14 | def publish(opts), do: GenServer.call(__MODULE__, {:publish, opts}) 15 | 16 | ## GenServer API 17 | 18 | def init(%{} = state) do 19 | {:ok, conn_pid} = Connection.start_link(self()) 20 | configuration = Application.get_env(:iris, Mqtt.Messenger) 21 | host = configuration |> Keyword.fetch!(:host) 22 | port = configuration |> Keyword.fetch!(:port) 23 | timeout = configuration |> Keyword.get(:timeout, 100) 24 | 25 | client_id = configuration |> Keyword.fetch!(:client_id) 26 | username = configuration |> Keyword.get(:username, "") 27 | password = configuration |> Keyword.get(:password, "") 28 | will_topic = configuration |> Keyword.get(:will_topic, "") 29 | will_message = configuration |> Keyword.get(:will_message, "") 30 | will_qos = configuration |> Keyword.get(:will_qos, 0) 31 | will_retain = configuration |> Keyword.get(:will_retain, 0) 32 | clean_session = configuration |> Keyword.get(:clean_session, 1) 33 | keep_alive = configuration |> Keyword.get(:keep_alive, 100) 34 | 35 | message = Message.connect(client_id, username, password, 36 | will_topic, will_message, will_qos, 37 | will_retain, clean_session, keep_alive) 38 | 39 | state = Map.merge(%{connection: conn_pid}, state) 40 | 41 | connect_opts = [host: host, port: port, timeout: timeout] 42 | 43 | case Connection.connect(state.connection, message, connect_opts) do 44 | :ok -> 45 | Logger.info "Connection to mqtt established" 46 | {:ok, state} 47 | {:error, _error} -> 48 | Logger.info "Cannot establish connection with mqtt" 49 | {:ok, state} 50 | end 51 | end 52 | 53 | def handle_call({:publish, opts}, _from, state) do 54 | topic = opts |> Keyword.fetch!(:topic) 55 | msg = opts |> Keyword.fetch!(:message) 56 | dup = opts |> Keyword.fetch!(:dup) 57 | qos = opts |> Keyword.fetch!(:qos) 58 | retain = opts |> Keyword.fetch!(:retain) 59 | 60 | message = 61 | case qos do 62 | 0 -> 63 | Message.publish(topic, msg, dup, qos, retain) 64 | _ -> 65 | id = opts |> Keyword.fetch!(:id) 66 | Message.publish(id, topic, msg, dup, qos, retain) 67 | end 68 | 69 | :ok = state.connection |> Connection.publish(message) 70 | {:reply, :ok, state} 71 | end 72 | 73 | def handle_call(:ping, _from, state) do 74 | :ok = state.connection |> Connection.ping 75 | {:reply, :ok, state} 76 | end 77 | 78 | def handle_info({:sent, %Message.Connect{} = message}, state) do 79 | {:noreply, state} 80 | end 81 | 82 | def handle_info({:received, %Message.ConnAck{} = message}, state) do 83 | {:noreply, state} 84 | end 85 | 86 | def handle_info({:sent, %Message.Publish{} = message}, state) do 87 | {:noreply, state} 88 | end 89 | 90 | def handle_info({:received, %Message.Publish{qos: qos} = message}, state) do 91 | case qos do 92 | 1 -> 93 | message = Message.publish_ack message.id 94 | :ok = state.connection |> Connection.publish_ack(message) 95 | _ -> 96 | :droped 97 | end 98 | 99 | {:noreply, state} 100 | end 101 | 102 | def handle_info({:sent, %Message.PubAck{} = message}, state) do 103 | {:noreply, state} 104 | end 105 | 106 | def handle_info({:received, %Message.PubRec{} = message}, state) do 107 | message = Message.publish_release message.id 108 | :ok = state.connection |> Connection.publish_release(message) 109 | {:noreply, state} 110 | end 111 | 112 | def handle_info({:sent, %Message.PubRel{} = message}, state) do 113 | {:noreply, state} 114 | end 115 | 116 | def handle_info({:received, %Message.PubComp{} = message}, state) do 117 | {:noreply, state} 118 | end 119 | 120 | def handle_info({:received, %Message.PubAck{} = message}, state) do 121 | {:noreply, state} 122 | end 123 | 124 | def handle_info({:sent, %Message.Subscribe{} = message}, state) do 125 | {:noreply, state} 126 | end 127 | 128 | def handle_info({:received, %Message.SubAck{} = message}, state) do 129 | {:noreply, state} 130 | end 131 | 132 | def handle_info({:sent, %Message.Unsubscribe{} = message}, state) do 133 | {:noreply, state} 134 | end 135 | 136 | def handle_info({:received, %Message.UnsubAck{} = message}, state) do 137 | {:noreply, state} 138 | end 139 | 140 | def handle_info({:sent, %Message.PingReq{} = message}, state) do 141 | {:noreply, state} 142 | end 143 | 144 | def handle_info({:received, %Message.PingResp{} = message}, state) do 145 | {:noreply, state} 146 | end 147 | 148 | end 149 | -------------------------------------------------------------------------------- /lib/mqtt/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Mqtt.Supervisor do 2 | use Supervisor 3 | 4 | def start_link do 5 | Supervisor.start_link(__MODULE__, :ok, name: __MODULE__) 6 | end 7 | 8 | def init(:ok) do 9 | children = [ 10 | worker(Mqtt.Messenger, []) 11 | ] 12 | 13 | supervise(children, strategy: :one_for_one) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Iris.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :iris, 6 | version: "0.0.1", 7 | elixir: "~> 1.5", 8 | elixirc_paths: elixirc_paths(Mix.env), 9 | compilers: [:phoenix, :gettext] ++ Mix.compilers, 10 | build_embedded: Mix.env == :prod, 11 | start_permanent: Mix.env == :prod, 12 | aliases: aliases(), 13 | deps: deps(), 14 | test_coverage: [tool: ExCoveralls], 15 | preferred_cli_env: ["coveralls": :test, "coveralls.detail": :test, "coveralls.post": :test, "coveralls.html": :test]] 16 | end 17 | 18 | # Configuration for the OTP application. 19 | # 20 | # Type `mix help compile.app` for more information. 21 | def application do 22 | [mod: {Iris.Application, []}, 23 | applications: [:phoenix, :phoenix_pubsub, :phoenix_html, :cowboy, :logger, :gettext, 24 | :phoenix_ecto, :postgrex, :bamboo]] 25 | end 26 | 27 | # Specifies which paths to compile per environment. 28 | defp elixirc_paths(:test), do: ["lib", "test/support"] 29 | defp elixirc_paths(_), do: ["lib"] 30 | 31 | # Specifies your project dependencies. 32 | # 33 | # Type `mix help deps` for examples and options. 34 | defp deps do 35 | [{:phoenix, "~> 1.3.0"}, 36 | {:phoenix_pubsub, "~> 1.0"}, 37 | {:phoenix_ecto, "~> 3.2"}, 38 | {:ecto_enum, "~> 1.0"}, 39 | {:postgrex, ">= 0.0.0"}, 40 | {:phoenix_html, "~> 2.10"}, 41 | {:phoenix_live_reload, "~> 1.0", only: :dev}, 42 | {:gettext, "~> 0.11"}, 43 | {:cowboy, "~> 1.0"}, 44 | {:excoveralls, "~> 0.6", only: :test}, 45 | {:hulaaki, git: "git://github.com/suvash/hulaaki", ref: "298a5af"}, 46 | {:bamboo, "~> 0.8"}, 47 | {:atomic_map, "~> 0.8"}] 48 | end 49 | 50 | # Aliases are shortcuts or tasks specific to the current project. 51 | # For example, to create, migrate and run the seeds file at once: 52 | # 53 | # $ mix ecto.setup 54 | # 55 | # See the documentation for `Mix` for more info on aliases. 56 | defp aliases do 57 | ["ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 58 | "ecto.reset": ["ecto.drop", "ecto.setup"], 59 | "test": ["ecto.create --quiet", "ecto.migrate", "test"]] 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"atomic_map": {:hex, :atomic_map, "0.9.2", "a3248fef5195788429aadb5968126fdaead2bc3010be620588b185a56f954192", [], [], "hexpm"}, 2 | "bamboo": {:hex, :bamboo, "0.8.0", "573889a3efcb906bb9d25a1c4caa4ca22f479235e1b8cc3260d8b88dabeb4b14", [:mix], [{:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, 3 | "certifi": {:hex, :certifi, "1.2.1", "c3904f192bd5284e5b13f20db3ceac9626e14eeacfbb492e19583cf0e37b22be", [:rebar3], [], "hexpm"}, 4 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, 5 | "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, 6 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"}, 7 | "db_connection": {:hex, :db_connection, "1.1.2", "2865c2a4bae0714e2213a0ce60a1b12d76a6efba0c51fbda59c9ab8d1accc7a8", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, 8 | "decimal": {:hex, :decimal, "1.4.0", "fac965ce71a46aab53d3a6ce45662806bdd708a4a95a65cde8a12eb0124a1333", [:mix], [], "hexpm"}, 9 | "ecto": {:hex, :ecto, "2.1.4", "d1ba932813ec0e0d9db481ef2c17777f1cefb11fc90fa7c142ff354972dfba7e", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, 10 | "ecto_enum": {:hex, :ecto_enum, "1.0.1", "7853cd36b2063c1e2ba2a2e2987dd4745afa1ab1e24b16f6dba3b6bdbd4c907b", [:mix], [{:ecto, "~> 2.1.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm"}, 11 | "excoveralls": {:hex, :excoveralls, "0.7.0", "05cb3332c2b0f799df3ab90eb7df1ae5a147c86776e91792848a12b7ed87242f", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, 12 | "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, 13 | "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], [], "hexpm"}, 14 | "gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [:mix], [], "hexpm"}, 15 | "hackney": {:hex, :hackney, "1.8.6", "21a725db3569b3fb11a6af17d5c5f654052ce9624219f1317e8639183de4a423", [:rebar3], [{:certifi, "1.2.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.0.2", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, 16 | "hulaaki": {:git, "git://github.com/suvash/hulaaki", "298a5afba75f1af576846016cda36f2a3feae6e6", [ref: "298a5af"]}, 17 | "idna": {:hex, :idna, "5.0.2", "ac203208ada855d95dc591a764b6e87259cb0e2a364218f215ad662daa8cd6b4", [:rebar3], [{:unicode_util_compat, "0.2.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, 18 | "jsx": {:hex, :jsx, "2.8.2", "7acc7d785b5abe8a6e9adbde926a24e481f29956dd8b4df49e3e4e7bcc92a018", [:mix, :rebar3], [], "hexpm"}, 19 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, 20 | "mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [:mix], [], "hexpm"}, 21 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, 22 | "phoenix": {:hex, :phoenix, "1.3.0", "1c01124caa1b4a7af46f2050ff11b267baa3edb441b45dbf243e979cd4c5891b", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, 23 | "phoenix_ecto": {:hex, :phoenix_ecto, "3.2.3", "450c749876ff1de4a78fdb305a142a76817c77a1cd79aeca29e5fc9a6c630b26", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, 24 | "phoenix_html": {:hex, :phoenix_html, "2.10.3", "432dec9e04372eeb7a6dbf59a778cf43eb44518441932217371fa535f2bcfd80", [], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, 25 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.0.8", "4333f9c74190f485a74866beff2f9304f069d53f047f5fbb0fb8d1ee4c495f73", [:mix], [{:fs, "~> 0.9.1", [hex: :fs, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2-rc", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm"}, 26 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.2", "bfa7fd52788b5eaa09cb51ff9fcad1d9edfeb68251add458523f839392f034c1", [:mix], [], "hexpm"}, 27 | "plug": {:hex, :plug, "1.4.3", "236d77ce7bf3e3a2668dc0d32a9b6f1f9b1f05361019946aae49874904be4aed", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}, 28 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, 29 | "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"}, 30 | "postgrex": {:hex, :postgrex, "0.13.3", "c277cfb2a9c5034d445a722494c13359e361d344ef6f25d604c2353185682bfc", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"}, 31 | "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"}, 32 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, 33 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.2.0", "dbbccf6781821b1c0701845eaf966c9b6d83d7c3bfc65ca2b78b88b8678bfa35", [:rebar3], [], "hexpm"}, 34 | "uuid": {:hex, :uuid, "1.1.7", "007afd58273bc0bc7f849c3bdc763e2f8124e83b957e515368c498b641f7ab69", [], [], "hexpm"}} 35 | -------------------------------------------------------------------------------- /priv/gettext/en/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. 2 | ## 3 | ## Do not add, change, or remove `msgid`s manually here as 4 | ## they're tied to the ones in the corresponding POT file 5 | ## (with the same domain). 6 | ## 7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge` 8 | ## to merge POT files into PO files. 9 | msgid "" 10 | msgstr "" 11 | "Language: en\n" 12 | 13 | ## From Ecto.Changeset.cast/4 14 | msgid "can't be blank" 15 | msgstr "" 16 | 17 | ## From Ecto.Changeset.unique_constraint/3 18 | msgid "has already been taken" 19 | msgstr "" 20 | 21 | ## From Ecto.Changeset.put_change/3 22 | msgid "is invalid" 23 | msgstr "" 24 | 25 | ## From Ecto.Changeset.validate_format/3 26 | msgid "has invalid format" 27 | msgstr "" 28 | 29 | ## From Ecto.Changeset.validate_subset/3 30 | msgid "has an invalid entry" 31 | msgstr "" 32 | 33 | ## From Ecto.Changeset.validate_exclusion/3 34 | msgid "is reserved" 35 | msgstr "" 36 | 37 | ## From Ecto.Changeset.validate_confirmation/3 38 | msgid "does not match confirmation" 39 | msgstr "" 40 | 41 | ## From Ecto.Changeset.no_assoc_constraint/3 42 | msgid "is still associated to this entry" 43 | msgstr "" 44 | 45 | msgid "are still associated to this entry" 46 | msgstr "" 47 | 48 | ## From Ecto.Changeset.validate_length/3 49 | msgid "should be %{count} character(s)" 50 | msgid_plural "should be %{count} character(s)" 51 | msgstr[0] "" 52 | msgstr[1] "" 53 | 54 | msgid "should have %{count} item(s)" 55 | msgid_plural "should have %{count} item(s)" 56 | msgstr[0] "" 57 | msgstr[1] "" 58 | 59 | msgid "should be at least %{count} character(s)" 60 | msgid_plural "should be at least %{count} character(s)" 61 | msgstr[0] "" 62 | msgstr[1] "" 63 | 64 | msgid "should have at least %{count} item(s)" 65 | msgid_plural "should have at least %{count} item(s)" 66 | msgstr[0] "" 67 | msgstr[1] "" 68 | 69 | msgid "should be at most %{count} character(s)" 70 | msgid_plural "should be at most %{count} character(s)" 71 | msgstr[0] "" 72 | msgstr[1] "" 73 | 74 | msgid "should have at most %{count} item(s)" 75 | msgid_plural "should have at most %{count} item(s)" 76 | msgstr[0] "" 77 | msgstr[1] "" 78 | 79 | ## From Ecto.Changeset.validate_number/3 80 | msgid "must be less than %{number}" 81 | msgstr "" 82 | 83 | msgid "must be greater than %{number}" 84 | msgstr "" 85 | 86 | msgid "must be less than or equal to %{number}" 87 | msgstr "" 88 | 89 | msgid "must be greater than or equal to %{number}" 90 | msgstr "" 91 | 92 | msgid "must be equal to %{number}" 93 | msgstr "" 94 | -------------------------------------------------------------------------------- /priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This file is a PO Template file. 2 | ## 3 | ## `msgid`s here are often extracted from source code. 4 | ## Add new translations manually only if they're dynamic 5 | ## translations that can't be statically extracted. 6 | ## 7 | ## Run `mix gettext.extract` to bring this file up to 8 | ## date. Leave `msgstr`s empty as changing them here as no 9 | ## effect: edit them in PO (`.po`) files instead. 10 | 11 | ## From Ecto.Changeset.cast/4 12 | msgid "can't be blank" 13 | msgstr "" 14 | 15 | ## From Ecto.Changeset.unique_constraint/3 16 | msgid "has already been taken" 17 | msgstr "" 18 | 19 | ## From Ecto.Changeset.put_change/3 20 | msgid "is invalid" 21 | msgstr "" 22 | 23 | ## From Ecto.Changeset.validate_format/3 24 | msgid "has invalid format" 25 | msgstr "" 26 | 27 | ## From Ecto.Changeset.validate_subset/3 28 | msgid "has an invalid entry" 29 | msgstr "" 30 | 31 | ## From Ecto.Changeset.validate_exclusion/3 32 | msgid "is reserved" 33 | msgstr "" 34 | 35 | ## From Ecto.Changeset.validate_confirmation/3 36 | msgid "does not match confirmation" 37 | msgstr "" 38 | 39 | ## From Ecto.Changeset.no_assoc_constraint/3 40 | msgid "is still associated to this entry" 41 | msgstr "" 42 | 43 | msgid "are still associated to this entry" 44 | msgstr "" 45 | 46 | ## From Ecto.Changeset.validate_length/3 47 | msgid "should be %{count} character(s)" 48 | msgid_plural "should be %{count} character(s)" 49 | msgstr[0] "" 50 | msgstr[1] "" 51 | 52 | msgid "should have %{count} item(s)" 53 | msgid_plural "should have %{count} item(s)" 54 | msgstr[0] "" 55 | msgstr[1] "" 56 | 57 | msgid "should be at least %{count} character(s)" 58 | msgid_plural "should be at least %{count} character(s)" 59 | msgstr[0] "" 60 | msgstr[1] "" 61 | 62 | msgid "should have at least %{count} item(s)" 63 | msgid_plural "should have at least %{count} item(s)" 64 | msgstr[0] "" 65 | msgstr[1] "" 66 | 67 | msgid "should be at most %{count} character(s)" 68 | msgid_plural "should be at most %{count} character(s)" 69 | msgstr[0] "" 70 | msgstr[1] "" 71 | 72 | msgid "should have at most %{count} item(s)" 73 | msgid_plural "should have at most %{count} item(s)" 74 | msgstr[0] "" 75 | msgstr[1] "" 76 | 77 | ## From Ecto.Changeset.validate_number/3 78 | msgid "must be less than %{number}" 79 | msgstr "" 80 | 81 | msgid "must be greater than %{number}" 82 | msgstr "" 83 | 84 | msgid "must be less than or equal to %{number}" 85 | msgstr "" 86 | 87 | msgid "must be greater than or equal to %{number}" 88 | msgstr "" 89 | 90 | msgid "must be equal to %{number}" 91 | msgstr "" 92 | -------------------------------------------------------------------------------- /priv/repo/migrations/20170629190008_create_service.exs: -------------------------------------------------------------------------------- 1 | defmodule Iris.Repo.Migrations.CreateService do 2 | use Ecto.Migration 3 | 4 | def change do 5 | 6 | ServiceTypeEnum.create_type 7 | create table(:services) do 8 | add :name, :string 9 | add :description, :string 10 | add :icon, :string 11 | add :token, :string 12 | add :type, :service_type 13 | 14 | timestamps() 15 | end 16 | 17 | end 18 | 19 | def down do 20 | drop_if_exists table(:services) 21 | ServiceTypeEnum.drop_type 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /priv/repo/migrations/20170629195901_create_message.exs: -------------------------------------------------------------------------------- 1 | defmodule Iris.Repo.Migrations.CreateMessage do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:messages) do 6 | add :title, :string 7 | add :text, :string 8 | add :priority, :integer 9 | add :link, :string 10 | add :service_token, :string 11 | 12 | timestamps() 13 | end 14 | 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /priv/repo/migrations/20170702202723_create_role.exs: -------------------------------------------------------------------------------- 1 | defmodule Iris.Repo.Migrations.CreateRole do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:roles) do 6 | add :title, :string 7 | add :admin, :boolean, default: false, null: false 8 | 9 | timestamps() 10 | end 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/repo/migrations/20170702202724_create_user.exs: -------------------------------------------------------------------------------- 1 | defmodule Iris.Repo.Migrations.CreateUser do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:users) do 6 | add :email, :string 7 | add :access_token, :string 8 | add :disabled_at, :utc_datetime, default: nil, null: true 9 | add :role_id, references(:roles) 10 | 11 | timestamps() 12 | end 13 | 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /priv/repo/migrations/20170704174839_create_token.exs: -------------------------------------------------------------------------------- 1 | defmodule Iris.Repo.Migrations.CreateToken do 2 | use Ecto.Migration 3 | 4 | def change do 5 | TokenTypeEnum.create_type 6 | create table(:tokens) do 7 | add :value, :string 8 | add :type, :token_type 9 | add :user_id, references(:users, on_delete: :nothing) 10 | 11 | timestamps() 12 | end 13 | create index(:tokens, [:user_id]) 14 | create index(:tokens, [:type]) 15 | create index(:tokens, [:value]) 16 | 17 | end 18 | 19 | def down do 20 | drop_if_exists table(:tokens) 21 | TokenTypeEnum.drop_type 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /priv/repo/migrations/20170719054344_create_device.exs: -------------------------------------------------------------------------------- 1 | defmodule Iris.Repo.Migrations.CreateDevice do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:devices) do 6 | add :name, :string, null: false 7 | add :password, :string, null: false 8 | add :access_token, :string 9 | add :client_id, :string, null: false 10 | add :status, :boolean, default: false, null: false 11 | add :last_synced, :datetime 12 | 13 | add :user_id, references(:users, on_delete: :delete_all) 14 | 15 | timestamps() 16 | end 17 | 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /priv/repo/seeds.exs: -------------------------------------------------------------------------------- 1 | alias Iris.{Repo, Role, User, Device} 2 | import Ecto.Query, only: [from: 2] 3 | 4 | find_or_create_role = fn role_name, admin -> 5 | case Repo.all(from r in Role, where: r.title == ^role_name and r.admin == ^admin) do 6 | [] -> 7 | IO.puts "Role: #{role_name} does not exists, creating" 8 | %Role{} 9 | |> Role.changeset(%{title: role_name, admin: admin}) 10 | |> Repo.insert!() 11 | [role] -> 12 | IO.puts "Role: #{role_name} already exists, skipping" 13 | role 14 | end 15 | end 16 | 17 | find_or_create_user = fn email, role -> 18 | case Repo.all(from u in User, where: u.email == ^email) do 19 | [] -> 20 | IO.puts "User: #{email} does not exists, creating" 21 | %User{} 22 | |> User.changeset(%{email: email, role_id: role.id}) 23 | |> Repo.insert!() 24 | [user] -> 25 | IO.puts "User: #{email} already exists, skipping" 26 | user 27 | end 28 | end 29 | 30 | find_or_create_device = fn name, client_id, access_token, user -> 31 | case Repo.all(from u in Device, where: u.name == ^name) do 32 | [] -> 33 | IO.puts "Device: #{name} does not exists, creating" 34 | %Device{} 35 | |> Device.changeset(%{name: name, password: "sagapo", client_id: client_id, access_token: access_token, status: true, user_id: user.id}) 36 | |> Repo.insert!() 37 | [user] -> 38 | IO.puts "Device: #{name} already exists, skipping" 39 | user 40 | end 41 | end 42 | 43 | user_role = find_or_create_role.("User Role", false) 44 | admin_role = find_or_create_role.("Admin Role", true) 45 | admin_user = find_or_create_user.("admin@dev.com", admin_role) 46 | simple_user = find_or_create_user.("user@dev.com", user_role) 47 | find_or_create_device.("iris_server", "iris_server", "access1", admin_user) 48 | find_or_create_device.("test_device", "client2", "access2", simple_user) 49 | -------------------------------------------------------------------------------- /test/iris_web/controllers/device_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.DeviceControllerTest do 2 | use IrisWeb.ConnCase 3 | 4 | alias Iris.{Device, TestHelper, Support.AuthenticationToken, Repo, Accounts} 5 | 6 | @valid_attributes %{last_synced: %{day: 17, hour: 14, min: 0, month: 4, sec: 0, year: 2010}, name: "some content", status: true, access_token: "token", client_id: "client id"} 7 | @valid_attributes2 %{last_synced: %{day: 17, hour: 14, min: 0, month: 4, sec: 0, year: 2010}, name: "some content", status: true, access_token: "token", client_id: "client id2"} 8 | @invalid_attributes %{name: nil, client_id: nil} 9 | 10 | setup do 11 | {:ok, user_role} = TestHelper.create_role(%{title: "User Role", admin: false}) 12 | {:ok, simple_user} = TestHelper.create_user(%{email: "test@test.com", role_id: user_role.id}) 13 | 14 | simple_token = AuthenticationToken.create_token(simple_user) 15 | simple_conn = TestHelper.login_user(simple_token, @endpoint) 16 | 17 | {:ok, device} = Accounts.create_device(@valid_attributes, simple_user) 18 | {:ok, conn: simple_conn, user: simple_user, device: device} 19 | end 20 | 21 | test "lists all entries on index", %{conn: conn} do 22 | conn = get conn, device_path(conn, :index) 23 | assert html_response(conn, 200) =~ "Devices" 24 | end 25 | 26 | test "renders form for new devices", %{conn: conn} do 27 | conn = get conn, device_path(conn, :new) 28 | assert html_response(conn, 200) =~ "New device" 29 | end 30 | 31 | test "creates device and redirects when data is valid", %{conn: conn} do 32 | conn = post conn, device_path(conn, :create), device: @valid_attributes2 33 | assert redirected_to(conn) == device_path(conn, :index) 34 | assert Repo.get_by(Device, @valid_attributes2) 35 | end 36 | 37 | test "does not create device and renders errors when data is invalid", %{conn: conn} do 38 | conn = post conn, device_path(conn, :create), device: @invalid_attributes 39 | assert html_response(conn, 200) =~ "New device" 40 | end 41 | 42 | test "shows chosen device", %{conn: conn, device: device} do 43 | conn = get conn, device_path(conn, :show, device) 44 | assert html_response(conn, 200) =~ "Show device" 45 | end 46 | 47 | test "renders page not found when id is nonexistent", %{conn: conn} do 48 | assert_error_sent 404, fn -> 49 | get conn, device_path(conn, :show, -1) 50 | end 51 | end 52 | 53 | test "renders form for editing chosen device", %{conn: conn, device: device} do 54 | conn = get conn, device_path(conn, :edit, device) 55 | assert html_response(conn, 200) =~ "Edit device" 56 | end 57 | 58 | test "updates chosen device and redirects when data is valid", %{conn: conn, device: device} do 59 | conn = put conn, device_path(conn, :update, device), device: @valid_attributes 60 | assert redirected_to(conn) == device_path(conn, :show, device) 61 | assert Repo.get_by(Device, @valid_attributes) 62 | end 63 | 64 | test "does not update chosen device and renders errors when data is invalid", %{conn: conn, device: device} do 65 | conn = put conn, device_path(conn, :update, device), device: @invalid_attributes 66 | assert html_response(conn, 200) =~ "Edit device" 67 | end 68 | 69 | test "deletes chosen device", %{conn: conn, device: device} do 70 | conn = delete conn, device_path(conn, :delete, device) 71 | assert redirected_to(conn) == device_path(conn, :index) 72 | refute Repo.get(Device, device.id) 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /test/iris_web/controllers/message_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.MessageControllerTest do 2 | use IrisWeb.ConnCase 3 | 4 | alias Iris.{Message, Repo} 5 | 6 | @valid_attrs %{link: "some content", priority: 42, service_token: "some content", text: "some content", title: "some content"} 7 | @invalid_attrs %{} 8 | 9 | setup %{conn: conn} do 10 | {:ok, conn: put_req_header(conn, "accept", "application/json")} 11 | end 12 | 13 | test "lists all entries on index", %{conn: conn} do 14 | conn = get conn, message_path(conn, :index) 15 | assert json_response(conn, 200)["data"] == [] 16 | end 17 | 18 | test "shows chosen resource", %{conn: conn} do 19 | message = Repo.insert! %Message{} 20 | conn = get conn, message_path(conn, :show, message) 21 | assert json_response(conn, 200) == %{"id" => message.id, 22 | "title" => message.title, 23 | "text" => message.text, 24 | "priority" => message.priority, 25 | "link" => message.link, 26 | "service_token" => message.service_token} 27 | end 28 | 29 | test "renders page not found when id is nonexistent", %{conn: conn} do 30 | assert_error_sent 404, fn -> 31 | get conn, message_path(conn, :show, -1) 32 | end 33 | end 34 | 35 | test "creates and renders resource when data is valid", %{conn: conn} do 36 | conn = post conn, message_path(conn, :create), @valid_attrs 37 | assert json_response(conn, 201)["id"] 38 | assert Repo.get_by(Message, @valid_attrs) 39 | end 40 | 41 | test "does not create resource and renders errors when data is invalid", %{conn: conn} do 42 | conn = post conn, message_path(conn, :create), @invalid_attrs 43 | assert json_response(conn, 422)["errors"] != %{} 44 | end 45 | 46 | test "updates and renders chosen resource when data is valid", %{conn: conn} do 47 | message = Repo.insert! %Message{} 48 | conn = put conn, message_path(conn, :update, message), @valid_attrs 49 | assert json_response(conn, 200)["id"] 50 | assert Repo.get_by(Message, @valid_attrs) 51 | end 52 | 53 | test "does not update chosen resource and renders errors when data is invalid", %{conn: conn} do 54 | message = Repo.insert! %Message{} 55 | conn = put conn, message_path(conn, :update, message), @invalid_attrs 56 | assert json_response(conn, 422)["errors"] != %{} 57 | end 58 | 59 | test "deletes chosen resource", %{conn: conn} do 60 | message = Repo.insert! %Message{} 61 | conn = delete conn, message_path(conn, :delete, message) 62 | assert response(conn, 204) 63 | refute Repo.get(Message, message.id) 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/iris_web/controllers/page_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.PageControllerTest do 2 | use IrisWeb.ConnCase 3 | 4 | alias Iris.{TestHelper, Support.AuthenticationToken} 5 | 6 | setup do 7 | {:ok, user_role} = TestHelper.create_role(%{title: "User Role", admin: false}) 8 | {:ok, simple_user} = TestHelper.create_user(%{email: "test@test.com", role_id: user_role.id}) 9 | 10 | {:ok, admin_role} = TestHelper.create_role(%{title: "Admin Role", admin: true}) 11 | {:ok, admin_user} = TestHelper.create_user(%{email: "admin@test.com", role_id: admin_role.id}) 12 | 13 | simple_token = AuthenticationToken.create_token(simple_user) 14 | admin_token = AuthenticationToken.create_token(admin_user) 15 | admin_conn = TestHelper.login_user(admin_token, @endpoint) 16 | {:ok, conn: build_conn(), simple_token: simple_token, admin_token: admin_token, admin_conn: admin_conn} 17 | end 18 | 19 | test "GET /", %{conn: conn} do 20 | conn = get conn, "/" 21 | assert html_response(conn, 200) =~ "Iris" 22 | end 23 | 24 | test "GET /dashboard", %{conn: conn} do 25 | conn = get conn, "/dashboard" 26 | assert response(conn, 401) =~ "Unauthorized" 27 | end 28 | 29 | test "GET /settings", %{conn: conn} do 30 | conn = get conn, "/settings" 31 | assert response(conn, 401) =~ "Unauthorized" 32 | end 33 | 34 | @tag admin: true 35 | test "GET /settings with admin user", %{admin_conn: conn} do 36 | conn = get conn, "/settings" 37 | assert html_response(conn, 200) =~ "Settings" 38 | end 39 | 40 | @tag admin: true 41 | test "GET /dashboard with admin user", %{admin_conn: conn} do 42 | conn = get conn, "/dashboard" 43 | assert response(conn, 200) =~ "Send notification" 44 | end 45 | 46 | end 47 | -------------------------------------------------------------------------------- /test/iris_web/controllers/registration_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Iris.RegistrationControllerTest do 2 | use IrisWeb.ConnCase 3 | use Bamboo.Test 4 | 5 | alias Iris.TestHelper 6 | alias IrisWeb.Email 7 | 8 | @valid_post_attrs %{"user" => %{email: "test2@test.com"}} 9 | @existing_user %{"user" => %{email: "test@test.com"}} 10 | @invalid_post_attrs %{"user" => %{email: nil}} 11 | 12 | setup do 13 | {:ok, role} = TestHelper.create_role(%{title: "User Role", admin: false}) 14 | {:ok, user} = TestHelper.create_user(%{email: "test@test.com", role_id: role.id}) 15 | {:ok, user: user} 16 | end 17 | 18 | test "GET /signup", %{conn: conn} do 19 | conn = get conn, "/signup" 20 | assert html_response(conn, 200) =~ "Signup" 21 | end 22 | 23 | test "POST /signup", %{conn: conn} do 24 | conn = post conn, registration_path(conn, :signup), @valid_post_attrs 25 | assert get_flash(conn, :info) == "You signed up successfully." 26 | assert redirected_to(conn) == session_path(conn, :login) 27 | end 28 | 29 | test "POST /signup don't leak information", %{conn: conn} do 30 | conn = post conn, registration_path(conn, :signup), @existing_user 31 | assert get_flash(conn, :info) == "You signed up successfully." 32 | assert redirected_to(conn) == session_path(conn, :login) 33 | end 34 | 35 | test "POST /signup with empty email address", %{conn: conn} do 36 | conn = post conn, registration_path(conn, :signup), @invalid_post_attrs 37 | assert redirected_to(conn) == registration_path(conn, :signup) 38 | end 39 | 40 | test "welcome email", %{user: user} do 41 | email = Email.welcome(user) 42 | 43 | assert email.to == user.email 44 | assert email.subject == "Welcome" 45 | assert email.text_body =~ "Welcome" 46 | assert email.html_body =~ "Welcome" 47 | end 48 | 49 | end 50 | -------------------------------------------------------------------------------- /test/iris_web/controllers/service_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.ServiceControllerTest do 2 | use IrisWeb.ConnCase 3 | 4 | alias Iris.{Service, TestHelper, Support.AuthenticationToken, Repo} 5 | @valid_attrs %{description: "some content", icon: "some content", name: "some content", token: "some content", type: "P"} 6 | @invalid_attrs %{} 7 | 8 | setup do 9 | {:ok, user_role} = TestHelper.create_role(%{title: "User Role", admin: false}) 10 | {:ok, simple_user} = TestHelper.create_user(%{email: "test@test.com", role_id: user_role.id}) 11 | 12 | simple_token = AuthenticationToken.create_token(simple_user) 13 | simple_conn = TestHelper.login_user(simple_token, @endpoint) 14 | 15 | {:ok, conn: simple_conn} 16 | end 17 | 18 | test "lists all entries on index", %{conn: conn} do 19 | conn = get conn, service_path(conn, :index) 20 | assert json_response(conn, 200)["data"] == [] 21 | end 22 | 23 | test "shows chosen resource", %{conn: conn} do 24 | service = Repo.insert! %Service{} 25 | conn = get conn, service_path(conn, :show, service) 26 | assert json_response(conn, 200) == %{"id" => service.id, 27 | "name" => service.name, 28 | "description" => service.description, 29 | "icon" => service.icon, 30 | "token" => service.token, 31 | "type" => service.type} 32 | end 33 | 34 | test "renders page not found when id is nonexistent", %{conn: conn} do 35 | assert_error_sent 404, fn -> 36 | get conn, service_path(conn, :show, -1) 37 | end 38 | end 39 | 40 | test "creates and renders resource when data is valid", %{conn: conn} do 41 | conn = post conn, service_path(conn, :create), @valid_attrs 42 | assert json_response(conn, 201)["id"] 43 | assert Repo.get_by(Service, @valid_attrs) 44 | end 45 | 46 | test "does not create resource and renders errors when data is invalid", %{conn: conn} do 47 | conn = post conn, service_path(conn, :create), @invalid_attrs 48 | assert json_response(conn, 422)["errors"] != %{} 49 | end 50 | 51 | test "updates and renders chosen resource when data is valid", %{conn: conn} do 52 | service = Repo.insert! %Service{} 53 | conn = put conn, service_path(conn, :update, service), @valid_attrs 54 | assert json_response(conn, 200)["id"] 55 | assert Repo.get_by(Service, @valid_attrs) 56 | end 57 | 58 | test "does not update chosen resource and renders errors when data is invalid", %{conn: conn} do 59 | service = Repo.insert! %Service{} 60 | conn = put conn, service_path(conn, :update, service), @invalid_attrs 61 | assert json_response(conn, 422)["errors"] != %{} 62 | end 63 | 64 | test "deletes chosen resource", %{conn: conn} do 65 | service = Repo.insert! %Service{} 66 | conn = delete conn, service_path(conn, :delete, service) 67 | assert response(conn, 204) 68 | refute Repo.get(Service, service.id) 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /test/iris_web/controllers/session_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.SessionControllerTest do 2 | use IrisWeb.ConnCase 3 | 4 | alias Iris.{TestHelper, Support.AuthenticationToken} 5 | 6 | @post_attrs %{"user" => %{email: "test@test.com"}} 7 | @invalid_post_attrs %{"user" => %{email: nil}} 8 | 9 | setup do 10 | {:ok, user_role} = TestHelper.create_role(%{title: "User Role", admin: false}) 11 | {:ok, simple_user} = TestHelper.create_user(%{email: "test@test.com", role_id: user_role.id}) 12 | 13 | {:ok, admin_role} = TestHelper.create_role(%{title: "Admin Role", admin: true}) 14 | {:ok, admin_user} = TestHelper.create_user(%{email: "admin@test.com", role_id: admin_role.id}) 15 | 16 | simple_token = AuthenticationToken.create_token(simple_user) 17 | admin_token = AuthenticationToken.create_token(admin_user) 18 | {:ok, simple_token: simple_token, admin_token: admin_token} 19 | end 20 | 21 | test "GET /login", %{conn: conn} do 22 | conn = get conn, "/login" 23 | assert html_response(conn, 200) =~ "Login" 24 | end 25 | 26 | test "POST /login with nil email address", %{conn: conn} do 27 | conn = post conn, session_path(conn, :login), @invalid_post_attrs 28 | assert get_flash(conn, :info) == "You have to provide an email address." 29 | assert redirected_to(conn) == session_path(conn, :login) 30 | end 31 | 32 | test "POST /login with nil", %{conn: conn} do 33 | conn = post conn, session_path(conn, :login), @post_attrs 34 | assert get_flash(conn, :info) == "Check your inbox." 35 | assert redirected_to(conn) == page_path(conn, :index) 36 | end 37 | 38 | @tag admin: true 39 | test "GET /login with a valid admin token", %{conn: conn, admin_token: token} do 40 | conn = get conn, session_path(conn, :login), %{t: token} 41 | current_user = Plug.Conn.get_session(conn, :current_user) 42 | assert current_user.role.id != nil 43 | assert get_flash(conn, :info) == "You signed in successfully." 44 | assert redirected_to(conn) == page_path(conn, :dashboard) 45 | end 46 | 47 | test "GET /login with a valid user token", %{conn: conn, simple_token: token} do 48 | conn = get conn, session_path(conn, :login), %{t: token} 49 | current_user = Plug.Conn.get_session(conn, :current_user) 50 | assert current_user.role.id != nil 51 | assert get_flash(conn, :info) == "You signed in successfully." 52 | assert redirected_to(conn) == page_path(conn, :dashboard) 53 | end 54 | 55 | test "GET /login with an invalid token", %{conn: conn} do 56 | conn = get conn, session_path(conn, :login), %{t: "meow"} 57 | assert get_flash(conn, :error) == "Invalid token." 58 | assert redirected_to(conn) == session_path(conn, :login) 59 | end 60 | 61 | test "GET /logout", %{simple_token: simple_token} do 62 | conn = TestHelper.login_user(simple_token, @endpoint) 63 | conn = get conn, session_path(conn, :logout) 64 | current_user = Plug.Conn.get_session(conn, :current_user) 65 | assert redirected_to(conn) == page_path(conn, :index) 66 | assert current_user == nil 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /test/iris_web/controllers/user_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.UserControllerTest do 2 | use IrisWeb.ConnCase 3 | 4 | alias Iris.{User, TestHelper, Support.AuthenticationToken, Repo} 5 | @valid_attributes %{email: "test@test.com"} 6 | @valid_attributes_new_user %{email: "test2@test.com"} 7 | @invalid_attributes %{} 8 | 9 | setup do 10 | {:ok, user_role} = TestHelper.create_role(%{title: "User Role", admin: false}) 11 | {:ok, simple_user} = TestHelper.create_user(%{email: "test@test.com", role_id: user_role.id}) 12 | {:ok, admin_role} = TestHelper.create_role(%{title: "Admin Role", admin: true}) 13 | {:ok, admin_user} = TestHelper.create_user(%{email: "admin@test.com", role_id: admin_role.id}) 14 | admin_token = AuthenticationToken.create_token(admin_user) 15 | admin_conn = TestHelper.login_user(admin_token, @endpoint) 16 | {:ok, conn: admin_conn, simple_user: simple_user} 17 | end 18 | 19 | test "lists all users on index", %{conn: conn} do 20 | conn = get conn, user_path(conn, :index) 21 | assert html_response(conn, 200) =~ "Listing users" 22 | end 23 | 24 | test "renders form for a new user", %{conn: conn} do 25 | conn = get conn, user_path(conn, :new) 26 | assert html_response(conn, 200) =~ "New user" 27 | end 28 | 29 | test "creates a user and redirects when data is valid", %{conn: conn} do 30 | conn = post conn, user_path(conn, :create), user: @valid_attributes_new_user 31 | assert redirected_to(conn) == user_path(conn, :index) 32 | assert Repo.get_by(User, @valid_attributes) 33 | end 34 | 35 | test "does not create a user and renders errors when data is invalid", %{conn: conn} do 36 | conn = post conn, user_path(conn, :create), user: @invalid_attributes 37 | assert html_response(conn, 200) =~ "New user" 38 | end 39 | 40 | test "shows chosen user", %{conn: conn} do 41 | user = Repo.insert! %User{} 42 | conn = get conn, user_path(conn, :show, user) 43 | assert html_response(conn, 200) =~ "Show user" 44 | end 45 | 46 | test "renders page not found when id is nonexistent", %{conn: conn} do 47 | assert_error_sent 404, fn -> 48 | get conn, user_path(conn, :show, -1) 49 | end 50 | end 51 | 52 | test "renders form for editing chosen user", %{conn: conn, simple_user: simple_user} do 53 | conn = get conn, user_path(conn, :edit, simple_user) 54 | assert html_response(conn, 200) =~ "Edit user" 55 | end 56 | 57 | test "updates chosen user and redirects when data is valid", %{conn: conn, simple_user: simple_user} do 58 | conn = put conn, user_path(conn, :update, simple_user), user: @valid_attributes 59 | assert redirected_to(conn) == user_path(conn, :show, simple_user) 60 | assert Repo.get_by(User, @valid_attributes) 61 | end 62 | 63 | test "does not update chosen user and renders errors when data is invalid", %{conn: conn} do 64 | user = Repo.insert! %User{} 65 | conn = put conn, user_path(conn, :update, user), user: @invalid_attributes 66 | assert html_response(conn, 200) =~ "Edit user" 67 | end 68 | 69 | test "deletes chosen user", %{conn: conn} do 70 | user = Repo.insert! %User{} 71 | conn = delete conn, user_path(conn, :delete, user) 72 | assert redirected_to(conn) == user_path(conn, :index) 73 | refute Repo.get(User, user.id) 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /test/iris_web/models/device_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Iris.DeviceTest do 2 | use Iris.DataCase 3 | 4 | alias Iris.Device 5 | 6 | @valid_attributes %{last_synced: %{day: 17, hour: 14, min: 0, month: 4, sec: 0, year: 2010}, name: "some content", status: true, access_token: "some content", client_id: "client id", user_id: 2, password: "sagapo"} 7 | @invalid_attributes %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Device.changeset(%Device{}, @valid_attributes) 11 | assert changeset.valid?() 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = Device.changeset(%Device{}, @invalid_attributes) 16 | refute changeset.valid?() 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/iris_web/models/message_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Iris.MessageTest do 2 | use Iris.DataCase 3 | 4 | alias Iris.Message 5 | 6 | @valid_attrs %{link: "some content", priority: 42, service_token: "some content", text: "some content", title: "some content"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Message.changeset(%Message{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = Message.changeset(%Message{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/iris_web/models/role_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Iris.RoleTest do 2 | use Iris.DataCase 3 | 4 | alias Iris.Role 5 | 6 | @valid_attrs %{admin: true, title: "some content"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Role.changeset(%Role{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = Role.changeset(%Role{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/iris_web/models/service_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Iris.ServiceTest do 2 | use Iris.DataCase 3 | 4 | alias Iris.Service 5 | 6 | @valid_attrs %{description: "some content", icon: "some content", name: "some content", token: "some content", type: "P"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Service.changeset(%Service{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = Service.changeset(%Service{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/iris_web/models/token_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Iris.TokenTest do 2 | use Iris.DataCase 3 | 4 | alias Iris.{Token, TestHelper} 5 | 6 | setup do 7 | {:ok, role} = TestHelper.create_role(%{title: "User Role", admin: false}) 8 | {:ok, user} = TestHelper.create_user(%{email: "test@test.com", role_id: role.id}) 9 | {:ok, user: user} 10 | end 11 | 12 | test "changeset with valid attributes", %{user: user} do 13 | changeset = Token.create_auth_token(%Token{}, user) 14 | assert changeset.valid? 15 | end 16 | 17 | test "changeset with invalid attributes" do 18 | changeset = Token.create_auth_token(%Token{}, nil) 19 | refute changeset.valid? 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/iris_web/models/user_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Iris.UserTest do 2 | use Iris.DataCase 3 | 4 | alias Iris.User 5 | 6 | @valid_attrs %{disabled_at: nil, email: "some@emal.com", role_id: 1} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = User.changeset(%User{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = User.changeset(%User{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/iris_web/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Iris.ErrorViewTest do 2 | use IrisWeb.ConnCase, async: true 3 | 4 | # Bring render/3 and render_to_string/3 for testing custom views 5 | import Phoenix.View 6 | 7 | test "renders 404.html" do 8 | assert render_to_string(IrisWeb.ErrorView, "404.html", []) == 9 | "Page not found" 10 | end 11 | 12 | test "render 500.html" do 13 | assert render_to_string(IrisWeb.ErrorView, "500.html", []) == 14 | "Internal server error" 15 | end 16 | 17 | test "render any other" do 18 | assert render_to_string(IrisWeb.ErrorView, "505.html", []) == 19 | "Internal server error" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/iris_web/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Iris.LayoutViewTest do 2 | use IrisWeb.ConnCase, async: true 3 | 4 | alias Iris.{TestHelper, Support.AuthenticationToken} 5 | alias IrisWeb.LayoutView 6 | 7 | setup do 8 | {:ok, user_role} = TestHelper.create_role(%{title: "User Role", admin: false}) 9 | {:ok, simple_user} = TestHelper.create_user(%{email: "test@test.com", role_id: user_role.id}) 10 | 11 | {:ok, admin_role} = TestHelper.create_role(%{title: "User Role", admin: true}) 12 | {:ok, admin_user} = TestHelper.create_user(%{email: "admin@test.com", role_id: admin_role.id}) 13 | 14 | simple_token = AuthenticationToken.create_token(simple_user) 15 | admin_token = AuthenticationToken.create_token(admin_user) 16 | 17 | simple_conn = TestHelper.login_user(simple_token, @endpoint) 18 | admin_conn = TestHelper.login_user(admin_token, @endpoint) 19 | {:ok, admin_conn: admin_conn, simple_conn: simple_conn} 20 | end 21 | 22 | test "current user returns the user in the session", %{simple_conn: conn} do 23 | assert LayoutView.current_user(conn) 24 | end 25 | 26 | test "current user returns nothing if there is no user in the session", %{simple_conn: conn} do 27 | conn = get conn, session_path(conn, :logout) 28 | refute LayoutView.current_user(conn) 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /test/iris_web/views/page_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Iris.PageViewTest do 2 | use IrisWeb.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.ChannelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | channel tests. 5 | 6 | Such tests rely on `Phoenix.ChannelTest` and also 7 | import other functionality to make it easier 8 | to build common datastructures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with channels 21 | use Phoenix.ChannelTest 22 | 23 | # The default endpoint for testing 24 | @endpoint IrisWeb.Endpoint 25 | end 26 | end 27 | 28 | setup tags do 29 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Iris.Repo) 30 | unless tags[:async] do 31 | Ecto.Adapters.SQL.Sandbox.mode(Iris.Repo, {:shared, self()}) 32 | end 33 | :ok 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule IrisWeb.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | import other functionality to make it easier 8 | to build common datastructures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with connections 21 | use Phoenix.ConnTest 22 | import IrisWeb.Router.Helpers 23 | 24 | # The default endpoint for testing 25 | @endpoint IrisWeb.Endpoint 26 | end 27 | end 28 | 29 | setup tags do 30 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Iris.Repo) 31 | unless tags[:async] do 32 | Ecto.Adapters.SQL.Sandbox.mode(Iris.Repo, {:shared, self()}) 33 | end 34 | {:ok, conn: Phoenix.ConnTest.build_conn()} 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/support/data_case.ex: -------------------------------------------------------------------------------- 1 | defmodule Iris.DataCase do 2 | @moduledoc """ 3 | This module defines the setup for tests requiring 4 | access to the application's data layer. 5 | 6 | You may define functions here to be used as helpers in 7 | your tests. 8 | 9 | Finally, if the test case interacts with the database, 10 | it cannot be async. For this reason, every test runs 11 | inside a transaction which is reset at the beginning 12 | of the test unless the test case is marked as async. 13 | """ 14 | 15 | use ExUnit.CaseTemplate 16 | 17 | using do 18 | quote do 19 | alias Iris.Repo 20 | 21 | import Ecto 22 | import Ecto.Changeset 23 | import Ecto.Query 24 | import Iris.DataCase 25 | end 26 | end 27 | 28 | setup tags do 29 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Iris.Repo) 30 | 31 | unless tags[:async] do 32 | Ecto.Adapters.SQL.Sandbox.mode(Iris.Repo, {:shared, self()}) 33 | end 34 | 35 | :ok 36 | end 37 | 38 | @doc """ 39 | A helper that transform changeset errors to a map of messages. 40 | 41 | assert {:error, changeset} = Accounts.create_user(%{password: "short"}) 42 | assert "password is too short" in errors_on(changeset).password 43 | assert %{password: ["password is too short"]} = errors_on(changeset) 44 | 45 | """ 46 | def errors_on(changeset) do 47 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> 48 | Enum.reduce(opts, message, fn {key, value}, acc -> 49 | String.replace(acc, "%{#{key}}", to_string(value)) 50 | end) 51 | end) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/support/test_helper.ex: -------------------------------------------------------------------------------- 1 | defmodule Iris.TestHelper do 2 | 3 | import Phoenix.ConnTest, only: [dispatch: 5, build_conn: 0] 4 | 5 | alias Iris.{Repo, Role, User, Accounts} 6 | alias IrisWeb.Router.Helpers 7 | 8 | def login_user(token, endpoint) do 9 | conn = build_conn() 10 | dispatch(conn, endpoint, :get, Helpers.session_path(conn, :login), t: token) 11 | end 12 | 13 | def create_role(%{title: title, admin: admin}) do 14 | Role.changeset(%Role{}, %{title: title, admin: admin}) 15 | |> Repo.insert 16 | end 17 | 18 | def create_user(attributes) do 19 | email = Map.get(attributes, :email) 20 | if user = Repo.get_by(User, email: email) do 21 | Repo.delete(user) 22 | end 23 | Accounts.create_user(attributes) 24 | end 25 | 26 | end 27 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | Ecto.Adapters.SQL.Sandbox.mode(Iris.Repo, :manual) 4 | --------------------------------------------------------------------------------