├── .env.example ├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── apps ├── core │ ├── README.md │ ├── config │ │ ├── config.exs │ │ ├── dev.exs │ │ ├── prod.exs │ │ ├── prod.secret.exs │ │ └── test.exs │ ├── graphql │ │ ├── resolver.ex │ │ ├── schema.ex │ │ └── types │ │ │ ├── auth_mutations.ex │ │ │ ├── user.ex │ │ │ └── viewer.ex │ ├── lib │ │ ├── common.ex │ │ ├── core.ex │ │ ├── core │ │ │ ├── guardian_serializer.ex │ │ │ ├── models │ │ │ │ ├── ability.ex │ │ │ │ └── user.ex │ │ │ └── repo.ex │ │ └── plug │ │ │ ├── graphiql_wrapper.ex │ │ │ ├── graphql_current_user.ex │ │ │ └── graphql_wrapper.ex │ ├── mix.exs │ ├── priv │ │ └── repo │ │ │ ├── migrations │ │ │ ├── 20160926184041_create_extension_citext.exs │ │ │ ├── 20160927213957_create_users.exs │ │ │ └── 20160927214005_create_guardian_tokens.exs │ │ │ └── seeds.exs │ └── test │ │ ├── support │ │ ├── factory.ex │ │ └── model_case.ex │ │ └── test_helper.exs ├── star_wars │ ├── README.md │ ├── config │ │ ├── config.exs │ │ ├── dev.exs │ │ ├── prod.exs │ │ ├── prod.secret.exs │ │ └── test.exs │ ├── graphql │ │ ├── db.ex │ │ ├── mutations.ex │ │ ├── schema.ex │ │ └── types.ex │ ├── lib │ │ ├── plug │ │ │ ├── graphiql_wrapper.ex │ │ │ └── graphql_wrapper.ex │ │ └── star_wars.ex │ └── mix.exs └── webapp │ ├── .gitignore │ ├── README.md │ ├── config │ ├── config.exs │ ├── dev.exs │ ├── prod.exs │ └── test.exs │ ├── lib │ ├── webapp.ex │ └── webapp │ │ └── endpoint.ex │ ├── mix.exs │ ├── priv │ ├── gettext │ │ ├── en │ │ │ └── LC_MESSAGES │ │ │ │ └── errors.po │ │ └── errors.pot │ └── static │ │ ├── css │ │ └── deleteme │ │ ├── favicon.ico │ │ ├── images │ │ └── phoenix.png │ │ ├── js │ │ └── deleteme │ │ └── robots.txt │ ├── spec │ ├── espec_phoenix_extend.ex │ ├── graphql │ │ └── resolver_spec.exs │ ├── lib │ │ └── plug │ │ │ └── graphql_context_spec.exs │ ├── models │ │ └── user_spec.exs │ ├── phoenix_helper.exs │ ├── spec_helper.exs │ └── support │ │ └── factory.ex │ ├── test │ ├── controllers │ │ └── page_controller_test.exs │ ├── support │ │ ├── acceptance_case.ex │ │ ├── channel_case.ex │ │ └── conn_case.ex │ └── test_helper.exs │ └── web │ ├── channels │ └── user_socket.ex │ ├── controllers │ ├── doc_controller.ex │ └── page_controller.ex │ ├── gettext.ex │ ├── router.ex │ ├── static │ └── css │ │ └── app.css │ ├── templates │ ├── layout │ │ └── app.html.eex │ └── page │ │ ├── graphiql.html.eex │ │ ├── index.html.eex │ │ ├── star_wars.html.eex │ │ └── user_widget.html.eex │ ├── views │ ├── error_helpers.ex │ ├── error_view.ex │ ├── layout_view.ex │ └── page_view.ex │ └── web.ex ├── config └── config.exs ├── docker-compose.dev.example.yml ├── docs ├── react_router_relay_and_auth.md └── use_case_examples.md ├── insecure_key ├── mix.exs ├── mix.lock ├── schema.json └── ui ├── core ├── package.json └── src │ ├── AppLayout.js │ ├── babelRelayPlugin.js │ ├── docs │ ├── CodeBlock.js │ ├── DocPage.js │ ├── TableOfContents.js │ ├── hljs.scss │ ├── index.js │ └── styles.scss │ ├── index.js │ ├── lib │ ├── auth.js │ └── mdlUpgrade.js │ ├── login │ ├── index.js │ ├── mutation.js │ └── styles.scss │ ├── my-graphiql │ ├── index.js │ └── styles.scss │ ├── shared │ ├── LogoutLink.js │ ├── NodeQuery.js │ └── ViewerQuery.js │ └── users │ ├── AddUserMutation.js │ ├── Edit.js │ ├── EditUserMutation.js │ ├── Form.js │ ├── List.js │ ├── New.js │ ├── index.js │ └── styles.scss ├── package.json ├── star_wars ├── package.json └── src │ ├── AddShipMutation.js │ ├── AppLayout.js │ ├── Factions.js │ ├── RelayQueryConfig.js │ ├── Ship.js │ ├── babelRelayPlugin.js │ └── index.js ├── webpack.config.js └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | ## SET ENV VARS TO BE USED IN THIS PROJECT 2 | 3 | # ENV for Elixir and Node respectively 4 | MIX_ENV="dev" 5 | NODE_ENV="development" 6 | 7 | # Webapp 8 | PWR2_HTTP_PORT="4000" 9 | PWR2_HTTPS_PORT="4443" 10 | 11 | # Core 12 | PWR2_CORE_DB="PWR2_core_dev" 13 | PWR2_CORE_DB_HOST="localhost" 14 | PWR2_CORE_DB_PORT="55433" 15 | PWR2_CORE_DB_USER="postgres" 16 | PWR2_CORE_DB_PASSWORD="" 17 | PWR2_CORE_TEST_DB="PWR2_core_test" 18 | PWR2_CORE_GRAPHQL_URL="http://localhost:4000/graphql" 19 | 20 | # StarWars 21 | PWR2_STAR_WARS_GRAPHQL_URL="http://localhost:4000/star_wars/graphql" 22 | 23 | # mailer 24 | PWR2_MAILER_SERVER="localhost" 25 | PWR2_MAILER_USERNAME="dev_user" 26 | PWR2_MAILER_PASSWORD="dev_password" 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Elixir/Phoenix default ignored files 2 | /_build 3 | /cover 4 | /deps 5 | 6 | # Generated on crash by the Erlang VM 7 | erl_crash.dump 8 | *.ez 9 | 10 | # Atom's RemoteSync plugin config file 11 | .remote-sync.json 12 | 13 | # See docker-compose.*.example.yml to create your own docker-compose.yml 14 | docker-compose.yml 15 | 16 | # Webapp 17 | apps/webapp/priv/static/* 18 | 19 | # ui 20 | ui/node_modules 21 | 22 | # Environment vars 23 | .env 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | We **try** to use [Semantic Versioning](http://semver.org) and some sort of **Kanban** in our software development processes _as much as possible_. The [issues](https://github.com/iporaitech/pwr2-docker/issues) are grouped into [milestones](https://github.com/iporaitech/pwr2-docker/milestones) and most of the times will be labeled as: **Feature**, **Bug**, **Task**. 4 | 5 | A milestone can be either a **Release** or a **Hotfix**. A **Release** must increment the MINOR version number and usually corresponds to the work (issues) done in 1 sprint. A **Hotfix** must increment the PATCH version number and usually contains **Bug** fixes. We name milestones prefixing the name with the word **Release** or **Hotfix** accordingly. 6 | 7 | If you're in our organization and have rights to push to this repository, we recommend creating a branch **per issue** using the following naming conventions: 8 | 9 | 1. Start the branch name with the issue number, i.e.: 23-fix-logout-error. 10 | 2. If the issue is labeled with **Feature** add `-feature-` after the issue number, i.e.: 23-feature-login-with-facebook. 11 | 3. If you need to do some work specific for a **release** or a **hotfix** create a corresponding branch, i.e.: **release-v0.5.0** or **hotfix-v0.5.1** 12 | 13 | If you don't have rights to push directly to this repo but still want to contribute, just send us a pull request to master, explaining in it what issues it solves. 14 | 15 | Always try to write meaningful commit messages referencing/closing issues. 16 | 17 | We use **master** for our main/default branch. When a pull request is accepted, we rebase it to master so we can keep a linear history. When we have code ready to be release to "production", we create an annotated tag `git tag ...` which can be seen in the [releases](https://github.com/iporaitech/pwr2-docker/releases) section of this repo. This section also serves as our CHANGELOG. 18 | 19 | A **Task** is something that is not complex enough to be defined as a **Feature**. For example, changing a text, fixing a typo, or other _relatively minor tasks_. 20 | 21 | Last but not least, [stop using `git pull`](https://adamcod.es/2014/12/10/git-pull-correct-workflow.html) 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM phusion/baseimage:0.9.19 2 | 3 | # Set basic ENV vars 4 | ENV HOME=/root TERM=xterm-color 5 | 6 | # Elixir requires UTF-8 7 | RUN locale-gen en_US.UTF-8 8 | ENV LANG=en_US.UTF-8 \ 9 | LANGUAGE=en_US:en \ 10 | LC_ALL=en_US.UTF-8 11 | 12 | WORKDIR $HOME 13 | 14 | # Use baseimage-docker's init process. 15 | CMD ["/sbin/my_init"] 16 | 17 | # Install packages needed later 18 | RUN apt-get update && apt-get install -y wget git inotify-tools postgresql-client build-essential 19 | 20 | # Download and install Erlang and Elixir 21 | RUN wget https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb \ 22 | && dpkg -i erlang-solutions_1.0_all.deb \ 23 | && apt-get update \ 24 | && apt-get install -y esl-erlang elixir 25 | RUN rm erlang-solutions*.deb 26 | 27 | # Install Node.js 28 | ENV NODE_VERSION_MAJOR=7 29 | RUN curl -sL https://deb.nodesource.com/setup_$NODE_VERSION_MAJOR.x | bash - && apt-get install -y nodejs 30 | 31 | # Install Yarn 32 | RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - 33 | RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list 34 | RUN apt-get update && apt-get install yarn 35 | 36 | # Create app user and set WORKDIR to its home dir 37 | RUN adduser --ingroup staff --disabled-password --gecos "" app 38 | ENV APP_HOME=/home/app 39 | ENV UMBRELLA_ROOT=/home/app/pwr2 40 | RUN mkdir $UMBRELLA_ROOT $UMBRELLA_ROOT/ui 41 | WORKDIR $APP_HOME 42 | 43 | # Install Hex, Phoenix and Rebar 44 | RUN setuser app mix local.hex --force 45 | RUN setuser app mix local.rebar --force 46 | 47 | # Create Elixir deps and node_modules dirs outside UMBRELLA_ROOT and symlink 48 | # from inside to keep them "safe" when mounting a source volume from HOST 49 | # into UMBRELLA_ROOT. 50 | RUN mkdir $APP_HOME/deps $APP_HOME/node_modules 51 | RUN cd $UMBRELLA_ROOT && ln -s $APP_HOME/deps deps 52 | RUN cd $UMBRELLA_ROOT/ui && ln -s $APP_HOME/node_modules node_modules 53 | 54 | # Add app's node_modules to PATH so they can be found by npm 55 | ENV PATH="/home/app/node_modules/.bin:$PATH" 56 | 57 | # Copy source code 58 | COPY . $UMBRELLA_ROOT 59 | 60 | # Set ownership and permissions 61 | RUN chown -R app:staff /home/app && chmod -R g+s /home/app 62 | 63 | # Uninstall some "heavy" packages and clean up apt-get 64 | RUN apt-get remove build-essential -y 65 | RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 66 | 67 | # Enable SSH (Authorized keys must be copied in each specific project/environment) 68 | RUN rm -f /etc/service/sshd/down 69 | RUN /etc/my_init.d/00_regen_ssh_host_keys.sh 70 | 71 | MAINTAINER Iporaitech 72 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Iporaitech 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 | # Phoenix, Webpack, React and Relay (pwr2) 2 | 3 | Docker image based on Ubuntu providing a base setup for a Phoenix+Webpack+Relay+React project, with some sugar and conventions to develop and build your own web applications. 4 | 5 | **You can also use this project without docker. See #49 for more info** 6 | 7 | _We will improve docs and code including test coverage in the next releases_ 8 | 9 | **NOTICE:** The default branch for this repo is **develop**. Check the [README](https://github.com/iporaitech/pwr2-docker/blob/master/README.md) on master to see what's in the last release. 10 | 11 | ## What is this for? 12 | 13 | You can use this to start/learn/critic an Elixir Umbrella project with a **core** child Phoenix app acting as the web endpoint for _core_ stuff and forwarding requests to `/star-wars` from its router to a Star Wars example app, also a Phoenix app under the same umbrella. 14 | 15 | The **core** also provides the base assets used for other umbrella children as well as functionality to build (_via Webpack_) assets for other children apps. 16 | 17 | So far we've implemented the following: 18 | 19 | * A GraphQL endpoint implemented in Elixir with Absinthe. 20 | * Authentication using JWTs (JSON Web Tokens) via GraphQL (LoginMutation & LogoutMutation). 21 | * Hardcoded Role based Authorization. 22 | * StarWars GraphQL example as **core's** sibling Phoenix app. 23 | * GraphiQL console. 24 | * Some interesting React/Relay components in the client(browser), including a router. 25 | * CSS Modules _integration_ with Material Design Lite, now extracted to [react-to-mdl](https://github.com/iporaitech/react-to-mdl). 26 | * Testing framework for the backend (Elixir/Phoenix). 27 | * [Docs](docs) where you can find additional information about this project. 28 | 29 | 30 | ## Umbrella Architecture 31 | 32 | This whole Umbrella Architecture section is a _WIP_. 33 | 34 | ### The workflow and "lifecycle" 35 | 36 | 1. Create umbrella 37 | 2. Create Core Phoenix app inside umbrella, run it on `HTTP_PORT` making sure everything works. 38 | 3. Turn off Core. 39 | 4. Create StarWars app inside umbrella, run it _also_ on `HTTP_PORT` making sure everything works. 40 | 5. Turn off StarWars. 41 | 6. Configure `Core.Mixfile` to depend on `{:star_wars, in_umbrella: true}`. 42 | 7. Configure `Core.Router` to `forward "/star-wars", StarWars.Router`. 43 | 8. Don't start `StarWars.Endpoint`. Comment it out in **lib/star_wars.ex**. 44 | 9. Create **core/webpack.umbrella.config.js** containing instructions to build one commons bundle and one bundle for each app in the umbrella. 45 | 10. Define a script in **core/package.json** to start Webpack using **webpack.umbrella.config** to build & watch the assets in _dev_. 46 | 11. Adjust watchers in **core/config/dev.exs**. 47 | 12. Turn ON `iex -S mix phoenix.server` from the root of the umbrella making sure everything works. 48 | 13. Analyse and REFACTOR to architecture. After applying principles like DRY, SRP and others, now every app in the umbrella is responsible for a particular _domain_. 49 | 50 | 51 | ### About the front-end Architecture 52 | 53 | * Apps outside umbrella. 54 | * Apps inside umbrella running simultaneously. 55 | * Apps using its own Phoenix page layout and bundle. 56 | * _Apps using page layout from a sibling_ 57 | * _Other scenarios_ 58 | 59 | _Also talk about **yarn**_ 60 | 61 | ## Requirements 62 | 63 | To run this software you need to install [Docker](https://www.docker.com/) on your system. It is very simple and there are a lot of guides and tutorials out there on the Internet. 64 | 65 | >NOTICE: this was only tested in Unix like OS (Mac OS X and Ubuntu) but it should run on Windows without major problems. 66 | 67 | ## Usage Instructions 68 | 69 | With Docker working on localhost, you can setup this project in 2 ways: 70 | 71 | 1. [Setup cloning this GitHub repo and build the image](#setup-cloning-this-github-repo-and-building-the-image). 72 | 2. [Setup pulling Docker image from Docker Cloud repository](#setup-pulling-docker-image-from-docker-cloud-repository). 73 | 74 | With all setup, you can start the corresponding containers (_aka services_) by running the following command in your console: 75 | 76 | ```bash 77 | docker-compose up 78 | ``` 79 | 80 | > you can also just use the [docker-compose.dev.example.yml](docker-compose.dev.example.yml) file directly. 81 | 82 | ```bash 83 | docker-compose -f docker-compose.dev.example.yml up 84 | ``` 85 | 86 | > add `-d` to the above commands if you want them to execute in background 87 | 88 | When the containers are all up and running, you can go inside the web app container and start the web app so you can play with it. To go inside the container execute the following command in the root directory of the project: 89 | > Assuming the files are placed in a directory named **pwr2-docker** and the generated container is named **pwr2docker_web_1** 90 | 91 | `docker exec -it pwr2docker_web_1 bash` 92 | 93 | > You can see the names of running containers with the command `docker ps` on your localhost 94 | 95 | Once inside the container, you'll need to switch to **app** user in order to execute Elixir/Phoenix commands. To switch to **app** user run: 96 | 97 | `su app` 98 | 99 | > All the stuff in **/home/app** belongs to **app** user. You might experience some errors if you try to execute [Elixir](#elixir) commands as **root**. Take a look at the Dockerfile to see how all the stuff is installed 100 | 101 | Once switched to **app** user and in the `APP_HOME` directory, you can: 102 | 103 | 1. Create and migrate **dev** database: `mix do ecto.create, ecto.migrate` 104 | 2. Create and migrate **test** database: `MIX_ENV=test mix do ecto.create, ecto.migrate` 105 | 3. Run db seeds to create a **superadmin** dev user: `mix run apps/webapp/priv/repo/seeds.exs`. Take a look at the seeds file to get the corresponding credentials. 106 | 4. Start the application server: `mix phoenix.server`, or if you want to start the server in a Elixir console `iex -S mix phoenix.server` 107 | 108 | Now you're ready to start making requests to the web app on the port you specified and [try out some examples](#examples). 109 | 110 | ### Testing 111 | 112 | 1. Run all tests `MIX_ENV=test mix espec --trace`. 113 | 2. Run tests while coding, for example, to run a specfic spec: `mix test.watch spec/models/user_spec.exs --trace --focus`. 114 | 115 | Check [ESpec](https://github.com/antonmi/espec), [ESpec.Phoenix](https://github.com/antonmi/espec_phoenix) and [mix test.watch](https://github.com/lpil/mix-test.watch) to see more info about the testing framework used in this project. 116 | 117 | ### Stopping containers 118 | 119 | Execute `docker-compose stop` in the same directory where you started them with `docker-compose up ...`. 120 | 121 | If you want to stop them when running in foreground just press **CTRL+C** once. 122 | 123 | ### Setup cloning this GitHub repo and building the image 124 | 125 | 1. Clone the project to your localhost (_you might want to fork it to your account before_) 126 | 2. Copy the [docker-compose.dev.example.yml](docker-compose.dev.example.yml) to **docker-compose.yml** and adjust the ports and [volumes](#about-docker-volumes). 127 | 3. Build the image. In the root directory of the project execute `docker build .` 128 | 4. When the build finishes, **tag** the image to match the _web image_ defined in your **docker-compose** file. For example `docker tag IMAGE_ID pwr2:latest` 129 | 130 | Now you're ready to [start the containers](#usage-instructions) and try some stuff. 131 | 132 | ### Setup pulling Docker image from Docker Cloud repository 133 | 134 | 1. Open you console or terminal and execute `docker login`. You might not need this. 135 | 2. Pull the image from Docker Cloud. Execute `docker pull iporaitech/pwr2-docker:latest`. You can also search for other **tags** besides latest. 136 | 3. Copy the [docker-compose.dev.example.yml](docker-compose.dev.example.yml) to **docker-compose.yml** on your localhost and adjust the ports, [volumes](#about-docker-volumes) and **image** in **web** section to match the image tag you pulled in the step above. 137 | 138 | Now you're ready to [start the containers](#usage-instructions) and try some stuff. 139 | 140 | Once the containers are up and running you can copy the source code of the base project from the container to your localhost with the following command: 141 | 142 | `rsync -rav -e "ssh -p2224 -i insecure_key" --exclude "_build" --exclude "deps" root@localhost:/home/app/webapp/* .` 143 | 144 | > Assuming you exposed the port 2224 to access the container via SSH 145 | 146 | **Notice:** Tags/Releases in this GitHub repo are the equivalent to tags in the Docker Cloud repository, master is the **latest** image in DockerHub. 147 | 148 | ## Examples 149 | 150 | Once all setup and with the app running and assuming your `HTTP_PORT` is 4000, you can: 151 | 152 | 0. Login with credentials available in [priv/repo/seeds.exs](priv/repo/seeds.exs). Logout is also available. 153 | 1. Visit http://localhost:4000/admin/graphiql to access a [GraphiQL](https://github.com/graphql/graphiql) IDE. 154 | 2. Visit http://localhost:4000/admin/star-wars to experiment with our implementation of the [Relay Star Wars example](https://github.com/relayjs/relay-examples/tree/master/star-wars). The _[database](./web/graphql/star_wars_db.ex)_ for this example is implemented as an [Elixir.Agent](http://elixir-lang.org/docs/stable/elixir/Agent.html) 155 | 3. You can also use something like Google Chrome's Advanced Rest Client(ARC) or any other JSON API client and (with the corresponding Authorization header) send queries to http://localhost:4000/graphql like: 156 | 157 | ```JSON 158 | { 159 | "query": "query GetFactions($names:[String]){factions(names: $names) {id name}}", 160 | "variables": { 161 | "names": ["Galactic Empire", "Alliance to Restore the Republic"] 162 | } 163 | } 164 | ``` 165 | 166 | ## About the technology stack 167 | 168 | Basically, the stack is composed of server application and a Javascript client _rendered in the browser_. 169 | 170 | Following a brief description of major components of this technology stack. 171 | See also the [Dockerfile](https://github.com/iporaitech/pwr2-docker/blob/master/Dockerfile). 172 | 173 | ### Baseimage-docker 174 | 175 | _A minimal Ubuntu base image modified for Docker-friendliness_. This is the base Ubuntu image for this images (see `FROM` in the Dockerfile). It provides a **correct** init process, syslog-ng, logrotate among other stuff needed in most server installations. 176 | 177 | **Current**: [phusion/baseimage:0.9.19](https://github.com/phusion/baseimage-docker/tree/rel-0.9.19) 178 | 179 | ### Erlang/OTP 180 | 181 | [Erlang](http://erlang.org) is a programming language used to build massively scalable soft real-time systems with requirements on high availability. Some of its uses are in telecoms, banking, e-commerce, computer telephony and instant messaging. Erlang's runtime system has built-in support for concurrency, distribution and fault tolerance. 182 | 183 | _This provides the Virtual Machine(VM) on top of which Elixir and Phoenix run_ 184 | 185 | **Current**: [Erlang/OTP 19.0](https://github.com/erlang/otp/tree/maint-19) 186 | 187 | ### Elixir 188 | 189 | [Elixir](http://elixir-lang.org/) is a dynamic, functional language designed for building scalable and maintainable applications. 190 | 191 | Elixir leverages the Erlang VM, known for running low-latency, distributed and fault-tolerant systems, while also being successfully used in web development and the embedded software domain. 192 | 193 | **Current**: [Elixir 1.3.1](https://github.com/elixir-lang/elixir/tree/v1.3.1) 194 | 195 | ### Phoenix 196 | A productive web framework that does not compromise speed and maintainability. 197 | 198 | [Phoenix](http://www.phoenixframework.org) leverages the Erlang VM ability to handle millions of connections alongside Elixir's beautiful syntax and productive tooling for building fault-tolerant systems. 199 | 200 | _We use Phoenix as our backend programming framework_ 201 | 202 | See [mix.exs](mix.exs) to check current version. 203 | 204 | ### Absinthe 205 | 206 | [GraphQL](http://graphql.org) implementation for Elixir, specifically using the packages [Absinthe.Relay](https://github.com/absinthe-graphql/absinthe_relay) and [Absinthe.Plug](https://github.com/absinthe-graphql/absinthe_plug). 207 | 208 | See [mix.exs](mix.exs) to check current versions. 209 | 210 | Also, you might want to checkout the Relay GraphQL specifications to write GraphQL schemas to support Relay apps: 211 | 212 | * [Object Identification](https://facebook.github.io/relay/graphql/objectidentification.htm) 213 | * [Cursor Connections](https://facebook.github.io/relay/graphql/connections.htm) 214 | * [Input Object Mutations](https://facebook.github.io/relay/graphql/mutations.htm) 215 | 216 | 217 | ### Node 218 | [Node.js®](https://nodejs.org/en/) is a JavaScript runtime built on Chrome's V8 JavaScript engine. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient. Node.js' package ecosystem, npm, is the largest ecosystem of open source libraries in the world. 219 | 220 | _Phoenix relies on Node to compile and handle static assets._ 221 | 222 | Take a look at [package.json](package.json) to see all NPM packages used in this application. 223 | 224 | **Current**: [Node v6.3.1](https://github.com/nodejs/node/tree/v6.3.1) 225 | 226 | ### Webpack 227 | 228 | [webpack](https://webpack.github.io) is a **module bundler**. webpack takes modules with dependencies and generates static assets representing those modules. 229 | 230 | _We use this instead of_ **brunch.io** _, which is the default to compile static assets in Phoenix_ 231 | 232 | Take a look at [webpack.config.js](webpack.config.js) to see how the loaders and plugins are configured to make things work in the front-end. 233 | 234 | See [package.json](package.json) to see the current versions of these NPM packages. 235 | 236 | ### Relay & React 237 | 238 | [Relay](https://facebook.github.io/relay/) is a Javascript framework for building data-driven React applications. 239 | 240 | [React](https://facebook.github.io/react/) is a Javascript library for building user interfaces. 241 | 242 | See [package.json](package.json) to see the current versions of these dependencies and other NPM packages in this project. 243 | 244 | ## Base Phoenix+Webpack project 245 | 246 | Check mix.exs and mix.lock to know Elixir dependencies and package.json to know about Javascript dependencies. 247 | 248 | ### How it was created 249 | 250 | Inside the container in `APP_HOME` directory with command `mix phoenix.new --no-brunch`. 251 | 252 | Later on, we've added Webpack and its Babel stuff; loader, presets, polyfill and runtime. 253 | 254 | Take a look at **config/dev.exs** to see how we've configured a watcher for `npm start`, this is how you get live reloading when editing files. 255 | 256 | ## About Docker Volumes 257 | 258 | Although getting better in each release, we still find Docker for Mac volumes to be very slow. That's why we commented out the **volumes** configuration in the **web** section in the **docker-compose.dev.example.yml** file. Obviously, this is not a problem when running Docker on Linux. 259 | 260 | Instead of mounting a volume to work with source code on the container we use [Atom](https://atom.io) _IDE_ with the [remote-sync](https://atom.io/packages/remote-sync) plugin. When this plugin is enabled everytime you save a file it gets automatically copied to the container via SSH. Probably, you might find similar or better strategies with your IDE or you local setup. 261 | 262 | If you want to try volumes when running the image pulled directly from Docker Cloud, remember to first get the source code of this project into your localhost in the directory you want to share with the container. This is because the volumes are mounted from host to container (HOST:CONTAINER in the volumes definition in the **docker-compose** file). If the file don't exist in your localhost when mounting a dir they won't exist in your container in the specified directory even if they did exist in the image. 263 | 264 | ## Issues and Contributing 265 | 266 | See [CONTRIBUTING.md](CONTRIBUTING.md). 267 | 268 | Anyways, just create an issue if you have any question or want something but don't know exactly what it is. 269 | 270 | ## Core Maintainers 271 | With love from **[Iporaitech](http://www.iporaitech.com)**, a small startup based in Paraguay. 272 | * [Hisa Ishibashi](https://github.com/hisapy) 273 | * [Edipo Da Silva](https://github.com/edipox) 274 | * [Tania Paiva](https://github.com/taniadaniela) 275 | 276 | ## License 277 | This project is licensed under [MIT](https://github.com/iporaitech/pwr2-docker/blob/master/LICENSE). 278 | -------------------------------------------------------------------------------- /apps/core/README.md: -------------------------------------------------------------------------------- 1 | # Core Umbrella Child app 2 | -------------------------------------------------------------------------------- /apps/core/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 :core, 10 | ecto_repos: [Core.Repo] 11 | 12 | # Configures Elixir's Logger 13 | config :logger, :console, 14 | format: "$time $metadata[$level] $message\n", 15 | metadata: [:request_id] 16 | 17 | #guardian config 18 | config :guardian, Guardian, 19 | hooks: GuardianDb, 20 | allowed_algos: ["HS512"], # optional 21 | verify_module: Guardian.JWT, # optional 22 | issuer: "Core", 23 | ttl: { 30, :days }, 24 | verify_issuer: true, # optional 25 | secret_key: "JYUIKDM2CQE87DAWG3CY4RNWL8", 26 | # TODO: vv 27 | # secret_key: fn -> 28 | # System.get_env("SECRET_KEY_PASSPHRASE") |> JOSE.JWK.from_file(System.get_env("SECRET_KEY_FILE")) 29 | # end 30 | serializer: Core.GuardianSerializer 31 | 32 | config :guardian_db, GuardianDb, 33 | repo: Core.Repo, 34 | sweep_interval: 120 # minutes 35 | 36 | # Import environment specific config. This must remain at the bottom 37 | # of this file so it overrides the configuration defined above. 38 | import_config "#{Mix.env}.exs" 39 | -------------------------------------------------------------------------------- /apps/core/config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # Do not include metadata nor timestamps in development logs 4 | config :logger, :console, format: "[$level] $message\n" 5 | 6 | # DB config 7 | config :core, Core.Repo, 8 | adapter: Ecto.Adapters.Postgres, 9 | username: System.get_env("PWR2_CORE_DB_USER"), 10 | password: System.get_env("PWR2_CORE_DB_PASSWORD"), 11 | database: System.get_env("PWR2_CORE_DB"), 12 | hostname: System.get_env("PWR2_CORE_DB_HOST"), 13 | port: System.get_env("PWR2_CORE_DB_PORT"), 14 | pool_size: 10 15 | -------------------------------------------------------------------------------- /apps/core/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 :core, Core.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 :core, Core.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 :core, Core.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 :core, Core.Endpoint, server: true 57 | # 58 | # You will also need to set the application root to `.` in order 59 | # for the new static assets to be served after a hot upgrade: 60 | # 61 | # config :core, Core.Endpoint, root: "." 62 | 63 | # Finally import the config/prod.secret.exs 64 | # which should be versioned separately. 65 | import_config "prod.secret.exs" 66 | -------------------------------------------------------------------------------- /apps/core/config/prod.secret.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # In this file, we keep production configuration that 4 | # you likely want to automate and keep it away from 5 | # your version control system. 6 | # 7 | # You should document the content of this 8 | # file or create a script for recreating it, since it's 9 | # kept out of version control and might be hard to recover 10 | # or recreate for your teammates (or you later on). 11 | config :core, Core.Endpoint, 12 | secret_key_base: "XMqzZQHFhTDDvG/LEErDWttNS+ymnNJ+4TvqF/9LarCCJ5DcVa/9azSupRRWhDgY" 13 | 14 | # Configure your database 15 | config :core, Core.Repo, 16 | adapter: Ecto.Adapters.Postgres, 17 | username: "postgres", 18 | password: "postgres", 19 | database: "core_prod", 20 | pool_size: 20 21 | -------------------------------------------------------------------------------- /apps/core/config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # Print only warnings and errors during test 4 | config :logger, level: :warn 5 | 6 | # Configure your database 7 | config :core, Core.Repo, 8 | adapter: Ecto.Adapters.Postgres, 9 | username: System.get_env("PWR2_CORE_DB_USER"), 10 | password: System.get_env("PWR2_CORE_DB_PASSWORD"), 11 | database: System.get_env("PWR2_CORE_TEST_DB"), 12 | hostname: System.get_env("PWR2_CORE_DB_HOST"), 13 | port: System.get_env("PWR2_CORE_DB_PORT"), 14 | pool: Ecto.Adapters.SQL.Sandbox 15 | -------------------------------------------------------------------------------- /apps/core/graphql/resolver.ex: -------------------------------------------------------------------------------- 1 | defmodule Core.GraphQL.Resolver do 2 | @moduledoc """ 3 | Functions to resolve GraphQL schema fields. 4 | """ 5 | 6 | import Canada, only: [can?: 2] 7 | alias Core.Repo 8 | alias Core.User 9 | 10 | @unauthorized_error {:error, "Unauthorized"} 11 | @unauthenticated_error {:error, "User not signed in"} 12 | 13 | def unauthenticated_error, do: @unauthenticated_error 14 | def unauthorized_error, do: @unauthorized_error 15 | 16 | @doc """ 17 | This resolve must be before others to match it first in case of nil user 18 | """ 19 | def resolve(_, _, nil) do 20 | @unauthenticated_error 21 | end 22 | 23 | def resolve(:user, id, current_user) do 24 | with user <- Repo.get(User, id), 25 | true <- current_user |> can?(read user) do 26 | {:ok, user} 27 | else 28 | false -> @unauthorized_error 29 | end 30 | end 31 | 32 | 33 | 34 | end 35 | -------------------------------------------------------------------------------- /apps/core/graphql/schema.ex: -------------------------------------------------------------------------------- 1 | defmodule Core.GraphQL.Schema do 2 | use Absinthe.Schema 3 | use Absinthe.Relay.Schema 4 | alias Core.GraphQL.Resolver 5 | 6 | import_types Core.GraphQL.Types.AuthMutations 7 | import_types Core.GraphQL.Types.Viewer 8 | import_types Core.GraphQL.Types.User 9 | 10 | query do 11 | import_fields :viewer_field 12 | 13 | @desc "This is the field used by Relay to identify an object in the Graph" 14 | node field do 15 | resolve fn 16 | %{type: type, id: id}, %{context: %{current_user: u}} -> 17 | Resolver.resolve(type, id, u) 18 | _, _ -> 19 | {:ok, nil} 20 | end 21 | end 22 | end # END query 23 | 24 | mutation do 25 | import_fields :auth_mutations 26 | import_fields :user_mutations 27 | 28 | end # END mutation 29 | 30 | @desc """ 31 | Node interface provided by Absinthe.Relay. 32 | """ 33 | node interface do 34 | resolve_type fn 35 | %Core.User{}, _-> 36 | :user 37 | end 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /apps/core/graphql/types/auth_mutations.ex: -------------------------------------------------------------------------------- 1 | defmodule Core.GraphQL.Types.AuthMutations do 2 | use Absinthe.Schema.Notation 3 | use Absinthe.Relay.Schema.Notation 4 | import Comeonin.Bcrypt, only: [checkpw: 2, dummy_checkpw: 0] 5 | 6 | #TODO: 7 | # 1. Create resolve functions for these mutations in their own module and test them. It 8 | # will be easier to test the resolvers in isolation than embedded in the muations. 9 | # 2. Refactor to use Scalar types for email and password. 10 | # See http://graphql.org/learn/schema/#scalar-types 11 | 12 | object :auth_mutations do 13 | payload field :login do 14 | input do 15 | field :email, non_null(:string) 16 | field :password, non_null(:string) 17 | end 18 | output do 19 | field :access_token, :string 20 | end 21 | resolve fn 22 | %{email: email, password: password}, _ -> 23 | case authenticate_user(email, password) do 24 | {:ok, user} -> 25 | case Guardian.encode_and_sign(user) do 26 | {:ok, jwt, _full_claims} -> 27 | {:ok, %{access_token: jwt}} 28 | {:error, reason} -> 29 | {:error, reason} 30 | end 31 | {:error, reason} -> 32 | {:error, reason} 33 | end 34 | end 35 | end 36 | 37 | payload field :logout do 38 | output do 39 | field :logged_out, :boolean 40 | end 41 | resolve fn 42 | _, %{context: %{jwt: jwt}} -> 43 | case Guardian.revoke!(jwt) do 44 | :ok -> 45 | {:ok, %{logged_out: true}} 46 | {:error, reason} -> 47 | {:error, reason, logged_out: false} 48 | end 49 | _, _-> 50 | {:error, %{ 51 | reason: "Missing access token", 52 | logged_out: false 53 | }} 54 | end 55 | end 56 | end 57 | 58 | # TODO: move this to User model (or resolver?) 59 | defp authenticate_user(email, password) do 60 | user = Core.Repo.get_by(Core.User, email: email) 61 | cond do 62 | user && checkpw(password, user.password_hash) -> 63 | {:ok, user} 64 | user -> 65 | {:error, :unauthorized} 66 | true -> 67 | dummy_checkpw() 68 | {:error, :not_found} 69 | end 70 | end 71 | 72 | end 73 | -------------------------------------------------------------------------------- /apps/core/graphql/types/user.ex: -------------------------------------------------------------------------------- 1 | defmodule Core.GraphQL.Types.User do 2 | 3 | use Absinthe.Schema.Notation 4 | use Absinthe.Relay.Schema.Notation 5 | alias Absinthe.Relay.Connection 6 | alias Core.{GraphQL.Schema, User, Repo} 7 | import Ecto.Query, only: [from: 2] 8 | import Canada, only: [can?: 2] 9 | 10 | # 11 | # Mutations 12 | # 13 | object :user_mutations do 14 | @desc """ 15 | Add a User to DB if success 16 | """ 17 | payload field :add_user do 18 | input do 19 | field :first_name, non_null(:string) 20 | field :last_name, non_null(:string) 21 | field :phone, :string 22 | field :email, non_null(:string) 23 | field :role, non_null(:string) 24 | field :password, non_null(:string) 25 | field :password_confirmation, non_null(:string) 26 | end 27 | output do 28 | field :user, :user 29 | field :errors, list_of(:string) 30 | end 31 | resolve fn 32 | args, %{context: %{current_user: current_user}} -> 33 | 34 | authorized = current_user |> can?(write User) 35 | 36 | insert_user = fn args -> 37 | %User{} 38 | |> User.registration_changeset(args) 39 | |> User.password_changeset(args) 40 | |> Repo.insert 41 | end 42 | 43 | with true <- authorized, 44 | {:ok, %User{} = user} <- insert_user.(args) 45 | do 46 | {:ok, %{user: user}} 47 | else 48 | false -> 49 | Core.GraphQL.Resolver.unauthorized_error 50 | {:error, %{errors: errors}} -> 51 | {:ok, %{ 52 | errors: Enum.map(errors, fn (err) -> 53 | {field, {field_err_msg, _type_info}} = err 54 | "#{field}: #{field_err_msg}" 55 | end) 56 | }} 57 | {:error, message} -> 58 | {:error, message} 59 | end 60 | end 61 | end 62 | 63 | @desc """ 64 | Edit a User to DB if success 65 | """ 66 | payload field :edit_user do 67 | input do 68 | field :id, non_null(:id) 69 | field :first_name, non_null(:string) 70 | field :last_name, non_null(:string) 71 | field :phone, :string 72 | field :email, non_null(:string) 73 | field :role, non_null(:string) 74 | field :password, :string 75 | field :password_confirmation, :string 76 | end 77 | output do 78 | field :user, :user 79 | field :errors, list_of(:string) 80 | end 81 | resolve fn 82 | args, %{context: %{current_user: current_user}} -> 83 | 84 | authorized = current_user |> can?(write User) 85 | 86 | {:ok, data} = Absinthe.Relay.Node.from_global_id(args.id, Core.GraphQL.Schema) 87 | 88 | update_user = fn args -> 89 | Repo.get!(User, data.id) 90 | |> User.registration_changeset(args) 91 | |> Repo.update 92 | end 93 | 94 | with true <- authorized, 95 | {:ok, %User{} = user} <- update_user.(args) 96 | do 97 | {:ok, %{user: user}} 98 | else 99 | false -> 100 | Core.GraphQL.Resolver.unauthorized_error 101 | {:error, %{errors: errors}} -> 102 | {:ok, %{ 103 | errors: Enum.map(errors, fn (err) -> 104 | {field, {field_err_msg, _type_info}} = err 105 | "#{field}: #{field_err_msg}" 106 | end) 107 | }} 108 | {:error, message} -> 109 | {:error, message} 110 | end 111 | end 112 | end 113 | end # END object :user_mutations 114 | 115 | end 116 | -------------------------------------------------------------------------------- /apps/core/graphql/types/viewer.ex: -------------------------------------------------------------------------------- 1 | defmodule Core.GraphQL.Types.Viewer do 2 | use Absinthe.Schema.Notation 3 | use Absinthe.Relay.Schema.Notation 4 | alias Absinthe.Relay.Connection 5 | alias Core.{GraphQL.Schema, User, Repo} 6 | import Ecto.Query, only: [from: 2] 7 | import Canada, only: [can?: 2] 8 | import Absinthe.Relay.Node, only: [from_global_id: 2] 9 | 10 | node object :user do 11 | field :first_name, :string 12 | field :last_name, :string 13 | field :email, :string 14 | field :phone, :string 15 | field :role, :string 16 | field :password, :string 17 | field :password_confirmation, :string 18 | end 19 | 20 | @desc """ 21 | User connection. 22 | """ 23 | connection node_type: :user 24 | 25 | object :viewer do 26 | field :profile, :user 27 | 28 | @desc "PWR2 users" 29 | connection field :users, node_type: :user do 30 | 31 | resolve fn 32 | query_params, %{source: viewer, context: %{current_user: current_user}} -> 33 | authorized = current_user |> can?(write User) 34 | order = (Map.get(query_params, :order) == "asc") && :asc || :desc 35 | pagination_args = 36 | Map.take(query_params, [:first, :after, :last, :before]) 37 | if authorized do 38 | conn = 39 | (from u in User, order_by: [{^order, u.inserted_at}]) 40 | |> Connection.from_query(&Repo.all/1, pagination_args) 41 | else 42 | Core.GraphQL.Resolver.unauthorized_error 43 | end 44 | {:ok, conn} 45 | end 46 | end 47 | end 48 | 49 | object :viewer_field do 50 | field :viewer, :viewer do 51 | resolve fn 52 | _, %{context: %{current_user: current_user}} -> 53 | {:ok, %{profile: current_user}} 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /apps/core/lib/common.ex: -------------------------------------------------------------------------------- 1 | defmodule Core.Common do 2 | def model do 3 | quote do 4 | use Ecto.Schema 5 | 6 | import Ecto 7 | import Ecto.Changeset 8 | import Ecto.Query 9 | end 10 | end 11 | 12 | @doc """ 13 | When used, dispatch to the appropriate controller/view/etc. 14 | """ 15 | defmacro __using__(which) when is_atom(which) do 16 | apply(__MODULE__, which, []) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /apps/core/lib/core.ex: -------------------------------------------------------------------------------- 1 | defmodule Core do 2 | use Application 3 | 4 | # See http://elixir-lang.org/docs/stable/elixir/Application.html 5 | # for more information on OTP Applications 6 | def start(_type, _args) do 7 | import Supervisor.Spec 8 | 9 | # Define workers and child supervisors to be supervised 10 | children = [ 11 | supervisor(Core.Repo, []), 12 | 13 | # Start your own worker by calling: StarWars.Worker.start_link(arg1, arg2, arg3) 14 | # worker(StarWars.Worker, [arg1, arg2, arg3]), 15 | worker(GuardianDb.ExpiredSweeper, []) 16 | ] 17 | 18 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html 19 | # for other strategies and supported options 20 | opts = [strategy: :one_for_one, name: Core.Supervisor] 21 | Supervisor.start_link(children, opts) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /apps/core/lib/core/guardian_serializer.ex: -------------------------------------------------------------------------------- 1 | defmodule Core.GuardianSerializer do 2 | @behaviour Guardian.Serializer 3 | 4 | alias Core.Repo 5 | alias Core.User 6 | 7 | def for_token(user = %User{}), do: { :ok, "User:#{user.id}" } 8 | def for_token(_), do: { :error, "Unknown resource type" } 9 | 10 | def from_token("User:" <> id), do: { :ok, Repo.get(User, id) } 11 | def from_token(_), do: { :error, "Unknown resource type" } 12 | end 13 | -------------------------------------------------------------------------------- /apps/core/lib/core/models/ability.ex: -------------------------------------------------------------------------------- 1 | alias Core.User 2 | alias Core.Company 3 | alias Core.Person 4 | #check is Core.Repo 5 | defimpl Canada.Can, for: User do 6 | 7 | # 8 | # User abilities 9 | # 10 | def can?(%User{id: user_id}, action, %User{id: user_id}) 11 | when action in [:update, :read, :show, :edit], do: true 12 | def can?(%User{}, action, Company) 13 | when action in [:read, :index, :show], do: true 14 | def can?(%User{}, action, Person) 15 | when action in [:read, :index, :show], do: true 16 | 17 | # 18 | # Admin abilities 19 | # 20 | def can?(%User{role: "admin"}, _, model) 21 | when model in [Company, Person], do: true 22 | 23 | # 24 | # Superadmin abilities (can do everything) 25 | # 26 | def can?(%User{role: "superadmin"}, _, _), do: true 27 | 28 | # 29 | # Undefined is always false 30 | # 31 | def can?(_, _, _), do: false 32 | end 33 | -------------------------------------------------------------------------------- /apps/core/lib/core/models/user.ex: -------------------------------------------------------------------------------- 1 | defmodule Core.User do 2 | use Core.Common, :model 3 | 4 | schema "users" do 5 | field :first_name, :string 6 | field :last_name, :string 7 | field :email, :string 8 | field :phone, :string 9 | field :role, :string 10 | field :password, :string, virtual: true 11 | field :password_hash, :string 12 | 13 | timestamps 14 | end 15 | 16 | # Default validations params used by changesets 17 | # 18 | 19 | @valid_name_length [min: 1, max: 64] 20 | @valid_password_length [min: 8, max: 120] 21 | @valid_email_format ~r/@/ 22 | @valid_roles ~w(superadmin admin client) 23 | 24 | def valid_name_length, do: @valid_name_length 25 | def valid_roles, do: @valid_roles 26 | 27 | # changesets 28 | # 29 | 30 | def changeset(model, params \\ :empty) do 31 | # we need validate_required below because cast/3 filters "" fields out. 32 | model 33 | |> cast(params, ~w(first_name last_name phone)) 34 | |> validate_required([:first_name, :last_name]) 35 | |> validate_length(:first_name, @valid_name_length) 36 | |> validate_length(:last_name, @valid_name_length) 37 | end 38 | 39 | def registration_changeset(model, params) do 40 | # See https://github.com/antonmi/espec#limitations for info about why use __MODULE__ 41 | model 42 | |> __MODULE__.changeset(params) 43 | |> cast(params, [:email, :role]) 44 | |> validate_required([:email, :role]) 45 | |> validate_inclusion(:role, @valid_roles) 46 | |> validate_format(:email, @valid_email_format) 47 | |> unique_constraint(:email) 48 | |> encrypt_passwd() 49 | end 50 | 51 | def password_changeset(model, params) do 52 | model 53 | |> cast(params, [:password]) 54 | |> validate_required([:password]) 55 | |> validate_length(:password, @valid_password_length) 56 | |> validate_confirmation(:password, message: "does not match password!") 57 | end 58 | 59 | # Helper functions 60 | # 61 | defp encrypt_passwd(changeset) do 62 | case changeset do 63 | %Ecto.Changeset{valid?: true, changes: %{password: passwd}} -> 64 | put_change(changeset, :password_hash, Comeonin.Bcrypt.hashpwsalt(passwd)) 65 | _ -> 66 | changeset 67 | end 68 | end 69 | 70 | end 71 | -------------------------------------------------------------------------------- /apps/core/lib/core/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule Core.Repo do 2 | use Ecto.Repo, otp_app: :core 3 | end 4 | -------------------------------------------------------------------------------- /apps/core/lib/plug/graphiql_wrapper.ex: -------------------------------------------------------------------------------- 1 | defmodule Core.Plug.GraphiQLWrapper do 2 | @moduledoc """ 3 | We need to define this module to be able to use Absinthe.Plug multiple times 4 | in Webapp.Router and avoid the error: 5 | 6 | `Absinthe.Plug` has already been forwarded to. A module can only be 7 | forwarded a single time. 8 | """ 9 | 10 | defdelegate init(opts), to: Absinthe.Plug.GraphiQL 11 | defdelegate call(conn, opts), to: Absinthe.Plug.GraphiQL 12 | end 13 | -------------------------------------------------------------------------------- /apps/core/lib/plug/graphql_current_user.ex: -------------------------------------------------------------------------------- 1 | defmodule Core.Plug.GraphQLCurrentUser do 2 | @behaviour Plug 3 | import Plug.Conn 4 | 5 | def init(opts), do: opts 6 | 7 | def call(conn, _opts) do 8 | case build_context(conn) do 9 | {:ok, context} -> 10 | put_private(conn, :absinthe, %{context: context}) 11 | nil -> 12 | send_resp(conn, 403, "Invalid access token") 13 | _ -> 14 | send_resp(conn, 400, nil) 15 | end 16 | end 17 | 18 | defp build_context(conn) do 19 | with user <- Guardian.Plug.current_resource(conn) do 20 | {:ok, %{current_user: user, jwt: Guardian.Plug.current_token(conn)}} 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /apps/core/lib/plug/graphql_wrapper.ex: -------------------------------------------------------------------------------- 1 | defmodule Core.Plug.GraphQLWrapper do 2 | @moduledoc """ 3 | We need to define this module to be able to use Absinthe.Plug multiple times 4 | in Webapp.Router and avoid the error: 5 | 6 | `Absinthe.Plug` has already been forwarded to. A module can only be 7 | forwarded a single time. 8 | """ 9 | 10 | defdelegate init(opts), to: Absinthe.Plug 11 | defdelegate call(conn, opts), to: Absinthe.Plug 12 | end 13 | -------------------------------------------------------------------------------- /apps/core/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Core.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :core, 6 | version: "0.3.4", 7 | elixir: "~> 1.3", 8 | elixirc_paths: elixirc_paths(Mix.env), 9 | compilers: [:gettext] ++ Mix.compilers, 10 | build_embedded: Mix.env == :prod, 11 | start_permanent: Mix.env == :prod, 12 | build_path: "../../_build", 13 | config_path: "../../config/config.exs", 14 | deps_path: "../../deps", 15 | lockfile: "../../mix.lock", 16 | aliases: aliases(), 17 | deps: deps()] 18 | end 19 | 20 | # Configuration for the OTP application. 21 | # 22 | # Type `mix help compile.app` for more information. 23 | def application do 24 | [ 25 | mod: {Core, []}, 26 | applications: applications(Mix.env) 27 | ] 28 | end 29 | # TODO: add specific packages for test env 30 | def applications(env) when env in [:test] do 31 | applications(:default) ++ [:ex_machina] 32 | end 33 | def applications(_) do 34 | [ 35 | :logger, 36 | :gettext, 37 | :ecto, 38 | :postgrex 39 | ] 40 | end 41 | 42 | # Specifies which paths to compile per environment. 43 | defp elixirc_paths(:test), do: ["lib", "graphql", "test/support"] 44 | defp elixirc_paths(_), do: ["lib", "graphql"] 45 | 46 | # Specifies your project dependencies. 47 | # 48 | # Type `mix help deps` for examples and options. 49 | defp deps do 50 | [ 51 | {:ecto, "~> 2.0"}, 52 | {:postgrex, ">= 0.0.0"}, 53 | {:gettext, "~> 0.13"}, 54 | {:comeonin, "~> 2.4"}, 55 | {:guardian, "~> 0.13.0"}, 56 | {:guardian_db, "~> 0.7"}, 57 | {:canada, "~> 1.0.0"}, 58 | {:absinthe_plug, "~> 1.2"}, 59 | {:absinthe_relay, "~> 1.2"}, 60 | 61 | #test packages 62 | {:mix_test_watch, "~> 0.2", only: :test}, 63 | {:ex_machina, "~> 1.0", only: :test} 64 | ] 65 | end 66 | 67 | # Aliases are shortcuts or tasks specific to the current project. 68 | # For example, to create, migrate and run the seeds file at once: 69 | # 70 | # $ mix ecto.setup 71 | # 72 | # See the documentation for `Mix` for more info on aliases. 73 | defp aliases do 74 | [ 75 | "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 76 | "ecto.reset": ["ecto.drop", "ecto.setup"], 77 | "test": ["ecto.create --quiet", "ecto.migrate", "test"] 78 | ] 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /apps/core/priv/repo/migrations/20160926184041_create_extension_citext.exs: -------------------------------------------------------------------------------- 1 | defmodule Core.Repo.Migrations.CreateExtensionCitext do 2 | use Ecto.Migration 3 | 4 | # For info about citext see: 5 | # https://www.postgresql.org/docs/9.1/static/citext.html 6 | def up do 7 | execute("CREATE EXTENSION citext;") 8 | end 9 | 10 | def down do 11 | execute("DROP EXTENSION citext;") 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /apps/core/priv/repo/migrations/20160927213957_create_users.exs: -------------------------------------------------------------------------------- 1 | defmodule Core.Repo.Migrations.CreateUsers do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:users) do 6 | add :first_name, :string, null: false 7 | add :last_name, :string, null: false 8 | add :email, :citext, null: false 9 | add :phone, :string 10 | add :role, :string 11 | add :password_hash, :string 12 | 13 | timestamps 14 | end 15 | 16 | create unique_index(:users, [:email]) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /apps/core/priv/repo/migrations/20160927214005_create_guardian_tokens.exs: -------------------------------------------------------------------------------- 1 | defmodule Core.Repo.Migrations.CreateGuardianTokens do 2 | use Ecto.Migration 3 | 4 | def up do 5 | create table(:guardian_tokens, primary_key: false) do 6 | add :jti, :string, primary_key: true 7 | add :typ, :string 8 | add :aud, :string 9 | add :iss, :string 10 | add :sub, :string 11 | add :exp, :bigint 12 | add :jwt, :text 13 | add :claims, :map 14 | timestamps 15 | end 16 | create unique_index(:guardian_tokens, [:jti, :aud]) 17 | end 18 | 19 | def down do 20 | drop table(:guardian_tokens) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /apps/core/priv/repo/seeds.exs: -------------------------------------------------------------------------------- 1 | # Script for populating the database. You can run it as: 2 | # 3 | # mix run priv/repo/seeds.exs 4 | # 5 | # Inside the script, you can read and write to any of your 6 | # repositories directly: 7 | # 8 | # Core.Repo.insert!(%Core.SomeModel{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | alias Core.Repo 13 | alias Core.User 14 | 15 | %User{} 16 | |> User.registration_changeset(%{ 17 | role: "superadmin", 18 | first_name: "John", 19 | last_name: "Rambo", 20 | email: "jrambo@test.com", 21 | password: "12341234"}) 22 | |> Repo.insert! 23 | -------------------------------------------------------------------------------- /apps/core/test/support/factory.ex: -------------------------------------------------------------------------------- 1 | defmodule Core.Factory do 2 | use ExMachina.Ecto, repo: Core.Repo 3 | 4 | @doc """ 5 | User factory and its role and password helpers 6 | """ 7 | def user_factory do 8 | %Core.User{ 9 | first_name: "Joe", 10 | last_name: "Rambo", 11 | email: sequence(:email, &"joe_#{&1}@test.com") 12 | } 13 | end 14 | def make_admin(user) do 15 | %{user | role: "admin"} 16 | end 17 | def make_superadmin(user) do 18 | %{user | role: "superadmin"} 19 | end 20 | def set_password(user, password) do 21 | user 22 | |> Core.User.registration_changeset(%{password: password}) 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /apps/core/test/support/model_case.ex: -------------------------------------------------------------------------------- 1 | defmodule Core.ModelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | model tests. 5 | 6 | You may define functions here to be used as helpers in 7 | your model tests. See `errors_on/2`'s definition as reference. 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 Core.Repo 20 | 21 | import Ecto 22 | import Ecto.Changeset 23 | import Ecto.Query 24 | import Core.ModelCase 25 | end 26 | end 27 | 28 | setup tags do 29 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Core.Repo) 30 | 31 | unless tags[:async] do 32 | Ecto.Adapters.SQL.Sandbox.mode(Core.Repo, {:shared, self()}) 33 | end 34 | 35 | :ok 36 | end 37 | 38 | @doc """ 39 | Helper for returning list of errors in a struct when given certain data. 40 | 41 | ## Examples 42 | 43 | Given a User schema that lists `:name` as a required field and validates 44 | `:password` to be safe, it would return: 45 | 46 | iex> errors_on(%User{}, %{password: "password"}) 47 | [password: "is unsafe", name: "is blank"] 48 | 49 | You could then write your assertion like: 50 | 51 | assert {:password, "is unsafe"} in errors_on(%User{}, %{password: "password"}) 52 | 53 | You can also create the changeset manually and retrieve the errors 54 | field directly: 55 | 56 | iex> changeset = User.changeset(%User{}, password: "password") 57 | iex> {:password, "is unsafe"} in changeset.errors 58 | true 59 | """ 60 | def errors_on(struct, data) do 61 | struct.__struct__.changeset(struct, data) 62 | |> Ecto.Changeset.traverse_errors(&Core.ErrorHelpers.translate_error/1) 63 | |> Enum.flat_map(fn {key, errors} -> for msg <- errors, do: {key, msg} end) 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /apps/core/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start 2 | -------------------------------------------------------------------------------- /apps/star_wars/README.md: -------------------------------------------------------------------------------- 1 | # StarWars schema example 2 | -------------------------------------------------------------------------------- /apps/star_wars/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 | # Configures Elixir's Logger 9 | config :logger, :console, 10 | format: "$time $metadata[$level] $message\n", 11 | metadata: [:request_id] 12 | 13 | # Import environment specific config. This must remain at the bottom 14 | # of this file so it overrides the configuration defined above. 15 | import_config "#{Mix.env}.exs" 16 | -------------------------------------------------------------------------------- /apps/star_wars/config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # Do not include metadata nor timestamps in development logs 4 | config :logger, :console, format: "[$level] $message\n" 5 | 6 | # TODO: Add DB config HERE 7 | -------------------------------------------------------------------------------- /apps/star_wars/config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | # Do not print debug messages in production 3 | config :logger, level: :info 4 | 5 | import_config "prod.secret.exs" 6 | -------------------------------------------------------------------------------- /apps/star_wars/config/prod.secret.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /apps/star_wars/config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # Print only warnings and errors during test 4 | config :logger, level: :warn 5 | 6 | # TODO: Add DB config HERE 7 | -------------------------------------------------------------------------------- /apps/star_wars/graphql/db.ex: -------------------------------------------------------------------------------- 1 | defmodule StarWars.GraphQL.DB do 2 | @moduledoc """ 3 | DB is a "in memory" database implemented with Elixir.Agent to support the [Relay Star Wars example](https://github.com/relayjs/relay-examples/blob/master/star-wars) 4 | 5 | NOTICE: in the original example the format of the data is id => name where name is a string. 6 | """ 7 | 8 | @initial_state %{ 9 | ship: %{ 10 | "1" => %{id: "1", name: "X-Wing", type: :star_wars_ship}, 11 | "2" => %{id: "2", name: "Y-Wing", type: :star_wars_ship}, 12 | "3" => %{id: "3", name: "A-Wing", type: :star_wars_ship}, 13 | "4" => %{id: "4", name: "Millenium Falcon", type: :star_wars_ship}, 14 | "5" => %{id: "5", name: "Home One", type: :star_wars_ship}, 15 | "6" => %{id: "6", name: "TIE Fighter", type: :star_wars_ship}, 16 | "7" => %{id: "7", name: "TIE Interceptor", type: :star_wars_ship}, 17 | "8" => %{id: "8", name: "Executor", type: :star_wars_ship} 18 | }, 19 | faction: %{ 20 | "1" => %{ 21 | id: "1", 22 | name: "Alliance to Restore the Republic", 23 | ships: ["1", "2", "3", "4", "5"] 24 | }, 25 | "2" => %{ 26 | id: "2", 27 | name: "Galactic Empire", 28 | ships: ["6", "7", "8"] 29 | } 30 | } 31 | } 32 | 33 | @doc """ 34 | Initialize a DB process (Agent) 35 | """ 36 | def start_link do 37 | Agent.start_link(fn -> @initial_state end, name: __MODULE__) 38 | end 39 | 40 | def stop do 41 | Agent.stop(__MODULE__) 42 | end 43 | 44 | def get(type, id) do 45 | case Agent.get(__MODULE__, &get_in(&1, [type, id])) do 46 | nil -> 47 | {:error, "No #{type} with ID #{id}"} 48 | result -> 49 | {:ok, result} 50 | end 51 | end 52 | 53 | @doc """ 54 | Create a new Ship and assign it to Faction identified by faction_id 55 | 56 | NOTICE: this function is not "concurrent" safe because there is no "lock" on "next_ship_id" 57 | and also is doesn't take care of referential integrity. 58 | """ 59 | def create_ship(ship_name, faction_id) do 60 | next_ship_id = Agent.get(__MODULE__, fn data -> 61 | Map.keys(data[:ship]) 62 | |> Enum.map(fn id -> String.to_integer(id) end) 63 | |> Enum.sort 64 | |> List.last 65 | |> Kernel.+(1) # there's a deprecation msg when trying to pipe to + 66 | |> Integer.to_string 67 | end) 68 | 69 | ship_data = %{id: next_ship_id, name: ship_name, type: :star_wars_ship} 70 | case Agent.update(__MODULE__, &put_in(&1, [:ship, next_ship_id], ship_data)) do 71 | nil -> 72 | {:error, "Could not create ship"} 73 | :ok -> 74 | faction_ships = Agent.get(__MODULE__, &Map.get(&1, :faction))[faction_id][:ships] 75 | faction_ships = faction_ships ++ [next_ship_id] 76 | Agent.update(__MODULE__, &put_in(&1, [:faction, faction_id, :ships], faction_ships)) 77 | {:ok, ship_data} 78 | end 79 | end 80 | 81 | def dump_db do 82 | Agent.get(__MODULE__, fn state -> state end) 83 | end 84 | 85 | def get_factions(names) do 86 | factions = Agent.get(__MODULE__, &Map.get(&1, :faction)) |> Map.values 87 | Enum.map(names, fn name -> 88 | factions 89 | |> Enum.find(&(&1.name == name)) 90 | end) 91 | end 92 | 93 | def get_faction(id) do 94 | Agent.get(__MODULE__, &get_in(&1, [:faction, id])) 95 | end 96 | 97 | end 98 | -------------------------------------------------------------------------------- /apps/star_wars/graphql/mutations.ex: -------------------------------------------------------------------------------- 1 | defmodule StarWars.GraphQL.Mutations do 2 | use Absinthe.Schema.Notation 3 | use Absinthe.Relay.Schema.Notation 4 | alias Absinthe.Relay.Connection 5 | alias StarWars.GraphQL.DB 6 | 7 | object :star_wars_mutations do 8 | @desc """ 9 | Creates the IntroduceShip mutation. 10 | 11 | In the mutator configuration in getConfigs() in AddShipMutation.js, edgeName; 12 | which is required for RANGE_ADD mutation types, is set to newShipEdge, that's why 13 | new_ship_edge is in the output block. 14 | """ 15 | payload field :introduce_ship do 16 | input do 17 | field :faction_id, non_null(:id) 18 | field :ship_name, non_null(:string) 19 | end 20 | output do 21 | field :faction, :faction 22 | field :new_ship_edge, :ship_edge 23 | end 24 | resolve fn 25 | # TODO: Add and use context for Auth 26 | %{faction_id: faction_id, ship_name: ship_name}, _ -> 27 | # just fail if not :ok 28 | {:ok, ship} = DB.create_ship(ship_name, faction_id) 29 | faction = DB.get_faction(faction_id) 30 | 31 | # In the original examples a cursorFromObjectInConnection() imported 32 | # from graphql-relay is used to get the cursor. I didn't find 33 | # an implementation for such function in Absinthe.Relay 34 | # 35 | # NOTICE Connection.from_list implementation requires you to pass pagination args. 36 | # I think this should be fixed by implicitly asking for all the edges in the 37 | # connection 38 | cursor = (Connection.from_list(faction.ships, %{last: length(faction.ships)}).edges 39 | |> Enum.find(fn e -> e.node == ship.id end)).cursor 40 | 41 | # resolve fn must return {:ok, data} tuple 42 | {:ok, %{ 43 | faction: faction, 44 | new_ship_edge: %{ 45 | node: ship, 46 | cursor: cursor 47 | } 48 | }} 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /apps/star_wars/graphql/schema.ex: -------------------------------------------------------------------------------- 1 | defmodule StarWars.GraphQL.Schema do 2 | use Absinthe.Schema 3 | use Absinthe.Relay.Schema 4 | alias StarWars.GraphQL.DB, as: StarWarsDB 5 | 6 | # StarWars exaple types and mutations 7 | import_types StarWars.GraphQL.Types 8 | import_types StarWars.GraphQL.Mutations 9 | 10 | def hola do 11 | "hola hisa" 12 | end 13 | 14 | query do 15 | import_fields :star_wars_root_field 16 | 17 | @desc "This is the field used by Relay to identify an object in the Graph" 18 | node field do 19 | resolve fn 20 | %{type: type, id: id}, _ -> 21 | resolve(type, id) # resolve/2 defined below 22 | _, _ -> 23 | {:ok, nil} 24 | end 25 | end 26 | end # END query 27 | 28 | mutation do 29 | import_fields :star_wars_mutations 30 | end # END mutation 31 | 32 | @desc """ 33 | Node interface provided by Absinthe.Relay. 34 | """ 35 | node interface do 36 | resolve_type fn 37 | %{type: :star_wars_ship}, _ -> 38 | :ship 39 | %{ships: _}, _ -> 40 | :faction 41 | end 42 | end 43 | 44 | # 45 | # Resolvers 46 | # 47 | defp resolve(:ship, id) do 48 | with {:ok, ship} <- StarWarsDB.get(:ship, id) do 49 | {:ok, ship} 50 | else 51 | {:error, _no_ship_msg} -> {:ok, nil} 52 | end 53 | end 54 | 55 | defp resolve(:faction, id) do 56 | with {:ok, faction} <- StarWarsDB.get(:faction, id) do 57 | {:ok, faction} 58 | else 59 | {:error, _no_faction_msg} -> {:ok, nil} 60 | end 61 | end 62 | 63 | end 64 | -------------------------------------------------------------------------------- /apps/star_wars/graphql/types.ex: -------------------------------------------------------------------------------- 1 | defmodule StarWars.GraphQL.Types do 2 | @moduledoc """ 3 | This is an Elixir/Absinthe.Relay version of the GraphQL schema defined in: 4 | https://github.com/relayjs/relay-examples/blob/master/star-wars/data/schema.js 5 | 6 | and inspired on 7 | https://github.com/absinthe-graphql/absinthe_relay/tree/master/test/star_wars 8 | """ 9 | use Absinthe.Schema.Notation 10 | use Absinthe.Relay.Schema.Notation 11 | 12 | alias Absinthe.Relay.Connection 13 | alias StarWars.GraphQL.DB 14 | 15 | object :star_wars_root_field do 16 | @desc """ 17 | StarWars field to be used in the RootQueryType. 18 | """ 19 | field :factions, list_of(:faction) do 20 | arg :names, list_of(:string) 21 | resolve fn 22 | args, _ -> 23 | {:ok, DB.get_factions(args[:names])} 24 | end 25 | end 26 | end 27 | 28 | 29 | @desc """ 30 | A ship in the Star Wars saga 31 | 32 | This implements the following type system shorthand: 33 | type Ship : Node { 34 | id: String! 35 | name: String 36 | } 37 | """ 38 | node object :ship do 39 | field :name, non_null(:string), description: "The name of the ship" 40 | end 41 | 42 | @desc """ 43 | A faction in the Star Wars saga 44 | 45 | This implements the following type system shorthand: 46 | type Faction : Node { 47 | id: String! 48 | name: String 49 | ships: ShipConnection 50 | } 51 | """ 52 | node object :faction do 53 | @desc "id of faction in db" 54 | field :faction_id, :id do 55 | resolve fn 56 | _, %{source: faction} -> 57 | {:ok, faction.id} 58 | end 59 | end 60 | 61 | @desc "The name of the faction" 62 | field :name, :string 63 | 64 | @desc "The ships used by the faction." 65 | connection field :ships, node_type: :ship do 66 | resolve fn 67 | pagination_args, %{source: faction} -> 68 | conn = Connection.from_list( 69 | Enum.map(faction.ships, fn 70 | id -> 71 | with {:ok, value} <- DB.get(:ship, id) do 72 | value 73 | end 74 | end), 75 | pagination_args 76 | ) 77 | {:ok, conn} 78 | end 79 | end 80 | end 81 | 82 | @desc """ 83 | We define a connection between a faction and its ships. 84 | 85 | connectionType implements the following type system shorthand: 86 | type ShipConnection { 87 | edges: [ShipEdge] 88 | pageInfo: PageInfo! 89 | } 90 | 91 | connectionType has an edges field - a list of edgeTypes that implement the 92 | following type system shorthand: 93 | type ShipEdge { 94 | cursor: String! 95 | node: Ship 96 | } 97 | """ 98 | connection node_type: :ship 99 | 100 | end 101 | -------------------------------------------------------------------------------- /apps/star_wars/lib/plug/graphiql_wrapper.ex: -------------------------------------------------------------------------------- 1 | defmodule StarWars.Plug.GraphiQLWrapper do 2 | @moduledoc """ 3 | We need to define this module to be able to use Absinthe.Plug multiple times 4 | in Webapp.Router and avoid the error: 5 | 6 | `Absinthe.Plug` has already been forwarded to. A module can only be 7 | forwarded a single time. 8 | """ 9 | 10 | defdelegate init(opts), to: Absinthe.Plug.GraphiQL 11 | defdelegate call(conn, opts), to: Absinthe.Plug.GraphiQL 12 | end 13 | -------------------------------------------------------------------------------- /apps/star_wars/lib/plug/graphql_wrapper.ex: -------------------------------------------------------------------------------- 1 | defmodule StarWars.Plug.GraphQLWrapper do 2 | @moduledoc """ 3 | We need to define this module to be able to use Absinthe.Plug multiple times 4 | in Webapp.Router and avoid the error: 5 | 6 | `Absinthe.Plug` has already been forwarded to. A module can only be 7 | forwarded a single time. 8 | """ 9 | 10 | defdelegate init(opts), to: Absinthe.Plug 11 | defdelegate call(conn, opts), to: Absinthe.Plug 12 | end 13 | -------------------------------------------------------------------------------- /apps/star_wars/lib/star_wars.ex: -------------------------------------------------------------------------------- 1 | defmodule StarWars do 2 | use Application 3 | 4 | # See http://elixir-lang.org/docs/stable/elixir/Application.html 5 | # for more information on OTP Applications 6 | def start(_type, _args) do 7 | import Supervisor.Spec 8 | 9 | # Define workers and child supervisors to be supervised 10 | children = [ 11 | # Start your own worker by calling: StarWars.Worker.start_link(arg1, arg2, arg3) 12 | # worker(StarWars.Worker, [arg1, arg2, arg3]), 13 | worker(StarWars.GraphQL.DB, []) 14 | ] 15 | 16 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html 17 | # for other strategies and supported options 18 | opts = [strategy: :one_for_one, name: StarWars.Supervisor] 19 | Supervisor.start_link(children, opts) 20 | end 21 | 22 | end 23 | -------------------------------------------------------------------------------- /apps/star_wars/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule StarWars.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :star_wars, 6 | version: "0.3.5", 7 | elixir: "~> 1.3", 8 | elixirc_paths: elixirc_paths(Mix.env), 9 | compilers: [:gettext] ++ Mix.compilers, 10 | build_embedded: Mix.env == :prod, 11 | start_permanent: Mix.env == :prod, 12 | build_path: "../../_build", 13 | config_path: "../../config/config.exs", 14 | deps_path: "../../deps", 15 | lockfile: "../../mix.lock", 16 | aliases: aliases(), 17 | deps: deps()] 18 | end 19 | 20 | # Configuration for the OTP application. 21 | # 22 | # Type `mix help compile.app` for more information. 23 | def application do 24 | [ 25 | mod: {StarWars, []}, 26 | applications: [ 27 | :logger 28 | ] 29 | ] 30 | end 31 | 32 | # Specifies which paths to compile per environment. 33 | defp elixirc_paths(:test), do: ["lib", "graphql", "test/support"] 34 | defp elixirc_paths(_), do: ["lib", "graphql"] 35 | 36 | # Specifies your project dependencies. 37 | # 38 | # Type `mix help deps` for examples and options. 39 | defp deps do 40 | [ 41 | {:absinthe_relay, "~> 1.2"}, 42 | {:absinthe_plug, "~> 1.2"}, 43 | 44 | #test packages 45 | {:mix_test_watch, "~> 0.2", only: :test} 46 | ] 47 | end 48 | 49 | # Aliases are shortcuts or tasks specific to the current project. 50 | # For example, to create, migrate and run the seeds file at once: 51 | # 52 | # $ mix ecto.setup 53 | # 54 | # See the documentation for `Mix` for more info on aliases. 55 | defp aliases do 56 | [] 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /apps/webapp/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iporaitech/pwr2-docker/c454db0b851b9d00db868a64b96e567d4a0cc3d9/apps/webapp/.gitignore -------------------------------------------------------------------------------- /apps/webapp/README.md: -------------------------------------------------------------------------------- 1 | # Webapp 2 | 3 | To start your Phoenix app: 4 | 5 | * Install dependencies with `mix deps.get` 6 | * Start Phoenix endpoint with `mix phoenix.server` 7 | 8 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 9 | 10 | ## Learn more 11 | 12 | * Official website: http://www.phoenixframework.org/ 13 | * Guides: http://phoenixframework.org/docs/overview 14 | * Docs: https://hexdocs.pm/phoenix 15 | * Mailing list: http://groups.google.com/group/phoenix-talk 16 | * Source: https://github.com/phoenixframework/phoenix 17 | -------------------------------------------------------------------------------- /apps/webapp/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 | # Configures the endpoint 9 | config :webapp, Webapp.Endpoint, 10 | url: [host: "localhost"], 11 | secret_key_base: "ZBOeN5ogBV5cq2p+ttL4pJbU/7EIVMbkyxBYPebuhf6MzNYjO4HOtp9g2uDdQU9v", 12 | render_errors: [view: Webapp.ErrorView, accepts: ~w(html json)], 13 | pubsub: [name: Webapp.PubSub, 14 | adapter: Phoenix.PubSub.PG2] 15 | 16 | # Configures Elixir's Logger 17 | config :logger, :console, 18 | format: "$time $metadata[$level] $message\n", 19 | metadata: [:request_id] 20 | 21 | # Import environment specific config. This must remain at the bottom 22 | # of this file so it overrides the configuration defined above. 23 | import_config "#{Mix.env}.exs" 24 | -------------------------------------------------------------------------------- /apps/webapp/config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | require System 3 | 4 | # For development, we disable any cache and enable 5 | # debugging and code reloading. 6 | # 7 | # The watchers configuration can be used to run external 8 | # watchers to your application. For example, we use it 9 | # with brunch.io to recompile .js and .css sources. 10 | config :webapp, Webapp.Endpoint, 11 | http: [port: {:system, "PWR2_HTTP_PORT"}], 12 | debug_errors: true, 13 | code_reloader: true, 14 | check_origin: false, 15 | watchers: [ 16 | yarn: [ 17 | "start", 18 | cd: Path.expand("../../../ui", __DIR__) 19 | ] 20 | ] 21 | 22 | # Watch static and templates for browser reloading. 23 | config :webapp, Webapp.Endpoint, 24 | live_reload: [ 25 | patterns: [ 26 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$}, 27 | ~r{priv/gettext/.*(po)$}, 28 | ~r{web/views/.*(ex)$}, 29 | ~r{web/templates/.*(eex)$} 30 | ] 31 | ] 32 | 33 | # Do not include metadata nor timestamps in development logs 34 | config :logger, :console, format: "[$level] $message\n" 35 | 36 | # Set a higher stacktrace during development. Avoid configuring such 37 | # in production as building large stacktraces may be expensive. 38 | config :phoenix, :stacktrace_depth, 20 39 | -------------------------------------------------------------------------------- /apps/webapp/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 :webapp, Webapp.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 :webapp, Webapp.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 :webapp, Webapp.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 :webapp, Webapp.Endpoint, server: true 57 | # 58 | # You will also need to set the application root to `.` in order 59 | # for the new static assets to be served after a hot upgrade: 60 | # 61 | # config :webapp, Webapp.Endpoint, root: "." 62 | 63 | # Finally import the config/prod.secret.exs 64 | # which should be versioned separately. 65 | import_config "prod.secret.exs" 66 | -------------------------------------------------------------------------------- /apps/webapp/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 :webapp, Webapp.Endpoint, 6 | http: [port: {:system, "PWR2_HTTP_PORT"}], 7 | server: false 8 | 9 | # Print only warnings and errors during test 10 | config :logger, level: :warn 11 | 12 | # Configure your database 13 | config :webapp, Webapp.Repo, 14 | adapter: Ecto.Adapters.Postgres, 15 | username: System.get_env("DB_USER"), 16 | password: System.get_env("DB_USER_PASSWORD"), 17 | database: System.get_env("TEST_DB_NAME"), 18 | hostname: System.get_env("DB_HOST"), 19 | port: 5432, 20 | pool: Ecto.Adapters.SQL.Sandbox 21 | -------------------------------------------------------------------------------- /apps/webapp/lib/webapp.ex: -------------------------------------------------------------------------------- 1 | defmodule Webapp do 2 | use Application 3 | 4 | # See http://elixir-lang.org/docs/stable/elixir/Application.html 5 | # for more information on OTP Applications 6 | def start(_type, _args) do 7 | import Supervisor.Spec 8 | 9 | # Define workers and child supervisors to be supervised 10 | children = [ 11 | # Start the endpoint when the application starts 12 | supervisor(Webapp.Endpoint, []), 13 | # Start your own worker by calling: Webapp.Worker.start_link(arg1, arg2, arg3) 14 | # worker(Webapp.Worker, [arg1, arg2, arg3]) 15 | ] 16 | 17 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html 18 | # for other strategies and supported options 19 | opts = [strategy: :one_for_one, name: Webapp.Supervisor] 20 | Supervisor.start_link(children, opts) 21 | end 22 | 23 | # Tell Phoenix to update the endpoint configuration 24 | # whenever the application is updated. 25 | def config_change(changed, _new, removed) do 26 | Webapp.Endpoint.config_change(changed, removed) 27 | :ok 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /apps/webapp/lib/webapp/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule Webapp.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :webapp 3 | 4 | socket "/socket", Webapp.UserSocket 5 | 6 | # This is needed by Wallaby to run CONCURRENT Feature tests 7 | # if Application.get_env(:webapp, :sql_sandbox) do 8 | # plug Phoenix.Ecto.SQL.Sandbox 9 | # end 10 | 11 | # Serve at "/" the static files from "priv/static" directory. 12 | # 13 | # You should set gzip to true if you are running phoenix.digest 14 | # when deploying your static files in production. 15 | plug Plug.Static, 16 | at: "/", from: :webapp, gzip: false, 17 | only: ~w(css fonts images js favicon.ico robots.txt) 18 | 19 | # Code reloading can be explicitly enabled under the 20 | # :code_reloader configuration of your endpoint. 21 | if code_reloading? do 22 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 23 | plug Phoenix.LiveReloader 24 | plug Phoenix.CodeReloader 25 | end 26 | 27 | plug Plug.RequestId 28 | plug Plug.Logger 29 | 30 | plug Plug.Parsers, 31 | parsers: [:urlencoded, :multipart, :json], 32 | pass: ["*/*"], 33 | json_decoder: Poison 34 | 35 | plug Plug.MethodOverride 36 | plug Plug.Head 37 | 38 | # The session will be stored in the cookie and signed, 39 | # this means its contents can be read but not tampered with. 40 | # Set :encryption_salt if you would also like to encrypt it. 41 | plug Plug.Session, 42 | store: :cookie, 43 | key: "_webapp_key", 44 | signing_salt: "/6zggnaD" 45 | 46 | plug Webapp.Router 47 | end 48 | -------------------------------------------------------------------------------- /apps/webapp/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Webapp.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :webapp, 6 | version: "0.3.4", 7 | elixir: "~> 1.3", 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 | build_path: "../../_build", 13 | config_path: "../../config/config.exs", 14 | deps_path: "../../deps", 15 | lockfile: "../../mix.lock", 16 | aliases: aliases(), 17 | deps: deps()] 18 | end 19 | 20 | # Configuration for the OTP application. 21 | # 22 | # Type `mix help compile.app` for more information. 23 | def application do 24 | [ 25 | mod: {Webapp, []}, 26 | applications: applications(Mix.env) 27 | ] 28 | end 29 | 30 | def applications(_) do 31 | [ 32 | :phoenix, 33 | :phoenix_html, 34 | :cowboy, 35 | :logger, 36 | :gettext 37 | ] 38 | end 39 | 40 | # Specifies which paths to compile per environment. 41 | defp elixirc_paths(:test), do: ["lib", "web", "test/support"] 42 | defp elixirc_paths(_), do: ["lib", "web"] 43 | 44 | # Specifies your project dependencies. 45 | # 46 | # Type `mix help deps` for examples and options. 47 | defp deps do 48 | [{:phoenix, "~> 1.2.0"}, 49 | {:phoenix_pubsub, "~> 1.0"}, 50 | {:phoenix_html, "~> 2.8"}, 51 | {:phoenix_live_reload, "~> 1.0", only: :dev}, 52 | {:gettext, "~> 0.13"}, 53 | {:cowboy, "~> 1.0"}, 54 | {:comeonin, "~> 2.4"}, 55 | {:guardian, "~> 0.13.0"}, 56 | {:absinthe_plug, "~> 1.2"}, 57 | {:core, in_umbrella: true}, 58 | {:star_wars, in_umbrella: true}, 59 | 60 | #test packages 61 | {:mix_test_watch, "~> 0.2", only: :test}, 62 | {:wallaby, "~> 0.14.0", only: [:dev, :test]} 63 | ] 64 | end 65 | 66 | # Aliases are shortcuts or tasks specific to the current project. 67 | # See the documentation for `Mix` for more info on aliases. 68 | defp aliases do 69 | [] 70 | end 71 | 72 | end 73 | -------------------------------------------------------------------------------- /apps/webapp/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 | -------------------------------------------------------------------------------- /apps/webapp/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 | -------------------------------------------------------------------------------- /apps/webapp/priv/static/css/deleteme: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iporaitech/pwr2-docker/c454db0b851b9d00db868a64b96e567d4a0cc3d9/apps/webapp/priv/static/css/deleteme -------------------------------------------------------------------------------- /apps/webapp/priv/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iporaitech/pwr2-docker/c454db0b851b9d00db868a64b96e567d4a0cc3d9/apps/webapp/priv/static/favicon.ico -------------------------------------------------------------------------------- /apps/webapp/priv/static/images/phoenix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iporaitech/pwr2-docker/c454db0b851b9d00db868a64b96e567d4a0cc3d9/apps/webapp/priv/static/images/phoenix.png -------------------------------------------------------------------------------- /apps/webapp/priv/static/js/deleteme: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iporaitech/pwr2-docker/c454db0b851b9d00db868a64b96e567d4a0cc3d9/apps/webapp/priv/static/js/deleteme -------------------------------------------------------------------------------- /apps/webapp/priv/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 | -------------------------------------------------------------------------------- /apps/webapp/spec/espec_phoenix_extend.ex: -------------------------------------------------------------------------------- 1 | defmodule ESpec.Phoenix.Extend do 2 | def model do 3 | quote do 4 | alias Webapp.Repo 5 | end 6 | end 7 | 8 | def controller do 9 | quote do 10 | alias Webapp 11 | import Webapp.Router.Helpers 12 | 13 | @endpoint Webapp.Endpoint 14 | end 15 | end 16 | 17 | def view do 18 | quote do 19 | import Webapp.Router.Helpers 20 | end 21 | end 22 | 23 | def channel do 24 | quote do 25 | alias Webapp.Repo 26 | 27 | @endpoint Webapp.Endpoint 28 | end 29 | end 30 | 31 | defmacro __using__(which) when is_atom(which) do 32 | apply(__MODULE__, which, []) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /apps/webapp/spec/graphql/resolver_spec.exs: -------------------------------------------------------------------------------- 1 | defmodule Webapp.GraphQL.ResolverSpec do 2 | use ESpec 3 | import Webapp.Factory 4 | alias Webapp.GraphQL.Resolver 5 | 6 | # TODO: DRY, DRY & DRY !!! 7 | let :current_user, do: insert(:user) 8 | let :user, do: insert(:user) 9 | 10 | # NOTE: In the examples below, we stub can? with the expected behaviour 11 | # to not rely on possibly changing authorization(roles) rules 12 | describe "resolve(:user, id, current_user)" do 13 | context "when current_user can read User idenfified by id" do 14 | before do: allow(Canada.Can).to accept(:can?, fn(_, _, _) -> true end) 15 | it "returns {:ok, user}" do 16 | expect(Resolver.resolve(:user, user.id, current_user)).to eq({:ok, user}) 17 | end 18 | end 19 | context "when id DOES NOT identify any User" do 20 | before do: allow(Canada.Can).to accept(:can?, fn(_, _, _) -> true end) 21 | it "returns {:ok, nil}" do 22 | expect(Resolver.resolve(:user, 999, current_user)).to eq({:ok, nil}) 23 | end 24 | end 25 | context "when current_user CAN'T read User idenfified by id" do 26 | before do: allow(Canada.Can).to accept(:can?, fn(_, _, _) -> false end) 27 | it "returns {:error, \"Unauthorized\" }" do 28 | expect(Resolver.resolve(:user, user.id, current_user)).to eq({:error, "Unauthorized"}) 29 | end 30 | end 31 | end 32 | 33 | describe "resolve(_, _, nil)... current_user is nil" do 34 | it "returns {:error, \"User not signed in\"}" do 35 | expect(Resolver.resolve(:user, user.id, nil)).to eq({:error, "User not signed in"}) 36 | end 37 | end 38 | 39 | # TODO: Don't forget to remove this StarWars stuff after extracting for pwr2-docker 40 | describe "StarWars examples" do 41 | describe "resolve(:ship, id, current_user)" do 42 | let :ship do 43 | {:ok, ship} = StarWars.GraphQL.DB.get(:ship, "1") 44 | ship 45 | end 46 | context "when current_user can read Ship idenfified by id" do 47 | it "returns {:ok, ship}" do 48 | expect Resolver.resolve(:ship, ship.id, current_user |> make_superadmin) 49 | |> to(eq {:ok, ship}) 50 | end 51 | end 52 | context "when id DOES NOT identify any Ship" do 53 | it "returns {:ok, nil}" do 54 | expect Resolver.resolve(:ship, "xxx", current_user |> make_superadmin) 55 | |> to(eq {:ok, nil}) 56 | end 57 | end 58 | context "when current_user CAN'T read Ship idenfified by id" do 59 | it "returns {:error, \"Unauthorized\" }" do 60 | expect Resolver.resolve(:ship, ship.id, current_user) 61 | |> to(eq {:error, "Unauthorized"}) 62 | end 63 | end 64 | end 65 | 66 | describe "resolve(:faction, id, current_user)" do 67 | let :faction do 68 | {:ok, faction} = StarWars.GraphQL.DB.get(:faction, "1") 69 | faction 70 | end 71 | context "when current_user can read Faction idenfified by id" do 72 | it "returns {:ok, faction}" do 73 | expect Resolver.resolve(:faction, faction.id, current_user |> make_superadmin) 74 | |> to(eq {:ok, faction}) 75 | end 76 | end 77 | context "when id DOES NOT identify any Faction" do 78 | it "returns {:ok, nil}" do 79 | expect Resolver.resolve(:faction, "xxx", current_user |> make_superadmin) 80 | |> to(eq {:ok, nil}) 81 | end 82 | end 83 | context "when current_user CAN'T read Faction idenfified by id" do 84 | it "returns {:error, \"Unauthorized\" }" do 85 | expect Resolver.resolve(:faction, faction.id, current_user) 86 | |> to(eq {:error, "Unauthorized"}) 87 | end 88 | end 89 | end 90 | end 91 | 92 | end 93 | -------------------------------------------------------------------------------- /apps/webapp/spec/lib/plug/graphql_context_spec.exs: -------------------------------------------------------------------------------- 1 | defmodule Webapp.Plug.GraphQLContextSpec do 2 | @endpoint Webapp.Endpoint 3 | use ESpec 4 | use Phoenix.ConnTest 5 | import Webapp.Factory 6 | 7 | let :user, do: insert(:user) 8 | 9 | describe "call/2" do 10 | context "when Guardian.Plug.LoadResource finds an user from the current_token in conn" do 11 | it "assigns %{context: %{current_user: user, jwt: current_token}} to :absinthe in conn" do 12 | {:ok, jwt, _full_claims} = Guardian.encode_and_sign(user) 13 | conn = build_conn 14 | |> put_req_header("authorization", "Bearer #{jwt}") 15 | 16 | expect(conn.private[:absinthe]).to be_nil 17 | conn = post(conn, "/graphql") 18 | expect(conn.private[:absinthe]).to eq(%{context: %{current_user: user, jwt: jwt}}) 19 | end 20 | end 21 | 22 | context "when Guardian.Plug.LoadResource DOESN'T find an user" do 23 | it "assigns %{context: %{current_user: nil, jwt: nil}} to :absinthe in conn" do 24 | jwt = "whatever" 25 | conn = build_conn 26 | |> put_req_header("authorization", "Bearer #{jwt}") 27 | 28 | expect(conn.private[:absinthe]).to be_nil 29 | conn = post(conn, "/graphql") 30 | expect(conn.private[:absinthe]).to eq(%{context: %{current_user: nil, jwt: nil}}) 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /apps/webapp/spec/models/user_spec.exs: -------------------------------------------------------------------------------- 1 | defmodule Webapp.UserSpec do 2 | use ESpec.Phoenix, model: User 3 | alias Webapp.User 4 | 5 | @valid_params %{ 6 | first_name: "J", 7 | last_name: "R", 8 | email: "j@r.com", 9 | role: "admin", 10 | password: "12341234" 11 | } 12 | 13 | describe "changeset/2" do 14 | let :min, do: Keyword.fetch!(User.valid_name_length, :min) 15 | let :max, do: Keyword.fetch!(User.valid_name_length, :max) 16 | 17 | # Apparently is not possible to loop through a list to create examples (it). 18 | # For example for f <- ~w(first_name last_name), do it "validates length of #{f}" end 19 | # ** (FunctionClauseError) no function clause matching in ESpec.ExampleHelpers.it/1 20 | # 21 | 22 | it "validates length of first_name and last_name" do 23 | expect User.changeset(%User{}, @valid_params).valid? 24 | |> to(be_true) 25 | 26 | for f <- [:first_name, :last_name] do 27 | too_short = %{@valid_params | f => String.slice(@valid_params.first_name, 0, min-1)} 28 | too_long = %{@valid_params | f => String.duplicate("a", max+1)} 29 | 30 | for params <- [too_short, too_long] do 31 | expect User.changeset(%User{}, params).valid? 32 | |> to(be_false) 33 | end 34 | 35 | end 36 | end 37 | end # END changeset/2 38 | 39 | describe "registration_changeset/2" do 40 | it "calls changeset" do 41 | allow User |> to(accept :changeset) 42 | User.registration_changeset(%User{}, @valid_params) 43 | expect User |> to(accepted :changeset, [%User{}, @valid_params]) 44 | end 45 | 46 | it "validates inclusion of role in User.valid_roles" do 47 | for r <- ["", "anything"] do 48 | expect User.registration_changeset(%User{}, %{@valid_params | role: r}).valid? 49 | |> to(be_false) 50 | end 51 | end 52 | it "validates format and uniqueness of email" 53 | it "validates length of password" 54 | it "encrypts password hash" 55 | end # END registration_changeset/2 56 | 57 | end 58 | -------------------------------------------------------------------------------- /apps/webapp/spec/phoenix_helper.exs: -------------------------------------------------------------------------------- 1 | Code.require_file("spec/espec_phoenix_extend.ex") 2 | 3 | Ecto.Adapters.SQL.Sandbox.mode(Webapp.Repo, :manual) 4 | -------------------------------------------------------------------------------- /apps/webapp/spec/spec_helper.exs: -------------------------------------------------------------------------------- 1 | Code.require_file("#{__DIR__}/phoenix_helper.exs") 2 | 3 | ESpec.configure fn(config) -> 4 | config.before fn(_tags) -> 5 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Webapp.Repo) 6 | end 7 | 8 | config.finally fn(_shared) -> 9 | Ecto.Adapters.SQL.Sandbox.checkin(Webapp.Repo, []) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /apps/webapp/spec/support/factory.ex: -------------------------------------------------------------------------------- 1 | defmodule Webapp.Factory do 2 | use ExMachina.Ecto, repo: Webapp.Repo 3 | 4 | def user_factory do 5 | %Webapp.User{ 6 | first_name: "Jane", 7 | last_name: "Smith", 8 | email: sequence(:email, &"email#{&1}@example.com") 9 | } 10 | end 11 | 12 | def make_admin(user) do 13 | %{user | role: "admin"} 14 | end 15 | 16 | def make_superadmin(user) do 17 | %{user | role: "superadmin"} 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /apps/webapp/test/controllers/page_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Webapp.PageControllerTest do 2 | use Webapp.ConnCase, async: true 3 | 4 | describe "index - GET /:app_name" do 5 | test "renders js bundle specific to :app_name", %{conn: conn} do 6 | app_name = "any-app-name" 7 | conn = get(conn, "/#{app_name}") 8 | assert_html(conn, app_name) 9 | end 10 | end 11 | 12 | describe "index - GET /" do 13 | test "renders core.js", %{conn: conn} do 14 | conn = get(conn, "/") 15 | assert_html(conn) 16 | end 17 | end 18 | 19 | describe "index - GET /login" do 20 | test "renders core.js", %{conn: conn} do 21 | conn = get(conn, "/login") 22 | assert_html(conn) 23 | end 24 | end 25 | 26 | defp assert_html(conn, app_name \\ nil) do 27 | resp = html_response(conn, 200) 28 | js_filename = app_name && "#{app_name}.js" || "core.js" 29 | 30 | assert resp =~ "
" 31 | assert resp =~ "" 32 | assert resp =~ "" 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /apps/webapp/test/support/acceptance_case.ex: -------------------------------------------------------------------------------- 1 | defmodule Webapp.AcceptanceCase do 2 | use ExUnit.CaseTemplate 3 | 4 | using do 5 | quote do 6 | use Wallaby.DSL 7 | 8 | import Webapp.Router.Helpers 9 | import Webapp.Factory 10 | import Webapp.AcceptanceCase 11 | 12 | # The default endpoint for testing 13 | @endpoint Webapp.Endpoint 14 | end 15 | end 16 | 17 | setup tags do 18 | # {:ok, session} = Wallaby.start_session(metadata: metadata) 19 | # {:ok, session: session} 20 | :ok 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /apps/webapp/test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule Webapp.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 and query models. 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 Webapp.Endpoint 25 | end 26 | end 27 | 28 | setup tags do 29 | 30 | :ok 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /apps/webapp/test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule Webapp.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 and query models. 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 Webapp.Router.Helpers 23 | 24 | # The default endpoint for testing 25 | @endpoint Webapp.Endpoint 26 | end 27 | end 28 | 29 | setup tags do 30 | {:ok, conn: Phoenix.ConnTest.build_conn()} 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /apps/webapp/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | # TODO: install phantomjs in Dockerfile to enable this conf 2 | # {:ok, _} = Application.ensure_all_started(:wallaby) 3 | # Application.put_env(:wallaby, :base_url, Webapp.Endpoint.url) 4 | 5 | # By default exclude slow and expensive feature test 6 | ExUnit.configure(exclude: [feature: true]) 7 | 8 | ExUnit.start 9 | -------------------------------------------------------------------------------- /apps/webapp/web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule Webapp.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | # channel "room:*", Webapp.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 | # Webapp.Endpoint.broadcast("users_socket:#{user.id}", "disconnect", %{}) 34 | # 35 | # Returning `nil` makes this socket anonymous. 36 | def id(_socket), do: nil 37 | end 38 | -------------------------------------------------------------------------------- /apps/webapp/web/controllers/doc_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule Webapp.DocController do 2 | use Webapp.Web, :controller 3 | @docs_path System.get_env("APP_HOME") <> "/docs" 4 | 5 | def index(conn, _params) do 6 | {:ok, list_of_docs_name} = File.ls(@docs_path) 7 | list_of_docs = Enum.map(list_of_docs_name, fn(filename) -> 8 | %{title: doc_title(filename), filename: "#{filename}"} 9 | end) 10 | json conn, %{data: list_of_docs} 11 | end 12 | 13 | def show(conn, %{"_format" => "json", "filename" => filename}) do 14 | {:ok, _} = File.cwd 15 | doc_path = "#{@docs_path}/#{filename}" 16 | doc_data = File.read!(doc_path) 17 | json conn, %{data: doc_data} 18 | end 19 | 20 | def show(conn, %{"filename" => _}) do 21 | # TODO: create plug to set app_name in router. 22 | render conn, Webapp.PageView, "index.html", app_name: "core" 23 | end 24 | 25 | defp doc_title(doc_name) do 26 | doc_name 27 | |> String.replace("_", " ") 28 | |> String.replace(".md", "") 29 | |> String.capitalize 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /apps/webapp/web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule Webapp.PageController do 2 | use Webapp.Web, :controller 3 | 4 | def index(conn, %{"app_name" => app_name }) do 5 | render conn, "index.html", app_name: app_name 6 | end 7 | def index(conn, _params) do 8 | render conn, "index.html", app_name: "core" 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /apps/webapp/web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule Webapp.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 Webapp.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: :webapp 24 | end 25 | -------------------------------------------------------------------------------- /apps/webapp/web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule Webapp.Router do 2 | use Webapp.Web, :router 3 | 4 | # Pipelines 5 | pipeline :browser do 6 | plug :accepts, ["html", "json"] 7 | plug :fetch_session 8 | plug :fetch_flash 9 | plug :protect_from_forgery 10 | plug :put_secure_browser_headers 11 | end 12 | pipeline :graphql do 13 | plug :accepts, ["json"] 14 | plug Guardian.Plug.VerifyHeader, realm: "Bearer" 15 | plug Guardian.Plug.LoadResource, serializer: Core.GuardianSerializer 16 | plug Core.Plug.GraphQLCurrentUser, repo: Core.Repo 17 | end 18 | 19 | # Routes 20 | scope "/admin", Webapp, as: :admin do 21 | pipe_through [:browser] 22 | 23 | Enum.each ["/", "/graphiql", "users", "users/:id/edit"], fn path -> 24 | get path, PageController, :index 25 | end 26 | end 27 | 28 | scope "/docs", Webapp do 29 | pipe_through [:browser] 30 | 31 | get "/", PageController, :docs 32 | get "/index", DocController, :index 33 | get "/:filename", DocController, :show 34 | end 35 | 36 | scope "/", Webapp do 37 | pipe_through [:browser] 38 | 39 | get "/", PageController, :index 40 | get "/login", PageController, :index 41 | 42 | # From here on we go to "apps" not specified before 43 | # TODO: create plug to handle app_name NOT FOUND 44 | get "/:app_name", PageController, :index 45 | get "/:app_name/graphiql", PageController, :index 46 | end 47 | 48 | scope "/graphql" do 49 | pipe_through [:graphql] 50 | 51 | forward "/graphiql", Core.Plug.GraphiQLWrapper, schema: Core.GraphQL.Schema 52 | forward "/", Core.Plug.GraphQLWrapper, schema: Core.GraphQL.Schema 53 | end 54 | 55 | # TODO: find out how to dinamically init Absinthe.Plug 56 | scope "/star_wars/graphql" do 57 | pipe_through [:graphql] 58 | 59 | forward "/graphiql", StarWars.Plug.GraphiQLWrapper, schema: StarWars.GraphQL.Schema 60 | forward "/", StarWars.Plug.GraphQLWrapper, schema: StarWars.GraphQL.Schema 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /apps/webapp/web/static/css/app.css: -------------------------------------------------------------------------------- 1 | /* 2 | Put here general app css if needed 3 | */ 4 | -------------------------------------------------------------------------------- /apps/webapp/web/templates/layout/app.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
19 |
20 | {this.props.literal}
21 |
22 |
23 | );
24 | }
25 | };
26 |
27 | export default CodeBlock;
28 |
--------------------------------------------------------------------------------
/ui/core/src/docs/DocPage.js:
--------------------------------------------------------------------------------
1 | // file: docs/DocPage.js
2 | import React from 'react';
3 | import { withRouter } from 'react-router';
4 | import ReactMarkdown from 'react-markdown';
5 | import CodeBlock from './CodeBlock';
6 |
7 | // Base components
8 | import { mdlUpgrade } from 'react-to-mdl';
9 | import Grid, { Cell } from 'react-to-mdl/grid';
10 |
11 | // CSS
12 | import CSSModules from 'react-css-modules';
13 | import styles from './styles.scss';
14 | import './hljs.scss';
15 |
16 | class DocPage extends React.Component {
17 | constructor(props) {
18 | super(props);
19 | }
20 |
21 | render() {
22 | return (
23 |