├── .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 | pwr2 11 | 12 | "> 13 | "> 14 | 15 | 16 | 17 | <%= render @view_module, @view_template, assigns %> 18 | 19 | <%= if Mix.env == :test do %> 20 | 21 | <% end %> 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /apps/webapp/web/templates/page/graphiql.html.eex: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /apps/webapp/web/templates/page/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /apps/webapp/web/templates/page/star_wars.html.eex: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /apps/webapp/web/templates/page/user_widget.html.eex: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /apps/webapp/web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule Webapp.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | use Phoenix.HTML 7 | 8 | @doc """ 9 | Generates tag for inlined form input errors. 10 | """ 11 | def error_tag(form, field) do 12 | if error = form.errors[field] do 13 | content_tag :span, translate_error(error), class: "help-block" 14 | end 15 | end 16 | 17 | @doc """ 18 | Translates an error message using gettext. 19 | """ 20 | def translate_error({msg, opts}) do 21 | # Because error messages were defined within Ecto, we must 22 | # call the Gettext module passing our Gettext backend. We 23 | # also use the "errors" domain as translations are placed 24 | # in the errors.po file. 25 | # Ecto will pass the :count keyword if the error message is 26 | # meant to be pluralized. 27 | # On your own code and templates, depending on whether you 28 | # need the message to be pluralized or not, this could be 29 | # written simply as: 30 | # 31 | # dngettext "errors", "1 file", "%{count} files", count 32 | # dgettext "errors", "is invalid" 33 | # 34 | if count = opts[:count] do 35 | Gettext.dngettext(Webapp.Gettext, "errors", msg, msg, count, opts) 36 | else 37 | Gettext.dgettext(Webapp.Gettext, "errors", msg, opts) 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /apps/webapp/web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Webapp.ErrorView do 2 | use Webapp.Web, :view 3 | 4 | def render("404.html", _assigns) do 5 | "Page not found" 6 | end 7 | 8 | def render("500.html", _assigns) do 9 | "Internal server error" 10 | end 11 | 12 | # In case no render clause matches or no 13 | # template is found, let's render it as 500 14 | def template_not_found(_template, assigns) do 15 | render "500.html", assigns 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /apps/webapp/web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Webapp.LayoutView do 2 | use Webapp.Web, :view 3 | 4 | def server_js_vars(app_name) do 5 | cond do 6 | Enum.member?([:dev, :test], Mix.env) -> 7 | graphql = app_name != "core" && "/#{app_name}/graphql" || "/graphql" 8 | graphiql = graphql <> "/graphiql" 9 | """ 10 | 11 | var GRAPHQL_URL = '#{graphql}'; 12 | var GRAPHIQL_URL = '#{graphiql}' 13 | """ 14 | :prod -> 15 | """ 16 | 17 | var GRAPHQL_URL = '#{System.get_env("GRAPHQL_URL")}'; 18 | """ 19 | end 20 | |> raw 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /apps/webapp/web/views/page_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Webapp.PageView do 2 | use Webapp.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /apps/webapp/web/web.ex: -------------------------------------------------------------------------------- 1 | defmodule Webapp.Web do 2 | @moduledoc """ 3 | A module that keeps using definitions for controllers, 4 | views and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use Webapp.Web, :controller 9 | use Webapp.Web, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. 17 | """ 18 | 19 | def controller do 20 | quote do 21 | use Phoenix.Controller 22 | 23 | import Webapp.Router.Helpers 24 | import Webapp.Gettext 25 | end 26 | end 27 | 28 | def view do 29 | quote do 30 | use Phoenix.View, root: "web/templates" 31 | 32 | # Import convenience functions from controllers 33 | import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1, action_name: 1] 34 | 35 | # Use all HTML functionality (forms, tags, etc) 36 | use Phoenix.HTML 37 | 38 | import Webapp.Router.Helpers 39 | import Webapp.ErrorHelpers 40 | import Webapp.Gettext 41 | 42 | # TODO: Add tests for this and move it to its own module 43 | def view_action(conn) do 44 | view = view_module(conn) |> to_string 45 | "#{String.replace(view, ~r/.+\.(.+)View/, "\\g{1}")}-#{action_name conn}" 46 | |> String.downcase 47 | end 48 | end 49 | end 50 | 51 | def router do 52 | quote do 53 | use Phoenix.Router 54 | end 55 | end 56 | 57 | def channel do 58 | quote do 59 | use Phoenix.Channel 60 | import Webapp.Gettext 61 | end 62 | end 63 | 64 | @doc """ 65 | When used, dispatch to the appropriate controller/view/etc. 66 | """ 67 | defmacro __using__(which) when is_atom(which) do 68 | apply(__MODULE__, which, []) 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /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 | use Mix.Config 4 | 5 | # The configuration defined here will only affect the dependencies 6 | # in the apps directory when commands are executed from the umbrella 7 | # project. For this reason, it is preferred to configure each child 8 | # application directly and import its configuration, as done below. 9 | import_config "../apps/*/config/config.exs" 10 | 11 | # Sample configuration (overrides the imported configuration above): 12 | # 13 | # config :logger, :console, 14 | # level: :info, 15 | # format: "$date $time [$level] $metadata$message\n", 16 | # metadata: [:user_id] 17 | -------------------------------------------------------------------------------- /docker-compose.dev.example.yml: -------------------------------------------------------------------------------- 1 | web: 2 | image: iporaitech/pwr2-docker:latest 3 | command: /sbin/my_init --enable-insecure-key #enable insecure key for dev 4 | # volumes: 5 | # - .:/home/app/pwr2 6 | links: 7 | - db 8 | ports: 9 | - "4000:4000" # use this port for HTTP_PORT 10 | - "4443:4443" # use this port for HTTPS_PORT 11 | - "2222:22" # use this port to access container via "normal" SSH 12 | - "44369:4369" # use this port to config SSH forward to distributed node 13 | - "9001:9001" # use this port when starting webapp as distributed node 14 | environment: 15 | MIX_ENV: "${MIX_ENV}" 16 | NODE_ENV: "${NODE_ENV}" 17 | 18 | # Webapp 19 | PWR2_HTTP_PORT: "${PWR2_HTTP_PORT}" 20 | PWR2_HTTPS_PORT: "${PWR2_HTTPS_PORT}" 21 | 22 | # Core 23 | PWR2_CORE_DB: "${PWR2_CORE_DB}" 24 | PWR2_CORE_DB_HOST: "${PWR2_CORE_DB_HOST}" 25 | PWR2_CORE_DB_PORT: "${PWR2_CORE_DB_PORT}" 26 | PWR2_CORE_DB_USER: "${PWR2_CORE_DB_USER}" 27 | PWR2_CORE_DB_PASSWORD: "${PWR2_CORE_DB_PASSWORD}" 28 | PWR2_CORE_TEST_DB: "${PWR2_CORE_TEST_DB}" 29 | PWR2_CORE_GRAPHQL_URL: "${PWR2_CORE_GRAPHQL_URL}" 30 | 31 | # StarWars 32 | PWR2_STAR_WARS_GRAPHQL_URL: "${PWR2_STAR_WARS_GRAPHQL_URL}" 33 | 34 | # Mailer 35 | PWR2_MAILER_SERVER: "${PWR2_MAILER_SERVER}" 36 | PWR2_MAILER_USERNAME: "${PWR2_MAILER_USERNAME}" 37 | PWR2_MAILER_PASSWORD: "${PWR2_MAILER_PASSWORD}" 38 | volumes_from: 39 | - mix_deps 40 | 41 | db: 42 | image: postgres:9.4.4 43 | ports: 44 | - "55432:5432" # use this port to access Postgres from outside of the container. 45 | 46 | mix_deps: 47 | image: iporaitech/pwr2-docker:latest 48 | volumes: 49 | - /home/app/mix_deps 50 | -------------------------------------------------------------------------------- /docs/react_router_relay_and_auth.md: -------------------------------------------------------------------------------- 1 | # react-router-relay and Auth 2 | 3 | ## Auth 4 | 5 | [Auth](apps/webapp/web/static/js/lib/auth.js) is a class created to manage the `Relay.Environment` and any information related to the authentication process, for example the [JWT access token](https://jwt.io/introduction/). Most of the `Auth` functions are self explanatory the only thing that could be a little tricky is the `logout(callback)`. Logout receives an optional callback function as argument, but `Auth` also have an `onLogout` property that is an optional function that is executed before the mentioned callback. This `onLogout` property is very useful because `Auth` is imported as a singleton so you can set this property in any moment. You can see this in action in the code bellow. 6 | 7 | 8 | ## react-router-relay 9 | 10 | We need to reset the Router environment because when an user logs out we need to remove all the data stored there. A simple solution would be to use environment as a function, in that case we could change the function's returned value and it would be very straightforward. But as [react-router-relay does not support this feature](https://goo.gl/vwxrPK) we have to do the following: 11 | ```javascript 12 | /* 13 | Extract routes as a constant so it is re used for every render 14 | To avoid errors like: 15 | Warning: [react-router] You cannot change ; it will be ignored 16 | */ 17 | 18 | const routes = ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ) 29 | 30 | /* 31 | Wrapp the routes in another component so you can use the state to trigger 32 | react render every time the environment changes 33 | */ 34 | 35 | class Application extends React.Component{ 36 | 37 | constructor(props){ 38 | super(props); 39 | this.state = { environment: Auth.getEnvironment() }; 40 | Auth.onLogout = ()=> this.handleLogout(); 41 | } 42 | 43 | /* 44 | Now every time you change the state's environment 45 | For example with 'handleLogout', react will re render the routes 46 | */ 47 | handleLogout(){ 48 | this.setState({ environment: Auth.getEnvironment() }) 49 | } 50 | 51 | render(){ 52 | return ( 53 | 56 | {routes} 57 | 58 | ); 59 | } 60 | } 61 | 62 | ReactDOM.render( 63 | , 64 | document.getElementById('react-root') 65 | ); 66 | 67 | ``` 68 | -------------------------------------------------------------------------------- /docs/use_case_examples.md: -------------------------------------------------------------------------------- 1 | # Use Case examples. 2 | 3 | Here we will provide some examples on how to pwr2-docker your project. 4 | 5 | ## Add pwr2-docker to a recently started project. 6 | 7 | You can add stuff from **pwr2-docker** in your project by performing the following procedure. 8 | 9 | 1. Add **pwr2-docker** as a remote in your local git repository 10 | 11 | `git remote add pwr2 git@github.com:iporaitech/pwr2-docker.git` 12 | 13 | 2. Fetch **pwr2-docker** repo to your local 14 | 15 | `git fetch pwr2` 16 | 17 | 3. Now, you can merge the pwr2 branch you want into your project. For example: 18 | 19 | ``` 20 | # Squash commits so you don't pollute your project's history 21 | git merge --squash pwr2/master 22 | 23 | # Resolve possible conflicts 24 | git commit -m "Start base system from pwr2-docker/master" 25 | ``` 26 | 27 | 4. Adjust your **docker-compose.yml** and follow the usage instructions in the README. 28 | 29 | That's it. Now you'll have a working base system based on the technologies in **pwr2-docker**. 30 | 31 | 32 | ## Update your pwr2-docker based project. 33 | > TODO 34 | 35 | ## Add specific pwr2-docker stuff into your project. 36 | > TODO (maybe here we'll explain how to cherrypick specific commits) 37 | -------------------------------------------------------------------------------- /insecure_key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEA1ZswRub+3DvSEnBiyM5YRpRzRYV88vO1X2j867u6pyCHUNXv 3 | RRCr7ahMLPIVYsZwlHb4sF+Zb3DJOBH+E265o93chdMxbWG44k0spf10JRevA0JX 4 | NrEwHR8vesCR74e5MuddbSic88lsEqnnn+Fo3lStvE6nBp6tbqdEu7GhTtHSYejn 5 | wwINnA5ocsHkd1YE9L2Scqw1e4bXveTAQnSvhqe33QshGXFpt0tQwRWngah887f2 6 | P54wFSm2C/UyFT7pvIjINKzIi4vUoXz/nU+V7neTmt3XDdjloYg3ycOaX4RSVneO 7 | HCf7hkcEKbzbPzzSrGAAYYC5UzFB+ImsIbtV2wIDAQABAoIBAQCjROxgtX2Gft7y 8 | Ix8Ol9IXmK6HLCI2XZt7ovb3hFWGGzHy0qMBql2P2Tzoed1o038Hq+woe9n+uTnE 9 | dtQ6rD6PByzgyW2VSsWTjCOdeJ5HH9Qw7ItXDZZWHBkhfYHOkXI4e2oI3qshGAtY 10 | NLALn7KVhioJriCyyaSM2KOLx5khcY+EJ1inQfwQJKqPGsdKc72liz07T8ifRj+m 11 | NLKtwrxlK3IXYfIdgLp/1pCKdrC80DhprMsD4xvNgq4pCR9jd4FoqM9t/Up5ppTm 12 | +p6A/bDwdIPh6cFFeyMP+G3+bTlW1Gg7RLoNCc6qh53WWVgEOQqdLHcQ8Ge4RLmb 13 | wLUmnRuRAoGBAPfXYfjpPZi8rPIQpux13Bs7xaS1/Fa9WqrEfrPptFdUVHeFCGY8 14 | qOUVewPviHdbs0nB71Ynk9/e96agFYijQdqTQzVnpYI4i8GiGk5gPMiB2UYeJ/HZ 15 | mIB3jtWyf6Z/GO0hJ1a6mX0XD3zJGNqFaiwqaYgdO1Fwh9gcH3O2lHyjAoGBANyj 16 | TGDBYHpxPu6uKcGreLd0SgO61PEj7aOSNfrBB2PK83A+zjZCFZRIWqjfrkxGG6+a 17 | 2WuHbEHuCGvu2V5juHYxbAD/38iV/lQl/2xyvN1eR/baE3US06qn6idxjnmeNZDy 18 | DelAx1RGuEvLX1TNAzDTxBwYyzH3W2RpKAUAD11pAoGAN38YJhd8Pn5JL68A4cQG 19 | dGau/BHwHjAqZEC5qmmzgzaT72tvlQ0SOLHVqOzzHt7+x45QnHciSqfvxnTkPYNp 20 | FJuTGhtKWV12FfbJczFjivZgg63u/d3eoy2iY0GkCdE98KNS3r3L7tHCGwwgr5Xe 21 | T2Nz3BHHnZXYJVEuzcddeocCgYEAnhDjPAHtw2p0Inxlb9kPb6aBC/ECcwtBSUkL 22 | IOy/BZA1HPnxs89eNFAtmwQ8k2o6lXDDSJTJSuZj5CdGVKfuU8aOUJz/Tm2eudxL 23 | A/+jLJhJyCBthhcJyx3m04E4CAr+5ytyKeP9qXPMvoghcNg66/UabuKYV+CU+feX 24 | 8xUa7NkCgYEAlX8HGvWMmiG+ZRFB//3Loy87bBxGlN0pUtCEScabZxdB2HkI9Vp7 25 | Yr67QIZ3y7T88Mhkwam54JCjiV+3TZbSyRMOjkqf7UhTCZC6hHNqdUnlpv4bJWeW 26 | i5Eun8ltYxBnemNc2QGxA4r+KCspi+pRvWNGzL3PFVBGXiLsmOMul78= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Pwr2.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [apps_path: "apps", 6 | build_embedded: Mix.env == :prod, 7 | start_permanent: Mix.env == :prod, 8 | deps: deps()] 9 | end 10 | 11 | # Dependencies can be Hex packages: 12 | # 13 | # {:mydep, "~> 0.3.0"} 14 | # 15 | # Or git/path repositories: 16 | # 17 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} 18 | # 19 | # Type "mix help deps" for more examples and options. 20 | # 21 | # Dependencies listed here are available only for this project 22 | # and cannot be accessed from applications inside the apps folder 23 | defp deps do 24 | [] 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"absinthe": {:hex, :absinthe, "1.2.3", "634da49aacc4c9568760cdf3fe8c400ce9368815985e366e893ea8fcbe58f405", [:mix], []}, 2 | "absinthe_plug": {:hex, :absinthe_plug, "1.2.0", "5cccb5ee6bb26ab7e0e9285731db1d1ac30864a7217fb1a76b6a3edb3070af4a", [], [{:absinthe, "~> 1.2.0", [hex: :absinthe, optional: false]}, {:plug, "~> 1.0", [hex: :plug, optional: false]}]}, 3 | "absinthe_relay": {:hex, :absinthe_relay, "1.2.0", "503141482874c8a430320cb867007d5cc99390cc40a8e24d065d994bf97dc5ad", [], [{:absinthe, "~> 1.2.0", [hex: :absinthe, optional: false]}, {:ecto, "~> 1.0 or ~> 2.0", [hex: :ecto, optional: true]}]}, 4 | "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [], []}, 5 | "canada": {:hex, :canada, "1.0.1", "da96d0ff101a0c2a6cc7b07d92b8884ff6508f058781d3679999416feacf41c5", [], []}, 6 | "certifi": {:hex, :certifi, "0.7.0", "861a57f3808f7eb0c2d1802afeaae0fa5de813b0df0979153cbafcd853ababaf", [], []}, 7 | "comeonin": {:hex, :comeonin, "2.6.0", "74c288338b33205f9ce97e2117bb9a2aaab103a1811d243443d76fdb62f904ac", [:make, :mix], []}, 8 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [], []}, 9 | "cowboy": {:hex, :cowboy, "1.0.4", "a324a8df9f2316c833a470d918aaf73ae894278b8aa6226ce7a9bf699388f878", [], [{:cowlib, "~> 1.0.0", [hex: :cowlib, optional: false]}, {:ranch, "~> 1.0", [hex: :ranch, optional: false]}]}, 10 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [], []}, 11 | "db_connection": {:hex, :db_connection, "1.1.0", "b2b88db6d7d12f99997b584d09fad98e560b817a20dab6a526830e339f54cdb3", [], [{:connection, "~> 1.0.2", [hex: :connection, optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, optional: true]}]}, 12 | "decimal": {:hex, :decimal, "1.3.1", "157b3cedb2bfcb5359372a7766dd7a41091ad34578296e951f58a946fcab49c6", [], []}, 13 | "ecto": {:hex, :ecto, "2.0.6", "9dcbf819c2a77f67a66b83739b7fcc00b71aaf6c100016db4f798930fa4cfd47", [], [{:db_connection, "~> 1.0", [hex: :db_connection, optional: true]}, {:decimal, "~> 1.1.2 or ~> 1.2", [hex: :decimal, optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, optional: true]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: false]}, {:postgrex, "~> 0.12.0", [hex: :postgrex, optional: true]}, {:sbroker, "~> 1.0-beta", [hex: :sbroker, optional: true]}]}, 14 | "espec": {:hex, :espec, "1.2.0", "93a057d9b200619c6a64676f27b5e7d3d6d80097763232a76bb88d8b0c3db28d", [:mix], [{:meck, "~> 0.8.4", [hex: :meck, optional: false]}]}, 15 | "espec_phoenix": {:hex, :espec_phoenix, "0.6.4", "a8ab48e21ae261466848c41bce520f88028ecf439b9cbd436758ccde824db474", [:mix], [{:espec, ">= 1.2.0", [hex: :espec, optional: false]}, {:phoenix, ">= 1.0.0", [hex: :phoenix, optional: false]}]}, 16 | "ex_machina": {:hex, :ex_machina, "1.0.2", "1cc49e1a09e3f7ab2ecb630c17f14c2872dc4ec145d6d05a9c3621936a63e34f", [], [{:ecto, "~> 2.0", [hex: :ecto, optional: true]}]}, 17 | "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [], []}, 18 | "gettext": {:hex, :gettext, "0.13.0", "daafbddc5cda12738bb93b01d84105fe75b916a302f1c50ab9fb066b95ec9db4", [:mix], []}, 19 | "guardian": {:hex, :guardian, "0.13.0", "37c5b5302617276093570ee938baca146f53e1d5de1f5c2b8effb1d2fea596d2", [:mix], [{:jose, "~> 1.8", [hex: :jose, optional: false]}, {:phoenix, "~> 1.2.0", [hex: :phoenix, optional: true]}, {:plug, "~> 1.0", [hex: :plug, optional: false]}, {:poison, ">= 1.3.0", [hex: :poison, optional: false]}, {:uuid, ">=1.1.1", [hex: :uuid, optional: false]}]}, 20 | "guardian_db": {:hex, :guardian_db, "0.7.0", "40fecd29504002213f3321cf474f1a30f9186c99a900d1a29bd9b972243a1ac8", [], [{:ecto, "~> 2.0.0-rc", [hex: :ecto, optional: false]}, {:guardian, "~> 0.12", [hex: :guardian, optional: false]}, {:postgrex, ">= 0.11.1", [hex: :postgrex, optional: true]}]}, 21 | "hackney": {:hex, :hackney, "1.6.3", "d489d7ca2d4323e307bedc4bfe684323a7bf773ecfd77938f3ee8074e488e140", [], [{:certifi, "0.7.0", [hex: :certifi, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, optional: false]}]}, 22 | "httpoison": {:hex, :httpoison, "0.10.0", "4727b3a5e57e9a4ff168a3c2883e20f1208103a41bccc4754f15a9366f49b676", [], [{:hackney, "~> 1.6.3", [hex: :hackney, optional: false]}]}, 23 | "idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [], []}, 24 | "jose": {:hex, :jose, "1.8.0", "1ee027c5c0ff3922e3bfe58f7891509e8f87f771ba609ee859e623cc60237574", [], [{:base64url, "~> 0.0.1", [hex: :base64url, optional: false]}]}, 25 | "meck": {:hex, :meck, "0.8.4", "59ca1cd971372aa223138efcf9b29475bde299e1953046a0c727184790ab1520", [], []}, 26 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [], []}, 27 | "mime": {:hex, :mime, "1.0.1", "05c393850524767d13a53627df71beeebb016205eb43bfbd92d14d24ec7a1b51", [], []}, 28 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [], []}, 29 | "mix_test_watch": {:hex, :mix_test_watch, "0.2.6", "9fcc2b1b89d1594c4a8300959c19d50da2f0ff13642c8f681692a6e507f92cab", [], [{:fs, "~> 0.9.1", [hex: :fs, optional: false]}]}, 30 | "phoenix": {:hex, :phoenix, "1.2.1", "6dc592249ab73c67575769765b66ad164ad25d83defa3492dc6ae269bd2a68ab", [], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, optional: false]}, {:plug, "~> 1.1", [hex: :plug, optional: false]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: false]}]}, 31 | "phoenix_ecto": {:hex, :phoenix_ecto, "3.0.1", "42eb486ef732cf209d0a353e791806721f33ff40beab0a86f02070a5649ed00a", [], [{:ecto, "~> 2.0", [hex: :ecto, optional: false]}, {:phoenix_html, "~> 2.6", [hex: :phoenix_html, optional: true]}, {:plug, "~> 1.0", [hex: :plug, optional: false]}]}, 32 | "phoenix_html": {:hex, :phoenix_html, "2.8.0", "777598a4b6609ad6ab8b180f7b25c9af2904644e488922bb9b9b03ce988d20b1", [], [{:plug, "~> 1.0", [hex: :plug, optional: false]}]}, 33 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.0.6", "4490d588c4f60248b1c5f1f0dc0a7271e1aed4bddbd8b1542630f7bf6bc7b012", [:mix], [{:fs, "~> 0.9.1", [hex: :fs, optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2-rc", [hex: :phoenix, optional: false]}]}, 34 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.1", "c10ddf6237007c804bf2b8f3c4d5b99009b42eca3a0dfac04ea2d8001186056a", [], []}, 35 | "plug": {:hex, :plug, "1.3.0", "6e2b01afc5db3fd011ca4a16efd9cb424528c157c30a44a0186bcc92c7b2e8f3", [], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, optional: true]}, {:mime, "~> 1.0", [hex: :mime, optional: false]}]}, 36 | "poison": {:hex, :poison, "2.2.0", "4763b69a8a77bd77d26f477d196428b741261a761257ff1cf92753a0d4d24a63", [], []}, 37 | "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [], []}, 38 | "postgrex": {:hex, :postgrex, "0.12.1", "2f8b46cb3a44dcd42f42938abedbfffe7e103ba4ce810ccbeee8dcf27ca0fb06", [], [{:connection, "~> 1.0", [hex: :connection, optional: false]}, {:db_connection, "~> 1.0-rc.4", [hex: :db_connection, optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, optional: false]}]}, 39 | "ranch": {:hex, :ranch, "1.2.1", "a6fb992c10f2187b46ffd17ce398ddf8a54f691b81768f9ef5f461ea7e28c762", [], []}, 40 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [], []}, 41 | "uuid": {:hex, :uuid, "1.1.5", "96cb36d86ee82f912efea4d50464a5df606bf3f1163d6bdbb302d98474969369", [], []}, 42 | "wallaby": {:hex, :wallaby, "0.14.0", "476a97a7d592fbd0e3875337c02afeee1ae059e8247791d99b90286ad97917b0", [], [{:httpoison, "~> 0.9", [hex: :httpoison, optional: false]}, {:poison, ">= 1.4.0", [hex: :poison, optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: false]}]}} 43 | -------------------------------------------------------------------------------- /ui/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pwr2-ui-core", 3 | "description": "UI for the core app inside pwr2-docker's umbrella project.", 4 | "main": "webpack.config.js", 5 | "version": "0.3.4", 6 | "contributors": [ 7 | { 8 | "name": "Iporaitech and others", 9 | "url": "https://github.com/iporaitech/pwr2-docker/graphs/contributors" 10 | } 11 | ], 12 | "readme": "https://github.com/iporaitech/pwr2-docker/blob/master/README.md", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/iporaitech/pwr2-docker" 16 | }, 17 | 18 | "license": "MIT" 19 | } 20 | -------------------------------------------------------------------------------- /ui/core/src/AppLayout.js: -------------------------------------------------------------------------------- 1 | // file: layout/index.js 2 | import React from 'react'; 3 | 4 | // Base components 5 | import LogoutLink from 'core/shared/LogoutLink'; 6 | import Layout, { 7 | LayoutHeader, 8 | LayoutIcon, 9 | LayoutHeaderRow, 10 | LayoutTitle, 11 | LayoutSpacer, 12 | Navigation, 13 | NavigationLink, 14 | LayoutDrawer, 15 | LayoutContent, 16 | } from 'react-to-mdl/layout'; 17 | 18 | export default class extends React.Component { 19 | render() { 20 | return ( 21 | 22 | 23 | 24 | 25 | pwr2-docker 26 | 27 | 28 | Home 29 | Star Wars 30 | GraphiQL 31 | 32 | 33 | 34 | 35 | 36 | 37 | Admin 38 | Users 39 | GraphiQL 40 | Documentation 41 | 42 | 43 | 44 | 45 | 46 | { this.props.children } 47 | 48 | 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ui/core/src/babelRelayPlugin.js: -------------------------------------------------------------------------------- 1 | // TODO: Create a dev version of this that use introspectionQuery to get schema and 2 | // another one that use a schema.json generated file to be compiled with the assets. 3 | const babelRelayPlugin = require('babel-relay-plugin'); 4 | 5 | /** 6 | * This should be for production 7 | */ 8 | // const path = require('path'); 9 | // const schema = require(path.resolve('schema.json')); 10 | 11 | /** 12 | * This should be for development 13 | */ 14 | const introspectionQuery = require('graphql/utilities').introspectionQuery; 15 | const request = require('sync-request'); 16 | const server = process.env.PWR2_CORE_GRAPHQL_URL; 17 | // Get the GraphQL schema from the server to be used later by Relay 18 | const response = request('POST', server, { 19 | qs: { 20 | query: introspectionQuery 21 | } 22 | }); 23 | const schema = JSON.parse(response.body.toString('utf-8')); 24 | 25 | // In the end need we need to export this 26 | module.exports = babelRelayPlugin(schema.data, { 27 | abortOnError: true, 28 | }); 29 | -------------------------------------------------------------------------------- /ui/core/src/docs/CodeBlock.js: -------------------------------------------------------------------------------- 1 | // file: docs/CodeBlock.js 2 | import React from 'react'; 3 | import hljs from 'highlight.js' 4 | 5 | class CodeBlock extends React.Component{ 6 | componentDidMount() { 7 | this.highlightCode(); 8 | } 9 | componentDidUpdate() { 10 | this.highlightCode(); 11 | } 12 | highlightCode() { 13 | hljs.highlightBlock(this.refs.code); 14 | } 15 | 16 | render() { 17 | return ( 18 |
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 | 24 | 25 |
26 | 28 |
29 |
30 |
31 | ); 32 | } 33 | } 34 | 35 | /*** exports ***/ 36 | export default withRouter( 37 | mdlUpgrade( 38 | CSSModules(DocPage, styles) 39 | ) 40 | ); 41 | -------------------------------------------------------------------------------- /ui/core/src/docs/TableOfContents.js: -------------------------------------------------------------------------------- 1 | // file: docs/TableOfContents.js 2 | import React from 'react'; 3 | import { withRouter, Link } from 'react-router'; 4 | 5 | // Base components 6 | import { mdlUpgrade } from 'react-to-mdl'; 7 | import Grid, { Cell } from 'react-to-mdl/grid'; 8 | import Button from 'react-to-mdl/button'; 9 | import Menu, { MenuItem } from 'react-to-mdl/menu'; 10 | import Layout, { LayoutIcon } from 'react-to-mdl/layout'; 11 | 12 | // CSS 13 | import CSSModules from 'react-css-modules'; 14 | import styles from './styles.scss'; 15 | 16 | class TableOfContents extends React.Component { 17 | render() { 18 | 19 | return ( 20 | 21 | 22 | 25 | 26 | Table Of Contents 27 | {this.props.docs.map((item) => { 28 | return 29 | 32 | {item.title} 33 | 34 | ; 35 | })} 36 | 37 | 38 | 39 | ); 40 | } 41 | } 42 | 43 | /*** exports ***/ 44 | export default withRouter( 45 | mdlUpgrade( 46 | CSSModules(TableOfContents, styles) 47 | ) 48 | ); 49 | -------------------------------------------------------------------------------- /ui/core/src/docs/hljs.scss: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | github.com style (c) Vasily Polovnyov 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | color: #333; 12 | background: #f8f8f8; 13 | } 14 | 15 | .hljs-comment, .hljs-quote { 16 | color: #998; 17 | font-style: italic; 18 | } 19 | 20 | .hljs-keyword, .hljs-selector-tag, .hljs-subst { 21 | color: #333; 22 | font-weight: bold; 23 | } 24 | 25 | .hljs-number, .hljs-literal, .hljs-variable, .hljs-template-variable, .hljs-tag .hljs-attr { 26 | color: #008080; 27 | } 28 | 29 | .hljs-string, .hljs-doctag { 30 | color: #d14; 31 | } 32 | 33 | .hljs-title, .hljs-section, .hljs-selector-id { 34 | color: #900; 35 | font-weight: bold; 36 | } 37 | 38 | .hljs-subst { 39 | font-weight: normal; 40 | } 41 | 42 | .hljs-type, .hljs-class .hljs-title { 43 | color: #458; 44 | font-weight: bold; 45 | } 46 | 47 | .hljs-tag, .hljs-name, .hljs-attribute { 48 | color: #000080; 49 | font-weight: normal; 50 | } 51 | 52 | .hljs-regexp, .hljs-link { 53 | color: #009926; 54 | } 55 | 56 | .hljs-symbol, .hljs-bullet { 57 | color: #990073; 58 | } 59 | 60 | .hljs-built_in, .hljs-builtin-name { 61 | color: #0086b3; 62 | } 63 | 64 | .hljs-meta { 65 | color: #999; 66 | font-weight: bold; 67 | } 68 | 69 | .hljs-deletion { 70 | background: #fdd; 71 | } 72 | 73 | .hljs-addition { 74 | background: #dfd; 75 | } 76 | 77 | .hljs-emphasis { 78 | font-style: italic; 79 | } 80 | 81 | .hljs-strong { 82 | font-weight: bold; 83 | } 84 | -------------------------------------------------------------------------------- /ui/core/src/docs/index.js: -------------------------------------------------------------------------------- 1 | // file: docs/index.js 2 | import React from 'react'; 3 | import { withRouter } from 'react-router'; 4 | import fetch from 'isomorphic-fetch'; 5 | import TableOfContents from './TableOfContents'; 6 | import DocPage from './DocPage'; 7 | 8 | // Base components 9 | import { mdlUpgrade } from 'react-to-mdl'; 10 | import Spinner from 'react-to-mdl/spinner'; 11 | import Layout from 'react-to-mdl/layout'; 12 | 13 | // CSS 14 | import CSSModules from 'react-css-modules'; 15 | import styles from './styles.scss'; 16 | 17 | class Index extends React.Component { 18 | constructor(props) { 19 | super(props); 20 | this.state = { 21 | file: '', 22 | docs: [], 23 | filename: this.props.params.filename 24 | }; 25 | } 26 | 27 | componentDidMount() { 28 | this.loadTOC(); 29 | this.fetchDocsData(`${this.state.filename}?_format=json`) 30 | .then(response => { 31 | this.setState({ 32 | file: response.data, 33 | }) 34 | }); 35 | } 36 | componentDidUpdate() { 37 | let currentFileName = this.props.params.filename; 38 | if(this.state.filename != currentFileName){ 39 | this.fetchDocsData(`${currentFileName}?_format=json`) 40 | .then(response => { 41 | this.setState({ 42 | file: response.data, 43 | filename: currentFileName 44 | }) 45 | }); 46 | } 47 | } 48 | responseToJson(response) { 49 | if(response.status >= 400) throw new Error(response.status); 50 | return response.json(); 51 | } 52 | fetchDocsData(path) { 53 | // TODO: change window.location.origin to a URL variable rendered from the server. 54 | return fetch(`${window.location.origin}/docs/${path}`) 55 | .then(this.responseToJson) 56 | .catch(err => { 57 | console.log(`Request failed. ${err}`) 58 | }); 59 | } 60 | loadTOC() { 61 | this.fetchDocsData('index') 62 | .then(response => { 63 | this.setState({ 64 | docs: response.data, 65 | }) 66 | }); 67 | } 68 | 69 | render() { 70 | return ( 71 |
72 | 73 | 74 |
75 | ); 76 | } 77 | } 78 | 79 | /*** exports ***/ 80 | export default withRouter( 81 | mdlUpgrade( 82 | CSSModules(Index, styles) 83 | ) 84 | ); 85 | -------------------------------------------------------------------------------- /ui/core/src/docs/styles.scss: -------------------------------------------------------------------------------- 1 | @import '~material-design-lite/src/card/card'; 2 | @import '~material-design-lite/src/palette/palette'; 3 | 4 | .toc-menu li:first-child { 5 | @extend .mdl-card__title; 6 | @extend .mdl-color--primary; 7 | @extend .mdl-color-text--white; 8 | margin-bottom: 10px !important; 9 | } 10 | 11 | .toc-menu li:first-child a { 12 | @extend .mdl-card__title-text; 13 | } 14 | 15 | .toc-menu a { 16 | color: black; 17 | text-decoration: none !important; 18 | } 19 | 20 | h2.mdl-menu__title-text { 21 | border-bottom: 0; 22 | } 23 | 24 | .doc-page a { 25 | color: #4183C4; 26 | text-decoration: none; 27 | } 28 | 29 | .doc-page a.absent { 30 | color: #cc0000; 31 | } 32 | 33 | .doc-page a.anchor { 34 | display: block; 35 | padding-left: 30px; 36 | margin-left: -30px; 37 | cursor: pointer; 38 | position: absolute; 39 | top: 0; 40 | left: 0; 41 | bottom: 0; 42 | } 43 | 44 | .doc-page a:hover { 45 | text-decoration: underline; 46 | } 47 | 48 | .doc-page h1, h2, h3, h4, h5, h6 { 49 | margin: 20px 0 10px; 50 | padding: 0; 51 | font-weight: bold; 52 | -webkit-font-smoothing: antialiased; 53 | cursor: text; 54 | position: relative; 55 | } 56 | 57 | .doc-page h1 tt, .doc-page h1 code { 58 | font-size: inherit; 59 | } 60 | 61 | .doc-page h2 tt, .doc-page h2 code { 62 | font-size: inherit; 63 | } 64 | 65 | .doc-page h3 tt, .doc-page h3 code { 66 | font-size: inherit; 67 | } 68 | 69 | .doc-page h4 tt, .doc-page h4 code { 70 | font-size: inherit; 71 | } 72 | 73 | .doc-page h5 tt, .doc-page h5 code { 74 | font-size: inherit; 75 | } 76 | 77 | .doc-page h6 tt, .doc-page h6 code { 78 | font-size: inherit; 79 | } 80 | 81 | .doc-page h1 { 82 | font-size: 34px; 83 | border-bottom: 1px solid #DDDDDD; 84 | color: black; 85 | } 86 | 87 | .doc-page h2 { 88 | font-size: 24px; 89 | border-bottom: 1px solid #DDDDDD; 90 | color: black; 91 | } 92 | 93 | .doc-page h3 { 94 | font-size: 18px; 95 | } 96 | 97 | .doc-page h4 { 98 | font-size: 16px; 99 | } 100 | 101 | .doc-page h5 { 102 | font-size: 14px; 103 | } 104 | 105 | .doc-page h6 { 106 | color: #777777; 107 | font-size: 14px; 108 | } 109 | 110 | .doc-page p, .doc-page blockquote, .doc-page ul, .doc-page ol, .doc-page dl, .doc-page li, .doc-page table, .doc-page pre { 111 | margin: 15px 0; 112 | } 113 | 114 | .doc-page hr { 115 | border: 0 none; 116 | color: #DDDDDD; 117 | height: 4px; 118 | padding: 0; 119 | } 120 | 121 | body>.doc-page h2:first-child { 122 | margin-top: 0; 123 | padding-top: 0; 124 | } 125 | 126 | body>.doc-page h1:first-child { 127 | margin-top: 0; 128 | padding-top: 0; 129 | } 130 | 131 | body>.doc-page h1:first-child+h2 { 132 | margin-top: 0; 133 | padding-top: 0; 134 | } 135 | 136 | body>.doc-page h3:first-child, body>.doc-page h4:first-child, body>.doc-page h5:first-child, body>.doc-page h6:first-child { 137 | margin-top: 0; 138 | padding-top: 0; 139 | } 140 | 141 | .doc-page a:first-child h1, .doc-page a:first-child h2, .doc-page a:first-child h3, .doc-page a:first-child h4, .doc-page a:first-child h5, .doc-page a:first-child h6 { 142 | margin-top: 0; 143 | padding-top: 0; 144 | } 145 | 146 | .doc-page h1 p, .doc-page h2 p, .doc-page h3 p, .doc-page h4 p, .doc-page h5 p, .doc-page h6 p { 147 | margin-top: 0; 148 | } 149 | 150 | .doc-page li p.first { 151 | display: inline-block; 152 | } 153 | 154 | .doc-page ul, .doc-page ol { 155 | padding-left: 30px; 156 | } 157 | 158 | .doc-page ul :first-child, .doc-page ol :first-child { 159 | margin-top: 0; 160 | } 161 | 162 | .doc-page ul :last-child, .doc-page ol :last-child { 163 | margin-bottom: 0; 164 | } 165 | 166 | .doc-page dl { 167 | padding: 0; 168 | } 169 | 170 | .doc-page dl dt { 171 | font-size: 14px; 172 | font-weight: bold; 173 | font-style: italic; 174 | padding: 0; 175 | margin: 15px 0 5px; 176 | } 177 | 178 | .doc-page dl dt:first-child { 179 | padding: 0; 180 | } 181 | 182 | .doc-page dl dt> :first-child { 183 | margin-top: 0; 184 | } 185 | 186 | .doc-page dl dt> :last-child { 187 | margin-bottom: 0; 188 | } 189 | 190 | .doc-page dl dd { 191 | margin: 0 0 15px; 192 | padding: 0 15px; 193 | } 194 | 195 | .doc-page dl dd> :first-child { 196 | margin-top: 0; 197 | } 198 | 199 | .doc-page dl dd> :last-child { 200 | margin-bottom: 0; 201 | } 202 | 203 | .doc-page blockquote { 204 | border-left: 4px solid #dddddd; 205 | padding: 0 15px; 206 | color: #777777; 207 | } 208 | 209 | .doc-page blockquote> :first-child { 210 | margin-top: 0; 211 | } 212 | 213 | .doc-page blockquote> :last-child { 214 | margin-bottom: 0; 215 | } 216 | 217 | .doc-page blockquote::before, .doc-page blockquote::after{ 218 | display: none; 219 | } 220 | 221 | .doc-page table { 222 | padding: 0; 223 | } 224 | 225 | .doc-page table tr { 226 | border-top: 1px solid #DDDDDD; 227 | background-color: white; 228 | margin: 0; 229 | padding: 0; 230 | } 231 | 232 | .doc-page table tr:nth-child(2n) { 233 | background-color: #f8f8f8; 234 | } 235 | 236 | .doc-page table tr th { 237 | font-weight: bold; 238 | border: 1px solid #DDDDDD; 239 | text-align: left; 240 | margin: 0; 241 | padding: 6px 13px; 242 | } 243 | 244 | .doc-page table tr td { 245 | border: 1px solid #DDDDDD; 246 | text-align: left; 247 | margin: 0; 248 | padding: 6px 13px; 249 | } 250 | 251 | .doc-page table tr th :first-child, .doc-page table tr td :first-child { 252 | margin-top: 0; 253 | } 254 | 255 | .doc-page table tr th :last-child, .doc-page table tr td :last-child { 256 | margin-bottom: 0; 257 | } 258 | 259 | .doc-page img { 260 | max-width: 100%; 261 | } 262 | 263 | .doc-page span.frame { 264 | display: block; 265 | overflow: hidden; 266 | } 267 | 268 | .doc-page span.frame>span { 269 | border: 1px solid #dddddd; 270 | display: block; 271 | float: left; 272 | overflow: hidden; 273 | margin: 13px 0 0; 274 | padding: 7px; 275 | width: auto; 276 | } 277 | 278 | .doc-page span.frame span img { 279 | display: block; 280 | float: left; 281 | } 282 | 283 | .doc-page span.frame span span { 284 | clear: both; 285 | color: #333333; 286 | display: block; 287 | padding: 5px 0 0; 288 | } 289 | 290 | .doc-page span.align-center { 291 | display: block; 292 | overflow: hidden; 293 | clear: both; 294 | } 295 | 296 | .doc-page span.align-center>span { 297 | display: block; 298 | overflow: hidden; 299 | margin: 13px auto 0; 300 | text-align: center; 301 | } 302 | 303 | .doc-page span.align-center span img { 304 | margin: 0 auto; 305 | text-align: center; 306 | } 307 | 308 | .doc-page span.align-right { 309 | display: block; 310 | overflow: hidden; 311 | clear: both; 312 | } 313 | 314 | .doc-page span.align-right>span { 315 | display: block; 316 | overflow: hidden; 317 | margin: 13px 0 0; 318 | text-align: right; 319 | } 320 | 321 | .doc-page span.align-right span img { 322 | margin: 0; 323 | text-align: right; 324 | } 325 | 326 | .doc-page span.float-left { 327 | display: block; 328 | margin-right: 13px; 329 | overflow: hidden; 330 | float: left; 331 | } 332 | 333 | .doc-page span.float-left span { 334 | margin: 13px 0 0; 335 | } 336 | 337 | .doc-page span.float-right { 338 | display: block; 339 | margin-left: 13px; 340 | overflow: hidden; 341 | float: right; 342 | } 343 | 344 | .doc-page span.float-right>span { 345 | display: block; 346 | overflow: hidden; 347 | margin: 13px auto 0; 348 | text-align: right; 349 | } 350 | 351 | .doc-page code, tt { 352 | margin: 0 2px; 353 | padding: 0 5px; 354 | white-space: nowrap; 355 | border: 1px solid #eaeaea; 356 | background-color: #f8f8f8; 357 | border-radius: 3px; 358 | } 359 | 360 | .doc-page pre code { 361 | margin: 0; 362 | padding: 0; 363 | white-space: pre; 364 | border: none; 365 | background: transparent; 366 | } 367 | 368 | .doc-page pre { 369 | background-color: #f8f8f8; 370 | border: 1px solid #DDDDDD; 371 | font-size: 13px; 372 | line-height: 19px; 373 | overflow: auto; 374 | padding: 6px 10px; 375 | border-radius: 3px; 376 | } 377 | 378 | .doc-page pre code, .doc-page pre tt { 379 | background-color: transparent; 380 | border: none; 381 | } 382 | -------------------------------------------------------------------------------- /ui/core/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * file: ui/core/src/index.js 3 | */ 4 | 5 | import React from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | import Relay from 'react-relay'; 8 | import { 9 | Router, 10 | Route, 11 | IndexRoute, 12 | browserHistory, 13 | applyRouterMiddleware 14 | } from 'react-router' 15 | import useRelay from 'react-router-relay'; 16 | 17 | // App components 18 | import AppLayout from './AppLayout'; 19 | import Login from './login'; 20 | import Users from './users'; 21 | 22 | // Auth singleton 23 | import Auth from 'core/lib/auth'; 24 | 25 | // Import some global CSS (some because not implemented in shared) 26 | import 'material-design-lite/src/shadow/_shadow.scss'; 27 | import 'material-design-lite/src/typography/_typography.scss'; 28 | import 'react-select/dist/react-select.css'; 29 | 30 | 31 | // Just a tmp component for IndexRoute 32 | class Hello extends React.Component { 33 | render() { 34 | return(

Hello Public Section

) 35 | } 36 | } 37 | 38 | // function requireAuth(prevState, nextState, replace) { 39 | function requireAuth(nextState, replace) { 40 | if (!Auth.loggedIn()) { 41 | replace({ 42 | pathname: "/login", 43 | state: { nextPathname: nextState.location.pathname } 44 | }) 45 | } 46 | } 47 | 48 | function verifySession(nextState, replace){ 49 | if(Auth.loggedIn()) 50 | replace({ pathname: "/admin" }) 51 | } 52 | 53 | function redirectToDefaultDoc(nextState, replace) { 54 | replace({ pathname: "/docs/use_case_examples.md" }) 55 | } 56 | 57 | const importError = (err) => { 58 | console.log(err); 59 | } 60 | 61 | /** 62 | TODO: find out why can't call import() from inside a function 63 | */ 64 | const routes = ( 65 | 66 | 67 | { 69 | import('core/login').then(module => { 70 | cb(null, module.default); 71 | }).catch(importError) 72 | }} 73 | onEnter={verifySession} 74 | /> 75 | 76 | 77 | { 80 | import('core/my-graphiql').then(module => { 81 | cb(null, module.default); 82 | }).catch(importError) 83 | }} 84 | /> 85 | { 88 | import('core/users').then(module => { 89 | cb(null, module.default); 90 | }).catch(importError) 91 | }} 92 | queries={Users.ViewerQuery} 93 | /> 94 | { 97 | import('core/users/Edit').then(module => { 98 | cb(null, module.default); 99 | }).catch(importError) 100 | }} 101 | queries={Users.NodeQuery} 102 | /> 103 | 104 | 105 | 106 | { 109 | import('core/docs').then(module => { 110 | cb(null, module.default); 111 | }).catch(importError) 112 | }} 113 | /> 114 | 115 | 116 | ) 117 | 118 | class Application extends React.Component{ 119 | constructor(props){ 120 | super(props); 121 | this.state = {environment: Auth.getEnvironment()}; 122 | Auth.afterLogin = () => this.resetRelayEnvironment(); 123 | Auth.afterLogout = () => this.resetRelayEnvironment(); 124 | } 125 | 126 | resetRelayEnvironment(){ 127 | this.setState({environment: Auth.getEnvironment()}); 128 | } 129 | 130 | render(){ 131 | return ( 132 | 135 | {routes} 136 | 137 | ); 138 | } 139 | } 140 | 141 | ReactDOM.render( 142 | , 143 | document.getElementById('react-root') 144 | ); 145 | -------------------------------------------------------------------------------- /ui/core/src/lib/auth.js: -------------------------------------------------------------------------------- 1 | import Relay from 'react-relay'; 2 | 3 | class Auth { 4 | 5 | constructor() { 6 | this.environment = new Relay.Environment(); 7 | // Inject a new network layer into Relay with NO token 8 | this.environment.injectNetworkLayer(this._buildNetworkLayer()); 9 | } 10 | 11 | getToken() { 12 | return localStorage.accessToken; 13 | } 14 | 15 | login(token) { 16 | // persist token in localStorage 17 | localStorage.accessToken = token; 18 | // "Renew" this.environment each login 19 | this.environment = new Relay.Environment(); 20 | this.environment.injectNetworkLayer(this._buildNetworkLayer()); 21 | if (this.afterLogin) this.afterLogin(); 22 | } 23 | 24 | logout(callback) { 25 | // delete token from localStorage 26 | delete localStorage.accessToken; 27 | // Renew this.environment each logout to override 28 | this.environment = new Relay.Environment(); 29 | this.environment.injectNetworkLayer(this._buildNetworkLayer()); 30 | if (callback) callback(); 31 | if (this.afterLogout) this.afterLogout(); 32 | } 33 | 34 | loggedIn() { 35 | return !!localStorage.accessToken; 36 | } 37 | 38 | getEnvironment(){ 39 | return this.environment; 40 | } 41 | 42 | _buildNetworkLayer(){ 43 | let props = this.loggedIn() ? { 44 | headers: { Authorization: 'Bearer ' + this.getToken() } 45 | } : null; 46 | return new Relay.DefaultNetworkLayer(GRAPHQL_URL, props); 47 | } 48 | } 49 | 50 | // Export singleton 51 | module.exports = new Auth(); 52 | -------------------------------------------------------------------------------- /ui/core/src/lib/mdlUpgrade.js: -------------------------------------------------------------------------------- 1 | // file: lib/mdlUpgrade.js 2 | import { findDOMNode } from 'react-dom'; 3 | import { mdl } from "exports?mdl=componentHandler!material-design-lite/material"; 4 | 5 | export default (WrappedComponent) => { 6 | return class extends WrappedComponent { 7 | componentDidMount() { 8 | if(super.componentDidMount){ 9 | super.componentDidMount(); 10 | } 11 | mdl.upgradeElements(findDOMNode(this)); 12 | } 13 | componentWillUnmount(){ 14 | if(super.componentWillUnmount){ 15 | super.componentWillUnmount(); 16 | } 17 | mdl.downgradeElements(findDOMNode(this)); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ui/core/src/login/index.js: -------------------------------------------------------------------------------- 1 | // file: login/index.js 2 | 3 | import React, { PropTypes } from 'react'; 4 | import Relay from 'react-relay'; 5 | import { withRouter } from 'react-router'; 6 | import Auth from 'core/lib/auth'; 7 | import LoginMutation from './mutation'; 8 | 9 | // Base components 10 | import { mdlUpgrade } from 'react-to-mdl'; 11 | import Spinner from 'react-to-mdl/spinner'; 12 | import Layout from 'react-to-mdl/layout'; 13 | import Textfield from 'react-to-mdl/textfield'; 14 | 15 | // CSS 16 | import CSSModules from 'react-css-modules'; 17 | import styles from './styles.scss'; 18 | 19 | class Login extends React.Component { 20 | static propTypes = { 21 | styles: PropTypes.object 22 | } 23 | 24 | constructor(props) { 25 | super(props); 26 | this.state = { 27 | hasError: false, 28 | isLoading: false 29 | } 30 | } 31 | 32 | handleSubmit(event) { 33 | event.preventDefault(); 34 | this.setState({isLoading: true}); 35 | 36 | this.props.relay.commitUpdate( 37 | new LoginMutation({ 38 | email: this.refs.email.value(), 39 | password: this.refs.password.value() 40 | }), { 41 | onSuccess: response => { 42 | Auth.login(response.login.accessToken); 43 | const { location, router } = this.props; 44 | 45 | if (location.state && location.state.nextPathname) { 46 | router.replace(location.state.nextPathname) 47 | } else { 48 | router.replace('/') 49 | } 50 | }, 51 | onFailure: transaction => { 52 | this.setState({ 53 | hasError: true, 54 | isLoading: false 55 | }); 56 | } 57 | } 58 | ); 59 | } 60 | 61 | render() { 62 | const { styles } = this.props; 63 | const { isLoading, hasError } = this.state; 64 | 65 | return ( 66 | 67 |
68 |
69 |
70 |

Login

71 |
72 |
73 |
74 | { isLoading && (
75 | 76 |
)} 77 | 78 |
79 | { this.state.hasError && ( 80 | 81 | Incorrect email or password 82 | 83 | )} 84 | 89 | 90 | 96 |
97 | 98 |
99 | 102 |
103 |
104 |
105 |
106 |
107 |
108 | ) 109 | } 110 | } 111 | 112 | /*** exports ***/ 113 | export default Relay.createContainer( 114 | withRouter( 115 | mdlUpgrade( 116 | CSSModules(Login, styles) 117 | ) 118 | ),{ 119 | fragments: {} 120 | } 121 | ); 122 | -------------------------------------------------------------------------------- /ui/core/src/login/mutation.js: -------------------------------------------------------------------------------- 1 | import Relay from 'react-relay'; 2 | 3 | export default class extends Relay.Mutation { 4 | getMutation() { 5 | return Relay.QL`mutation {login}`; 6 | } 7 | 8 | getVariables() { 9 | return { 10 | email: this.props.email, 11 | password: this.props.password, 12 | }; 13 | } 14 | 15 | // TODO: Add field to LoginPayload to get errors 16 | getFatQuery() { 17 | return Relay.QL` 18 | fragment on LoginPayload @relay(pattern: true) { 19 | accessToken 20 | } 21 | `; 22 | } 23 | 24 | getConfigs() { 25 | return [{ 26 | type: 'REQUIRED_CHILDREN', 27 | children: [Relay.QL` 28 | fragment on LoginPayload { 29 | accessToken 30 | } 31 | `], 32 | }]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ui/core/src/login/styles.scss: -------------------------------------------------------------------------------- 1 | @import '~material-design-lite/src/layout/layout'; 2 | @import '~material-design-lite/src/card/card'; 3 | @import '~material-design-lite/src/button/button'; 4 | @import '~material-design-lite/src/shadow/shadow'; 5 | @import '~material-design-lite/src/typography/typography'; 6 | @import '~material-design-lite/src/palette/palette'; 7 | 8 | .login-layout { 9 | @extend .mdl-color--grey-100; 10 | align-items: center; 11 | justify-content: center; 12 | } 13 | 14 | .login-content{ 15 | @extend .mdl-layout__content; 16 | padding: 24px; 17 | flex: none; 18 | } 19 | 20 | .card { 21 | @extend .mdl-card; 22 | @extend .mdl-shadow--6dp; 23 | color: blue; 24 | } 25 | 26 | .card-title { 27 | @extend .mdl-card__title; 28 | @extend .mdl-color--primary; 29 | @extend .mdl-color-text--white; 30 | color: yellow; 31 | } 32 | 33 | .login-error { 34 | @extend .mdl-typography--caption-color-contrast; 35 | color: red; 36 | text-align: center; 37 | font-size: 14px; 38 | } 39 | 40 | .login-button { 41 | @extend .mdl-button; 42 | @extend .mdl-button--colored; 43 | } 44 | 45 | .loading { 46 | background-color: rgba(255, 255, 255, 0.7); 47 | width: 100%; 48 | height: 80%; 49 | position: absolute; 50 | z-index: 10; 51 | display: flex; 52 | justify-content: center; 53 | 54 | div { 55 | align-self: center; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ui/core/src/my-graphiql/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import GraphiQL from 'graphiql'; 3 | import fetch from 'isomorphic-fetch'; 4 | import CSSModules from 'react-css-modules'; 5 | import 'graphiql/graphiql.css'; 6 | import styles from './styles.scss'; 7 | import Auth from 'core/lib/auth'; 8 | 9 | class MyGraphiQL extends React.Component { 10 | render(){ 11 | return( 12 |
13 | { 15 | return fetch(GRAPHIQL_URL, { 16 | method: 'post', 17 | headers: { 18 | "Content-Type": "application/json", 19 | "Authorization": `Bearer ${Auth.getToken()}` 20 | }, 21 | body: JSON.stringify(graphQLParams), 22 | }).then(response => response.json()); 23 | } 24 | } /> 25 |
26 | ) 27 | } 28 | } 29 | 30 | export default CSSModules(MyGraphiQL, styles); 31 | -------------------------------------------------------------------------------- /ui/core/src/my-graphiql/styles.scss: -------------------------------------------------------------------------------- 1 | .graphiql-wrapper { 2 | height: 100%; 3 | margin: 0; 4 | width: 100%; 5 | overflow: hidden; 6 | 7 | & > div { 8 | height: 100vh; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ui/core/src/shared/LogoutLink.js: -------------------------------------------------------------------------------- 1 | // file: shared/LogoutLink.js 2 | import React from 'react'; 3 | import Relay from 'react-relay'; 4 | import Auth from 'core/lib/auth'; 5 | import { withRouter } from 'react-router'; 6 | 7 | class LogoutMutation extends Relay.Mutation { 8 | getVariables() { 9 | return null; 10 | } 11 | 12 | getMutation() { 13 | return Relay.QL`mutation { 14 | logout 15 | }`; 16 | } 17 | 18 | // TODO: Add field to LoginPayload to get errors 19 | getFatQuery() { 20 | return Relay.QL` 21 | fragment on LogoutPayload { 22 | loggedOut 23 | } 24 | `; 25 | } 26 | 27 | getConfigs() { 28 | return [{ 29 | type: 'REQUIRED_CHILDREN', 30 | children: [Relay.QL` 31 | fragment on LogoutPayload { 32 | loggedOut 33 | } 34 | `], 35 | }]; 36 | } 37 | } 38 | 39 | 40 | class LogoutLink extends React.Component { 41 | handleClick(event){ 42 | event.preventDefault(); 43 | 44 | this.props.relay.commitUpdate( 45 | new LogoutMutation(), { 46 | onSuccess: response => { 47 | if (response.logout.loggedOut){ 48 | Auth.logout(() => { 49 | this.props.router.replace('/login') 50 | }); 51 | } else { 52 | throw new Error("Could not logout") 53 | } 54 | } 55 | } 56 | ); 57 | } 58 | 59 | render(){ 60 | return this.handleClick(e)}> 61 | Logout 62 | 63 | } 64 | } 65 | 66 | export default Relay.createContainer( 67 | withRouter(LogoutLink), {fragments: {}} 68 | ); 69 | -------------------------------------------------------------------------------- /ui/core/src/shared/NodeQuery.js: -------------------------------------------------------------------------------- 1 | /** 2 | * file: shared/NodeQuery.js 3 | * Fetch an item through its unique identifier 4 | */ 5 | import Relay from 'react-relay'; 6 | 7 | export default { 8 | node: () => Relay.QL` 9 | query { 10 | node(id: $id) 11 | } 12 | ` 13 | }; 14 | -------------------------------------------------------------------------------- /ui/core/src/shared/ViewerQuery.js: -------------------------------------------------------------------------------- 1 | /** 2 | * file: shared/ViewerQuery.js 3 | * 4 | */ 5 | import Relay from 'react-relay'; 6 | 7 | export default { 8 | viewer: () => Relay.QL` 9 | query { 10 | viewer 11 | } 12 | ` 13 | }; 14 | -------------------------------------------------------------------------------- /ui/core/src/users/AddUserMutation.js: -------------------------------------------------------------------------------- 1 | // file: src/users/AddUserMutation.js 2 | 3 | import Relay from 'react-relay'; 4 | 5 | export default class AddUserMutation extends Relay.Mutation { 6 | getMutation() { 7 | return Relay.QL`mutation {addUser}`; 8 | } 9 | 10 | getVariables() { 11 | const { user } = this.props; 12 | 13 | return { ...user }; 14 | } 15 | 16 | getFatQuery() { 17 | return Relay.QL` 18 | fragment on AddUserPayload { 19 | user { 20 | id 21 | } 22 | errors 23 | } 24 | `; 25 | } 26 | 27 | getConfigs() { 28 | return [{ 29 | type: 'REQUIRED_CHILDREN', 30 | children: [ 31 | Relay.QL` 32 | fragment on AddUserPayload { 33 | user { 34 | id 35 | } 36 | errors 37 | } 38 | ` 39 | ] 40 | }]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ui/core/src/users/Edit.js: -------------------------------------------------------------------------------- 1 | // file: users/Edit.js 2 | 3 | import React, { PropTypes } from 'react'; 4 | import Relay from 'react-relay'; 5 | import { withRouter } from 'react-router'; 6 | import EditUserMutation from './EditUserMutation'; 7 | 8 | // Base components 9 | import Form from './Form'; 10 | 11 | // CSS 12 | import CSSModules from 'react-css-modules'; 13 | import { mdlUpgrade } from 'react-to-mdl'; 14 | import styles from './styles.scss'; 15 | import 'react-select/dist/react-select.css'; 16 | 17 | class Edit extends React.Component { 18 | static propTypes = { 19 | user: PropTypes.object, 20 | title: PropTypes.string 21 | } 22 | 23 | static defaultProps = { 24 | user: null, 25 | title: 'Edit User' 26 | } 27 | 28 | constructor(props) { 29 | super(props) 30 | this.changeUser = this.changeUser.bind(this); 31 | this.handleSubmit = this.handleSubmit.bind(this); 32 | this.state = { 33 | user: Object.assign({}, props.node), 34 | hasError: false, 35 | errors: [], 36 | isLoading: false 37 | } 38 | } 39 | 40 | changeUser(attrs) { 41 | const user = Object.assign({}, this.state.user, attrs); 42 | this.setState(Object.assign({}, this.state, {user})); 43 | } 44 | 45 | handleSubmit(event) { 46 | event.preventDefault(); 47 | const user = this.state.user; 48 | delete user["__dataID__"]; 49 | 50 | this.props.relay.commitUpdate( 51 | new EditUserMutation({ 52 | user 53 | }),{ 54 | onSuccess: response => { 55 | this.setState({isLoading: false}); 56 | // TODO: get rid of reload and use router. 57 | // TODO: Redirect to the updated list of users without reloading page and show errors in the form 58 | // Hint: Tabs js is interferring with React in members/index 59 | // window.location.reload(); 60 | }, 61 | onFailure: transaction => { 62 | console.log(transaction.getError().source); 63 | this.setState({ 64 | hasError: true, 65 | isLoading: false 66 | }); 67 | } 68 | } 69 | ); 70 | 71 | this.setState({isLoading: true}); 72 | } 73 | 74 | render() { 75 | const { title } = this.props; 76 | 77 | return ( 78 |
80 | ) 81 | } 82 | } 83 | 84 | export default Relay.createContainer( 85 | withRouter( 86 | mdlUpgrade( 87 | CSSModules(Edit, styles) 88 | ) 89 | ),{ 90 | fragments: { 91 | node: () => Relay.QL` 92 | fragment on Node { 93 | ... on User{ 94 | id 95 | firstName 96 | lastName 97 | role 98 | email 99 | phone 100 | } 101 | } 102 | ` 103 | } 104 | } 105 | ); 106 | -------------------------------------------------------------------------------- /ui/core/src/users/EditUserMutation.js: -------------------------------------------------------------------------------- 1 | // file: src/users/EditUserMutation.js 2 | 3 | import Relay from 'react-relay'; 4 | 5 | export default class EditUserMutation extends Relay.Mutation { 6 | getMutation() { 7 | return Relay.QL`mutation {editUser}`; 8 | } 9 | 10 | getVariables() { 11 | const { user } = this.props; 12 | 13 | return { ...user }; 14 | } 15 | 16 | getFatQuery() { 17 | return Relay.QL` 18 | fragment on EditUserPayload { 19 | user { 20 | id 21 | } 22 | errors 23 | } 24 | `; 25 | } 26 | 27 | getConfigs() { 28 | return [{ 29 | type: 'REQUIRED_CHILDREN', 30 | children: [ 31 | Relay.QL` 32 | fragment on EditUserPayload { 33 | user { 34 | id 35 | } 36 | errors 37 | } 38 | ` 39 | ] 40 | }]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ui/core/src/users/Form.js: -------------------------------------------------------------------------------- 1 | // file: users/Form.js 2 | 3 | import React, { PropTypes } from 'react'; 4 | import Relay from 'react-relay'; 5 | import { withRouter } from 'react-router'; 6 | 7 | // Base components 8 | import Grid, { Cell } from 'react-to-mdl/grid'; 9 | import Layout from 'react-to-mdl/layout'; 10 | import Textfield from 'react-to-mdl/textfield'; 11 | import Select from 'react-select'; 12 | import Button from 'react-to-mdl/button' 13 | import Spinner from 'react-to-mdl/spinner'; 14 | 15 | // CSS 16 | import CSSModules from 'react-css-modules'; 17 | import { mdlUpgrade } from 'react-to-mdl'; 18 | import styles from './styles.scss'; 19 | import 'react-select/dist/react-select.css'; 20 | 21 | class Form extends React.Component { 22 | 23 | render() { 24 | const { title, edit } = this.props; 25 | const { user, isLoading, hasError, errors} = this.props.state; 26 | 27 | return ( 28 |
29 | 30 | 31 |

{title}

32 |
33 |
34 | 35 | 36 | { hasError && ( 37 | 38 | 39 | )} 40 | 41 | 42 | this.props.handleSubmit(e)}> 43 | {isLoading && (
44 | 45 |
)} 46 | 47 | 48 | this.props.changeUser({ 53 | first_name: e.target.value 54 | })} 55 | /> 56 | 57 | 58 | this.props.changeUser({ 63 | last_name: e.target.value 64 | })} 65 | /> 66 | 67 | 68 | 69 | 70 | this.props.changeUser({ 75 | email: e.target.value 76 | })} 77 | /> 78 | 79 | 80 | { /* TODO: Load options values to select from graphql */ } 81 | 74 | 75 | {factions[0].name} 76 | 77 | 78 | 91 | 92 | 93 | 98 | 99 | 100 | 101 | 102 | 103 | {factions.map((faction, index) => ( 104 | 105 |

{faction.name}

106 |
    107 | {faction.ships.edges.map(({node}) => ( 108 |
  1. 109 | ))} 110 |
111 |
112 | ))} 113 |
114 | 115 | ); 116 | } 117 | } // class StarWarsApp 118 | 119 | /*** exports ***/ 120 | // TODO: change harcoded ships(first: 100) and implement pagination 121 | const shipsLimit = 1000 122 | export default Relay.createContainer( 123 | mdlUpgrade(StarWarsFactions), { 124 | fragments: { 125 | factions: () => Relay.QL` 126 | fragment on Faction @relay(plural: true) { 127 | id, 128 | factionId, 129 | name, 130 | ships(first: 100) { 131 | edges { 132 | node { 133 | id 134 | ${Ship.getFragment('ship')} 135 | } 136 | } 137 | } 138 | ${AddShipMutation.getFragment('faction')}, 139 | } 140 | ` 141 | } 142 | } 143 | ); 144 | -------------------------------------------------------------------------------- /ui/star_wars/src/RelayQueryConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * file: star-wars/RelayQueryConfig.js 3 | * 4 | * Exports an object containing queries meant to be used as RelayQueryConfig(aka Relay route) 5 | * 6 | * For more info, see: 7 | * https://facebook.github.io/relay/docs/guides-routes.html 8 | * 9 | * TODO: Find out how to pass params from router, i.e.: 10 | * factions: () => Relay.QL` 11 | * query { factions(names: $factionNames) 12 | * }`, 13 | */ 14 | import Relay from 'react-relay'; 15 | 16 | export default { 17 | factions: () => Relay.QL` 18 | query { 19 | factions( 20 | names: ["Galactic Empire", "Alliance to Restore the Republic"] 21 | ) 22 | } 23 | ` 24 | }; 25 | -------------------------------------------------------------------------------- /ui/star_wars/src/Ship.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Relay from 'react-relay'; 3 | 4 | class StarWarsShip extends React.Component { 5 | render() { 6 | const {ship} = this.props; 7 | return
{ship.name}
; 8 | } 9 | } 10 | 11 | export default Relay.createContainer(StarWarsShip, { 12 | fragments: { 13 | ship: () => Relay.QL` 14 | fragment on Ship { 15 | id, 16 | name 17 | } 18 | `, 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /ui/star_wars/src/babelRelayPlugin.js: -------------------------------------------------------------------------------- 1 | // TODO: Create a dev version of this that use introspectionQuery to get schema and 2 | // another one that use a schema.json generated file to be compiled with the assets. 3 | const babelRelayPlugin = require('babel-relay-plugin'); 4 | 5 | /** 6 | * This should be for production 7 | */ 8 | // const path = require('path'); 9 | // const schema = require(path.join(process.env.APP_HOME, 'apps/star_wars/schema.json')); 10 | 11 | /** 12 | * This should be for development 13 | */ 14 | const introspectionQuery = require('graphql/utilities').introspectionQuery; 15 | const request = require('sync-request'); 16 | const server = process.env.PWR2_STAR_WARS_GRAPHQL_URL; 17 | 18 | // Get the GraphQL schema from the server to be used later by Relay 19 | const response = request('POST', server, { 20 | qs: { 21 | query: introspectionQuery 22 | } 23 | }); 24 | const schema = JSON.parse(response.body.toString('utf-8')); 25 | 26 | // In the end need we need to export this 27 | module.exports = babelRelayPlugin(schema.data, { 28 | abortOnError: true, 29 | }); 30 | -------------------------------------------------------------------------------- /ui/star_wars/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * file: web/static/js/index.js 3 | */ 4 | 5 | import React from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | import Relay from 'react-relay'; 8 | import useRelay from 'react-router-relay'; 9 | import { 10 | Router, 11 | Route, 12 | IndexRoute, 13 | browserHistory, 14 | applyRouterMiddleware 15 | } from 'react-router' 16 | 17 | // App components 18 | import AppLayout from './AppLayout'; 19 | import Factions from './Factions'; 20 | import Queries from './RelayQueryConfig'; 21 | 22 | // Import some global CSS (some because not implemented in shared) 23 | import 'material-design-lite/src/shadow/_shadow.scss'; 24 | import 'material-design-lite/src/radio/_radio.scss'; 25 | import 'material-design-lite/src/typography/_typography.scss'; 26 | import 'react-select/dist/react-select.css'; 27 | 28 | let store = new Relay.Environment(); 29 | store.injectNetworkLayer( 30 | new Relay.DefaultNetworkLayer('/star_wars/graphql') 31 | ); 32 | 33 | const importError = (err) => { 34 | console.log(err); 35 | } 36 | 37 | ReactDOM.render( 38 | 41 | 42 | 43 | { 46 | import('core/my-graphiql').then(module => { 47 | cb(null, module.default); 48 | }).catch(importError) 49 | }} 50 | /> 51 | 52 | , 53 | document.getElementById('react-root') 54 | ); 55 | -------------------------------------------------------------------------------- /ui/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const ExtractTextPlugin = require("extract-text-webpack-plugin"); 4 | 5 | 6 | // 7 | // Define some helper functions 8 | // 9 | const buildBabelLoader = function(entryName) { 10 | return { 11 | test: /\.jsx?$/, 12 | loader: 'babel-loader', 13 | include: path.resolve(__dirname, `${entryName}`), 14 | options: { 15 | plugins: [ 16 | path.resolve(`./${entryName}/src/babelRelayPlugin`), 17 | 'transform-runtime' 18 | ], 19 | presets: ['react', ['es2015', {"modules": false}], 'stage-0'] 20 | } 21 | } 22 | } 23 | 24 | // 25 | // Export final webpack configuration 26 | // 27 | module.exports = { 28 | devtool: "cheap-module-eval-source-map", 29 | context: __dirname, 30 | resolve: { 31 | modules: [ 32 | 'node_modules', 33 | path.resolve(__dirname) 34 | ], 35 | alias: { 36 | core: 'core/src', 37 | star_wars: 'star_wars/src' 38 | } 39 | }, 40 | entry: { 41 | polyfill: 'babel-polyfill', 42 | core: './core/src', 43 | star_wars: './star_wars/src' 44 | }, 45 | output: { 46 | path: path.resolve('../apps/webapp/priv/static'), 47 | publicPath: '/', 48 | filename: 'js/[name].js', 49 | chunkFilename: 'js/[id].chunk.js' 50 | }, 51 | plugins: [ 52 | new webpack.optimize.CommonsChunkPlugin({ 53 | name: 'common', 54 | filename: 'js/common.js', 55 | chunks: ["core", "star_wars"] 56 | }), 57 | new ExtractTextPlugin("css/[name].css") 58 | ], 59 | module: { 60 | rules: [ 61 | buildBabelLoader('core'), 62 | buildBabelLoader('star_wars'), 63 | { 64 | test: /(react-select|graphiql)\.css$/, 65 | loader: ExtractTextPlugin.extract({ 66 | fallbackLoader: "style-loader", 67 | loader: "css-loader?source!resolve-url-loader", 68 | }) 69 | },{ 70 | test: /\.scss$/, 71 | loader: ExtractTextPlugin.extract({ 72 | fallbackLoader: 'style-loader', 73 | loader: [ 74 | 'css-loader?modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]', 75 | 'resolve-url-loader', 76 | 'sass-loader?sourceMap' 77 | ] 78 | }) 79 | } 80 | ] 81 | } 82 | }; 83 | --------------------------------------------------------------------------------