Repository: chernyshof/react-phoenix-users-boilerplate
Branch: master
Commit: 2642c88aadff
Files: 93
Total size: 116.9 KB
Directory structure:
gitextract_rd6bx7e2/
├── .credo.exs
├── .gitignore
├── LICENSE
├── Procfile
├── README.md
├── assets/
│ ├── .babelrc
│ ├── .eslintrc
│ ├── .travis.yml
│ ├── NERD_tree_1
│ ├── app/
│ │ ├── actions/
│ │ │ ├── errors.js
│ │ │ └── session.js
│ │ ├── assets/
│ │ │ └── scss/
│ │ │ ├── components/
│ │ │ │ ├── app.scss
│ │ │ │ └── sign.scss
│ │ │ ├── index.scss
│ │ │ └── variables.scss
│ │ ├── components/
│ │ │ ├── App/
│ │ │ │ └── index.js
│ │ │ ├── NotFound/
│ │ │ │ └── index.js
│ │ │ ├── Sign/
│ │ │ │ ├── Login/
│ │ │ │ │ ├── LoginForm/
│ │ │ │ │ │ └── index.js
│ │ │ │ │ └── index.js
│ │ │ │ └── Signup/
│ │ │ │ ├── SignupForm/
│ │ │ │ │ └── index.js
│ │ │ │ ├── index.js
│ │ │ │ └── index.test.js
│ │ │ ├── Utils/
│ │ │ │ ├── ErrorMessage/
│ │ │ │ │ └── index.js
│ │ │ │ ├── FormInput/
│ │ │ │ │ └── index.js
│ │ │ │ ├── MatchAuthenticated/
│ │ │ │ │ └── index.js
│ │ │ │ └── RedirectAuthenticated/
│ │ │ │ └── index.js
│ │ │ └── index.js
│ │ ├── config/
│ │ │ ├── CustomRedbox.js
│ │ │ └── Root.js
│ │ ├── index.html
│ │ ├── main.js
│ │ ├── reducers/
│ │ │ ├── errors.js
│ │ │ ├── index.js
│ │ │ └── session.js
│ │ ├── sagas/
│ │ │ ├── index.js
│ │ │ └── session.js
│ │ ├── store/
│ │ │ └── index.js
│ │ ├── utils/
│ │ │ ├── api.js
│ │ │ └── socket.js
│ │ └── vendors/
│ │ └── .gitkeep
│ ├── package.json
│ ├── private/
│ │ └── jest/
│ │ ├── componentsMock.js
│ │ ├── fileMock.js
│ │ ├── setupTests.js
│ │ └── shim.js
│ ├── webpack.config.js
│ └── webpack.production.config.js
├── config/
│ ├── config.exs
│ ├── dev.exs
│ ├── prod.exs
│ └── test.exs
├── elixir_buildpack.config
├── lib/
│ ├── boilerplate/
│ │ ├── accounts/
│ │ │ ├── accounts.ex
│ │ │ └── user.ex
│ │ ├── application.ex
│ │ └── repo.ex
│ ├── boilerplate.ex
│ ├── boilerplate_web/
│ │ ├── channels/
│ │ │ └── user_socket.ex
│ │ ├── controllers/
│ │ │ ├── api/
│ │ │ │ ├── session_controller.ex
│ │ │ │ └── user_controller.ex
│ │ │ ├── auth_error_controller.ex
│ │ │ ├── fallback_controller.ex
│ │ │ └── page_controller.ex
│ │ ├── endpoint.ex
│ │ ├── gettext.ex
│ │ ├── guardian.ex
│ │ ├── router.ex
│ │ ├── templates/
│ │ │ └── layout/
│ │ │ └── app.html.eex
│ │ └── views/
│ │ ├── changeset_view.ex
│ │ ├── error_helpers.ex
│ │ ├── error_view.ex
│ │ ├── layout_view.ex
│ │ ├── page_view.ex
│ │ ├── session_view.ex
│ │ └── user_view.ex
│ └── boilerplate_web.ex
├── mix.exs
├── phoenix_static_buildpack.config
├── priv/
│ ├── gettext/
│ │ ├── en/
│ │ │ └── LC_MESSAGES/
│ │ │ └── errors.po
│ │ └── errors.pot
│ └── repo/
│ ├── migrations/
│ │ ├── 20170731093912_create_users.exs
│ │ └── 20170731093913_insert_superuser.exs
│ └── seeds.exs
└── test/
├── boilerplate/
│ └── accounts/
│ └── accounts_test.exs
├── boilerplate_web/
│ ├── controllers/
│ │ ├── page_controller_test.exs
│ │ └── user_controller_test.exs
│ └── views/
│ ├── error_view_test.exs
│ ├── layout_view_test.exs
│ └── page_view_test.exs
├── support/
│ ├── channel_case.ex
│ ├── conn_case.ex
│ └── data_case.ex
└── test_helper.exs
================================================
FILE CONTENTS
================================================
================================================
FILE: .credo.exs
================================================
# This file contains the configuration for Credo and you are probably reading
# this after creating it with `mix credo.gen.config`.
#
# If you find anything wrong or unclear in this file, please report an
# issue on GitHub: https://github.com/rrrene/credo/issues
#
%{
#
# You can have as many configs as you like in the `configs:` field.
configs: [
%{
#
# Run any config using `mix credo -C <name>`. If no config name is given
# "default" is used.
name: "default",
#
# these are the files included in the analysis
files: %{
#
# you can give explicit globs or simply directories
# in the latter case `**/*.{ex,exs}` will be used
included: ["lib/", "src/", "web/", "apps/"],
excluded: [~r"/_build/", ~r"/deps/"]
},
#
# If you create your own checks, you must specify the source files for
# them here, so they can be loaded by Credo before running the analysis.
requires: [],
#
# Credo automatically checks for updates, like e.g. Hex does.
# You can disable this behaviour below:
check_for_updates: true,
#
# You can customize the parameters of any check by adding a second element
# to the tuple.
#
# To disable a check put `false` as second element:
#
# {Credo.Check.Design.DuplicatedCode, false}
#
checks: [
{Credo.Check.Consistency.ExceptionNames},
{Credo.Check.Consistency.LineEndings},
{Credo.Check.Consistency.SpaceAroundOperators},
{Credo.Check.Consistency.SpaceInParentheses},
{Credo.Check.Consistency.TabsOrSpaces},
# For some checks, like AliasUsage, you can only customize the priority
# Priority values are: `low, normal, high, higher`
{Credo.Check.Design.AliasUsage, priority: :low},
# For others you can set parameters
# If you don't want the `setup` and `test` macro calls in ExUnit tests
# or the `schema` macro in Ecto schemas to trigger DuplicatedCode, just
# set the `excluded_macros` parameter to `[:schema, :setup, :test]`.
{Credo.Check.Design.DuplicatedCode, excluded_macros: []},
# You can also customize the exit_status of each check.
# If you don't want TODO comments to cause `mix credo` to fail, just
# set this value to 0 (zero).
{Credo.Check.Design.TagTODO, exit_status: 2},
{Credo.Check.Design.TagFIXME},
{Credo.Check.Readability.FunctionNames},
{Credo.Check.Readability.LargeNumbers},
{Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 80},
{Credo.Check.Readability.ModuleAttributeNames},
{Credo.Check.Readability.ModuleDoc, false},
{Credo.Check.Readability.ModuleNames},
{Credo.Check.Readability.ParenthesesInCondition},
{Credo.Check.Readability.PredicateFunctionNames},
{Credo.Check.Readability.TrailingBlankLine},
{Credo.Check.Readability.TrailingWhiteSpace},
{Credo.Check.Readability.VariableNames},
{Credo.Check.Refactor.ABCSize},
# {Credo.Check.Refactor.CaseTrivialMatches}, # deprecated in 0.4.0
{Credo.Check.Refactor.CondStatements},
{Credo.Check.Refactor.FunctionArity},
{Credo.Check.Refactor.MatchInCondition},
{Credo.Check.Refactor.PipeChainStart, false},
{Credo.Check.Refactor.CyclomaticComplexity},
{Credo.Check.Refactor.NegatedConditionsInUnless},
{Credo.Check.Refactor.NegatedConditionsWithElse},
{Credo.Check.Refactor.Nesting},
{Credo.Check.Refactor.UnlessWithElse},
{Credo.Check.Warning.IExPry},
{Credo.Check.Warning.IoInspect},
{Credo.Check.Warning.NameRedeclarationByAssignment},
{Credo.Check.Warning.NameRedeclarationByCase},
{Credo.Check.Warning.NameRedeclarationByDef},
{Credo.Check.Warning.NameRedeclarationByFn},
{Credo.Check.Warning.OperationOnSameValues},
{Credo.Check.Warning.BoolOperationOnSameValues},
{Credo.Check.Warning.UnusedEnumOperation},
{Credo.Check.Warning.UnusedKeywordOperation},
{Credo.Check.Warning.UnusedListOperation},
{Credo.Check.Warning.UnusedStringOperation},
{Credo.Check.Warning.UnusedTupleOperation},
{Credo.Check.Warning.OperationWithConstantResult},
# Custom checks can be created using `mix credo.gen.check`.
#
]
}
]
}
================================================
FILE: .gitignore
================================================
# App artifacts
/_build
/db
/deps
/*.ez
# Generated on crash by the VM
erl_crash.dump
# Since we are building assets from assets/,
# we ignore priv/static. You may want to comment
# this depending on your deployment strategy.
/priv/static/
# Files matching config/*.secret.exs pattern contain sensitive
# data and you should not commit them into version control.
#
# Alternatively, you may comment the line below and commit the
# secrets files as long as you replace their contents by environment
# variables.
/config/*.secret.exs
# Assets
# Dependency directories
/assets/node_modules
# Ignore test coverage results
/assets/coverage
# Optional npm cache directory
/assets/.npm
/assets/npm-debug.log*
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2017 Dmitriy Chernyshov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Procfile
================================================
web: MIX_ENV=prod mix phx.server
================================================
FILE: README.md
================================================
# React + Phoenix boilerplate
Demo: http://phxboilerplate.herokuapp.com
This is a basic setup for an React(16) + Phoenix(1.3)/Elixir(1.7) project, using webpack(4) and users with authentication.
## STARTING PROJECT
#### [You should have git installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
```
git clone https://github.com/chernyshof/react-phoenix-users-boilerplate appname
cd appname
```
#### Changing app name in files(commands for unix like systems)
```
grep -rl Boilerplate | xargs sed -i s@Boilerplate@Appname@g
grep --exclude={package.json,yarn.lock,.babelrc} -rl boilerplate | xargs sed -i s@boilerplate@appname@g
find . -depth -exec rename 's/boilerplate/appname/g' {} \;
```
Or if you're using different rename version
```
find . -iname "*boilerplate*" -exec rename boilerplate appname '{}' \;
```
#### Reinit git
```
rm -rf .git
git init
git add priv/static/favicon.ico -f
git add priv/static/images/phoenix.png -f
```
#### Download dependencies
```
mix deps.get
mix ecto.create
mix ecto.migrate
cd assets
yarn install
cd ..
```
#### Start server
```
mix phx.server
```
## SUPERUSER
After running `mix ecto.migrate` command you will have superuser:
```
email: admin@admin.com
password: 12345678
```
You probably wanna change it :)
## SETUP
#### Redux logger
If you want to turn redux logger on just assign `true` to useReduxLogger in app/store/index.js
```javascript
...
const useReduxLogger = true;
...
```
## DEPLOYING TO HEROKU
[You should have installed heroku-cli](https://devcenter.heroku.com/articles/heroku-cli)
#### Create heroku application
```
heroku create --buildpack "https://github.com/HashNuke/heroku-buildpack-elixir.git"
```
#### Optional change app address
```
heroku apps:rename appname
```
#### Adding phoenix buildpack
```
heroku buildpacks:add https://github.com/chernyshof/heroku-buildpack-phoenix-static.git
```
#### Add you address
in `config/prod.exs`
change in config, :appname, Appname.Repo, url line(if needed)
```elixir
url: [scheme: "https", host: "appnameaddress.herokuapp.com", port: 443],
```
#### Creating Environment Variables
```
heroku addons:create heroku-postgresql:hobby-dev
heroku config:set POOL_SIZE=18
```
#### Secret key
gen secret key
```
$ mix phx.gen.secret
xvafzY4y01jYuzLm3ecJqo008dVnU3CN4f+MamNd1Zue4pXvfvUjbiXT8akaIF53
```
##### now set key that you got in heroku
```
heroku config:set SECRET_KEY_BASE="xvafzY4y01jYuzLm3ecJqo008dVnU3CN4f+MamNd1Zue4pXvfvUjbiXT8akaIF53"
```
#### Guardian secret key
```
$ mix phx.gen.secret
xvafzY4y01jYuzLm3ecJqo008dVnU3CN4f+MamNd1Zue4pXvfvUjbiXT8akaIF53
```
##### now set key that you got in heroku
```
heroku config:set GUARDIAN_SECRET_KEY="xvafzY4y01jYuzLm3ecJqo008dVnU3CN4f+MamNd1Zue4pXvfvUjbiXT8akaIF53"
```
#### Deploy time!
```
git push heroku master
heroku run "POOL_SIZE=2 mix ecto.migrate"
```
## REQUIREMENTS
- [Elixir](http://elixir-lang.org/)/[Mix](http://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html)/[Phoenix](http://www.phoenixframework.org/) ([Installation guide](http://www.phoenixframework.org/docs/installation), [Phoenix1.3](https://gist.github.com/chrismccord/71ab10d433c98b714b75c886eff17357))
- [Node.js](https://nodejs.org/en/)/[yarn](https://yarnpkg.com/) or [npm](https://www.npmjs.com/)
- A [PostgreSQL](https://www.postgresql.org/) server running on your machine.
- [Watchman](https://facebook.github.io/watchman/) file watching service
## USED PLUGINS AND TECHNOLOGIES
**Frontend**
* [React](https://github.com/facebook/react)
* [React hot reloader](https://github.com/gaearon/react-hot-loader) Tweak React components in real time.
* [Redux logger](https://github.com/evgenyrodionov/redux-logger)
* [React Router 4](https://github.com/ReactTraining/react-router) Declarative routing for React.
* [Babel](http://babeljs.io) For ES6 and ES7 magic.
* [Webpack 4](http://webpack.github.io) For bundling.
* [Webpack Dev Middleware](http://webpack.github.io/docs/webpack-dev-middleware.html)
* [Redux](https://github.com/reactjs/redux) Predictable state container for JavaScript apps.
* [Redux Dev Tools](https://github.com/gaearon/redux-devtools) DevTools for Redux with hot reloading, action replay, and customizable UI. Watch [Dan Abramov's talk](https://www.youtube.com/watch?v=xsSnOQynTHs)
* [Redux Saga](https://github.com/redux-saga/redux-saga) Middleware for Redux - used in async actions.
* [React Router Redux 5](https://github.com/reactjs/react-router-redux) Ruthlessly simple bindings to keep react-router and redux in sync.
* [ESLint](http://eslint.org) And many librarys for this.
* [Jest](https://facebook.github.io/jest/) JavaScript Testing framework.
* [Enzyme](http://airbnb.io/enzyme/) JavaScript Testing utilities for React.
* [Sass](http://sass-lang.com/) Css extenstion language.
* [PostCSS](http://postcss.org/) Tool for transforming css to javascript
* [husky](https://github.com/typicode/husky) Husky can prevent bad commit, push and more.
* [OpenBrowserPlugin](https://github.com/baldore/open-browser-webpack-plugin) Opens a new browser tab when Webpack loads.
and other stuff...
**Backend**
* [Elixir 1.7](http://elixir-lang.org/)
* [Phoenix 1.3](http://www.phoenixframework.org/)
* [Credo](https://github.com/rrrene/credo) Static code analysis tool for the Elixir language.
================================================
FILE: assets/.babelrc
================================================
{
"presets": [
"@babel/preset-env",
"@babel/preset-react",
],
"plugins": [
// Stage 0
"@babel/plugin-proposal-function-bind",
// Stage 1
"@babel/plugin-proposal-export-default-from",
"@babel/plugin-proposal-logical-assignment-operators",
["@babel/plugin-proposal-optional-chaining", { loose: false }],
["@babel/plugin-proposal-pipeline-operator", { proposal: "minimal" }],
["@babel/plugin-proposal-nullish-coalescing-operator", { loose: false }],
"@babel/plugin-proposal-do-expressions",
// Stage 2
["@babel/plugin-proposal-decorators", { legacy: true }],
"@babel/plugin-proposal-function-sent",
"@babel/plugin-proposal-export-namespace-from",
"@babel/plugin-proposal-numeric-separator",
"@babel/plugin-proposal-throw-expressions",
// Stage 3
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-syntax-import-meta",
["@babel/plugin-proposal-class-properties", { loose: false }],
"@babel/plugin-proposal-json-strings",
// -\_(:/)_/-
'@babel/plugin-transform-modules-commonjs',
'@babel/plugin-proposal-object-rest-spread',
]
}
================================================
FILE: assets/.eslintrc
================================================
{
"parser": "babel-eslint",
"plugins": ["react", "import"],
"env": {
"browser": true,
"jest": true,
"node": true,
"mocha": true
},
"extends": ["airbnb"],
"rules": {
"space-before-function-paren": 0,
"react/prefer-stateless-function": 0,
"react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
"react/jsx-one-expression-per-line": 0, // fix
"react/destructuring-assignment": 0,
"jsx-a11y/href-no-hash": "off",
"linebreak-style": 0,
"global-require": 0,
"jsx-a11y/anchor-is-valid": [ "error", {
"components": [ "Link" ],
"specialLink": [ "to", "hrefLeft", "hrefRight" ],
"aspects": [ "noHref", "invalidHref", "preferButton" ]
}],
},
"settings": {
"import/resolver": {
"webpack": {
"config": "webpack.config.js"
}
}
}
}
================================================
FILE: assets/.travis.yml
================================================
sudo: false
language: node_js
cache:
directories:
- node_modules
branches:
only:
- master
- /^greenkeeper/.*$/
notifications:
email: false
node_js:
- 6.1
- 6
- 5.6.0
- 4.3.0
before_install:
- npm i -g npm@^3.8.2
before_script:
- npm prune
script:
- npm run test
install: npm install
================================================
FILE: assets/NERD_tree_1
================================================
@babel/plugin-transform-modules-commonjs @babel/plugin-proposal-export-default-from @babel/plugin-proposal-export-namespace-from @babel/plugin-proposal-class-properties @babel/plugin-proposal-object-rest-spread
================================================
FILE: assets/app/actions/errors.js
================================================
export const types = {
NEW_ERROR: 'ERRORS/NEW_ERROR',
};
export const newError = message => ({ type: types.NEW_ERROR, message });
================================================
FILE: assets/app/actions/session.js
================================================
export const types = {
LOGIN_REQUEST: 'SESSION/LOGIN_REQUEST',
SIGNUP_REQUEST: 'SESSION/SIGNUP_REQUEST',
LOGOUT: 'SESSION/LOGOUT',
AUTHENTICATION_REQUEST: 'SESSION/AUTHENTICATION_REQUEST',
AUTHENTICATION_SUCCESS: 'SESSION/AUTHENTICATION_SUCCESS',
AUTHENTICATION_FAILURE: 'SESSION/AUTHENTICATION_FAILURE',
};
export const login = data => ({ type: types.LOGIN_REQUEST, data });
export const signup = data => ({ type: types.SIGNUP_REQUEST, data });
export const logout = () => ({ type: types.LOGOUT });
export const authenticate = () => ({ type: types.AUTHENTICATION_REQUEST });
export const unauthenticate = () => ({ type: types.AUTHENTICATION_FAILURE });
================================================
FILE: assets/app/assets/scss/components/app.scss
================================================
.app{
padding-top: 50px;
form, ul, table {
margin-top: 20px;
margin-bottom: 20px;
}
.username {
color: $primary_color;
}
/* Phoenix flash messages */
.alert:empty { display: none; }
/* Phoenix inline forms in links and buttons */
form.link, form.button {
display: inline;
}
/* Custom page header */
.header {
border-bottom: 1px solid $grey;
}
.logo {
width: 519px;
height: 71px;
display: inline-block;
margin-bottom: 1em;
background-image: url("/images/phoenix.png");
background-size: 519px 71px;
}
/* Everything but the jumbotron gets side spacing for mobile first views */
.header,
.marketing {
padding-right: 15px;
padding-left: 15px;
}
/* Customize container */
@media (min-width: 768px) {
.container {
max-width: 730px;
}
}
.container-narrow > hr {
margin: 30px 0;
}
/* Main marketing message */
.jumbotron {
text-align: center;
border-bottom: 1px solid $grey;
}
/* Supporting marketing content */
.marketing {
margin: 35px 0;
}
/* Responsive: Portrait tablets and up */
@media screen and (min-width: 768px) {
/* Remove the padding we set earlier */
.header,
.marketing {
padding-right: 0;
padding-left: 0;
}
/* Space out the masthead */
.header {
margin-bottom: 30px;
}
/* Remove the bottom border on the jumbotron for visual effect */
.jumbotron {
border-bottom: 0;
}
}
}
================================================
FILE: assets/app/assets/scss/components/sign.scss
================================================
.login, .signup {
height: 100%;
width: 100%;
background: $grey;
padding-top: 40px;
.input {
position: relative;
.validation-error {
white-space: nowrap;
padding: 0 5px;
padding-bottom: 2px;
top: 5px;
left: 110%;
position: absolute;
background: $error;
color: white;
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
&:before {
content:"";
position: absolute;
right: 100%;
width: 0;
height: 0;
border-top: 13px solid transparent;
border-right: 20px solid $error;
border-bottom: 13px solid transparent;
}
}
}
.form-login, .form-signup {
margin: 0 auto;
max-width: 330px;
padding: 15px;
background: white;
}
.form-login {
input[type="email"] {
margin-bottom: -1px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
input[type="password"] {
border-top-right-radius: 0;
border-top-left-radius: 0;
margin-bottom: 25px;
}
}
.form-signup {
input[name="name"] {
margin-bottom: -1px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
input[name="password"] {
border-top-right-radius: 0;
border-top-left-radius: 0;
margin-bottom: 25px;
}
input[type="email"],
input[name="username"] {
margin-bottom: -1px;
border-radius: 0;
border-radius: 0;
}
}
}
================================================
FILE: assets/app/assets/scss/index.scss
================================================
@import 'variables.scss';
@import 'components/sign.scss';
@import 'components/app.scss';
body, html, #root, .full-height {
margin: 0;
height: 100%;
width: 100%;
font-family: sans-serif;
}
================================================
FILE: assets/app/assets/scss/variables.scss
================================================
$grey: #EEEEEE;
$error: #e53935;
$primary_color: #F05423;
================================================
FILE: assets/app/components/App/index.js
================================================
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import DocumentTitle from 'react-document-title';
import { logout } from 'actions/session';
export class App extends Component {
render() {
const { username } = this.props;
return (
<DocumentTitle title="Home">
<div className="app">
<div className="container col-md-8">
<header className="header">
<nav>
<span className="logo" />
<ul className="nav nav-pills pull-right">
<li><a href="http://www.phoenixframework.org/docs">Get Started</a></li>
<button type="button" className="btn btn-sm" onClick={this.props.logout}>Logout</button>
</ul>
</nav>
</header>
<main role="main">
<div className="jumbotron">
<h2 id="heading">
Hello, <span className="username">{ username }</span>!
</h2>
<h2>Welcome to Phoenix</h2>
<p className="lead">A productive web framework that<br />does not compromise speed and maintainability.</p>
</div>
<div className="row marketing">
<div className="col-lg-6">
<h4>Resources</h4>
<ul>
<li>
<a href="http://phoenixframework.org/docs/overview">Guides</a>
</li>
<li>
<a href="https://hexdocs.pm/phoenix">Docs</a>
</li>
<li>
<a href="https://github.com/phoenixframework/phoenix">Source</a>
</li>
</ul>
</div>
<div className="col-lg-6">
<h4>Help</h4>
<ul>
<li>
<a href="http://groups.google.com/group/phoenix-talk">Mailing list</a>
</li>
<li>
<a href="http://webchat.freenode.net/?channels=elixir-lang">#elixir-lang on freenode IRC</a>
</li>
<li>
<a href="https://twitter.com/elixirphoenix">@elixirphoenix</a>
</li>
</ul>
</div>
</div>
</main>
</div>
</div>
</DocumentTitle>
);
}
}
App.propTypes = {
username: PropTypes.string,
logout: PropTypes.func.isRequired,
};
App.defaultProps = {
username: '',
};
const mapDispatchToProps = dispatch => ({
dispatch,
logout: () => dispatch(logout()),
});
const mapStateProps = state => ({
username: state.session.currentUser.username,
});
export default connect(mapStateProps, mapDispatchToProps)(App);
================================================
FILE: assets/app/components/NotFound/index.js
================================================
import React, { Component } from 'react';
export default class NotFound extends Component {
render() {
return (
<h2 id="heading">404 Not Found</h2>
);
}
}
================================================
FILE: assets/app/components/Sign/Login/LoginForm/index.js
================================================
import React, { Component } from 'react';
import { Field, reduxForm } from 'redux-form';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import { FormInput } from 'components';
import { login } from 'actions/session';
class LoginForm extends Component {
submit = (data, dispatch) => dispatch(login(data));
render() {
const { handleSubmit, submittingForm, invalid } = this.props;
return (
<form className="form-login card" onSubmit={handleSubmit(this.submit)} noValidate>
<h3>Login to Boilerplate</h3>
<Field
name="email"
type="email"
component={FormInput}
placeholder="Email"
/>
<Field
name="password"
type="password"
component={FormInput}
placeholder="Password"
/>
<button
type="submit"
disabled={invalid || submittingForm}
className="btn btn-primary btn-lg btn-block"
>
{submittingForm ? 'Logging in...' : 'Login'}
</button>
<hr />
<Link to="/signup" className="btn">
Create a new account
</Link>
</form>
);
}
}
LoginForm.propTypes = {
submittingForm: PropTypes.bool,
invalid: PropTypes.bool.isRequired,
handleSubmit: PropTypes.func.isRequired,
};
LoginForm.defaultProps = {
submittingForm: false,
};
const validate = (values) => {
const errors = {};
const emailRegex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
if (!values.email) {
errors.email = 'Required';
} else if (!emailRegex.test(values.email)) {
errors.email = 'Invalid email';
}
if (!values.password) {
errors.password = 'Required';
} else if (values.password.length < 6 || values.password.length > 100) {
errors.password = 'Must be more than 5 characters and less than 101';
}
return errors;
};
export default reduxForm({
form: 'login',
validate,
})(LoginForm);
================================================
FILE: assets/app/components/Sign/Login/index.js
================================================
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import DocumentTitle from 'react-document-title';
import { LoginForm } from 'components';
export class Login extends Component {
render() {
const { submittingForm } = this.props;
return (
<DocumentTitle title="Login">
<div className="login">
<div className="container">
<LoginForm submittingForm={submittingForm} />
</div>
</div>
</DocumentTitle>
);
}
}
Login.propTypes = {
submittingForm: PropTypes.bool,
};
Login.defaultProps = {
submittingForm: false,
};
const mapStateProps = state => ({
submittingForm: state.session.submittingForm,
});
export default connect(mapStateProps)(Login);
================================================
FILE: assets/app/components/Sign/Signup/SignupForm/index.js
================================================
import React, { Component } from 'react';
import { Field, reduxForm } from 'redux-form';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import { FormInput } from 'components';
import { signup } from 'actions/session';
class SignupForm extends Component {
submit = (data, dispatch) => dispatch(signup(data));
render() {
const { handleSubmit, submittingForm, invalid } = this.props;
return (
<form className="form-signup card" onSubmit={handleSubmit(this.submit)} noValidate>
<h3>Create an account</h3>
<Field
name="name"
type="text"
component={FormInput}
placeholder="Full name"
className="form-control"
/>
<Field
name="username"
type="text"
component={FormInput}
placeholder="Username"
className="form-control"
/>
<Field
name="email"
type="email"
component={FormInput}
placeholder="Email"
className="form-control"
/>
<Field
name="password"
type="password"
component={FormInput}
placeholder="Password"
className="form-control"
/>
<button
type="submit"
disabled={invalid || submittingForm}
className="btn btn-primary btn-lg btn-block"
>
{submittingForm ? 'Submitting...' : 'Sign up'}
</button>
<hr />
<Link to="/login" className="btn">
Login to your account
</Link>
</form>
);
}
}
SignupForm.propTypes = {
submittingForm: PropTypes.bool,
invalid: PropTypes.bool.isRequired,
handleSubmit: PropTypes.func.isRequired,
};
SignupForm.defaultProps = {
submittingForm: false,
};
const validate = (values) => {
const errors = {};
const emailRegex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
const usernameRegex = /^[A-Za-z0-9._]+$/;
if (!values.name) {
errors.name = 'Required';
} else if (values.name.length < 1 || values.name.length > 255) {
errors.name = 'Must be less than 256 characters';
}
if (!values.username) {
errors.username = 'Required';
} else if (!usernameRegex.test(values.username)) {
errors.username = 'Only EN letters, digits, \'.\' and \'_\' are allowed';
} else if (values.username.length < 1 || values.username.length > 26) {
errors.username = 'Must be less than 27 characters';
}
if (!values.email) {
errors.email = 'Required';
} else if (!emailRegex.test(values.email)) {
errors.email = 'Invalid email';
}
if (!values.password) {
errors.password = 'Required';
} else if (values.password.length < 6 || values.password.length > 100) {
errors.password = 'Must be more than 5 characters and less than 101';
}
return errors;
};
export default reduxForm({
form: 'signup',
validate,
})(SignupForm);
================================================
FILE: assets/app/components/Sign/Signup/index.js
================================================
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import DocumentTitle from 'react-document-title';
import { SignupForm } from 'components';
export class Signup extends Component {
render() {
const { submittingForm } = this.props;
return (
<DocumentTitle title="Signup">
<div className="signup">
<div className="container">
<SignupForm submittingForm={submittingForm} />
</div>
</div>
</DocumentTitle>
);
}
}
Signup.propTypes = {
submittingForm: PropTypes.bool,
};
Signup.defaultProps = {
submittingForm: false,
};
const mapStateProps = state => ({
submittingForm: state.session.submittingForm,
});
export default connect(mapStateProps)(Signup);
================================================
FILE: assets/app/components/Sign/Signup/index.test.js
================================================
import React from 'react';
import { shallow } from 'enzyme';
import { Signup } from '.';
describe('Signup', () => {
it('shoud exists', () => {
const wrapper = shallow(<Signup />);
expect(wrapper.exists()).toBe(true);
});
});
================================================
FILE: assets/app/components/Utils/ErrorMessage/index.js
================================================
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import NotificationSystem from 'react-notification-system';
export class ErrorMessage extends Component {
constructor(props) {
super(props);
this.notificationSystem = null;
}
componentDidUpdate(prevProps) {
const { errors } = prevProps;
for (let i = 0; i < this.props.errors.length; i += 1) {
if (errors[i] !== this.props.errors[i] && this.props.errors[i]) {
this.addNotification(this.props.errors[i]);
}
}
}
addNotification(message) {
this.notificationSystem.addNotification({
message,
level: 'error',
});
}
render() {
return (
<div className="errorMessage">
<NotificationSystem ref={(n) => {
this.notificationSystem = n;
return n;
}}
/>
</div>
);
}
}
ErrorMessage.propTypes = {
errors: PropTypes.array, // eslint-disable-line react/forbid-prop-types
};
ErrorMessage.defaultProps = {
errors: [],
};
const mapStateProps = state => ({
errors: state.errors.errors,
});
export default connect(mapStateProps)(ErrorMessage);
================================================
FILE: assets/app/components/Utils/FormInput/index.js
================================================
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export default class Input extends Component {
render() {
const {
input,
type,
placeholder,
meta,
} = this.props;
return (
<div className="input">
<input
{...input}
type={type}
placeholder={placeholder}
className="form-control"
/>
{meta.touched
&& meta.error
&& <div className="validation-error">{meta.error}</div>
}
</div>
);
}
}
Input.propTypes = {
input: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
label: PropTypes.string,
type: PropTypes.string,
placeholder: PropTypes.string,
meta: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
};
Input.defaultProps = {
label: '',
type: '',
placeholder: '',
};
================================================
FILE: assets/app/components/Utils/MatchAuthenticated/index.js
================================================
import React, { Component } from 'react';
import { Route, Redirect } from 'react-router';
import PropTypes from 'prop-types';
export default class MatchAuthenticated extends Component {
render() {
const {
path,
exact,
isAuthenticated,
willAuthenticate,
component,
} = this.props;
const RouteComponent = component;
return (
<Route
exact={exact}
path={path}
render={(props) => {
if (isAuthenticated) { return <RouteComponent {...props} />; }
if (willAuthenticate) { return null; }
if (!willAuthenticate && !isAuthenticated) { return <Redirect to={{ pathname: '/login' }} />; }
return null;
}}
/>
);
}
}
MatchAuthenticated.propTypes = {
component: PropTypes.any.isRequired, // eslint-disable-line react/forbid-prop-types
path: PropTypes.string.isRequired,
exact: PropTypes.bool,
isAuthenticated: PropTypes.bool.isRequired,
willAuthenticate: PropTypes.bool.isRequired,
};
MatchAuthenticated.defaultProps = {
exact: false,
};
================================================
FILE: assets/app/components/Utils/RedirectAuthenticated/index.js
================================================
import React, { Component } from 'react';
import { Route, Redirect } from 'react-router';
import PropTypes from 'prop-types';
export default class RedirectAuthenticated extends Component {
render() {
const {
path,
exact,
isAuthenticated,
willAuthenticate,
component,
} = this.props;
const RouteComponent = component;
return (
<Route
exact={exact}
path={path}
render={(props) => {
if (isAuthenticated) { return <Redirect to={{ pathname: '/' }} />; }
if (willAuthenticate) { return null; }
return <RouteComponent {...props} />;
}}
/>
);
}
}
RedirectAuthenticated.propTypes = {
component: PropTypes.any.isRequired, // eslint-disable-line react/forbid-prop-types
path: PropTypes.string.isRequired,
exact: PropTypes.bool,
isAuthenticated: PropTypes.bool.isRequired,
willAuthenticate: PropTypes.bool.isRequired,
};
RedirectAuthenticated.defaultProps = {
exact: true,
};
================================================
FILE: assets/app/components/index.js
================================================
const req = require.context('.', true, /[^/]+\/[^/]+\/index\.js$/);
req.keys().forEach((key) => {
const componentName = key.replace(/^.+\/([^/]+)\/index\.js/, '$1');
module.exports[componentName] = req(key).default;
});
================================================
FILE: assets/app/config/CustomRedbox.js
================================================
import React from 'react';
import PropTypes from 'prop-types';
import Redbox from 'redbox-react';
const styles = {
redbox: {
boxSizing: 'border-box',
fontFamily: 'sans-serif',
position: 'fixed',
padding: 10,
top: '0px',
left: '0px',
bottom: '0px',
right: '0px',
width: '100%',
background: 'rgb(92,107,192)',
color: 'white',
zIndex: 2147483647,
textAlign: 'left',
fontSize: '16px',
lineHeight: 1.2,
overflow: 'auto',
},
message: {
fontWeight: 'bold',
},
stack: {
fontFamily: 'monospace',
marginTop: '2em',
},
frame: {
marginTop: '1em',
},
file: {
fontSize: '0.8em',
color: 'rgba(255, 255, 255, 0.7)',
},
linkToFile: {
textDecoration: 'none',
color: 'rgba(255, 255, 255, 0.7)',
},
};
export default class CustomRedbox extends React.Component {
render() {
return (
<Redbox
error={this.props.error}
style={styles}
/>);
}
}
CustomRedbox.propTypes = {
error: PropTypes.string.isRequired,
};
================================================
FILE: assets/app/config/Root.js
================================================
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Route, Switch } from 'react-router';
import { ConnectedRouter as Router } from 'react-router-redux';
import PropTypes from 'prop-types';
import { authenticate, unauthenticate } from 'actions/session';
import {
App,
ErrorMessage,
NotFound,
Signup,
Login,
MatchAuthenticated,
RedirectAuthenticated,
} from 'components';
class Root extends Component {
componentDidMount() {
const token = localStorage.getItem('token');
if (token) {
this.props.authenticate();
} else {
this.props.unauthenticate();
}
}
render() {
const { isAuthenticated, willAuthenticate } = this.props;
const authProps = {
isAuthenticated,
willAuthenticate,
};
return (
<div className="full-height">
<ErrorMessage />
<Router history={this.props.history}>
<Switch>
<MatchAuthenticated exact path="/" component={App} {...authProps} />
<RedirectAuthenticated exact path="/signup" component={Signup} {...authProps} />
<RedirectAuthenticated exact path="/login" component={Login} {...authProps} />
<Route component={NotFound} />
</Switch>
</Router>
</div>
);
}
}
Root.propTypes = {
history: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
authenticate: PropTypes.func.isRequired,
unauthenticate: PropTypes.func.isRequired,
isAuthenticated: PropTypes.bool.isRequired,
willAuthenticate: PropTypes.bool.isRequired,
};
const mapDispatchToProps = dispatch => ({
dispatch,
authenticate: () => dispatch(authenticate()),
unauthenticate: () => dispatch(unauthenticate()),
});
const mapStateProps = state => ({
isAuthenticated: state.session.isAuthenticated,
willAuthenticate: state.session.willAuthenticate,
});
export default connect(mapStateProps, mapDispatchToProps)(Root);
================================================
FILE: assets/app/index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ReactJS Boilerplate</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
================================================
FILE: assets/app/main.js
================================================
import React from 'react';
import ReactDOM from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import { Provider } from 'react-redux';
import createHistory from 'history/createBrowserHistory';
import configureStore, { sagaMiddleware } from 'store';
import Root from 'config/Root';
import CustomRedbox from 'config/CustomRedbox';
import Sagas from 'sagas';
import 'bootstrap/dist/css/bootstrap.css';
import 'font-awesome/css/font-awesome.css';
const history = createHistory();
const store = configureStore(history);
sagaMiddleware.run(Sagas);
const render = (Component) => {
ReactDOM.render(
<AppContainer errorReporter={CustomRedbox}>
<Provider store={store}>
<Component history={history} />
</Provider>
</AppContainer>,
document.getElementById('root'),
);
};
render(Root);
if (module.hot) {
module.hot.accept('./config/Root', () => {
const newApp = require('./config/Root').default;
render(newApp);
});
}
================================================
FILE: assets/app/reducers/errors.js
================================================
import { types as errorTypes } from 'actions/errors';
const initialState = {
errors: [],
};
export default function (state = initialState, action) {
switch (action.type) {
case errorTypes.NEW_ERROR:
return {
...state,
errors: [...state.errors, action.message],
};
default:
return state;
}
}
================================================
FILE: assets/app/reducers/index.js
================================================
import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
import { reducer as form } from 'redux-form';
import session from 'reducers/session';
import errors from 'reducers/errors';
import { types as sessionTypes } from 'actions/session';
const appReducer = combineReducers({
form,
session,
errors,
routing: routerReducer,
});
export default function (state, action) {
if (action.type === sessionTypes.LOGOUT) {
return appReducer(undefined, action);
}
return appReducer(state, action);
}
================================================
FILE: assets/app/reducers/session.js
================================================
import { types as sessionTypes } from 'actions/session';
const initialState = {
isAuthenticated: false,
willAuthenticate: true,
submittingForm: false,
currentUser: {},
};
export default function (state = initialState, action) {
switch (action.type) {
case sessionTypes.LOGIN_REQUEST:
return {
...state,
submittingForm: true,
};
case sessionTypes.SIGNUP_REQUEST:
return {
...state,
submittingForm: true,
};
case sessionTypes.AUTHENTICATION_SUCCESS:
return {
...state,
isAuthenticated: true,
willAuthenticate: false,
submittingForm: false,
currentUser: action.response.data,
};
case sessionTypes.AUTHENTICATION_FAILURE:
return {
...state,
willAuthenticate: false,
submittingForm: false,
};
case sessionTypes.LOGOUT:
return {
...state,
isAuthenticated: false,
willAuthenticate: false,
submittingForm: false,
currentUser: {},
};
default:
return state;
}
}
================================================
FILE: assets/app/sagas/index.js
================================================
import { fork } from 'redux-saga/effects';
import Session from 'sagas/session';
const sagas = [
...Session,
];
export default function* root() {
yield sagas.map(saga => fork(saga));
}
================================================
FILE: assets/app/sagas/session.js
================================================
import { takeEvery } from 'redux-saga';
import { call, put } from 'redux-saga/effects';
import { push } from 'react-router-redux';
import { reset } from 'redux-form';
import api from 'utils/api';
import { types as sessionTypes } from 'actions/session';
import { types as errorTypes } from 'actions/errors';
function setCurrentUser(response) {
localStorage.setItem('token', JSON.stringify(response.meta.token));
}
// Login
function login(data) {
return api.post('/sessions', data);
}
function* callLogin({ data }) {
const result = yield call(login, data);
if (result.data.data) {
yield put({ type: sessionTypes.AUTHENTICATION_SUCCESS, response: result.data });
setCurrentUser(result.data);
yield put(reset('signup'));
} else {
yield put({ type: sessionTypes.AUTHENTICATION_FAILURE });
yield put({ type: errorTypes.NEW_ERROR, message: result.data.errors });
localStorage.removeItem('token');
}
}
function* loginSaga() {
yield* takeEvery(sessionTypes.LOGIN_REQUEST, callLogin);
}
// Signup
function signup(data) {
return api.post('/users', data);
}
function* callSignup({ data }) {
const result = yield call(signup, data);
if (result.data.data) {
yield put({ type: sessionTypes.AUTHENTICATION_SUCCESS, response: result.data });
setCurrentUser(result.data);
yield put(reset('signup'));
yield put(push('/'));
} else {
yield put({ type: sessionTypes.AUTHENTICATION_FAILURE });
yield put({ type: errorTypes.NEW_ERROR, message: result.data.errors });
localStorage.removeItem('token');
yield put(push('/login'));
}
}
function* signupSaga() {
yield* takeEvery(sessionTypes.SIGNUP_REQUEST, callSignup);
}
// Logout
function logout() {
return api.delete('/sessions');
}
function* callLogout() {
yield call(logout);
localStorage.removeItem('token');
}
function* logoutSaga() {
yield* takeEvery(sessionTypes.LOGOUT, callLogout);
}
// Authenticate
function authenticate() {
return api.post('/sessions/refresh');
}
function* callAuthenticate() {
const result = yield call(authenticate);
if (result.data.data) {
yield put({ type: sessionTypes.AUTHENTICATION_SUCCESS, response: result.data });
setCurrentUser(result.data);
} else {
yield put({ type: sessionTypes.AUTHENTICATION_FAILURE });
yield put({ type: errorTypes.NEW_ERROR, message: result.data.errors });
localStorage.removeItem('token');
window.location = '/login';
}
}
function* authenticateSaga() {
yield* takeEvery(sessionTypes.AUTHENTICATION_REQUEST, callAuthenticate);
}
export default [loginSaga, signupSaga, logoutSaga, authenticateSaga];
================================================
FILE: assets/app/store/index.js
================================================
import '@babel/polyfill';
import { createStore, applyMiddleware, compose } from 'redux';
import { createLogger } from 'redux-logger';
import createSagaMiddleware from 'redux-saga';
import { routerMiddleware } from 'react-router-redux';
import reducers from 'reducers';
const loggerMiddleware = createLogger({
level: 'info',
collapsed: true,
});
const useReduxLogger = false;
export const sagaMiddleware = createSagaMiddleware();
/* eslint-disable no-underscore-dangle */
export default function configureStore(browserHistory) {
const router = routerMiddleware(browserHistory);
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
// const reduxRouterMiddleware = syncHistoryWithStore(browserHistory);
let middleware = [sagaMiddleware, router];
if (useReduxLogger && process.env.NODE_ENV !== 'production') {
middleware = [...middleware, loggerMiddleware];
}
const createStoreWithMiddleware = composeEnhancers(applyMiddleware(...middleware))(createStore);
return createStoreWithMiddleware(reducers);
}
/* eslint-enable */
================================================
FILE: assets/app/utils/api.js
================================================
import axios from 'axios';
const API = '/api';
function headers() {
const token = JSON.parse(localStorage.getItem('token'));
return {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer: ${token}`,
};
}
function queryString(params) {
const query = Object.keys(params)
.map(k => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
.join('&');
return `${query.length ? '?' : ''}${query}`;
}
export default {
fetch(url, params = {}) {
return axios.get(`${API}${url}${queryString(params)}`, { headers: headers() });
},
post(url, data) {
return axios.post(`${API}${url}`, data, { headers: headers() })
.catch(error => error.response);
},
patch(url, data) {
return axios.patch(`${API}${url}`, data, { headers: headers() });
},
delete(url) {
return axios.delete(`${API}${url}`, { headers: headers() });
},
};
================================================
FILE: assets/app/utils/socket.js
================================================
/* eslint-disable */
// NOTE: The contents of this file will only be executed if
// you uncomment its entry in "assets/js/app.js".
// To use Phoenix channels, the first step is to import Socket
// and connect at the socket path in "lib/web/endpoint.ex":
import { Socket } from 'phoenix';
let socket = new Socket('/socket', { params: { token: window.userToken } });
// When you connect, you'll often need to authenticate the client.
// For example, imagine you have an authentication plug, `MyAuth`,
// which authenticates the session and assigns a `:current_user`.
// If the current user exists you can assign the user's token in
// the connection for use in the layout.
//
// In your "lib/web/router.ex":
//
// pipeline :browser do
// ...
// plug MyAuth
// plug :put_user_token
// end
//
// defp put_user_token(conn, _) do
// if current_user = conn.assigns[:current_user] do
// token = Phoenix.Token.sign(conn, "user socket", current_user.id)
// assign(conn, :user_token, token)
// else
// conn
// end
// end
//
// Now you need to pass this token to JavaScript. You can do so
// inside a script tag in "lib/web/templates/layout/app.html.eex":
//
// <script>window.userToken = "<%= assigns[:user_token] %>";</script>
//
// You will need to verify the user token in the "connect/2" function
// in "lib/web/channels/user_socket.ex":
//
// def connect(%{"token" => token}, socket) do
// # max_age: 1209600 is equivalent to two weeks in seconds
// case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do
// {:ok, user_id} ->
// {:ok, assign(socket, :user, user_id)}
// {:error, reason} ->
// :error
// end
// end
//
// Finally, pass the token on connect as below. Or remove it
// from connect if you don't care about authentication.
socket.connect();
// Now that you are connected, you can join channels with a topic:
let channel = socket.channel('topic:subtopic', {});
channel.join()
.receive('ok', (resp) => { console.log('Joined successfully', resp);})
.receive('error', (resp) => { console.log('Unable to join', resp);})
export default socket;
/* eslint-enable */
================================================
FILE: assets/app/vendors/.gitkeep
================================================
================================================
FILE: assets/package.json
================================================
{
"name": "react-webpack-boilerplate",
"version": "0.0.1",
"description": "Minimalistic ES6 React boilerplate",
"main": "index.js",
"scripts": {
"prestart": "npm run production",
"start": "webpack-dev-server --colors --stdin",
"production": "webpack --config webpack.production.config.js --progress --profile --colors",
"lint": "eslint app",
"lintf": "eslint app --fix",
"test": "jest",
"precommit": "cross-env npm run lint && npm test && cd .. && mix test",
"prepush": "cross-env npm run lint && npm test && cd .. && mix test",
"watch": "webpack --watch --color"
},
"author": "Dmitriy Chernyshov <dmitriy.chernyshof@gmail.com>",
"license": "MIT",
"jest": {
"modulePaths": [
"app"
],
"moduleDirectories": [
"node_modules"
],
"moduleNameMapper": {
"^.+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/private/jest/fileMock.js",
"^components$": "<rootDir>/private/jest/componentsMock.js"
},
"setupFiles": [
"<rootDir>/private/jest/shim.js",
"<rootDir>/private/jest/setupTests.js"
]
},
"dependencies": {
"@babel/polyfill": "^7.0.0",
"axios": "^0.18.0",
"bootstrap": "^4.1.3",
"cross-env": "^5.2.0",
"font-awesome": "^4.7.0",
"history": "^4.7.2",
"jquery": "^3.3.1",
"lodash": "^4.17.11",
"md5": "^2.2.1",
"moment": "^2.22.2",
"phoenix": "^1.3.4",
"popper.js": "^1.14.4",
"prop-types": "^15.6.2",
"react": "^16.6.0",
"react-document-title": "^2.0.3",
"react-dom": "^16.6.0",
"react-hot-loader": "^4.3.11",
"react-notification-system": "^0.2.17",
"react-redux": "^5.0.7",
"react-router": "^4.3.1",
"react-router-dom": "^4.3.1",
"react-router-redux": "^5.0.0-alpha.9",
"redbox-react": "^1.6.0",
"redux": "^4.0.1",
"redux-form": "^7.4.2",
"redux-logger": "^3.0.6",
"redux-saga": "^0.16.2",
"tether": "^1.4.5"
},
"devDependencies": {
"@babel/cli": "^7.1.2",
"@babel/core": "^7.1.2",
"@babel/plugin-proposal-class-properties": "^7.1.0",
"@babel/plugin-proposal-decorators": "^7.1.2",
"@babel/plugin-proposal-do-expressions": "^7.0.0",
"@babel/plugin-proposal-export-default-from": "^7.0.0",
"@babel/plugin-proposal-export-namespace-from": "^7.0.0",
"@babel/plugin-proposal-function-bind": "^7.0.0",
"@babel/plugin-proposal-function-sent": "^7.1.0",
"@babel/plugin-proposal-json-strings": "^7.0.0",
"@babel/plugin-proposal-logical-assignment-operators": "^7.0.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.0.0",
"@babel/plugin-proposal-numeric-separator": "^7.0.0",
"@babel/plugin-proposal-object-rest-spread": "^7.0.0",
"@babel/plugin-proposal-optional-chaining": "^7.0.0",
"@babel/plugin-proposal-pipeline-operator": "^7.0.0",
"@babel/plugin-proposal-throw-expressions": "^7.0.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-syntax-import-meta": "^7.0.0",
"@babel/plugin-transform-modules-commonjs": "^7.1.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-react": "^7.0.0",
"@babel/preset-stage-0": "^7.0.0",
"@babel/preset-stage-1": "^7.0.0",
"@babel/preset-stage-2": "^7.0.0",
"@babel/preset-stage-3": "^7.0.0",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "^10.0.1",
"babel-jest": "^23.6.0",
"babel-loader": "^8.0.4",
"copy-webpack-plugin": "^4.5.4",
"css-hot-loader": "^1.4.2",
"css-loader": "^1.0.0",
"enzyme": "^3.7.0",
"enzyme-adapter-react-16": "^1.6.0",
"eslint": "^5.7.0",
"eslint-config-airbnb": "^17.1.0",
"eslint-import-resolver-webpack": "^0.10.1",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-jsx-a11y": "^6.1.2",
"eslint-plugin-react": "^7.11.1",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"file-loader": "^2.0.0",
"html-webpack-plugin": "^3.2.0",
"husky": "^1.1.2",
"jest": "^23.6.0",
"node-sass": "^4.9.4",
"open-browser-webpack-plugin": "^0.0.5",
"postcss-loader": "^3.0.0",
"react-test-renderer": "^16.6.0",
"sass-loader": "^7.1.0",
"style-loader": "^0.23.1",
"uglifyjs-webpack-plugin": "^2.0.1",
"url-loader": "^1.1.2",
"webpack": "^4.23.1",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.10"
}
}
================================================
FILE: assets/private/jest/componentsMock.js
================================================
import React from 'react'
import PropTypes from 'prop-types'
module.exports = new Proxy({}, {
get: (target, property) => {
const Mock = props => <span>{props.children}</span>
Mock.displayName = property
Mock.propTypes = {
children: PropTypes.any,
}
return Mock
},
})
================================================
FILE: assets/private/jest/fileMock.js
================================================
export default 'file'
================================================
FILE: assets/private/jest/setupTests.js
================================================
import { configure } from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'
configure({ adapter: new Adapter() })
================================================
FILE: assets/private/jest/shim.js
================================================
global.requestAnimationFrame = /* istanbul ignore next */ (callback) => {
setTimeout(callback, 0)
}
================================================
FILE: assets/webpack.config.js
================================================
const { resolve } = require('path');
const publicPath = 'http://localhost:3000/';
const webpack = require('webpack');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const OpenBrowserPlugin = require('open-browser-webpack-plugin');
// const MiniCssExtractPlugin = require('mini-css-extract-plugin');
// const ExtractCssChunks = require("extract-css-chunks-webpack-plugin")
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const config = {
devtool: 'cheap-module-eval-source-map',
// devtool: 'inline-source-map',
entry: [
'react-hot-loader/patch',
// activate HMR for React
'webpack-dev-server/client?http://localhost:3000',
// bundle the client for webpack-dev-server
// and connect to the provided endpoint
'webpack/hot/only-dev-server',
// bundle the client for hot reloading
// only- means to only hot reload for successful updates
'./main.js',
// entry point
'./assets/scss/index.scss',
'jquery/dist/jquery.js',
'tether/dist/js/tether.js',
'bootstrap/dist/js/bootstrap.js',
],
resolve: {
alias: {
components: resolve(__dirname, 'app/components/index.js'),
reducers: resolve(__dirname, 'app/reducers'),
actions: resolve(__dirname, 'app/actions'),
config: resolve(__dirname, 'app/config'),
store: resolve(__dirname, 'app/store'),
utils: resolve(__dirname, 'app/utils'),
sagas: resolve(__dirname, 'app/sagas'),
},
},
output: {
filename: 'js/app.js',
path: resolve(__dirname, '../priv/static'),
publicPath: publicPath,
hotUpdateChunkFilename: 'hot-update.js',
hotUpdateMainFilename: 'hot-update.json',
crossOriginLoading: "anonymous",
},
context: resolve(__dirname, 'app'),
mode: 'development',
devServer: {
host: 'localhost',
port: 3000,
headers: {
'Access-Control-Allow-Origin': '*',
},
hot: true,
// enable HMR on the server
historyApiFallback: true,
// respond to 404s with index.html
contentBase: resolve(__dirname, '../priv/static'),
publicPath: '/',
// overlay: true,
overlay: {
warnings: true,
errors: true,
},
},
module: {
rules: [
{
test: /\.js$/,
loaders: [
'babel-loader',
],
exclude: /node_modules/,
},
{
test: /\.scss$/, // files ending with .scss
use: ['css-hot-loader'].concat(ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader', 'sass-loader'],
})),
// use: [
// // MiniCssExtractPlugin.loader,
// ExtractCssChunks.loader,
// // 'css-hot-loader',
// 'css-loader',
// 'sass-loader',
// ],
// use: ['css-hot-loader'].concat(MiniCssExtractPlugin.extract({
// fallback: 'style-loader',
// use: ['css-loader', 'sass-loader'],
// })),
},
{ test: /\.css$/, loader: ['style-loader', 'css-loader'] },
{ test: /\.(png|jpg)$/, use: 'url-loader?limit=15000' },
{ test: /\.svg$/, loader: 'url-loader?limit=65000&mimetype=image/svg+xml&name=fonts/[name].[ext]' },
{ test: /\.woff$/, loader: 'url-loader?limit=65000&mimetype=application/font-woff&name=fonts/[name].[ext]' },
{ test: /\.woff2$/, loader: 'url-loader?limit=65000&mimetype=application/font-woff2&name=fonts/[name].[ext]' },
{ test: /\.[ot]tf$/, loader: 'url-loader?limit=65000&mimetype=application/octet-stream&name=fonts/[name].[ext]' },
{ test: /\.eot$/, loader: 'url-loader?limit=65000&mimetype=application/vnd.ms-fontobject&name=fonts/[name].[ext]' },
],
},
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
'window.jQuery': 'jquery',
Tether: 'tether',
}),
new webpack.HotModuleReplacementPlugin(),
// enable HMR globally
new webpack.NamedModulesPlugin(),
// prints more readable module names in the browser console on HMR updates
new webpack.NoEmitOnErrorsPlugin(),
// do not emit compiled assets that include errors
new CopyWebpackPlugin([{ from: 'vendors', to: 'vendors' }]),
new OpenBrowserPlugin({ url: 'http://localhost:4000' }),
// new ExtractCssChunks(
// {
// // Options similar to the same options in webpackOptions.output
// // both options are optional
// filename: "./css/style.css",
// hot: true, // if you want HMR - we try to automatically inject hot reloading but if it's not working, add it to the config
// orderWarning: true, // Disable to remove warnings about conflicting order between imports
// reloadAll: true, // when desperation kicks in - this is a brute force HMR flag
// cssModules: true // if you use cssModules, this can help.
// }),
// new MiniCssExtractPlugin({ filename: 'css/style.css', hot: true}),
new ExtractTextPlugin({ filename: './css/style.css', disable: false, allChunks: true }),
],
};
module.exports = config;
================================================
FILE: assets/webpack.production.config.js
================================================
const { resolve } = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
// const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const config = {
devtool: 'cheap-module-source-map',
entry: [
'./main.js',
'./assets/scss/index.scss',
],
context: resolve(__dirname, 'app'),
resolve: {
alias: {
components: resolve(__dirname, 'app/components/index.js'),
reducers: resolve(__dirname, 'app/reducers'),
actions: resolve(__dirname, 'app/actions'),
config: resolve(__dirname, 'app/config'),
store: resolve(__dirname, 'app/store'),
utils: resolve(__dirname, 'app/utils'),
sagas: resolve(__dirname, 'app/sagas'),
},
},
output: {
filename: 'js/app.js',
path: resolve(__dirname, '../priv/static'),
publicPath: '/',
},
optimization: {
minimizer: [
new UglifyJsPlugin({
uglifyOptions: {
// beautify: false,
// mangle: {
// screw_ie8: true,
// keep_fnames: true,
// },
compress: {
booleans: true,
// screw_ie8: true,
},
output: {
comments: false,
},
minify: {},
}
}),
]
},
plugins: [
new HtmlWebpackPlugin({
template: `${__dirname}/app/index.html`,
filename: 'index.html',
inject: 'body',
}),
new webpack.optimize.OccurrenceOrderPlugin(),
new webpack.LoaderOptionsPlugin({
minimize: true,
debug: false,
}),
new webpack.DefinePlugin({ 'process.env': { NODE_ENV: JSON.stringify('production') } }),
// new MiniCssExtractPlugin({ filename: 'css/style.css'}),
new ExtractTextPlugin({ filename: 'css/style.css', disable: false, allChunks: true }),
new CopyWebpackPlugin([{ from: './vendors', to: 'vendors' }]),
],
module: {
rules: [
{
test: /\.js?$/,
exclude: /node_modules/,
use: [{
loader: 'babel-loader',
}]
},
{
test: /\.scss$/,
exclude: /node_modules/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [
'css-loader',
{ loader: 'sass-loader', query: { sourceMap: false } },
],
}),
// use: [
// MiniCssExtractPlugin.loader,
// 'css-hot-loader',
// 'css-loader',
// 'sass-loader',
// ],
// use: MiniCssExtractPlugin.extract({
// fallback: 'style-loader',
// use: [
// 'css-loader',
// { loader: 'sass-loader', query: { sourceMap: false } },
// ],
// }),
},
{ test: /\.css$/, loader: ['style-loader', 'css-loader'] },
{ test: /\.(png|jpg)$/, use: 'url-loader?limit=15000' },
{ test: /\.svg$/, loader: 'url-loader?limit=65000&mimetype=image/svg+xml&name=fonts/[name].[ext]' },
{ test: /\.woff$/, loader: 'url-loader?limit=65000&mimetype=application/font-woff&name=fonts/[name].[ext]' },
{ test: /\.woff2$/, loader: 'url-loader?limit=65000&mimetype=application/font-woff2&name=fonts/[name].[ext]' },
{ test: /\.[ot]tf$/, loader: 'url-loader?limit=65000&mimetype=application/octet-stream&name=fonts/[name].[ext]' },
{ test: /\.eot$/, loader: 'url-loader?limit=65000&mimetype=application/vnd.ms-fontobject&name=fonts/[name].[ext]' },
],
},
};
module.exports = config;
================================================
FILE: config/config.exs
================================================
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.
use Mix.Config
# General application configuration
config :boilerplate,
ecto_repos: [Boilerplate.Repo]
# Configures the endpoint
config :boilerplate, BoilerplateWeb.Endpoint,
url: [host: "localhost"],
secret_key_base: "H5AcfaG9fEz2gjjKEBJ3PNT7peIPEo270GOHwgataQEsfdx8Ujsi3aDlxel77eQH",
render_errors: [view: BoilerplateWeb.ErrorView, accepts: ~w(html json)],
pubsub: [name: Boilerplate.PubSub,
adapter: Phoenix.PubSub.PG2]
# Configures Elixir's Logger
config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:user_id]
# Configures Guardian
config :boilerplate, BoilerplateWeb.Guardian,
issuer: "boilerplate",
ttl: {30, :days},
verify_issuer: true
# serializer: Boilerplate.GuardianSerializer
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env}.exs"
================================================
FILE: config/dev.exs
================================================
use Mix.Config
# For development, we disable any cache and enable
# debugging and code reloading.
#
# The watchers configuration can be used to run external
# watchers to your application. For example, we use it
# with brunch.io to recompile .js and .css sources.
config :boilerplate, BoilerplateWeb.Endpoint,
http: [port: 4000],
debug_errors: true,
code_reloader: true,
check_origin: false,
watchers: [node: ["node_modules/webpack-dev-server/bin/webpack-dev-server.js", "--colors", "--stdin", cd: Path.expand("../assets", __DIR__)]]
# Not working in windows because of npm permission issue
# watchers: [npm: ["run", "start", cd: Path.expand("../assets", __DIR__)]]
# watchers: [npm: ["run", "watch", cd: Path.expand("../assets", __DIR__)]]
# ## SSL Support
#
# In order to use HTTPS in development, a self-signed
# certificate can be generated by running the following
# command from your terminal:
#
# openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" -keyout priv/server.key -out priv/server.pem
#
# The `http:` config above can be replaced with:
#
# https: [port: 4000, keyfile: "priv/server.key", certfile: "priv/server.pem"],
#
# If desired, both `http:` and `https:` keys can be
# configured to run both http and https servers on
# different ports.
# Watch static and templates for browser reloading.
config :boilerplate, BoilerplateWeb.Endpoint,
live_reload: [
patterns: [
~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
~r{priv/gettext/.*(po)$},
~r{lib/boilerplate_web/views/.*(ex)$},
~r{lib/boilerplate_web/templates/.*(eex)$}
]
]
# Do not include metadata nor timestamps in development logs
config :logger, :console, format: "[$level] $message\n"
# Set a higher stacktrace during development. Avoid configuring such
# in production as building large stacktraces may be expensive.
config :phoenix, :stacktrace_depth, 20
# Configure your database
config :boilerplate, Boilerplate.Repo,
adapter: Ecto.Adapters.Postgres,
username: "postgres",
password: "postgres",
database: "boilerplate_dev",
hostname: "localhost",
pool_size: 10
# Configure Guardian secret key
config :boilerplate, BoilerplateWeb.Guardian,
secret_key: "OSMuzr1uWJthItzsyXItnRoM3MLNaZXUwkamEHTwxUBYPPDuQTLPJnMBMiMATRjF"
================================================
FILE: config/prod.exs
================================================
use Mix.Config
# For production, we often load configuration from external
# sources, such as your system environment. For this reason,
# you won't find the :http configuration below, but set inside
# BoilerplateWeb.Endpoint.init/2 when load_from_system_env is
# true. Any dynamic configuration should be done there.
#
# Don't forget to configure the url host to something meaningful,
# Phoenix uses this information when generating URLs.
#
# Finally, we also include the path to a cache manifest
# containing the digested version of static files. This
# manifest is generated by the mix phx.digest task
# which you typically run after static files are built.
config :boilerplate, BoilerplateWeb.Endpoint,
load_from_system_env: true,
url: [scheme: "https", host: "phxboilerplate.herokuapp.com", port: 443],
force_ssl: [rewrite_on: [:x_forwarded_proto]],
cache_static_manifest: "priv/static/cache_manifest.json",
secret_key_base: Map.fetch!(System.get_env(), "SECRET_KEY_BASE")
# Do not print debug messages in production
config :logger, level: :info
# Configure database
config :boilerplate, Boilerplate.Repo,
adapter: Ecto.Adapters.Postgres,
url: System.get_env("DATABASE_URL"),
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
ssl: true
# ## SSL Support
#
# To get SSL working, you will need to add the `https` key
# to the previous section and set your `:url` port to 443:
#
# config :boilerplate, BoilerplateWeb.Endpoint,
# ...
# url: [host: "example.com", port: 443],
# https: [:inet6,
# port: 443,
# keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
# certfile: System.get_env("SOME_APP_SSL_CERT_PATH")]
#
# Where those two env variables return an absolute path to
# the key and cert in disk or a relative path inside priv,
# for example "priv/ssl/server.key".
#
# We also recommend setting `force_ssl`, ensuring no data is
# ever sent via http, always redirecting to https:
#
# config :boilerplate, BoilerplateWeb.Endpoint,
# force_ssl: [hsts: true]
#
# Check `Plug.SSL` for all available options in `force_ssl`.
# ## Using releases
#
# If you are doing OTP releases, you need to instruct Phoenix
# to start the server for all endpoints:
#
# config :phoenix, :serve_endpoints, true
#
# Alternatively, you can configure exactly which server to
# start per endpoint:
#
# config :boilerplate, BoilerplateWeb.Endpoint, server: true
#
config :boilerplate, BoilerplateWeb.Guardian,
secret_key: System.get_env("GUARDIAN_SECRET_KEY")
# Finally import the config/prod.secret.exs
# which should be versioned separately.
# import_config "prod.secret.exs"
================================================
FILE: config/test.exs
================================================
use Mix.Config
# We don't run a server during test. If one is required,
# you can enable the server option below.
config :boilerplate, BoilerplateWeb.Endpoint,
http: [port: 4001],
server: false
# Print only warnings and errors during test
config :logger, level: :warn
# Reduce number of rounds
config :bcrypt_elixir, log_rounds: 4
# Configure your database
config :boilerplate, Boilerplate.Repo,
adapter: Ecto.Adapters.Postgres,
username: "postgres",
password: "postgres",
database: "boilerplate_test",
hostname: "localhost",
pool: Ecto.Adapters.SQL.Sandbox
# Configure Guardian secret key
config :boilerplate, BoilerplateWeb.Guardian,
secret_key: "OSMuzr1uWJthItzsyXItnRoM3MLNaZXUwkamEHTwxUBYPPDuQTLPJnMBMiMATRjF"
================================================
FILE: elixir_buildpack.config
================================================
elixir_version=1.6.1
================================================
FILE: lib/boilerplate/accounts/accounts.ex
================================================
defmodule Boilerplate.Accounts do
@moduledoc """
The Accounts context.
"""
import Ecto.{Query, Changeset}, warn: false
alias Boilerplate.Repo
alias Boilerplate.Accounts.User
@doc """
Returns the list of users.
## Examples
iex> list_users()
[%User{}, ...]
"""
def list_users do
Repo.all(User)
end
@doc """
Gets a single user.
Raises `Ecto.NoResultsError` if the User does not exist.
## Examples
iex> get_user!(123)
%User{}
iex> get_user!(456)
** (Ecto.NoResultsError)
"""
def get_user!(id), do: Repo.get!(User, id)
def get_user_by_username(username) do
username = String.downcase(username)
Repo.one(from(p in User, where: fragment("lower(?)", p.username) == ^username)) ||
{:error, :user_not_found}
end
@doc """
Creates a user.
## Examples
iex> create_user(%{field: value})
{:ok, %User{}}
iex> create_user(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_user(attrs \\ %{}, superuser \\ %{}) do
if !is_exist_username(attrs) do
if !is_exist_email(attrs) do
%User{}
|> match_superuser_registration_changeset(attrs, superuser)
|> Repo.insert()
else
{:error, :already_taken_email}
end
else
{:error, :already_taken_username}
end
end
@doc """
Updates a user.
## Examples
iex> update_user(user, %{field: new_value})
{:ok, %User{}}
iex> update_user(user, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_user(%User{} = user, attrs, current_user \\ %{}) do
if is_superuser_or_same_user?(user, current_user) do
user
|> match_superuser_changeset(attrs, current_user)
|> Repo.update()
else
{:error, :forbidden}
end
end
@doc """
Deletes a User.
## Examples
iex> delete_user(user)
{:ok, %User{}}
iex> delete_user(user)
{:error, %Ecto.Changeset{}}
"""
# def delete_user(%User{} = user), do: delete_user(user, user)
def delete_user(%User{} = user, %User{} = current_user) do
if is_superuser_or_same_user?(user, current_user) do
Repo.delete(user)
else
{:error, :forbidden}
end
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking user changes.
## Examples
iex> change_user(user)
%Ecto.Changeset{source: %User{}}
"""
def change_user(%User{} = user) do
User.changeset(user, %{})
end
defp is_exist_username(attrs) do
username = attrs["username"] || attrs[:username]
if username do
username = String.downcase(username)
Repo.one(
from(
p in User,
where: fragment("lower(?)", p.username) == fragment("lower(?)", ^username)
)
)
end
end
defp is_exist_email(attrs) do
email = attrs["email"] || attrs[:email]
if email do
email = String.downcase(email)
Repo.one(
from(p in User, where: fragment("lower(?)", p.email) == fragment("lower(?)", ^email))
)
end
end
def get_current_token(conn) do
BoilerplateWeb.Guardian.Plug.current_token(conn)
end
def sign_out(conn) do
BoilerplateWeb.Guardian.Plug.sign_out(conn)
end
def get_claims(conn) do
BoilerplateWeb.Guardian.Plug.current_claims(conn)
end
def refresh_token(jwt) do
BoilerplateWeb.Guardian.refresh(jwt)
end
def get_current_user(conn) do
BoilerplateWeb.Guardian.Plug.current_resource(conn)
end
def sign_in_user(conn, user) do
BoilerplateWeb.Guardian.Plug.sign_in(conn, user)
end
def authenticate(%{"email" => email, "password" => password}) do
user =
Repo.one(
from(p in User, where: fragment("lower(?)", p.email) == fragment("lower(?)", ^email))
)
case check_password(user, password) do
true -> {:ok, user}
_ -> {:error, :wrong_credentials}
end
end
def update_last_login(%User{} = user) do
last_login = NaiveDateTime.utc_now()
user
|> User.last_login_changeset(%{last_login: last_login})
|> Repo.update()
end
defp check_password(user, password) do
case user do
nil -> Comeonin.Bcrypt.dummy_checkpw()
_ -> Comeonin.Bcrypt.checkpw(password, user.password_hash)
end
end
defp is_superuser?(user) do
Map.get(user, :is_superuser)
end
def is_the_same_users?(user, current_user) do
id1 = Map.get(user, :id)
id2 = Map.get(current_user, :id)
id1 == id2
end
defp match_superuser_changeset(%User{} = user, attrs, superuser) do
if is_superuser?(superuser) do
User.superuser_changeset(user, attrs)
else
User.changeset(user, attrs)
end
end
defp match_superuser_registration_changeset(%User{} = user, attrs, superuser) do
if is_superuser?(superuser) do
User.superuser_registration_changeset(user, attrs)
else
User.registration_changeset(user, attrs)
end
end
defp is_superuser_or_same_user?(user, current_user) do
is_superuser?(current_user) || is_the_same_users?(user, current_user)
end
end
================================================
FILE: lib/boilerplate/accounts/user.ex
================================================
defmodule Boilerplate.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
alias Boilerplate.Accounts.User
schema "users" do
field(:name, :string)
field(:email, :string)
field(:password, :string, virtual: true)
field(:password_hash, :string)
field(:username, :string)
field(:is_staff, :boolean, default: false)
field(:is_superuser, :boolean, default: false)
field(:last_login, :naive_datetime)
timestamps()
end
@doc false
def changeset(%User{} = user, attrs) do
user
|> cast(attrs, [:name, :username, :email])
|> validate_required([:name, :username, :email])
|> validate_length(:username, min: 1, max: 20)
|> validate_format(:username, ~r/^[A-Za-z0-9._]+$/)
|> validate_format(:email, ~r/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/)
|> unique_constraint(:username)
|> unique_constraint(:email)
end
@doc false
def superuser_changeset(%User{} = user, attrs) do
user
|> changeset(attrs)
|> cast(attrs, [:is_staff, :is_superuser])
end
@doc false
def registration_changeset(%User{} = user, attrs) do
user
|> changeset(attrs)
|> cast(attrs, [:password])
|> validate_length(:password, min: 6, max: 100)
|> put_password_hash
end
@doc false
def superuser_registration_changeset(%User{} = user, attrs) do
user
|> superuser_changeset(attrs)
|> cast(attrs, [:password])
|> validate_length(:password, min: 6, max: 100)
|> put_password_hash
end
@doc false
def last_login_changeset(%User{} = user, attrs) do
user
|> cast(attrs, [:last_login])
|> validate_required([:last_login])
end
defp put_password_hash(changeset) do
case changeset do
%Ecto.Changeset{valid?: true, changes: %{password: password}} ->
put_change(changeset, :password_hash, Comeonin.Bcrypt.hashpwsalt(password))
_ ->
changeset
end
end
end
================================================
FILE: lib/boilerplate/application.ex
================================================
defmodule Boilerplate.Application do
use Application
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
def start(_type, _args) do
import Supervisor.Spec
# Define workers and child supervisors to be supervised
children = [
# Start the Ecto repository
supervisor(Boilerplate.Repo, []),
# Start the endpoint when the application starts
supervisor(BoilerplateWeb.Endpoint, [])
# Start your own worker by calling: Boilerplate.Worker.start_link(arg1, arg2, arg3)
# worker(Boilerplate.Worker, [arg1, arg2, arg3]),
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Boilerplate.Supervisor]
Supervisor.start_link(children, opts)
end
# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
def config_change(changed, _new, removed) do
BoilerplateWeb.Endpoint.config_change(changed, removed)
:ok
end
end
================================================
FILE: lib/boilerplate/repo.ex
================================================
defmodule Boilerplate.Repo do
use Ecto.Repo, otp_app: :boilerplate
@doc """
Dynamically loads the repository url from the
DATABASE_URL environment variable.
"""
def init(_, opts) do
{:ok, Keyword.put(opts, :url, System.get_env("DATABASE_URL"))}
end
end
================================================
FILE: lib/boilerplate.ex
================================================
defmodule Boilerplate do
@moduledoc """
Boilerplate keeps the contexts that define your domain
and business logic.
Contexts are also responsible for managing your data, regardless
if it comes from the database, an external API or others.
"""
end
================================================
FILE: lib/boilerplate_web/channels/user_socket.ex
================================================
defmodule BoilerplateWeb.UserSocket do
use Phoenix.Socket
## Channels
# channel "room:*", BoilerplateWeb.RoomChannel
## Transports
transport(:websocket, Phoenix.Transports.WebSocket, timeout: 45_000)
# transport :longpoll, Phoenix.Transports.LongPoll
# Socket params are passed from the client and can
# be used to verify and authenticate a user. After
# verification, you can put default assigns into
# the socket that will be set for all channels, ie
#
# {:ok, assign(socket, :user_id, verified_user_id)}
#
# To deny connection, return `:error`.
#
# See `Phoenix.Token` documentation for examples in
# performing token verification on connect.
def connect(_params, socket) do
{:ok, socket}
end
# Socket id's are topics that allow you to identify all sockets for a given user:
#
# def id(socket), do: "user_socket:#{socket.assigns.user_id}"
#
# Would allow you to broadcast a "disconnect" event and terminate
# all active sockets and channels for a given user:
#
# BoilerplateWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
#
# Returning `nil` makes this socket anonymous.
def id(_socket), do: nil
end
================================================
FILE: lib/boilerplate_web/controllers/api/session_controller.ex
================================================
defmodule BoilerplateWeb.SessionController do
use BoilerplateWeb, :controller
alias Boilerplate.Accounts
action_fallback(BoilerplateWeb.FallbackController)
def create(conn, params) do
with {:ok, user} <- Accounts.authenticate(params) do
new_conn = Accounts.sign_in_user(conn, user)
jwt = Accounts.get_current_token(new_conn)
spawn(fn -> Accounts.update_last_login(user) end)
new_conn
|> put_status(:created)
|> render("show.json", user: user, jwt: jwt)
end
end
def delete(conn, _) do
user = Accounts.get_current_user(conn)
with new_conn = Accounts.sign_out(conn) do
spawn(fn -> Accounts.update_last_login(user) end)
new_conn
|> put_status(:ok)
|> render("delete.json")
end
end
def refresh(conn, _params) do
user = Accounts.get_current_user(conn)
with jwt = Accounts.get_current_token(conn),
{:ok, _, {new_jwt, _new_claims}} <- Accounts.refresh_token(jwt) do
spawn(fn -> Accounts.update_last_login(user) end)
conn
|> put_status(:ok)
|> render("show.json", user: user, jwt: new_jwt)
end
end
end
================================================
FILE: lib/boilerplate_web/controllers/api/user_controller.ex
================================================
defmodule BoilerplateWeb.UserController do
use BoilerplateWeb, :controller
alias Boilerplate.Accounts
alias Boilerplate.Accounts.User
action_fallback(BoilerplateWeb.FallbackController)
def index(conn, _) do
users = Accounts.list_users()
current_user = Accounts.get_current_user(conn)
render(conn, "index.json", users: users, current_user: current_user)
end
def show(conn, %{"username" => username}) do
with %User{} = user <- Accounts.get_user_by_username(username) do
current_user = Accounts.get_current_user(conn)
render(conn, "show.json", user: user, current_user: current_user)
end
end
def update(conn, %{"id" => id, "user" => user_params}) do
user = Accounts.get_user!(id)
current_user = Accounts.get_current_user(conn)
with {:ok, %User{} = user} <- Accounts.update_user(user, user_params, current_user) do
render(conn, "show.json", user: user)
end
end
def create(conn, user_params) do
with {:ok, %User{} = user} <- Accounts.create_user(user_params) do
new_conn = Accounts.sign_in_user(conn, user)
jwt = Accounts.get_current_token(new_conn)
new_conn
|> put_status(:created)
|> render(BoilerplateWeb.SessionView, "show.json", user: user, jwt: jwt)
end
end
def delete(conn, %{"id" => id}) do
user = Accounts.get_user!(id)
current_user = Accounts.get_current_user(conn)
with {:ok, %User{}} <- Accounts.delete_user(user, current_user) do
send_resp(conn, :no_content, "")
end
end
end
================================================
FILE: lib/boilerplate_web/controllers/auth_error_controller.ex
================================================
defmodule BoilerplateWeb.AuthErrorController do
import Plug.Conn
use BoilerplateWeb, :controller
def auth_error(conn, {_type, _reason}, _opts) do
conn
|> put_status(:unauthorized)
|> render(BoilerplateWeb.SessionView, "wrong_credentials.json")
end
end
================================================
FILE: lib/boilerplate_web/controllers/fallback_controller.ex
================================================
defmodule BoilerplateWeb.FallbackController do
@moduledoc """
Translates controller action results into valid `Plug.Conn` responses.
See `Phoenix.Controller.action_fallback/1` for more details.
"""
use BoilerplateWeb, :controller
def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
conn
|> put_status(:unprocessable_entity)
|> render(BoilerplateWeb.ChangesetView, "error.json", changeset: changeset)
end
def call(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
|> render(BoilerplateWeb.ErrorView, :"404")
end
def call(conn, {:error, :wrong_credentials}) do
conn
|> put_status(:unprocessable_entity)
|> render(BoilerplateWeb.SessionView, "wrong_credentials.json")
end
def call(conn, {:error, :no_session}) do
conn
|> put_status(:unprocessable_entity)
|> render(BoilerplateWeb.SessionView, "no_session.json")
end
def call(conn, {:error, :invalid_issuer}) do
conn
|> put_status(:bad_request)
|> render(BoilerplateWeb.SessionView, "invalid_issuer.json")
end
def call(conn, {:error, :already_taken_username}) do
conn
|> put_status(:bad_request)
|> render(BoilerplateWeb.SessionView, "already_taken_username.json")
end
def call(conn, {:error, :already_taken_email}) do
conn
|> put_status(:bad_request)
|> render(BoilerplateWeb.SessionView, "already_taken_email.json")
end
def call(conn, {:error, "Unknown resource type"}) do
conn
|> put_status(:unauthorized)
|> render(BoilerplateWeb.SessionView, "wrong_token.json")
end
def call(conn, {:error, :user_not_found}) do
conn
|> put_status(:not_found)
|> render(BoilerplateWeb.UserView, "404.json")
end
def call(conn, {:error, :forbidden}) do
conn
|> put_status(:forbidden)
|> render(BoilerplateWeb.ErrorView, "permission_denied.json")
end
end
================================================
FILE: lib/boilerplate_web/controllers/page_controller.ex
================================================
defmodule BoilerplateWeb.PageController do
use BoilerplateWeb, :controller
def index(conn, _params) do
render(conn, "index.html")
end
end
================================================
FILE: lib/boilerplate_web/endpoint.ex
================================================
defmodule BoilerplateWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :boilerplate
socket("/socket", BoilerplateWeb.UserSocket)
# Serve at "/" the static files from "priv/static" directory.
#
# You should set gzip to true if you are running phoenix.digest
# when deploying your static files in production.
plug(
Plug.Static,
at: "/",
from: :boilerplate,
gzip: false,
only: ~w(css fonts images js favicon.ico robots.txt)
)
# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.
if code_reloading? do
socket("/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket)
plug(Phoenix.LiveReloader)
plug(Phoenix.CodeReloader)
end
plug(Plug.Logger)
plug(
Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Poison
)
plug Plug.MethodOverride
plug Plug.Head
# The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with.
# Set :encryption_salt if you would also like to encrypt it.
plug(
Plug.Session,
store: :cookie,
key: "_boilerplate_key",
signing_salt: "7ptxdQY6"
)
plug BoilerplateWeb.Router
@doc """
Callback invoked for dynamically configuring the endpoint.
It receives the endpoint configuration and checks if
configuration should be loaded from the system environment.
"""
def init(_key, config) do
if config[:load_from_system_env] do
port = System.get_env("PORT") || raise "expected the PORT environment variable to be set"
{:ok, Keyword.put(config, :http, [:inet6, port: port])}
else
{:ok, config}
end
end
end
================================================
FILE: lib/boilerplate_web/gettext.ex
================================================
defmodule BoilerplateWeb.Gettext do
@moduledoc """
A module providing Internationalization with a gettext-based API.
By using [Gettext](https://hexdocs.pm/gettext),
your module gains a set of macros for translations, for example:
import BoilerplateWeb.Gettext
# Simple translation
gettext "Here is the string to translate"
# Plural translation
ngettext "Here is the string to translate",
"Here are the strings to translate",
3
# Domain-based translation
dgettext "errors", "Here is the error message to translate"
See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
"""
use Gettext, otp_app: :boilerplate
end
================================================
FILE: lib/boilerplate_web/guardian.ex
================================================
defmodule BoilerplateWeb.Guardian do
use Guardian, otp_app: :boilerplate
def subject_for_token(resource, _claims) do
# You can use any value for the subject of your token but
# it should be useful in retrieving the resource later, see
# how it being used on `resource_from_claims/1` function.
# A unique `id` is a good subject, a non-unique email address
# is a poor subject.
sub = to_string(resource.id)
{:ok, sub}
end
def resource_from_claims(claims) do
# Here we'll look up our resource from the claims, the subject can be
# found in the `"sub"` key. In `above subject_for_token/2` we returned
# the resource id so here we'll rely on that to look it up.
id = claims["sub"]
resource = Boilerplate.Accounts.get_user!(id)
{:ok, resource}
end
end
================================================
FILE: lib/boilerplate_web/router.ex
================================================
defmodule BoilerplateWeb.Router do
use BoilerplateWeb, :router
pipeline :browser do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(:fetch_flash)
plug(:protect_from_forgery)
plug(:put_secure_browser_headers)
end
pipeline :api do
plug(:accepts, ["json"])
end
pipeline :unauthorized do
plug(:fetch_session)
end
pipeline :authorized do
plug(:fetch_session)
plug(
Guardian.Plug.Pipeline,
module: BoilerplateWeb.Guardian,
error_handler: BoilerplateWeb.AuthErrorController
)
plug(Guardian.Plug.VerifySession)
plug(Guardian.Plug.LoadResource)
end
scope "/api", BoilerplateWeb do
pipe_through(:api)
scope "/" do
pipe_through(:unauthorized)
post("/sessions", SessionController, :create)
resources("/users", UserController, only: [:create])
end
scope "/" do
pipe_through(:authorized)
delete("/sessions", SessionController, :delete)
post("/sessions/refresh", SessionController, :refresh)
resources("/users", UserController, except: [:create])
end
end
scope "/", BoilerplateWeb do
# Use the default browser stack
pipe_through(:browser)
get("/*path", PageController, :index)
end
end
================================================
FILE: lib/boilerplate_web/templates/layout/app.html.eex
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="description">
<meta name="author" content="Dmitriy Chernyshov">
<title>Boilerplate</title>
<%= if Mix.env == :dev do %>
<link crossorigin rel="stylesheet" href="http://localhost:3000/css/style.css">
<% else %>
<link rel="stylesheet" href="<%= static_path(@conn, "/css/style.css") %>">
<% end %>
</head>
<body>
<div id="root"></div>
<%= if Mix.env == :dev do %>
<script crossorigin src='http://localhost:3000/js/app.js'></script>
<% else %>
<script src="<%= static_path(@conn, "/js/app.js") %>"></script>
<% end %>
</body>
</html>
================================================
FILE: lib/boilerplate_web/views/changeset_view.ex
================================================
defmodule BoilerplateWeb.ChangesetView do
use BoilerplateWeb, :view
@doc """
Traverses and translates changeset errors.
See `Ecto.Changeset.traverse_errors/2` and
`BoilerplateWeb.ErrorHelpers.translate_error/1` for more details.
"""
def translate_errors(changeset) do
Ecto.Changeset.traverse_errors(changeset, &translate_error/1)
end
def render("error.json", %{changeset: changeset}) do
# When encoded, the changeset returns its errors
# as a JSON object. So we just pass it forward.
%{errors: translate_errors(changeset)}
end
end
================================================
FILE: lib/boilerplate_web/views/error_helpers.ex
================================================
defmodule BoilerplateWeb.ErrorHelpers do
@moduledoc """
Conveniences for translating and building error messages.
"""
use Phoenix.HTML
@doc """
Generates tag for inlined form input errors.
"""
def error_tag(form, field) do
Enum.map(Keyword.get_values(form.errors, field), fn error ->
content_tag(:span, translate_error(error), class: "help-block")
end)
end
@doc """
Translates an error message using gettext.
"""
def translate_error({msg, opts}) do
# Because error messages were defined within Ecto, we must
# call the Gettext module passing our Gettext backend. We
# also use the "errors" domain as translations are placed
# in the errors.po file.
# Ecto will pass the :count keyword if the error message is
# meant to be pluralized.
# On your own code and templates, depending on whether you
# need the message to be pluralized or not, this could be
# written simply as:
#
# dngettext "errors", "1 file", "%{count} files", count
# dgettext "errors", "is invalid"
#
if count = opts[:count] do
Gettext.dngettext(BoilerplateWeb.Gettext, "errors", msg, msg, count, opts)
else
Gettext.dgettext(BoilerplateWeb.Gettext, "errors", msg, opts)
end
end
end
================================================
FILE: lib/boilerplate_web/views/error_view.ex
================================================
defmodule BoilerplateWeb.ErrorView do
use BoilerplateWeb, :view
def render("permission_denied", _assigns) do
%{errors: "Permission denied"}
end
def render("404.html", _assigns) do
"Page not found"
end
def render("500.html", _assigns) do
"Internal server error"
end
# In case no render clause matches or no
# template is found, let's render it as 500
def template_not_found(_template, assigns) do
render("500.html", assigns)
end
end
================================================
FILE: lib/boilerplate_web/views/layout_view.ex
================================================
defmodule BoilerplateWeb.LayoutView do
use BoilerplateWeb, :view
end
================================================
FILE: lib/boilerplate_web/views/page_view.ex
================================================
defmodule BoilerplateWeb.PageView do
use BoilerplateWeb, :view
end
================================================
FILE: lib/boilerplate_web/views/session_view.ex
================================================
defmodule BoilerplateWeb.SessionView do
use BoilerplateWeb, :view
alias BoilerplateWeb.SessionView
def render("show.json", %{user: user, jwt: jwt}) do
%{data: render_one(user, BoilerplateWeb.UserView, "user.json"), meta: %{token: jwt}}
end
def render("delete.json", _) do
%{ok: true}
end
def render("no_session.json", _) do
%{errors: "invalid or expired session token"}
end
def render("wrong_credentials.json", _) do
%{errors: "Wrong email or password"}
end
def render("invalid_issuer.json", _) do
%{errors: "invalid issuer"}
end
def render("already_taken_username.json", _) do
%{errors: "This username has already been taken. Please take another username"}
end
def render("already_taken_email.json", _) do
%{errors: "This email has already been taken. Please take another email"}
end
def render("invalid_token.json", _) do
%{errors: "invalid token"}
end
end
================================================
FILE: lib/boilerplate_web/views/user_view.ex
================================================
defmodule BoilerplateWeb.UserView do
use BoilerplateWeb, :view
alias BoilerplateWeb.UserView
def render("index.json", %{users: users, current_user: %{is_staff: true}}) do
%{
data: %{
users: render_many(users, UserView, "user.json")
}
}
end
def render("index.json", %{users: users}) do
%{
data: %{
users: render_many(users, UserView, "show_user.json")
}
}
end
def render("show.json", %{user: user, current_user: %{is_staff: true}}) do
%{data: render_one(user, UserView, "user.json")}
end
def render("show.json", %{user: user}) do
%{data: render_one(user, UserView, "show_user.json")}
end
def render("user.json", %{user: user}) do
%{
id: user.id,
username: user.username,
name: user.name,
email: user.email,
last_login: user.last_login,
is_staff: user.is_staff,
is_superuser: user.is_superuser
}
end
def render("show_user.json", %{user: user}) do
%{id: user.id, username: user.username, name: user.name, last_login: user.last_login}
# email: user.email
end
def render("404.json", _assigns) do
%{errors: "User not found"}
end
end
================================================
FILE: lib/boilerplate_web.ex
================================================
defmodule BoilerplateWeb do
@moduledoc """
The entrypoint for defining your web interface, such
as controllers, views, channels and so on.
This can be used in your application as:
use BoilerplateWeb, :controller
use BoilerplateWeb, :view
The definitions below will be executed for every view,
controller, etc, so keep them short and clean, focused
on imports, uses and aliases.
Do NOT define functions inside the quoted expressions
below. Instead, define any helper function in modules
and import those modules here.
"""
def controller do
quote do
use Phoenix.Controller, namespace: BoilerplateWeb
import Plug.Conn
import BoilerplateWeb.Router.Helpers
import BoilerplateWeb.Gettext
end
end
def view do
quote do
use Phoenix.View,
root: "lib/boilerplate_web/templates",
namespace: BoilerplateWeb
# Import convenience functions from controllers
import Phoenix.Controller, only: [get_flash: 2, view_module: 1]
# Use all HTML functionality (forms, tags, etc)
use Phoenix.HTML
import BoilerplateWeb.Router.Helpers
import BoilerplateWeb.ErrorHelpers
import BoilerplateWeb.Gettext
end
end
def router do
quote do
use Phoenix.Router
import Plug.Conn
import Phoenix.Controller
end
end
def channel do
quote do
use Phoenix.Channel
import BoilerplateWeb.Gettext
end
end
@doc """
When used, dispatch to the appropriate controller/view/etc.
"""
defmacro __using__(which) when is_atom(which) do
apply(__MODULE__, which, [])
end
end
================================================
FILE: mix.exs
================================================
defmodule Boilerplate.Mixfile do
use Mix.Project
def project do
[
app: :boilerplate,
version: "0.0.1",
elixir: "~> 1.6",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps()
]
end
# Configuration for the OTP application.
#
# Type `mix help compile.app` for more information.
def application do
[
mod: {Boilerplate.Application, []},
extra_applications: [:logger, :runtime_tools, :comeonin]
]
end
# Specifies which paths to compile per environment.
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
# Specifies your project dependencies.
#
# Type `mix help deps` for examples and options.
defp deps do
[
{:phoenix, "~> 1.3.4"},
{:phoenix_pubsub, "~> 1.1"},
{:phoenix_ecto, "~> 3.5"},
{:postgrex, ">= 0.0.0"},
{:phoenix_html, "~> 2.12"},
{:phoenix_live_reload, "~> 1.1", only: :dev},
{:gettext, "~> 0.16"},
{:credo, "~> 0.10", only: [:dev, :test], runtime: false},
{:cowboy, "~> 1.1"},
{:comeonin, "~> 4.1"},
{:bcrypt_elixir, "~> 1.1"},
{:plug_cowboy, "~> 1.0"},
{:guardian, "~> 1.1"}
]
end
# Aliases are shortcuts or tasks specific to the current project.
# For example, to create, migrate and run the seeds file at once:
#
# $ mix ecto.setup
#
# See the documentation for `Mix` for more info on aliases.
defp aliases do
[
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"ecto.reset": ["ecto.drop", "ecto.setup"],
test: ["ecto.create --quiet", "ecto.migrate", "test"]
]
end
end
================================================
FILE: phoenix_static_buildpack.config
================================================
node_version=10.12.0
npm_version=6.4.1
assets_path=assets
phoenix_ex=phx
================================================
FILE: priv/gettext/en/LC_MESSAGES/errors.po
================================================
## `msgid`s in this file come from POT (.pot) files.
##
## Do not add, change, or remove `msgid`s manually here as
## they're tied to the ones in the corresponding POT file
## (with the same domain).
##
## Use `mix gettext.extract --merge` or `mix gettext.merge`
## to merge POT files into PO files.
msgid ""
msgstr ""
"Language: en\n"
## From Ecto.Changeset.cast/4
msgid "can't be blank"
msgstr ""
## From Ecto.Changeset.unique_constraint/3
msgid "has already been taken"
msgstr ""
## From Ecto.Changeset.put_change/3
msgid "is invalid"
msgstr ""
## From Ecto.Changeset.validate_acceptance/3
msgid "must be accepted"
msgstr ""
## From Ecto.Changeset.validate_format/3
msgid "has invalid format"
msgstr ""
## From Ecto.Changeset.validate_subset/3
msgid "has an invalid entry"
msgstr ""
## From Ecto.Changeset.validate_exclusion/3
msgid "is reserved"
msgstr ""
## From Ecto.Changeset.validate_confirmation/3
msgid "does not match confirmation"
msgstr ""
## From Ecto.Changeset.no_assoc_constraint/3
msgid "is still associated with this entry"
msgstr ""
msgid "are still associated with this entry"
msgstr ""
## From Ecto.Changeset.validate_length/3
msgid "should be %{count} character(s)"
msgid_plural "should be %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have %{count} item(s)"
msgid_plural "should have %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should be at least %{count} character(s)"
msgid_plural "should be at least %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have at least %{count} item(s)"
msgid_plural "should have at least %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should be at most %{count} character(s)"
msgid_plural "should be at most %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have at most %{count} item(s)"
msgid_plural "should have at most %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
## From Ecto.Changeset.validate_number/3
msgid "must be less than %{number}"
msgstr ""
msgid "must be greater than %{number}"
msgstr ""
msgid "must be less than or equal to %{number}"
msgstr ""
msgid "must be greater than or equal to %{number}"
msgstr ""
msgid "must be equal to %{number}"
msgstr ""
================================================
FILE: priv/gettext/errors.pot
================================================
## This file is a PO Template file.
##
## `msgid`s here are often extracted from source code.
## Add new translations manually only if they're dynamic
## translations that can't be statically extracted.
##
## Run `mix gettext.extract` to bring this file up to
## date. Leave `msgstr`s empty as changing them here as no
## effect: edit them in PO (`.po`) files instead.
## From Ecto.Changeset.cast/4
msgid "can't be blank"
msgstr ""
## From Ecto.Changeset.unique_constraint/3
msgid "has already been taken"
msgstr ""
## From Ecto.Changeset.put_change/3
msgid "is invalid"
msgstr ""
## From Ecto.Changeset.validate_acceptance/3
msgid "must be accepted"
msgstr ""
## From Ecto.Changeset.validate_format/3
msgid "has invalid format"
msgstr ""
## From Ecto.Changeset.validate_subset/3
msgid "has an invalid entry"
msgstr ""
## From Ecto.Changeset.validate_exclusion/3
msgid "is reserved"
msgstr ""
## From Ecto.Changeset.validate_confirmation/3
msgid "does not match confirmation"
msgstr ""
## From Ecto.Changeset.no_assoc_constraint/3
msgid "is still associated with this entry"
msgstr ""
msgid "are still associated with this entry"
msgstr ""
## From Ecto.Changeset.validate_length/3
msgid "should be %{count} character(s)"
msgid_plural "should be %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have %{count} item(s)"
msgid_plural "should have %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should be at least %{count} character(s)"
msgid_plural "should be at least %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have at least %{count} item(s)"
msgid_plural "should have at least %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should be at most %{count} character(s)"
msgid_plural "should be at most %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have at most %{count} item(s)"
msgid_plural "should have at most %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
## From Ecto.Changeset.validate_number/3
msgid "must be less than %{number}"
msgstr ""
msgid "must be greater than %{number}"
msgstr ""
msgid "must be less than or equal to %{number}"
msgstr ""
msgid "must be greater than or equal to %{number}"
msgstr ""
msgid "must be equal to %{number}"
msgstr ""
================================================
FILE: priv/repo/migrations/20170731093912_create_users.exs
================================================
defmodule Boilerplate.Repo.Migrations.CreateUsers do
use Ecto.Migration
def change do
create table(:users) do
add :name, :string, null: false
add :username, :string, null: false
add :email, :string, null: false
add :password_hash, :string, null: false
add :is_staff, :boolean, null: false, default: false
add :is_superuser, :boolean, null: false, default: false
add :last_login, :naive_datetime, default: fragment("now()")
timestamps()
end
create index(:users, ["lower(username)"], unique: true)
create index(:users, ["lower(email)"], unique: true)
# In case if you not using postgres(not quite what we want but still better than nothing):
# create index(:users, :username, unique: true)
# create index(:users, :email, unique: true)
end
end
================================================
FILE: priv/repo/migrations/20170731093913_insert_superuser.exs
================================================
defmodule Boilerplate.Repo.Migrations.CreateUsers do
use Ecto.Migration
def change do
Boilerplate.Accounts.create_user(%{name: "admin", username: "admin", password: "12345678", email: "admin@admin.com", is_superuser: true, is_staff: true}, %{is_superuser: true})
end
end
================================================
FILE: priv/repo/seeds.exs
================================================
# Script for populating the database. You can run it as:
#
# mix run priv/repo/seeds.exs
#
# Inside the script, you can read and write to any of your
# repositories directly:
#
# Boilerplate.Repo.insert!(%Boilerplate.SomeSchema{})
#
# We recommend using the bang functions (`insert!`, `update!`
# and so on) as they will fail if something goes wrong.
================================================
FILE: test/boilerplate/accounts/accounts_test.exs
================================================
defmodule Boilerplate.AccountsTest do
use Boilerplate.DataCase
alias Boilerplate.Accounts
describe "users" do
alias Boilerplate.Accounts.User
@valid_attrs %{
name: "Test Name",
email: "test@test.com",
password: "password",
username: "username"
}
@update_attrs %{
name: "Updated Name",
email: "test_update@test.com",
password: "updated_password",
username: "testusername_updated"
}
@invalid_attrs %{name: nil, email: nil, password_hash: nil, username: nil}
@valid_attrs2 %{
name: "Test Name",
email: "test2@test.com",
password: "password",
username: "UsErNaMe"
}
@valid_attrs3 %{
name: "Test Name",
email: "TeSt@TeSt.com",
password: "password",
username: "username2"
}
def user_fixture(attrs \\ %{}) do
{:ok, user} =
attrs
|> Enum.into(@valid_attrs)
|> Accounts.create_user()
user
end
defp usermap(user), do: Map.drop(user, [:last_login, :password])
defp is_the_same_users([], []), do: true
defp is_the_same_users([user | t], [user2 | t2]) do
user = usermap(user)
user2 = usermap(user2)
# assert
assert user == user2
is_the_same_users(t, t2)
end
defp is_the_same_users(user, user2) do
is_the_same_users([user], [user2])
end
test "list_users/0 returns all users" do
user = user_fixture()
admin = Accounts.get_user_by_username("admin")
is_the_same_users(Accounts.list_users(), [admin, user])
end
test "get_user_by_username/1 returns the user with given username" do
user = user_fixture()
user2 = Accounts.get_user_by_username(user.username)
is_the_same_users(user, user2)
end
test "get_user_by_username/1 returns the user with given username but different case" do
user = user_fixture()
user2 = Accounts.get_user_by_username("UsErNaMe")
is_the_same_users(user, user2)
end
test "get_user!/1 returns the user with given id" do
user = user_fixture()
user2 = Accounts.get_user!(user.id)
is_the_same_users(user, user2)
end
test "create_user/1 with valid data creates a user" do
assert {:ok, %User{} = user} = Accounts.create_user(@valid_attrs)
assert user.name == @valid_attrs.name
assert user.email == @valid_attrs.email
assert user.username == @valid_attrs.username
end
test "create_user/1 with valid data and username has already been taken returns error changeset" do
assert {:ok, %User{} = user} = Accounts.create_user(@valid_attrs)
assert user.name == @valid_attrs.name
assert user.email == @valid_attrs.email
assert user.username == @valid_attrs.username
assert {:error, :already_taken_username} = Accounts.create_user(@valid_attrs2)
end
test "create_user/1 with valid data and email has already been taken returns error changeset" do
assert {:ok, %User{} = user} = Accounts.create_user(@valid_attrs)
assert user.name == @valid_attrs.name
assert user.email == @valid_attrs.email
assert user.username == @valid_attrs.username
assert {:error, :already_taken_email} = Accounts.create_user(@valid_attrs3)
end
test "create_user/1 with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = Accounts.create_user(@invalid_attrs)
end
test "update_user/2 with valid data updates the user" do
user = user_fixture()
assert {:ok, user} = Accounts.update_user(user, @update_attrs, user)
assert %User{} = user
assert user.email == @update_attrs.email
assert user.name == @update_attrs.name
assert user.username == @update_attrs.username
end
test "update_user/2 not updates current_user rights if user is not admin" do
user = user_fixture()
assert {:ok, user} = Accounts.update_user(user, %{is_staff: true, is_superuser: true}, user)
assert %User{} = user
assert user.is_staff == false
assert user.is_superuser == false
end
test "update_user/2 updates current_user rights if user is admin" do
user = user_fixture()
admin = Accounts.get_user_by_username("admin")
assert {:ok, user} =
Accounts.update_user(user, %{is_staff: true, is_superuser: true}, admin)
assert %User{} = user
assert user.is_staff == true
assert user.is_superuser == true
end
test "update_user/2 with invalid data returns error changeset" do
user = user_fixture()
assert {:ok, user} = Accounts.update_user(user, @update_attrs, user)
assert {:error, %Ecto.Changeset{}} = Accounts.update_user(user, @invalid_attrs, user)
user_u = Accounts.get_user!(user.id)
is_the_same_users(user_u, user)
end
test "delete_user/1 deletes the user" do
user = user_fixture()
assert {:ok, %User{}} = Accounts.delete_user(user, user)
assert_raise Ecto.NoResultsError, fn -> Accounts.get_user!(user.id) end
end
test "change_user/1 returns a user changeset" do
user = user_fixture()
assert %Ecto.Changeset{} = Accounts.change_user(user)
end
end
end
================================================
FILE: test/boilerplate_web/controllers/page_controller_test.exs
================================================
defmodule BoilerplateWeb.PageControllerTest do
use BoilerplateWeb.ConnCase
test "GET /", %{conn: conn} do
conn = get(conn, "/")
assert html_response(conn, 200)
end
end
================================================
FILE: test/boilerplate_web/controllers/user_controller_test.exs
================================================
defmodule BoilerplateWeb.UserControllerTest do
use BoilerplateWeb.ConnCase
alias Boilerplate.Accounts
alias Boilerplate.Accounts.User
@create_attrs %{
name: "Test Name",
email: "test@test.com",
password: "password",
username: "username"
}
@create_attrs2 %{
name: "Test Name",
email: "test2@test.com",
password: "password",
username: "username2"
}
@update_attrs %{
name: "Updated Name",
email: "test_update@test.com",
password: "updated_password",
username: "testusername_updated"
}
@invalid_attrs %{name: nil, email: nil, password_hash: nil, username: nil}
defp usermap(user), do: Map.drop(user, [:last_login, :password, "last_login", "password"])
defp is_the_same_users([], []), do: true
defp is_the_same_users([user | t], [user2 | t2]) do
user = usermap(user)
user2 = usermap(user2)
# assert
assert user == user2
is_the_same_users(t, t2)
end
defp is_the_same_users(user, user2) do
is_the_same_users([user], [user2])
end
def fixture(:user) do
{:ok, user} = Accounts.create_user(@create_attrs)
user
end
setup %{conn: conn} do
{:ok, conn: put_req_header(conn, "accept", "application/json")}
end
describe "index users" do
test "lists all users", %{conn: conn} do
t = Accounts.get_user_by_username("admin")
admin = %{
"id" => t.id,
"name" => t.name,
"username" => t.username,
"email" => t.email,
"is_staff" => t.is_staff,
"is_superuser" => t.is_superuser
}
conn = Boilerplate.Accounts.sign_in_user(conn, t)
conn = get(conn, user_path(conn, :index))
assert %{"users" => users} = json_response(conn, 200)["data"]
is_the_same_users(users, [admin])
end
test "list should not be showed for unauthorized user", %{conn: conn} do
conn = get(conn, user_path(conn, :index))
assert json_response(conn, 401)["errors"]
end
end
describe "show user" do
test "show user", %{conn: conn} do
t = Accounts.get_user_by_username("admin")
admin = %{
"id" => t.id,
"name" => t.name,
"username" => t.username,
"email" => t.email,
"is_staff" => t.is_staff,
"is_superuser" => t.is_superuser
}
conn = Boilerplate.Accounts.sign_in_user(conn, t)
conn = get(conn, user_path(conn, :show, t.id, %{"username" => "admin"}))
assert user = json_response(conn, 200)["data"]
is_the_same_users(user, admin)
end
test "list should not be showed for unauthorized user", %{conn: conn} do
conn = get(conn, user_path(conn, :show, 1, %{"username" => "admin"}))
assert json_response(conn, 401)["errors"]
end
end
describe "create user" do
test "renders user when data is valid", %{conn: conn} do
conn = post(conn, user_path(conn, :create), @create_attrs)
assert %{"id" => id} = json_response(conn, 201)["data"]
conn = get(conn, user_path(conn, :show, id, %{"username" => @create_attrs.username}))
assert data = json_response(conn, 200)["data"]
assert Map.drop(data, ["last_login"]) == %{
"id" => id,
"name" => @create_attrs.name,
"username" => @create_attrs.username
}
end
test "renders errors when data is invalid", %{conn: conn} do
conn = post(conn, user_path(conn, :create), @invalid_attrs)
assert json_response(conn, 422)["errors"] != %{}
end
end
describe "update user" do
setup [:create_user]
test "renders user when data is valid and users are the same", %{
conn: conn,
user: %User{id: id} = user
} do
conn = Boilerplate.Accounts.sign_in_user(conn, user)
new_conn = put(conn, user_path(conn, :update, user), user: @update_attrs)
assert %{"id" => ^id} = json_response(new_conn, 200)["data"]
conn = get(conn, user_path(conn, :show, id, %{"username" => @update_attrs.username}))
assert data = json_response(conn, 200)["data"]
assert Map.drop(data, ["last_login"]) == %{
"id" => id,
"name" => @update_attrs.name,
"username" => @update_attrs.username
}
# "email" => @update_attrs.email,
end
test "renders user when data is valid and admin is changing", %{
conn: conn,
user: %User{id: id} = user
} do
admin = Accounts.get_user_by_username("admin")
conn = Boilerplate.Accounts.sign_in_user(conn, admin)
new_conn = put(conn, user_path(conn, :update, user), user: @update_attrs)
assert %{"id" => ^id} = json_response(new_conn, 200)["data"]
conn = get(conn, user_path(conn, :show, id, %{"username" => @update_attrs.username}))
assert data = json_response(conn, 200)["data"]
assert Map.drop(data, ["last_login"]) == %{
"id" => id,
"name" => @update_attrs.name,
"username" => @update_attrs.username,
"email" => @update_attrs.email,
"is_staff" => false,
"is_superuser" => false
}
end
test "not renders user when data is valid and users are not the same", %{
conn: conn,
user: %User{id: id} = user
} do
{:ok, %User{} = user2} = Accounts.create_user(@create_attrs2)
conn = Boilerplate.Accounts.sign_in_user(conn, user2)
new_conn = put(conn, user_path(conn, :update, user), user: @update_attrs)
assert json_response(new_conn, 403)
conn = get(conn, user_path(conn, :show, id, %{"username" => @create_attrs.username}))
assert data = json_response(conn, 200)["data"]
assert Map.drop(data, ["last_login"]) == %{
"id" => id,
"name" => @create_attrs.name,
"username" => @create_attrs.username
}
# "email" => @update_attrs.email,
end
test "not renders user when data is valid and user are not authorised", %{
conn: conn,
user: %User{id: id} = user
} do
new_conn = put(conn, user_path(conn, :update, user), user: @update_attrs)
assert json_response(new_conn, 401)["errors"]
conn = Boilerplate.Accounts.sign_in_user(conn, user)
conn = get(conn, user_path(conn, :show, id, %{"username" => @create_attrs.username}))
assert data = json_response(conn, 200)["data"]
assert Map.drop(data, ["last_login"]) == %{
"id" => id,
"name" => @create_attrs.name,
"username" => @create_attrs.username
}
# "email" => @update_attrs.email,
end
test "renders errors when data is invalid", %{conn: conn, user: user} do
conn = Boilerplate.Accounts.sign_in_user(conn, user)
conn = put(conn, user_path(conn, :update, user), user: @invalid_attrs)
assert json_response(conn, 422)["errors"] != %{}
end
end
describe "delete user" do
setup [:create_user]
test "deletes chosen user when users are the same", %{conn: conn, user: user} do
new_conn = Boilerplate.Accounts.sign_in_user(conn, user)
new_conn = delete(new_conn, user_path(new_conn, :delete, user.id))
assert response(new_conn, 204)
admin = Accounts.get_user_by_username("admin")
conn = Boilerplate.Accounts.sign_in_user(conn, admin)
conn = get(conn, user_path(conn, :show, user.id, %{"username" => user.username}))
assert response(conn, 404)
end
test "deletes chosen user when admin is deleting", %{conn: conn, user: user} do
admin = Accounts.get_user_by_username("admin")
new_conn = Boilerplate.Accounts.sign_in_user(conn, admin)
new_conn = delete(new_conn, user_path(new_conn, :delete, user.id))
assert response(new_conn, 204)
conn = Boilerplate.Accounts.sign_in_user(conn, admin)
conn = get(conn, user_path(conn, :show, user.id, %{"username" => user.username}))
assert response(conn, 404)
end
test "not deletes chosen user when users are not the same", %{conn: conn, user: user} do
{:ok, %User{} = user2} = Accounts.create_user(@create_attrs2)
new_conn = Boilerplate.Accounts.sign_in_user(conn, user2)
new_conn = delete(new_conn, user_path(new_conn, :delete, user.id))
assert response(new_conn, 403)
admin = Accounts.get_user_by_username("admin")
conn = Boilerplate.Accounts.sign_in_user(conn, admin)
conn = get(conn, user_path(conn, :show, user.id, %{"username" => user.username}))
assert response(conn, 200)
end
test "not deletes chosen user when user are not authorised", %{conn: conn, user: user} do
new_conn = delete(conn, user_path(conn, :delete, user.id))
assert json_response(new_conn, 401)["errors"]
admin = Accounts.get_user_by_username("admin")
conn = Boilerplate.Accounts.sign_in_user(conn, admin)
conn = get(conn, user_path(conn, :show, user.id, %{"username" => user.username}))
assert response(conn, 200)
end
end
defp create_user(_) do
user = fixture(:user)
{:ok, user: user}
end
end
================================================
FILE: test/boilerplate_web/views/error_view_test.exs
================================================
defmodule BoilerplateWeb.ErrorViewTest do
use BoilerplateWeb.ConnCase, async: true
# Bring render/3 and render_to_string/3 for testing custom views
import Phoenix.View
test "renders 404.html" do
assert render_to_string(BoilerplateWeb.ErrorView, "404.html", []) == "Page not found"
end
test "renders 500.html" do
assert render_to_string(BoilerplateWeb.ErrorView, "500.html", []) == "Internal server error"
end
test "render any other" do
assert render_to_string(BoilerplateWeb.ErrorView, "505.html", []) == "Internal server error"
end
end
================================================
FILE: test/boilerplate_web/views/layout_view_test.exs
================================================
defmodule BoilerplateWeb.LayoutViewTest do
use BoilerplateWeb.ConnCase, async: true
end
================================================
FILE: test/boilerplate_web/views/page_view_test.exs
================================================
defmodule BoilerplateWeb.PageViewTest do
use BoilerplateWeb.ConnCase, async: true
end
================================================
FILE: test/support/channel_case.ex
================================================
defmodule BoilerplateWeb.ChannelCase do
@moduledoc """
This module defines the test case to be used by
channel tests.
Such tests rely on `Phoenix.ChannelTest` and also
import other functionality to make it easier
to build common datastructures and query the data layer.
Finally, if the test case interacts with the database,
it cannot be async. For this reason, every test runs
inside a transaction which is reset at the beginning
of the test unless the test case is marked as async.
"""
use ExUnit.CaseTemplate
using do
quote do
# Import conveniences for testing with channels
use Phoenix.ChannelTest
# The default endpoint for testing
@endpoint BoilerplateWeb.Endpoint
end
end
setup tags do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Boilerplate.Repo)
unless tags[:async] do
Ecto.Adapters.SQL.Sandbox.mode(Boilerplate.Repo, {:shared, self()})
end
:ok
end
end
================================================
FILE: test/support/conn_case.ex
================================================
defmodule BoilerplateWeb.ConnCase do
@moduledoc """
This module defines the test case to be used by
tests that require setting up a connection.
Such tests rely on `Phoenix.ConnTest` and also
import other functionality to make it easier
to build common datastructures and query the data layer.
Finally, if the test case interacts with the database,
it cannot be async. For this reason, every test runs
inside a transaction which is reset at the beginning
of the test unless the test case is marked as async.
"""
use ExUnit.CaseTemplate
using do
quote do
# Import conveniences for testing with connections
use Phoenix.ConnTest
import BoilerplateWeb.Router.Helpers
# The default endpoint for testing
@endpoint BoilerplateWeb.Endpoint
end
end
setup tags do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Boilerplate.Repo)
unless tags[:async] do
Ecto.Adapters.SQL.Sandbox.mode(Boilerplate.Repo, {:shared, self()})
end
{:ok, conn: Phoenix.ConnTest.build_conn()}
end
end
================================================
FILE: test/support/data_case.ex
================================================
defmodule Boilerplate.DataCase do
@moduledoc """
This module defines the setup for tests requiring
access to the application's data layer.
You may define functions here to be used as helpers in
your tests.
Finally, if the test case interacts with the database,
it cannot be async. For this reason, every test runs
inside a transaction which is reset at the beginning
of the test unless the test case is marked as async.
"""
use ExUnit.CaseTemplate
using do
quote do
alias Boilerplate.Repo
import Ecto
import Ecto.Changeset
import Ecto.Query
import Boilerplate.DataCase
end
end
setup tags do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Boilerplate.Repo)
unless tags[:async] do
Ecto.Adapters.SQL.Sandbox.mode(Boilerplate.Repo, {:shared, self()})
end
:ok
end
@doc """
A helper that transform changeset errors to a map of messages.
assert {:error, changeset} = Accounts.create_user(%{password: "short"})
assert "password is too short" in errors_on(changeset).password
assert %{password: ["password is too short"]} = errors_on(changeset)
"""
def errors_on(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
Enum.reduce(opts, message, fn {key, value}, acc ->
String.replace(acc, "%{#{key}}", to_string(value))
end)
end)
end
end
================================================
FILE: test/test_helper.exs
================================================
ExUnit.start()
Ecto.Adapters.SQL.Sandbox.mode(Boilerplate.Repo, :manual)
gitextract_rd6bx7e2/
├── .credo.exs
├── .gitignore
├── LICENSE
├── Procfile
├── README.md
├── assets/
│ ├── .babelrc
│ ├── .eslintrc
│ ├── .travis.yml
│ ├── NERD_tree_1
│ ├── app/
│ │ ├── actions/
│ │ │ ├── errors.js
│ │ │ └── session.js
│ │ ├── assets/
│ │ │ └── scss/
│ │ │ ├── components/
│ │ │ │ ├── app.scss
│ │ │ │ └── sign.scss
│ │ │ ├── index.scss
│ │ │ └── variables.scss
│ │ ├── components/
│ │ │ ├── App/
│ │ │ │ └── index.js
│ │ │ ├── NotFound/
│ │ │ │ └── index.js
│ │ │ ├── Sign/
│ │ │ │ ├── Login/
│ │ │ │ │ ├── LoginForm/
│ │ │ │ │ │ └── index.js
│ │ │ │ │ └── index.js
│ │ │ │ └── Signup/
│ │ │ │ ├── SignupForm/
│ │ │ │ │ └── index.js
│ │ │ │ ├── index.js
│ │ │ │ └── index.test.js
│ │ │ ├── Utils/
│ │ │ │ ├── ErrorMessage/
│ │ │ │ │ └── index.js
│ │ │ │ ├── FormInput/
│ │ │ │ │ └── index.js
│ │ │ │ ├── MatchAuthenticated/
│ │ │ │ │ └── index.js
│ │ │ │ └── RedirectAuthenticated/
│ │ │ │ └── index.js
│ │ │ └── index.js
│ │ ├── config/
│ │ │ ├── CustomRedbox.js
│ │ │ └── Root.js
│ │ ├── index.html
│ │ ├── main.js
│ │ ├── reducers/
│ │ │ ├── errors.js
│ │ │ ├── index.js
│ │ │ └── session.js
│ │ ├── sagas/
│ │ │ ├── index.js
│ │ │ └── session.js
│ │ ├── store/
│ │ │ └── index.js
│ │ ├── utils/
│ │ │ ├── api.js
│ │ │ └── socket.js
│ │ └── vendors/
│ │ └── .gitkeep
│ ├── package.json
│ ├── private/
│ │ └── jest/
│ │ ├── componentsMock.js
│ │ ├── fileMock.js
│ │ ├── setupTests.js
│ │ └── shim.js
│ ├── webpack.config.js
│ └── webpack.production.config.js
├── config/
│ ├── config.exs
│ ├── dev.exs
│ ├── prod.exs
│ └── test.exs
├── elixir_buildpack.config
├── lib/
│ ├── boilerplate/
│ │ ├── accounts/
│ │ │ ├── accounts.ex
│ │ │ └── user.ex
│ │ ├── application.ex
│ │ └── repo.ex
│ ├── boilerplate.ex
│ ├── boilerplate_web/
│ │ ├── channels/
│ │ │ └── user_socket.ex
│ │ ├── controllers/
│ │ │ ├── api/
│ │ │ │ ├── session_controller.ex
│ │ │ │ └── user_controller.ex
│ │ │ ├── auth_error_controller.ex
│ │ │ ├── fallback_controller.ex
│ │ │ └── page_controller.ex
│ │ ├── endpoint.ex
│ │ ├── gettext.ex
│ │ ├── guardian.ex
│ │ ├── router.ex
│ │ ├── templates/
│ │ │ └── layout/
│ │ │ └── app.html.eex
│ │ └── views/
│ │ ├── changeset_view.ex
│ │ ├── error_helpers.ex
│ │ ├── error_view.ex
│ │ ├── layout_view.ex
│ │ ├── page_view.ex
│ │ ├── session_view.ex
│ │ └── user_view.ex
│ └── boilerplate_web.ex
├── mix.exs
├── phoenix_static_buildpack.config
├── priv/
│ ├── gettext/
│ │ ├── en/
│ │ │ └── LC_MESSAGES/
│ │ │ └── errors.po
│ │ └── errors.pot
│ └── repo/
│ ├── migrations/
│ │ ├── 20170731093912_create_users.exs
│ │ └── 20170731093913_insert_superuser.exs
│ └── seeds.exs
└── test/
├── boilerplate/
│ └── accounts/
│ └── accounts_test.exs
├── boilerplate_web/
│ ├── controllers/
│ │ ├── page_controller_test.exs
│ │ └── user_controller_test.exs
│ └── views/
│ ├── error_view_test.exs
│ ├── layout_view_test.exs
│ └── page_view_test.exs
├── support/
│ ├── channel_case.ex
│ ├── conn_case.ex
│ └── data_case.ex
└── test_helper.exs
SYMBOL INDEX (175 symbols across 50 files)
FILE: assets/app/components/App/index.js
class App (line 9) | class App extends Component {
method render (line 10) | render() {
FILE: assets/app/components/NotFound/index.js
class NotFound (line 3) | class NotFound extends Component {
method render (line 4) | render() {
FILE: assets/app/components/Sign/Login/LoginForm/index.js
class LoginForm (line 9) | class LoginForm extends Component {
method render (line 12) | render() {
FILE: assets/app/components/Sign/Login/index.js
class Login (line 8) | class Login extends Component {
method render (line 9) | render() {
FILE: assets/app/components/Sign/Signup/SignupForm/index.js
class SignupForm (line 9) | class SignupForm extends Component {
method render (line 12) | render() {
FILE: assets/app/components/Sign/Signup/index.js
class Signup (line 8) | class Signup extends Component {
method render (line 9) | render() {
FILE: assets/app/components/Utils/ErrorMessage/index.js
class ErrorMessage (line 6) | class ErrorMessage extends Component {
method constructor (line 7) | constructor(props) {
method componentDidUpdate (line 13) | componentDidUpdate(prevProps) {
method addNotification (line 24) | addNotification(message) {
method render (line 32) | render() {
FILE: assets/app/components/Utils/FormInput/index.js
class Input (line 4) | class Input extends Component {
method render (line 5) | render() {
FILE: assets/app/components/Utils/MatchAuthenticated/index.js
class MatchAuthenticated (line 6) | class MatchAuthenticated extends Component {
method render (line 7) | render() {
FILE: assets/app/components/Utils/RedirectAuthenticated/index.js
class RedirectAuthenticated (line 6) | class RedirectAuthenticated extends Component {
method render (line 7) | render() {
FILE: assets/app/config/CustomRedbox.js
class CustomRedbox (line 44) | class CustomRedbox extends React.Component {
method render (line 45) | render() {
FILE: assets/app/config/Root.js
class Root (line 20) | class Root extends Component {
method componentDidMount (line 21) | componentDidMount() {
method render (line 31) | render() {
FILE: assets/app/sagas/session.js
function setCurrentUser (line 10) | function setCurrentUser(response) {
function login (line 17) | function login(data) {
function signup (line 41) | function signup(data) {
function logout (line 68) | function logout() {
function authenticate (line 83) | function authenticate() {
FILE: assets/app/store/index.js
function configureStore (line 19) | function configureStore(browserHistory) {
FILE: assets/app/utils/api.js
constant API (line 3) | const API = '/api';
function headers (line 5) | function headers() {
function queryString (line 15) | function queryString(params) {
method fetch (line 23) | fetch(url, params = {}) {
method post (line 27) | post(url, data) {
method patch (line 32) | patch(url, data) {
method delete (line 36) | delete(url) {
FILE: lib/boilerplate.ex
class Boilerplate (line 1) | defmodule Boilerplate
FILE: lib/boilerplate/accounts/accounts.ex
class Boilerplate.Accounts (line 1) | defmodule Boilerplate.Accounts
method list_users (line 20) | def list_users do
method get_user! (line 38) | def get_user!(id), do: Repo.get!(User, id)
method get_user_by_username (line 40) | def get_user_by_username(username) do
method create_user (line 59) | def create_user(attrs \\ %{}, superuser \\ %{}) do
method update_user (line 85) | def update_user(%User{} = user, attrs, current_user \\ %{}) do
method delete_user (line 108) | def delete_user(%User{} = user, %User{} = current_user) do
method change_user (line 125) | def change_user(%User{} = user) do
method is_exist_username (line 129) | defp is_exist_username(attrs) do
method is_exist_email (line 144) | defp is_exist_email(attrs) do
method get_current_token (line 156) | def get_current_token(conn) do
method sign_out (line 160) | def sign_out(conn) do
method get_claims (line 164) | def get_claims(conn) do
method refresh_token (line 168) | def refresh_token(jwt) do
method get_current_user (line 172) | def get_current_user(conn) do
method sign_in_user (line 176) | def sign_in_user(conn, user) do
method authenticate (line 180) | def authenticate(%{"email" => email, "password" => password}) do
method update_last_login (line 192) | def update_last_login(%User{} = user) do
method check_password (line 200) | defp check_password(user, password) do
method is_superuser? (line 207) | defp is_superuser?(user) do
method is_the_same_users? (line 211) | def is_the_same_users?(user, current_user) do
method match_superuser_changeset (line 217) | defp match_superuser_changeset(%User{} = user, attrs, superuser) do
method match_superuser_registration_changeset (line 225) | defp match_superuser_registration_changeset(%User{} = user, attrs, sup...
method is_superuser_or_same_user? (line 233) | defp is_superuser_or_same_user?(user, current_user) do
FILE: lib/boilerplate/accounts/user.ex
class Boilerplate.Accounts.User (line 1) | defmodule Boilerplate.Accounts.User
method changeset (line 20) | def changeset(%User{} = user, attrs) do
method superuser_changeset (line 32) | def superuser_changeset(%User{} = user, attrs) do
method registration_changeset (line 39) | def registration_changeset(%User{} = user, attrs) do
method superuser_registration_changeset (line 48) | def superuser_registration_changeset(%User{} = user, attrs) do
method last_login_changeset (line 57) | def last_login_changeset(%User{} = user, attrs) do
method put_password_hash (line 63) | defp put_password_hash(changeset) do
FILE: lib/boilerplate/application.ex
class Boilerplate.Application (line 1) | defmodule Boilerplate.Application
method start (line 6) | def start(_type, _args) do
method config_change (line 27) | def config_change(changed, _new, removed) do
FILE: lib/boilerplate/repo.ex
class Boilerplate.Repo (line 1) | defmodule Boilerplate.Repo
method init (line 8) | def init(_, opts) do
FILE: lib/boilerplate_web.ex
class BoilerplateWeb (line 1) | defmodule BoilerplateWeb
method controller (line 20) | def controller do
method view (line 29) | def view do
method router (line 47) | def router do
method channel (line 55) | def channel do
FILE: lib/boilerplate_web/channels/user_socket.ex
class BoilerplateWeb.UserSocket (line 1) | defmodule BoilerplateWeb.UserSocket
method connect (line 22) | def connect(_params, socket) do
method id (line 36) | def id(_socket), do: nil
FILE: lib/boilerplate_web/controllers/api/session_controller.ex
class BoilerplateWeb.SessionController (line 1) | defmodule BoilerplateWeb.SessionController
method create (line 8) | def create(conn, params) do
method delete (line 21) | def delete(conn, _) do
method refresh (line 33) | def refresh(conn, _params) do
FILE: lib/boilerplate_web/controllers/api/user_controller.ex
class BoilerplateWeb.UserController (line 1) | defmodule BoilerplateWeb.UserController
method index (line 9) | def index(conn, _) do
method show (line 16) | def show(conn, %{"username" => username}) do
method update (line 23) | def update(conn, %{"id" => id, "user" => user_params}) do
method create (line 32) | def create(conn, user_params) do
method delete (line 43) | def delete(conn, %{"id" => id}) do
FILE: lib/boilerplate_web/controllers/auth_error_controller.ex
class BoilerplateWeb.AuthErrorController (line 1) | defmodule BoilerplateWeb.AuthErrorController
method auth_error (line 5) | def auth_error(conn, {_type, _reason}, _opts) do
FILE: lib/boilerplate_web/controllers/fallback_controller.ex
class BoilerplateWeb.FallbackController (line 1) | defmodule BoilerplateWeb.FallbackController
method call (line 9) | def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
method call (line 15) | def call(conn, {:error, :not_found}) do
method call (line 21) | def call(conn, {:error, :wrong_credentials}) do
method call (line 27) | def call(conn, {:error, :no_session}) do
method call (line 33) | def call(conn, {:error, :invalid_issuer}) do
method call (line 39) | def call(conn, {:error, :already_taken_username}) do
method call (line 45) | def call(conn, {:error, :already_taken_email}) do
method call (line 51) | def call(conn, {:error, "Unknown resource type"}) do
method call (line 57) | def call(conn, {:error, :user_not_found}) do
method call (line 63) | def call(conn, {:error, :forbidden}) do
FILE: lib/boilerplate_web/controllers/page_controller.ex
class BoilerplateWeb.PageController (line 1) | defmodule BoilerplateWeb.PageController
method index (line 4) | def index(conn, _params) do
FILE: lib/boilerplate_web/endpoint.ex
class BoilerplateWeb.Endpoint (line 1) | defmodule BoilerplateWeb.Endpoint
method init (line 56) | def init(_key, config) do
FILE: lib/boilerplate_web/gettext.ex
class BoilerplateWeb.Gettext (line 1) | defmodule BoilerplateWeb.Gettext
FILE: lib/boilerplate_web/guardian.ex
class BoilerplateWeb.Guardian (line 1) | defmodule BoilerplateWeb.Guardian
method subject_for_token (line 4) | def subject_for_token(resource, _claims) do
method resource_from_claims (line 14) | def resource_from_claims(claims) do
FILE: lib/boilerplate_web/router.ex
class BoilerplateWeb.Router (line 1) | defmodule BoilerplateWeb.Router
FILE: lib/boilerplate_web/views/changeset_view.ex
class BoilerplateWeb.ChangesetView (line 1) | defmodule BoilerplateWeb.ChangesetView
method translate_errors (line 10) | def translate_errors(changeset) do
method render (line 14) | def render("error.json", %{changeset: changeset}) do
FILE: lib/boilerplate_web/views/error_helpers.ex
class BoilerplateWeb.ErrorHelpers (line 1) | defmodule BoilerplateWeb.ErrorHelpers
method error_tag (line 11) | def error_tag(form, field) do
method translate_error (line 20) | def translate_error({msg, opts}) do
FILE: lib/boilerplate_web/views/error_view.ex
class BoilerplateWeb.ErrorView (line 1) | defmodule BoilerplateWeb.ErrorView
method render (line 4) | def render("permission_denied", _assigns) do
method render (line 8) | def render("404.html", _assigns) do
method render (line 12) | def render("500.html", _assigns) do
method template_not_found (line 18) | def template_not_found(_template, assigns) do
FILE: lib/boilerplate_web/views/layout_view.ex
class BoilerplateWeb.LayoutView (line 1) | defmodule BoilerplateWeb.LayoutView
FILE: lib/boilerplate_web/views/page_view.ex
class BoilerplateWeb.PageView (line 1) | defmodule BoilerplateWeb.PageView
FILE: lib/boilerplate_web/views/session_view.ex
class BoilerplateWeb.SessionView (line 1) | defmodule BoilerplateWeb.SessionView
method render (line 5) | def render("show.json", %{user: user, jwt: jwt}) do
method render (line 9) | def render("delete.json", _) do
method render (line 13) | def render("no_session.json", _) do
method render (line 17) | def render("wrong_credentials.json", _) do
method render (line 21) | def render("invalid_issuer.json", _) do
method render (line 25) | def render("already_taken_username.json", _) do
method render (line 29) | def render("already_taken_email.json", _) do
method render (line 33) | def render("invalid_token.json", _) do
FILE: lib/boilerplate_web/views/user_view.ex
class BoilerplateWeb.UserView (line 1) | defmodule BoilerplateWeb.UserView
method render (line 5) | def render("index.json", %{users: users, current_user: %{is_staff: tru...
method render (line 13) | def render("index.json", %{users: users}) do
method render (line 21) | def render("show.json", %{user: user, current_user: %{is_staff: true}}...
method render (line 25) | def render("show.json", %{user: user}) do
method render (line 29) | def render("user.json", %{user: user}) do
method render (line 41) | def render("show_user.json", %{user: user}) do
method render (line 46) | def render("404.json", _assigns) do
FILE: mix.exs
class Boilerplate.Mixfile (line 1) | defmodule Boilerplate.Mixfile
method project (line 4) | def project do
method application (line 20) | def application do
method elixirc_paths (line 28) | defp elixirc_paths(:test), do: ["lib", "test/support"]
method elixirc_paths (line 29) | defp elixirc_paths(_), do: ["lib"]
method deps (line 34) | defp deps do
method aliases (line 58) | defp aliases do
FILE: priv/repo/migrations/20170731093912_create_users.exs
class Boilerplate.Repo.Migrations.CreateUsers (line 1) | defmodule Boilerplate.Repo.Migrations.CreateUsers
method change (line 4) | def change do
FILE: priv/repo/migrations/20170731093913_insert_superuser.exs
class Boilerplate.Repo.Migrations.CreateUsers (line 1) | defmodule Boilerplate.Repo.Migrations.CreateUsers
method change (line 4) | def change do
FILE: test/boilerplate/accounts/accounts_test.exs
class Boilerplate.AccountsTest (line 1) | defmodule Boilerplate.AccountsTest
FILE: test/boilerplate_web/controllers/page_controller_test.exs
class BoilerplateWeb.PageControllerTest (line 1) | defmodule BoilerplateWeb.PageControllerTest
FILE: test/boilerplate_web/controllers/user_controller_test.exs
class BoilerplateWeb.UserControllerTest (line 1) | defmodule BoilerplateWeb.UserControllerTest
method usermap (line 27) | defp usermap(user), do: Map.drop(user, [:last_login, :password, "last_...
method is_the_same_users (line 29) | defp is_the_same_users([], []), do: true
method is_the_same_users (line 31) | defp is_the_same_users([user | t], [user2 | t2]) do
method is_the_same_users (line 39) | defp is_the_same_users(user, user2) do
method fixture (line 43) | def fixture(:user) do
method create_user (line 264) | defp create_user(_) do
FILE: test/boilerplate_web/views/error_view_test.exs
class BoilerplateWeb.ErrorViewTest (line 1) | defmodule BoilerplateWeb.ErrorViewTest
FILE: test/boilerplate_web/views/layout_view_test.exs
class BoilerplateWeb.LayoutViewTest (line 1) | defmodule BoilerplateWeb.LayoutViewTest
FILE: test/boilerplate_web/views/page_view_test.exs
class BoilerplateWeb.PageViewTest (line 1) | defmodule BoilerplateWeb.PageViewTest
FILE: test/support/channel_case.ex
class BoilerplateWeb.ChannelCase (line 1) | defmodule BoilerplateWeb.ChannelCase
FILE: test/support/conn_case.ex
class BoilerplateWeb.ConnCase (line 1) | defmodule BoilerplateWeb.ConnCase
FILE: test/support/data_case.ex
class Boilerplate.DataCase (line 1) | defmodule Boilerplate.DataCase
method errors_on (line 46) | def errors_on(changeset) do
Condensed preview — 93 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (132K chars).
[
{
"path": ".credo.exs",
"chars": 4491,
"preview": "# This file contains the configuration for Credo and you are probably reading\n# this after creating it with `mix credo.g"
},
{
"path": ".gitignore",
"chars": 707,
"preview": "# App artifacts\n/_build\n/db\n/deps\n/*.ez\n\n# Generated on crash by the VM\nerl_crash.dump\n\n# Since we are building assets f"
},
{
"path": "LICENSE",
"chars": 1075,
"preview": "MIT License\n\nCopyright (c) 2017 Dmitriy Chernyshov\n\nPermission is hereby granted, free of charge, to any person obtainin"
},
{
"path": "Procfile",
"chars": 33,
"preview": "web: MIX_ENV=prod mix phx.server\n"
},
{
"path": "README.md",
"chars": 5294,
"preview": "# React + Phoenix boilerplate\n\nDemo: http://phxboilerplate.herokuapp.com\n\nThis is a basic setup for an React(16) + Phoen"
},
{
"path": "assets/.babelrc",
"chars": 1228,
"preview": "{\r\n \"presets\": [\r\n \"@babel/preset-env\",\r\n \"@babel/preset-react\",\r\n ],\r\n\r\n \"plugins\": [\r\n // Stage 0\r\n "
},
{
"path": "assets/.eslintrc",
"chars": 902,
"preview": "{\r\n \"parser\": \"babel-eslint\",\r\n \"plugins\": [\"react\", \"import\"],\r\n \"env\": {\r\n \"browser\": true,\r\n \"jest\": true,\r\n"
},
{
"path": "assets/.travis.yml",
"chars": 318,
"preview": "sudo: false\nlanguage: node_js\ncache:\n directories:\n - node_modules\nbranches:\n only:\n - master\n - /^greenk"
},
{
"path": "assets/NERD_tree_1",
"chars": 212,
"preview": "@babel/plugin-transform-modules-commonjs @babel/plugin-proposal-export-default-from @babel/plugin-proposal-export-namesp"
},
{
"path": "assets/app/actions/errors.js",
"chars": 133,
"preview": "export const types = {\n NEW_ERROR: 'ERRORS/NEW_ERROR',\n};\n\nexport const newError = message => ({ type: types.NEW_ERROR,"
},
{
"path": "assets/app/actions/session.js",
"chars": 667,
"preview": "export const types = {\n LOGIN_REQUEST: 'SESSION/LOGIN_REQUEST',\n SIGNUP_REQUEST: 'SESSION/SIGNUP_REQUEST',\n LOGOUT: '"
},
{
"path": "assets/app/assets/scss/components/app.scss",
"chars": 1518,
"preview": ".app{\n padding-top: 50px;\n\n form, ul, table {\n margin-top: 20px;\n margin-bottom: 20px;\n }\n \n .username {\n "
},
{
"path": "assets/app/assets/scss/components/sign.scss",
"chars": 1506,
"preview": ".login, .signup {\n height: 100%;\n width: 100%;\n background: $grey;\n padding-top: 40px;\n\n .input {\n position: rel"
},
{
"path": "assets/app/assets/scss/index.scss",
"chars": 198,
"preview": "@import 'variables.scss';\n@import 'components/sign.scss';\n@import 'components/app.scss';\n\n\nbody, html, #root, .full-heig"
},
{
"path": "assets/app/assets/scss/variables.scss",
"chars": 58,
"preview": "$grey: #EEEEEE;\n$error: #e53935;\n$primary_color: #F05423;\n"
},
{
"path": "assets/app/components/App/index.js",
"chars": 3002,
"preview": "import React, { Component } from 'react';\r\nimport { connect } from 'react-redux';\r\nimport PropTypes from 'prop-types';\r\n"
},
{
"path": "assets/app/components/NotFound/index.js",
"chars": 183,
"preview": "import React, { Component } from 'react';\r\n\r\nexport default class NotFound extends Component {\r\n render() {\r\n return"
},
{
"path": "assets/app/components/Sign/Login/LoginForm/index.js",
"chars": 2071,
"preview": "import React, { Component } from 'react';\nimport { Field, reduxForm } from 'redux-form';\nimport { Link } from 'react-rou"
},
{
"path": "assets/app/components/Sign/Login/index.js",
"chars": 796,
"preview": "import React, { Component } from 'react';\nimport { connect } from 'react-redux';\nimport PropTypes from 'prop-types';\nimp"
},
{
"path": "assets/app/components/Sign/Signup/SignupForm/index.js",
"chars": 3041,
"preview": "import React, { Component } from 'react';\nimport { Field, reduxForm } from 'redux-form';\nimport { Link } from 'react-rou"
},
{
"path": "assets/app/components/Sign/Signup/index.js",
"chars": 803,
"preview": "import React, { Component } from 'react';\nimport { connect } from 'react-redux';\nimport PropTypes from 'prop-types';\nimp"
},
{
"path": "assets/app/components/Sign/Signup/index.test.js",
"chars": 238,
"preview": "import React from 'react';\nimport { shallow } from 'enzyme';\nimport { Signup } from '.';\n\ndescribe('Signup', () => {\n i"
},
{
"path": "assets/app/components/Utils/ErrorMessage/index.js",
"chars": 1195,
"preview": "import React, { Component } from 'react';\nimport { connect } from 'react-redux';\nimport PropTypes from 'prop-types';\nimp"
},
{
"path": "assets/app/components/Utils/FormInput/index.js",
"chars": 913,
"preview": "import React, { Component } from 'react';\nimport PropTypes from 'prop-types';\n\nexport default class Input extends Compon"
},
{
"path": "assets/app/components/Utils/MatchAuthenticated/index.js",
"chars": 1077,
"preview": "import React, { Component } from 'react';\nimport { Route, Redirect } from 'react-router';\nimport PropTypes from 'prop-ty"
},
{
"path": "assets/app/components/Utils/RedirectAuthenticated/index.js",
"chars": 1010,
"preview": "import React, { Component } from 'react';\nimport { Route, Redirect } from 'react-router';\nimport PropTypes from 'prop-ty"
},
{
"path": "assets/app/components/index.js",
"chars": 225,
"preview": "const req = require.context('.', true, /[^/]+\\/[^/]+\\/index\\.js$/);\n\nreq.keys().forEach((key) => {\n const componentName"
},
{
"path": "assets/app/config/CustomRedbox.js",
"chars": 1046,
"preview": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport Redbox from 'redbox-react';\n\nconst styles = {\n re"
},
{
"path": "assets/app/config/Root.js",
"chars": 1963,
"preview": "import React, { Component } from 'react';\nimport { connect } from 'react-redux';\nimport { Route, Switch } from 'react-ro"
},
{
"path": "assets/app/index.html",
"chars": 163,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <title>ReactJS Boilerplate</title>\n</head>\n<body>"
},
{
"path": "assets/app/main.js",
"chars": 977,
"preview": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport { AppContainer } from 'react-hot-loader';\nimport { P"
},
{
"path": "assets/app/reducers/errors.js",
"chars": 341,
"preview": "import { types as errorTypes } from 'actions/errors';\n\nconst initialState = {\n errors: [],\n};\n\nexport default function "
},
{
"path": "assets/app/reducers/index.js",
"chars": 544,
"preview": "import { combineReducers } from 'redux';\nimport { routerReducer } from 'react-router-redux';\nimport { reducer as form } "
},
{
"path": "assets/app/reducers/session.js",
"chars": 1092,
"preview": "import { types as sessionTypes } from 'actions/session';\n\nconst initialState = {\n isAuthenticated: false,\n willAuthent"
},
{
"path": "assets/app/sagas/index.js",
"chars": 190,
"preview": "import { fork } from 'redux-saga/effects';\nimport Session from 'sagas/session';\n\nconst sagas = [\n ...Session,\n];\n\nexpor"
},
{
"path": "assets/app/sagas/session.js",
"chars": 2636,
"preview": "import { takeEvery } from 'redux-saga';\nimport { call, put } from 'redux-saga/effects';\nimport { push } from 'react-rout"
},
{
"path": "assets/app/store/index.js",
"chars": 1081,
"preview": "import '@babel/polyfill';\nimport { createStore, applyMiddleware, compose } from 'redux';\nimport { createLogger } from 'r"
},
{
"path": "assets/app/utils/api.js",
"chars": 922,
"preview": "import axios from 'axios';\n\nconst API = '/api';\n\nfunction headers() {\n const token = JSON.parse(localStorage.getItem('t"
},
{
"path": "assets/app/utils/socket.js",
"chars": 2237,
"preview": "/* eslint-disable */\n// NOTE: The contents of this file will only be executed if\n// you uncomment its entry in \"assets/j"
},
{
"path": "assets/app/vendors/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "assets/package.json",
"chars": 4385,
"preview": "{\n \"name\": \"react-webpack-boilerplate\",\n \"version\": \"0.0.1\",\n \"description\": \"Minimalistic ES6 React boilerplate\",\n "
},
{
"path": "assets/private/jest/componentsMock.js",
"chars": 300,
"preview": "import React from 'react'\nimport PropTypes from 'prop-types'\n\nmodule.exports = new Proxy({}, {\n get: (target, property)"
},
{
"path": "assets/private/jest/fileMock.js",
"chars": 22,
"preview": "export default 'file'\n"
},
{
"path": "assets/private/jest/setupTests.js",
"chars": 120,
"preview": "import { configure } from 'enzyme'\nimport Adapter from 'enzyme-adapter-react-16'\n\nconfigure({ adapter: new Adapter() })\n"
},
{
"path": "assets/private/jest/shim.js",
"chars": 102,
"preview": "global.requestAnimationFrame = /* istanbul ignore next */ (callback) => {\n setTimeout(callback, 0)\n}\n"
},
{
"path": "assets/webpack.config.js",
"chars": 5236,
"preview": "const { resolve } = require('path');\r\n\r\nconst publicPath = 'http://localhost:3000/'; \r\nconst webpack = require('webpack'"
},
{
"path": "assets/webpack.production.config.js",
"chars": 3692,
"preview": "const { resolve } = require('path');\nconst webpack = require('webpack');\nconst HtmlWebpackPlugin = require('html-webpack"
},
{
"path": "config/config.exs",
"chars": 1140,
"preview": "# This file is responsible for configuring your application\n# and its dependencies with the aid of the Mix.Config module"
},
{
"path": "config/dev.exs",
"chars": 2350,
"preview": "use Mix.Config\n\n# For development, we disable any cache and enable\n# debugging and code reloading.\n#\n# The watchers conf"
},
{
"path": "config/prod.exs",
"chars": 2678,
"preview": "use Mix.Config\n\n# For production, we often load configuration from external\n# sources, such as your system environment. "
},
{
"path": "config/test.exs",
"chars": 738,
"preview": "use Mix.Config\n\n# We don't run a server during test. If one is required,\n# you can enable the server option below.\nconfi"
},
{
"path": "elixir_buildpack.config",
"chars": 21,
"preview": "elixir_version=1.6.1\n"
},
{
"path": "lib/boilerplate/accounts/accounts.ex",
"chars": 5086,
"preview": "defmodule Boilerplate.Accounts do\n @moduledoc \"\"\"\n The Accounts context.\n \"\"\"\n\n import Ecto.{Query, Changeset}, warn"
},
{
"path": "lib/boilerplate/accounts/user.ex",
"chars": 1921,
"preview": "defmodule Boilerplate.Accounts.User do\n use Ecto.Schema\n import Ecto.Changeset\n alias Boilerplate.Accounts.User\n\n sc"
},
{
"path": "lib/boilerplate/application.ex",
"chars": 1050,
"preview": "defmodule Boilerplate.Application do\n use Application\n\n # See https://hexdocs.pm/elixir/Application.html\n # for more "
},
{
"path": "lib/boilerplate/repo.ex",
"chars": 272,
"preview": "defmodule Boilerplate.Repo do\n use Ecto.Repo, otp_app: :boilerplate\n\n @doc \"\"\"\n Dynamically loads the repository url "
},
{
"path": "lib/boilerplate.ex",
"chars": 259,
"preview": "defmodule Boilerplate do\n @moduledoc \"\"\"\n Boilerplate keeps the contexts that define your domain\n and business logic."
},
{
"path": "lib/boilerplate_web/channels/user_socket.ex",
"chars": 1203,
"preview": "defmodule BoilerplateWeb.UserSocket do\n use Phoenix.Socket\n\n ## Channels\n # channel \"room:*\", BoilerplateWeb.RoomChan"
},
{
"path": "lib/boilerplate_web/controllers/api/session_controller.ex",
"chars": 1147,
"preview": "defmodule BoilerplateWeb.SessionController do\n use BoilerplateWeb, :controller\n\n alias Boilerplate.Accounts\n\n action_"
},
{
"path": "lib/boilerplate_web/controllers/api/user_controller.ex",
"chars": 1536,
"preview": "defmodule BoilerplateWeb.UserController do\n use BoilerplateWeb, :controller\n\n alias Boilerplate.Accounts\n alias Boile"
},
{
"path": "lib/boilerplate_web/controllers/auth_error_controller.ex",
"chars": 273,
"preview": "defmodule BoilerplateWeb.AuthErrorController do\n import Plug.Conn\n use BoilerplateWeb, :controller\n\n def auth_error(c"
},
{
"path": "lib/boilerplate_web/controllers/fallback_controller.ex",
"chars": 1885,
"preview": "defmodule BoilerplateWeb.FallbackController do\n @moduledoc \"\"\"\n Translates controller action results into valid `Plug."
},
{
"path": "lib/boilerplate_web/controllers/page_controller.ex",
"chars": 149,
"preview": "defmodule BoilerplateWeb.PageController do\n use BoilerplateWeb, :controller\n\n def index(conn, _params) do\n render(c"
},
{
"path": "lib/boilerplate_web/endpoint.ex",
"chars": 1708,
"preview": "defmodule BoilerplateWeb.Endpoint do\n use Phoenix.Endpoint, otp_app: :boilerplate\n\n socket(\"/socket\", BoilerplateWeb.U"
},
{
"path": "lib/boilerplate_web/gettext.ex",
"chars": 720,
"preview": "defmodule BoilerplateWeb.Gettext do\n @moduledoc \"\"\"\n A module providing Internationalization with a gettext-based API."
},
{
"path": "lib/boilerplate_web/guardian.ex",
"chars": 810,
"preview": "defmodule BoilerplateWeb.Guardian do\n use Guardian, otp_app: :boilerplate\n\n def subject_for_token(resource, _claims) d"
},
{
"path": "lib/boilerplate_web/router.ex",
"chars": 1252,
"preview": "defmodule BoilerplateWeb.Router do\n use BoilerplateWeb, :router\n\n pipeline :browser do\n plug(:accepts, [\"html\"])\n "
},
{
"path": "lib/boilerplate_web/templates/layout/app.html.eex",
"chars": 833,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <meta http-equiv=\"X-UA-Compatible\" content=\"IE="
},
{
"path": "lib/boilerplate_web/views/changeset_view.ex",
"chars": 570,
"preview": "defmodule BoilerplateWeb.ChangesetView do\n use BoilerplateWeb, :view\n\n @doc \"\"\"\n Traverses and translates changeset e"
},
{
"path": "lib/boilerplate_web/views/error_helpers.ex",
"chars": 1277,
"preview": "defmodule BoilerplateWeb.ErrorHelpers do\n @moduledoc \"\"\"\n Conveniences for translating and building error messages.\n "
},
{
"path": "lib/boilerplate_web/views/error_view.ex",
"chars": 474,
"preview": "defmodule BoilerplateWeb.ErrorView do\n use BoilerplateWeb, :view\n\n def render(\"permission_denied\", _assigns) do\n %{"
},
{
"path": "lib/boilerplate_web/views/layout_view.ex",
"chars": 71,
"preview": "defmodule BoilerplateWeb.LayoutView do\n use BoilerplateWeb, :view\nend\n"
},
{
"path": "lib/boilerplate_web/views/page_view.ex",
"chars": 69,
"preview": "defmodule BoilerplateWeb.PageView do\n use BoilerplateWeb, :view\nend\n"
},
{
"path": "lib/boilerplate_web/views/session_view.ex",
"chars": 935,
"preview": "defmodule BoilerplateWeb.SessionView do\n use BoilerplateWeb, :view\n alias BoilerplateWeb.SessionView\n\n def render(\"sh"
},
{
"path": "lib/boilerplate_web/views/user_view.ex",
"chars": 1192,
"preview": "defmodule BoilerplateWeb.UserView do\n use BoilerplateWeb, :view\n alias BoilerplateWeb.UserView\n\n def render(\"index.js"
},
{
"path": "lib/boilerplate_web.ex",
"chars": 1641,
"preview": "defmodule BoilerplateWeb do\n @moduledoc \"\"\"\n The entrypoint for defining your web interface, such\n as controllers, vi"
},
{
"path": "mix.exs",
"chars": 1797,
"preview": "defmodule Boilerplate.Mixfile do\n use Mix.Project\n\n def project do\n [\n app: :boilerplate,\n version: \"0.0."
},
{
"path": "phoenix_static_buildpack.config",
"chars": 73,
"preview": "node_version=10.12.0\nnpm_version=6.4.1\nassets_path=assets\nphoenix_ex=phx\n"
},
{
"path": "priv/gettext/en/LC_MESSAGES/errors.po",
"chars": 2197,
"preview": "## `msgid`s in this file come from POT (.pot) files.\n##\n## Do not add, change, or remove `msgid`s manually here as\n## th"
},
{
"path": "priv/gettext/errors.pot",
"chars": 2230,
"preview": "## This file is a PO Template file.\n##\n## `msgid`s here are often extracted from source code.\n## Add new translations ma"
},
{
"path": "priv/repo/migrations/20170731093912_create_users.exs",
"chars": 827,
"preview": "defmodule Boilerplate.Repo.Migrations.CreateUsers do\n use Ecto.Migration\n\n def change do\n create table(:users) do\n "
},
{
"path": "priv/repo/migrations/20170731093913_insert_superuser.exs",
"chars": 282,
"preview": "defmodule Boilerplate.Repo.Migrations.CreateUsers do\n use Ecto.Migration\n\n def change do\n Boilerplate.Accounts.crea"
},
{
"path": "priv/repo/seeds.exs",
"chars": 359,
"preview": "# Script for populating the database. You can run it as:\n#\n# mix run priv/repo/seeds.exs\n#\n# Inside the script, you "
},
{
"path": "test/boilerplate/accounts/accounts_test.exs",
"chars": 5226,
"preview": "defmodule Boilerplate.AccountsTest do\n use Boilerplate.DataCase\n\n alias Boilerplate.Accounts\n\n describe \"users\" do\n "
},
{
"path": "test/boilerplate_web/controllers/page_controller_test.exs",
"chars": 183,
"preview": "defmodule BoilerplateWeb.PageControllerTest do\n use BoilerplateWeb.ConnCase\n\n test \"GET /\", %{conn: conn} do\n conn "
},
{
"path": "test/boilerplate_web/controllers/user_controller_test.exs",
"chars": 9095,
"preview": "defmodule BoilerplateWeb.UserControllerTest do\n use BoilerplateWeb.ConnCase\n\n alias Boilerplate.Accounts\n alias Boile"
},
{
"path": "test/boilerplate_web/views/error_view_test.exs",
"chars": 571,
"preview": "defmodule BoilerplateWeb.ErrorViewTest do\n use BoilerplateWeb.ConnCase, async: true\n\n # Bring render/3 and render_to_s"
},
{
"path": "test/boilerplate_web/views/layout_view_test.exs",
"chars": 90,
"preview": "defmodule BoilerplateWeb.LayoutViewTest do\n use BoilerplateWeb.ConnCase, async: true\nend\n"
},
{
"path": "test/boilerplate_web/views/page_view_test.exs",
"chars": 88,
"preview": "defmodule BoilerplateWeb.PageViewTest do\n use BoilerplateWeb.ConnCase, async: true\nend\n"
},
{
"path": "test/support/channel_case.ex",
"chars": 953,
"preview": "defmodule BoilerplateWeb.ChannelCase do\n @moduledoc \"\"\"\n This module defines the test case to be used by\n channel tes"
},
{
"path": "test/support/conn_case.ex",
"chars": 1058,
"preview": "defmodule BoilerplateWeb.ConnCase do\n @moduledoc \"\"\"\n This module defines the test case to be used by\n tests that req"
},
{
"path": "test/support/data_case.ex",
"chars": 1403,
"preview": "defmodule Boilerplate.DataCase do\n @moduledoc \"\"\"\n This module defines the setup for tests requiring\n access to the a"
},
{
"path": "test/test_helper.exs",
"chars": 74,
"preview": "ExUnit.start()\n\nEcto.Adapters.SQL.Sandbox.mode(Boilerplate.Repo, :manual)\n"
}
]
About this extraction
This page contains the full source code of the chernyshof/react-phoenix-users-boilerplate GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 93 files (116.9 KB), approximately 34.3k tokens, and a symbol index with 175 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.