├── web
├── views
│ ├── app_view.ex
│ ├── page_view.ex
│ ├── layout_view.ex
│ ├── error_view.ex
│ └── error_helpers.ex
├── static
│ ├── assets
│ │ ├── favicon.ico
│ │ ├── images
│ │ │ ├── phoenix.png
│ │ │ └── landing
│ │ │ │ └── phoenix.png
│ │ └── robots.txt
│ ├── js
│ │ ├── index.js
│ │ ├── app
│ │ │ ├── index.js
│ │ │ ├── reducers
│ │ │ │ ├── index.js
│ │ │ │ ├── ws.js
│ │ │ │ └── visitors.js
│ │ │ ├── routes
│ │ │ │ └── index.js
│ │ │ ├── store
│ │ │ │ └── index.js
│ │ │ ├── containers
│ │ │ │ ├── App
│ │ │ │ │ └── index.js
│ │ │ │ └── index.js
│ │ │ ├── actions
│ │ │ │ └── ws.js
│ │ │ └── components
│ │ │ │ └── Main
│ │ │ │ └── index.js
│ │ ├── landing
│ │ │ ├── index.js
│ │ │ ├── reducers
│ │ │ │ ├── index.js
│ │ │ │ ├── auth.js
│ │ │ │ ├── ws.js
│ │ │ │ └── visitors.js
│ │ │ ├── routes
│ │ │ │ └── index.js
│ │ │ ├── containers
│ │ │ │ ├── App
│ │ │ │ │ └── index.js
│ │ │ │ └── index.js
│ │ │ ├── store
│ │ │ │ └── index.js
│ │ │ ├── actions
│ │ │ │ ├── auth.js
│ │ │ │ └── ws.js
│ │ │ └── components
│ │ │ │ ├── Main
│ │ │ │ └── index.js
│ │ │ │ ├── Login
│ │ │ │ └── index.js
│ │ │ │ └── Registration
│ │ │ │ └── index.js
│ │ ├── reducers
│ │ │ ├── index.js
│ │ │ ├── ws.js
│ │ │ └── visitors.js
│ │ ├── routes
│ │ │ └── index.js
│ │ ├── containers
│ │ │ ├── App
│ │ │ │ └── index.js
│ │ │ └── index.js
│ │ ├── store
│ │ │ └── index.js
│ │ ├── actions
│ │ │ └── ws.js
│ │ └── components
│ │ │ └── Main
│ │ │ └── index.js
│ └── styles
│ │ ├── app
│ │ └── index.less
│ │ ├── index.less
│ │ └── landing
│ │ └── index.less
├── templates
│ ├── app
│ │ └── index.html.eex
│ ├── page
│ │ └── index.html.eex
│ └── layout
│ │ ├── app.html.eex
│ │ └── landing.html.eex
├── channels
│ ├── visitors_channel.ex
│ ├── user_socket.ex
│ └── auth_channel.ex
├── router.ex
├── gettext.ex
├── controllers
│ ├── app_controller.ex
│ └── page_controller.ex
├── web.ex
└── models
│ └── user.ex
├── lib
├── reph
│ ├── repo.ex
│ ├── react_io.ex
│ ├── guardian
│ │ └── serializer.ex
│ ├── endpoint.ex
│ └── visitors.ex
└── reph.ex
├── test
├── views
│ ├── layout_view_test.exs
│ ├── page_view_test.exs
│ └── error_view_test.exs
├── test_helper.exs
├── controllers
│ └── page_controller_test.exs
├── models
│ └── user_test.exs
└── support
│ ├── channel_case.ex
│ ├── conn_case.ex
│ └── model_case.ex
├── priv
├── repo
│ ├── migrations
│ │ ├── 20160525021310_create_user.exs
│ │ └── 20160525021240_create_tokens.exs
│ └── seeds.exs
└── gettext
│ ├── errors.pot
│ └── en
│ └── LC_MESSAGES
│ └── errors.po
├── config
├── test.exs
├── dev.exs
├── config.exs
└── prod.exs
├── .gitignore
├── webpack.app.server.config.js
├── webpack.server.config.js
├── webpack.landing.server.config.js
├── package.json
├── mix.exs
├── README.md
├── webpack.app.config.js
├── webpack.config.js
├── webpack.landing.config.js
└── mix.lock
/web/views/app_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Reph.AppView do
2 | use Reph.Web, :view
3 | end
--------------------------------------------------------------------------------
/lib/reph/repo.ex:
--------------------------------------------------------------------------------
1 | defmodule Reph.Repo do
2 | use Ecto.Repo, otp_app: :reph
3 | end
4 |
--------------------------------------------------------------------------------
/web/views/page_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Reph.PageView do
2 | use Reph.Web, :view
3 | end
4 |
--------------------------------------------------------------------------------
/web/views/layout_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Reph.LayoutView do
2 | use Reph.Web, :view
3 | end
4 |
--------------------------------------------------------------------------------
/test/views/layout_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Reph.LayoutViewTest do
2 | use Reph.ConnCase, async: true
3 | end
--------------------------------------------------------------------------------
/test/views/page_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Reph.PageViewTest do
2 | use Reph.ConnCase, async: true
3 | end
4 |
--------------------------------------------------------------------------------
/web/static/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chvanikoff/reph2/HEAD/web/static/assets/favicon.ico
--------------------------------------------------------------------------------
/web/static/assets/images/phoenix.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chvanikoff/reph2/HEAD/web/static/assets/images/phoenix.png
--------------------------------------------------------------------------------
/lib/reph/react_io.ex:
--------------------------------------------------------------------------------
1 | defmodule Reph.ReactIO do
2 | use StdJsonIo, otp_app: :reph, script: "node_modules/react-stdio/bin/react-stdio"
3 | end
--------------------------------------------------------------------------------
/web/static/assets/images/landing/phoenix.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chvanikoff/reph2/HEAD/web/static/assets/images/landing/phoenix.png
--------------------------------------------------------------------------------
/web/templates/app/index.html.eex:
--------------------------------------------------------------------------------
1 |
<%= raw @html %>
2 |
--------------------------------------------------------------------------------
/web/templates/page/index.html.eex:
--------------------------------------------------------------------------------
1 | <%= raw @html %>
2 |
--------------------------------------------------------------------------------
/web/static/js/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 |
4 | import Index from "containers";
5 |
6 |
7 | ReactDOM.render(, document.getElementById("index"));
--------------------------------------------------------------------------------
/web/static/js/app/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 |
4 | import Index from "containers";
5 |
6 |
7 | ReactDOM.render(, document.getElementById("index"));
--------------------------------------------------------------------------------
/web/static/js/landing/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 |
4 | import Index from "containers";
5 |
6 |
7 | ReactDOM.render(, document.getElementById("index"));
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start
2 |
3 | Mix.Task.run "ecto.create", ~w(-r Reph.Repo --quiet)
4 | Mix.Task.run "ecto.migrate", ~w(-r Reph.Repo --quiet)
5 | Ecto.Adapters.SQL.begin_test_transaction(Reph.Repo)
6 |
7 |
--------------------------------------------------------------------------------
/web/static/assets/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/test/controllers/page_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Reph.PageControllerTest do
2 | use Reph.ConnCase
3 |
4 | test "GET /", %{conn: conn} do
5 | conn = get conn, "/"
6 | assert html_response(conn, 200) =~ "Welcome to Phoenix!"
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/web/static/js/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux";
2 | import { routerReducer } from "react-router-redux";
3 |
4 | import visitors from "./visitors";
5 | import ws from "./ws";
6 |
7 |
8 | export default combineReducers({
9 | routing: routerReducer,
10 | visitors,
11 | ws
12 | });
--------------------------------------------------------------------------------
/web/static/js/app/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux";
2 | import { routerReducer } from "react-router-redux";
3 |
4 | import visitors from "./visitors";
5 | import ws from "./ws";
6 |
7 |
8 | export default combineReducers({
9 | routing: routerReducer,
10 | visitors,
11 | ws
12 | });
--------------------------------------------------------------------------------
/web/static/js/routes/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Route, IndexRoute } from "react-router";
3 |
4 | import AppContainer from "containers/App";
5 | import Main from "components/Main";
6 |
7 |
8 | export default (
9 |
10 | );
--------------------------------------------------------------------------------
/web/static/js/app/routes/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Route, IndexRoute } from "react-router";
3 |
4 | import AppContainer from "containers/App";
5 | import Main from "components/Main";
6 |
7 |
8 | export default (
9 |
10 | );
--------------------------------------------------------------------------------
/priv/repo/migrations/20160525021310_create_user.exs:
--------------------------------------------------------------------------------
1 | defmodule Reph.Repo.Migrations.CreateUser do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:users) do
6 | add :email, :string
7 | add :password, :string
8 |
9 | timestamps
10 | end
11 | create unique_index(:users, [:email])
12 | end
13 | end
--------------------------------------------------------------------------------
/web/static/js/landing/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux";
2 | import { routerReducer } from "react-router-redux";
3 |
4 | import visitors from "./visitors";
5 | import auth from "./auth";
6 | import ws from "./ws";
7 |
8 |
9 | export default combineReducers({
10 | routing: routerReducer,
11 | visitors,
12 | auth,
13 | ws
14 | });
--------------------------------------------------------------------------------
/priv/repo/seeds.exs:
--------------------------------------------------------------------------------
1 | # Script for populating the database. You can run it as:
2 | #
3 | # mix run priv/repo/seeds.exs
4 | #
5 | # Inside the script, you can read and write to any of your
6 | # repositories directly:
7 | #
8 | # Reph.Repo.insert!(%Reph.SomeModel{})
9 | #
10 | # We recommend using the bang functions (`insert!`, `update!`
11 | # and so on) as they will fail if something goes wrong.
12 |
--------------------------------------------------------------------------------
/lib/reph/guardian/serializer.ex:
--------------------------------------------------------------------------------
1 | defmodule Reph.Guardian.Serializer do
2 | @behaviour Guardian.Serializer
3 |
4 | alias Reph.{Repo, User}
5 |
6 | def for_token(user = %User{}), do: { :ok, "User:#{user.id}" }
7 | def for_token(_), do: { :error, "Unknown resource type" }
8 |
9 | def from_token("User:" <> id), do: { :ok, Repo.get(User, String.to_integer(id)) }
10 | def from_token(_), do: { :error, "Unknown resource type" }
11 | end
--------------------------------------------------------------------------------
/web/views/error_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Reph.ErrorView do
2 | use Reph.Web, :view
3 |
4 | def render("404.html", _assigns) do
5 | "Page not found"
6 | end
7 |
8 | def render("500.html", _assigns) do
9 | "Server internal error"
10 | end
11 |
12 | # In case no render clause matches or no
13 | # template is found, let's render it as 500
14 | def template_not_found(_template, assigns) do
15 | render "500.html", assigns
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/test/models/user_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Reph.UserTest do
2 | use Reph.ModelCase
3 |
4 | alias Reph.User
5 |
6 | @valid_attrs %{email: "some content", password: "some content"}
7 | @invalid_attrs %{}
8 |
9 | test "changeset with valid attributes" do
10 | changeset = User.changeset(%User{}, @valid_attrs)
11 | assert changeset.valid?
12 | end
13 |
14 | test "changeset with invalid attributes" do
15 | changeset = User.changeset(%User{}, @invalid_attrs)
16 | refute changeset.valid?
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20160525021240_create_tokens.exs:
--------------------------------------------------------------------------------
1 | defmodule Reph.Repo.Migrations.CreateTokens do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:tokens, primary_key: false) do
6 | add :jti, :string, primary_key: true
7 | add :typ, :string
8 | add :aud, :string
9 | add :iss, :string
10 | add :sub, :string
11 | add :exp, :bigint
12 | add :jwt, :text
13 | add :claims, :map
14 | timestamps
15 | end
16 | create unique_index(:tokens, [:jti, :aud])
17 | end
18 | end
--------------------------------------------------------------------------------
/web/static/js/landing/reducers/auth.js:
--------------------------------------------------------------------------------
1 | const initialState = {
2 | login_failed: false,
3 | registration_errors: []
4 | };
5 |
6 | export default function reducer(state = initialState, action = {}) {
7 | switch (action.type) {
8 | case "AUTH_REGISTRATION_ERROR":
9 | return {
10 | ...state,
11 | registration_errors: action.errors
12 | };
13 | case "AUTH_LOGIN_ERROR":
14 | return {
15 | ...state,
16 | login_failed: action.value
17 | }
18 | default:
19 | return state;
20 | }
21 | }
--------------------------------------------------------------------------------
/web/static/js/containers/App/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default class App extends React.Component {
4 | render() {
5 | return
6 |
14 | {this.props.children}
15 |
;
16 | }
17 | };
--------------------------------------------------------------------------------
/config/test.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # We don't run a server during test. If one is required,
4 | # you can enable the server option below.
5 | config :reph, Reph.Endpoint,
6 | http: [port: 4001],
7 | server: false
8 |
9 | # Print only warnings and errors during test
10 | config :logger, level: :warn
11 |
12 | # Configure your database
13 | config :reph, Reph.Repo,
14 | adapter: Ecto.Adapters.Postgres,
15 | username: "postgres",
16 | password: "postgres",
17 | database: "reph_test",
18 | hostname: "localhost",
19 | pool: Ecto.Adapters.SQL.Sandbox
20 |
--------------------------------------------------------------------------------
/web/static/js/reducers/ws.js:
--------------------------------------------------------------------------------
1 | const initialState = {
2 | socket: null,
3 | channels: {}
4 | };
5 |
6 | export default function reducer(state = initialState, action = {}) {
7 | switch (action.type) {
8 | case "SOCKET_CONNECTED":
9 | return {
10 | ...state,
11 | socket: action.socket
12 | };
13 | case "CHANNEL_JOINED":
14 | return {
15 | ...state,
16 | channels: {
17 | ...state.channels,
18 | [action.name]: action.channel
19 | }
20 | }
21 | default:
22 | return state;
23 | }
24 | }
--------------------------------------------------------------------------------
/web/static/js/app/reducers/ws.js:
--------------------------------------------------------------------------------
1 | const initialState = {
2 | socket: null,
3 | channels: {}
4 | };
5 |
6 | export default function reducer(state = initialState, action = {}) {
7 | switch (action.type) {
8 | case "SOCKET_CONNECTED":
9 | return {
10 | ...state,
11 | socket: action.socket
12 | };
13 | case "CHANNEL_JOINED":
14 | return {
15 | ...state,
16 | channels: {
17 | ...state.channels,
18 | [action.name]: action.channel
19 | }
20 | }
21 | default:
22 | return state;
23 | }
24 | }
--------------------------------------------------------------------------------
/web/static/js/landing/reducers/ws.js:
--------------------------------------------------------------------------------
1 | const initialState = {
2 | socket: null,
3 | channels: {}
4 | };
5 |
6 | export default function reducer(state = initialState, action = {}) {
7 | switch (action.type) {
8 | case "SOCKET_CONNECTED":
9 | return {
10 | ...state,
11 | socket: action.socket
12 | };
13 | case "CHANNEL_JOINED":
14 | return {
15 | ...state,
16 | channels: {
17 | ...state.channels,
18 | [action.name]: action.channel
19 | }
20 | }
21 | default:
22 | return state;
23 | }
24 | }
--------------------------------------------------------------------------------
/web/static/js/landing/routes/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Route, IndexRoute } from "react-router";
3 |
4 | import AppContainer from "containers/App";
5 | import Main from "components/Main";
6 | import Login from "components/Login";
7 | import Registration from "components/Registration";
8 |
9 |
10 | export default (
11 |
12 |
13 |
14 |
15 |
16 |
17 | );
--------------------------------------------------------------------------------
/web/templates/layout/app.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Hello Reph!
11 | ">
12 |
13 |
14 |
15 | <%= render @view_module, @view_template, assigns %>
16 |
17 |
18 |
--------------------------------------------------------------------------------
/web/channels/visitors_channel.ex:
--------------------------------------------------------------------------------
1 | defmodule Reph.VisitorsChannel do
2 | use Reph.Web, :channel
3 |
4 | def join("visitors", _params, socket) do
5 | send(self, :after_join)
6 | {:ok, socket}
7 | end
8 |
9 | def handle_info(:after_join, socket) do
10 | push(socket, "init", Reph.Visitors.state())
11 | {:ok, _} = Reph.Visitors.add()
12 | {:noreply, socket}
13 | end
14 | def handle_info(%{event: event}, socket) when event in ["add", "remove"] do
15 | push(socket, event, %{})
16 | {:noreply, socket}
17 | end
18 |
19 | def terminate(_, _) do
20 | {:ok, _} = Reph.Visitors.remove()
21 | :ok
22 | end
23 | end
--------------------------------------------------------------------------------
/web/templates/layout/landing.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Hello Reph!
11 | ">
12 |
13 |
14 |
15 | <%= render @view_module, @view_template, assigns %>
16 |
17 |
18 |
--------------------------------------------------------------------------------
/test/views/error_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Reph.ErrorViewTest do
2 | use Reph.ConnCase, async: true
3 |
4 | # Bring render/3 and render_to_string/3 for testing custom views
5 | import Phoenix.View
6 |
7 | test "renders 404.html" do
8 | assert render_to_string(Reph.ErrorView, "404.html", []) ==
9 | "Page not found"
10 | end
11 |
12 | test "render 500.html" do
13 | assert render_to_string(Reph.ErrorView, "500.html", []) ==
14 | "Server internal error"
15 | end
16 |
17 | test "render any other" do
18 | assert render_to_string(Reph.ErrorView, "505.html", []) ==
19 | "Server internal error"
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # App artifacts
2 | /_build
3 | /db
4 | /deps
5 | /*.ez
6 |
7 | # Generate on crash by the VM
8 | erl_crash.dump
9 |
10 | # Static artifacts
11 | /node_modules
12 |
13 | # Since we are building assets from web/static,
14 | # we ignore priv/static. You may want to comment
15 | # this depending on your deployment strategy.
16 | /priv/static/
17 |
18 | # The config/prod.secret.exs file by default contains sensitive
19 | # data and you should not commit it into version control.
20 | #
21 | # Alternatively, you may comment the line below and commit the
22 | # secrets file as long as you replace its contents by environment
23 | # variables.
24 | /config/prod.secret.exs
25 |
--------------------------------------------------------------------------------
/webpack.app.server.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | entry: {
3 | component: "./web/static/js/app/containers/index.js",
4 | },
5 | output: {
6 | path: "./priv/static/server/js",
7 | filename: "app.js",
8 | library: "app",
9 | libraryTarget: "commonjs2"
10 | },
11 | module: {
12 | loaders: [{
13 | test: /\.js$/,
14 | exclude: /node_modules/,
15 | loader: "babel",
16 | query: {
17 | plugins: ["transform-decorators-legacy"],
18 | presets: ["react", "es2015", "stage-2"],
19 | }
20 | }],
21 | },
22 | resolve: {
23 | extensions: ["", ".js"],
24 | modulesDirectories: ["node_modules", __dirname + "/web/static/js/app"],
25 | }
26 | };
--------------------------------------------------------------------------------
/web/static/js/landing/containers/App/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link, IndexLink } from 'react-router';
3 |
4 | export default class App extends React.Component {
5 | render() {
6 | return
7 |
8 |
14 |
15 |
16 | {this.props.children}
17 |
;
18 | }
19 | };
--------------------------------------------------------------------------------
/web/router.ex:
--------------------------------------------------------------------------------
1 | defmodule Reph.Router do
2 | use Reph.Web, :router
3 |
4 | pipeline :browser do
5 | plug :accepts, ["html"]
6 | plug :fetch_session
7 | plug :fetch_flash
8 | plug :protect_from_forgery
9 | plug :put_secure_browser_headers
10 | end
11 |
12 | pipeline :app do
13 | plug Guardian.Plug.VerifySession
14 | plug Guardian.Plug.LoadResource
15 | end
16 |
17 | scope "/app", Reph do
18 | pipe_through [:browser, :app]
19 |
20 | get "/logout", AppController, :logout
21 | get "/*path", AppController, :index
22 | end
23 |
24 | scope "/", Reph do
25 | pipe_through :browser # Use the default browser stack
26 | get "/*path", PageController, :index
27 | end
28 | end
--------------------------------------------------------------------------------
/webpack.server.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | entry: {
3 | component: "./web/static/js/landing/containers/index.js",
4 | },
5 | output: {
6 | path: "./priv/static/server/js",
7 | filename: "landing.js",
8 | library: "landing",
9 | libraryTarget: "commonjs2"
10 | },
11 | module: {
12 | loaders: [{
13 | test: /\.js$/,
14 | exclude: /node_modules/,
15 | loader: "babel",
16 | query: {
17 | plugins: ["transform-decorators-legacy"],
18 | presets: ["react", "es2015", "stage-2"],
19 | }
20 | }],
21 | },
22 | resolve: {
23 | extensions: ["", ".js"],
24 | modulesDirectories: ["node_modules", __dirname + "/web/static/js/landing"],
25 | }
26 | };
--------------------------------------------------------------------------------
/webpack.landing.server.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | entry: {
3 | component: "./web/static/js/landing/containers/index.js",
4 | },
5 | output: {
6 | path: "./priv/static/server/js",
7 | filename: "landing.js",
8 | library: "landing",
9 | libraryTarget: "commonjs2"
10 | },
11 | module: {
12 | loaders: [{
13 | test: /\.js$/,
14 | exclude: /node_modules/,
15 | loader: "babel",
16 | query: {
17 | plugins: ["transform-decorators-legacy"],
18 | presets: ["react", "es2015", "stage-2"],
19 | }
20 | }],
21 | },
22 | resolve: {
23 | extensions: ["", ".js"],
24 | modulesDirectories: ["node_modules", __dirname + "/web/static/js/landing"],
25 | }
26 | };
--------------------------------------------------------------------------------
/web/gettext.ex:
--------------------------------------------------------------------------------
1 | defmodule Reph.Gettext do
2 | @moduledoc """
3 | A module providing Internationalization with a gettext-based API.
4 |
5 | By using [Gettext](http://hexdocs.pm/gettext),
6 | your module gains a set of macros for translations, for example:
7 |
8 | import Reph.Gettext
9 |
10 | # Simple translation
11 | gettext "Here is the string to translate"
12 |
13 | # Plural translation
14 | ngettext "Here is the string to translate",
15 | "Here are the strings to translate",
16 | 3
17 |
18 | # Domain-based translation
19 | dgettext "errors", "Here is the error message to translate"
20 |
21 | See the [Gettext Docs](http://hexdocs.pm/gettext) for detailed usage.
22 | """
23 | use Gettext, otp_app: :reph
24 | end
25 |
--------------------------------------------------------------------------------
/web/static/js/store/index.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from "redux";
2 | import thunkMiddleware from "redux-thunk";
3 |
4 | import reducers from "reducers";
5 | import WSActions from "actions/ws";
6 |
7 |
8 | const devToolsExt = typeof window === "object" && typeof window.devToolsExtension !== "undefined"
9 | ? window.devToolsExtension()
10 | : f => f;
11 |
12 | export default function configureStore(initialState) {
13 | const store = createStore(
14 | reducers,
15 | initialState,
16 | compose(
17 | applyMiddleware(thunkMiddleware),
18 | devToolsExt
19 | )
20 | );
21 | if (typeof window !== "undefined") {
22 | store.dispatch(WSActions.socket_connect());
23 | store.dispatch(WSActions.channel_join("visitors"));
24 | }
25 | return store;
26 | }
--------------------------------------------------------------------------------
/web/static/js/app/store/index.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from "redux";
2 | import thunkMiddleware from "redux-thunk";
3 |
4 | import reducers from "reducers";
5 | import WSActions from "actions/ws";
6 |
7 |
8 | const devToolsExt = typeof window === "object" && typeof window.devToolsExtension !== "undefined"
9 | ? window.devToolsExtension()
10 | : f => f;
11 |
12 | export default function configureStore(initialState) {
13 | const store = createStore(
14 | reducers,
15 | initialState,
16 | compose(
17 | applyMiddleware(thunkMiddleware),
18 | devToolsExt
19 | )
20 | );
21 | if (typeof window !== "undefined") {
22 | store.dispatch(WSActions.socket_connect());
23 | store.dispatch(WSActions.channel_join("visitors"));
24 | }
25 | return store;
26 | }
--------------------------------------------------------------------------------
/web/static/js/landing/store/index.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from "redux";
2 | import thunkMiddleware from "redux-thunk";
3 |
4 | import reducers from "reducers";
5 | import WSActions from "actions/ws";
6 |
7 |
8 | const devToolsExt = typeof window === "object" && typeof window.devToolsExtension !== "undefined"
9 | ? window.devToolsExtension()
10 | : f => f;
11 |
12 | export default function configureStore(initialState) {
13 | const store = createStore(
14 | reducers,
15 | initialState,
16 | compose(
17 | applyMiddleware(thunkMiddleware),
18 | devToolsExt
19 | )
20 | );
21 | if (typeof window !== "undefined") {
22 | store.dispatch(WSActions.socket_connect());
23 | store.dispatch(WSActions.channel_join("visitors"));
24 | store.dispatch(WSActions.channel_join("auth"));
25 | }
26 | return store;
27 | }
--------------------------------------------------------------------------------
/web/static/js/reducers/visitors.js:
--------------------------------------------------------------------------------
1 | const initialState = {
2 | total: 0,
3 | online: 0,
4 | max_online: 0,
5 | };
6 |
7 | export default function reducer(state = initialState, action = {}) {
8 | switch (action.type) {
9 | case "VISITORS_INIT":
10 | return {
11 | total: action.total,
12 | online: action.online,
13 | max_online: action.max_online,
14 | };
15 | case "VISITORS_ADD":
16 | const new_online = state.online + 1;
17 | let max_online = state.max_online;
18 |
19 | if (new_online > max_online) {
20 | max_online = max_online + 1;
21 | }
22 | return {
23 | total: state.total + 1,
24 | online: new_online,
25 | max_online: max_online,
26 | };
27 | case "VISITORS_REMOVE":
28 | return {
29 | ...state,
30 | online: state.online - 1
31 | }
32 | default:
33 | return state;
34 | }
35 | }
--------------------------------------------------------------------------------
/web/static/js/app/reducers/visitors.js:
--------------------------------------------------------------------------------
1 | const initialState = {
2 | total: 0,
3 | online: 0,
4 | max_online: 0,
5 | };
6 |
7 | export default function reducer(state = initialState, action = {}) {
8 | switch (action.type) {
9 | case "VISITORS_INIT":
10 | return {
11 | total: action.total,
12 | online: action.online,
13 | max_online: action.max_online,
14 | };
15 | case "VISITORS_ADD":
16 | const new_online = state.online + 1;
17 | let max_online = state.max_online;
18 |
19 | if (new_online > max_online) {
20 | max_online = max_online + 1;
21 | }
22 | return {
23 | total: state.total + 1,
24 | online: new_online,
25 | max_online: max_online,
26 | };
27 | case "VISITORS_REMOVE":
28 | return {
29 | ...state,
30 | online: state.online - 1
31 | }
32 | default:
33 | return state;
34 | }
35 | }
--------------------------------------------------------------------------------
/web/static/js/landing/reducers/visitors.js:
--------------------------------------------------------------------------------
1 | const initialState = {
2 | total: 0,
3 | online: 0,
4 | max_online: 0,
5 | };
6 |
7 | export default function reducer(state = initialState, action = {}) {
8 | switch (action.type) {
9 | case "VISITORS_INIT":
10 | return {
11 | total: action.total,
12 | online: action.online,
13 | max_online: action.max_online,
14 | };
15 | case "VISITORS_ADD":
16 | const new_online = state.online + 1;
17 | let max_online = state.max_online;
18 |
19 | if (new_online > max_online) {
20 | max_online = max_online + 1;
21 | }
22 | return {
23 | total: state.total + 1,
24 | online: new_online,
25 | max_online: max_online,
26 | };
27 | case "VISITORS_REMOVE":
28 | return {
29 | ...state,
30 | online: state.online - 1
31 | }
32 | default:
33 | return state;
34 | }
35 | }
--------------------------------------------------------------------------------
/web/controllers/app_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule Reph.AppController do
2 | use Reph.Web, :controller
3 |
4 | plug Guardian.Plug.EnsureAuthenticated, handler: __MODULE__
5 |
6 | def index(conn, %{"path" => path}) do
7 | visitors = Reph.Visitors.state()
8 | initial_state = %{"visitors" => visitors}
9 | props = %{
10 | "location" => "/app" <> Enum.join(path, "/"),
11 | "initial_state" => initial_state
12 | }
13 |
14 | result = Reph.ReactIO.json_call!(%{
15 | component: "./priv/static/server/js/app.js",
16 | props: props,
17 | })
18 |
19 | conn
20 | |> put_layout("app.html")
21 | |> render("index.html", html: result["html"], props: initial_state)
22 | end
23 |
24 | def logout(conn, _params) do
25 | conn
26 | |> Guardian.Plug.sign_out()
27 | |> redirect(to: "/")
28 | end
29 |
30 | def unauthenticated(conn, _params) do
31 | redirect(conn, to: "/")
32 | end
33 | end
--------------------------------------------------------------------------------
/web/controllers/page_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule Reph.PageController do
2 | use Reph.Web, :controller
3 |
4 | def index(conn, %{"token" => token}) do
5 | case Guardian.decode_and_verify(token) do
6 | { :ok, claims } ->
7 | Guardian.revoke!(token)
8 | {:ok, user} = Guardian.serializer.from_token(claims["sub"])
9 | conn
10 | |> Guardian.Plug.sign_in(user)
11 | |> redirect(to: "/app")
12 | _ ->
13 | redirect(conn, to: "/")
14 | end
15 | end
16 | def index(conn, _params) do
17 | visitors = Reph.Visitors.state()
18 | initial_state = %{"visitors" => visitors}
19 | props = %{
20 | "location" => conn.request_path,
21 | "initial_state" => initial_state
22 | }
23 |
24 | result = Reph.ReactIO.json_call!(%{
25 | component: "./priv/static/server/js/landing.js",
26 | props: props,
27 | })
28 |
29 | conn
30 | |> put_layout("landing.html")
31 | |> render("index.html", html: result["html"], props: initial_state)
32 | end
33 | end
--------------------------------------------------------------------------------
/config/dev.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | webpack = fn(name) ->
4 | {"node", [
5 | "node_modules/webpack/bin/webpack.js",
6 | "--watch-stdin",
7 | "--colors",
8 | "--config",
9 | "webpack.#{name}.config.js"
10 | ]}
11 | end
12 |
13 | config :reph, Reph.Endpoint,
14 | http: [port: 4000],
15 | debug_errors: true,
16 | code_reloader: true,
17 | check_origin: false,
18 | watchers: ["landing", "landing.server", "app", "app.server"] |> Enum.map(&(webpack.(&1)))
19 |
20 | config :reph, Reph.Endpoint,
21 | live_reload: [
22 | patterns: [
23 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
24 | ~r{priv/gettext/.*(po)$},
25 | ~r{web/views/.*(ex)$},
26 | ~r{web/templates/.*(eex)$}
27 | ]
28 | ]
29 |
30 | config :logger, :console, format: "[$level] $message\n"
31 |
32 | config :phoenix, :stacktrace_depth, 20
33 |
34 | config :reph, Reph.Repo,
35 | adapter: Ecto.Adapters.Postgres,
36 | username: "postgres",
37 | password: "postgres",
38 | database: "reph_dev",
39 | hostname: "localhost",
40 | pool_size: 10
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "bootstrap": "^3.3.6",
4 | "phoenix": "file:deps/phoenix",
5 | "react": "^15.0.2",
6 | "react-dom": "^15.0.2",
7 | "react-redux": "^4.4.5",
8 | "react-router": "^2.4.0",
9 | "react-router-redux": "^4.0.4",
10 | "redux": "^3.5.2"
11 | },
12 | "devDependencies": {
13 | "autoprefixer": "^6.3.6",
14 | "babel-core": "^6.8.0",
15 | "babel-loader": "^6.2.4",
16 | "babel-plugin-transform-decorators-legacy": "^1.3.4",
17 | "babel-preset-es2015": "^6.6.0",
18 | "babel-preset-react": "^6.5.0",
19 | "babel-preset-stage-2": "^6.5.0",
20 | "copy-webpack-plugin": "^2.1.3",
21 | "css-loader": "^0.23.1",
22 | "extract-text-webpack-plugin": "^1.0.1",
23 | "file-loader": "^0.8.5",
24 | "imports-loader": "^0.6.5",
25 | "less": "^2.7.0",
26 | "less-loader": "^2.2.3",
27 | "postcss-loader": "^0.9.1",
28 | "react-stdio": "github:chvanikoff/react-stdio",
29 | "redux-thunk": "^2.0.1",
30 | "style-loader": "^0.13.1",
31 | "url-loader": "^0.5.7",
32 | "webpack": "^1.13.0"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/test/support/channel_case.ex:
--------------------------------------------------------------------------------
1 | defmodule Reph.ChannelCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | channel tests.
5 |
6 | Such tests rely on `Phoenix.ChannelTest` and also
7 | imports other functionality to make it easier
8 | to build and query models.
9 |
10 | Finally, if the test case interacts with the database,
11 | it cannot be async. For this reason, every test runs
12 | inside a transaction which is reset at the beginning
13 | of the test unless the test case is marked as async.
14 | """
15 |
16 | use ExUnit.CaseTemplate
17 |
18 | using do
19 | quote do
20 | # Import conveniences for testing with channels
21 | use Phoenix.ChannelTest
22 |
23 | alias Reph.Repo
24 | import Ecto
25 | import Ecto.Changeset
26 | import Ecto.Query, only: [from: 1, from: 2]
27 |
28 |
29 | # The default endpoint for testing
30 | @endpoint Reph.Endpoint
31 | end
32 | end
33 |
34 | setup tags do
35 | unless tags[:async] do
36 | Ecto.Adapters.SQL.restart_test_transaction(Reph.Repo, [])
37 | end
38 |
39 | :ok
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/web/static/js/app/containers/App/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default class App extends React.Component {
4 | render() {
5 | return
6 |
24 | {this.props.children}
25 |
;
26 | }
27 | };
--------------------------------------------------------------------------------
/web/views/error_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule Reph.ErrorHelpers do
2 | @moduledoc """
3 | Conveniences for translating and building error messages.
4 | """
5 |
6 | use Phoenix.HTML
7 |
8 | @doc """
9 | Generates tag for inlined form input errors.
10 | """
11 | def error_tag(form, field) do
12 | if error = form.errors[field] do
13 | content_tag :span, translate_error(error), class: "help-block"
14 | end
15 | end
16 |
17 | @doc """
18 | Translates an error message using gettext.
19 | """
20 | def translate_error({msg, opts}) do
21 | # Because error messages were defined within Ecto, we must
22 | # call the Gettext module passing our Gettext backend. We
23 | # also use the "errors" domain as translations are placed
24 | # in the errors.po file. On your own code and templates,
25 | # this could be written simply as:
26 | #
27 | # dngettext "errors", "1 file", "%{count} files", count
28 | #
29 | Gettext.dngettext(Reph.Gettext, "errors", msg, msg, opts[:count], opts)
30 | end
31 |
32 | def translate_error(msg) do
33 | Gettext.dgettext(Reph.Gettext, "errors", msg)
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/lib/reph.ex:
--------------------------------------------------------------------------------
1 | defmodule Reph do
2 | use Application
3 |
4 | # See http://elixir-lang.org/docs/stable/elixir/Application.html
5 | # for more information on OTP Applications
6 | def start(_type, _args) do
7 | import Supervisor.Spec, warn: false
8 |
9 | children = [
10 | # Start the endpoint when the application starts
11 | supervisor(Reph.Endpoint, []),
12 | # Start the Ecto repository
13 | supervisor(Reph.Repo, []),
14 | # Here you could define other workers and supervisors as children
15 | # worker(Reph.Worker, [arg1, arg2, arg3]),
16 | worker(Reph.Visitors, []),
17 | supervisor(Reph.ReactIO, [])
18 | ]
19 |
20 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
21 | # for other strategies and supported options
22 | opts = [strategy: :one_for_one, name: Reph.Supervisor]
23 | Supervisor.start_link(children, opts)
24 | end
25 |
26 | # Tell Phoenix to update the endpoint configuration
27 | # whenever the application is updated.
28 | def config_change(changed, _new, removed) do
29 | Reph.Endpoint.config_change(changed, removed)
30 | :ok
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/reph/endpoint.ex:
--------------------------------------------------------------------------------
1 | defmodule Reph.Endpoint do
2 | use Phoenix.Endpoint, otp_app: :reph
3 |
4 | socket "/socket", Reph.UserSocket
5 |
6 | # Serve at "/" the static files from "priv/static" directory.
7 | #
8 | # You should set gzip to true if you are running phoenix.digest
9 | # when deploying your static files in production.
10 | plug Plug.Static,
11 | at: "/", from: :reph, gzip: false,
12 | only: ~w(css fonts images js favicon.ico robots.txt)
13 |
14 | # Code reloading can be explicitly enabled under the
15 | # :code_reloader configuration of your endpoint.
16 | if code_reloading? do
17 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
18 | plug Phoenix.LiveReloader
19 | plug Phoenix.CodeReloader
20 | end
21 |
22 | plug Plug.RequestId
23 | plug Plug.Logger
24 |
25 | plug Plug.Parsers,
26 | parsers: [:urlencoded, :multipart, :json],
27 | pass: ["*/*"],
28 | json_decoder: Poison
29 |
30 | plug Plug.MethodOverride
31 | plug Plug.Head
32 |
33 | plug Plug.Session,
34 | store: :cookie,
35 | key: "_reph_key",
36 | signing_salt: "1Cm8tZWK"
37 |
38 | plug Reph.Router
39 | end
40 |
--------------------------------------------------------------------------------
/web/static/js/landing/actions/auth.js:
--------------------------------------------------------------------------------
1 | const processToken = (token) => {
2 | window.location = "/?token=" + token;
3 | }
4 |
5 | const authActions = {
6 | register: (params) => {
7 | return (dispatch, getState) => {
8 | const { ws } = getState();
9 | ws.channels.auth
10 | .push("signup", params)
11 | .receive("ok", (msg) => {
12 | processToken(msg.token);
13 | })
14 | .receive("error", (msg) => {
15 | dispatch({
16 | type: 'AUTH_REGISTRATION_ERROR',
17 | errors: msg.errors
18 | });
19 | });
20 | }
21 | },
22 | login: (params) => {
23 | return (dispatch, getState) => {
24 | dispatch({
25 | type: 'AUTH_LOGIN_ERROR',
26 | value: false
27 | });
28 | const { ws } = getState();
29 | ws.channels.auth
30 | .push("login", params)
31 | .receive("ok", (msg) => {
32 | processToken(msg.token);
33 | })
34 | .receive("error", (msg) => {
35 | dispatch({
36 | type: 'AUTH_LOGIN_ERROR',
37 | value: true
38 | });
39 | });
40 | }
41 | }
42 | };
43 |
44 | export default authActions;
--------------------------------------------------------------------------------
/test/support/conn_case.ex:
--------------------------------------------------------------------------------
1 | defmodule Reph.ConnCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | tests that require setting up a connection.
5 |
6 | Such tests rely on `Phoenix.ConnTest` and also
7 | imports other functionality to make it easier
8 | to build and query models.
9 |
10 | Finally, if the test case interacts with the database,
11 | it cannot be async. For this reason, every test runs
12 | inside a transaction which is reset at the beginning
13 | of the test unless the test case is marked as async.
14 | """
15 |
16 | use ExUnit.CaseTemplate
17 |
18 | using do
19 | quote do
20 | # Import conveniences for testing with connections
21 | use Phoenix.ConnTest
22 |
23 | alias Reph.Repo
24 | import Ecto
25 | import Ecto.Changeset
26 | import Ecto.Query, only: [from: 1, from: 2]
27 |
28 | import Reph.Router.Helpers
29 |
30 | # The default endpoint for testing
31 | @endpoint Reph.Endpoint
32 | end
33 | end
34 |
35 | setup tags do
36 | unless tags[:async] do
37 | Ecto.Adapters.SQL.restart_test_transaction(Reph.Repo, [])
38 | end
39 |
40 | {:ok, conn: Phoenix.ConnTest.conn()}
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/web/channels/user_socket.ex:
--------------------------------------------------------------------------------
1 | defmodule Reph.UserSocket do
2 | use Phoenix.Socket
3 |
4 | ## Channels
5 | channel "visitors", Reph.VisitorsChannel
6 | channel "auth", Reph.AuthChannel
7 |
8 |
9 | ## Transports
10 | transport :websocket, Phoenix.Transports.WebSocket
11 | # transport :longpoll, Phoenix.Transports.LongPoll
12 |
13 | # Socket params are passed from the client and can
14 | # be used to verify and authenticate a user. After
15 | # verification, you can put default assigns into
16 | # the socket that will be set for all channels, ie
17 | #
18 | # {:ok, assign(socket, :user_id, verified_user_id)}
19 | #
20 | # To deny connection, return `:error`.
21 | #
22 | # See `Phoenix.Token` documentation for examples in
23 | # performing token verification on connect.
24 | def connect(_params, socket) do
25 | {:ok, socket}
26 | end
27 |
28 | # Socket id's are topics that allow you to identify all sockets for a given user:
29 | #
30 | # def id(socket), do: "users_socket:#{socket.assigns.user_id}"
31 | #
32 | # Would allow you to broadcast a "disconnect" event and terminate
33 | # all active sockets and channels for a given user:
34 | #
35 | # Reph.Endpoint.broadcast("users_socket:#{user.id}", "disconnect", %{})
36 | #
37 | # Returning `nil` makes this socket anonymous.
38 | def id(_socket), do: nil
39 | end
40 |
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | # This file is responsible for configuring your application
2 | # and its dependencies with the aid of the Mix.Config module.
3 | #
4 | # This configuration file is loaded before any dependency and
5 | # is restricted to this project.
6 | use Mix.Config
7 |
8 | # Configures the endpoint
9 | config :reph, Reph.Endpoint,
10 | url: [host: "localhost"],
11 | root: Path.dirname(__DIR__),
12 | secret_key_base: "W9Rk2Uu1+dUjpBkwhf7hSy83qJswuPWiBI8VpLTqoljK0sOZ0yAhokodzXLPcul6",
13 | render_errors: [accepts: ~w(html json)],
14 | pubsub: [name: Reph.PubSub,
15 | adapter: Phoenix.PubSub.PG2]
16 |
17 | # Configures Elixir's Logger
18 | config :logger, :console,
19 | format: "$time $metadata[$level] $message\n",
20 | metadata: [:request_id]
21 |
22 | # Import environment specific config. This must remain at the bottom
23 | # of this file so it overrides the configuration defined above.
24 | import_config "#{Mix.env}.exs"
25 |
26 | # Configure phoenix generators
27 | config :phoenix, :generators,
28 | migration: true,
29 | binary_id: false
30 |
31 | config :guardian, Guardian,
32 | issuer: "reph2",
33 | ttl: {30, :days},
34 | secret_key: "Mqp7VxR2nxq0ghXeHuFfVW1spo5kIuqK7N126MIFxyfSSBnsqNER0rMp1SfyXsrE",
35 | serializer: Reph.Guardian.Serializer,
36 | hooks: GuardianDb
37 |
38 | config :guardian_db, GuardianDb,
39 | repo: Reph.Repo,
40 | schema_name: "tokens"
--------------------------------------------------------------------------------
/lib/reph/visitors.ex:
--------------------------------------------------------------------------------
1 | defmodule Reph.Visitors do
2 | use GenServer
3 |
4 | @initial_state %{
5 | "total" => 0,
6 | "online" => 0,
7 | "max_online" => 0
8 | }
9 |
10 | def start_link() do
11 | GenServer.start_link(__MODULE__, [], name: __MODULE__)
12 | end
13 |
14 | def add, do: GenServer.call(__MODULE__, :add)
15 |
16 | def remove, do: GenServer.call(__MODULE__, :remove)
17 |
18 | def state, do: GenServer.call(__MODULE__, :state)
19 |
20 | def init(_args) do
21 | {:ok, @initial_state}
22 | end
23 |
24 | def handle_call(:add, _, state) do
25 | new_online = state["online"] + 1
26 | max_online = case new_online > state["max_online"] do
27 | true -> new_online
28 | false -> state["max_online"]
29 | end
30 | new_state = %{state |
31 | "total" => state["total"] + 1,
32 | "online" => new_online,
33 | "max_online" => max_online
34 | }
35 | Phoenix.PubSub.broadcast(Reph.PubSub, "visitors", %{event: "add"})
36 | {:reply, {:ok, new_state}, new_state}
37 | end
38 | def handle_call(:remove, _, state) do
39 | new_state = %{state |
40 | "online" => state["online"] - 1
41 | }
42 | Phoenix.PubSub.broadcast(Reph.PubSub, "visitors", %{event: "remove"})
43 | {:reply, {:ok, new_state}, new_state}
44 | end
45 | def handle_call(:state, _, state) do
46 | {:reply, state, state}
47 | end
48 | end
--------------------------------------------------------------------------------
/web/static/js/containers/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Provider } from "react-redux";
3 | import { Router, RouterContext, browserHistory, createMemoryHistory, match } from "react-router";
4 |
5 | import configureStore from "../store";
6 | import routes from "../routes";
7 |
8 |
9 | export default class Index extends React.Component {
10 | render() {
11 | let initialState, history, router;
12 | if (typeof window === "undefined") {
13 | initialState = this.props.initial_state;
14 | history = createMemoryHistory();
15 | match({ routes, location: this.props.location, history }, (err, redirect, props) => {
16 | if (props) {
17 | router = ;
18 | }
19 | // Since it's a very basic app, we don't handle any errors, however in real app you will have do this.
20 | // Please, refer to https://github.com/reactjs/react-router/blob/master/docs/guides/ServerRendering.md
21 | // to find more relevant information.
22 | });
23 | } else {
24 | initialState = window.__INITIAL_STATE__;
25 | history = browserHistory;
26 | router =
27 | {routes}
28 | ;
29 | }
30 | const store = configureStore(initialState);
31 |
32 | return (
33 |
34 | {router}
35 |
36 | );
37 | }
38 | }
--------------------------------------------------------------------------------
/web/static/js/app/containers/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Provider } from "react-redux";
3 | import { Router, RouterContext, browserHistory, createMemoryHistory, match } from "react-router";
4 |
5 | import configureStore from "../store";
6 | import routes from "../routes";
7 |
8 |
9 | export default class Index extends React.Component {
10 | render() {
11 | let initialState, history, router;
12 | if (typeof window === "undefined") {
13 | initialState = this.props.initial_state;
14 | history = createMemoryHistory();
15 | match({ routes, location: this.props.location, history }, (err, redirect, props) => {
16 | if (props) {
17 | router = ;
18 | }
19 | // Since it's a very basic app, we don't handle any errors, however in real app you will have do this.
20 | // Please, refer to https://github.com/reactjs/react-router/blob/master/docs/guides/ServerRendering.md
21 | // to find more relevant information.
22 | });
23 | } else {
24 | initialState = window.__INITIAL_STATE__;
25 | history = browserHistory;
26 | router =
27 | {routes}
28 | ;
29 | }
30 | const store = configureStore(initialState);
31 |
32 | return (
33 |
34 | {router}
35 |
36 | );
37 | }
38 | }
--------------------------------------------------------------------------------
/web/static/js/landing/containers/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Provider } from "react-redux";
3 | import { Router, RouterContext, browserHistory, createMemoryHistory, match } from "react-router";
4 |
5 | import configureStore from "../store";
6 | import routes from "../routes";
7 |
8 |
9 | export default class Index extends React.Component {
10 | render() {
11 | let initialState, history, router;
12 | if (typeof window === "undefined") {
13 | initialState = this.props.initial_state;
14 | history = createMemoryHistory();
15 | match({ routes, location: this.props.location, history }, (err, redirect, props) => {
16 | if (props) {
17 | router = ;
18 | }
19 | // Since it's a very basic app, we don't handle any errors, however in real app you will have do this.
20 | // Please, refer to https://github.com/reactjs/react-router/blob/master/docs/guides/ServerRendering.md
21 | // to find more relevant information.
22 | });
23 | } else {
24 | initialState = window.__INITIAL_STATE__;
25 | history = browserHistory;
26 | router =
27 | {routes}
28 | ;
29 | }
30 | const store = configureStore(initialState);
31 |
32 | return (
33 |
34 | {router}
35 |
36 | );
37 | }
38 | }
--------------------------------------------------------------------------------
/web/channels/auth_channel.ex:
--------------------------------------------------------------------------------
1 | defmodule Reph.AuthChannel do
2 | use Reph.Web, :channel
3 |
4 | alias Reph.User
5 |
6 | def join("auth", _params, socket) do
7 | {:ok, socket}
8 | end
9 |
10 | def handle_in("login", params, socket) do
11 | response = case User.signin(params) do
12 | {:ok, user} ->
13 | {:ok, %{"token" => get_sl_token(user)}}
14 | {:error, error} ->
15 | {:error, %{"error" => error}}
16 | end
17 | {:reply, response, socket}
18 | end
19 | def handle_in("signup", params, socket) do
20 | response = case User.signup(params) do
21 | {:ok, user} ->
22 | {:ok, %{"token" => get_sl_token(user)}}
23 | {:error, changeset} ->
24 | errors = Enum.reduce(changeset.errors, %{}, fn {field, detail}, acc ->
25 | Map.put(acc, field, render_detail(field, detail))
26 | end)
27 | {:error, %{"errors" => errors}}
28 | end
29 | {:reply, response, socket}
30 | end
31 |
32 | defp render_detail(_field, {message, values}) do
33 | Enum.reduce(values, message, fn({k, v}, acc) ->
34 | String.replace(acc, "%{#{k}}", to_string(v))
35 | end)
36 | end
37 | defp render_detail(field, "can't be blank" = message), do: "#{field} - #{message}"
38 | defp render_detail(_field, message), do: message
39 |
40 | defp get_sl_token(user) do
41 | ttl = {10, :seconds}
42 | {:ok, jwt, _full_claims} = Guardian.encode_and_sign(user, :disposable, %{"ttl" => ttl})
43 | jwt
44 | end
45 | end
--------------------------------------------------------------------------------
/web/static/js/landing/components/Main/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default class Main extends React.Component {
4 | render() {
5 | return
6 |
7 |
Welcome to Phoenix!
8 |
A productive web framework that
does not compromise speed and maintainability.
9 |
10 |
11 |
42 | ;
43 | }
44 | };
--------------------------------------------------------------------------------
/web/static/js/actions/ws.js:
--------------------------------------------------------------------------------
1 | import { Socket } from 'phoenix';
2 |
3 |
4 | const setupHandlers = (name, channel, dispatch) => {
5 | switch (name) {
6 | case "visitors":
7 | channel.on("init", (msg) => {
8 | dispatch({
9 | type: "VISITORS_INIT",
10 | total: msg.total,
11 | online: msg.online,
12 | max_online: msg.max_online
13 | });
14 | });
15 | channel.on("add", () => {
16 | dispatch({
17 | type: "VISITORS_ADD"
18 | });
19 | });
20 | channel.on("remove", () => {
21 | dispatch({
22 | type: "VISITORS_REMOVE"
23 | });
24 | });
25 | break;
26 | default:
27 | break;
28 | }
29 | }
30 |
31 | export default {
32 | socket_connect: () => {
33 | return (dispatch) => {
34 | const socket = new Socket('/socket', {});
35 | socket.connect();
36 | dispatch({
37 | type: 'SOCKET_CONNECTED',
38 | socket: socket
39 | });
40 | }
41 | },
42 | channel_join: (name, alias = null) => {
43 | alias = alias === null ? name : alias;
44 | return (dispatch, getState) => {
45 | const { ws } = getState();
46 | if (ws.socket !== null) {
47 | const channel = ws.socket.channel(name);
48 | channel.join().receive('ok', () => {
49 | setupHandlers(alias, channel, dispatch);
50 | dispatch({
51 | type: 'CHANNEL_JOINED',
52 | name: alias,
53 | channel: channel
54 | });
55 | });
56 | }
57 | }
58 | }
59 | };
--------------------------------------------------------------------------------
/web/static/js/app/actions/ws.js:
--------------------------------------------------------------------------------
1 | import { Socket } from 'phoenix';
2 |
3 |
4 | const setupHandlers = (name, channel, dispatch) => {
5 | switch (name) {
6 | case "visitors":
7 | channel.on("init", (msg) => {
8 | dispatch({
9 | type: "VISITORS_INIT",
10 | total: msg.total,
11 | online: msg.online,
12 | max_online: msg.max_online
13 | });
14 | });
15 | channel.on("add", () => {
16 | dispatch({
17 | type: "VISITORS_ADD"
18 | });
19 | });
20 | channel.on("remove", () => {
21 | dispatch({
22 | type: "VISITORS_REMOVE"
23 | });
24 | });
25 | break;
26 | default:
27 | break;
28 | }
29 | }
30 |
31 | export default {
32 | socket_connect: () => {
33 | return (dispatch) => {
34 | const socket = new Socket('/socket', {});
35 | socket.connect();
36 | dispatch({
37 | type: 'SOCKET_CONNECTED',
38 | socket: socket
39 | });
40 | }
41 | },
42 | channel_join: (name, alias = null) => {
43 | alias = alias === null ? name : alias;
44 | return (dispatch, getState) => {
45 | const { ws } = getState();
46 | if (ws.socket !== null) {
47 | const channel = ws.socket.channel(name);
48 | channel.join().receive('ok', () => {
49 | setupHandlers(alias, channel, dispatch);
50 | dispatch({
51 | type: 'CHANNEL_JOINED',
52 | name: alias,
53 | channel: channel
54 | });
55 | });
56 | }
57 | }
58 | }
59 | };
--------------------------------------------------------------------------------
/web/static/js/landing/actions/ws.js:
--------------------------------------------------------------------------------
1 | import { Socket } from 'phoenix';
2 |
3 |
4 | const setupHandlers = (name, channel, dispatch) => {
5 | switch (name) {
6 | case "visitors":
7 | channel.on("init", (msg) => {
8 | dispatch({
9 | type: "VISITORS_INIT",
10 | total: msg.total,
11 | online: msg.online,
12 | max_online: msg.max_online
13 | });
14 | });
15 | channel.on("add", () => {
16 | dispatch({
17 | type: "VISITORS_ADD"
18 | });
19 | });
20 | channel.on("remove", () => {
21 | dispatch({
22 | type: "VISITORS_REMOVE"
23 | });
24 | });
25 | break;
26 | default:
27 | break;
28 | }
29 | }
30 |
31 | export default {
32 | socket_connect: () => {
33 | return (dispatch) => {
34 | const socket = new Socket('/socket', {});
35 | socket.connect();
36 | dispatch({
37 | type: 'SOCKET_CONNECTED',
38 | socket: socket
39 | });
40 | }
41 | },
42 | channel_join: (name, alias = null) => {
43 | alias = alias === null ? name : alias;
44 | return (dispatch, getState) => {
45 | const { ws } = getState();
46 | if (ws.socket !== null) {
47 | const channel = ws.socket.channel(name);
48 | channel.join().receive('ok', () => {
49 | setupHandlers(alias, channel, dispatch);
50 | dispatch({
51 | type: 'CHANNEL_JOINED',
52 | name: alias,
53 | channel: channel
54 | });
55 | });
56 | }
57 | }
58 | }
59 | };
--------------------------------------------------------------------------------
/web/static/styles/app/index.less:
--------------------------------------------------------------------------------
1 | @import "~bootstrap/less/bootstrap";
2 |
3 | @color_1: #fff;
4 | body {
5 | padding-top: 50px;
6 | }
7 | .sub-header {
8 | padding-bottom: 10px;
9 | border-bottom: 1px solid #eee;
10 | }
11 | .navbar-fixed-top {
12 | border: 0;
13 | }
14 | .sidebar {
15 | display: none;
16 | }
17 | .nav-sidebar {
18 | margin-right: -21px;
19 | margin-bottom: 20px;
20 | margin-left: -20px;
21 | >li {
22 | >a {
23 | padding-right: 20px;
24 | padding-left: 20px;
25 | }
26 | }
27 | >.active {
28 | >a {
29 | color: @color_1;
30 | background-color: #428bca;
31 | &:hover {
32 | color: @color_1;
33 | background-color: #428bca;
34 | }
35 | &:focus {
36 | color: @color_1;
37 | background-color: #428bca;
38 | }
39 | }
40 | }
41 | }
42 | .main {
43 | padding: 20px;
44 | .page-header {
45 | margin-top: 0;
46 | }
47 | }
48 | .placeholders {
49 | margin-bottom: 30px;
50 | text-align: center;
51 | h4 {
52 | margin-bottom: 0;
53 | }
54 | }
55 | .placeholder {
56 | margin-bottom: 20px;
57 | img {
58 | display: inline-block;
59 | border-radius: 50%;
60 | }
61 | }
62 | @media (min-width: 768px) {
63 | .sidebar {
64 | position: fixed;
65 | top: 51px;
66 | bottom: 0;
67 | left: 0;
68 | z-index: 1000;
69 | display: block;
70 | padding: 20px;
71 | overflow-x: hidden;
72 | overflow-y: auto;
73 | background-color: #f5f5f5;
74 | border-right: 1px solid #eee;
75 | }
76 | .main {
77 | padding-right: 40px;
78 | padding-left: 40px;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/web/static/styles/index.less:
--------------------------------------------------------------------------------
1 | @import "~bootstrap/less/bootstrap";
2 |
3 | /* Space out content a bit */
4 | body, form, ul, table {
5 | margin-top: 20px;
6 | margin-bottom: 20px;
7 | }
8 |
9 | /* Phoenix flash messages */
10 | .alert:empty { display: none; }
11 |
12 | /* Phoenix inline forms in links and buttons */
13 | form.link, form.button {
14 | display: inline;
15 | }
16 |
17 | /* Custom page header */
18 | .header {
19 | border-bottom: 1px solid #e5e5e5;
20 | }
21 | .logo {
22 | width: 519px;
23 | height: 71px;
24 | display: inline-block;
25 | margin-bottom: 1em;
26 | background-image: url("/images/phoenix.png");
27 | background-size: 519px 71px;
28 | }
29 |
30 | /* Everything but the jumbotron gets side spacing for mobile first views */
31 | .header,
32 | .marketing {
33 | padding-right: 15px;
34 | padding-left: 15px;
35 | }
36 |
37 | /* Customize container */
38 | @media (min-width: 768px) {
39 | .container {
40 | max-width: 730px;
41 | }
42 | }
43 | .container-narrow > hr {
44 | margin: 30px 0;
45 | }
46 |
47 | /* Main marketing message */
48 | .jumbotron {
49 | text-align: center;
50 | border-bottom: 1px solid #e5e5e5;
51 | }
52 |
53 | /* Supporting marketing content */
54 | .marketing {
55 | margin: 35px 0;
56 | }
57 |
58 | /* Responsive: Portrait tablets and up */
59 | @media screen and (min-width: 768px) {
60 | /* Remove the padding we set earlier */
61 | .header,
62 | .marketing {
63 | padding-right: 0;
64 | padding-left: 0;
65 | }
66 | /* Space out the masthead */
67 | .header {
68 | margin-bottom: 30px;
69 | }
70 | /* Remove the bottom border on the jumbotron for visual effect */
71 | .jumbotron {
72 | border-bottom: 0;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/test/support/model_case.ex:
--------------------------------------------------------------------------------
1 | defmodule Reph.ModelCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | model tests.
5 |
6 | You may define functions here to be used as helpers in
7 | your model tests. See `errors_on/2`'s definition as reference.
8 |
9 | Finally, if the test case interacts with the database,
10 | it cannot be async. For this reason, every test runs
11 | inside a transaction which is reset at the beginning
12 | of the test unless the test case is marked as async.
13 | """
14 |
15 | use ExUnit.CaseTemplate
16 |
17 | using do
18 | quote do
19 | alias Reph.Repo
20 |
21 | import Ecto
22 | import Ecto.Changeset
23 | import Ecto.Query, only: [from: 1, from: 2]
24 | import Reph.ModelCase
25 | end
26 | end
27 |
28 | setup tags do
29 | unless tags[:async] do
30 | Ecto.Adapters.SQL.restart_test_transaction(Reph.Repo, [])
31 | end
32 |
33 | :ok
34 | end
35 |
36 | @doc """
37 | Helper for returning list of errors in model when passed certain data.
38 |
39 | ## Examples
40 |
41 | Given a User model that lists `:name` as a required field and validates
42 | `:password` to be safe, it would return:
43 |
44 | iex> errors_on(%User{}, %{password: "password"})
45 | [password: "is unsafe", name: "is blank"]
46 |
47 | You could then write your assertion like:
48 |
49 | assert {:password, "is unsafe"} in errors_on(%User{}, %{password: "password"})
50 |
51 | You can also create the changeset manually and retrieve the errors
52 | field directly:
53 |
54 | iex> changeset = User.changeset(%User{}, password: "password")
55 | iex> {:password, "is unsafe"} in changeset.errors
56 | true
57 | """
58 | def errors_on(model, data) do
59 | model.__struct__.changeset(model, data).errors
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Reph.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [app: :reph,
6 | version: "0.0.1",
7 | elixir: "~> 1.0",
8 | elixirc_paths: elixirc_paths(Mix.env),
9 | compilers: [:phoenix, :gettext] ++ Mix.compilers,
10 | build_embedded: Mix.env == :prod,
11 | start_permanent: Mix.env == :prod,
12 | aliases: aliases,
13 | deps: deps]
14 | end
15 |
16 | # Configuration for the OTP application.
17 | #
18 | # Type `mix help compile.app` for more information.
19 | def application do
20 | [mod: {Reph, []},
21 | applications: [:phoenix, :phoenix_html, :cowboy,
22 | :logger, :gettext, :phoenix_ecto, :postgrex,
23 | :std_json_io, :guardian, :comeonin]]
24 | end
25 |
26 | # Specifies which paths to compile per environment.
27 | defp elixirc_paths(:test), do: ["lib", "web", "test/support"]
28 | defp elixirc_paths(_), do: ["lib", "web"]
29 |
30 | # Specifies your project dependencies.
31 | #
32 | # Type `mix help deps` for examples and options.
33 | defp deps do
34 | [{:phoenix, "~> 1.1.4"},
35 | {:postgrex, ">= 0.0.0"},
36 | {:phoenix_ecto, "~> 2.0"},
37 | {:phoenix_html, "~> 2.4"},
38 | {:phoenix_live_reload, "~> 1.0", only: :dev},
39 | {:gettext, "~> 0.9"},
40 | {:cowboy, "~> 1.0"},
41 | {:std_json_io, "~> 0.1"},
42 | {:comeonin, "~> 2.4"},
43 | {:guardian, "~> 0.11"},
44 | {:guardian_db, "~> 0.5"}]
45 | end
46 |
47 | # Aliases are shortcut or tasks specific to the current project.
48 | # For example, to create, migrate and run the seeds file at once:
49 | #
50 | # $ mix ecto.setup
51 | #
52 | # See the documentation for `Mix` for more info on aliases.
53 | defp aliases do
54 | ["ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
55 | "ecto.reset": ["ecto.drop", "ecto.setup"]]
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/web/static/js/app/components/Main/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import { Link, IndexLink } from 'react-router';
4 |
5 | class Main extends React.Component {
6 | render() {
7 | const { visitors } = this.props;
8 | const img_url = (number) => {
9 | return "https://placehold.it/200?text=" + number;
10 | };
11 | return
12 |
13 |
18 |
19 |
Dashboard
20 |
21 |
22 |
23 |
})
24 |
Total
25 |
26 |
27 |
})
28 |
Max. online
29 |
30 |
31 |
})
32 |
Online
33 |
34 |
35 |
36 |
37 |
;
38 | }
39 | };
40 |
41 | const mapStateToProps = (state) => {
42 | return {
43 | visitors: state.visitors
44 | };
45 | };
46 | export default connect(mapStateToProps)(Main);
--------------------------------------------------------------------------------
/web/static/js/landing/components/Login/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 |
4 | import authActions from "actions/auth";
5 |
6 |
7 | class Login extends React.Component {
8 | componentDidMount() {
9 | // Clean up all previously set errors before rendering the element
10 | this.props.dispatch({
11 | type: 'AUTH_LOGIN_ERROR',
12 | value: false
13 | });
14 | }
15 | render() {
16 | let error;
17 | if (this.props.login_failed) {
18 | error =
19 |
Invalid login or password
20 |
;
21 | }
22 | return
23 |
24 |
33 |
34 | ;
35 | }
36 |
37 | handleSubmit(e) {
38 | e.preventDefault();
39 |
40 | const { email, password } = this.refs;
41 | const { dispatch } = this.props;
42 | const params = {email: email.value, password: password.value};
43 |
44 | dispatch(authActions.login(params));
45 | }
46 | };
47 |
48 | const mapStateToProps = (state) => {
49 | return {
50 | login_failed: state.auth.login_failed
51 | };
52 | };
53 | export default connect(mapStateToProps)(Login);
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # RePh 2
2 |
3 | ## Tutorial for the app can be found [here](https://medium.com/@chvanikoff/phoenix-react-love-story-reph-2-14a6dcadbbd0)
4 |
5 | ## [Demo](https://reph2.herokuapp.com/)
6 | (The demo is available 18h a day due to Heroku free nodes limitations, if you see `Application Error` message - this is the case)
7 |
8 | React + Redux + Phoenix simple application
9 |
10 | This application is evolution of [RePh](https://github.com/chvanikoff/reph/) and now it provides authorization and different frontend bundles for landing page and application: styles, JS and images are all different.
11 |
12 | Authorization/registration is done via websockets and one-time JWT is used to get user authorized on server side (this is required for server-side rendering where app can use user's data)
13 |
14 | Tech. stack:
15 |
16 | * Phoenix
17 | * Webpack
18 | * React
19 | * Redux
20 | * JWT
21 |
22 | Batteries included:
23 |
24 | * Authorization
25 | * Different bundles for landing page and app itself
26 | * Server-side rendering
27 | * Bootstrap
28 | * LESS (to ease integration of Bootstrap 3)
29 | * [Redux Devtools for Chrome extension](https://github.com/zalmoxisus/redux-devtools-extension)
30 |
31 | To start your Phoenix app:
32 |
33 | * Install dependencies with `mix deps.get`
34 | * Create and migrate your database with `mix ecto.setup`
35 | * Install Node.js dependencies with `npm install`
36 | * Start Phoenix endpoint with `mix phoenix.server`
37 |
38 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
39 |
40 | Ready to run in production? Please [check our deployment guides](http://www.phoenixframework.org/docs/deployment).
41 |
42 | ## Learn more
43 |
44 | * Official website: http://www.phoenixframework.org/
45 | * Guides: http://phoenixframework.org/docs/overview
46 | * Docs: http://hexdocs.pm/phoenix
47 | * Mailing list: http://groups.google.com/group/phoenix-talk
48 | * Source: https://github.com/phoenixframework/phoenix
--------------------------------------------------------------------------------
/web/web.ex:
--------------------------------------------------------------------------------
1 | defmodule Reph.Web do
2 | @moduledoc """
3 | A module that keeps using definitions for controllers,
4 | views and so on.
5 |
6 | This can be used in your application as:
7 |
8 | use Reph.Web, :controller
9 | use Reph.Web, :view
10 |
11 | The definitions below will be executed for every view,
12 | controller, etc, so keep them short and clean, focused
13 | on imports, uses and aliases.
14 |
15 | Do NOT define functions inside the quoted expressions
16 | below.
17 | """
18 |
19 | def model do
20 | quote do
21 | use Ecto.Schema
22 |
23 | import Ecto
24 | import Ecto.Changeset
25 | import Ecto.Query, only: [from: 1, from: 2]
26 | end
27 | end
28 |
29 | def controller do
30 | quote do
31 | use Phoenix.Controller
32 |
33 | alias Reph.Repo
34 | import Ecto
35 | import Ecto.Query, only: [from: 1, from: 2]
36 |
37 | import Reph.Router.Helpers
38 | import Reph.Gettext
39 | end
40 | end
41 |
42 | def view do
43 | quote do
44 | use Phoenix.View, root: "web/templates"
45 |
46 | # Import convenience functions from controllers
47 | import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1]
48 |
49 | # Use all HTML functionality (forms, tags, etc)
50 | use Phoenix.HTML
51 |
52 | import Reph.Router.Helpers
53 | import Reph.ErrorHelpers
54 | import Reph.Gettext
55 | end
56 | end
57 |
58 | def router do
59 | quote do
60 | use Phoenix.Router
61 | end
62 | end
63 |
64 | def channel do
65 | quote do
66 | use Phoenix.Channel
67 |
68 | alias Reph.Repo
69 | import Ecto
70 | import Ecto.Query, only: [from: 1, from: 2]
71 | import Reph.Gettext
72 | end
73 | end
74 |
75 | @doc """
76 | When used, dispatch to the appropriate controller/view/etc.
77 | """
78 | defmacro __using__(which) when is_atom(which) do
79 | apply(__MODULE__, which, [])
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/web/models/user.ex:
--------------------------------------------------------------------------------
1 | defmodule Reph.User do
2 | use Reph.Web, :model
3 |
4 | alias Reph.Repo
5 |
6 | schema "users" do
7 | field :email, :string
8 | field :password, :string
9 | field :password_plain, :string, virtual: true
10 | field :terms_confirmed, :boolean, virtual: true
11 |
12 | timestamps
13 | end
14 |
15 | @required_fields ~w(email password_plain terms_confirmed)
16 | @optional_fields ~w(password)
17 |
18 | def changeset(model, params \\ :empty) do
19 | model
20 | |> cast(params, @required_fields, @optional_fields)
21 | |> validate_format(:email, ~r/@/, message: "Email format is not valid")
22 | |> validate_length(:password_plain, min: 5, message: "Password should be 5 or more characters long")
23 | |> validate_confirmation(:password_plain, message: "Password confirmation doesn’t match")
24 | |> unique_constraint(:email, message: "This email is already taken")
25 | |> validate_change(:terms_confirmed, fn
26 | _, true -> []
27 | _, _ -> [terms_confirmed: "Please confirm terms and conditions"]
28 | end)
29 | |> cs_encrypt_password()
30 | end
31 |
32 | def signup(params) do
33 | %__MODULE__{}
34 | |> changeset(params)
35 | |> Repo.insert()
36 | end
37 |
38 | def signin(params) do
39 | email = Map.get(params, "email", "")
40 | password = Map.get(params, "password", "")
41 | __MODULE__
42 | |> Repo.get_by(email: String.downcase(email))
43 | |> check_password(password)
44 | end
45 |
46 | defp cs_encrypt_password(%Ecto.Changeset{valid?: true, changes: %{password_plain: pwd}} = cs) do
47 | put_change(cs, :password, Comeonin.Bcrypt.hashpwsalt(pwd))
48 | end
49 | defp cs_encrypt_password(cs), do: cs
50 |
51 | defp check_password(%__MODULE__{password: hash} = user, password) do
52 | case Comeonin.Bcrypt.checkpw(password, hash) do
53 | true -> {:ok, user}
54 | false -> {:error, "Invalid email or password"}
55 | end
56 | end
57 | defp check_password(nil, _password) do
58 | if Mix.env == :prod do
59 | Comeonin.Bcrypt.dummy_checkpw()
60 | end
61 | {:error, "Invalid email or password"}
62 | end
63 | end
--------------------------------------------------------------------------------
/web/static/js/components/Main/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 |
4 |
5 | class Main extends React.Component {
6 | render() {
7 | const { visitors } = this.props;
8 | return
9 |
10 |
Welcome to Phoenix!
11 |
A productive web framework that
does not compromise speed and maintainability.
12 |
13 |
14 |
15 |
16 |
Visitors:
17 |
18 |
19 |
20 |
21 |
Total: {visitors.total}
22 |
23 |
24 |
Max. online: {visitors.max_online}
25 |
26 |
27 |
Online: {visitors.online}
28 |
29 |
30 |
31 |
62 | ;
63 | }
64 | };
65 |
66 | const mapStateToProps = (state) => {
67 | return {
68 | visitors: state.visitors
69 | };
70 | };
71 | export default connect(mapStateToProps)(Main);
--------------------------------------------------------------------------------
/config/prod.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # For production, we configure the host to read the PORT
4 | # from the system environment. Therefore, you will need
5 | # to set PORT=80 before running your server.
6 | #
7 | # You should also configure the url host to something
8 | # meaningful, we use this information when generating URLs.
9 | #
10 | # Finally, we also include the path to a manifest
11 | # containing the digested version of static files. This
12 | # manifest is generated by the mix phoenix.digest task
13 | # which you typically run after static files are built.
14 | config :reph, Reph.Endpoint,
15 | http: [port: {:system, "PORT"}],
16 | url: [host: "example.com", port: 80],
17 | cache_static_manifest: "priv/static/manifest.json"
18 |
19 | # Do not print debug messages in production
20 | config :logger, level: :info
21 |
22 | # ## SSL Support
23 | #
24 | # To get SSL working, you will need to add the `https` key
25 | # to the previous section and set your `:url` port to 443:
26 | #
27 | # config :reph, Reph.Endpoint,
28 | # ...
29 | # url: [host: "example.com", port: 443],
30 | # https: [port: 443,
31 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
32 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")]
33 | #
34 | # Where those two env variables return an absolute path to
35 | # the key and cert in disk or a relative path inside priv,
36 | # for example "priv/ssl/server.key".
37 | #
38 | # We also recommend setting `force_ssl`, ensuring no data is
39 | # ever sent via http, always redirecting to https:
40 | #
41 | # config :reph, Reph.Endpoint,
42 | # force_ssl: [hsts: true]
43 | #
44 | # Check `Plug.SSL` for all available options in `force_ssl`.
45 |
46 | # ## Using releases
47 | #
48 | # If you are doing OTP releases, you need to instruct Phoenix
49 | # to start the server for all endpoints:
50 | #
51 | # config :phoenix, :serve_endpoints, true
52 | #
53 | # Alternatively, you can configure exactly which server to
54 | # start per endpoint:
55 | #
56 | # config :reph, Reph.Endpoint, server: true
57 | #
58 | # You will also need to set the application root to `.` in order
59 | # for the new static assets to be served after a hot upgrade:
60 | #
61 | # config :reph, Reph.Endpoint, root: "."
62 |
63 | # Finally import the config/prod.secret.exs
64 | # which should be versioned separately.
65 | import_config "prod.secret.exs"
66 |
--------------------------------------------------------------------------------
/webpack.app.config.js:
--------------------------------------------------------------------------------
1 | const env = process.env.MIX_ENV === "prod" ? "production" : "development";
2 | const Webpack = require("webpack");
3 | const ExtractTextPlugin = require("extract-text-webpack-plugin");
4 | const Autoprefixer = require("autoprefixer");
5 |
6 | const plugins = {
7 | production: [
8 | new Webpack.optimize.UglifyJsPlugin({
9 | compress: {warnings: false}
10 | })
11 | ],
12 | development: []
13 | }
14 |
15 | const URLLoader = (dir, mimetype, limit) => {
16 | return "url?" + [
17 | `limit=${limit}`,
18 | `mimetype=${mimetype}`,
19 | `name=${dir}/app/[name].[ext]`
20 | ].join("&");
21 | };
22 |
23 | module.exports = {
24 | entry: [
25 | "./web/static/js/app/index.js",
26 | "./web/static/styles/app/index.less"
27 | ],
28 | output: {
29 | path: "./priv/static",
30 | filename: "js/app.js",
31 | publicPath: "/",
32 | },
33 | module: {
34 | loaders: [{
35 | test: /\.js$/,
36 | exclude: /node_modules/,
37 | loader: "babel",
38 | query: {
39 | plugins: ["transform-decorators-legacy"],
40 | presets: ["react", "es2015", "stage-2"],
41 | }
42 | }, {
43 | test: /\.less$/,
44 | loader: ExtractTextPlugin.extract("style", "css?localIdentName=[hash:base64]!postcss!less")
45 | }, {
46 | test: /\.png$/,
47 | loader: URLLoader("images", "image/png", 10000)
48 | }, {
49 | test: /\.gif$/,
50 | loader: URLLoader("images", "image/gif", 10000)
51 | }, {
52 | test: /\.jpg$/,
53 | loader: URLLoader("images", "image/jpeg", 10000)
54 | }, {
55 | test: /\.(woff|woff2)$/,
56 | loader: URLLoader("fonts", "application/x-font-woff", 10000)
57 | }, {
58 | test: /\.ttf$/,
59 | loader: URLLoader("fonts", "application/x-font-ttf", 10000)
60 | }, {
61 | test: /\.eot$/,
62 | loader: URLLoader("fonts", "application/vnd.ms-fontobject", 10000)
63 | }, {
64 | test: /\.svg$/,
65 | loader: URLLoader("fonts", "image/svg+xml", 10000)
66 | }],
67 | },
68 | postcss: [
69 | Autoprefixer({
70 | browsers: ["last 2 versions"]
71 | })
72 | ],
73 | resolve: {
74 | extensions: ["", ".js", ".less", ".css"],
75 | modulesDirectories: ["node_modules", __dirname + "/web/static/js/app"],
76 | alias: {
77 | styles: __dirname + "/web/static/styles/app"
78 | }
79 | },
80 | plugins: [
81 | // Important to keep React file size down
82 | new Webpack.DefinePlugin({
83 | "process.env": {
84 | "NODE_ENV": JSON.stringify(env),
85 | },
86 | }),
87 | new Webpack.optimize.DedupePlugin(),
88 | new ExtractTextPlugin("css/app.css")
89 | ].concat(plugins[env])
90 | };
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const env = process.env.MIX_ENV === "prod" ? "production" : "development";
2 | const Webpack = require("webpack");
3 | const ExtractTextPlugin = require("extract-text-webpack-plugin");
4 | const CopyPlugin = require("copy-webpack-plugin");
5 | const Autoprefixer = require("autoprefixer");
6 |
7 | const plugins = {
8 | production: [
9 | new Webpack.optimize.UglifyJsPlugin({
10 | compress: {warnings: false}
11 | })
12 | ],
13 | development: []
14 | }
15 |
16 | const URLLoader = (dir, mimetype, limit) => {
17 | return "url?" + [
18 | `limit=${limit}`,
19 | `mimetype=${mimetype}`,
20 | `name=${dir}/landing/[name].[ext]`
21 | ].join("&");
22 | };
23 |
24 | module.exports = {
25 | entry: [
26 | "./web/static/js/landing/index.js",
27 | "./web/static/styles/landing/index.less"
28 | ],
29 | output: {
30 | path: "./priv/static",
31 | filename: "js/landing.js",
32 | publicPath: "/",
33 | },
34 | module: {
35 | loaders: [{
36 | test: /\.js$/,
37 | exclude: /node_modules/,
38 | loader: "babel",
39 | query: {
40 | plugins: ["transform-decorators-legacy"],
41 | presets: ["react", "es2015", "stage-2"],
42 | }
43 | }, {
44 | test: /\.less$/,
45 | loader: ExtractTextPlugin.extract("style", "css?localIdentName=[hash:base64]!postcss!less")
46 | }, {
47 | test: /\.png$/,
48 | loader: URLLoader("images", "image/png", 10000)
49 | }, {
50 | test: /\.gif$/,
51 | loader: URLLoader("images", "image/gif", 10000)
52 | }, {
53 | test: /\.jpg$/,
54 | loader: URLLoader("images", "image/jpeg", 10000)
55 | }, {
56 | test: /\.(woff|woff2)$/,
57 | loader: URLLoader("fonts", "application/x-font-woff", 10000)
58 | }, {
59 | test: /\.ttf$/,
60 | loader: URLLoader("fonts", "application/x-font-ttf", 10000)
61 | }, {
62 | test: /\.eot$/,
63 | loader: URLLoader("fonts", "application/vnd.ms-fontobject", 10000)
64 | }, {
65 | test: /\.svg$/,
66 | loader: URLLoader("fonts", "image/svg+xml", 10000)
67 | }],
68 | },
69 | postcss: [
70 | Autoprefixer({
71 | browsers: ["last 2 versions"]
72 | })
73 | ],
74 | resolve: {
75 | extensions: ["", ".js", ".less", ".css"],
76 | modulesDirectories: ["node_modules", __dirname + "/web/static/js/landing"],
77 | alias: {
78 | styles: __dirname + "/web/static/styles/landing"
79 | }
80 | },
81 | plugins: [
82 | // Important to keep React file size down
83 | new Webpack.DefinePlugin({
84 | "process.env": {
85 | "NODE_ENV": JSON.stringify(env),
86 | },
87 | }),
88 | new Webpack.optimize.DedupePlugin(),
89 | new ExtractTextPlugin("css/landing.css"),
90 | new CopyPlugin([{from: "./web/static/assets"}])
91 | ].concat(plugins[env])
92 | };
--------------------------------------------------------------------------------
/web/static/styles/landing/index.less:
--------------------------------------------------------------------------------
1 | @import "~bootstrap/less/bootstrap";
2 |
3 | /* Space out content a bit */
4 | body, form, ul, table {
5 | margin-top: 20px;
6 | margin-bottom: 20px;
7 | }
8 |
9 | /* Phoenix flash messages */
10 | .alert:empty { display: none; }
11 |
12 | /* Phoenix inline forms in links and buttons */
13 | form.link, form.button {
14 | display: inline;
15 | }
16 |
17 | /* Custom page header */
18 | .header {
19 | border-bottom: 1px solid #e5e5e5;
20 | }
21 | .logo {
22 | width: 519px;
23 | height: 71px;
24 | display: inline-block;
25 | margin-bottom: 1em;
26 | background-image: url("/images/landing/phoenix.png");
27 | background-size: 519px 71px;
28 | }
29 |
30 | /* Everything but the jumbotron gets side spacing for mobile first views */
31 | .header,
32 | .marketing {
33 | padding-right: 15px;
34 | padding-left: 15px;
35 | }
36 |
37 | /* Customize container */
38 | @media (min-width: 768px) {
39 | .container {
40 | max-width: 730px;
41 | }
42 | }
43 | .container-narrow > hr {
44 | margin: 30px 0;
45 | }
46 |
47 | /* Main marketing message */
48 | .jumbotron {
49 | text-align: center;
50 | border-bottom: 1px solid #e5e5e5;
51 | }
52 |
53 | /* Supporting marketing content */
54 | .marketing {
55 | margin: 35px 0;
56 | }
57 |
58 | /* Responsive: Portrait tablets and up */
59 | @media screen and (min-width: 768px) {
60 | /* Remove the padding we set earlier */
61 | .header,
62 | .marketing {
63 | padding-right: 0;
64 | padding-left: 0;
65 | }
66 | /* Space out the masthead */
67 | .header {
68 | margin-bottom: 30px;
69 | }
70 | /* Remove the bottom border on the jumbotron for visual effect */
71 | .jumbotron {
72 | border-bottom: 0;
73 | }
74 | }
75 |
76 | .form-auth {
77 | margin: 0 auto;
78 | max-width: 330px;
79 | padding: 15px;
80 | .form-auth-heading {
81 | margin-bottom: 10px;
82 | }
83 | .checkbox {
84 | font-weight: normal;
85 | margin-bottom: 10px;
86 | }
87 | .form-control {
88 | -moz-box-sizing: border-box;
89 | -webkit-box-sizing: border-box;
90 | box-sizing: border-box;
91 | font-size: 16px;
92 | height: auto;
93 | padding: 10px;
94 | position: relative;
95 | &:focus {
96 | z-index: 2;
97 | }
98 | }
99 | .input-first {
100 | border-bottom-left-radius: 0;
101 | border-bottom-right-radius: 0;
102 | margin-bottom: -1px;
103 | }
104 | .input-inner {
105 | border-bottom-left-radius: 0;
106 | border-bottom-right-radius: 0;
107 | border-top-left-radius: 0;
108 | border-top-right-radius: 0;
109 | margin-bottom: -1px;
110 | }
111 | .input-last {
112 | border-top-left-radius: 0;
113 | border-top-right-radius: 0;
114 | margin-bottom: 10px;
115 | }
116 | }
--------------------------------------------------------------------------------
/webpack.landing.config.js:
--------------------------------------------------------------------------------
1 | const env = process.env.MIX_ENV === "prod" ? "production" : "development";
2 | const Webpack = require("webpack");
3 | const ExtractTextPlugin = require("extract-text-webpack-plugin");
4 | const CopyPlugin = require("copy-webpack-plugin");
5 | const Autoprefixer = require("autoprefixer");
6 |
7 | const plugins = {
8 | production: [
9 | new Webpack.optimize.UglifyJsPlugin({
10 | compress: {warnings: false}
11 | })
12 | ],
13 | development: []
14 | }
15 |
16 | const URLLoader = (dir, mimetype, limit) => {
17 | return "url?" + [
18 | `limit=${limit}`,
19 | `mimetype=${mimetype}`,
20 | `name=${dir}/landing/[name].[ext]`
21 | ].join("&");
22 | };
23 |
24 | module.exports = {
25 | entry: [
26 | "./web/static/js/landing/index.js",
27 | "./web/static/styles/landing/index.less"
28 | ],
29 | output: {
30 | path: "./priv/static",
31 | filename: "js/landing.js",
32 | publicPath: "/",
33 | },
34 | module: {
35 | loaders: [{
36 | test: /\.js$/,
37 | exclude: /node_modules/,
38 | loader: "babel",
39 | query: {
40 | plugins: ["transform-decorators-legacy"],
41 | presets: ["react", "es2015", "stage-2"],
42 | }
43 | }, {
44 | test: /\.less$/,
45 | loader: ExtractTextPlugin.extract("style", "css?localIdentName=[hash:base64]!postcss!less")
46 | }, {
47 | test: /\.png$/,
48 | loader: URLLoader("images", "image/png", 10000)
49 | }, {
50 | test: /\.gif$/,
51 | loader: URLLoader("images", "image/gif", 10000)
52 | }, {
53 | test: /\.jpg$/,
54 | loader: URLLoader("images", "image/jpeg", 10000)
55 | }, {
56 | test: /\.(woff|woff2)$/,
57 | loader: URLLoader("fonts", "application/x-font-woff", 10000)
58 | }, {
59 | test: /\.ttf$/,
60 | loader: URLLoader("fonts", "application/x-font-ttf", 10000)
61 | }, {
62 | test: /\.eot$/,
63 | loader: URLLoader("fonts", "application/vnd.ms-fontobject", 10000)
64 | }, {
65 | test: /\.svg$/,
66 | loader: URLLoader("fonts", "image/svg+xml", 10000)
67 | }],
68 | },
69 | postcss: [
70 | Autoprefixer({
71 | browsers: ["last 2 versions"]
72 | })
73 | ],
74 | resolve: {
75 | extensions: ["", ".js", ".less", ".css"],
76 | modulesDirectories: ["node_modules", __dirname + "/web/static/js/landing"],
77 | alias: {
78 | styles: __dirname + "/web/static/styles/landing"
79 | }
80 | },
81 | plugins: [
82 | // Important to keep React file size down
83 | new Webpack.DefinePlugin({
84 | "process.env": {
85 | "NODE_ENV": JSON.stringify(env),
86 | },
87 | }),
88 | new Webpack.optimize.DedupePlugin(),
89 | new ExtractTextPlugin("css/landing.css"),
90 | new CopyPlugin([{from: "./web/static/assets"}])
91 | ].concat(plugins[env])
92 | };
--------------------------------------------------------------------------------
/priv/gettext/errors.pot:
--------------------------------------------------------------------------------
1 | ## This file is a PO Template file. `msgid`s here are often extracted from
2 | ## source code; add new translations manually only if they're dynamic
3 | ## translations that can't be statically extracted. Run `mix
4 | ## gettext.extract` to bring this file up to date. Leave `msgstr`s empty as
5 | ## changing them here as no effect; edit them in PO (`.po`) files instead.
6 |
7 | ## From Ecto.Changeset.cast/4
8 | msgid "can't be blank"
9 | msgstr ""
10 |
11 | ## From Ecto.Changeset.unique_constraint/3
12 | msgid "has already been taken"
13 | msgstr ""
14 |
15 | ## From Ecto.Changeset.put_change/3
16 | msgid "is invalid"
17 | msgstr ""
18 |
19 | ## From Ecto.Changeset.validate_format/3
20 | msgid "has invalid format"
21 | msgstr ""
22 |
23 | ## From Ecto.Changeset.validate_subset/3
24 | msgid "has an invalid entry"
25 | msgstr ""
26 |
27 | ## From Ecto.Changeset.validate_exclusion/3
28 | msgid "is reserved"
29 | msgstr ""
30 |
31 | ## From Ecto.Changeset.validate_confirmation/3
32 | msgid "does not match confirmation"
33 | msgstr ""
34 |
35 | ## From Ecto.Changeset.no_assoc_constraint/3
36 | msgid "is still associated to this entry"
37 | msgstr ""
38 |
39 | msgid "are still associated to this entry"
40 | msgstr ""
41 |
42 | ## From Ecto.Changeset.validate_length/3
43 | msgid "should be %{count} character(s)"
44 | msgid_plural "should be %{count} character(s)"
45 | msgstr[0] ""
46 | msgstr[1] ""
47 |
48 | msgid "should have %{count} item(s)"
49 | msgid_plural "should have %{count} item(s)"
50 | msgstr[0] ""
51 | msgstr[1] ""
52 |
53 | msgid "should be at least %{count} character(s)"
54 | msgid_plural "should be at least %{count} character(s)"
55 | msgstr[0] ""
56 | msgstr[1] ""
57 |
58 | msgid "should have at least %{count} item(s)"
59 | msgid_plural "should have at least %{count} item(s)"
60 | msgstr[0] ""
61 | msgstr[1] ""
62 |
63 | msgid "should be at most %{count} character(s)"
64 | msgid_plural "should be at most %{count} character(s)"
65 | msgstr[0] ""
66 | msgstr[1] ""
67 |
68 | msgid "should have at most %{count} item(s)"
69 | msgid_plural "should have at most %{count} item(s)"
70 | msgstr[0] ""
71 | msgstr[1] ""
72 |
73 | ## From Ecto.Changeset.validate_number/3
74 | msgid "must be less than %{count}"
75 | msgid_plural "must be less than %{count}"
76 | msgstr[0] ""
77 | msgstr[1] ""
78 |
79 | msgid "must be greater than %{count}"
80 | msgid_plural "must be greater than %{count}"
81 | msgstr[0] ""
82 | msgstr[1] ""
83 |
84 | msgid "must be less than or equal to %{count}"
85 | msgid_plural "must be less than or equal to %{count}"
86 | msgstr[0] ""
87 | msgstr[1] ""
88 |
89 | msgid "must be greater than or equal to %{count}"
90 | msgid_plural "must be greater than or equal to %{count}"
91 | msgstr[0] ""
92 | msgstr[1] ""
93 |
94 | msgid "must be equal to %{count}"
95 | msgid_plural "must be equal to %{count}"
96 | msgstr[0] ""
97 | msgstr[1] ""
98 |
--------------------------------------------------------------------------------
/priv/gettext/en/LC_MESSAGES/errors.po:
--------------------------------------------------------------------------------
1 | ## `msgid`s in this file come from POT (.pot) files. Do not add, change, or
2 | ## remove `msgid`s manually here as they're tied to the ones in the
3 | ## corresponding POT file (with the same domain). Use `mix gettext.extract
4 | ## --merge` or `mix gettext.merge` to merge POT files into PO files.
5 | msgid ""
6 | msgstr ""
7 | "Language: en\n"
8 |
9 | ## From Ecto.Changeset.cast/4
10 | msgid "can't be blank"
11 | msgstr ""
12 |
13 | ## From Ecto.Changeset.unique_constraint/3
14 | msgid "has already been taken"
15 | msgstr ""
16 |
17 | ## From Ecto.Changeset.put_change/3
18 | msgid "is invalid"
19 | msgstr ""
20 |
21 | ## From Ecto.Changeset.validate_format/3
22 | msgid "has invalid format"
23 | msgstr ""
24 |
25 | ## From Ecto.Changeset.validate_subset/3
26 | msgid "has an invalid entry"
27 | msgstr ""
28 |
29 | ## From Ecto.Changeset.validate_exclusion/3
30 | msgid "is reserved"
31 | msgstr ""
32 |
33 | ## From Ecto.Changeset.validate_confirmation/3
34 | msgid "does not match confirmation"
35 | msgstr ""
36 |
37 | ## From Ecto.Changeset.no_assoc_constraint/3
38 | msgid "is still associated to this entry"
39 | msgstr ""
40 |
41 | msgid "are still associated to this entry"
42 | msgstr ""
43 |
44 | ## From Ecto.Changeset.validate_length/3
45 | msgid "should be %{count} character(s)"
46 | msgid_plural "should be %{count} character(s)"
47 | msgstr[0] ""
48 | msgstr[1] ""
49 |
50 | msgid "should have %{count} item(s)"
51 | msgid_plural "should have %{count} item(s)"
52 | msgstr[0] ""
53 | msgstr[1] ""
54 |
55 | msgid "should be at least %{count} character(s)"
56 | msgid_plural "should be at least %{count} character(s)"
57 | msgstr[0] ""
58 | msgstr[1] ""
59 |
60 | msgid "should have at least %{count} item(s)"
61 | msgid_plural "should have at least %{count} item(s)"
62 | msgstr[0] ""
63 | msgstr[1] ""
64 |
65 | msgid "should be at most %{count} character(s)"
66 | msgid_plural "should be at most %{count} character(s)"
67 | msgstr[0] ""
68 | msgstr[1] ""
69 |
70 | msgid "should have at most %{count} item(s)"
71 | msgid_plural "should have at most %{count} item(s)"
72 | msgstr[0] ""
73 | msgstr[1] ""
74 |
75 | ## From Ecto.Changeset.validate_number/3
76 | msgid "must be less than %{count}"
77 | msgid_plural "must be less than %{count}"
78 | msgstr[0] ""
79 | msgstr[1] ""
80 |
81 | msgid "must be greater than %{count}"
82 | msgid_plural "must be greater than %{count}"
83 | msgstr[0] ""
84 | msgstr[1] ""
85 |
86 | msgid "must be less than or equal to %{count}"
87 | msgid_plural "must be less than or equal to %{count}"
88 | msgstr[0] ""
89 | msgstr[1] ""
90 |
91 | msgid "must be greater than or equal to %{count}"
92 | msgid_plural "must be greater than or equal to %{count}"
93 | msgstr[0] ""
94 | msgstr[1] ""
95 |
96 | msgid "must be equal to %{count}"
97 | msgid_plural "must be equal to %{count}"
98 | msgstr[0] ""
99 | msgstr[1] ""
100 |
--------------------------------------------------------------------------------
/web/static/js/landing/components/Registration/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 |
4 | import authActions from 'actions/auth';
5 |
6 |
7 | class Registration extends React.Component {
8 | componentDidMount() {
9 | // Clean up all previously set errors before rendering the element
10 | this.props.dispatch({
11 | type: 'AUTH_REGISTRATION_ERROR',
12 | errors: []
13 | });
14 | }
15 |
16 | render() {
17 | const errors = this.buildErrors(this.props.errors);
18 | return
19 |
37 | ;
38 | }
39 |
40 | handleSubmit(e) {
41 | e.preventDefault();
42 |
43 | const data = {
44 | email: this.refs.email.value,
45 | password_plain: this.refs.password.value,
46 | password_plain_confirmation: this.refs.password_confirmation.value,
47 | terms_confirmed: this.refs.terms_confirmation.checked
48 | };
49 | const { dispatch } = this.props;
50 |
51 | dispatch(authActions.register(data));
52 | }
53 |
54 | buildErrors(errors) {
55 | if (errors == []) {
56 | return null;
57 | }
58 | let errors_elements = [];
59 | let key, el, err_key;
60 | let i = 0;
61 | for (err_key in errors) {
62 | i++;
63 | key = `signupError${i}`;
64 | el = {errors[err_key]};
65 | errors_elements.push(el);
66 | }
67 | return ;
70 | }
71 | };
72 |
73 | const mapStateToProps = (state) => {
74 | return {
75 | errors: state.auth.registration_errors
76 | };
77 | };
78 | export default connect(mapStateToProps)(Registration);
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{"base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], []},
2 | "comeonin": {:hex, :comeonin, "2.4.0", "2dc7526b6352f7cf03de79c1e19b5154ac021da48b0c1790a12841ef5ca00340", [:mix, :make, :make], []},
3 | "connection": {:hex, :connection, "1.0.2", "f4a06dd3ecae4141aa66f94ce92ea4c4b8753069472814932f1cadbc3078ab80", [:mix], []},
4 | "cowboy": {:hex, :cowboy, "1.0.4", "a324a8df9f2316c833a470d918aaf73ae894278b8aa6226ce7a9bf699388f878", [:rebar, :make], [{:cowlib, "~> 1.0.0", [hex: :cowlib, optional: false]}, {:ranch, "~> 1.0", [hex: :ranch, optional: false]}]},
5 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], []},
6 | "db_connection": {:hex, :db_connection, "0.2.5", "3e5e28019e0ec744345568d22a2f5206109bff0e2571f4d7819e0d14cf955f3e", [:mix], [{:sbroker, "~> 0.7", [hex: :sbroker, optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: true]}, {:connection, "~> 1.0.2", [hex: :connection, optional: false]}]},
7 | "decimal": {:hex, :decimal, "1.1.2", "79a769d4657b2d537b51ef3c02d29ab7141d2b486b516c109642d453ee08e00c", [:mix], []},
8 | "ecto": {:hex, :ecto, "1.1.5", "5c181b737a650117466b0540aa323198a0fb9d518e52718ac2c299f4047187e1", [:mix], [{:sbroker, "~> 0.7", [hex: :sbroker, optional: true]}, {:postgrex, "~> 0.11.0", [hex: :postgrex, optional: true]}, {:poolboy, "~> 1.4", [hex: :poolboy, optional: false]}, {:poison, "~> 1.0", [hex: :poison, optional: true]}, {:mariaex, "~> 0.5.0 or ~> 0.6.0", [hex: :mariaex, optional: true]}, {:decimal, "~> 1.0", [hex: :decimal, optional: false]}]},
9 | "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], []},
10 | "gettext": {:hex, :gettext, "0.11.0", "80c1dd42d270482418fa158ec5ba073d2980e3718bacad86f3d4ad71d5667679", [:mix], []},
11 | "guardian": {:hex, :guardian, "0.11.1", "8555d357b9499188e90e12063001255ea808d895f1fe94fc10f7b5ac52b0619a", [:mix], [{:uuid, ">=1.1.1", [hex: :uuid, optional: false]}, {:poison, ">= 1.3.0", [hex: :poison, optional: false]}, {:plug, "~> 1.0", [hex: :plug, optional: false]}, {:jose, "~> 1.6", [hex: :jose, optional: false]}]},
12 | "guardian_db": {:hex, :guardian_db, "0.5.0", "19c0a6bb85cd46a23c8f0768b4f8a916148107d525daf2aa272dc5387b536223", [:mix], [{:postgrex, ">= 0.9.1", [hex: :postgrex, optional: true]}, {:guardian, "~> 0.10", [hex: :guardian, optional: false]}, {:ecto, ">= 0.11.0", [hex: :ecto, optional: false]}]},
13 | "jose": {:hex, :jose, "1.7.5", "f19f890f7aaa1261230af691e2f1e0fc60bf44bce7d34c86022d0efa8875628a", [:mix, :rebar], [{:base64url, "~> 0.0.1", [hex: :base64url, optional: false]}]},
14 | "phoenix": {:hex, :phoenix, "1.1.4", "65809fba92eb94377372a5fb5a561197654bb8406e773cc47ca1a031bbe58019", [:mix], [{:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: false]}, {:plug, "~> 1.0", [hex: :plug, optional: false]}, {:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}]},
15 | "phoenix_ecto": {:hex, :phoenix_ecto, "2.0.1", "24532a443b1686150fd14819a4a366eccf41135da2a2ed6ae2f78597828ecb0a", [:mix], [{:poison, "~> 1.3", [hex: :poison, optional: true]}, {:phoenix_html, "~> 2.2", [hex: :phoenix_html, optional: true]}, {:ecto, "~> 1.1.2", [hex: :ecto, optional: false]}]},
16 | "phoenix_html": {:hex, :phoenix_html, "2.5.1", "631053f9e345fecb5c87d9e0ccd807f7266d27e2ee4269817067af425fd81ba8", [:mix], [{:plug, "~> 0.13 or ~> 1.0", [hex: :plug, optional: false]}]},
17 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.0.5", "829218c4152ba1e9848e2bf8e161fcde6b4ec679a516259442561d21fde68d0b", [:mix], [{:phoenix, "~> 1.0 or ~> 1.2-rc", [hex: :phoenix, optional: false]}, {:fs, "~> 0.9.1", [hex: :fs, optional: false]}]},
18 | "plug": {:hex, :plug, "1.1.4", "2eee0e85ad420db96e075b3191d3764d6fff61422b101dc5b02e9cce99cacfc7", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}]},
19 | "poison": {:hex, :poison, "1.5.2", "560bdfb7449e3ddd23a096929fb9fc2122f709bcc758b2d5d5a5c7d0ea848910", [:mix], []},
20 | "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], []},
21 | "porcelain": {:hex, :porcelain, "2.0.1", "9c3db2b47d8cf6879c0d9ac79db8657333974a88faff09e856569e00c1b5e119", [:mix], []},
22 | "postgrex": {:hex, :postgrex, "0.11.1", "f48af70c0a58b9bfd1aaa456ec4273624554cfb837726b6a7f0701da4a94b2dd", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, optional: false]}, {:db_connection, "~> 0.2", [hex: :db_connection, optional: false]}, {:connection, "~> 1.0", [hex: :connection, optional: false]}]},
23 | "ranch": {:hex, :ranch, "1.2.1", "a6fb992c10f2187b46ffd17ce398ddf8a54f691b81768f9ef5f461ea7e28c762", [:make], []},
24 | "std_json_io": {:hex, :std_json_io, "0.1.0", "e099eeb8b62048cfff2c4d0edc6ea69f3d31e95f1d2df88657500f8109c183ed", [:mix], [{:porcelain, "~> 2.0", [hex: :porcelain, optional: false]}, {:poolboy, "~> 1.5.1", [hex: :poolboy, optional: false]}, {:poison, "~> 1.5.0", [hex: :poison, optional: false]}, {:fs, "~> 0.9.1", [hex: :fs, optional: false]}]},
25 | "uuid": {:hex, :uuid, "1.1.3", "06ca38801a1a95b751701ca40716bb97ddf76dfe7e26da0eec7dba636740d57a", [:mix], []}}
26 |
--------------------------------------------------------------------------------