23 | <% end %>
--------------------------------------------------------------------------------
/lib/chat_gdg_web/gettext.ex:
--------------------------------------------------------------------------------
1 | defmodule ChatGdgWeb.Gettext do
2 | @moduledoc """
3 | A module providing Internationalization with a gettext-based API.
4 |
5 | By using [Gettext](https://hexdocs.pm/gettext),
6 | your module gains a set of macros for translations, for example:
7 |
8 | import ChatGdgWeb.Gettext
9 |
10 | # Simple translation
11 | gettext "Here is the string to translate"
12 |
13 | # Plural translation
14 | ngettext "Here is the string to translate",
15 | "Here are the strings to translate",
16 | 3
17 |
18 | # Domain-based translation
19 | dgettext "errors", "Here is the error message to translate"
20 |
21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
22 | """
23 | use Gettext, otp_app: :chat_gdg
24 | end
25 |
--------------------------------------------------------------------------------
/lib/chat_gdg_web/channels/room_channel.ex:
--------------------------------------------------------------------------------
1 | defmodule ChatGdgWeb.RoomChannel do
2 | use ChatGdgWeb, :channel
3 | alias ChatGdgWeb.Presence
4 |
5 | def join("room:lobby", _, socket) do
6 | send self(), :after_join
7 | {:ok, socket}
8 | end
9 |
10 | def handle_info(:after_join, socket) do
11 | Presence.track(socket, socket.assigns.user, %{
12 | online_at: :os.system_time(:milli_seconds)
13 | })
14 | push socket, "presence_state", Presence.list(socket)
15 | {:noreply, socket}
16 | end
17 |
18 |
19 | def handle_in("message:new", message, socket) do
20 | broadcast! socket, "message:new", %{
21 | user: socket.assigns.user,
22 | body: message,
23 | timestamp: :os.system_time(:milli_seconds)
24 | }
25 | {:noreply, socket}
26 | end
27 |
28 | end
--------------------------------------------------------------------------------
/lib/chat_gdg_web/controllers/session_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule ChatGdgWeb.SessionController do
2 | use ChatGdgWeb, :controller
3 | import ChatGdgWeb.Auth
4 | alias ChatGdg.Repo
5 |
6 | def new(conn, _params) do
7 | render(conn, "new.html")
8 | end
9 |
10 | def create(conn, %{"session" => %{"email" => user, "password" => password}}) do
11 | case login_with(conn, user, password, repo: Repo) do
12 | {:ok, conn} ->
13 | logged_user = Guardian.Plug.current_resource(conn)
14 | conn
15 | |> put_flash(:info, "logged in!")
16 | |> redirect(to: page_path(conn, :index))
17 | {:error, _reason, conn} ->
18 | conn
19 | |> put_flash(:error, "Wrong username/password")
20 | |> render("new.html")
21 | end
22 | end
23 |
24 | def delete(conn, _) do
25 | conn
26 | |> Guardian.Plug.sign_out
27 | |> redirect(to: "/")
28 | end
29 |
30 | end
--------------------------------------------------------------------------------
/lib/chat_gdg_web/templates/user/index.html.eex:
--------------------------------------------------------------------------------
1 |
List of users
2 |
3 |
4 |
5 |
6 |
Email
7 |
8 |
9 |
10 |
11 |
12 | <%= for user <- @users do %>
13 |
14 |
<%= user.email %>
15 |
16 | <%= link "Profile", to: user_path(@conn, :show, user), class: "btn btn-primary btn-xs" %>
17 | <%= link "Edit", to: user_path(@conn, :edit, user), class: "btn btn-warning btn-xs" %>
18 | <%= link "Delete User", to: user_path(@conn, :delete, user), method: :delete, data: [confirm: "Are you sure?"], class: "btn btn-danger btn-xs" %>
19 |
20 |
21 |
22 |
23 | <% end %>
24 |
25 |
26 |
27 |
28 | <%= link "New user", to: user_path(@conn, :new), class: "btn btn-info" %>
29 |
30 | <%= if logged_in?(@conn) do %>
31 | <%= link "Chat", to: page_path(@conn, :index), class: "btn btn-success" %>
32 | <% else %>
33 | <% end %>
--------------------------------------------------------------------------------
/lib/chat_gdg/user.ex:
--------------------------------------------------------------------------------
1 | defmodule ChatGdg.User do
2 | use Ecto.Schema
3 | import Ecto.Changeset
4 | alias ChatGdg.User
5 |
6 |
7 | schema "users" do
8 | field :email, :string
9 | field :encrypt_pass, :string
10 | field :password, :string, virtual: true
11 |
12 | timestamps()
13 | end
14 |
15 | @doc false
16 | def changeset(%User{} = user, attrs \\ %{}) do
17 | user
18 | |> cast(attrs, [:email, :password])
19 | |> validate_required([:email, :password])
20 | |> unique_constraint(:email)
21 | end
22 |
23 | def reg_changeset(%User{} = user, attrs \\ %{}) do
24 | user
25 | |> changeset(attrs)
26 | |> cast(attrs, [:password], [])
27 | |> validate_required(:password, min: 5)
28 | |> hash_pw()
29 | end
30 |
31 | defp hash_pw(changeset) do
32 | case changeset do
33 | %Ecto.Changeset{valid?: true, changes: %{password: p}} ->
34 | put_change(changeset, :encrypt_pass, Comeonin.Pbkdf2.hashpwsalt(p))
35 |
36 | _ ->
37 | changeset
38 | end
39 | end
40 |
41 | end
--------------------------------------------------------------------------------
/test/support/channel_case.ex:
--------------------------------------------------------------------------------
1 | defmodule ChatGdgWeb.ChannelCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | channel tests.
5 |
6 | Such tests rely on `Phoenix.ChannelTest` and also
7 | import other functionality to make it easier
8 | to build common datastructures and query the data layer.
9 |
10 | Finally, if the test case interacts with the database,
11 | it cannot be async. For this reason, every test runs
12 | inside a transaction which is reset at the beginning
13 | of the test unless the test case is marked as async.
14 | """
15 |
16 | use ExUnit.CaseTemplate
17 |
18 | using do
19 | quote do
20 | # Import conveniences for testing with channels
21 | use Phoenix.ChannelTest
22 |
23 | # The default endpoint for testing
24 | @endpoint ChatGdgWeb.Endpoint
25 | end
26 | end
27 |
28 |
29 | setup tags do
30 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(ChatGdg.Repo)
31 | unless tags[:async] do
32 | Ecto.Adapters.SQL.Sandbox.mode(ChatGdg.Repo, {:shared, self()})
33 | end
34 | :ok
35 | end
36 |
37 | end
38 |
--------------------------------------------------------------------------------
/lib/chat_gdg/application.ex:
--------------------------------------------------------------------------------
1 | defmodule ChatGdg.Application do
2 | use Application
3 |
4 | # See https://hexdocs.pm/elixir/Application.html
5 | # for more information on OTP Applications
6 | def start(_type, _args) do
7 | import Supervisor.Spec
8 |
9 | # Define workers and child supervisors to be supervised
10 | children = [
11 | # Start the Ecto repository
12 | supervisor(ChatGdg.Repo, []),
13 | # Start the endpoint when the application starts
14 | supervisor(ChatGdgWeb.Endpoint, []),
15 | # Start your own worker by calling: ChatGdg.Worker.start_link(arg1, arg2, arg3)
16 | supervisor(ChatGdgWeb.Presence, []),
17 | # worker(ChatGdg.Worker, [arg1, arg2, arg3]),
18 | ]
19 |
20 | # See https://hexdocs.pm/elixir/Supervisor.html
21 | # for other strategies and supported options
22 | opts = [strategy: :one_for_one, name: ChatGdg.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 | ChatGdgWeb.Endpoint.config_change(changed, removed)
30 | :ok
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/test/support/conn_case.ex:
--------------------------------------------------------------------------------
1 | defmodule ChatGdgWeb.ConnCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | tests that require setting up a connection.
5 |
6 | Such tests rely on `Phoenix.ConnTest` and also
7 | import other functionality to make it easier
8 | to build common datastructures and query the data layer.
9 |
10 | Finally, if the test case interacts with the database,
11 | it cannot be async. For this reason, every test runs
12 | inside a transaction which is reset at the beginning
13 | of the test unless the test case is marked as async.
14 | """
15 |
16 | use ExUnit.CaseTemplate
17 |
18 | using do
19 | quote do
20 | # Import conveniences for testing with connections
21 | use Phoenix.ConnTest
22 | import ChatGdgWeb.Router.Helpers
23 |
24 | # The default endpoint for testing
25 | @endpoint ChatGdgWeb.Endpoint
26 | end
27 | end
28 |
29 |
30 | setup tags do
31 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(ChatGdg.Repo)
32 | unless tags[:async] do
33 | Ecto.Adapters.SQL.Sandbox.mode(ChatGdg.Repo, {:shared, self()})
34 | end
35 | {:ok, conn: Phoenix.ConnTest.build_conn()}
36 | end
37 |
38 | end
39 |
--------------------------------------------------------------------------------
/lib/chat_gdg_web/templates/layout/app.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Hello ChatGdg!
11 | ">
12 |
13 |
14 |
15 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/lib/chat_gdg_web/channels/user_socket.ex:
--------------------------------------------------------------------------------
1 | defmodule ChatGdgWeb.UserSocket do
2 | use Phoenix.Socket
3 |
4 | ## Channels
5 | channel "room:*", ChatGdgWeb.RoomChannel
6 |
7 | ## Transports
8 | transport :websocket, Phoenix.Transports.WebSocket
9 | # transport :longpoll, Phoenix.Transports.LongPoll
10 |
11 | # Socket params are passed from the client and can
12 | # be used to verify and authenticate a user. After
13 | # verification, you can put default assigns into
14 | # the socket that will be set for all channels, ie
15 | #
16 | # {:ok, assign(socket, :user_id, verified_user_id)}
17 | #
18 | # To deny connection, return `:error`.
19 | #
20 | # See `Phoenix.Token` documentation for examples in
21 | # performing token verification on connect.
22 | def connect(%{"user" => user}, socket) do
23 | {:ok, assign(socket, :user, user)}
24 | end
25 |
26 | # Socket id's are topics that allow you to identify all sockets for a given user:
27 | #
28 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}"
29 | #
30 | # Would allow you to broadcast a "disconnect" event and terminate
31 | # all active sockets and channels for a given user:
32 | #
33 | # ChatGdgWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
34 | #
35 | # Returning `nil` makes this socket anonymous.
36 | def id(_socket), do: nil
37 | end
38 |
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | # This file is responsible for configuring your application
2 | # and its dependencies with the aid of the Mix.Config module.
3 | #
4 | # This configuration file is loaded before any dependency and
5 | # is restricted to this project.
6 | use Mix.Config
7 |
8 | # General application configuration
9 | config :chat_gdg,
10 | ecto_repos: [ChatGdg.Repo]
11 |
12 | # Configures the endpoint
13 | config :chat_gdg, ChatGdgWeb.Endpoint,
14 | url: [host: "localhost"],
15 | secret_key_base: "NlJYR/Y1zE93ITLRD+SnV6Uv8Z1XCBjzTEcH/kPsZKmcnYwoNoDDzj4XZeFUq85w",
16 | render_errors: [view: ChatGdgWeb.ErrorView, accepts: ~w(html json)],
17 | pubsub: [name: ChatGdg.PubSub,
18 | adapter: Phoenix.PubSub.PG2]
19 |
20 | # Configures Elixir's Logger
21 | config :logger, :console,
22 | format: "$time $metadata[$level] $message\n",
23 | metadata: [:request_id]
24 |
25 | # Import environment specific config. This must remain at the bottom
26 | # of this file so it overrides the configuration defined above.
27 | config :guardian, Guardian,
28 | allowed_algos: ["HS512"], # optional
29 | verify_module: Guardian.JWT, # optional
30 | issuer: "ChatGdg",
31 | ttl: {30, :days},
32 | allowed_drift: 2000,
33 | verify_issuer: true, # optional
34 | secret_key: "dY3otygFOMcX1zXEwQ11JFIQdZp0Z+C0xEF1lx5gOpef/mYrsWu28dW++FBvm7qi",
35 | serializer: ChatGdg.GuardianSerializer
36 |
37 | import_config "#{Mix.env}.exs"
38 |
--------------------------------------------------------------------------------
/lib/chat_gdg_web/views/error_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule ChatGdgWeb.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 | Enum.map(Keyword.get_values(form.errors, field), fn (error) ->
13 | content_tag :span, translate_error(error), class: "help-block"
14 | end)
15 | end
16 |
17 | @doc """
18 | Translates an error message using gettext.
19 | """
20 | def translate_error({msg, opts}) do
21 | # Because error messages were defined within Ecto, we must
22 | # call the Gettext module passing our Gettext backend. We
23 | # also use the "errors" domain as translations are placed
24 | # in the errors.po file.
25 | # Ecto will pass the :count keyword if the error message is
26 | # meant to be pluralized.
27 | # On your own code and templates, depending on whether you
28 | # need the message to be pluralized or not, this could be
29 | # written simply as:
30 | #
31 | # dngettext "errors", "1 file", "%{count} files", count
32 | # dgettext "errors", "is invalid"
33 | #
34 | if count = opts[:count] do
35 | Gettext.dngettext(ChatGdgWeb.Gettext, "errors", msg, msg, count, opts)
36 | else
37 | Gettext.dgettext(ChatGdgWeb.Gettext, "errors", msg, opts)
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/test/support/data_case.ex:
--------------------------------------------------------------------------------
1 | defmodule ChatGdg.DataCase do
2 | @moduledoc """
3 | This module defines the setup for tests requiring
4 | access to the application's data layer.
5 |
6 | You may define functions here to be used as helpers in
7 | your tests.
8 |
9 | Finally, if the test case interacts with the database,
10 | it cannot be async. For this reason, every test runs
11 | inside a transaction which is reset at the beginning
12 | of the test unless the test case is marked as async.
13 | """
14 |
15 | use ExUnit.CaseTemplate
16 |
17 | using do
18 | quote do
19 | alias ChatGdg.Repo
20 |
21 | import Ecto
22 | import Ecto.Changeset
23 | import Ecto.Query
24 | import ChatGdg.DataCase
25 | end
26 | end
27 |
28 | setup tags do
29 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(ChatGdg.Repo)
30 |
31 | unless tags[:async] do
32 | Ecto.Adapters.SQL.Sandbox.mode(ChatGdg.Repo, {:shared, self()})
33 | end
34 |
35 | :ok
36 | end
37 |
38 | @doc """
39 | A helper that transform changeset errors to a map of messages.
40 |
41 | assert {:error, changeset} = Accounts.create_user(%{password: "short"})
42 | assert "password is too short" in errors_on(changeset).password
43 | assert %{password: ["password is too short"]} = errors_on(changeset)
44 |
45 | """
46 | def errors_on(changeset) do
47 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
48 | Enum.reduce(opts, message, fn {key, value}, acc ->
49 | String.replace(acc, "%{#{key}}", to_string(value))
50 | end)
51 | end)
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/assets/brunch-config.js:
--------------------------------------------------------------------------------
1 | exports.config = {
2 | // See http://brunch.io/#documentation for docs.
3 | files: {
4 | javascripts: {
5 | joinTo: "js/app.js"
6 |
7 | // To use a separate vendor.js bundle, specify two files path
8 | // http://brunch.io/docs/config#-files-
9 | // joinTo: {
10 | // "js/app.js": /^js/,
11 | // "js/vendor.js": /^(?!js)/
12 | // }
13 | //
14 | // To change the order of concatenation of files, explicitly mention here
15 | // order: {
16 | // before: [
17 | // "vendor/js/jquery-2.1.1.js",
18 | // "vendor/js/bootstrap.min.js"
19 | // ]
20 | // }
21 | },
22 | stylesheets: {
23 | joinTo: "css/app.css"
24 | },
25 | templates: {
26 | joinTo: "js/app.js"
27 | }
28 | },
29 |
30 | conventions: {
31 | // This option sets where we should place non-css and non-js assets in.
32 | // By default, we set this to "/assets/static". Files in this directory
33 | // will be copied to `paths.public`, which is "priv/static" by default.
34 | assets: /^(static)/
35 | },
36 |
37 | // Phoenix paths configuration
38 | paths: {
39 | // Dependencies and current project directories to watch
40 | watched: ["static", "css", "js", "vendor"],
41 | // Where to compile files to
42 | public: "../priv/static"
43 | },
44 |
45 | // Configure your plugins
46 | plugins: {
47 | babel: {
48 | // Do not use ES6 compiler in vendor code
49 | ignore: [/vendor/]
50 | }
51 | },
52 |
53 | modules: {
54 | autoRequire: {
55 | "js/app.js": ["js/app"]
56 | }
57 | },
58 |
59 | npm: {
60 | enabled: true
61 | }
62 | };
63 |
--------------------------------------------------------------------------------
/lib/chat_gdg_web/router.ex:
--------------------------------------------------------------------------------
1 | defmodule ChatGdgWeb.Router do
2 | use ChatGdgWeb, :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 | plug Guardian.Plug.VerifySession
11 | plug Guardian.Plug.LoadResource
12 | end
13 |
14 | # define a new pipeline (browser authentication)
15 | pipeline :browser_auth do
16 | plug Guardian.Plug.VerifySession
17 | plug Guardian.Plug.EnsureAuthenticated, handler: ChatGdgWeb.Token
18 | plug Guardian.Plug.LoadResource
19 | end
20 |
21 | pipeline :api do
22 | plug :accepts, ["json"]
23 | end
24 |
25 | scope "/", ChatGdgWeb do
26 | pipe_through :browser
27 |
28 | # unauthorized users can only trig the new and create functions
29 | resources "/users", UserController, [:new, :create]
30 |
31 | # let's generate create and delete operations for sessions
32 | resources "/sessions", SessionController, only: [:create, :delete]
33 |
34 | # now we redirect the root path to SessionController to check users session
35 | get "/", SessionController, :new
36 | end
37 |
38 | # define a new pipeline which uses both :browser and :browser_auth for authenticated users
39 | scope "/", ChatGdgWeb do
40 | pipe_through [:browser, :browser_auth]
41 |
42 | # authenticated users can only trig the show, onde
43 | resources "/users", UserController, only: [:show, :index, :update]
44 |
45 | # in here we direct the authenticated users to chat screen with Page Controller
46 | get "/chat", PageController, :index
47 | end
48 |
49 | # Other scopes may use custom stacks.
50 | # scope "/api", ChatGdgWeb do
51 | # pipe_through :api
52 | # end
53 | end
54 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule ChatGdg.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :chat_gdg,
7 | version: "0.0.1",
8 | elixir: "~> 1.4",
9 | elixirc_paths: elixirc_paths(Mix.env),
10 | compilers: [:phoenix, :gettext] ++ Mix.compilers,
11 | start_permanent: Mix.env == :prod,
12 | aliases: aliases(),
13 | deps: deps()
14 | ]
15 | end
16 |
17 | # Configuration for the OTP application.
18 | #
19 | # Type `mix help compile.app` for more information.
20 | def application do
21 | [
22 | mod: {ChatGdg.Application, []},
23 | extra_applications: [:logger, :runtime_tools]
24 | ]
25 | end
26 |
27 | # Specifies which paths to compile per environment.
28 | defp elixirc_paths(:test), do: ["lib", "test/support"]
29 | defp elixirc_paths(_), do: ["lib"]
30 |
31 | # Specifies your project dependencies.
32 | #
33 | # Type `mix help deps` for examples and options.
34 | defp deps do
35 | [
36 | {:phoenix, "~> 1.3.0"},
37 | {:phoenix_pubsub, "~> 1.0"},
38 | {:phoenix_ecto, "~> 3.2"},
39 | {:postgrex, ">= 0.0.0"},
40 | {:phoenix_html, "~> 2.10"},
41 | {:phoenix_live_reload, "~> 1.0", only: :dev},
42 | {:gettext, "~> 0.11"},
43 | {:cowboy, "~> 1.0"},
44 | {:comeonin, "~> 3.0"},
45 | {:guardian, "~> 0.14"}
46 | ]
47 | end
48 |
49 | # Aliases are shortcuts or tasks specific to the current project.
50 | # For example, to create, migrate and run the seeds file at once:
51 | #
52 | # $ mix ecto.setup
53 | #
54 | # See the documentation for `Mix` for more info on aliases.
55 | defp aliases do
56 | [
57 | "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
58 | "ecto.reset": ["ecto.drop", "ecto.setup"],
59 | "test": ["ecto.create --quiet", "ecto.migrate", "test"]
60 | ]
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/lib/chat_gdg_web/endpoint.ex:
--------------------------------------------------------------------------------
1 | defmodule ChatGdgWeb.Endpoint do
2 | use Phoenix.Endpoint, otp_app: :chat_gdg
3 |
4 | socket "/socket", ChatGdgWeb.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: :chat_gdg, gzip: false,
12 | only: ~w(css fonts images js favicon.ico robots.txt)
13 |
14 | # Code reloading can be explicitly enabled under the
15 | # :code_reloader configuration of your endpoint.
16 | if code_reloading? do
17 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
18 | plug Phoenix.LiveReloader
19 | plug Phoenix.CodeReloader
20 | end
21 |
22 | plug Plug.RequestId
23 | plug Plug.Logger
24 |
25 | plug Plug.Parsers,
26 | parsers: [:urlencoded, :multipart, :json],
27 | pass: ["*/*"],
28 | json_decoder: Poison
29 |
30 | plug Plug.MethodOverride
31 | plug Plug.Head
32 |
33 | # The session will be stored in the cookie and signed,
34 | # this means its contents can be read but not tampered with.
35 | # Set :encryption_salt if you would also like to encrypt it.
36 | plug Plug.Session,
37 | store: :cookie,
38 | key: "_chat_gdg_key",
39 | signing_salt: "1A9Jo5Dn"
40 |
41 | plug ChatGdgWeb.Router
42 |
43 | @doc """
44 | Callback invoked for dynamically configuring the endpoint.
45 |
46 | It receives the endpoint configuration and checks if
47 | configuration should be loaded from the system environment.
48 | """
49 | def init(_key, config) do
50 | if config[:load_from_system_env] do
51 | port = System.get_env("PORT") || raise "expected the PORT environment variable to be set"
52 | {:ok, Keyword.put(config, :http, [:inet6, port: port])}
53 | else
54 | {:ok, config}
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/lib/chat_gdg_web.ex:
--------------------------------------------------------------------------------
1 | defmodule ChatGdgWeb do
2 | @moduledoc """
3 | The entrypoint for defining your web interface, such
4 | as controllers, views, channels and so on.
5 |
6 | This can be used in your application as:
7 |
8 | use ChatGdgWeb, :controller
9 | use ChatGdgWeb, :view
10 |
11 | The definitions below will be executed for every view,
12 | controller, etc, so keep them short and clean, focused
13 | on imports, uses and aliases.
14 |
15 | Do NOT define functions inside the quoted expressions
16 | below. Instead, define any helper function in modules
17 | and import those modules here.
18 | """
19 |
20 | def controller do
21 | quote do
22 | use Phoenix.Controller, namespace: ChatGdgWeb
23 | import Plug.Conn
24 | import ChatGdgWeb.Router.Helpers
25 | import ChatGdgWeb.Gettext
26 | end
27 | end
28 |
29 | def view do
30 | quote do
31 | use Phoenix.View, root: "lib/chat_gdg_web/templates",
32 | namespace: ChatGdgWeb
33 |
34 | # Import convenience functions from controllers
35 | import Phoenix.Controller, only: [get_flash: 2, view_module: 1]
36 |
37 | # Use all HTML functionality (forms, tags, etc)
38 | use Phoenix.HTML
39 |
40 | import ChatGdgWeb.Router.Helpers
41 | import ChatGdgWeb.ErrorHelpers
42 | import ChatGdgWeb.Gettext
43 | import ChatGdgWeb.ViewHelper
44 | end
45 | end
46 |
47 | def router do
48 | quote do
49 | use Phoenix.Router
50 | import Plug.Conn
51 | import Phoenix.Controller
52 | end
53 | end
54 |
55 | def channel do
56 | quote do
57 | use Phoenix.Channel
58 | import ChatGdgWeb.Gettext
59 | end
60 | end
61 |
62 | @doc """
63 | When used, dispatch to the appropriate controller/view/etc.
64 | """
65 | defmacro __using__(which) when is_atom(which) do
66 | apply(__MODULE__, which, [])
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/config/dev.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # For development, we disable any cache and enable
4 | # debugging and code reloading.
5 | #
6 | # The watchers configuration can be used to run external
7 | # watchers to your application. For example, we use it
8 | # with brunch.io to recompile .js and .css sources.
9 | config :chat_gdg, ChatGdgWeb.Endpoint,
10 | http: [port: 4000],
11 | debug_errors: true,
12 | code_reloader: true,
13 | check_origin: false,
14 | watchers: [node: ["node_modules/brunch/bin/brunch", "watch", "--stdin",
15 | cd: Path.expand("../assets", __DIR__)]]
16 |
17 | # ## SSL Support
18 | #
19 | # In order to use HTTPS in development, a self-signed
20 | # certificate can be generated by running the following
21 | # command from your terminal:
22 | #
23 | # openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" -keyout priv/server.key -out priv/server.pem
24 | #
25 | # The `http:` config above can be replaced with:
26 | #
27 | # https: [port: 4000, keyfile: "priv/server.key", certfile: "priv/server.pem"],
28 | #
29 | # If desired, both `http:` and `https:` keys can be
30 | # configured to run both http and https servers on
31 | # different ports.
32 |
33 | # Watch static and templates for browser reloading.
34 | config :chat_gdg, ChatGdgWeb.Endpoint,
35 | live_reload: [
36 | patterns: [
37 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
38 | ~r{priv/gettext/.*(po)$},
39 | ~r{lib/chat_gdg_web/views/.*(ex)$},
40 | ~r{lib/chat_gdg_web/templates/.*(eex)$}
41 | ]
42 | ]
43 |
44 | # Do not include metadata nor timestamps in development logs
45 | config :logger, :console, format: "[$level] $message\n"
46 |
47 | # Set a higher stacktrace during development. Avoid configuring such
48 | # in production as building large stacktraces may be expensive.
49 | config :phoenix, :stacktrace_depth, 20
50 |
51 | # Configure your database
52 | config :chat_gdg, ChatGdg.Repo,
53 | adapter: Ecto.Adapters.Postgres,
54 | username: "postgres",
55 | password: "postgres",
56 | database: "chat_gdg_dev",
57 | hostname: "localhost",
58 | pool_size: 10
59 |
--------------------------------------------------------------------------------
/assets/js/app.js:
--------------------------------------------------------------------------------
1 | import "phoenix_html"
2 | import {Socket, Presence} from "phoenix"
3 |
4 | // get the user element from index.html
5 | let user = document.getElementById("User").innerText
6 |
7 | // generate a socket connection with the user parameter
8 | let socket = new Socket("/socket", {params: {user: user}})
9 | socket.connect()
10 |
11 | // create an empty js object to handle presences
12 | let presences = {}
13 |
14 | let formatTimestamp = (timestamp) => {
15 | let date = new Date(timestamp)
16 | return date.toLocaleTimeString()
17 | }
18 | let listBy = (user, {metas: metas}) => {
19 | return {
20 | user: user,
21 | onlineAt: formatTimestamp(metas[0].online_at)
22 | }
23 | }
24 |
25 | // get the UserList element from index.html
26 | let userList = document.getElementById("UserList")
27 | let render = (presences) => {
28 | userList.innerHTML = Presence.list(presences, listBy)
29 | .map(presence => `
30 |
31 | ${presence.user}
32 | online since ${presence.onlineAt}
33 |
34 | `)
35 | .join("")
36 | }
37 |
38 | // handle with the single channel
39 | // create a connection between client and room:lobby channel and set the presences settings
40 | let room = socket.channel("room:lobby", {})
41 | room.on("presence_state", state => {
42 | presences = Presence.syncState(presences, state)
43 | render(presences)
44 | })
45 |
46 | room.on("presence_diff", diff => {
47 | presences = Presence.syncDiff(presences, diff)
48 | render(presences)
49 | })
50 |
51 | // join the room
52 | room.join()
53 |
54 | // get the NewMessage element from index.html
55 | let messageInput = document.getElementById("NewMessage")
56 | messageInput.addEventListener("keypress", (e) => {
57 | if (e.keyCode == 13 && messageInput.value != "") {
58 | room.push("message:new", messageInput.value)
59 | messageInput.value = ""
60 | }
61 | })
62 |
63 | let messageList = document.getElementById("MessageList")
64 | let renderMessage = (message) => {
65 | let messageElement = document.createElement("li")
66 | messageElement.innerHTML = `
67 | ${message.user}
68 | ${formatTimestamp(message.timestamp)}
69 |
${message.body}
70 | `
71 | messageList.appendChild(messageElement)
72 | messageList.scrollTop = messageList.scrollHeight;
73 | }
74 |
75 | room.on("message:new", message => renderMessage(message))
--------------------------------------------------------------------------------
/assets/js/socket.js:
--------------------------------------------------------------------------------
1 | // NOTE: The contents of this file will only be executed if
2 | // you uncomment its entry in "assets/js/app.js".
3 |
4 | // To use Phoenix channels, the first step is to import Socket
5 | // and connect at the socket path in "lib/web/endpoint.ex":
6 | import {Socket} from "phoenix"
7 |
8 | let socket = new Socket("/socket", {params: {token: window.userToken}})
9 |
10 | // When you connect, you'll often need to authenticate the client.
11 | // For example, imagine you have an authentication plug, `MyAuth`,
12 | // which authenticates the session and assigns a `:current_user`.
13 | // If the current user exists you can assign the user's token in
14 | // the connection for use in the layout.
15 | //
16 | // In your "lib/web/router.ex":
17 | //
18 | // pipeline :browser do
19 | // ...
20 | // plug MyAuth
21 | // plug :put_user_token
22 | // end
23 | //
24 | // defp put_user_token(conn, _) do
25 | // if current_user = conn.assigns[:current_user] do
26 | // token = Phoenix.Token.sign(conn, "user socket", current_user.id)
27 | // assign(conn, :user_token, token)
28 | // else
29 | // conn
30 | // end
31 | // end
32 | //
33 | // Now you need to pass this token to JavaScript. You can do so
34 | // inside a script tag in "lib/web/templates/layout/app.html.eex":
35 | //
36 | //
37 | //
38 | // You will need to verify the user token in the "connect/2" function
39 | // in "lib/web/channels/user_socket.ex":
40 | //
41 | // def connect(%{"token" => token}, socket) do
42 | // # max_age: 1209600 is equivalent to two weeks in seconds
43 | // case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do
44 | // {:ok, user_id} ->
45 | // {:ok, assign(socket, :user, user_id)}
46 | // {:error, reason} ->
47 | // :error
48 | // end
49 | // end
50 | //
51 | // Finally, pass the token on connect as below. Or remove it
52 | // from connect if you don't care about authentication.
53 |
54 | socket.connect()
55 |
56 | // Now that you are connected, you can join channels with a topic:
57 | let channel = socket.channel("topic:subtopic", {})
58 | channel.join()
59 | .receive("ok", resp => { console.log("Joined successfully", resp) })
60 | .receive("error", resp => { console.log("Unable to join", resp) })
61 |
62 | export default socket
63 |
--------------------------------------------------------------------------------
/config/prod.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # For production, we often load configuration from external
4 | # sources, such as your system environment. For this reason,
5 | # you won't find the :http configuration below, but set inside
6 | # ChatGdgWeb.Endpoint.init/2 when load_from_system_env is
7 | # true. Any dynamic configuration should be done there.
8 | #
9 | # Don't forget to configure the url host to something meaningful,
10 | # Phoenix uses this information when generating URLs.
11 | #
12 | # Finally, we also include the path to a cache manifest
13 | # containing the digested version of static files. This
14 | # manifest is generated by the mix phx.digest task
15 | # which you typically run after static files are built.
16 | config :chat_gdg, ChatGdgWeb.Endpoint,
17 | load_from_system_env: true,
18 | url: [host: "example.com", port: 80],
19 | cache_static_manifest: "priv/static/cache_manifest.json"
20 |
21 | # Do not print debug messages in production
22 | config :logger, level: :info
23 |
24 | # ## SSL Support
25 | #
26 | # To get SSL working, you will need to add the `https` key
27 | # to the previous section and set your `:url` port to 443:
28 | #
29 | # config :chat_gdg, ChatGdgWeb.Endpoint,
30 | # ...
31 | # url: [host: "example.com", port: 443],
32 | # https: [:inet6,
33 | # port: 443,
34 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
35 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")]
36 | #
37 | # Where those two env variables return an absolute path to
38 | # the key and cert in disk or a relative path inside priv,
39 | # for example "priv/ssl/server.key".
40 | #
41 | # We also recommend setting `force_ssl`, ensuring no data is
42 | # ever sent via http, always redirecting to https:
43 | #
44 | # config :chat_gdg, ChatGdgWeb.Endpoint,
45 | # force_ssl: [hsts: true]
46 | #
47 | # Check `Plug.SSL` for all available options in `force_ssl`.
48 |
49 | # ## Using releases
50 | #
51 | # If you are doing OTP releases, you need to instruct Phoenix
52 | # to start the server for all endpoints:
53 | #
54 | # config :phoenix, :serve_endpoints, true
55 | #
56 | # Alternatively, you can configure exactly which server to
57 | # start per endpoint:
58 | #
59 | # config :chat_gdg, ChatGdgWeb.Endpoint, server: true
60 | #
61 |
62 | # Finally import the config/prod.secret.exs
63 | # which should be versioned separately.
64 | import_config "prod.secret.exs"
65 |
--------------------------------------------------------------------------------
/priv/gettext/en/LC_MESSAGES/errors.po:
--------------------------------------------------------------------------------
1 | ## `msgid`s in this file come from POT (.pot) files.
2 | ##
3 | ## Do not add, change, or remove `msgid`s manually here as
4 | ## they're tied to the ones in the corresponding POT file
5 | ## (with the same domain).
6 | ##
7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge`
8 | ## to merge POT files into PO files.
9 | msgid ""
10 | msgstr ""
11 | "Language: en\n"
12 |
13 | ## From Ecto.Changeset.cast/4
14 | msgid "can't be blank"
15 | msgstr ""
16 |
17 | ## From Ecto.Changeset.unique_constraint/3
18 | msgid "has already been taken"
19 | msgstr ""
20 |
21 | ## From Ecto.Changeset.put_change/3
22 | msgid "is invalid"
23 | msgstr ""
24 |
25 | ## From Ecto.Changeset.validate_acceptance/3
26 | msgid "must be accepted"
27 | msgstr ""
28 |
29 | ## From Ecto.Changeset.validate_format/3
30 | msgid "has invalid format"
31 | msgstr ""
32 |
33 | ## From Ecto.Changeset.validate_subset/3
34 | msgid "has an invalid entry"
35 | msgstr ""
36 |
37 | ## From Ecto.Changeset.validate_exclusion/3
38 | msgid "is reserved"
39 | msgstr ""
40 |
41 | ## From Ecto.Changeset.validate_confirmation/3
42 | msgid "does not match confirmation"
43 | msgstr ""
44 |
45 | ## From Ecto.Changeset.no_assoc_constraint/3
46 | msgid "is still associated with this entry"
47 | msgstr ""
48 |
49 | msgid "are still associated with this entry"
50 | msgstr ""
51 |
52 | ## From Ecto.Changeset.validate_length/3
53 | msgid "should be %{count} character(s)"
54 | msgid_plural "should be %{count} character(s)"
55 | msgstr[0] ""
56 | msgstr[1] ""
57 |
58 | msgid "should have %{count} item(s)"
59 | msgid_plural "should have %{count} item(s)"
60 | msgstr[0] ""
61 | msgstr[1] ""
62 |
63 | msgid "should be at least %{count} character(s)"
64 | msgid_plural "should be at least %{count} character(s)"
65 | msgstr[0] ""
66 | msgstr[1] ""
67 |
68 | msgid "should have at least %{count} item(s)"
69 | msgid_plural "should have at least %{count} item(s)"
70 | msgstr[0] ""
71 | msgstr[1] ""
72 |
73 | msgid "should be at most %{count} character(s)"
74 | msgid_plural "should be at most %{count} character(s)"
75 | msgstr[0] ""
76 | msgstr[1] ""
77 |
78 | msgid "should have at most %{count} item(s)"
79 | msgid_plural "should have at most %{count} item(s)"
80 | msgstr[0] ""
81 | msgstr[1] ""
82 |
83 | ## From Ecto.Changeset.validate_number/3
84 | msgid "must be less than %{number}"
85 | msgstr ""
86 |
87 | msgid "must be greater than %{number}"
88 | msgstr ""
89 |
90 | msgid "must be less than or equal to %{number}"
91 | msgstr ""
92 |
93 | msgid "must be greater than or equal to %{number}"
94 | msgstr ""
95 |
96 | msgid "must be equal to %{number}"
97 | msgstr ""
98 |
--------------------------------------------------------------------------------
/priv/gettext/errors.pot:
--------------------------------------------------------------------------------
1 | ## This file is a PO Template file.
2 | ##
3 | ## `msgid`s here are often extracted from source code.
4 | ## Add new translations manually only if they're dynamic
5 | ## translations that can't be statically extracted.
6 | ##
7 | ## Run `mix gettext.extract` to bring this file up to
8 | ## date. Leave `msgstr`s empty as changing them here as no
9 | ## effect: edit them in PO (`.po`) files instead.
10 |
11 | ## From Ecto.Changeset.cast/4
12 | msgid "can't be blank"
13 | msgstr ""
14 |
15 | ## From Ecto.Changeset.unique_constraint/3
16 | msgid "has already been taken"
17 | msgstr ""
18 |
19 | ## From Ecto.Changeset.put_change/3
20 | msgid "is invalid"
21 | msgstr ""
22 |
23 | ## From Ecto.Changeset.validate_acceptance/3
24 | msgid "must be accepted"
25 | msgstr ""
26 |
27 | ## From Ecto.Changeset.validate_format/3
28 | msgid "has invalid format"
29 | msgstr ""
30 |
31 | ## From Ecto.Changeset.validate_subset/3
32 | msgid "has an invalid entry"
33 | msgstr ""
34 |
35 | ## From Ecto.Changeset.validate_exclusion/3
36 | msgid "is reserved"
37 | msgstr ""
38 |
39 | ## From Ecto.Changeset.validate_confirmation/3
40 | msgid "does not match confirmation"
41 | msgstr ""
42 |
43 | ## From Ecto.Changeset.no_assoc_constraint/3
44 | msgid "is still associated with this entry"
45 | msgstr ""
46 |
47 | msgid "are still associated with this entry"
48 | msgstr ""
49 |
50 | ## From Ecto.Changeset.validate_length/3
51 | msgid "should be %{count} character(s)"
52 | msgid_plural "should be %{count} character(s)"
53 | msgstr[0] ""
54 | msgstr[1] ""
55 |
56 | msgid "should have %{count} item(s)"
57 | msgid_plural "should have %{count} item(s)"
58 | msgstr[0] ""
59 | msgstr[1] ""
60 |
61 | msgid "should be at least %{count} character(s)"
62 | msgid_plural "should be at least %{count} character(s)"
63 | msgstr[0] ""
64 | msgstr[1] ""
65 |
66 | msgid "should have at least %{count} item(s)"
67 | msgid_plural "should have at least %{count} item(s)"
68 | msgstr[0] ""
69 | msgstr[1] ""
70 |
71 | msgid "should be at most %{count} character(s)"
72 | msgid_plural "should be at most %{count} character(s)"
73 | msgstr[0] ""
74 | msgstr[1] ""
75 |
76 | msgid "should have at most %{count} item(s)"
77 | msgid_plural "should have at most %{count} item(s)"
78 | msgstr[0] ""
79 | msgstr[1] ""
80 |
81 | ## From Ecto.Changeset.validate_number/3
82 | msgid "must be less than %{number}"
83 | msgstr ""
84 |
85 | msgid "must be greater than %{number}"
86 | msgstr ""
87 |
88 | msgid "must be less than or equal to %{number}"
89 | msgstr ""
90 |
91 | msgid "must be greater than or equal to %{number}"
92 | msgstr ""
93 |
94 | msgid "must be equal to %{number}"
95 | msgstr ""
96 |
--------------------------------------------------------------------------------
/lib/chat_gdg_web/controllers/user_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule ChatGdgWeb.UserController do
2 | use ChatGdgWeb, :controller
3 |
4 | alias ChatGdg.User
5 | alias ChatGdg.Repo
6 |
7 | def index(conn, _params) do
8 | users = Repo.all(User)
9 | render(conn, "index.html", users: users)
10 | end
11 |
12 | def show(conn, %{"id" => id}) do
13 | user = Repo.get!(User, id)
14 | render(conn, "show.html", user: user)
15 | end
16 |
17 | def new(conn, _params) do
18 | changeset = User.changeset(%User{})
19 | render(conn, "new.html", changeset: changeset)
20 | end
21 |
22 | def create(conn, %{"user" => user_params}) do
23 | changeset = User.reg_changeset(%User{}, user_params)
24 | case Repo.insert(changeset) do
25 | {:ok, _user} ->
26 | conn
27 | |> put_flash(:info, "User created successfully.")
28 | |> redirect(to: user_path(conn, :index))
29 | {:error, changeset} ->
30 | render(conn, "new.html", changeset: changeset)
31 | end
32 | end
33 |
34 | def edit(conn, %{"id" => id}) do
35 | user = Repo.get!(User, id)
36 | cond do
37 | user == Guardian.Plug.current_resource(conn) ->
38 | changeset = User.changeset(user)
39 | render(conn, "edit.html", user: user, changeset: changeset)
40 | :error ->
41 | conn
42 | |> put_flash(:error, "No access")
43 | |> redirect(to: user_path(conn, :index))
44 | end
45 | end
46 |
47 | def update(conn, %{"id" => id, "user" => user_params}) do
48 | user = Repo.get!(User, id)
49 | changeset = User.reg_changeset(user, user_params)
50 | cond do
51 | user == Guardian.Plug.current_resource(conn) ->
52 | case Repo.update(changeset) do
53 | {:ok, user} ->
54 | conn
55 | |> put_flash(:info, "User updated successfully.")
56 | |> redirect(to: user_path(conn, :show, user))
57 | {:error, changeset} ->
58 | render(conn, "edit.html", user: user, changeset: changeset)
59 | end
60 | :error ->
61 | conn
62 | |> put_flash(:error, "No access")
63 | |> redirect(to: user_path(conn, :index))
64 | end
65 | end
66 |
67 | def delete(conn, %{"id" => id}) do
68 | user = Repo.get!(User, id)
69 | cond do
70 | user == Guardian.Plug.current_resource(conn) ->
71 | Repo.delete!(user)
72 | conn
73 | |> Guardian.Plug.sign_out
74 | |> put_flash(:danger, "User deleted successfully.")
75 | |> redirect(to: session_path(conn, :new))
76 | :error ->
77 | conn
78 | |> put_flash(:error, "No access")
79 | |> redirect(to: user_path(conn, :index))
80 | end
81 | end
82 |
83 | end
--------------------------------------------------------------------------------
/lib/chat_gdg_web/channels/presence.ex:
--------------------------------------------------------------------------------
1 | defmodule ChatGdgWeb.Presence do
2 | @moduledoc """
3 | Provides presence tracking to channels and processes.
4 |
5 | See the [`Phoenix.Presence`](http://hexdocs.pm/phoenix/Phoenix.Presence.html)
6 | docs for more details.
7 |
8 | ## Usage
9 |
10 | Presences can be tracked in your channel after joining:
11 |
12 | defmodule ChatGdg.MyChannel do
13 | use ChatGdgWeb, :channel
14 | alias ChatGdg.Presence
15 |
16 | def join("some:topic", _params, socket) do
17 | send(self, :after_join)
18 | {:ok, assign(socket, :user_id, ...)}
19 | end
20 |
21 | def handle_info(:after_join, socket) do
22 | push socket, "presence_state", Presence.list(socket)
23 | {:ok, _} = Presence.track(socket, socket.assigns.user_id, %{
24 | online_at: inspect(System.system_time(:seconds))
25 | })
26 | {:noreply, socket}
27 | end
28 | end
29 |
30 | In the example above, `Presence.track` is used to register this
31 | channel's process as a presence for the socket's user ID, with
32 | a map of metadata. Next, the current presence list for
33 | the socket's topic is pushed to the client as a `"presence_state"` event.
34 |
35 | Finally, a diff of presence join and leave events will be sent to the
36 | client as they happen in real-time with the "presence_diff" event.
37 | See `Phoenix.Presence.list/2` for details on the presence datastructure.
38 |
39 | ## Fetching Presence Information
40 |
41 | The `fetch/2` callback is triggered when using `list/1`
42 | and serves as a mechanism to fetch presence information a single time,
43 | before broadcasting the information to all channel subscribers.
44 | This prevents N query problems and gives you a single place to group
45 | isolated data fetching to extend presence metadata.
46 |
47 | The function receives a topic and map of presences and must return a
48 | map of data matching the Presence datastructure:
49 |
50 | %{"123" => %{metas: [%{status: "away", phx_ref: ...}],
51 | "456" => %{metas: [%{status: "online", phx_ref: ...}]}
52 |
53 | The `:metas` key must be kept, but you can extend the map of information
54 | to include any additional information. For example:
55 |
56 | def fetch(_topic, entries) do
57 | users = entries |> Map.keys() |> Accounts.get_users_map(entries)
58 | # => %{"123" => %{name: "User 123"}, "456" => %{name: nil}}
59 |
60 | for {key, %{metas: metas}} <- entries, into: %{} do
61 | {key, %{metas: metas, user: users[key]}}
62 | end
63 | end
64 |
65 | The function above fetches all users from the database who
66 | have registered presences for the given topic. The fetched
67 | information is then extended with a `:user` key of the user's
68 | information, while maintaining the required `:metas` field from the
69 | original presence data.
70 | """
71 | use Phoenix.Presence, otp_app: :chat_gdg,
72 | pubsub_server: ChatGdg.PubSub
73 | end
74 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{"base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [], [], "hexpm"},
2 | "comeonin": {:hex, :comeonin, "3.2.0", "cb10995a22aed6812667efb3856f548818c85d85394d8132bc116fbd6995c1ef", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"},
3 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"},
4 | "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
5 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"},
6 | "db_connection": {:hex, :db_connection, "1.1.2", "2865c2a4bae0714e2213a0ce60a1b12d76a6efba0c51fbda59c9ab8d1accc7a8", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
7 | "decimal": {:hex, :decimal, "1.4.1", "ad9e501edf7322f122f7fc151cce7c2a0c9ada96f2b0155b8a09a795c2029770", [:mix], [], "hexpm"},
8 | "ecto": {:hex, :ecto, "2.2.6", "3fd1067661d6d64851a0d4db9acd9e884c00d2d1aa41cc09da687226cf894661", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
9 | "elixir_make": {:hex, :elixir_make, "0.4.0", "992f38fabe705bb45821a728f20914c554b276838433349d4f2341f7a687cddf", [], [], "hexpm"},
10 | "file_system": {:hex, :file_system, "0.2.2", "7f1e9de4746f4eb8a4ca8f2fbab582d84a4e40fa394cce7bfcb068b988625b06", [:mix], [], "hexpm"},
11 | "gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [:mix], [], "hexpm"},
12 | "guardian": {:hex, :guardian, "0.14.5", "6d4e89b673accdacbc092ad000dc7494019426bd898eebf699caf1d19000cdcd", [], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.2 and < 1.4.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.3", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, ">= 1.3.0 and < 4.0.0", [hex: :poison, repo: "hexpm", optional: false]}, {:uuid, ">=1.1.1", [hex: :uuid, repo: "hexpm", optional: false]}], "hexpm"},
13 | "jose": {:hex, :jose, "1.8.4", "7946d1e5c03a76ac9ef42a6e6a20001d35987afd68c2107bcd8f01a84e75aa73", [], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"},
14 | "mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [:mix], [], "hexpm"},
15 | "phoenix": {:hex, :phoenix, "1.3.0", "1c01124caa1b4a7af46f2050ff11b267baa3edb441b45dbf243e979cd4c5891b", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
16 | "phoenix_ecto": {:hex, :phoenix_ecto, "3.3.0", "702f6e164512853d29f9d20763493f2b3bcfcb44f118af2bc37bb95d0801b480", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
17 | "phoenix_html": {:hex, :phoenix_html, "2.10.5", "4f9df6b0fb7422a9440a73182a566cb9cbe0e3ffe8884ef9337ccf284fc1ef0a", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
18 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.1.3", "1d178429fc8950b12457d09c6afec247bfe1fcb6f36209e18fbb0221bdfe4d41", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2 or ~> 1.3", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm"},
19 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.2", "bfa7fd52788b5eaa09cb51ff9fcad1d9edfeb68251add458523f839392f034c1", [:mix], [], "hexpm"},
20 | "plug": {:hex, :plug, "1.4.3", "236d77ce7bf3e3a2668dc0d32a9b6f1f9b1f05361019946aae49874904be4aed", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"},
21 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
22 | "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"},
23 | "postgrex": {:hex, :postgrex, "0.13.3", "c277cfb2a9c5034d445a722494c13359e361d344ef6f25d604c2353185682bfc", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"},
24 | "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"},
25 | "uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [], [], "hexpm"}}
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ChatGdg - GDG Izmir 2017
2 |
3 | A real-time chat application using websockets written in Elixir Phoenix Web Framework
4 |
5 | To start finished version of the sample application written in this tutorial:
6 |
7 | * Install dependencies with `mix deps.get`
8 | * Create and migrate your database with `mix ecto.create && mix ecto.migrate`
9 | * Install Node.js dependencies with `cd assets && npm install`
10 | * Start Phoenix endpoint with `mix phx.server`
11 |
12 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
13 |
14 | ----
15 | ## Table of Contents
16 |
17 | - [ChatGdg - GDG Izmir 2017](#chatgdg---gdg-izmir-2017)
18 | - [Table of Contents](#table-of-contents)
19 | - [Installing Requirements and Setting Up Development Environment](#installing-requirements-and-setting-up-development-environment)
20 | - [Erlang 18 or later](#erlang-18-or-later)
21 | - [Elixir 1.4 or later](#elixir-14-or-later)
22 | - [Phoenix](#phoenix)
23 | - [node.js (>= 5.0.0)](#nodejs-500)
24 | - [PostgreSQL](#postgresql)
25 | - [inotify-tools (for linux users)](#inotify-tools-for-linux-users)
26 | - [Creating skeleton of a phoenix application with mix](#creating-skeleton-of-a-phoenix-application-with-mix)
27 | - [Setting Configuration File](#setting-configuration-file)
28 | - [Editing homepage](#editing-homepage)
29 | - [Using Presence Module and Activating Channels](#using-presence-module-and-activating-channels)
30 | - [Generating Presence Module](#generating-presence-module)
31 | - [Activating Channels](#activating-channels)
32 | - [Creating the Database Schema (not Model!)](#creating-the-database-schema-not-model)
33 | - [Insert a sample data from iex shell](#insert-a-sample-data-from-iex-shell)
34 | - [Generating UserController and UserView](#generating-usercontroller-and-userview)
35 | - [Usage of Resources](#usage-of-resources)
36 | - [Controller](#controller)
37 | - [View](#view)
38 | - [CRUD Operations](#crud-operations)
39 | - [Create a User](#create-a-user)
40 | - [Update and Delete](#update-and-delete)
41 | - [Hash the Password](#hash-the-password)
42 | - [User Authentication](#user-authentication)
43 | - [Using Guardian as an Authenticator](#using-guardian-as-an-authenticator)
44 | - [Editing Router](#editing-router)
45 | - [Tokens](#tokens)
46 | - [Sessions](#sessions)
47 | - [User Restrictions](#user-restrictions)
48 | - [Changing Layout](#changing-layout)
49 | - [Generate a Helper Function](#generate-a-helper-function)
50 | - [Show Username in the Chat Page](#show-username-in-the-chat-page)
51 | - [Entering Chat Room](#entering-chat-room)
52 | - [Additional Resource](#additional-resource)
53 |
54 | ## Installing Requirements and Setting Up Development Environment
55 |
56 | Follow the instructions.
57 |
58 | ### Erlang 18 or later
59 |
60 | When we install Elixir using instructions from the Elixir [Installation Page](https://elixir-lang.org/install.html), we will usually get Erlang too. If Erlang was not installed along with Elixir, please see the [Erlang Instructions](https://elixir-lang.org/install.html#installing-erlang) section of the Elixir Installation Page for instructions.
61 |
62 | ### Elixir 1.4 or later
63 |
64 | **Mac OS X**
65 | Macports
66 | Run: sudo port install elixir
67 |
68 | **Homebrew**
69 | Update your homebrew to latest: brew update
70 | Run: brew install elixir
71 |
72 | ### Phoenix
73 |
74 | ```bash
75 | mix archive.install https://github.com/phoenixframework/archives/raw/master/phx_new.ez
76 | ```
77 |
78 | ### node.js (>= 5.0.0)
79 |
80 | Install from the [link.](https://nodejs.org/en/download/)
81 |
82 | ### PostgreSQL
83 |
84 | Install from the [link.](https://www.postgresql.org/download/)
85 |
86 | ### inotify-tools (for linux users)
87 |
88 | For Ubuntu 14.04/16.04
89 |
90 | ```bash
91 | sudo apt-get install inotify-tool
92 | ```
93 |
94 | For other distros download from [here](https://github.com/rvoicilas/inotify-tools/wiki)
95 |
96 | Links taken from [installation docs](https://hexdocs.pm/phoenix/installation.html#content)
97 |
98 | ## Creating skeleton of a phoenix application with mix
99 |
100 | ```bash
101 | mix phx.new chat_gdg
102 | ```
103 |
104 | Press y and enter to fetch and install dependencies
105 |
106 | Then the output should be:
107 |
108 | ```bash
109 | * creating chat_gdg/assets/css/app.css
110 | * creating chat_gdg/assets/css/phoenix.css
111 | * creating chat_gdg/assets/js/app.js
112 | * creating chat_gdg/assets/js/socket.js
113 | * creating chat_gdg/assets/package.json
114 | * creating chat_gdg/assets/static/robots.txt
115 | * creating chat_gdg/assets/static/images/phoenix.png
116 | * creating chat_gdg/assets/static/favicon.ico
117 |
118 | Fetch and install dependencies? [Yn] Y
119 | * running mix deps.get
120 | * running mix deps.compile
121 | * running cd assets && npm install && node node_modules/brunch/bin/brunch build
122 |
123 | We are all set! Go into your application by running:
124 |
125 | $ cd chat_gdg
126 |
127 | Then configure your database in config/dev.exs and run:
128 |
129 | $ mix ecto.create
130 |
131 | Start your Phoenix app with:
132 |
133 | $ mix phx.server
134 |
135 | You can also run your app inside IEx (Interactive Elixir) as:
136 |
137 | $ iex -S mix phx.server
138 | ```
139 |
140 | ### Setting Configuration File
141 |
142 | Open the **chat_gdg/config/dev.exs** file with a text editor and set the database credentials.
143 | Example:
144 |
145 | ```elixir
146 | # Configure your database
147 | config :chat_gdg, ChatGdg.Repo,
148 | adapter: Ecto.Adapters.Postgres,
149 | username: "postgres",
150 | password: "postgres",
151 | database: "chat_gdg_dev",
152 | hostname: "localhost",
153 | pool_size: 10
154 | ```
155 |
156 | Make sure database application is up and run these commands to fire up:
157 |
158 | ```bash
159 | cd chat_gdg
160 | mix ecto create
161 | mix phx.server
162 | ```
163 |
164 | Take a look at the application structure.
165 |
166 | A simple hello world application is ready.
167 |
168 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
169 |
170 | ## Editing homepage
171 |
172 | Take a look at the **router.ex**
173 |
174 | In **lib/chat_gdg_web/router.ex**
175 |
176 | ```elixir
177 | scope "/", ChatGdgWeb do
178 | pipe_through :browser # Use the default browser stack
179 |
180 | get "/", PageController, :index
181 | end
182 | ```
183 |
184 | `get "/", PageController, :index` this line directs the **Page Controller**
185 | Now check the **Page Controller**
186 |
187 | ```elixir
188 | defmodule ChatGdgWeb.PageController do
189 | use ChatGdgWeb, :controller
190 |
191 | def index(conn, _params) do
192 | render conn, "index.html"
193 | end
194 | end
195 | ```
196 |
197 | In the **Page Controller** the **index** function renders the **index.html** template.
198 |
199 | Let's edit the lib/chat_gdg_web/templates/page/index.html.eex as:
200 |
201 | ```html
202 |
203 |
204 | Hello, <%= @conn.params["user"] %>!
205 |
206 |
207 |
Messages:
208 |
209 |
210 |
211 |
212 |
Who’s Online
213 |
214 |
Loading online users...
215 |
216 |
217 |
218 | ```
219 |
220 | Again visit [`localhost:4000`](http://localhost:4000) from your browser.
221 |
222 | And [`http://0.0.0.0:4000/?user=joearms`](http://0.0.0.0:4000/?user=joearms)
223 |
224 | In the index.html.eex `Hello, <%= @conn.params["user"] %>!` line the `@conn.params["user"]` shows us the user from the connection's query parameters.
225 |
226 | ## Using Presence Module and Activating Channels
227 |
228 | [Presence Doc](https://hexdocs.pm/phoenix/Phoenix.Presence.html)
229 |
230 | [Channels Doc](https://hexdocs.pm/phoenix/channels.html)
231 |
232 | ### Generating Presence Module
233 |
234 | We will use the Presence module to track users status (is online & online since)
235 |
236 | Provides Presence tracking to processes and channels.
237 |
238 | This behaviour provides presence features such as fetching presences for a given topic, as well as handling diffs of join and leave events as they occur in real-time. Using this module defines a supervisor and allows the calling module to implement the Phoenix.Tracker behaviour which starts a tracker process to handle presence information.
239 |
240 | To create our presence with this command:
241 |
242 | ```bash
243 | mix phx.gen.presence
244 | ```
245 |
246 | output
247 |
248 | ```bash
249 | * creating lib/chat_gdg_web/channels/presence.ex
250 |
251 | Add your new module to your supervision tree,
252 | in lib/chat_gdg/application.ex:
253 |
254 | children = [
255 | ...
256 | supervisor(ChatGdgWeb.Presence, []),
257 | ]
258 |
259 | You're all set! See the Phoenix.Presence docs for more details:
260 | http://hexdocs.pm/phoenix/Phoenix.Presence.html
261 | ```
262 |
263 | ### Activating Channels
264 |
265 | Let's activate the channel to use websockets:
266 |
267 | Uncomment the this line in the **lib/chat_gdg_web/channels/user_socket.ex**
268 |
269 | ```elixir
270 | channel "room:*", ChatGdgWeb.RoomChannel
271 | ```
272 |
273 | and edit the **connect** function:
274 |
275 | ```elixir
276 | def connect(%{"user" => user}, socket) do
277 | {:ok, assign(socket, :user, user)}
278 | end
279 | ```
280 |
281 | Then create a module to handle room channel:
282 | Create a file named **room_channel.ex** under **lib/chat_gdg_web/channels**
283 |
284 | ```elixir
285 | defmodule ChatGdg.RoomChannel do
286 | use ChatGdg.Web, :channel
287 | alias ChatGdg.Presence
288 |
289 | def join("room:lobby", _, socket) do
290 | send self(), :after_join
291 | {:ok, socket}
292 | end
293 |
294 | def handle_info(:after_join, socket) do
295 | Presence.track(socket, socket.assigns.user, %{
296 | online_at: :os.system_time(:milli_seconds)
297 | })
298 | push socket, "presence_state", Presence.list(socket)
299 | {:noreply, socket}
300 | end
301 |
302 | def connect(%{"user" => user}, socket) do
303 | {:ok, assign(socket, :user, user)}
304 | end
305 |
306 | end
307 | ```
308 |
309 | Modify the **app.js** file under the **assets/js/** such as:
310 |
311 | ```javascript
312 | import "phoenix_html"
313 | import {Socket, Presence} from "phoenix"
314 |
315 | // get the user element from index.html
316 | let user = document.getElementById("User").innerText
317 |
318 | // generate a socket connection with the user parameter
319 | let socket = new Socket("/socket", {params: {user: user}})
320 | socket.connect()
321 |
322 | // create an empty js object to handle presences
323 | let presences = {}
324 |
325 | let formatTimestamp = (timestamp) => {
326 | let date = new Date(timestamp)
327 | return date.toLocaleTimeString()
328 | }
329 | let listBy = (user, {metas: metas}) => {
330 | return {
331 | user: user,
332 | onlineAt: formatTimestamp(metas[0].online_at)
333 | }
334 | }
335 |
336 | // get the UserList element from index.html
337 | let userList = document.getElementById("UserList")
338 | let render = (presences) => {
339 | userList.innerHTML = Presence.list(presences, listBy)
340 | .map(presence => `
341 |
342 | ${presence.user}
343 | online since ${presence.onlineAt}
344 |
345 | `)
346 | .join("")
347 | }
348 |
349 | // handle with the single channel
350 | // create a connection between client and room:lobby channel and set the presences settings
351 | let room = socket.channel("room:lobby", {})
352 | room.on("presence_state", state => {
353 | presences = Presence.syncState(presences, state)
354 | render(presences)
355 | })
356 |
357 | room.on("presence_diff", diff => {
358 | presences = Presence.syncDiff(presences, diff)
359 | render(presences)
360 | })
361 |
362 | // join the room
363 | room.join()
364 |
365 | // get the NewMessage element from index.html
366 | let messageInput = document.getElementById("NewMessage")
367 | messageInput.addEventListener("keypress", (e) => {
368 | if (e.keyCode == 13 && messageInput.value != "") {
369 | room.push("message:new", messageInput.value)
370 | messageInput.value = ""
371 | }
372 | })
373 |
374 | let messageList = document.getElementById("MessageList")
375 | let renderMessage = (message) => {
376 | let messageElement = document.createElement("li")
377 | messageElement.innerHTML = `
378 | ${message.user}
379 | ${formatTimestamp(message.timestamp)}
380 |
${message.body}
381 | `
382 | messageList.appendChild(messageElement)
383 | messageList.scrollTop = messageList.scrollHeight;
384 | }
385 |
386 | room.on("message:new", message => renderMessage(message))
387 | ```
388 |
389 | ## Creating the Database Schema (not Model!)
390 |
391 | ```bash
392 | mix phx.gen.schema User users email:unique encrypt_pass:string
393 | ```
394 |
395 | output:
396 |
397 | ```bash
398 | * creating lib/chat_gdg/user.ex
399 | * creating priv/repo/migrations/20171201161350_create_users.exs
400 |
401 | Remember to update your repository by running migrations:
402 |
403 | $ mix ecto.migrate
404 | ```
405 |
406 | take a look at model file and migration file:
407 |
408 | ```bash
409 | cat priv/repo/migrations/20171201161350_create_users.exs
410 | ```
411 |
412 | ```elixir
413 | defmodule ChatGdg.Repo.Migrations.CreateUsers do
414 | use Ecto.Migration
415 |
416 | def change do
417 | create table(:users) do
418 | add :email, :string
419 | add :encrypt_pass, :string
420 |
421 | timestamps()
422 | end
423 |
424 | create unique_index(:users, [:email])
425 | end
426 | end
427 | ```
428 |
429 | ```bash
430 | cat lib/chat_gdg/user.ex
431 | ```
432 |
433 | ```elixir
434 | defmodule ChatGdg.User do
435 | use Ecto.Schema
436 | import Ecto.Changeset
437 | alias ChatGdg.User
438 |
439 |
440 | schema "users" do
441 | field :email, :string
442 | field :encrypt_pass, :string
443 |
444 | timestamps()
445 | end
446 |
447 | @doc false
448 | def changeset(%User{} = user, attrs) do
449 | user
450 | |> cast(attrs, [:email, :encrypt_pass])
451 | |> validate_required([:email, :encrypt_pass])
452 | |> unique_constraint(:email)
453 | end
454 | end
455 | ```
456 |
457 | Add a field line for real(!) password but not store with the functionality of virtual parameter
458 |
459 | ```elixir
460 | schema "users" do
461 | field :email, :string
462 | field :encrypt_pass, :string
463 | field :password, :string, virtual: true
464 |
465 | timestamps()
466 | end
467 | ```
468 |
469 | and edit the changeset function as below
470 |
471 | ```elixir
472 | def changeset(%User{} = user, attrs) do
473 | user
474 | |> cast(attrs, [:email, :password])
475 | |> validate_required([:email, :password])
476 | |> unique_constraint(:email)
477 | end
478 | ```
479 |
480 | [Take a look]('https://hexdocs.pm/ecto/Ecto.Schema.html#field/3') at usage of virtual field in Ecto schema
481 |
482 | Then apply the migration with the mix command
483 |
484 | ```bash
485 | mix ecto.migrate
486 | ```
487 |
488 | output:
489 |
490 | ```bash
491 | Compiling 1 file (.ex)
492 | Generated chat_gdg app
493 | [info] == Running ChatGdg.Repo.Migrations.CreateUsers.change/0 forward
494 | [info] create table users
495 | [info] create index users_email_index
496 | [info] == Migrated in 0.0s
497 | ```
498 |
499 | ### Insert a sample data from iex shell
500 |
501 | ```bash
502 | iex -S mix phx.server
503 | Erlang/OTP 19 [erts-8.3] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false]
504 |
505 | [info] Running ChatGdgWeb.Endpoint with Cowboy using http://0.0.0.0:4000
506 | Interactive Elixir (1.4.0) - press Ctrl+C to exit (type h() ENTER for help)
507 | iex(1)> 19:46:54 - info: compiled 6 files into 2 files, copied 3 in 914 ms
508 | alias ChatGdg.Repo
509 | ChatGdg.Repo
510 | iex(2)> alias ChatGdg.User
511 | ChatGdg.User
512 | iex(3)> Repo.all(User)
513 | [debug] QUERY OK source="users" db=1.7ms queue=0.1ms
514 | SELECT u0."id", u0."email", u0."encrypt_pass", u0."inserted_at", u0."updated_at" FROM "users" AS u0 []
515 | []
516 | iex(4)> Repo.insert(%User{email: "joearms@erlang.com", encrypt_pass: "password"})
517 | [debug] QUERY OK db=7.8ms
518 | INSERT INTO "users" ("email","encrypt_pass","inserted_at","updated_at") VALUES ($1,$2,$3,$4) RETURNING "id" ["joearms@erlang.com", "password", {{2017, 12, 1}, {16, 49, 29, 841382}}, {{2017, 12, 1}, {16, 49, 29, 843639}}]
519 | {:ok,
520 | %ChatGdg.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
521 | email: "joearms@erlang.com", encrypt_pass: "password", id: 1,
522 | inserted_at: ~N[2017-12-01 16:49:29.841382], password: nil,
523 | updated_at: ~N[2017-12-01 16:49:29.843639]}}
524 | ```
525 |
526 | ## Generating UserController and UserView
527 |
528 | ### Usage of Resources
529 |
530 | To handle CRUD operations add **UserController** the **router.ex** with [resources](https://hexdocs.pm/phoenix/routing.html#resources) macro.
531 |
532 | ```elixir
533 | scope "/", ChatGdgWeb do
534 | pipe_through :browser # Use the default browser stack
535 | resources "/users", UserController
536 | get "/", PageController, :index
537 | end
538 | ```
539 |
540 | then check our new routes with the command:
541 |
542 | ```bash
543 | mix phx.routes
544 | Compiling 1 file (.ex)
545 | user_path GET /users ChatGdgWeb.UserController :index
546 | user_path GET /users/:id/edit ChatGdgWeb.UserController :edit
547 | user_path GET /users/new ChatGdgWeb.UserController :new
548 | user_path GET /users/:id ChatGdgWeb.UserController :show
549 | user_path POST /users ChatGdgWeb.UserController :create
550 | user_path PATCH /users/:id ChatGdgWeb.UserController :update
551 | PUT /users/:id ChatGdgWeb.UserController :update
552 | user_path DELETE /users/:id ChatGdgWeb.UserController :delete
553 | page_path GET / ChatGdgWeb.PageController :index
554 | ```
555 |
556 | ### Controller
557 |
558 | Create the skeleton of **user_controller.ex** at **lib/chat_gdg_web/controllers/**
559 |
560 | ```elixir
561 | defmodule ChatGdgWeb.UserController do
562 | use ChatGdgWeb, :controller
563 |
564 | alias ChatGdg.User
565 | alias ChatGdg.Repo
566 |
567 | def index(conn, _params) do
568 | users = Repo.all(User)
569 | render(conn, "index.html", users: users)
570 | end
571 |
572 | def show(conn, %{"id" => id}) do
573 | user = Repo.get!(User, id)
574 | render(conn, "show.html", user: user)
575 | end
576 | end
577 | ```
578 |
579 | ### View
580 |
581 | And create the **user_view.ex** at **lib/chat_gdg_web/views/**
582 |
583 | ```elixir
584 | defmodule ChatGdgWeb.UserView do
585 | use ChatGdgWeb, :view
586 | end
587 | ```
588 |
589 | And finally create the **index.html.eex** at **lib/chat_gdg_web/templates/user/**
590 |
591 | ```html
592 |
List of users
593 |
594 |
595 |
596 |
597 |
Email
598 |
599 |
600 |
601 |
602 |
603 | <%= for user <- @users do %>
604 |
605 |
<%= user.email %>
606 |
607 | <%= link "Profile", to: user_path(@conn, :show, user), class: "btn btn-default btn-xs" %>
608 | <%= link "Edit", to: user_path(@conn, :edit, user), class: "btn btn-default btn-xs" %>
609 | <%= link "Delete User", to: user_path(@conn, :delete, user), method: :delete, data: [confirm: "Are you sure?"], class: "btn btn-danger btn-xs" %>
610 |
611 |
612 |
613 |
614 | <% end %>
615 |
616 |
617 |
618 |
619 | <%= link "New user", to: user_path(@conn, :new) %>
620 | ```
621 |
622 | Now visit [`localhost:4000/users`](http://localhost:4000/users) from your browser and try the Profile button!
623 |
624 | *If you try to click **Edit** or **Delete User** buttons you will get error because we did not define new-create, edit-update and delete functions!*
625 |
626 | ## CRUD Operations
627 |
628 | ### Create a User
629 |
630 | To create a user we need to add **new** and **create** functions in the our controller. Add these functions to **user_controller.ex**
631 |
632 | ```elixir
633 | def new(conn, _params) do
634 | changeset = User.changeset(%User{})
635 | render(conn, "new.html", changeset: changeset)
636 | end
637 |
638 | def create(conn, %{"user" => user_params}) do
639 | changeset = User.reg_changeset(%User{}, user_params)
640 | case Repo.insert(changeset) do
641 | {:ok, _user} ->
642 | conn
643 | |> put_flash(:info, "User created successfully.")
644 | |> redirect(to: user_path(conn, :index))
645 | {:error, changeset} ->
646 | render(conn, "new.html", changeset: changeset)
647 | end
648 | end
649 | ```
650 |
651 | Then create a **new.html.eex** and **form.html.eex** template. **form.html.eex** will be inherited and handle in create and edit operations.
652 |
653 | new.html.eex
654 |
655 | ```html
656 |