├── priv ├── repo │ ├── migrations │ │ ├── .formatter.exs │ │ └── 20191227155621_create_users.exs │ └── seeds.exs └── gettext │ ├── en │ └── LC_MESSAGES │ │ └── errors.po │ └── errors.pot ├── test ├── test_helper.exs ├── api_ecommerce_web │ ├── views │ │ └── error_view_test.exs │ └── controllers │ │ └── user_controller_test.exs ├── support │ ├── channel_case.ex │ ├── conn_case.ex │ └── data_case.ex └── api_ecommerce │ └── auth_test.exs ├── lib ├── api_ecommerce │ ├── ecto_enums.ex │ ├── repo.ex │ ├── application.ex │ ├── auth │ │ └── user.ex │ └── auth.ex ├── api_ecommerce.ex ├── api_ecommerce_web │ ├── auth_error_handler.ex │ ├── auth_pipeline.ex │ ├── router.ex │ ├── views │ │ ├── changeset_view.ex │ │ ├── error_view.ex │ │ ├── user_view.ex │ │ └── error_helpers.ex │ ├── controllers │ │ ├── fallback_controller.ex │ │ └── user_controller.ex │ ├── gettext.ex │ ├── channels │ │ └── user_socket.ex │ └── endpoint.ex ├── guardian.ex └── api_ecommerce_web.ex ├── .idea ├── .gitignore ├── libraries │ ├── geo.xml │ ├── jsx.xml │ ├── x509.xml │ ├── binpp.xml │ ├── credo.xml │ ├── ex_doc.xml │ ├── jsone.xml │ ├── ojson.xml │ ├── poison.xml │ ├── benchee.xml │ ├── dialyxir.xml │ ├── dialyze.xml │ ├── earmark.xml │ ├── hackney.xml │ ├── inch_ex.xml │ ├── kadabra.xml │ ├── libdecaf.xml │ ├── libsodium.xml │ ├── mariaex.xml │ ├── excoveralls.xml │ ├── benchee_json.xml │ ├── phoenix_html.xml │ ├── websocket_client.xml │ ├── cowboy.xml │ ├── cowlib.xml │ ├── ranch.xml │ ├── base64url.xml │ ├── telemetry.xml │ ├── postgrex.xml │ ├── ecto.xml │ ├── jason.xml │ ├── myxql.xml │ ├── decimal.xml │ ├── ecto_sql.xml │ ├── guardian.xml │ ├── ecto_enum.xml │ ├── connection.xml │ ├── elixir_make.xml │ ├── plug_cowboy.xml │ ├── plug_crypto.xml │ ├── phoenix_ecto.xml │ ├── db_connection.xml │ ├── phoenix_pubsub.xml │ ├── plug.xml │ ├── mime.xml │ ├── gettext.xml │ ├── phoenix.xml │ ├── comeonin.xml │ ├── jose.xml │ └── bcrypt_elixir.xml ├── dictionaries │ └── luisl.xml ├── vcs.xml ├── modules.xml ├── misc.xml ├── dataSources.xml └── api-ecommerce.iml ├── .formatter.exs ├── config ├── test.exs ├── config.exs ├── prod.secret.exs ├── dev.exs └── prod.exs ├── README.md ├── .gitignore ├── mix.exs └── mix.lock /priv/repo/migrations/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto_sql], 3 | inputs: ["*.exs"] 4 | ] 5 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | Ecto.Adapters.SQL.Sandbox.mode(ApiEcommerce.Repo, :manual) 3 | -------------------------------------------------------------------------------- /lib/api_ecommerce/ecto_enums.ex: -------------------------------------------------------------------------------- 1 | import EctoEnum 2 | defenum StatusEnum, active: 0, inactive: 1, deleted: 2 3 | defenum RoleEnum, member: 0, admin: 1 -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /workspace.xml 3 | # Datasource local storage ignored files 4 | /dataSources/ 5 | /dataSources.local.xml 6 | -------------------------------------------------------------------------------- /lib/api_ecommerce/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule ApiEcommerce.Repo do 2 | use Ecto.Repo, 3 | otp_app: :api_ecommerce, 4 | adapter: Ecto.Adapters.MyXQL 5 | end 6 | -------------------------------------------------------------------------------- /.idea/libraries/geo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/libraries/jsx.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/libraries/x509.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/libraries/binpp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/libraries/credo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/libraries/ex_doc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/libraries/jsone.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/libraries/ojson.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/libraries/poison.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/libraries/benchee.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/libraries/dialyxir.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/libraries/dialyze.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/libraries/earmark.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/libraries/hackney.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/libraries/inch_ex.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/libraries/kadabra.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/libraries/libdecaf.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/libraries/libsodium.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/libraries/mariaex.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/libraries/excoveralls.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto, :phoenix], 3 | inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | subdirectories: ["priv/*/migrations"] 5 | ] 6 | -------------------------------------------------------------------------------- /.idea/libraries/benchee_json.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/libraries/phoenix_html.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/libraries/websocket_client.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/dictionaries/luisl.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ecommerce 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/libraries/cowboy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/libraries/cowlib.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/libraries/ranch.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/libraries/base64url.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/libraries/telemetry.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/api_ecommerce.ex: -------------------------------------------------------------------------------- 1 | defmodule ApiEcommerce do 2 | @moduledoc """ 3 | ApiEcommerce keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/libraries/postgrex.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /lib/api_ecommerce_web/auth_error_handler.ex: -------------------------------------------------------------------------------- 1 | defmodule ApiEcommerce.AuthErrorHandler do 2 | import Plug.Conn 3 | 4 | @behaviour Guardian.Plug.ErrorHandler 5 | 6 | @impl Guardian.Plug.ErrorHandler 7 | def auth_error(conn, {type, _reason}, _opts) do 8 | conn 9 | |> put_resp_content_type("application/json") 10 | |> send_resp(401, '') 11 | end 12 | 13 | end -------------------------------------------------------------------------------- /.idea/libraries/ecto.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/libraries/jason.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/libraries/myxql.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /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 | # ApiEcommerce.Repo.insert!(%ApiEcommerce.SomeSchema{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | -------------------------------------------------------------------------------- /.idea/libraries/decimal.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/libraries/ecto_sql.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/libraries/guardian.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/api_ecommerce_web/auth_pipeline.ex: -------------------------------------------------------------------------------- 1 | defmodule ApiEcommerce.Guardian.AuthPipeline do 2 | use Guardian.Plug.Pipeline, otp_app: :api_ecommerce, 3 | module: ApiEcommerce.Guardian, 4 | error_handler: ApiEcommerce.AuthErrorHandler 5 | 6 | plug Guardian.Plug.VerifyHeader, realm: "Bearer" 7 | plug Guardian.Plug.EnsureAuthenticated 8 | plug Guardian.Plug.LoadResource 9 | end -------------------------------------------------------------------------------- /.idea/libraries/ecto_enum.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/libraries/connection.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/libraries/elixir_make.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/libraries/plug_cowboy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/libraries/plug_crypto.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/libraries/phoenix_ecto.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/libraries/db_connection.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/libraries/phoenix_pubsub.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/libraries/plug.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/libraries/mime.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/libraries/gettext.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/libraries/phoenix.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/libraries/comeonin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/libraries/jose.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/dataSources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | mysql.8 6 | true 7 | com.mysql.cj.jdbc.Driver 8 | jdbc:mysql://localhost:3306 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/api_ecommerce_web/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ApiEcommerceWeb.ErrorViewTest do 2 | use ApiEcommerceWeb.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.json" do 8 | assert render(ApiEcommerceWeb.ErrorView, "404.json", []) == %{errors: %{detail: "Not Found"}} 9 | end 10 | 11 | test "renders 500.json" do 12 | assert render(ApiEcommerceWeb.ErrorView, "500.json", []) == 13 | %{errors: %{detail: "Internal Server Error"}} 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/guardian.ex: -------------------------------------------------------------------------------- 1 | defmodule ApiEcommerce.Guardian do 2 | use Guardian, otp_app: :api_ecommerce 3 | 4 | def subject_for_token(user, _claims) do 5 | sub = to_string(user.id) 6 | {:ok, sub} 7 | end 8 | 9 | def subject_for_token(_, _) do 10 | {:error, :reason_for_error} 11 | end 12 | 13 | def resource_from_claims(claims) do 14 | id = claims["sub"] 15 | resource = ApiEcommerce.Auth.get_user!(id) 16 | {:ok, resource} 17 | end 18 | 19 | def resource_from_claims(_claims) do 20 | {:error, :reason_for_error} 21 | end 22 | end -------------------------------------------------------------------------------- /.idea/libraries/bcrypt_elixir.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # Configure your database 4 | config :api_ecommerce, ApiEcommerce.Repo, 5 | username: "admin", 6 | password: "admin", 7 | database: "api_ecommerce_test", 8 | hostname: "localhost", 9 | pool: Ecto.Adapters.SQL.Sandbox 10 | 11 | # We don't run a server during test. If one is required, 12 | # you can enable the server option below. 13 | config :api_ecommerce, ApiEcommerceWeb.Endpoint, 14 | http: [port: 4002], 15 | server: false 16 | 17 | # Print only warnings and errors during test 18 | config :logger, level: :warn 19 | 20 | config :bcrypt_elixir, :log_rounds, 4 -------------------------------------------------------------------------------- /lib/api_ecommerce_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule ApiEcommerceWeb.Router do 2 | use ApiEcommerceWeb, :router 3 | 4 | alias ApiEcommerce.Guardian 5 | 6 | pipeline :api do 7 | plug :accepts, ["json"] 8 | end 9 | 10 | pipeline :auth do 11 | plug Guardian.AuthPipeline 12 | end 13 | 14 | scope "/v1", ApiEcommerceWeb do 15 | pipe_through :api 16 | 17 | post "/users/sign_in", UserController, :sign_in 18 | post "/users/sign_up", UserController, :create 19 | end 20 | 21 | scope "/v1", ApiEcommerceWeb do 22 | pipe_through [:api, :auth] 23 | 24 | resources "/users", UserController, except: [:new, :create, :edit] 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/api_ecommerce_web/views/changeset_view.ex: -------------------------------------------------------------------------------- 1 | defmodule ApiEcommerceWeb.ChangesetView do 2 | use ApiEcommerceWeb, :view 3 | 4 | @doc """ 5 | Traverses and translates changeset errors. 6 | 7 | See `Ecto.Changeset.traverse_errors/2` and 8 | `ApiEcommerceWeb.ErrorHelpers.translate_error/1` for more details. 9 | """ 10 | def translate_errors(changeset) do 11 | Ecto.Changeset.traverse_errors(changeset, &translate_error/1) 12 | end 13 | 14 | def render("error.json", %{changeset: changeset}) do 15 | # When encoded, the changeset returns its errors 16 | # as a JSON object. So we just pass it forward. 17 | %{errors: translate_errors(changeset)} 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/api_ecommerce_web/controllers/fallback_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule ApiEcommerceWeb.FallbackController do 2 | @moduledoc """ 3 | Translates controller action results into valid `Plug.Conn` responses. 4 | 5 | See `Phoenix.Controller.action_fallback/1` for more details. 6 | """ 7 | use ApiEcommerceWeb, :controller 8 | 9 | def call(conn, {:error, :not_found}) do 10 | conn 11 | |> put_status(:not_found) 12 | |> put_view(ApiEcommerceWeb.ErrorView) 13 | |> render(:"404") 14 | end 15 | 16 | def call(conn, {:error, %Ecto.Changeset{}}) do 17 | conn 18 | |> put_status(:unprocessable_entity) 19 | |> put_view(ApiEcommerceWeb.ErrorView) 20 | |> render(:"422") 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ApiEcommerce 2 | 3 | To start your Phoenix server: 4 | 5 | * Install dependencies with `mix deps.get` 6 | * Create and migrate your database with `mix ecto.setup` 7 | * Start Phoenix endpoint with `mix phx.server` 8 | 9 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 10 | 11 | Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). 12 | 13 | ## Learn more 14 | 15 | * Official website: http://www.phoenixframework.org/ 16 | * Guides: https://hexdocs.pm/phoenix/overview.html 17 | * Docs: https://hexdocs.pm/phoenix 18 | * Mailing list: http://groups.google.com/group/phoenix-talk 19 | * Source: https://github.com/phoenixframework/phoenix 20 | -------------------------------------------------------------------------------- /lib/api_ecommerce_web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule ApiEcommerceWeb.ErrorView do 2 | use ApiEcommerceWeb, :view 3 | 4 | # If you want to customize a particular status code 5 | # for a certain format, you may uncomment below. 6 | # def render("500.json", _assigns) do 7 | # %{errors: %{detail: "Internal Server Error"}} 8 | # end 9 | 10 | # By default, Phoenix returns the status message from 11 | # the template name. For example, "404.json" becomes 12 | # "Not Found". 13 | def template_not_found(template, _assigns) do 14 | %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}} 15 | end 16 | 17 | def render("401.json", %{message: message}) do 18 | %{errors: %{detail: message}} 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/api_ecommerce_web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule ApiEcommerceWeb.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 ApiEcommerceWeb.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: :api_ecommerce 24 | end 25 | -------------------------------------------------------------------------------- /priv/repo/migrations/20191227155621_create_users.exs: -------------------------------------------------------------------------------- 1 | defmodule ApiEcommerce.Repo.Migrations.CreateUsers do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:users, primary_key: false) do 6 | add :id, :binary_id, primary_key: true 7 | add :name, :string 8 | add :email, :string, null: false 9 | add :status, :tinyint, null: false, default: 0 10 | add :role, :tinyint, null: false, default: 0 11 | add :password_hash, :string 12 | add :recovery_token, :string 13 | add :recovery_token_created_at, :naive_datetime 14 | 15 | timestamps() 16 | end 17 | 18 | create index(:users, [:id]) 19 | create index(:users, [:status]) 20 | create index(:users, [:role]) 21 | create unique_index(:users, [:recovery_token]) 22 | create unique_index(:users, [:email]) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | api_ecommerce-*.tar 24 | 25 | # Since we are building assets from assets/, 26 | # we ignore priv/static. You may want to comment 27 | # this depending on your deployment strategy. 28 | /priv/static/ 29 | -------------------------------------------------------------------------------- /lib/api_ecommerce_web/views/user_view.ex: -------------------------------------------------------------------------------- 1 | defmodule ApiEcommerceWeb.UserView do 2 | use ApiEcommerceWeb, :view 3 | alias ApiEcommerceWeb.UserView 4 | 5 | def render("index.json", %{users: users}) do 6 | %{data: render_many(users, UserView, "user.json")} 7 | end 8 | 9 | def render("show.json", %{user: user}) do 10 | %{data: render_one(user, UserView, "user.json")} 11 | end 12 | 13 | def render("sign_in.json", %{user: user, token: token}) do 14 | %{data: Map.merge(render_one(user, UserView, "user.json"), %{token: token})} 15 | end 16 | 17 | def render("sign_up.json", %{user: user, token: token}) do 18 | %{data: Map.merge(render_one(user, UserView, "user.json"), %{token: token})} 19 | end 20 | 21 | def render("user.json", %{user: user}) do 22 | %{ 23 | id: user.id, 24 | email: user.email, 25 | role: user.role, 26 | status: user.status 27 | } 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule ApiEcommerceWeb.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 data structures 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 ApiEcommerceWeb.Endpoint 25 | end 26 | end 27 | 28 | setup tags do 29 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(ApiEcommerce.Repo) 30 | 31 | unless tags[:async] do 32 | Ecto.Adapters.SQL.Sandbox.mode(ApiEcommerce.Repo, {:shared, self()}) 33 | end 34 | 35 | :ok 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/api_ecommerce/application.ex: -------------------------------------------------------------------------------- 1 | defmodule ApiEcommerce.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | def start(_type, _args) do 9 | # List all child processes to be supervised 10 | children = [ 11 | # Start the Ecto repository 12 | ApiEcommerce.Repo, 13 | # Start the PubSub system 14 | {Phoenix.PubSub, name: MyApp.PubSub}, 15 | # Start the endpoint when the application starts 16 | ApiEcommerceWeb.Endpoint 17 | # Starts a worker by calling: ApiEcommerce.Worker.start_link(arg) 18 | # {ApiEcommerce.Worker, arg}, 19 | ] 20 | 21 | # See https://hexdocs.pm/elixir/Supervisor.html 22 | # for other strategies and supported options 23 | opts = [strategy: :one_for_one, name: ApiEcommerce.Supervisor] 24 | Supervisor.start_link(children, opts) 25 | end 26 | 27 | # Tell Phoenix to update the endpoint configuration 28 | # whenever the application is updated. 29 | def config_change(changed, _new, removed) do 30 | ApiEcommerceWeb.Endpoint.config_change(changed, removed) 31 | :ok 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/api_ecommerce_web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule ApiEcommerceWeb.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | # channel "room:*", ApiEcommerceWeb.RoomChannel 6 | 7 | # Socket params are passed from the client and can 8 | # be used to verify and authenticate a user. After 9 | # verification, you can put default assigns into 10 | # the socket that will be set for all channels, ie 11 | # 12 | # {:ok, assign(socket, :user_id, verified_user_id)} 13 | # 14 | # To deny connection, return `:error`. 15 | # 16 | # See `Phoenix.Token` documentation for examples in 17 | # performing token verification on connect. 18 | def connect(_params, socket, _connect_info) do 19 | {:ok, socket} 20 | end 21 | 22 | # Socket id's are topics that allow you to identify all sockets for a given user: 23 | # 24 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}" 25 | # 26 | # Would allow you to broadcast a "disconnect" event and terminate 27 | # all active sockets and channels for a given user: 28 | # 29 | # ApiEcommerceWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) 30 | # 31 | # Returning `nil` makes this socket anonymous. 32 | def id(_socket), do: nil 33 | end 34 | -------------------------------------------------------------------------------- /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 | 7 | # General application configuration 8 | use Mix.Config 9 | 10 | config :api_ecommerce, 11 | ecto_repos: [ApiEcommerce.Repo], 12 | generators: [binary_id: true] 13 | 14 | # Configures the endpoint 15 | config :api_ecommerce, ApiEcommerceWeb.Endpoint, 16 | url: [host: "localhost"], 17 | secret_key_base: "xpSGNOCTXLgEQy1K1X+Q9aGfZZXx5Y37FxaXsUgrqf/zKk4KZwpPDCK0nWlUNrOh", 18 | render_errors: [view: ApiEcommerceWeb.ErrorView, accepts: ~w(json)], 19 | pubsub_server: MyApp.PubSub 20 | 21 | config :api_ecommerce, ApiEcommerce.Guardian, 22 | issuer: "api_ecommerce", 23 | secret_key: "RFl88y/O+rrGqRzWwdTGpsF68o07jhz60tfUnh0mTKhkGpeGo3Adzc8+xDuxXyd5" 24 | 25 | # Configures Elixir's Logger 26 | config :logger, :console, 27 | format: "$time $metadata[$level] $message\n", 28 | metadata: [:request_id] 29 | 30 | # Use Jason for JSON parsing in Phoenix 31 | config :phoenix, :json_library, Jason 32 | 33 | # Import environment specific config. This must remain at the bottom 34 | # of this file so it overrides the configuration defined above. 35 | import_config "#{Mix.env()}.exs" 36 | -------------------------------------------------------------------------------- /lib/api_ecommerce_web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule ApiEcommerceWeb.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | @doc """ 7 | Translates an error message using gettext. 8 | """ 9 | def translate_error({msg, opts}) do 10 | # When using gettext, we typically pass the strings we want 11 | # to translate as a static argument: 12 | # 13 | # # Translate "is invalid" in the "errors" domain 14 | # dgettext("errors", "is invalid") 15 | # 16 | # # Translate the number of files with plural rules 17 | # dngettext("errors", "1 file", "%{count} files", count) 18 | # 19 | # Because the error messages we show in our forms and APIs 20 | # are defined inside Ecto, we need to translate them dynamically. 21 | # This requires us to call the Gettext module passing our gettext 22 | # backend as first argument. 23 | # 24 | # Note we use the "errors" domain, which means translations 25 | # should be written to the errors.po file. The :count option is 26 | # set by Ecto and indicates we should also apply plural rules. 27 | if count = opts[:count] do 28 | Gettext.dngettext(ApiEcommerceWeb.Gettext, "errors", msg, msg, count, opts) 29 | else 30 | Gettext.dgettext(ApiEcommerceWeb.Gettext, "errors", msg, opts) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /config/prod.secret.exs: -------------------------------------------------------------------------------- 1 | # In this file, we load production configuration and secrets 2 | # from environment variables. You can also hardcode secrets, 3 | # although such is generally not recommended and you have to 4 | # remember to add this file to your .gitignore. 5 | use Mix.Config 6 | 7 | database_url = 8 | System.get_env("DATABASE_URL") || 9 | raise """ 10 | environment variable DATABASE_URL is missing. 11 | For example: ecto://USER:PASS@HOST/DATABASE 12 | """ 13 | 14 | config :api_ecommerce, ApiEcommerce.Repo, 15 | # ssl: true, 16 | url: database_url, 17 | pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10") 18 | 19 | secret_key_base = 20 | System.get_env("SECRET_KEY_BASE") || 21 | raise """ 22 | environment variable SECRET_KEY_BASE is missing. 23 | You can generate one by calling: mix phx.gen.secret 24 | """ 25 | 26 | config :api_ecommerce, ApiEcommerceWeb.Endpoint, 27 | http: [:inet6, port: String.to_integer(System.get_env("PORT") || "4000")], 28 | secret_key_base: secret_key_base 29 | 30 | # ## Using releases (Elixir v1.9+) 31 | # 32 | # If you are doing OTP releases, you need to instruct Phoenix 33 | # to start each relevant endpoint: 34 | # 35 | # config :api_ecommerce, ApiEcommerceWeb.Endpoint, server: true 36 | # 37 | # Then you can assemble a release by calling `mix release`. 38 | # See `mix help release` for more information. 39 | -------------------------------------------------------------------------------- /test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule ApiEcommerceWeb.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 data structures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | we enable the SQL sandbox, so changes done to the database 12 | are reverted at the end of every test. If you are using 13 | PostgreSQL, you can even run database tests asynchronously 14 | by setting `use ApiEcommerceWeb.ConnCase, async: true`, although 15 | this option is not recommendded for other databases. 16 | """ 17 | 18 | use ExUnit.CaseTemplate 19 | 20 | using do 21 | quote do 22 | # Import conveniences for testing with connections 23 | import Plug.Conn 24 | import Phoenix.ConnTest 25 | alias ApiEcommerceWeb.Router.Helpers, as: Routes 26 | 27 | # The default endpoint for testing 28 | @endpoint ApiEcommerceWeb.Endpoint 29 | end 30 | end 31 | 32 | setup tags do 33 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(ApiEcommerce.Repo) 34 | 35 | unless tags[:async] do 36 | Ecto.Adapters.SQL.Sandbox.mode(ApiEcommerce.Repo, {:shared, self()}) 37 | end 38 | 39 | {:ok, conn: Phoenix.ConnTest.build_conn()} 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/api_ecommerce_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule ApiEcommerceWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :api_ecommerce 3 | 4 | socket "/socket", ApiEcommerceWeb.UserSocket, 5 | websocket: true, 6 | longpoll: false 7 | 8 | # Serve at "/" the static files from "priv/static" directory. 9 | # 10 | # You should set gzip to true if you are running phx.digest 11 | # when deploying your static files in production. 12 | plug Plug.Static, 13 | at: "/", 14 | from: :api_ecommerce, 15 | gzip: false, 16 | only: ~w(css fonts images js favicon.ico robots.txt) 17 | 18 | # Code reloading can be explicitly enabled under the 19 | # :code_reloader configuration of your endpoint. 20 | if code_reloading? do 21 | plug Phoenix.CodeReloader 22 | end 23 | 24 | plug Plug.RequestId 25 | plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] 26 | 27 | plug Plug.Parsers, 28 | parsers: [:urlencoded, :multipart, :json], 29 | pass: ["*/*"], 30 | json_decoder: Phoenix.json_library() 31 | 32 | plug Plug.MethodOverride 33 | plug Plug.Head 34 | 35 | # The session will be stored in the cookie and signed, 36 | # this means its contents can be read but not tampered with. 37 | # Set :encryption_salt if you would also like to encrypt it. 38 | plug Plug.Session, 39 | store: :cookie, 40 | key: "_api_ecommerce_key", 41 | signing_salt: "RUXh0u9W" 42 | 43 | plug ApiEcommerceWeb.Router 44 | end 45 | -------------------------------------------------------------------------------- /lib/api_ecommerce/auth/user.ex: -------------------------------------------------------------------------------- 1 | defmodule ApiEcommerce.Auth.User do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | @primary_key {:id, :binary_id, autogenerate: true} 6 | @foreign_key_type :binary_id 7 | schema "users" do 8 | field :name, :string 9 | field :email, :string 10 | field :role, RoleEnum, default: :member 11 | field :status, StatusEnum, default: :active 12 | field :recovery_token, :string 13 | field :recovery_token_created_at, :naive_datetime 14 | field :password_hash, :string 15 | field :password, :string, virtual: true 16 | field :password_confirmation, :string, virtual: true 17 | 18 | timestamps() 19 | end 20 | 21 | @doc false 22 | def changeset(user, attrs) do 23 | user 24 | |> cast(attrs, [:name, :email, :status, :role, :password, :password_confirmation]) 25 | |> validate_required([:email, :status, :role, :password, :password_confirmation]) 26 | |> validate_format(:email, ~r/@/) 27 | |> validate_length(:password, min: 6) 28 | |> validate_confirmation(:password) # Check that password === password_confirmation 29 | |> unique_constraint(:email) 30 | |> put_password_hash() 31 | end 32 | 33 | defp put_password_hash(%Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset) do 34 | changeset 35 | |> change(Bcrypt.add_hash(password)) 36 | |> change(%{password_confirmation: nil}) 37 | end 38 | 39 | defp put_password_hash(changeset) do 40 | changeset 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/support/data_case.ex: -------------------------------------------------------------------------------- 1 | defmodule ApiEcommerce.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 | we enable the SQL sandbox, so changes done to the database 11 | are reverted at the end of every test. If you are using 12 | PostgreSQL, you can even run database tests asynchronously 13 | by setting `use ApiEcommerceWeb.DataCase, async: true`, although 14 | this option is not recommendded for other databases. 15 | """ 16 | 17 | use ExUnit.CaseTemplate 18 | 19 | using do 20 | quote do 21 | alias ApiEcommerce.Repo 22 | 23 | import Ecto 24 | import Ecto.Changeset 25 | import Ecto.Query 26 | import ApiEcommerce.DataCase 27 | end 28 | end 29 | 30 | setup tags do 31 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(ApiEcommerce.Repo) 32 | 33 | unless tags[:async] do 34 | Ecto.Adapters.SQL.Sandbox.mode(ApiEcommerce.Repo, {:shared, self()}) 35 | end 36 | 37 | :ok 38 | end 39 | 40 | @doc """ 41 | A helper that transforms changeset errors into a map of messages. 42 | 43 | assert {:error, changeset} = Accounts.create_user(%{password: "short"}) 44 | assert "password is too short" in errors_on(changeset).password 45 | assert %{password: ["password is too short"]} = errors_on(changeset) 46 | 47 | """ 48 | def errors_on(changeset) do 49 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> 50 | Regex.replace(~r"%{(\w+)}", message, fn _, key -> 51 | opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string() 52 | end) 53 | end) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/api_ecommerce_web.ex: -------------------------------------------------------------------------------- 1 | defmodule ApiEcommerceWeb 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 ApiEcommerceWeb, :controller 9 | use ApiEcommerceWeb, :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: ApiEcommerceWeb 23 | 24 | import Plug.Conn 25 | import ApiEcommerceWeb.Gettext 26 | alias ApiEcommerceWeb.Router.Helpers, as: Routes 27 | end 28 | end 29 | 30 | def view do 31 | quote do 32 | use Phoenix.View, 33 | root: "lib/api_ecommerce_web/templates", 34 | namespace: ApiEcommerceWeb 35 | 36 | # Import convenience functions from controllers 37 | import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1] 38 | 39 | import ApiEcommerceWeb.ErrorHelpers 40 | import ApiEcommerceWeb.Gettext 41 | alias ApiEcommerceWeb.Router.Helpers, as: Routes 42 | end 43 | end 44 | 45 | def router do 46 | quote do 47 | use Phoenix.Router 48 | import Plug.Conn 49 | import Phoenix.Controller 50 | end 51 | end 52 | 53 | def channel do 54 | quote do 55 | use Phoenix.Channel 56 | import ApiEcommerceWeb.Gettext 57 | end 58 | end 59 | 60 | @doc """ 61 | When used, dispatch to the appropriate controller/view/etc. 62 | """ 63 | defmacro __using__(which) when is_atom(which) do 64 | apply(__MODULE__, which, []) 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # Configure your database 4 | config :api_ecommerce, ApiEcommerce.Repo, 5 | username: "admin", 6 | password: "admin", 7 | database: "api_ecommerce_dev", 8 | hostname: "localhost", 9 | show_sensitive_data_on_connection_error: true, 10 | pool_size: 10 11 | 12 | # For development, we disable any cache and enable 13 | # debugging and code reloading. 14 | # 15 | # The watchers configuration can be used to run external 16 | # watchers to your application. For example, we use it 17 | # with webpack to recompile .js and .css sources. 18 | config :api_ecommerce, ApiEcommerceWeb.Endpoint, 19 | http: [port: 4000], 20 | debug_errors: true, 21 | code_reloader: true, 22 | check_origin: false, 23 | watchers: [] 24 | 25 | # ## SSL Support 26 | # 27 | # In order to use HTTPS in development, a self-signed 28 | # certificate can be generated by running the following 29 | # Mix task: 30 | # 31 | # mix phx.gen.cert 32 | # 33 | # Note that this task requires Erlang/OTP 20 or later. 34 | # Run `mix help phx.gen.cert` for more information. 35 | # 36 | # The `http:` config above can be replaced with: 37 | # 38 | # https: [ 39 | # port: 4001, 40 | # cipher_suite: :strong, 41 | # keyfile: "priv/cert/selfsigned_key.pem", 42 | # certfile: "priv/cert/selfsigned.pem" 43 | # ], 44 | # 45 | # If desired, both `http:` and `https:` keys can be 46 | # configured to run both http and https servers on 47 | # different ports. 48 | 49 | # Do not include metadata nor timestamps in development logs 50 | config :logger, :console, format: "[$level] $message\n" 51 | 52 | # Set a higher stacktrace during development. Avoid configuring such 53 | # in production as building large stacktraces may be expensive. 54 | config :phoenix, :stacktrace_depth, 20 55 | 56 | # Initialize plugs at runtime for faster development compilation 57 | config :phoenix, :plug_init_mode, :runtime 58 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ApiEcommerce.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :api_ecommerce, 7 | version: "0.1.0", 8 | elixir: "~> 1.5", 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: {ApiEcommerce.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.5.4"}, 37 | {:phoenix_pubsub, "~> 2.0"}, 38 | {:phoenix_ecto, "~> 4.0"}, 39 | {:ecto_sql, "~> 3.4.5"}, 40 | {:myxql, ">= 0.0.0"}, 41 | {:gettext, "~> 0.18.0"}, 42 | {:jason, "~> 1.2.1"}, 43 | {:plug_cowboy, "~> 2.3.0"}, 44 | {:bcrypt_elixir, "~> 2.2.0"}, 45 | {:guardian, "~> 2.1.1"}, 46 | {:ecto_enum, "~> 1.4"} 47 | ] 48 | end 49 | 50 | # Aliases are shortcuts or tasks specific to the current project. 51 | # For example, to create, migrate and run the seeds file at once: 52 | # 53 | # $ mix ecto.setup 54 | # 55 | # See the documentation for `Mix` for more info on aliases. 56 | defp aliases do 57 | [ 58 | "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 59 | "ecto.reset": ["ecto.drop", "ecto.setup"], 60 | test: ["ecto.create --quiet", "ecto.migrate", "test"] 61 | ] 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/api_ecommerce_web/controllers/user_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule ApiEcommerceWeb.UserController do 2 | use ApiEcommerceWeb, :controller 3 | 4 | alias ApiEcommerce.Auth 5 | alias ApiEcommerce.Auth.User 6 | alias ApiEcommerce.Guardian 7 | 8 | action_fallback ApiEcommerceWeb.FallbackController 9 | 10 | def index(conn, _params) do 11 | users = Auth.list_users() 12 | render(conn, "index.json", users: users) 13 | end 14 | 15 | def create(conn, %{"user" => user_params}) do 16 | with {:ok, %User{} = user} <- Auth.create_user(user_params), 17 | {:ok, token, _claims} <- Guardian.encode_and_sign(user) do 18 | conn 19 | |> put_status(:created) 20 | |> put_resp_header("location", Routes.user_path(conn, :show, user)) 21 | |> render("sign_up.json", user: user, token: token) 22 | end 23 | end 24 | 25 | def show(conn, %{"id" => id}) do 26 | user = Auth.get_user!(id) 27 | render(conn, "show.json", user: user) 28 | end 29 | 30 | def update(conn, %{"id" => id, "user" => user_params}) do 31 | user = Auth.get_user!(id) 32 | 33 | with {:ok, %User{} = user} <- Auth.update_user(user, user_params) do 34 | render(conn, "show.json", user: user) 35 | end 36 | end 37 | 38 | def delete(conn, %{"id" => id}) do 39 | user = Auth.get_user!(id) 40 | 41 | with {:ok, %User{}} <- Auth.delete_user(user) do 42 | send_resp(conn, :no_content, "") 43 | end 44 | end 45 | 46 | def sign_in(conn, %{"email" => email, "password" => password}) do 47 | case ApiEcommerce.Auth.authenticate_user(email, password) do 48 | {:ok, user, token} -> 49 | conn 50 | |> put_status(:ok) 51 | |> put_view(ApiEcommerceWeb.UserView) 52 | |> render("sign_in.json", user: user, token: token) 53 | 54 | {:error, message} -> 55 | conn 56 | |> put_status(:unauthorized) 57 | |> put_view(ApiEcommerceWeb.ErrorView) 58 | |> render("401.json", message: message) 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For production, don't forget to configure the url host 4 | # to something meaningful, Phoenix uses this information 5 | # when generating URLs. 6 | # 7 | # Note we also include the path to a cache manifest 8 | # containing the digested version of static files. This 9 | # manifest is generated by the `mix phx.digest` task, 10 | # which you should run after static files are built and 11 | # before starting your production server. 12 | config :api_ecommerce, ApiEcommerceWeb.Endpoint, 13 | url: [host: "example.com", port: 80], 14 | cache_static_manifest: "priv/static/cache_manifest.json" 15 | 16 | # Do not print debug messages in production 17 | config :logger, level: :info 18 | 19 | # ## SSL Support 20 | # 21 | # To get SSL working, you will need to add the `https` key 22 | # to the previous section and set your `:url` port to 443: 23 | # 24 | # config :api_ecommerce, ApiEcommerceWeb.Endpoint, 25 | # ... 26 | # url: [host: "example.com", port: 443], 27 | # https: [ 28 | # :inet6, 29 | # port: 443, 30 | # cipher_suite: :strong, 31 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 32 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH") 33 | # ] 34 | # 35 | # The `cipher_suite` is set to `:strong` to support only the 36 | # latest and more secure SSL ciphers. This means old browsers 37 | # and clients may not be supported. You can set it to 38 | # `:compatible` for wider support. 39 | # 40 | # `:keyfile` and `:certfile` expect an absolute path to the key 41 | # and cert in disk or a relative path inside priv, for example 42 | # "priv/ssl/server.key". For all supported SSL configuration 43 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 44 | # 45 | # We also recommend setting `force_ssl` in your endpoint, ensuring 46 | # no data is ever sent via http, always redirecting to https: 47 | # 48 | # config :api_ecommerce, ApiEcommerceWeb.Endpoint, 49 | # force_ssl: [hsts: true] 50 | # 51 | # Check `Plug.SSL` for all available options in `force_ssl`. 52 | 53 | # Finally import the config/prod.secret.exs which loads secrets 54 | # and configuration from environment variables. 55 | import_config "prod.secret.exs" 56 | -------------------------------------------------------------------------------- /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 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 has 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/api_ecommerce/auth.ex: -------------------------------------------------------------------------------- 1 | defmodule ApiEcommerce.Auth do 2 | @moduledoc """ 3 | The Auth context. 4 | """ 5 | 6 | import Ecto.Query, warn: false 7 | 8 | alias ApiEcommerce.Repo 9 | alias ApiEcommerce.Auth.User 10 | alias ApiEcommerce.Guardian 11 | 12 | @doc """ 13 | Returns the list of users. 14 | 15 | ## Examples 16 | 17 | iex> list_users() 18 | [%User{}, ...] 19 | 20 | """ 21 | def list_users do 22 | Repo.all(User) 23 | end 24 | 25 | @doc """ 26 | Gets a single user. 27 | 28 | Raises `Ecto.NoResultsError` if the User does not exist. 29 | 30 | ## Examples 31 | 32 | iex> get_user!(123) 33 | %User{} 34 | 35 | iex> get_user!(456) 36 | ** (Ecto.NoResultsError) 37 | 38 | """ 39 | def get_user!(id), do: Repo.get!(User, id) 40 | 41 | @doc """ 42 | Creates a user. 43 | 44 | ## Examples 45 | 46 | iex> create_user(%{field: value}) 47 | {:ok, %User{}} 48 | 49 | iex> create_user(%{field: bad_value}) 50 | {:error, %Ecto.Changeset{}} 51 | 52 | """ 53 | def create_user(attrs \\ %{}) do 54 | %User{} 55 | |> User.changeset(attrs) 56 | |> Repo.insert() 57 | end 58 | 59 | @doc """ 60 | Updates a user. 61 | 62 | ## Examples 63 | 64 | iex> update_user(user, %{field: new_value}) 65 | {:ok, %User{}} 66 | 67 | iex> update_user(user, %{field: bad_value}) 68 | {:error, %Ecto.Changeset{}} 69 | 70 | """ 71 | def update_user(%User{} = user, attrs) do 72 | user 73 | |> User.changeset(attrs) 74 | |> Repo.update() 75 | end 76 | 77 | @doc """ 78 | Deletes a User. 79 | 80 | ## Examples 81 | 82 | iex> delete_user(user) 83 | {:ok, %User{}} 84 | 85 | iex> delete_user(user) 86 | {:error, %Ecto.Changeset{}} 87 | 88 | """ 89 | def delete_user(%User{} = user) do 90 | Repo.delete(user) 91 | end 92 | 93 | @doc """ 94 | Returns an `%Ecto.Changeset{}` for tracking user changes. 95 | 96 | ## Examples 97 | 98 | iex> change_user(user) 99 | %Ecto.Changeset{source: %User{}} 100 | 101 | """ 102 | def change_user(%User{} = user) do 103 | User.changeset(user, %{}) 104 | end 105 | 106 | def authenticate_user(email, password) do 107 | query = from(u in User, where: u.email == ^email) 108 | query 109 | |> Repo.one() 110 | |> verify_password(password) 111 | |> gen_token() 112 | end 113 | 114 | defp verify_password(nil, _) do 115 | Bcrypt.no_user_verify() 116 | {:error, "Wrong username or password"} 117 | end 118 | 119 | defp verify_password(user, password) do 120 | if Bcrypt.verify_pass(password, user.password_hash) do 121 | user 122 | else 123 | {:error, "Wrong username or password"} 124 | end 125 | end 126 | 127 | defp gen_token(%User{} = user) do 128 | case Guardian.encode_and_sign(user) do 129 | {:ok, token, _claims} -> {:ok, user, token} 130 | _ -> {:error, :unauthorized} 131 | end 132 | end 133 | 134 | defp gen_token(error) do 135 | error 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /test/api_ecommerce/auth_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ApiEcommerce.AuthTest do 2 | use ApiEcommerce.DataCase 3 | 4 | alias ApiEcommerce.Auth 5 | 6 | describe "users" do 7 | alias ApiEcommerce.Auth.User 8 | 9 | @valid_attrs %{ 10 | name: "some name", 11 | email: "some@email", 12 | status: :active, 13 | role: :member, 14 | password: "some password", 15 | password_confirmation: "some password" 16 | } 17 | @update_attrs %{ 18 | name: "some updated name", 19 | email: "some@updated.email", 20 | status: :deleted, 21 | role: :admin, 22 | password: "some updated password", 23 | password_confirmation: "some updated password" 24 | } 25 | @invalid_attrs %{email: nil, status: nil, password: nil} 26 | 27 | def user_fixture(attrs \\ %{}) do 28 | {:ok, user} = 29 | attrs 30 | |> Enum.into(@valid_attrs) 31 | |> Auth.create_user() 32 | 33 | user 34 | end 35 | 36 | test "list_users/0 returns all users" do 37 | user = user_fixture() 38 | assert Auth.list_users() |> Enum.map(fn x -> x.email end) |> to_string =~ user.email 39 | end 40 | 41 | test "get_user!/1 returns the user with given id" do 42 | user = user_fixture() 43 | assert Auth.get_user!(user.id).id == user.id 44 | assert Auth.get_user!(user.id).email == user.email 45 | end 46 | 47 | test "create_user/1 with valid data creates a user" do 48 | assert {:ok, %User{} = user} = Auth.create_user(@valid_attrs) 49 | assert user.name == @valid_attrs.name 50 | assert user.email == @valid_attrs.email 51 | assert user.status == @valid_attrs.status 52 | assert user.role == @valid_attrs.role 53 | assert Bcrypt.verify_pass(@valid_attrs.password, user.password_hash) 54 | end 55 | 56 | test "create_user/1 with invalid data returns error changeset" do 57 | assert {:error, %Ecto.Changeset{}} = Auth.create_user(@invalid_attrs) 58 | end 59 | 60 | test "update_user/2 with valid data updates the user" do 61 | user = user_fixture() 62 | assert {:ok, %User{} = user} = Auth.update_user(user, @update_attrs) 63 | assert user.name == @update_attrs.name 64 | assert user.email == @update_attrs.email 65 | assert user.status == @update_attrs.status 66 | assert user.role == @update_attrs.role 67 | assert Bcrypt.verify_pass(@update_attrs.password, user.password_hash) 68 | end 69 | 70 | test "update_user/2 with invalid data returns error changeset" do 71 | user = user_fixture() 72 | assert {:error, %Ecto.Changeset{}} = Auth.update_user(user, @invalid_attrs) 73 | user1 = Auth.get_user!(user.id) 74 | assert user.email == user1.email 75 | assert user.status == user1.status 76 | assert Bcrypt.verify_pass(@valid_attrs.password, user.password_hash) 77 | end 78 | 79 | test "delete_user/1 deletes the user" do 80 | user = user_fixture() 81 | assert {:ok, %User{}} = Auth.delete_user(user) 82 | assert_raise Ecto.NoResultsError, fn -> Auth.get_user!(user.id) end 83 | end 84 | 85 | test "change_user/1 returns a user changeset" do 86 | user = user_fixture() 87 | assert %Ecto.Changeset{} = Auth.change_user(user) 88 | end 89 | 90 | test "authenticate_user/2 authenticates the user" do 91 | user = user_fixture() 92 | assert {:error, "Wrong username or password"} = Auth.authenticate_user("wrong email", "") 93 | 94 | assert {:ok, authenticated_user, token} = 95 | Auth.authenticate_user(user.email, @valid_attrs.password) 96 | 97 | assert %{user | password: nil, password_confirmation: nil} == authenticated_user 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /test/api_ecommerce_web/controllers/user_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ApiEcommerceWeb.UserControllerTest do 2 | use ApiEcommerceWeb.ConnCase 3 | 4 | alias ApiEcommerce.Auth 5 | alias ApiEcommerce.Auth.User 6 | alias ApiEcommerce.Guardian 7 | 8 | @create_attrs %{ 9 | email: "some@email", 10 | password: "some password", 11 | password_confirmation: "some password" 12 | } 13 | @update_attrs %{ 14 | email: "some@updated.email", 15 | password: "some updated password", 16 | password_confirmation: "some updated password" 17 | } 18 | @invalid_attrs %{email: nil, is_active: nil, password: nil} 19 | @current_user_attrs %{ 20 | email: "some_current@user.email", 21 | password: "some current user password", 22 | password_confirmation: "some current user password" 23 | } 24 | 25 | def fixture(:user) do 26 | {:ok, user} = Auth.create_user(@create_attrs) 27 | user 28 | end 29 | 30 | def fixture(:current_user) do 31 | {:ok, current_user} = Auth.create_user(@current_user_attrs) 32 | current_user 33 | end 34 | 35 | setup %{conn: conn} do 36 | {:ok, conn: conn, current_user: current_user} = setup_current_user(conn) 37 | {:ok, token, _} = Guardian.encode_and_sign(current_user) 38 | {:ok, conn: put_req_header(conn, "accept", "application/json"), current_user: current_user, token: token} 39 | end 40 | 41 | describe "index" do 42 | test "lists all users", %{conn: conn, current_user: current_user, token: token} do 43 | 44 | conn = conn 45 | |> put_req_header("authorization", "bearer: " <> token) 46 | |> get(Routes.user_path(conn, :index)) 47 | 48 | assert json_response(conn, 200)["data"] == [ 49 | %{ 50 | "id" => current_user.id, 51 | "email" => current_user.email, 52 | "status" => current_user.status |> Atom.to_string(), 53 | "role" => current_user.role |> Atom.to_string() 54 | } 55 | ] 56 | end 57 | end 58 | 59 | describe "create user" do 60 | test "renders user when data is valid", %{conn: conn, token: token} do 61 | request1 = post(conn, Routes.user_path(conn, :create), user: @create_attrs) 62 | assert %{"id" => id} = json_response(request1, 201)["data"] 63 | 64 | request2 = conn 65 | |> put_req_header("authorization", "bearer: " <> token) 66 | |> get(Routes.user_path(conn, :show, id)) 67 | 68 | assert json_response(request2, 200)["data"] == %{ 69 | "id" => id, 70 | "email" => @create_attrs.email, 71 | "status" => "active", 72 | "role" => "member" 73 | } 74 | end 75 | 76 | test "renders errors when data is invalid", %{conn: conn} do 77 | conn = post(conn, Routes.user_path(conn, :create), user: @invalid_attrs) 78 | assert json_response(conn, 422)["errors"] != %{} 79 | end 80 | end 81 | 82 | describe "update user" do 83 | setup [:create_user] 84 | 85 | test "renders user when data is valid", %{conn: conn, user: %User{id: id} = user, token: token} do 86 | request1 = conn 87 | |> put_req_header("authorization", "bearer: " <> token) 88 | |> put(Routes.user_path(conn, :update, user), user: @update_attrs) 89 | 90 | assert %{"id" => ^id} = json_response(request1, 200)["data"] 91 | 92 | request2 = conn 93 | |> put_req_header("authorization", "bearer: " <> token) 94 | |> get(Routes.user_path(conn, :show, id)) 95 | 96 | assert json_response(request2, 200)["data"] == %{ 97 | "id" => id, 98 | "email" => @update_attrs.email, 99 | "status" => user.status |> Atom.to_string(), 100 | "role" => user.role |> Atom.to_string() 101 | } 102 | end 103 | 104 | test "renders errors when data is invalid", %{conn: conn, user: user, token: token} do 105 | conn = conn 106 | |> put_req_header("authorization", "bearer: " <> token) 107 | |> put(Routes.user_path(conn, :update, user), user: @invalid_attrs) 108 | assert json_response(conn, 422)["errors"] != %{} 109 | end 110 | end 111 | 112 | describe "delete user" do 113 | setup [:create_user] 114 | 115 | test "deletes chosen user", %{conn: conn, user: user, token: token} do 116 | conn = conn 117 | |> put_req_header("authorization", "bearer: " <> token) 118 | |> delete(Routes.user_path(conn, :delete, user)) 119 | assert response(conn, 204) 120 | 121 | assert_error_sent 404, fn -> 122 | get(conn, Routes.user_path(conn, :show, user)) 123 | end 124 | end 125 | end 126 | 127 | describe "sign_in user" do 128 | test "renders user when user credentials are good", %{conn: conn, current_user: current_user} do 129 | conn = 130 | post( 131 | conn, 132 | Routes.user_path(conn, :sign_in, %{email: current_user.email, password: @current_user_attrs.password}) 133 | ) 134 | 135 | assert json_response(conn, 200)["data"]["id"] == current_user.id 136 | assert json_response(conn, 200)["data"]["email"] == current_user.email 137 | assert json_response(conn, 200)["data"]["status"] == current_user.status |> Atom.to_string() 138 | assert json_response(conn, 200)["data"]["role"] == current_user.role |> Atom.to_string() 139 | assert {:ok, claims} = Guardian.decode_and_verify(json_response(conn, 200)["data"]["token"]) 140 | end 141 | 142 | test "renders errors when user credentials are bad", %{conn: conn} do 143 | conn = 144 | post(conn, Routes.user_path(conn, :sign_in, %{email: "non-existent email", password: ""})) 145 | 146 | assert json_response(conn, 401)["errors"] == %{"detail" => "Wrong username or password"} 147 | end 148 | end 149 | 150 | defp create_user(_) do 151 | user = fixture(:user) 152 | {:ok, user: user} 153 | end 154 | 155 | defp setup_current_user(conn) do 156 | current_user = fixture(:current_user) 157 | 158 | { 159 | :ok, 160 | conn: conn, 161 | current_user: current_user 162 | } 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /.idea/api-ecommerce.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm", "fab09b20e3f5db886725544cbcf875b8e73ec93363954eb8a1a9ed834aa8c1f9"}, 3 | "bcrypt_elixir": {:hex, :bcrypt_elixir, "2.2.0", "3df902b81ce7fa8867a2ae30d20a1da6877a2c056bfb116fd0bc8a5f0190cea4", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "762be3fcb779f08207531bc6612cca480a338e4b4357abb49f5ce00240a77d1e"}, 4 | "comeonin": {:hex, :comeonin, "5.3.1", "7fe612b739c78c9c1a75186ef2d322ce4d25032d119823269d0aa1e2f1e20025", [:mix], [], "hexpm", "d6222483060c17f0977fad1b7401ef0c5863c985a64352755f366aee3799c245"}, 5 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"}, 6 | "cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"}, 7 | "cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"}, 8 | "db_connection": {:hex, :db_connection, "2.2.2", "3bbca41b199e1598245b716248964926303b5d4609ff065125ce98bcd368939e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "642af240d8a8affb93b4ba5a6fcd2bbcbdc327e1a524b825d383711536f8070c"}, 9 | "decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"}, 10 | "ecto": {:hex, :ecto, "3.4.5", "2bcd262f57b2c888b0bd7f7a28c8a48aa11dc1a2c6a858e45dd8f8426d504265", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8c6d1d4d524559e9b7a062f0498e2c206122552d63eacff0a6567ffe7a8e8691"}, 11 | "ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"}, 12 | "ecto_sql": {:hex, :ecto_sql, "3.4.5", "30161f81b167d561a9a2df4329c10ae05ff36eca7ccc84628f2c8b9fa1e43323", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "31990c6a3579b36a3c0841d34a94c275e727de8b84f58509da5f1b2032c98ac2"}, 13 | "elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"}, 14 | "gettext": {:hex, :gettext, "0.18.0", "406d6b9e0e3278162c2ae1de0a60270452c553536772167e2d701f028116f870", [:mix], [], "hexpm", "c3f850be6367ebe1a08616c2158affe4a23231c70391050bf359d5f92f66a571"}, 15 | "guardian": {:hex, :guardian, "2.1.1", "1f02b349f6ba765647cc834036a8d76fa4bd65605342fe3a031df3c99d0d411a", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "189b87ba7ce6b40d6ba029138098b96ffc4ae78f229f5b39539b9141af8bf0f8"}, 16 | "jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"}, 17 | "jose": {:hex, :jose, "1.10.1", "16d8e460dae7203c6d1efa3f277e25b5af8b659febfc2f2eb4bacf87f128b80a", [:mix, :rebar3], [], "hexpm", "3c7ddc8a9394b92891db7c2771da94bf819834a1a4c92e30857b7d582e2f8257"}, 18 | "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"}, 19 | "myxql": {:hex, :myxql, "0.4.1", "92a02822598d0e819cafa01d745b586e4e0adb6f30223cd817d22d26675d7026", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:geo, "~> 3.3", [hex: :geo, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "5d9cc26bd71a33d92d42a7fb2e70e4864b03719dac5a497d7b8a8c1883eee729"}, 20 | "phoenix": {:hex, :phoenix, "1.5.4", "0fca9ce7e960f9498d6315e41fcd0c80bfa6fbeb5fa3255b830c67fdfb7e703f", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4e516d131fde87b568abd62e1b14aa07ba7d5edfd230bab4e25cc9dedbb39135"}, 21 | "phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [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", "c5e666a341ff104d0399d8f0e4ff094559b2fde13a5985d4cb5023b2c2ac558b"}, 22 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"}, 23 | "plug": {:hex, :plug, "1.10.3", "c9cebe917637d8db0e759039cc106adca069874e1a9034fd6e3fdd427fd3c283", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "01f9037a2a1de1d633b5a881101e6a444bcabb1d386ca1e00bb273a1f1d9d939"}, 24 | "plug_cowboy": {:hex, :plug_cowboy, "2.3.0", "149a50e05cb73c12aad6506a371cd75750c0b19a32f81866e1a323dda9e0e99d", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bc595a1870cef13f9c1e03df56d96804db7f702175e4ccacdb8fc75c02a7b97e"}, 25 | "plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"}, 26 | "postgrex": {:hex, :postgrex, "0.15.3", "5806baa8a19a68c4d07c7a624ccdb9b57e89cbc573f1b98099e3741214746ae4", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, 27 | "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"}, 28 | "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, 29 | } 30 | --------------------------------------------------------------------------------