feat: Add basic auth framework
This commit is contained in:
		
							parent
							
								
									e2351a3269
								
							
						
					
					
						commit
						83b5910b32
					
				
					 99 changed files with 10502 additions and 570 deletions
				
			
		
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							|  | @ -35,3 +35,5 @@ npm-debug.log | |||
| 
 | ||||
| # Lock file for Brew, since the versions aren't really stable & isolated anyway | ||||
| Brewfile.lock.json | ||||
| 
 | ||||
| config/*.secret.exs | ||||
|  |  | |||
							
								
								
									
										5
									
								
								apps/auth/.formatter.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								apps/auth/.formatter.exs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| [ | ||||
|   import_deps: [:ecto], | ||||
|   inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"], | ||||
|   subdirectories: ["priv/*/migrations"] | ||||
| ] | ||||
							
								
								
									
										23
									
								
								apps/auth/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								apps/auth/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,23 @@ | |||
| # The directory Mix will write compiled artifacts to. | ||||
| /_build/ | ||||
| 
 | ||||
| # If you run "mix test --cover", coverage assets end up here. | ||||
| /cover/ | ||||
| 
 | ||||
| # The directory Mix downloads your dependencies sources to. | ||||
| /deps/ | ||||
| 
 | ||||
| # Where 3rd-party dependencies like ExDoc output generated docs. | ||||
| /doc/ | ||||
| 
 | ||||
| # Ignore .fetch files in case you like to edit your project deps locally. | ||||
| /.fetch | ||||
| 
 | ||||
| # If the VM crashes, it generates a dump, let's ignore it too. | ||||
| erl_crash.dump | ||||
| 
 | ||||
| # Also ignore archive artifacts (built via "mix archive.build"). | ||||
| *.ez | ||||
| 
 | ||||
| # Ignore package tarball (built via "mix hex.build"). | ||||
| auth-*.tar | ||||
							
								
								
									
										3
									
								
								apps/auth/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								apps/auth/README.md
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| # Auth | ||||
| 
 | ||||
| **TODO: Add description** | ||||
							
								
								
									
										3
									
								
								apps/auth/assets/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								apps/auth/assets/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| { | ||||
|   "lockfileVersion": 1 | ||||
| } | ||||
							
								
								
									
										9
									
								
								apps/auth/lib/auth.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								apps/auth/lib/auth.ex
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | |||
| defmodule Auth do | ||||
|   @moduledoc """ | ||||
|   Auth keeps the contexts that define your domain | ||||
|   and business logic. | ||||
| 
 | ||||
|   Contexts are also responsible for managing your data, regardless | ||||
|   if it comes from the database, an external API or others. | ||||
|   """ | ||||
| end | ||||
							
								
								
									
										20
									
								
								apps/auth/lib/auth/application.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								apps/auth/lib/auth/application.ex
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | |||
| defmodule Auth.Application do | ||||
|   # See https://hexdocs.pm/elixir/Application.html | ||||
|   # for more information on OTP Applications | ||||
|   @moduledoc false | ||||
| 
 | ||||
|   use Application | ||||
| 
 | ||||
|   def start(_type, _args) do | ||||
|     children = [ | ||||
|       # Start the Ecto repository | ||||
|       Auth.Repo, | ||||
|       # Start the PubSub system | ||||
|       {Phoenix.PubSub, name: Auth.PubSub} | ||||
|       # Start a worker by calling: Auth.Worker.start_link(arg) | ||||
|       # {Auth.Worker, arg} | ||||
|     ] | ||||
| 
 | ||||
|     Supervisor.start_link(children, strategy: :one_for_one, name: Auth.Supervisor) | ||||
|   end | ||||
| end | ||||
							
								
								
									
										5
									
								
								apps/auth/lib/auth/repo.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								apps/auth/lib/auth/repo.ex
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| defmodule Auth.Repo do | ||||
|   use Ecto.Repo, | ||||
|     otp_app: :auth, | ||||
|     adapter: Ecto.Adapters.Postgres | ||||
| end | ||||
							
								
								
									
										36
									
								
								apps/auth/lib/auth/users/user.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								apps/auth/lib/auth/users/user.ex
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,36 @@ | |||
| defmodule Auth.Users.User do | ||||
|   @moduledoc """ | ||||
|   The baseline user schema module. | ||||
|   """ | ||||
|   use Ecto.Schema | ||||
|   use Pow.Ecto.Schema | ||||
|   use Pow.Extension.Ecto.Schema, | ||||
|     extensions: [PowResetPassword, PowEmailConfirmation] | ||||
| 
 | ||||
|   import Pow.Ecto.Schema.Changeset, only: [new_password_changeset: 3] | ||||
| 
 | ||||
|   alias Ecto.Changeset | ||||
| 
 | ||||
|   schema "users" do | ||||
|     field :roles, {:array, :string} | ||||
| 
 | ||||
|     pow_user_fields() | ||||
| 
 | ||||
|     timestamps() | ||||
|   end | ||||
| 
 | ||||
|   def changeset(user_or_changeset, attrs) do | ||||
|     user_or_changeset | ||||
|     |> pow_user_id_field_changeset(attrs) | ||||
|     |> pow_current_password_changeset(attrs) | ||||
|     |> new_password_changeset(attrs, @pow_config) | ||||
|     |> Changeset.cast(attrs, [:roles]) | ||||
|     |> pow_extension_changeset(attrs) | ||||
|   end | ||||
| 
 | ||||
|   def reset_password_changeset(user = %user_mod{}, params) do | ||||
|     user | ||||
|     |> new_password_changeset(params, @pow_config) | ||||
|     |> Changeset.validate_required([:password]) | ||||
|   end | ||||
| end | ||||
							
								
								
									
										41
									
								
								apps/auth/lib/mix/tasks/create_admin.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								apps/auth/lib/mix/tasks/create_admin.ex
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,41 @@ | |||
| defmodule Mix.Tasks.Legendary.CreateAdmin do | ||||
|   @moduledoc """ | ||||
|   Mix task to create an admin user from the command line. | ||||
|   """ | ||||
|   use Mix.Task | ||||
| 
 | ||||
|   alias Auth.Users.User | ||||
|   alias Auth.Repo | ||||
|   alias Ecto.Changeset | ||||
| 
 | ||||
|   @shortdoc "Create an admin user." | ||||
|   def run(_) do | ||||
|     Application.ensure_all_started(:auth) | ||||
| 
 | ||||
|     email = ExPrompt.string_required("Email: ") | ||||
|     password = ExPrompt.password("Password: ") | ||||
| 
 | ||||
|    params = %{ | ||||
|       email: email, | ||||
|       password: password, | ||||
|       roles: ["admin"], | ||||
|     } | ||||
| 
 | ||||
|     %User{} | ||||
|     |> User.changeset(params) | ||||
|     |> maybe_confirm_email() | ||||
|     |> Repo.insert!() | ||||
|   end | ||||
| 
 | ||||
|   def maybe_confirm_email(changeset) do | ||||
|     field_list = User.__schema__(:fields) | ||||
| 
 | ||||
|     case  Enum.any?(field_list, &(&1 == :email_confirmed_at)) do | ||||
|       true -> | ||||
|         changeset | ||||
|         |> Changeset.cast(%{email_confirmed_at: DateTime.utc_now()}, [:email_confirmed_at]) | ||||
|       false -> | ||||
|         changeset | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										59
									
								
								apps/auth/mix.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								apps/auth/mix.exs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,59 @@ | |||
| defmodule Auth.MixProject do | ||||
|   use Mix.Project | ||||
| 
 | ||||
|   def project do | ||||
|     [ | ||||
|       app: :auth, | ||||
|       version: "0.1.0", | ||||
|       build_path: "../../_build", | ||||
|       config_path: "../../config/config.exs", | ||||
|       deps_path: "../../deps", | ||||
|       lockfile: "../../mix.lock", | ||||
|       elixir: "~> 1.7", | ||||
|       elixirc_paths: elixirc_paths(Mix.env()), | ||||
|       start_permanent: Mix.env() == :prod, | ||||
|       aliases: aliases(), | ||||
|       deps: deps() | ||||
|     ] | ||||
|   end | ||||
| 
 | ||||
|   # Configuration for the OTP application. | ||||
|   # | ||||
|   # Type `mix help compile.app` for more information. | ||||
|   def application do | ||||
|     [ | ||||
|       mod: {Auth.Application, []}, | ||||
|       extra_applications: [:logger, :runtime_tools] | ||||
|     ] | ||||
|   end | ||||
| 
 | ||||
|   # Specifies which paths to compile per environment. | ||||
|   defp elixirc_paths(:test), do: ["lib", "test/support"] | ||||
|   defp elixirc_paths(_), do: ["lib"] | ||||
| 
 | ||||
|   # Specifies your project dependencies. | ||||
|   # | ||||
|   # Type `mix help deps` for examples and options. | ||||
|   defp deps do | ||||
|     [ | ||||
|       {:ex_prompt, "~> 0.1.5"}, | ||||
|       {:phoenix_pubsub, "~> 2.0"}, | ||||
|       {:pow, "~> 1.0.20"}, | ||||
|       {:ecto_sql, "~> 3.4"}, | ||||
|       {:postgrex, ">= 0.0.0"}, | ||||
|       {:jason, "~> 1.0"} | ||||
|     ] | ||||
|   end | ||||
| 
 | ||||
|   # Aliases are shortcuts or tasks specific to the current project. | ||||
|   # | ||||
|   # See the documentation for `Mix` for more info on aliases. | ||||
|   defp aliases do | ||||
|     [ | ||||
|       setup: ["deps.get", "ecto.setup"], | ||||
|       "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], | ||||
|       "ecto.reset": ["ecto.drop", "ecto.setup"], | ||||
|       test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"] | ||||
|     ] | ||||
|   end | ||||
| end | ||||
							
								
								
									
										4
									
								
								apps/auth/priv/repo/migrations/.formatter.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								apps/auth/priv/repo/migrations/.formatter.exs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | |||
| [ | ||||
|   import_deps: [:ecto_sql], | ||||
|   inputs: ["*.exs"] | ||||
| ] | ||||
|  | @ -0,0 +1,14 @@ | |||
| defmodule Auth.Repo.Migrations.CreateUsers do | ||||
|   use Ecto.Migration | ||||
| 
 | ||||
|   def change do | ||||
|     create table(:users) do | ||||
|       add :email, :string, null: false | ||||
|       add :password_hash, :string | ||||
| 
 | ||||
|       timestamps() | ||||
|     end | ||||
| 
 | ||||
|     create unique_index(:users, [:email]) | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,13 @@ | |||
| defmodule Auth.Repo.Migrations.AddPowEmailConfirmationToUsers do | ||||
|   use Ecto.Migration | ||||
| 
 | ||||
|   def change do | ||||
|     alter table(:users) do | ||||
|       add :email_confirmation_token, :string | ||||
|       add :email_confirmed_at, :utc_datetime | ||||
|       add :unconfirmed_email, :string | ||||
|     end | ||||
| 
 | ||||
|     create unique_index(:users, [:email_confirmation_token]) | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,9 @@ | |||
| defmodule Auth.Repo.Migrations.AddRolesToUsers do | ||||
|   use Ecto.Migration | ||||
| 
 | ||||
|   def change do | ||||
|     alter table(:users) do | ||||
|       add :roles, {:array, :string}, default: [] | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										11
									
								
								apps/auth/priv/repo/seeds.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								apps/auth/priv/repo/seeds.exs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | |||
| # Script for populating the database. You can run it as: | ||||
| # | ||||
| #     mix run priv/repo/seeds.exs | ||||
| # | ||||
| # Inside the script, you can read and write to any of your | ||||
| # repositories directly: | ||||
| # | ||||
| #     Auth.Repo.insert!(%Auth.SomeSchema{}) | ||||
| # | ||||
| # We recommend using the bang functions (`insert!`, `update!` | ||||
| # and so on) as they will fail if something goes wrong. | ||||
							
								
								
									
										55
									
								
								apps/auth/test/support/data_case.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								apps/auth/test/support/data_case.ex
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,55 @@ | |||
| defmodule Auth.DataCase do | ||||
|   @moduledoc """ | ||||
|   This module defines the setup for tests requiring | ||||
|   access to the application's data layer. | ||||
| 
 | ||||
|   You may define functions here to be used as helpers in | ||||
|   your tests. | ||||
| 
 | ||||
|   Finally, if the test case interacts with the database, | ||||
|   we enable the SQL sandbox, so changes done to the database | ||||
|   are reverted at the end of every test. If you are using | ||||
|   PostgreSQL, you can even run database tests asynchronously | ||||
|   by setting `use Auth.DataCase, async: true`, although | ||||
|   this option is not recommended for other databases. | ||||
|   """ | ||||
| 
 | ||||
|   use ExUnit.CaseTemplate | ||||
| 
 | ||||
|   using do | ||||
|     quote do | ||||
|       alias Auth.Repo | ||||
| 
 | ||||
|       import Ecto | ||||
|       import Ecto.Changeset | ||||
|       import Ecto.Query | ||||
|       import Auth.DataCase | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   setup tags do | ||||
|     :ok = Ecto.Adapters.SQL.Sandbox.checkout(Auth.Repo) | ||||
| 
 | ||||
|     unless tags[:async] do | ||||
|       Ecto.Adapters.SQL.Sandbox.mode(Auth.Repo, {:shared, self()}) | ||||
|     end | ||||
| 
 | ||||
|     :ok | ||||
|   end | ||||
| 
 | ||||
|   @doc """ | ||||
|   A helper that transforms changeset errors into a map of messages. | ||||
| 
 | ||||
|       assert {:error, changeset} = Accounts.create_user(%{password: "short"}) | ||||
|       assert "password is too short" in errors_on(changeset).password | ||||
|       assert %{password: ["password is too short"]} = errors_on(changeset) | ||||
| 
 | ||||
|   """ | ||||
|   def errors_on(changeset) do | ||||
|     Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> | ||||
|       Regex.replace(~r"%{(\w+)}", message, fn _, key -> | ||||
|         opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string() | ||||
|       end) | ||||
|     end) | ||||
|   end | ||||
| end | ||||
							
								
								
									
										2
									
								
								apps/auth/test/test_helper.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								apps/auth/test/test_helper.exs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,2 @@ | |||
| ExUnit.start() | ||||
| Ecto.Adapters.SQL.Sandbox.mode(Auth.Repo, :manual) | ||||
							
								
								
									
										4
									
								
								apps/auth_web/.formatter.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								apps/auth_web/.formatter.exs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | |||
| [ | ||||
|   import_deps: [:phoenix], | ||||
|   inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"] | ||||
| ] | ||||
							
								
								
									
										34
									
								
								apps/auth_web/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								apps/auth_web/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,34 @@ | |||
| # The directory Mix will write compiled artifacts to. | ||||
| /_build/ | ||||
| 
 | ||||
| # If you run "mix test --cover", coverage assets end up here. | ||||
| /cover/ | ||||
| 
 | ||||
| # The directory Mix downloads your dependencies sources to. | ||||
| /deps/ | ||||
| 
 | ||||
| # Where 3rd-party dependencies like ExDoc output generated docs. | ||||
| /doc/ | ||||
| 
 | ||||
| # Ignore .fetch files in case you like to edit your project deps locally. | ||||
| /.fetch | ||||
| 
 | ||||
| # If the VM crashes, it generates a dump, let's ignore it too. | ||||
| erl_crash.dump | ||||
| 
 | ||||
| # Also ignore archive artifacts (built via "mix archive.build"). | ||||
| *.ez | ||||
| 
 | ||||
| # Ignore package tarball (built via "mix hex.build"). | ||||
| auth_web-*.tar | ||||
| 
 | ||||
| # If NPM crashes, it generates a log, let's ignore it too. | ||||
| npm-debug.log | ||||
| 
 | ||||
| # The directory NPM downloads your dependencies sources to. | ||||
| /assets/node_modules/ | ||||
| 
 | ||||
| # Since we are building assets from assets/, | ||||
| # we ignore priv/static. You may want to comment | ||||
| # this depending on your deployment strategy. | ||||
| /priv/static/ | ||||
							
								
								
									
										20
									
								
								apps/auth_web/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								apps/auth_web/README.md
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | |||
| # AuthWeb | ||||
| 
 | ||||
| To start your Phoenix server: | ||||
| 
 | ||||
|   * Install dependencies with `mix deps.get` | ||||
|   * Create and migrate your database with `mix ecto.setup` | ||||
|   * Install Node.js dependencies with `npm install` inside the `assets` directory | ||||
|   * Start Phoenix endpoint with `mix phx.server` | ||||
| 
 | ||||
| Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. | ||||
| 
 | ||||
| Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). | ||||
| 
 | ||||
| ## Learn more | ||||
| 
 | ||||
|   * Official website: https://www.phoenixframework.org/ | ||||
|   * Guides: https://hexdocs.pm/phoenix/overview.html | ||||
|   * Docs: https://hexdocs.pm/phoenix | ||||
|   * Forum: https://elixirforum.com/c/phoenix-forum | ||||
|   * Source: https://github.com/phoenixframework/phoenix | ||||
							
								
								
									
										5
									
								
								apps/auth_web/assets/.babelrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								apps/auth_web/assets/.babelrc
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| { | ||||
|     "presets": [ | ||||
|         "@babel/preset-env" | ||||
|     ] | ||||
| } | ||||
							
								
								
									
										31
									
								
								apps/auth_web/assets/css/app.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								apps/auth_web/assets/css/app.scss
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,31 @@ | |||
| /* This file is for your main application css. */ | ||||
| @import "./phoenix.css"; | ||||
| 
 | ||||
| /* Alerts and form errors */ | ||||
| .alert { | ||||
|   padding: 15px; | ||||
|   margin-bottom: 20px; | ||||
|   border: 1px solid transparent; | ||||
|   border-radius: 4px; | ||||
| } | ||||
| .alert-info { | ||||
|   color: #31708f; | ||||
|   background-color: #d9edf7; | ||||
|   border-color: #bce8f1; | ||||
| } | ||||
| .alert-warning { | ||||
|   color: #8a6d3b; | ||||
|   background-color: #fcf8e3; | ||||
|   border-color: #faebcc; | ||||
| } | ||||
| .alert-danger { | ||||
|   color: #a94442; | ||||
|   background-color: #f2dede; | ||||
|   border-color: #ebccd1; | ||||
| } | ||||
| .alert p { | ||||
|   margin-bottom: 0; | ||||
| } | ||||
| .alert:empty { | ||||
|   display: none; | ||||
| } | ||||
							
								
								
									
										101
									
								
								apps/auth_web/assets/css/phoenix.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								apps/auth_web/assets/css/phoenix.css
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										15
									
								
								apps/auth_web/assets/js/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								apps/auth_web/assets/js/app.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| // We need to import the CSS so that webpack will load it.
 | ||||
| // The MiniCssExtractPlugin is used to separate it out into
 | ||||
| // its own CSS file.
 | ||||
| import "../css/app.scss" | ||||
| 
 | ||||
| // webpack automatically bundles all modules in your
 | ||||
| // entry points. Those entry points can be configured
 | ||||
| // in "webpack.config.js".
 | ||||
| //
 | ||||
| // Import deps with the dep name or local files with a relative path, for example:
 | ||||
| //
 | ||||
| //     import {Socket} from "phoenix"
 | ||||
| //     import socket from "./socket"
 | ||||
| //
 | ||||
| import "phoenix_html" | ||||
							
								
								
									
										63
									
								
								apps/auth_web/assets/js/socket.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								apps/auth_web/assets/js/socket.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,63 @@ | |||
| // NOTE: The contents of this file will only be executed if
 | ||||
| // you uncomment its entry in "assets/js/app.js".
 | ||||
| 
 | ||||
| // To use Phoenix channels, the first step is to import Socket,
 | ||||
| // and connect at the socket path in "lib/web/endpoint.ex".
 | ||||
| //
 | ||||
| // Pass the token on params as below. Or remove it
 | ||||
| // from the params if you are not using authentication.
 | ||||
| import {Socket} from "phoenix" | ||||
| 
 | ||||
| let socket = new Socket("/socket", {params: {token: window.userToken}}) | ||||
| 
 | ||||
| // When you connect, you'll often need to authenticate the client.
 | ||||
| // For example, imagine you have an authentication plug, `MyAuth`,
 | ||||
| // which authenticates the session and assigns a `:current_user`.
 | ||||
| // If the current user exists you can assign the user's token in
 | ||||
| // the connection for use in the layout.
 | ||||
| //
 | ||||
| // In your "lib/web/router.ex":
 | ||||
| //
 | ||||
| //     pipeline :browser do
 | ||||
| //       ...
 | ||||
| //       plug MyAuth
 | ||||
| //       plug :put_user_token
 | ||||
| //     end
 | ||||
| //
 | ||||
| //     defp put_user_token(conn, _) do
 | ||||
| //       if current_user = conn.assigns[:current_user] do
 | ||||
| //         token = Phoenix.Token.sign(conn, "user socket", current_user.id)
 | ||||
| //         assign(conn, :user_token, token)
 | ||||
| //       else
 | ||||
| //         conn
 | ||||
| //       end
 | ||||
| //     end
 | ||||
| //
 | ||||
| // Now you need to pass this token to JavaScript. You can do so
 | ||||
| // inside a script tag in "lib/web/templates/layout/app.html.eex":
 | ||||
| //
 | ||||
| //     <script>window.userToken = "<%= assigns[:user_token] %>";</script>
 | ||||
| //
 | ||||
| // You will need to verify the user token in the "connect/3" function
 | ||||
| // in "lib/web/channels/user_socket.ex":
 | ||||
| //
 | ||||
| //     def connect(%{"token" => token}, socket, _connect_info) do
 | ||||
| //       # max_age: 1209600 is equivalent to two weeks in seconds
 | ||||
| //       case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do
 | ||||
| //         {:ok, user_id} ->
 | ||||
| //           {:ok, assign(socket, :user, user_id)}
 | ||||
| //         {:error, reason} ->
 | ||||
| //           :error
 | ||||
| //       end
 | ||||
| //     end
 | ||||
| //
 | ||||
| // Finally, connect to the socket:
 | ||||
| socket.connect() | ||||
| 
 | ||||
| // Now that you are connected, you can join channels with a topic:
 | ||||
| let channel = socket.channel("topic:subtopic", {}) | ||||
| channel.join() | ||||
|   .receive("ok", resp => { console.log("Joined successfully", resp) }) | ||||
|   .receive("error", resp => { console.log("Unable to join", resp) }) | ||||
| 
 | ||||
| export default socket | ||||
							
								
								
									
										7931
									
								
								apps/auth_web/assets/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										7931
									
								
								apps/auth_web/assets/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										27
									
								
								apps/auth_web/assets/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								apps/auth_web/assets/package.json
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | |||
| { | ||||
|   "repository": {}, | ||||
|   "description": " ", | ||||
|   "license": "MIT", | ||||
|   "scripts": { | ||||
|     "deploy": "webpack --mode production", | ||||
|     "watch": "webpack --mode development --watch" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "phoenix": "file:../../../deps/phoenix", | ||||
|     "phoenix_html": "file:../../../deps/phoenix_html" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@babel/core": "^7.0.0", | ||||
|     "@babel/preset-env": "^7.0.0", | ||||
|     "babel-loader": "^8.0.0", | ||||
|     "copy-webpack-plugin": "^5.1.1", | ||||
|     "css-loader": "^3.4.2", | ||||
|     "sass-loader": "^8.0.2", | ||||
|     "node-sass": "^4.13.1", | ||||
|     "mini-css-extract-plugin": "^0.9.0", | ||||
|     "optimize-css-assets-webpack-plugin": "^5.0.1", | ||||
|     "terser-webpack-plugin": "^2.3.2", | ||||
|     "webpack": "4.41.5", | ||||
|     "webpack-cli": "^3.3.2" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								apps/auth_web/assets/static/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								apps/auth_web/assets/static/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								apps/auth_web/assets/static/images/phoenix.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								apps/auth_web/assets/static/images/phoenix.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 14 KiB | 
							
								
								
									
										5
									
								
								apps/auth_web/assets/static/robots.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								apps/auth_web/assets/static/robots.txt
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file | ||||
| # | ||||
| # To ban all spiders from the entire site uncomment the next two lines: | ||||
| # User-agent: * | ||||
| # Disallow: / | ||||
							
								
								
									
										51
									
								
								apps/auth_web/assets/webpack.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								apps/auth_web/assets/webpack.config.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,51 @@ | |||
| const path = require('path'); | ||||
| const glob = require('glob'); | ||||
| const MiniCssExtractPlugin = require('mini-css-extract-plugin'); | ||||
| const TerserPlugin = require('terser-webpack-plugin'); | ||||
| const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); | ||||
| const CopyWebpackPlugin = require('copy-webpack-plugin'); | ||||
| 
 | ||||
| module.exports = (env, options) => { | ||||
|   const devMode = options.mode !== 'production'; | ||||
| 
 | ||||
|   return { | ||||
|     optimization: { | ||||
|       minimizer: [ | ||||
|         new TerserPlugin({ cache: true, parallel: true, sourceMap: devMode }), | ||||
|         new OptimizeCSSAssetsPlugin({}) | ||||
|       ] | ||||
|     }, | ||||
|     entry: { | ||||
|       'app': glob.sync('./vendor/**/*.js').concat(['./js/app.js']) | ||||
|     }, | ||||
|     output: { | ||||
|       filename: '[name].js', | ||||
|       path: path.resolve(__dirname, '../priv/static/js'), | ||||
|       publicPath: '/js/' | ||||
|     }, | ||||
|     devtool: devMode ? 'source-map' : undefined, | ||||
|     module: { | ||||
|       rules: [ | ||||
|         { | ||||
|           test: /\.js$/, | ||||
|           exclude: /node_modules/, | ||||
|           use: { | ||||
|             loader: 'babel-loader' | ||||
|           } | ||||
|         }, | ||||
|         { | ||||
|           test: /\.[s]?css$/, | ||||
|           use: [ | ||||
|             MiniCssExtractPlugin.loader, | ||||
|             'css-loader', | ||||
|             'sass-loader', | ||||
|           ], | ||||
|         } | ||||
|       ] | ||||
|     }, | ||||
|     plugins: [ | ||||
|       new MiniCssExtractPlugin({ filename: '../css/app.css' }), | ||||
|       new CopyWebpackPlugin([{ from: 'static/', to: '../' }]) | ||||
|     ] | ||||
|   } | ||||
| }; | ||||
							
								
								
									
										92
									
								
								apps/auth_web/lib/auth_web.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								apps/auth_web/lib/auth_web.ex
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,92 @@ | |||
| defmodule AuthWeb do | ||||
|   @moduledoc """ | ||||
|   The entrypoint for defining your web interface, such | ||||
|   as controllers, views, channels and so on. | ||||
| 
 | ||||
|   This can be used in your application as: | ||||
| 
 | ||||
|       use AuthWeb, :controller | ||||
|       use AuthWeb, :view | ||||
| 
 | ||||
|   The definitions below will be executed for every view, | ||||
|   controller, etc, so keep them short and clean, focused | ||||
|   on imports, uses and aliases. | ||||
| 
 | ||||
|   Do NOT define functions inside the quoted expressions | ||||
|   below. Instead, define any helper function in modules | ||||
|   and import those modules here. | ||||
|   """ | ||||
| 
 | ||||
|   def controller do | ||||
|     quote do | ||||
|       use Phoenix.Controller, namespace: AuthWeb | ||||
| 
 | ||||
|       import Plug.Conn | ||||
|       import AuthWeb.Gettext | ||||
|       alias AuthWeb.Router.Helpers, as: Routes | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def view do | ||||
|     quote do | ||||
|       use Phoenix.View, | ||||
|         root: "lib/auth_web/templates", | ||||
|         namespace: AuthWeb | ||||
| 
 | ||||
|       # Import convenience functions from controllers | ||||
|       import Phoenix.Controller, | ||||
|         only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1] | ||||
| 
 | ||||
|       # Include shared imports and aliases for views | ||||
|       unquote(view_helpers()) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def router do | ||||
|     quote do | ||||
|       use Phoenix.Router | ||||
| 
 | ||||
|       import Plug.Conn | ||||
|       import Phoenix.Controller | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def channel do | ||||
|     quote do | ||||
|       use Phoenix.Channel | ||||
|       import AuthWeb.Gettext | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   defp view_helpers do | ||||
|     quote do | ||||
|       # Use all HTML functionality (forms, tags, etc) | ||||
|       use Phoenix.HTML | ||||
| 
 | ||||
|       # Import basic rendering functionality (render, render_layout, etc) | ||||
|       import Phoenix.View | ||||
| 
 | ||||
|       import AuthWeb.ErrorHelpers | ||||
|       import AuthWeb.Helpers | ||||
|       import AuthWeb.Gettext | ||||
|       alias AuthWeb.Router.Helpers, as: Routes | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def mailer_view do | ||||
|     quote do | ||||
|       use Phoenix.View, root: "lib/auth_web/templates", | ||||
|                         namespace: AuthWeb | ||||
| 
 | ||||
|       use Phoenix.HTML | ||||
|       import CoreWeb.EmailHelpers | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   @doc """ | ||||
|   When used, dispatch to the appropriate controller/view/etc. | ||||
|   """ | ||||
|   defmacro __using__(which) when is_atom(which) do | ||||
|     apply(__MODULE__, which, []) | ||||
|   end | ||||
| end | ||||
							
								
								
									
										30
									
								
								apps/auth_web/lib/auth_web/application.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								apps/auth_web/lib/auth_web/application.ex
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | |||
| defmodule AuthWeb.Application do | ||||
|   # See https://hexdocs.pm/elixir/Application.html | ||||
|   # for more information on OTP Applications | ||||
|   @moduledoc false | ||||
| 
 | ||||
|   use Application | ||||
| 
 | ||||
|   def start(_type, _args) do | ||||
|     children = [ | ||||
|       # Start the Telemetry supervisor | ||||
|       AuthWeb.Telemetry, | ||||
|       # Start the Endpoint (http/https) | ||||
|       AuthWeb.Endpoint | ||||
|       # Start a worker by calling: AuthWeb.Worker.start_link(arg) | ||||
|       # {AuthWeb.Worker, arg} | ||||
|     ] | ||||
| 
 | ||||
|     # See https://hexdocs.pm/elixir/Supervisor.html | ||||
|     # for other strategies and supported options | ||||
|     opts = [strategy: :one_for_one, name: AuthWeb.Supervisor] | ||||
|     Supervisor.start_link(children, opts) | ||||
|   end | ||||
| 
 | ||||
|   # Tell Phoenix to update the endpoint configuration | ||||
|   # whenever the application is updated. | ||||
|   def config_change(changed, _new, removed) do | ||||
|     AuthWeb.Endpoint.config_change(changed, removed) | ||||
|     :ok | ||||
|   end | ||||
| end | ||||
							
								
								
									
										35
									
								
								apps/auth_web/lib/auth_web/channels/user_socket.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								apps/auth_web/lib/auth_web/channels/user_socket.ex
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,35 @@ | |||
| defmodule AuthWeb.UserSocket do | ||||
|   use Phoenix.Socket | ||||
| 
 | ||||
|   ## Channels | ||||
|   # channel "room:*", AuthWeb.RoomChannel | ||||
| 
 | ||||
|   # Socket params are passed from the client and can | ||||
|   # be used to verify and authenticate a user. After | ||||
|   # verification, you can put default assigns into | ||||
|   # the socket that will be set for all channels, ie | ||||
|   # | ||||
|   #     {:ok, assign(socket, :user_id, verified_user_id)} | ||||
|   # | ||||
|   # To deny connection, return `:error`. | ||||
|   # | ||||
|   # See `Phoenix.Token` documentation for examples in | ||||
|   # performing token verification on connect. | ||||
|   @impl true | ||||
|   def connect(_params, socket, _connect_info) do | ||||
|     {:ok, socket} | ||||
|   end | ||||
| 
 | ||||
|   # Socket id's are topics that allow you to identify all sockets for a given user: | ||||
|   # | ||||
|   #     def id(socket), do: "user_socket:#{socket.assigns.user_id}" | ||||
|   # | ||||
|   # Would allow you to broadcast a "disconnect" event and terminate | ||||
|   # all active sockets and channels for a given user: | ||||
|   # | ||||
|   #     AuthWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) | ||||
|   # | ||||
|   # Returning `nil` makes this socket anonymous. | ||||
|   @impl true | ||||
|   def id(_socket), do: nil | ||||
| end | ||||
							
								
								
									
										0
									
								
								apps/auth_web/lib/auth_web/controllers/.keep
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								apps/auth_web/lib/auth_web/controllers/.keep
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										54
									
								
								apps/auth_web/lib/auth_web/endpoint.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								apps/auth_web/lib/auth_web/endpoint.ex
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,54 @@ | |||
| defmodule AuthWeb.Endpoint do | ||||
|   use Phoenix.Endpoint, otp_app: :auth_web | ||||
| 
 | ||||
|   # The session will be stored in the cookie and signed, | ||||
|   # this means its contents can be read but not tampered with. | ||||
|   # Set :encryption_salt if you would also like to encrypt it. | ||||
|   @session_options [ | ||||
|     store: :cookie, | ||||
|     key: "_auth_web_key", | ||||
|     signing_salt: "bwwz3vUK" | ||||
|   ] | ||||
| 
 | ||||
|   socket "/socket", AuthWeb.UserSocket, | ||||
|     websocket: true, | ||||
|     longpoll: false | ||||
| 
 | ||||
|   socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]] | ||||
| 
 | ||||
|   # Serve at "/" the static files from "priv/static" directory. | ||||
|   # | ||||
|   # You should set gzip to true if you are running phx.digest | ||||
|   # when deploying your static files in production. | ||||
|   plug Plug.Static, | ||||
|     at: "/", | ||||
|     from: :auth_web, | ||||
|     gzip: false, | ||||
|     only: ~w(css fonts images js favicon.ico robots.txt) | ||||
| 
 | ||||
|   # Code reloading can be explicitly enabled under the | ||||
|   # :code_reloader configuration of your endpoint. | ||||
|   if code_reloading? do | ||||
|     socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket | ||||
|     plug Phoenix.LiveReloader | ||||
|     plug Phoenix.CodeReloader | ||||
|     plug Phoenix.Ecto.CheckRepoStatus, otp_app: :auth_web | ||||
|   end | ||||
| 
 | ||||
|   plug Phoenix.LiveDashboard.RequestLogger, | ||||
|     param_key: "request_logger", | ||||
|     cookie_key: "request_logger" | ||||
| 
 | ||||
|   plug Plug.RequestId | ||||
|   plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] | ||||
| 
 | ||||
|   plug Plug.Parsers, | ||||
|     parsers: [:urlencoded, :multipart, :json], | ||||
|     pass: ["*/*"], | ||||
|     json_decoder: Phoenix.json_library() | ||||
| 
 | ||||
|   plug Plug.MethodOverride | ||||
|   plug Plug.Head | ||||
|   plug Plug.Session, @session_options | ||||
|   plug AuthWeb.Router | ||||
| end | ||||
							
								
								
									
										24
									
								
								apps/auth_web/lib/auth_web/gettext.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								apps/auth_web/lib/auth_web/gettext.ex
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,24 @@ | |||
| defmodule AuthWeb.Gettext do | ||||
|   @moduledoc """ | ||||
|   A module providing Internationalization with a gettext-based API. | ||||
| 
 | ||||
|   By using [Gettext](https://hexdocs.pm/gettext), | ||||
|   your module gains a set of macros for translations, for example: | ||||
| 
 | ||||
|       import AuthWeb.Gettext | ||||
| 
 | ||||
|       # Simple translation | ||||
|       gettext("Here is the string to translate") | ||||
| 
 | ||||
|       # Plural translation | ||||
|       ngettext("Here is the string to translate", | ||||
|                "Here are the strings to translate", | ||||
|                3) | ||||
| 
 | ||||
|       # Domain-based translation | ||||
|       dgettext("errors", "Here is the error message to translate") | ||||
| 
 | ||||
|   See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. | ||||
|   """ | ||||
|   use Gettext, otp_app: :auth_web | ||||
| end | ||||
							
								
								
									
										26
									
								
								apps/auth_web/lib/auth_web/pow/mailer.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								apps/auth_web/lib/auth_web/pow/mailer.ex
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,26 @@ | |||
| defmodule AuthWeb.Pow.Mailer do | ||||
|   @moduledoc """ | ||||
|   Mailer module for Pow which links it to our well-styled defaults. | ||||
|   """ | ||||
|   use Pow.Phoenix.Mailer | ||||
| 
 | ||||
|   import Bamboo.Email | ||||
|   use Bamboo.Phoenix, view: AuthWeb.EmailView | ||||
| 
 | ||||
|   @impl true | ||||
|   def cast(%{user: user, subject: subject, text: text, html: html}) do | ||||
|     CoreEmail.base_email() | ||||
|     |> to(user.email) | ||||
|     |> subject(subject) | ||||
|     |> render(:pow_mail, html_body: html, text_body: text) | ||||
|   end | ||||
| 
 | ||||
|   @impl true | ||||
|   def process(email) do | ||||
|     # An asynchronous process should be used here to prevent enumeration | ||||
|     # attacks. Synchronous e-mail delivery can reveal whether a user already | ||||
|     # exists in the system or not. | ||||
| 
 | ||||
|     CoreMailer.deliver_later(email) | ||||
|   end | ||||
| end | ||||
							
								
								
									
										25
									
								
								apps/auth_web/lib/auth_web/router.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								apps/auth_web/lib/auth_web/router.ex
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,25 @@ | |||
| defmodule AuthWeb.Router do | ||||
|   use AuthWeb, :router | ||||
|   use Pow.Phoenix.Router | ||||
|   use Pow.Extension.Phoenix.Router, | ||||
|     extensions: [PowResetPassword, PowEmailConfirmation] | ||||
| 
 | ||||
|   pipeline :browser do | ||||
|     plug :accepts, ["html"] | ||||
|     plug :fetch_session | ||||
|     plug :fetch_flash | ||||
|     plug :protect_from_forgery | ||||
|     plug :put_secure_browser_headers | ||||
|   end | ||||
| 
 | ||||
|   pipeline :api do | ||||
|     plug :accepts, ["json"] | ||||
|   end | ||||
| 
 | ||||
|   scope "/" do | ||||
|     pipe_through :browser | ||||
| 
 | ||||
|     pow_routes() | ||||
|     pow_extension_routes() | ||||
|   end | ||||
| end | ||||
							
								
								
									
										58
									
								
								apps/auth_web/lib/auth_web/telemetry.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								apps/auth_web/lib/auth_web/telemetry.ex
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,58 @@ | |||
| defmodule AuthWeb.Telemetry do | ||||
|   @moduledoc """ | ||||
|   Telemetry configuration for AuthWeb app. | ||||
|   """ | ||||
|   use Supervisor | ||||
|   import Telemetry.Metrics | ||||
| 
 | ||||
|   def start_link(arg) do | ||||
|     Supervisor.start_link(__MODULE__, arg, name: __MODULE__) | ||||
|   end | ||||
| 
 | ||||
|   @impl true | ||||
|   def init(_arg) do | ||||
|     children = [ | ||||
|       # Telemetry poller will execute the given period measurements | ||||
|       # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics | ||||
|       {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} | ||||
|       # Add reporters as children of your supervision tree. | ||||
|       # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} | ||||
|     ] | ||||
| 
 | ||||
|     Supervisor.init(children, strategy: :one_for_one) | ||||
|   end | ||||
| 
 | ||||
|   def metrics do | ||||
|     [ | ||||
|       # Phoenix Metrics | ||||
|       summary("phoenix.endpoint.stop.duration", | ||||
|         unit: {:native, :millisecond} | ||||
|       ), | ||||
|       summary("phoenix.router_dispatch.stop.duration", | ||||
|         tags: [:route], | ||||
|         unit: {:native, :millisecond} | ||||
|       ), | ||||
| 
 | ||||
|       # Database Metrics | ||||
|       summary("auth_web.repo.query.total_time", unit: {:native, :millisecond}), | ||||
|       summary("auth_web.repo.query.decode_time", unit: {:native, :millisecond}), | ||||
|       summary("auth_web.repo.query.query_time", unit: {:native, :millisecond}), | ||||
|       summary("auth_web.repo.query.queue_time", unit: {:native, :millisecond}), | ||||
|       summary("auth_web.repo.query.idle_time", unit: {:native, :millisecond}), | ||||
| 
 | ||||
|       # VM Metrics | ||||
|       summary("vm.memory.total", unit: {:byte, :kilobyte}), | ||||
|       summary("vm.total_run_queue_lengths.total"), | ||||
|       summary("vm.total_run_queue_lengths.cpu"), | ||||
|       summary("vm.total_run_queue_lengths.io") | ||||
|     ] | ||||
|   end | ||||
| 
 | ||||
|   defp periodic_measurements do | ||||
|     [ | ||||
|       # A module, function and arguments to be invoked periodically. | ||||
|       # This function must call :telemetry.execute/3 and a metric must be added above. | ||||
|       # {AuthWeb, :count_users, []} | ||||
|     ] | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1 @@ | |||
| <%= @html_body |> raw %> | ||||
|  | @ -0,0 +1 @@ | |||
| <%= @text_body %> | ||||
							
								
								
									
										30
									
								
								apps/auth_web/lib/auth_web/templates/layout/app.html.eex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								apps/auth_web/lib/auth_web/templates/layout/app.html.eex
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | |||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
|   <head> | ||||
|     <meta charset="utf-8"/> | ||||
|     <meta http-equiv="X-UA-Compatible" content="IE=edge"/> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | ||||
|     <title>Core · Phoenix Framework</title> | ||||
|     <link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/> | ||||
|     <script defer type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script> | ||||
|   </head> | ||||
|   <body> | ||||
|     <style type="text/css"> | ||||
|       body { | ||||
|         background-color: #DADADA; | ||||
|       } | ||||
|       main > .grid { | ||||
|         height: 100vh; | ||||
|       } | ||||
|       .image { | ||||
|         margin-top: -100px; | ||||
|       } | ||||
|       .column { | ||||
|         max-width: 450px; | ||||
|       } | ||||
|     </style> | ||||
|     <main role="main" class="container"> | ||||
|       <%= @inner_content %> | ||||
|     </main> | ||||
|   </body> | ||||
| </html> | ||||
							
								
								
									
										38
									
								
								apps/auth_web/lib/auth_web/templates/page/index.html.eex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								apps/auth_web/lib/auth_web/templates/page/index.html.eex
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,38 @@ | |||
| <section class="phx-hero"> | ||||
|   <h1><%= gettext "Welcome to %{name}!", name: "Phoenix" %></h1> | ||||
|   <p>Peace-of-mind from prototype to production</p> | ||||
| </section> | ||||
| 
 | ||||
| <section class="row"> | ||||
|   <article class="column"> | ||||
|     <h2>Resources</h2> | ||||
|     <ul> | ||||
|       <li> | ||||
|         <a href="https://hexdocs.pm/phoenix/overview.html">Guides & Docs</a> | ||||
|       </li> | ||||
|       <li> | ||||
|         <a href="https://github.com/phoenixframework/phoenix">Source</a> | ||||
|       </li> | ||||
|       <li> | ||||
|         <a href="https://github.com/phoenixframework/phoenix/blob/v1.5/CHANGELOG.md">v1.5 Changelog</a> | ||||
|       </li> | ||||
|     </ul> | ||||
|   </article> | ||||
|   <article class="column"> | ||||
|     <h2>Help</h2> | ||||
|     <ul> | ||||
|       <li> | ||||
|         <a href="https://elixirforum.com/c/phoenix-forum">Forum</a> | ||||
|       </li> | ||||
|       <li> | ||||
|         <a href="https://webchat.freenode.net/?channels=elixir-lang">#elixir-lang on Freenode IRC</a> | ||||
|       </li> | ||||
|       <li> | ||||
|         <a href="https://twitter.com/elixirphoenix">Twitter @elixirphoenix</a> | ||||
|       </li> | ||||
|       <li> | ||||
|         <a href="https://elixir-slackin.herokuapp.com/">Elixir on Slack</a> | ||||
|       </li> | ||||
|     </ul> | ||||
|   </article> | ||||
| </section> | ||||
|  | @ -0,0 +1,24 @@ | |||
| <div class="ui middle aligned center aligned grid"> | ||||
|   <div class="column"> | ||||
|     <h2 class="ui teal image header"> | ||||
|       <div class="content"> | ||||
|         Update My Account | ||||
|       </div> | ||||
|     </h2> | ||||
|     <%= flash_block(@conn) %> | ||||
|     <%= changeset_error_block(@changeset) %> | ||||
|     <%= form_for @changeset, @action, [as: :user, class: "ui large form"], fn f -> %> | ||||
|       <div class="ui stacked left aligned segment"> | ||||
|         <%= styled_input f, :current_password, icon: "lock", type: "password" %> | ||||
| 
 | ||||
|         <%= styled_input f, Pow.Ecto.Schema.user_id_field(@changeset), icon: "user" %> | ||||
| 
 | ||||
|         <%= styled_input f, :password, icon: "lock", label: "New Password", type: "password", class: "right labeled" do %> | ||||
|           <div class="ui label js-passwordRevealer"><i class="eye icon" style="margin: 0;"></i></div> | ||||
|         <% end %> | ||||
| 
 | ||||
|         <%= submit "Create My Account", class: "ui fluid large teal submit button" %> | ||||
|       </div> | ||||
|     <% end %> | ||||
|   </div> | ||||
| </div> | ||||
|  | @ -0,0 +1,37 @@ | |||
| <div class="ui middle aligned center aligned grid"> | ||||
|   <div class="column"> | ||||
|     <h2 class="ui teal image header"> | ||||
|       <div class="content"> | ||||
|         Create My Account | ||||
|       </div> | ||||
|     </h2> | ||||
|     <%= flash_block(@conn) %> | ||||
|     <%= changeset_error_block(@changeset) %> | ||||
|     <%= form_for @changeset, @action, [as: :user, class: "ui large form"], fn f -> %> | ||||
|       <div class="ui stacked left aligned segment"> | ||||
|         <div class="field <%= error_class(f, :password) %>"> | ||||
|           <%= label f, Pow.Ecto.Schema.user_id_field(@changeset) %> | ||||
|           <div class="ui left icon input"> | ||||
|             <i class="user icon"></i> | ||||
|             <%= text_input f, Pow.Ecto.Schema.user_id_field(@changeset), placeholder: "E-mail address"  %> | ||||
|           </div> | ||||
|           <%= error_tag f, Pow.Ecto.Schema.user_id_field(@changeset), class: "ui pointing red basic label" %> | ||||
|         </div> | ||||
|         <div class="field <%= error_class(f, :password) %>"> | ||||
|           <%= label f, :password %> | ||||
|           <div class="ui left icon right labeled input"> | ||||
|             <i class="lock icon"></i> | ||||
|             <%= password_input f, :password, placeholder: "Password" %> | ||||
|             <div class="ui label js-passwordRevealer"><i class="eye icon" style="margin: 0;"></i></div> | ||||
|           </div> | ||||
|           <%= error_tag f, :password, class: "ui pointing red basic label" %> | ||||
|         </div> | ||||
|         <%= submit "Create My Account", class: "ui fluid large teal submit button" %> | ||||
|       </div> | ||||
|     <% end %> | ||||
| 
 | ||||
|     <div class="ui message"> | ||||
|       Already have an account? <span><%= link "Sign in", to: Routes.pow_session_path(@conn, :new) %></span> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
|  | @ -0,0 +1,40 @@ | |||
| <div class="ui middle aligned center aligned grid"> | ||||
|   <div class="column"> | ||||
|     <h2 class="ui teal image header"> | ||||
|       <div class="content"> | ||||
|         Log-in To My Account | ||||
|       </div> | ||||
|     </h2> | ||||
|     <%= flash_block(@conn) %> | ||||
|     <%= changeset_error_block(@changeset) %> | ||||
|     <%= form_for @changeset, @action, [as: :user, class: "ui large form"], fn f -> %> | ||||
|       <div class="ui stacked segment"> | ||||
|         <div class="field <%= error_class(f, :password) %>"> | ||||
|           <div class="ui left icon input"> | ||||
|             <i class="user icon"></i> | ||||
|             <%= text_input f, Pow.Ecto.Schema.user_id_field(@changeset), placeholder: "E-mail address"  %> | ||||
|           </div> | ||||
|           <%= error_tag f, Pow.Ecto.Schema.user_id_field(@changeset), class: "ui error message" %> | ||||
|         </div> | ||||
|         <div class="field <%= error_class(f, :password) %>"> | ||||
|           <div class="ui left icon input"> | ||||
|             <i class="lock icon"></i> | ||||
|             <%= password_input f, :password, placeholder: "Password" %> | ||||
|           </div> | ||||
|           <%= error_tag f, :password, class: "ui error message" %> | ||||
|         </div> | ||||
|         <%= submit "Login", class: "ui fluid large teal submit button" %> | ||||
|       </div> | ||||
|     <% end %> | ||||
| 
 | ||||
|     <div class="ui message"> | ||||
|       New to us? <span><%= link "Register", to: Routes.pow_registration_path(@conn, :new) %></span> | ||||
|     </div> | ||||
| 
 | ||||
|     <%= if pow_extension_enabled?(PowResetPassword) do %> | ||||
|       <div class="ui message"> | ||||
|         Forgot your password? <span><%= link("Reset password", to: Routes.pow_reset_password_reset_password_path(@conn, :new)) %></span> | ||||
|       </div> | ||||
|     <% end %> | ||||
|   </div> | ||||
| </div> | ||||
|  | @ -0,0 +1,19 @@ | |||
| <%= preview do %> | ||||
|   Please confirm your email address. | ||||
| <% end %> | ||||
| 
 | ||||
| <%= row do %> | ||||
|   <%= col 1, of: 1 do %> | ||||
|     <%= h1 do %> | ||||
|       Hello there, | ||||
|     <% end %> | ||||
| 
 | ||||
|     <%= p do %> | ||||
|       Please use the following link to confirm your e-mail address: | ||||
|     <% end %> | ||||
| 
 | ||||
|     <%= CoreWeb.EmailHelpers.button href: @url do %> | ||||
|       Confirm my email address | ||||
|     <% end %> | ||||
|   <% end %> | ||||
| <% end %> | ||||
|  | @ -0,0 +1,5 @@ | |||
| Hello there, | ||||
| 
 | ||||
| Please use the following link to confirm your e-mail address: | ||||
| 
 | ||||
| <%= @url %> | ||||
|  | @ -0,0 +1,23 @@ | |||
| <%= preview do %> | ||||
|   Password reset instructions. | ||||
| <% end %> | ||||
| 
 | ||||
| <%= row do %> | ||||
|   <%= col 1, of: 1 do %> | ||||
|     <%= h1 do %> | ||||
|       Hello there, | ||||
|     <% end %> | ||||
| 
 | ||||
|     <%= p do %> | ||||
|       Please use the following link to reset your password: | ||||
|     <% end %> | ||||
| 
 | ||||
|     <%= CoreWeb.EmailHelpers.button href: @url do %> | ||||
|       Reset my password | ||||
|     <% end %> | ||||
| 
 | ||||
|     <%= p do %> | ||||
|       You can disregard this email if you didn't request a password reset. | ||||
|     <% end %> | ||||
|   <% end %> | ||||
| <% end %> | ||||
|  | @ -0,0 +1,7 @@ | |||
| Hello there, | ||||
| 
 | ||||
| Please use the following link to reset your password: | ||||
| 
 | ||||
| <%= @url %> | ||||
| 
 | ||||
| You can disregard this email if you didn't request a password reset. | ||||
|  | @ -0,0 +1,19 @@ | |||
| <div class="ui middle aligned center aligned grid"> | ||||
|   <div class="column"> | ||||
|     <h2 class="ui teal image header"> | ||||
|       <div class="content"> | ||||
|         Reset Password | ||||
|       </div> | ||||
|     </h2> | ||||
|     <%= flash_block(@conn) %> | ||||
|     <%= changeset_error_block(@changeset) %> | ||||
|     <%= form_for @changeset, @action, [as: :user, class: "ui large form"], fn f -> %> | ||||
|       <div class="ui stacked left aligned segment"> | ||||
|         <%= styled_input f, :password, [icon: "lock", type: "password", class: "right labeled"] do %> | ||||
|           <div class="ui label js-passwordRevealer"><i class="eye icon" style="margin: 0;"></i></div> | ||||
|         <% end %> | ||||
|       </div> | ||||
|       <%= submit "Reset My Password", class: "ui fluid large teal submit button" %> | ||||
|     <% end %> | ||||
|   </div> | ||||
| </div> | ||||
|  | @ -0,0 +1,25 @@ | |||
| <div class="ui middle aligned center aligned grid"> | ||||
|   <div class="column"> | ||||
|     <h2 class="ui teal image header"> | ||||
|       <div class="content"> | ||||
|         Reset Password | ||||
|       </div> | ||||
|     </h2> | ||||
|     <%= flash_block(@conn) %> | ||||
|     <%= changeset_error_block(@changeset) %> | ||||
|     <%= form_for @changeset, @action, [as: :user, class: "ui large form"], fn f -> %> | ||||
|       <div class="ui stacked left aligned segment"> | ||||
|         <%= styled_input f, :email, [placeholder: "joe@example.org", icon: "user"] %> | ||||
|       </div> | ||||
|       <%= submit "Reset My Password", class: "ui fluid large teal submit button" %> | ||||
|     <% end %> | ||||
| 
 | ||||
|     <div class="ui message"> | ||||
|       Remembered your password? <span><%= link "Sign in", to: Routes.pow_session_path(@conn, :new) %></span> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="ui message"> | ||||
|       New to us? <span><%= link "Register", to: Routes.pow_registration_path(@conn, :new) %></span> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
							
								
								
									
										57
									
								
								apps/auth_web/lib/auth_web/views/error_helpers.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								apps/auth_web/lib/auth_web/views/error_helpers.ex
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,57 @@ | |||
| defmodule AuthWeb.ErrorHelpers do | ||||
|   @moduledoc """ | ||||
|   Conveniences for translating and building error messages. | ||||
|   """ | ||||
| 
 | ||||
|   use Phoenix.HTML | ||||
| 
 | ||||
|   def error_class(form, field) do | ||||
|     if Keyword.get_values(form.errors, field) |> Enum.any?() do | ||||
|       "error" | ||||
|     else | ||||
|       "" | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   @doc """ | ||||
|   Generates tag for inlined form input errors. | ||||
|   """ | ||||
|   def error_tag(form, field, opts \\ []) do | ||||
|     {extra_classes, _rest_opts} = Keyword.pop(opts, :class, "") | ||||
| 
 | ||||
|     Enum.map(Keyword.get_values(form.errors, field), fn error -> | ||||
|       content_tag(:span, translate_error(error), | ||||
|         class: "invalid-feedback #{extra_classes}", | ||||
|         phx_feedback_for: input_id(form, field) | ||||
|       ) | ||||
|     end) | ||||
|   end | ||||
| 
 | ||||
|   @doc """ | ||||
|   Translates an error message using gettext. | ||||
|   """ | ||||
|   def translate_error({msg, opts}) do | ||||
|     # When using gettext, we typically pass the strings we want | ||||
|     # to translate as a static argument: | ||||
|     # | ||||
|     #     # Translate "is invalid" in the "errors" domain | ||||
|     #     dgettext("errors", "is invalid") | ||||
|     # | ||||
|     #     # Translate the number of files with plural rules | ||||
|     #     dngettext("errors", "1 file", "%{count} files", count) | ||||
|     # | ||||
|     # Because the error messages we show in our forms and APIs | ||||
|     # are defined inside Ecto, we need to translate them dynamically. | ||||
|     # This requires us to call the Gettext module passing our gettext | ||||
|     # backend as first argument. | ||||
|     # | ||||
|     # Note we use the "errors" domain, which means translations | ||||
|     # should be written to the errors.po file. The :count option is | ||||
|     # set by Ecto and indicates we should also apply plural rules. | ||||
|     if count = opts[:count] do | ||||
|       Gettext.dngettext(AuthWeb.Gettext, "errors", msg, msg, count, opts) | ||||
|     else | ||||
|       Gettext.dgettext(AuthWeb.Gettext, "errors", msg, opts) | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										16
									
								
								apps/auth_web/lib/auth_web/views/error_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								apps/auth_web/lib/auth_web/views/error_view.ex
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | |||
| defmodule AuthWeb.ErrorView do | ||||
|   use AuthWeb, :view | ||||
| 
 | ||||
|   # If you want to customize a particular status code | ||||
|   # for a certain format, you may uncomment below. | ||||
|   # def render("500.html", _assigns) do | ||||
|   #   "Internal Server Error" | ||||
|   # end | ||||
| 
 | ||||
|   # By default, Phoenix returns the status message from | ||||
|   # the template name. For example, "404.html" becomes | ||||
|   # "Not Found". | ||||
|   def template_not_found(template, _assigns) do | ||||
|     Phoenix.Controller.status_message_from_template(template) | ||||
|   end | ||||
| end | ||||
							
								
								
									
										63
									
								
								apps/auth_web/lib/auth_web/views/helpers.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								apps/auth_web/lib/auth_web/views/helpers.ex
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,63 @@ | |||
| defmodule AuthWeb.Helpers do | ||||
|   @moduledoc """ | ||||
|   HTML helpers for our styled (Fomantic UI) forms. | ||||
|   """ | ||||
|   use Phoenix.HTML | ||||
|   import AuthWeb.ErrorHelpers | ||||
| 
 | ||||
|   import Phoenix.Controller, only: [get_flash: 2] | ||||
| 
 | ||||
|   def changeset_error_block(changeset) do | ||||
|     ~E""" | ||||
|     <%= if changeset.action do %> | ||||
|       <div class="ui negative message"> | ||||
|         <p>Oops, something went wrong! Please check the errors below.</p> | ||||
|       </div> | ||||
|     <% end %> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   def flash_block(conn) do | ||||
|     ~E""" | ||||
|     <%= [info: "info", error: "negative"] |> Enum.map(fn {level, class} ->  %> | ||||
|       <%= if get_flash(conn, level) do %> | ||||
|         <p class="ui message <%= class %>" role="alert"><%= get_flash(conn, level) %></p> | ||||
|       <% end %> | ||||
|     <% end) %> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   def styled_input(f, field, opts \\ []) do | ||||
|     styled_input(f, field, opts) do | ||||
|       "" | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def styled_input(f, field, opts, do: content) do | ||||
|     {icon, rest_opts} = Keyword.pop(opts, :icon, "") | ||||
|     {classes, rest_opts} = Keyword.pop(rest_opts, :class, "") | ||||
|     {label_text, rest_opts} = Keyword.pop(rest_opts, :label) | ||||
|     ~E""" | ||||
|     <div class="field <%= error_class(f, field) %>"> | ||||
|       <%= if label_text do %> | ||||
|         <%= label f, field, label_text %> | ||||
|       <% else %> | ||||
|         <%= label f, field %> | ||||
|       <% end %> | ||||
| 
 | ||||
|       <div class="ui left icon <%= classes %> input"> | ||||
|         <i class="<%= icon %> icon"></i> | ||||
|         <%= text_input f, field, rest_opts %> | ||||
|         <%= content %> | ||||
|       </div> | ||||
|       <%= error_tag f, field, class: "ui pointing red basic label" %> | ||||
|     </div> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   def pow_extension_enabled?(extension) do | ||||
|     {extensions, _rest} = Application.get_env(:auth_web, :pow) |> Keyword.pop(:extensions, []) | ||||
| 
 | ||||
|     Enum.any?(extensions, & &1 == PowResetPassword) | ||||
|   end | ||||
| end | ||||
							
								
								
									
										3
									
								
								apps/auth_web/lib/auth_web/views/layout_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								apps/auth_web/lib/auth_web/views/layout_view.ex
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| defmodule AuthWeb.LayoutView do | ||||
|   use AuthWeb, :view | ||||
| end | ||||
							
								
								
									
										3
									
								
								apps/auth_web/lib/auth_web/views/page_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								apps/auth_web/lib/auth_web/views/page_view.ex
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| defmodule AuthWeb.PageView do | ||||
|   use AuthWeb, :view | ||||
| end | ||||
							
								
								
									
										9
									
								
								apps/auth_web/lib/auth_web/views/pow/email_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								apps/auth_web/lib/auth_web/views/pow/email_view.ex
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | |||
| defmodule AuthWeb.EmailView do | ||||
|   use Phoenix.View, | ||||
|     root: "lib/auth_web/templates", | ||||
|     namespace: AuthWeb, | ||||
|     pattern: "**/*" | ||||
| 
 | ||||
|   import CoreWeb.EmailHelpers | ||||
|   import Phoenix.HTML, only: [raw: 1] | ||||
| end | ||||
|  | @ -0,0 +1,3 @@ | |||
| defmodule AuthWeb.Pow.RegistrationView do | ||||
|   use AuthWeb, :view | ||||
| end | ||||
							
								
								
									
										3
									
								
								apps/auth_web/lib/auth_web/views/pow/session_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								apps/auth_web/lib/auth_web/views/pow/session_view.ex
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| defmodule AuthWeb.Pow.SessionView do | ||||
|   use AuthWeb, :view | ||||
| end | ||||
|  | @ -0,0 +1,5 @@ | |||
| defmodule AuthWeb.PowEmailConfirmation.MailerView do | ||||
|   use AuthWeb, :mailer_view | ||||
| 
 | ||||
|   def subject(:email_confirmation, _assigns), do: "Confirm your email address" | ||||
| end | ||||
|  | @ -0,0 +1,5 @@ | |||
| defmodule AuthWeb.PowResetPassword.MailerView do | ||||
|   use AuthWeb, :mailer_view | ||||
| 
 | ||||
|   def subject(:reset_password, _assigns), do: "Reset password link" | ||||
| end | ||||
|  | @ -0,0 +1,5 @@ | |||
| defmodule AuthWeb.PowResetPassword.ResetPasswordView do | ||||
|   use AuthWeb, :view | ||||
| 
 | ||||
|   import AuthWeb.Helpers | ||||
| end | ||||
							
								
								
									
										66
									
								
								apps/auth_web/mix.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								apps/auth_web/mix.exs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,66 @@ | |||
| defmodule AuthWeb.MixProject do | ||||
|   use Mix.Project | ||||
| 
 | ||||
|   def project do | ||||
|     [ | ||||
|       app: :auth_web, | ||||
|       version: "0.1.0", | ||||
|       build_path: "../../_build", | ||||
|       config_path: "../../config/config.exs", | ||||
|       deps_path: "../../deps", | ||||
|       lockfile: "../../mix.lock", | ||||
|       elixir: "~> 1.7", | ||||
|       elixirc_paths: elixirc_paths(Mix.env()), | ||||
|       compilers: [:phoenix, :gettext] ++ Mix.compilers(), | ||||
|       start_permanent: Mix.env() == :prod, | ||||
|       aliases: aliases(), | ||||
|       deps: deps() | ||||
|     ] | ||||
|   end | ||||
| 
 | ||||
|   # Configuration for the OTP application. | ||||
|   # | ||||
|   # Type `mix help compile.app` for more information. | ||||
|   def application do | ||||
|     [ | ||||
|       mod: {AuthWeb.Application, []}, | ||||
|       extra_applications: [:logger, :runtime_tools] | ||||
|     ] | ||||
|   end | ||||
| 
 | ||||
|   # Specifies which paths to compile per environment. | ||||
|   defp elixirc_paths(:test), do: ["lib", "test/support"] | ||||
|   defp elixirc_paths(_), do: ["lib"] | ||||
| 
 | ||||
|   # Specifies your project dependencies. | ||||
|   # | ||||
|   # Type `mix help deps` for examples and options. | ||||
|   defp deps do | ||||
|     [ | ||||
|       {:auth, in_umbrella: true}, | ||||
|       {:core, in_umbrella: true}, | ||||
|       {:phoenix, "~> 1.5.3"}, | ||||
|       {:phoenix_ecto, "~> 4.0"}, | ||||
|       {:phoenix_html, "~> 2.11"}, | ||||
|       {:phoenix_live_reload, "~> 1.2", only: :dev}, | ||||
|       {:phoenix_live_dashboard, "~> 0.2.0"}, | ||||
|       {:pow, "~> 1.0.20"}, | ||||
|       {:telemetry_metrics, "~> 0.4"}, | ||||
|       {:telemetry_poller, "~> 0.4"}, | ||||
|       {:gettext, "~> 0.11"}, | ||||
|       {:jason, "~> 1.0"}, | ||||
|       {:plug_cowboy, "~> 2.0"} | ||||
|     ] | ||||
|   end | ||||
| 
 | ||||
|   # Aliases are shortcuts or tasks specific to the current project. | ||||
|   # | ||||
|   # See the documentation for `Mix` for more info on aliases. | ||||
|   defp aliases do | ||||
|     [ | ||||
|       setup: ["deps.get", "cmd npm install --prefix assets"], | ||||
|       test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"], | ||||
|       "ecto.migrate": [], | ||||
|     ] | ||||
|   end | ||||
| end | ||||
							
								
								
									
										97
									
								
								apps/auth_web/priv/gettext/en/LC_MESSAGES/errors.po
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								apps/auth_web/priv/gettext/en/LC_MESSAGES/errors.po
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,97 @@ | |||
| ## `msgid`s in this file come from POT (.pot) files. | ||||
| ## | ||||
| ## Do not add, change, or remove `msgid`s manually here as | ||||
| ## they're tied to the ones in the corresponding POT file | ||||
| ## (with the same domain). | ||||
| ## | ||||
| ## Use `mix gettext.extract --merge` or `mix gettext.merge` | ||||
| ## to merge POT files into PO files. | ||||
| msgid "" | ||||
| msgstr "" | ||||
| "Language: en\n" | ||||
| 
 | ||||
| ## From Ecto.Changeset.cast/4 | ||||
| msgid "can't be blank" | ||||
| msgstr "" | ||||
| 
 | ||||
| ## From Ecto.Changeset.unique_constraint/3 | ||||
| msgid "has already been taken" | ||||
| msgstr "" | ||||
| 
 | ||||
| ## From Ecto.Changeset.put_change/3 | ||||
| msgid "is invalid" | ||||
| msgstr "" | ||||
| 
 | ||||
| ## From Ecto.Changeset.validate_acceptance/3 | ||||
| msgid "must be accepted" | ||||
| msgstr "" | ||||
| 
 | ||||
| ## From Ecto.Changeset.validate_format/3 | ||||
| msgid "has invalid format" | ||||
| msgstr "" | ||||
| 
 | ||||
| ## From Ecto.Changeset.validate_subset/3 | ||||
| msgid "has an invalid entry" | ||||
| msgstr "" | ||||
| 
 | ||||
| ## From Ecto.Changeset.validate_exclusion/3 | ||||
| msgid "is reserved" | ||||
| msgstr "" | ||||
| 
 | ||||
| ## From Ecto.Changeset.validate_confirmation/3 | ||||
| msgid "does not match confirmation" | ||||
| msgstr "" | ||||
| 
 | ||||
| ## From Ecto.Changeset.no_assoc_constraint/3 | ||||
| msgid "is still associated with this entry" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "are still associated with this entry" | ||||
| msgstr "" | ||||
| 
 | ||||
| ## From Ecto.Changeset.validate_length/3 | ||||
| msgid "should be %{count} character(s)" | ||||
| msgid_plural "should be %{count} character(s)" | ||||
| msgstr[0] "" | ||||
| msgstr[1] "" | ||||
| 
 | ||||
| msgid "should have %{count} item(s)" | ||||
| msgid_plural "should have %{count} item(s)" | ||||
| msgstr[0] "" | ||||
| msgstr[1] "" | ||||
| 
 | ||||
| msgid "should be at least %{count} character(s)" | ||||
| msgid_plural "should be at least %{count} character(s)" | ||||
| msgstr[0] "" | ||||
| msgstr[1] "" | ||||
| 
 | ||||
| msgid "should have at least %{count} item(s)" | ||||
| msgid_plural "should have at least %{count} item(s)" | ||||
| msgstr[0] "" | ||||
| msgstr[1] "" | ||||
| 
 | ||||
| msgid "should be at most %{count} character(s)" | ||||
| msgid_plural "should be at most %{count} character(s)" | ||||
| msgstr[0] "" | ||||
| msgstr[1] "" | ||||
| 
 | ||||
| msgid "should have at most %{count} item(s)" | ||||
| msgid_plural "should have at most %{count} item(s)" | ||||
| msgstr[0] "" | ||||
| msgstr[1] "" | ||||
| 
 | ||||
| ## From Ecto.Changeset.validate_number/3 | ||||
| msgid "must be less than %{number}" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "must be greater than %{number}" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "must be less than or equal to %{number}" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "must be greater than or equal to %{number}" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "must be equal to %{number}" | ||||
| msgstr "" | ||||
							
								
								
									
										95
									
								
								apps/auth_web/priv/gettext/errors.pot
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								apps/auth_web/priv/gettext/errors.pot
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,95 @@ | |||
| ## This is a PO Template file. | ||||
| ## | ||||
| ## `msgid`s here are often extracted from source code. | ||||
| ## Add new translations manually only if they're dynamic | ||||
| ## translations that can't be statically extracted. | ||||
| ## | ||||
| ## Run `mix gettext.extract` to bring this file up to | ||||
| ## date. Leave `msgstr`s empty as changing them here has no | ||||
| ## effect: edit them in PO (`.po`) files instead. | ||||
| 
 | ||||
| ## From Ecto.Changeset.cast/4 | ||||
| msgid "can't be blank" | ||||
| msgstr "" | ||||
| 
 | ||||
| ## From Ecto.Changeset.unique_constraint/3 | ||||
| msgid "has already been taken" | ||||
| msgstr "" | ||||
| 
 | ||||
| ## From Ecto.Changeset.put_change/3 | ||||
| msgid "is invalid" | ||||
| msgstr "" | ||||
| 
 | ||||
| ## From Ecto.Changeset.validate_acceptance/3 | ||||
| msgid "must be accepted" | ||||
| msgstr "" | ||||
| 
 | ||||
| ## From Ecto.Changeset.validate_format/3 | ||||
| msgid "has invalid format" | ||||
| msgstr "" | ||||
| 
 | ||||
| ## From Ecto.Changeset.validate_subset/3 | ||||
| msgid "has an invalid entry" | ||||
| msgstr "" | ||||
| 
 | ||||
| ## From Ecto.Changeset.validate_exclusion/3 | ||||
| msgid "is reserved" | ||||
| msgstr "" | ||||
| 
 | ||||
| ## From Ecto.Changeset.validate_confirmation/3 | ||||
| msgid "does not match confirmation" | ||||
| msgstr "" | ||||
| 
 | ||||
| ## From Ecto.Changeset.no_assoc_constraint/3 | ||||
| msgid "is still associated with this entry" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "are still associated with this entry" | ||||
| msgstr "" | ||||
| 
 | ||||
| ## From Ecto.Changeset.validate_length/3 | ||||
| msgid "should be %{count} character(s)" | ||||
| msgid_plural "should be %{count} character(s)" | ||||
| msgstr[0] "" | ||||
| msgstr[1] "" | ||||
| 
 | ||||
| msgid "should have %{count} item(s)" | ||||
| msgid_plural "should have %{count} item(s)" | ||||
| msgstr[0] "" | ||||
| msgstr[1] "" | ||||
| 
 | ||||
| msgid "should be at least %{count} character(s)" | ||||
| msgid_plural "should be at least %{count} character(s)" | ||||
| msgstr[0] "" | ||||
| msgstr[1] "" | ||||
| 
 | ||||
| msgid "should have at least %{count} item(s)" | ||||
| msgid_plural "should have at least %{count} item(s)" | ||||
| msgstr[0] "" | ||||
| msgstr[1] "" | ||||
| 
 | ||||
| msgid "should be at most %{count} character(s)" | ||||
| msgid_plural "should be at most %{count} character(s)" | ||||
| msgstr[0] "" | ||||
| msgstr[1] "" | ||||
| 
 | ||||
| msgid "should have at most %{count} item(s)" | ||||
| msgid_plural "should have at most %{count} item(s)" | ||||
| msgstr[0] "" | ||||
| msgstr[1] "" | ||||
| 
 | ||||
| ## From Ecto.Changeset.validate_number/3 | ||||
| msgid "must be less than %{number}" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "must be greater than %{number}" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "must be less than or equal to %{number}" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "must be greater than or equal to %{number}" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "must be equal to %{number}" | ||||
| msgstr "" | ||||
							
								
								
									
										0
									
								
								apps/auth_web/test/auth_web/controllers/.keep
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								apps/auth_web/test/auth_web/controllers/.keep
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										14
									
								
								apps/auth_web/test/auth_web/views/error_view_test.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								apps/auth_web/test/auth_web/views/error_view_test.exs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,14 @@ | |||
| defmodule AuthWeb.ErrorViewTest do | ||||
|   use AuthWeb.ConnCase, async: true | ||||
| 
 | ||||
|   # Bring render/3 and render_to_string/3 for testing custom views | ||||
|   import Phoenix.View | ||||
| 
 | ||||
|   test "renders 404.html" do | ||||
|     assert render_to_string(AuthWeb.ErrorView, "404.html", []) == "Not Found" | ||||
|   end | ||||
| 
 | ||||
|   test "renders 500.html" do | ||||
|     assert render_to_string(AuthWeb.ErrorView, "500.html", []) == "Internal Server Error" | ||||
|   end | ||||
| end | ||||
							
								
								
									
										8
									
								
								apps/auth_web/test/auth_web/views/layout_view_test.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								apps/auth_web/test/auth_web/views/layout_view_test.exs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,8 @@ | |||
| defmodule AuthWeb.LayoutViewTest do | ||||
|   use AuthWeb.ConnCase, async: true | ||||
| 
 | ||||
|   # When testing helpers, you may want to import Phoenix.HTML and | ||||
|   # use functions such as safe_to_string() to convert the helper | ||||
|   # result into an HTML string. | ||||
|   # import Phoenix.HTML | ||||
| end | ||||
							
								
								
									
										3
									
								
								apps/auth_web/test/auth_web/views/page_view_test.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								apps/auth_web/test/auth_web/views/page_view_test.exs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| defmodule AuthWeb.PageViewTest do | ||||
|   use AuthWeb.ConnCase, async: true | ||||
| end | ||||
							
								
								
									
										40
									
								
								apps/auth_web/test/support/channel_case.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								apps/auth_web/test/support/channel_case.ex
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,40 @@ | |||
| defmodule AuthWeb.ChannelCase do | ||||
|   @moduledoc """ | ||||
|   This module defines the test case to be used by | ||||
|   channel tests. | ||||
| 
 | ||||
|   Such tests rely on `Phoenix.ChannelTest` and also | ||||
|   import other functionality to make it easier | ||||
|   to build common data structures and query the data layer. | ||||
| 
 | ||||
|   Finally, if the test case interacts with the database, | ||||
|   we enable the SQL sandbox, so changes done to the database | ||||
|   are reverted at the end of every test. If you are using | ||||
|   PostgreSQL, you can even run database tests asynchronously | ||||
|   by setting `use AuthWeb.ChannelCase, async: true`, although | ||||
|   this option is not recommended for other databases. | ||||
|   """ | ||||
| 
 | ||||
|   use ExUnit.CaseTemplate | ||||
| 
 | ||||
|   using do | ||||
|     quote do | ||||
|       # Import conveniences for testing with channels | ||||
|       import Phoenix.ChannelTest | ||||
|       import AuthWeb.ChannelCase | ||||
| 
 | ||||
|       # The default endpoint for testing | ||||
|       @endpoint AuthWeb.Endpoint | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   setup tags do | ||||
|     :ok = Ecto.Adapters.SQL.Sandbox.checkout(Auth.Repo) | ||||
| 
 | ||||
|     unless tags[:async] do | ||||
|       Ecto.Adapters.SQL.Sandbox.mode(Auth.Repo, {:shared, self()}) | ||||
|     end | ||||
| 
 | ||||
|     :ok | ||||
|   end | ||||
| end | ||||
							
								
								
									
										43
									
								
								apps/auth_web/test/support/conn_case.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								apps/auth_web/test/support/conn_case.ex
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,43 @@ | |||
| defmodule AuthWeb.ConnCase do | ||||
|   @moduledoc """ | ||||
|   This module defines the test case to be used by | ||||
|   tests that require setting up a connection. | ||||
| 
 | ||||
|   Such tests rely on `Phoenix.ConnTest` and also | ||||
|   import other functionality to make it easier | ||||
|   to build common data structures and query the data layer. | ||||
| 
 | ||||
|   Finally, if the test case interacts with the database, | ||||
|   we enable the SQL sandbox, so changes done to the database | ||||
|   are reverted at the end of every test. If you are using | ||||
|   PostgreSQL, you can even run database tests asynchronously | ||||
|   by setting `use AuthWeb.ConnCase, async: true`, although | ||||
|   this option is not recommended for other databases. | ||||
|   """ | ||||
| 
 | ||||
|   use ExUnit.CaseTemplate | ||||
| 
 | ||||
|   using do | ||||
|     quote do | ||||
|       # Import conveniences for testing with connections | ||||
|       import Plug.Conn | ||||
|       import Phoenix.ConnTest | ||||
|       import AuthWeb.ConnCase | ||||
| 
 | ||||
|       alias AuthWeb.Router.Helpers, as: Routes | ||||
| 
 | ||||
|       # The default endpoint for testing | ||||
|       @endpoint AuthWeb.Endpoint | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   setup tags do | ||||
|     :ok = Ecto.Adapters.SQL.Sandbox.checkout(Auth.Repo) | ||||
| 
 | ||||
|     unless tags[:async] do | ||||
|       Ecto.Adapters.SQL.Sandbox.mode(Auth.Repo, {:shared, self()}) | ||||
|     end | ||||
| 
 | ||||
|     {:ok, conn: Phoenix.ConnTest.build_conn()} | ||||
|   end | ||||
| end | ||||
							
								
								
									
										2
									
								
								apps/auth_web/test/test_helper.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								apps/auth_web/test/test_helper.exs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,2 @@ | |||
| ExUnit.start() | ||||
| Ecto.Adapters.SQL.Sandbox.mode(Auth.Repo, :manual) | ||||
|  | @ -50,5 +50,6 @@ defmodule Content.Endpoint do | |||
|   plug Plug.MethodOverride | ||||
|   plug Plug.Head | ||||
|   plug Plug.Session, @session_options | ||||
|   plug Pow.Plug.Session, otp_app: :content | ||||
|   plug Content.Router | ||||
| end | ||||
|  |  | |||
|  | @ -13,31 +13,9 @@ defmodule Content.Router do | |||
|     plug :accepts, ["json"] | ||||
|   end | ||||
| 
 | ||||
|   # Other scopes may use custom stacks. | ||||
|   # scope "/api", Content do | ||||
|   #   pipe_through :api | ||||
|   # end | ||||
| 
 | ||||
|   # Enables LiveDashboard only for development | ||||
|   # | ||||
|   # If you want to use the LiveDashboard in production, you should put | ||||
|   # it behind authentication and allow only admins to access it. | ||||
|   # If your application does not have an admins-only section yet, | ||||
|   # you can use Plug.BasicAuth to set up some basic authentication | ||||
|   # as long as you are also using SSL (which you should anyway). | ||||
|   if Mix.env() in [:dev, :test] do | ||||
|     import Phoenix.LiveDashboard.Router | ||||
| 
 | ||||
|     scope "/" do | ||||
|       pipe_through :browser | ||||
|       live_dashboard "/dashboard", metrics: Content.Telemetry | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   scope "/", Content do | ||||
|     pipe_through :browser | ||||
| 
 | ||||
|     get "/", PageController, :index | ||||
|     get "/pages/:id", PageController, :show | ||||
|     get "/:id", PageController, :show | ||||
|   end | ||||
| end | ||||
|  |  | |||
							
								
								
									
										23
									
								
								apps/content/lib/content/templates/layout/_menu.html.eex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								apps/content/lib/content/templates/layout/_menu.html.eex
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,23 @@ | |||
| <div class="ui container"> | ||||
|   <div class="ui large secondary inverted pointing menu"> | ||||
|     <a class="toc item"> | ||||
|       <i class="sidebar icon"></i> | ||||
|     </a> | ||||
|     <a class="active item">Home</a> | ||||
|     <a class="item">Work</a> | ||||
|     <a class="item">Company</a> | ||||
|     <a class="item">Careers</a> | ||||
|     <div class="right item"> | ||||
|       <%= if Pow.Plug.current_user(@conn) do %> | ||||
|         <%= link "Sign out", to: AuthWeb.Router.Helpers.pow_session_path(@conn, :delete), method: :delete, class: "ui inverted button" %> | ||||
|       <% else %> | ||||
|         <%= link to: AuthWeb.Router.Helpers.pow_session_path(@conn, :new), class: "ui inverted button" do %> | ||||
|           Log in | ||||
|         <% end %> | ||||
|         <%= link to: AuthWeb.Router.Helpers.pow_registration_path(@conn, :new), class: "ui inverted button" do %> | ||||
|           Sign Up | ||||
|         <% end %> | ||||
|       <% end %> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
|  | @ -16,7 +16,112 @@ | |||
|         <% end %> | ||||
|       <% end %> | ||||
| 
 | ||||
|       <style type="text/css"> | ||||
|         .hidden.menu { | ||||
|           display: none; | ||||
|         } | ||||
| 
 | ||||
|         .masthead.segment { | ||||
|           min-height: 700px; | ||||
|           padding: 1em 0em; | ||||
|         } | ||||
|         .masthead .logo.item img { | ||||
|           margin-right: 1em; | ||||
|         } | ||||
|         .masthead .ui.menu .ui.button { | ||||
|           margin-left: 0.5em; | ||||
|         } | ||||
|         .masthead h1.ui.header { | ||||
|           margin-top: 3em; | ||||
|           margin-bottom: 0em; | ||||
|           font-size: 4em; | ||||
|           font-weight: normal; | ||||
|         } | ||||
|         .masthead h2 { | ||||
|           font-size: 1.7em; | ||||
|           font-weight: normal; | ||||
|         } | ||||
| 
 | ||||
|         .ui.vertical.stripe { | ||||
|           padding: 8em 0em; | ||||
|         } | ||||
|         .ui.vertical.stripe h3 { | ||||
|           font-size: 2em; | ||||
|         } | ||||
|         .ui.vertical.stripe .button + h3, | ||||
|         .ui.vertical.stripe p + h3 { | ||||
|           margin-top: 3em; | ||||
|         } | ||||
|         .ui.vertical.stripe .floated.image { | ||||
|           clear: both; | ||||
|         } | ||||
|         .ui.vertical.stripe p { | ||||
|           font-size: 1.33em; | ||||
|         } | ||||
|         .ui.vertical.stripe .horizontal.divider { | ||||
|           margin: 3em 0em; | ||||
|         } | ||||
| 
 | ||||
|         .quote.stripe.segment { | ||||
|           padding: 0em; | ||||
|         } | ||||
|         .quote.stripe.segment .grid .column { | ||||
|           padding-top: 5em; | ||||
|           padding-bottom: 5em; | ||||
|         } | ||||
| 
 | ||||
|         .footer.segment { | ||||
|           padding: 5em 0em; | ||||
|         } | ||||
| 
 | ||||
|         .secondary.pointing.menu .toc.item { | ||||
|           display: none; | ||||
|         } | ||||
| 
 | ||||
|         @media only screen and (max-width: 700px) { | ||||
|           .ui.fixed.menu { | ||||
|             display: none !important; | ||||
|           } | ||||
|           .secondary.pointing.menu .item, | ||||
|           .secondary.pointing.menu .menu { | ||||
|             display: none; | ||||
|           } | ||||
|           .secondary.pointing.menu .toc.item { | ||||
|             display: block; | ||||
|           } | ||||
|           .masthead.segment { | ||||
|             min-height: 350px; | ||||
|           } | ||||
|           .masthead h1.ui.header { | ||||
|             font-size: 2em; | ||||
|             margin-top: 1.5em; | ||||
|           } | ||||
|           .masthead h2 { | ||||
|             margin-top: 0.5em; | ||||
|             font-size: 1.5em; | ||||
|           } | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|       </style> | ||||
| 
 | ||||
|       <!-- Sidebar Menu --> | ||||
|       <div class="ui vertical inverted sidebar menu"> | ||||
|         <a class="active item">Home</a> | ||||
|         <a class="item">Work</a> | ||||
|         <a class="item">Company</a> | ||||
|         <a class="item">Careers</a> | ||||
|         <a class="item">Login</a> | ||||
|         <a class="item">Signup</a> | ||||
|       </div> | ||||
| 
 | ||||
|       <!-- Page Contents --> | ||||
|       <div class="pusher"> | ||||
|         <div class="ui inverted vertical masthead center aligned segment"> | ||||
|           <%= render "_menu.html", assigns %> | ||||
|           <%= @inner_content %> | ||||
|         </div> | ||||
|       </div> | ||||
|     </main> | ||||
|   </body> | ||||
| </html> | ||||
|  |  | |||
|  | @ -1,124 +1,3 @@ | |||
| <style type="text/css"> | ||||
| 
 | ||||
|   .hidden.menu { | ||||
|     display: none; | ||||
|   } | ||||
| 
 | ||||
|   .masthead.segment { | ||||
|     min-height: 700px; | ||||
|     padding: 1em 0em; | ||||
|   } | ||||
|   .masthead .logo.item img { | ||||
|     margin-right: 1em; | ||||
|   } | ||||
|   .masthead .ui.menu .ui.button { | ||||
|     margin-left: 0.5em; | ||||
|   } | ||||
|   .masthead h1.ui.header { | ||||
|     margin-top: 3em; | ||||
|     margin-bottom: 0em; | ||||
|     font-size: 4em; | ||||
|     font-weight: normal; | ||||
|   } | ||||
|   .masthead h2 { | ||||
|     font-size: 1.7em; | ||||
|     font-weight: normal; | ||||
|   } | ||||
| 
 | ||||
|   .ui.vertical.stripe { | ||||
|     padding: 8em 0em; | ||||
|   } | ||||
|   .ui.vertical.stripe h3 { | ||||
|     font-size: 2em; | ||||
|   } | ||||
|   .ui.vertical.stripe .button + h3, | ||||
|   .ui.vertical.stripe p + h3 { | ||||
|     margin-top: 3em; | ||||
|   } | ||||
|   .ui.vertical.stripe .floated.image { | ||||
|     clear: both; | ||||
|   } | ||||
|   .ui.vertical.stripe p { | ||||
|     font-size: 1.33em; | ||||
|   } | ||||
|   .ui.vertical.stripe .horizontal.divider { | ||||
|     margin: 3em 0em; | ||||
|   } | ||||
| 
 | ||||
|   .quote.stripe.segment { | ||||
|     padding: 0em; | ||||
|   } | ||||
|   .quote.stripe.segment .grid .column { | ||||
|     padding-top: 5em; | ||||
|     padding-bottom: 5em; | ||||
|   } | ||||
| 
 | ||||
|   .footer.segment { | ||||
|     padding: 5em 0em; | ||||
|   } | ||||
| 
 | ||||
|   .secondary.pointing.menu .toc.item { | ||||
|     display: none; | ||||
|   } | ||||
| 
 | ||||
|   @media only screen and (max-width: 700px) { | ||||
|     .ui.fixed.menu { | ||||
|       display: none !important; | ||||
|     } | ||||
|     .secondary.pointing.menu .item, | ||||
|     .secondary.pointing.menu .menu { | ||||
|       display: none; | ||||
|     } | ||||
|     .secondary.pointing.menu .toc.item { | ||||
|       display: block; | ||||
|     } | ||||
|     .masthead.segment { | ||||
|       min-height: 350px; | ||||
|     } | ||||
|     .masthead h1.ui.header { | ||||
|       font-size: 2em; | ||||
|       margin-top: 1.5em; | ||||
|     } | ||||
|     .masthead h2 { | ||||
|       margin-top: 0.5em; | ||||
|       font-size: 1.5em; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
| </style> | ||||
| 
 | ||||
| <!-- Sidebar Menu --> | ||||
| <div class="ui vertical inverted sidebar menu"> | ||||
|   <a class="active item">Home</a> | ||||
|   <a class="item">Work</a> | ||||
|   <a class="item">Company</a> | ||||
|   <a class="item">Careers</a> | ||||
|   <a class="item">Login</a> | ||||
|   <a class="item">Signup</a> | ||||
| </div> | ||||
| 
 | ||||
| 
 | ||||
| <!-- Page Contents --> | ||||
| <div class="pusher"> | ||||
|   <div class="ui inverted vertical masthead center aligned segment"> | ||||
| 
 | ||||
|     <div class="ui container"> | ||||
|       <div class="ui large secondary inverted pointing menu"> | ||||
|         <a class="toc item"> | ||||
|           <i class="sidebar icon"></i> | ||||
|         </a> | ||||
|         <a class="active item">Home</a> | ||||
|         <a class="item">Work</a> | ||||
|         <a class="item">Company</a> | ||||
|         <a class="item">Careers</a> | ||||
|         <div class="right item"> | ||||
|           <a class="ui inverted button">Log in</a> | ||||
|           <a class="ui inverted button">Sign Up</a> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
| <div class="ui text container"> | ||||
|   <h1 class="ui inverted header"> | ||||
|     Imagine-a-Company | ||||
|  | @ -210,5 +89,3 @@ | |||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
|   </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -38,6 +38,8 @@ defmodule Content.MixProject do | |||
|   # Type `mix help deps` for examples and options. | ||||
|   defp deps do | ||||
|     [ | ||||
|       {:auth_web, in_umbrella: true}, | ||||
|       {:core, in_umbrella: true}, | ||||
|       {:excoveralls, "~> 0.10", only: [:dev, :test]}, | ||||
|       {:phoenix, "~> 1.5.3"}, | ||||
|       {:phoenix_ecto, "~> 4.0"}, | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ defmodule Content.PageControllerTest do | |||
|   use Content.ConnCase | ||||
| 
 | ||||
|   test "GET /", %{conn: conn} do | ||||
|     conn = get(conn, "/") | ||||
|     conn = get(conn, "/index") | ||||
|     assert html_response(conn, 200) | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -14,3 +14,32 @@ import "../css/app.scss" | |||
| //     import socket from "./socket"
 | ||||
| //
 | ||||
| import "phoenix_html" | ||||
| 
 | ||||
| function ready(fn) { | ||||
|   if (document.readyState != 'loading'){ | ||||
|     fn(); | ||||
|   } else { | ||||
|     document.addEventListener('DOMContentLoaded', fn); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function togglePasswordFieldVisibility() | ||||
| { | ||||
|   const passwordFields = document.querySelectorAll('[name="user[password]"]') | ||||
|   passwordFields.forEach((el) => { | ||||
|     if (el.type == 'password') | ||||
|     { | ||||
|       el.type = 'text' | ||||
|     }  | ||||
|     else | ||||
|     { | ||||
|       el.type = 'password' | ||||
|     } | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| ready(() => { | ||||
|   document.querySelectorAll('.js-passwordRevealer').forEach((el) => { | ||||
|     el.addEventListener('click', togglePasswordFieldVisibility) | ||||
|   }) | ||||
| }) | ||||
|  |  | |||
|  | @ -1,4 +1,8 @@ | |||
| defmodule Core.MapUtils do | ||||
|   @moduledoc """ | ||||
|   Generic additional utility functions for Maps. | ||||
|   """ | ||||
| 
 | ||||
|   def deep_merge(base, override) do | ||||
|     Map.merge(base, override, &deep_value/3) | ||||
|   end | ||||
|  |  | |||
|  | @ -50,5 +50,6 @@ defmodule CoreWeb.Endpoint do | |||
|   plug Plug.MethodOverride | ||||
|   plug Plug.Head | ||||
|   plug Plug.Session, @session_options | ||||
|   plug Pow.Plug.Session, otp_app: :auth_web | ||||
|   plug CoreWeb.Router | ||||
| end | ||||
|  |  | |||
|  | @ -13,22 +13,6 @@ defmodule CoreWeb.Router do | |||
|     plug :accepts, ["json"] | ||||
|   end | ||||
| 
 | ||||
|   scope "/", CoreWeb do | ||||
|     pipe_through :browser | ||||
|   end | ||||
| 
 | ||||
|   # Other scopes may use custom stacks. | ||||
|   # scope "/api", CoreWeb do | ||||
|   #   pipe_through :api | ||||
|   # end | ||||
| 
 | ||||
|   # Enables LiveDashboard only for development | ||||
|   # | ||||
|   # If you want to use the LiveDashboard in production, you should put | ||||
|   # it behind authentication and allow only admins to access it. | ||||
|   # If your application does not have an admins-only section yet, | ||||
|   # you can use Plug.BasicAuth to set up some basic authentication | ||||
|   # as long as you are also using SSL (which you should anyway). | ||||
|   if Mix.env() in [:dev, :test] do | ||||
|     import Phoenix.LiveDashboard.Router | ||||
| 
 | ||||
|  | @ -43,8 +27,14 @@ defmodule CoreWeb.Router do | |||
|     forward "/sent_emails", Bamboo.SentEmailViewerPlug | ||||
|   end | ||||
| 
 | ||||
|   scope "/", Content do | ||||
|     pipe_through :browser | ||||
| 
 | ||||
|     get "/", PageController, :index | ||||
|   end | ||||
| 
 | ||||
|   Application.get_env(:core, :router_forwards, []) | ||||
|   |> Enum.map(fn router -> | ||||
|     forward "/", router | ||||
|   |> Enum.map(fn {router, path} -> | ||||
|     forward path, router | ||||
|   end) | ||||
| end | ||||
|  |  | |||
|  | @ -0,0 +1 @@ | |||
| <br /> | ||||
|  | @ -214,6 +214,10 @@ | |||
|             <td> | ||||
|             <![endif]--> | ||||
| 
 | ||||
|             <%= header do %> | ||||
|                 <%= render "_header.html" %> | ||||
|             <% end %> | ||||
| 
 | ||||
|             <!-- Email Body : BEGIN --> | ||||
|             <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin: auto;"> | ||||
|                 <%= @inner_content %> | ||||
|  |  | |||
|  | @ -4,315 +4,5 @@ defmodule CoreWeb.CoreEmailView do | |||
|     namespace: CoreWeb, | ||||
|     pattern: "**/*" | ||||
| 
 | ||||
|   import Phoenix.HTML, only: [sigil_E: 2] | ||||
| 
 | ||||
|   def framework_styles do | ||||
|     %{ | ||||
|       background: %{ | ||||
|         color: "#222222", | ||||
|       }, | ||||
|       body: %{ | ||||
|         font_family: "sans-serif", | ||||
|         font_size: "15px", | ||||
|         line_height: "20px", | ||||
|         text_color: "#555555", | ||||
|       }, | ||||
|       button: %{ | ||||
|         border_radius: "4px", | ||||
|         border: "1px solid #000000", | ||||
|         background: "#222222", | ||||
|         color: "#ffffff", | ||||
|         font_family: "sans-serif", | ||||
|         font_size: "15px", | ||||
|         line_height: "15px", | ||||
|         text_decoration: "none", | ||||
|         padding: "13px 17px", | ||||
|         display: "block", | ||||
|       }, | ||||
|       column: %{ | ||||
|         background: "#FFFFFF", | ||||
|         padding: "0 10px 40px 10px", | ||||
|       }, | ||||
|       footer: %{ | ||||
|         padding: "20px", | ||||
|         font_family: "sans-serif", | ||||
|         font_size: "12px", | ||||
|         line_height: "15px", | ||||
|         text_align: "center", | ||||
|         color: "#ffffff", | ||||
|       }, | ||||
|       global: %{ | ||||
|         width: 600, | ||||
|       }, | ||||
|       h1: %{ | ||||
|         margin: "0 0 10px 0", | ||||
|         font_family: "sans-serif", | ||||
|         font_size: "25px", | ||||
|         line_height: "30px", | ||||
|         color: "#333333", | ||||
|         font_weight: "normal", | ||||
|       }, | ||||
|       h2: %{ | ||||
|         margin: "0 0 10px 0", | ||||
|         font_family: "sans-serif", | ||||
|         font_size: "18px", | ||||
|         line_height: "22px", | ||||
|         color: "#333333", | ||||
|         font_weight: "bold", | ||||
|       }, | ||||
|       header: %{ | ||||
|         padding: "20px 0", | ||||
|         text_align: "center", | ||||
|       }, | ||||
|       hero_image: %{ | ||||
|         background: "#dddddd", | ||||
|         display: "block", | ||||
|         margin: "auto", | ||||
|       }, | ||||
|       inner_column: %{ | ||||
|         padding: "10px 10px 0", | ||||
|       }, | ||||
|       li: %{ | ||||
|         margin: "0 0 0 10px", | ||||
|       }, | ||||
|       last_li: %{ | ||||
|         margin: "0 0 10px 30px", | ||||
|       }, | ||||
|       spacer: %{ | ||||
|         height: "40", | ||||
|         font_size: "0px", | ||||
|         line_height: "0px", | ||||
|       }, | ||||
|       ul: %{ | ||||
|         margin: "0 0 10px 0", | ||||
|         padding: "0", | ||||
|       }, | ||||
|     } | ||||
|   end | ||||
| 
 | ||||
|   def framework_styles(group) do | ||||
|     Map.get(framework_styles(), group, %{}) | ||||
|   end | ||||
| 
 | ||||
|   def application_styles(group) do | ||||
|     styles = Application.get_env(:core, :email, %{}) |> Map.get(:styles, %{}) | ||||
| 
 | ||||
|     Map.get(styles, group, %{}) | ||||
|   end | ||||
| 
 | ||||
|   def effective_styles(group, overrides \\ %{}) do | ||||
|     group | ||||
|     |> framework_styles() | ||||
|     |> Core.MapUtils.deep_merge(application_styles(group)) | ||||
|     |> Map.merge(overrides) | ||||
|   end | ||||
| 
 | ||||
|   def preview(do: content) do | ||||
|     ~E""" | ||||
|     <div style="max-height:0; overflow:hidden; mso-hide:all;" aria-hidden="true"> | ||||
|       <%= content %> | ||||
|     </div> | ||||
|     <!-- Visually Hidden Preheader Text : END --> | ||||
| 
 | ||||
|     <!-- Create white space after the desired preview text so email clients don’t pull other distracting text into the inbox preview. Extend as necessary. --> | ||||
|     <!-- Preview Text Spacing Hack : BEGIN --> | ||||
|     <div style="display: none; font-size: 1px; line-height: 1px; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden; mso-hide: all; font-family: sans-serif;"> | ||||
|         ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌  | ||||
|         ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌  | ||||
|         ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌  | ||||
|         ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌  | ||||
|         ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌  | ||||
|         ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌  | ||||
|     </div> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   def header(do: content) do | ||||
|     ~E""" | ||||
|     <tr> | ||||
|       <td style="<%= map_style(effective_styles(:header)) %>"> | ||||
|         <%= content %> | ||||
|       </td> | ||||
|     </tr> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   def spacer do | ||||
|     style = effective_styles(:spacer) | ||||
| 
 | ||||
|     ~E""" | ||||
|     <tr> | ||||
|       <td aria-hidden="true" height="<%= style[:height] %>" style="<%= map_style(style) %>"> | ||||
|           | ||||
|       </td> | ||||
|     </tr> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   def row(do: content) do | ||||
|     ~E""" | ||||
|     <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"> | ||||
|       <tr> | ||||
|         <%= content %> | ||||
|       </tr> | ||||
|     </table> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   def col(n, opts, do: content) do | ||||
|     {of, opts} = Keyword.pop!(opts, :of) | ||||
|     width = n * 100.0 / of | ||||
| 
 | ||||
|     ~E""" | ||||
|     <td valign="top" width="<%= width %>%" style="<%= map_style(effective_styles(:column)) %>"> | ||||
|       <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"> | ||||
|         <tr> | ||||
|           <td style="<%= map_style(effective_styles(:body)) %> <%= map_style(effective_styles(:inner_column)) %>"> | ||||
|             <%= content %> | ||||
|           </td> | ||||
|         </tr> | ||||
|       </table> | ||||
|     </td> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   def hero_image(opts) do | ||||
|     {src, _rest_opts} = Keyword.pop!(opts, :src) | ||||
| 
 | ||||
|     ~E""" | ||||
|     <%= row do %> | ||||
|       <%= col 1, of: 1 do %> | ||||
|         <img | ||||
|           src="<%= src %>" | ||||
|           width="<%= effective_styles(:global)[:width] %>" | ||||
|           height="auto" | ||||
|           alt="alt_text" | ||||
|           border="0" | ||||
|           style=" | ||||
|             <%= map_style(effective_styles(:body)) %> | ||||
|             width: 100%; | ||||
|             max-width: <%= effective_styles(:global)[:width] %>; | ||||
|             height: auto; | ||||
|             <%= map_style(effective_styles(:hero_image)) %> | ||||
|           " | ||||
|           class="g-img" | ||||
|         > | ||||
|       <% end %> | ||||
|     <% end %> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   def h1(do: content) do | ||||
|     ~E""" | ||||
|       <h1 style="<%= map_style(effective_styles(:h1)) %>"> | ||||
|         <%= content %> | ||||
|       </h1> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   def h2(do: content) do | ||||
|     ~E""" | ||||
|     <h2 style="<%= map_style(effective_styles(:h2)) %>"> | ||||
|       <%= content %> | ||||
|     </h2> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   def p(do: content) do | ||||
|     ~E""" | ||||
|     <p style="<%= map_style(effective_styles(:body)) %>"> | ||||
|       <%= content %> | ||||
|     </p> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   def button(opts, do: content) do | ||||
|     {overrides, opts_without_style} = Keyword.pop(opts, :style, %{}) | ||||
|     {href, _rest_opts} = Keyword.pop!(opts_without_style, :href) | ||||
| 
 | ||||
|     style = effective_styles(:button, overrides) | ||||
|     cell_style = style |> Map.take([:border_radius, :background]) | ||||
| 
 | ||||
|     ~E""" | ||||
|       <%= wrapper do %> | ||||
|         <td | ||||
|           class="button-td button-td-primary" | ||||
|           style="<%= map_style(cell_style) %>" | ||||
|         > | ||||
|           <a | ||||
|             class="button-a button-a-primary" | ||||
|             href="<%= href %>" | ||||
|             style="<%= map_style(style) %>" | ||||
|           > | ||||
|             <%= content %> | ||||
|           </a> | ||||
|         </td> | ||||
|       <% end %> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   def ul(opts) do | ||||
|     {items, _rest_opts} = Keyword.pop!(opts, :items) | ||||
| 
 | ||||
|     item_count = Enum.count(items) | ||||
| 
 | ||||
|     item_tags = | ||||
|       items | ||||
|       |> Enum.with_index() | ||||
|       |> Enum.map(fn {item, index} -> | ||||
|         li_for_ul(index, item_count, item) | ||||
|       end) | ||||
| 
 | ||||
|     ~E""" | ||||
|     <ul style="<%= map_style(effective_styles(:ul)) %>"> | ||||
|       <%= item_tags %> | ||||
|     </ul> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   def footer do | ||||
|     ~E""" | ||||
|     <%= wrapper do %> | ||||
|       <td style="<%= map_style(effective_styles(:footer)) %>"> | ||||
|         <%= I18n.t! "en", "email.company.name" %><br> | ||||
|         <span class="unstyle-auto-detected-links"> | ||||
|           <%= I18n.t! "en", "email.company.address" %><br> | ||||
|           <%= I18n.t! "en", "email.company.phone" %> | ||||
|         </span> | ||||
|         <br><br> | ||||
|       </td> | ||||
|     <% end %> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   defp li_for_ul(index, list_length, content) do | ||||
|     last_styles = if index == list_length - 1, do: map_style(effective_styles(:last_li)) | ||||
|     ~E""" | ||||
|       <li style="<%= map_style(effective_styles(:li)) %>"> | ||||
|         <%= content %> | ||||
|       </li> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   defp wrapper(do: content) do | ||||
|     ~E""" | ||||
|     <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;"> | ||||
|       <tr> | ||||
|         <%= content %> | ||||
|       </tr> | ||||
|     </table> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   defp map_style(map) do | ||||
|     map | ||||
|     |> Enum.map(fn {key, value} -> | ||||
|       new_key = | ||||
|         key | ||||
|         |> Atom.to_string() | ||||
|         |> String.replace("_", "-") | ||||
|       "#{new_key}: #{value};" | ||||
|     end) | ||||
|     |> Enum.join("\n") | ||||
|   end | ||||
|   import CoreWeb.EmailHelpers | ||||
| end | ||||
|  |  | |||
							
								
								
									
										317
									
								
								apps/core/lib/core_web/views/email_helpers.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										317
									
								
								apps/core/lib/core_web/views/email_helpers.ex
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,317 @@ | |||
| defmodule CoreWeb.EmailHelpers do | ||||
|   @moduledoc """ | ||||
|   HTML helpers for emails. | ||||
|   """ | ||||
| 
 | ||||
|   import Phoenix.HTML, only: [sigil_E: 2] | ||||
| 
 | ||||
|   def framework_styles do | ||||
|     %{ | ||||
|       background: %{ | ||||
|         color: "#222222", | ||||
|       }, | ||||
|       body: %{ | ||||
|         font_family: "sans-serif", | ||||
|         font_size: "15px", | ||||
|         line_height: "20px", | ||||
|         text_color: "#555555", | ||||
|       }, | ||||
|       button: %{ | ||||
|         border_radius: "4px", | ||||
|         border: "1px solid #000000", | ||||
|         background: "#222222", | ||||
|         color: "#ffffff", | ||||
|         font_family: "sans-serif", | ||||
|         font_size: "15px", | ||||
|         line_height: "15px", | ||||
|         text_decoration: "none", | ||||
|         padding: "13px 17px", | ||||
|         display: "block", | ||||
|       }, | ||||
|       column: %{ | ||||
|         background: "#FFFFFF", | ||||
|         padding: "0 10px 40px 10px", | ||||
|       }, | ||||
|       footer: %{ | ||||
|         padding: "20px", | ||||
|         font_family: "sans-serif", | ||||
|         font_size: "12px", | ||||
|         line_height: "15px", | ||||
|         text_align: "center", | ||||
|         color: "#ffffff", | ||||
|       }, | ||||
|       global: %{ | ||||
|         width: 600, | ||||
|       }, | ||||
|       h1: %{ | ||||
|         margin: "0 0 10px 0", | ||||
|         font_family: "sans-serif", | ||||
|         font_size: "25px", | ||||
|         line_height: "30px", | ||||
|         color: "#333333", | ||||
|         font_weight: "normal", | ||||
|       }, | ||||
|       h2: %{ | ||||
|         margin: "0 0 10px 0", | ||||
|         font_family: "sans-serif", | ||||
|         font_size: "18px", | ||||
|         line_height: "22px", | ||||
|         color: "#333333", | ||||
|         font_weight: "bold", | ||||
|       }, | ||||
|       header: %{ | ||||
|         padding: "20px 0", | ||||
|         text_align: "center", | ||||
|       }, | ||||
|       hero_image: %{ | ||||
|         background: "#dddddd", | ||||
|         display: "block", | ||||
|         margin: "auto", | ||||
|       }, | ||||
|       inner_column: %{ | ||||
|         padding: "10px 10px 0", | ||||
|       }, | ||||
|       li: %{ | ||||
|         margin: "0 0 0 10px", | ||||
|       }, | ||||
|       last_li: %{ | ||||
|         margin: "0 0 10px 30px", | ||||
|       }, | ||||
|       spacer: %{ | ||||
|         height: "40", | ||||
|         font_size: "0px", | ||||
|         line_height: "0px", | ||||
|       }, | ||||
|       ul: %{ | ||||
|         margin: "0 0 10px 0", | ||||
|         padding: "0", | ||||
|       }, | ||||
|     } | ||||
|   end | ||||
| 
 | ||||
|   def framework_styles(group) do | ||||
|     Map.get(framework_styles(), group, %{}) | ||||
|   end | ||||
| 
 | ||||
|   def application_styles(group) do | ||||
|     styles = Application.get_env(:core, :email, %{}) |> Map.get(:styles, %{}) | ||||
| 
 | ||||
|     Map.get(styles, group, %{}) | ||||
|   end | ||||
| 
 | ||||
|   def effective_styles(group, overrides \\ %{}) do | ||||
|     group | ||||
|     |> framework_styles() | ||||
|     |> Core.MapUtils.deep_merge(application_styles(group)) | ||||
|     |> Map.merge(overrides) | ||||
|   end | ||||
| 
 | ||||
|   def preview(do: content) do | ||||
|     ~E""" | ||||
|     <div style="max-height:0; overflow:hidden; mso-hide:all;" aria-hidden="true"> | ||||
|       <%= content %> | ||||
|     </div> | ||||
|     <!-- Visually Hidden Preheader Text : END --> | ||||
| 
 | ||||
|     <!-- Create white space after the desired preview text so email clients don’t pull other distracting text into the inbox preview. Extend as necessary. --> | ||||
|     <!-- Preview Text Spacing Hack : BEGIN --> | ||||
|     <div style="display: none; font-size: 1px; line-height: 1px; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden; mso-hide: all; font-family: sans-serif;"> | ||||
|         ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌  | ||||
|         ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌  | ||||
|         ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌  | ||||
|         ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌  | ||||
|         ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌  | ||||
|         ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌  | ||||
|     </div> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   def header(do: content) do | ||||
|     ~E""" | ||||
|     <tr> | ||||
|       <td style="<%= map_style(effective_styles(:header)) %>"> | ||||
|         <%= content %> | ||||
|       </td> | ||||
|     </tr> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   def spacer do | ||||
|     style = effective_styles(:spacer) | ||||
| 
 | ||||
|     ~E""" | ||||
|     <tr> | ||||
|       <td aria-hidden="true" height="<%= style[:height] %>" style="<%= map_style(style) %>"> | ||||
|           | ||||
|       </td> | ||||
|     </tr> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   def row(do: content) do | ||||
|     ~E""" | ||||
|     <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"> | ||||
|       <tr> | ||||
|         <%= content %> | ||||
|       </tr> | ||||
|     </table> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   def col(n, opts, do: content) do | ||||
|     {of, opts} = Keyword.pop!(opts, :of) | ||||
|     width = n * 100.0 / of | ||||
| 
 | ||||
|     ~E""" | ||||
|     <td valign="top" width="<%= width %>%" style="<%= map_style(effective_styles(:column)) %>"> | ||||
|       <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"> | ||||
|         <tr> | ||||
|           <td style="<%= map_style(effective_styles(:body)) %> <%= map_style(effective_styles(:inner_column)) %>"> | ||||
|             <%= content %> | ||||
|           </td> | ||||
|         </tr> | ||||
|       </table> | ||||
|     </td> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   def hero_image(opts) do | ||||
|     {src, _rest_opts} = Keyword.pop!(opts, :src) | ||||
| 
 | ||||
|     ~E""" | ||||
|     <%= row do %> | ||||
|       <%= col 1, of: 1 do %> | ||||
|         <img | ||||
|           src="<%= src %>" | ||||
|           width="<%= effective_styles(:global)[:width] %>" | ||||
|           height="auto" | ||||
|           alt="alt_text" | ||||
|           border="0" | ||||
|           style=" | ||||
|             <%= map_style(effective_styles(:body)) %> | ||||
|             width: 100%; | ||||
|             max-width: <%= effective_styles(:global)[:width] %>; | ||||
|             height: auto; | ||||
|             <%= map_style(effective_styles(:hero_image)) %> | ||||
|           " | ||||
|           class="g-img" | ||||
|         > | ||||
|       <% end %> | ||||
|     <% end %> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   def h1(do: content) do | ||||
|     ~E""" | ||||
|       <h1 style="<%= map_style(effective_styles(:h1)) %>"> | ||||
|         <%= content %> | ||||
|       </h1> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   def h2(do: content) do | ||||
|     ~E""" | ||||
|     <h2 style="<%= map_style(effective_styles(:h2)) %>"> | ||||
|       <%= content %> | ||||
|     </h2> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   def p(do: content) do | ||||
|     ~E""" | ||||
|     <p style="<%= map_style(effective_styles(:body)) %>"> | ||||
|       <%= content %> | ||||
|     </p> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   def button(opts, do: content) do | ||||
|     {overrides, opts_without_style} = Keyword.pop(opts, :style, %{}) | ||||
|     {href, _rest_opts} = Keyword.pop!(opts_without_style, :href) | ||||
| 
 | ||||
|     style = effective_styles(:button, overrides) | ||||
|     cell_style = style |> Map.take([:border_radius, :background]) | ||||
| 
 | ||||
|     ~E""" | ||||
|       <%= wrapper do %> | ||||
|         <td | ||||
|           class="button-td button-td-primary" | ||||
|           style="<%= map_style(cell_style) %>" | ||||
|         > | ||||
|           <a | ||||
|             class="button-a button-a-primary" | ||||
|             href="<%= href %>" | ||||
|             style="<%= map_style(style) %>" | ||||
|           > | ||||
|             <%= content %> | ||||
|           </a> | ||||
|         </td> | ||||
|       <% end %> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   def ul(opts) do | ||||
|     {items, _rest_opts} = Keyword.pop!(opts, :items) | ||||
| 
 | ||||
|     item_count = Enum.count(items) | ||||
| 
 | ||||
|     item_tags = | ||||
|       items | ||||
|       |> Enum.with_index() | ||||
|       |> Enum.map(fn {item, index} -> | ||||
|         li_for_ul(index, item_count, item) | ||||
|       end) | ||||
| 
 | ||||
|     ~E""" | ||||
|     <ul style="<%= map_style(effective_styles(:ul)) %>"> | ||||
|       <%= item_tags %> | ||||
|     </ul> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   def footer do | ||||
|     ~E""" | ||||
|     <%= wrapper do %> | ||||
|       <td style="<%= map_style(effective_styles(:footer)) %>"> | ||||
|         <%= I18n.t! "en", "email.company.name" %><br> | ||||
|         <span class="unstyle-auto-detected-links"> | ||||
|           <%= I18n.t! "en", "email.company.address" %><br> | ||||
|           <%= I18n.t! "en", "email.company.phone" %> | ||||
|         </span> | ||||
|         <br><br> | ||||
|       </td> | ||||
|     <% end %> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   defp li_for_ul(index, list_length, content) do | ||||
|     last_styles = if index == list_length - 1, do: map_style(effective_styles(:last_li)) | ||||
|     ~E""" | ||||
|       <li style="<%= map_style(effective_styles(:li)) %>"> | ||||
|         <%= content %> | ||||
|       </li> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   defp wrapper(do: content) do | ||||
|     ~E""" | ||||
|     <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;"> | ||||
|       <tr> | ||||
|         <%= content %> | ||||
|       </tr> | ||||
|     </table> | ||||
|     """ | ||||
|   end | ||||
| 
 | ||||
|   defp map_style(map) do | ||||
|     map | ||||
|     |> Enum.map(fn {key, value} -> | ||||
|       new_key = | ||||
|         key | ||||
|         |> Atom.to_string() | ||||
|         |> String.replace("_", "-") | ||||
|       "#{new_key}: #{value};" | ||||
|     end) | ||||
|     |> Enum.join("\n") | ||||
|   end | ||||
| end | ||||
|  | @ -39,9 +39,9 @@ defmodule Core.MixProject do | |||
|   defp deps do | ||||
|     [ | ||||
|       {:bamboo, "~> 1.5"}, | ||||
|       {:excoveralls, "~> 0.10", only: [:dev, :test]}, | ||||
|       {:credo, "~> 1.4", only: [:dev, :test], runtime: false}, | ||||
|       {:ex_cldr, "~> 2.13.0"}, | ||||
|       {:excoveralls, "~> 0.10", only: [:dev, :test]}, | ||||
|       {:phoenix, "~> 1.5.3"}, | ||||
|       {:phoenix_ecto, "~> 4.1"}, | ||||
|       {:ecto_sql, "~> 3.4"}, | ||||
|  | @ -50,6 +50,7 @@ defmodule Core.MixProject do | |||
|       {:phoenix_html, "~> 2.11"}, | ||||
|       {:phoenix_live_reload, "~> 1.2", only: :dev}, | ||||
|       {:phoenix_live_dashboard, "~> 0.2.0"}, | ||||
|       {:pow, "~> 1.0.20"}, | ||||
|       {:telemetry_metrics, "~> 0.4"}, | ||||
|       {:telemetry_poller, "~> 0.4"}, | ||||
|       {:gettext, "~> 0.11"}, | ||||
|  |  | |||
|  | @ -1,13 +1,39 @@ | |||
| use Mix.Config | ||||
| 
 | ||||
| # Configure Mix tasks and generators | ||||
| config :auth, | ||||
|   ecto_repos: [Auth.Repo] | ||||
| 
 | ||||
| config :auth_web, | ||||
|   ecto_repos: [Auth.Repo], | ||||
|   generators: [context_app: :auth] | ||||
| 
 | ||||
| config :auth_web, :pow, | ||||
|   user: Auth.Users.User, | ||||
|   repo: Auth.Repo, | ||||
|   extensions: [PowEmailConfirmation], | ||||
|   controller_callbacks: Pow.Extension.Phoenix.ControllerCallbacks, | ||||
|   mailer_backend: AuthWeb.Pow.Mailer, | ||||
|   web_mailer_module: AuthWeb, | ||||
|   web_module: AuthWeb | ||||
| 
 | ||||
| # Configures the endpoint | ||||
| config :auth_web, AuthWeb.Endpoint, | ||||
|   url: [host: "localhost"], | ||||
|   secret_key_base: "cjtU4RvTirW4yJZDkdqZJmaj7bvaQRrX6mevkoGYqzEuMujV/Q0w3utlO5+FUsUj", | ||||
|   render_errors: [view: AuthWeb.ErrorView, accepts: ~w(html json), layout: false], | ||||
|   pubsub_server: AuthWeb.PubSub, | ||||
|   live_view: [signing_salt: "AwljJYaY"] | ||||
| 
 | ||||
| config :core, | ||||
|   router_forwards: [Content.Router], | ||||
|   router_forwards: [{Content.Router, "/pages"}, {AuthWeb.Router, "/auth"}], | ||||
|   email_from: "example@example.org" | ||||
| 
 | ||||
| config :content, | ||||
|   generators: [context_app: false] | ||||
| 
 | ||||
| config :content, Content.Endpoint, server: false | ||||
| config :auth_web, AuthWeb.Endpoint, server: false | ||||
| 
 | ||||
| import_config "../apps/*/config/config.exs" | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,70 @@ | |||
| use Mix.Config | ||||
| 
 | ||||
| # Configure your database | ||||
| config :auth, Auth.Repo, | ||||
|   username: "postgres", | ||||
|   password: "postgres", | ||||
|   database: "auth_dev", | ||||
|   hostname: "localhost", | ||||
|   show_sensitive_data_on_connection_error: true, | ||||
|   pool_size: 10 | ||||
| 
 | ||||
| # For development, we disable any cache and enable | ||||
| # debugging and code reloading. | ||||
| # | ||||
| # The watchers configuration can be used to run external | ||||
| # watchers to your application. For example, we use it | ||||
| # with webpack to recompile .js and .css sources. | ||||
| config :auth_web, AuthWeb.Endpoint, | ||||
|   http: [port: 4000], | ||||
|   debug_errors: true, | ||||
|   code_reloader: true, | ||||
|   check_origin: false, | ||||
|   watchers: [ | ||||
|     node: [ | ||||
|       "node_modules/webpack/bin/webpack.js", | ||||
|       "--mode", | ||||
|       "development", | ||||
|       "--watch-stdin", | ||||
|       cd: Path.expand("../apps/auth_web/assets", __DIR__) | ||||
|     ] | ||||
|   ] | ||||
| 
 | ||||
| # ## SSL Support | ||||
| # | ||||
| # In order to use HTTPS in development, a self-signed | ||||
| # certificate can be generated by running the following | ||||
| # Mix task: | ||||
| # | ||||
| #     mix phx.gen.cert | ||||
| # | ||||
| # Note that this task requires Erlang/OTP 20 or later. | ||||
| # Run `mix help phx.gen.cert` for more information. | ||||
| # | ||||
| # The `http:` config above can be replaced with: | ||||
| # | ||||
| #     https: [ | ||||
| #       port: 4001, | ||||
| #       cipher_suite: :strong, | ||||
| #       keyfile: "priv/cert/selfsigned_key.pem", | ||||
| #       certfile: "priv/cert/selfsigned.pem" | ||||
| #     ], | ||||
| # | ||||
| # If desired, both `http:` and `https:` keys can be | ||||
| # configured to run both http and https servers on | ||||
| # different ports. | ||||
| 
 | ||||
| # Watch static and templates for browser reloading. | ||||
| config :auth_web, AuthWeb.Endpoint, | ||||
|   live_reload: [ | ||||
|     patterns: [ | ||||
|       ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", | ||||
|       ~r"priv/gettext/.*(po)$", | ||||
|       ~r"lib/auth_web/(live|views)/.*(ex)$", | ||||
|       ~r"lib/auth_web/templates/.*(eex)$" | ||||
|     ] | ||||
|   ] | ||||
| 
 | ||||
| # For development, we disable any cache and enable | ||||
| # debugging and code reloading. | ||||
| # | ||||
|  |  | |||
|  | @ -1,5 +0,0 @@ | |||
| email: | ||||
|   company: | ||||
|     name: "" | ||||
|     address: "" | ||||
|     phone: "" | ||||
|  | @ -1,5 +1,52 @@ | |||
| use Mix.Config | ||||
| 
 | ||||
| # For production, don't forget to configure the url host | ||||
| # to something meaningful, Phoenix uses this information | ||||
| # when generating URLs. | ||||
| # | ||||
| # Note we also include the path to a cache manifest | ||||
| # containing the digested version of static files. This | ||||
| # manifest is generated by the `mix phx.digest` task, | ||||
| # which you should run after static files are built and | ||||
| # before starting your production server. | ||||
| config :auth_web, AuthWeb.Endpoint, | ||||
|   url: [host: "example.com", port: 80], | ||||
|   cache_static_manifest: "priv/static/cache_manifest.json" | ||||
| 
 | ||||
| # ## SSL Support | ||||
| # | ||||
| # To get SSL working, you will need to add the `https` key | ||||
| # to the previous section and set your `:url` port to 443: | ||||
| # | ||||
| #     config :auth_web, AuthWeb.Endpoint, | ||||
| #       ... | ||||
| #       url: [host: "example.com", port: 443], | ||||
| #       https: [ | ||||
| #         port: 443, | ||||
| #         cipher_suite: :strong, | ||||
| #         keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), | ||||
| #         certfile: System.get_env("SOME_APP_SSL_CERT_PATH"), | ||||
| #         transport_options: [socket_opts: [:inet6]] | ||||
| #       ] | ||||
| # | ||||
| # The `cipher_suite` is set to `:strong` to support only the | ||||
| # latest and more secure SSL ciphers. This means old browsers | ||||
| # and clients may not be supported. You can set it to | ||||
| # `:compatible` for wider support. | ||||
| # | ||||
| # `:keyfile` and `:certfile` expect an absolute path to the key | ||||
| # and cert in disk or a relative path inside priv, for example | ||||
| # "priv/ssl/server.key". For all supported SSL configuration | ||||
| # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 | ||||
| # | ||||
| # We also recommend setting `force_ssl` in your endpoint, ensuring | ||||
| # no data is ever sent via http, always redirecting to https: | ||||
| # | ||||
| #     config :auth_web, AuthWeb.Endpoint, | ||||
| #       force_ssl: [hsts: true] | ||||
| # | ||||
| # Check `Plug.SSL` for all available options in `force_ssl`. | ||||
| 
 | ||||
| # For production, don't forget to configure the url host | ||||
| # to something meaningful, Phoenix uses this information | ||||
| # when generating URLs. | ||||
|  |  | |||
|  | @ -1,5 +1,24 @@ | |||
| use Mix.Config | ||||
| 
 | ||||
| # Configure your database | ||||
| # | ||||
| # The MIX_TEST_PARTITION environment variable can be used | ||||
| # to provide built-in test partitioning in CI environment. | ||||
| # Run `mix help test` for more information. | ||||
| config :auth, Auth.Repo, | ||||
|   username: "postgres", | ||||
|   password: "postgres", | ||||
|   database: "auth_test#{System.get_env("MIX_TEST_PARTITION")}", | ||||
|   hostname: System.get_env("DATABASE_URL") || "localhost", | ||||
|   pool: Ecto.Adapters.SQL.Sandbox | ||||
| 
 | ||||
| 
 | ||||
| # We don't run a server during test. If one is required, | ||||
| # you can enable the server option below. | ||||
| config :auth_web, AuthWeb.Endpoint, | ||||
|   http: [port: 4002], | ||||
|   server: false | ||||
| 
 | ||||
| # We don't run a server during test. If one is required, | ||||
| # you can enable the server option below. | ||||
| config :content, Content.Endpoint, | ||||
|  |  | |||
							
								
								
									
										2
									
								
								mix.lock
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								mix.lock
									
									
									
									
									
								
							|  | @ -12,6 +12,7 @@ | |||
|   "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"}, | ||||
|   "ecto_sql": {:hex, :ecto_sql, "3.4.4", "d28bac2d420f708993baed522054870086fd45016a9d09bb2cd521b9c48d32ea", [: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", "edb49af715dd72f213b66adfd0f668a43c17ed510b5d9ac7528569b23af57fe8"}, | ||||
|   "ex_cldr": {:hex, :ex_cldr, "2.13.0", "742f14a4afcfea61a190d603d8e555d2c91d71e4e8fc2520d5dc35616969e225", [:mix], [{:cldr_utils, "~> 2.3", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.13", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "5e4cf3e945ee60156a3342e2a762f69036ffbe1f80520cc88592d68f12c5db55"}, | ||||
|   "ex_prompt": {:hex, :ex_prompt, "0.1.5", "b136642d0962f8ea37b3c9fa185ad1f42c71c3b9c6c3950f0358d7f3d2db2970", [:mix], [], "hexpm", "ad19a404708c9c7b05d36090b2d074ceafbed248a8de1a22d45a05ebe6994b83"}, | ||||
|   "excoveralls": {:hex, :excoveralls, "0.13.0", "4e1b7cc4e0351d8d16e9be21b0345a7e165798ee5319c7800b9138ce17e0b38e", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "fe2a56c8909564e2e6764765878d7d5e141f2af3bc8ff3b018a68ee2a218fced"}, | ||||
|   "file_system": {:hex, :file_system, "0.2.8", "f632bd287927a1eed2b718f22af727c5aeaccc9a98d8c2bd7bff709e851dc986", [:mix], [], "hexpm", "97a3b6f8d63ef53bd0113070102db2ce05352ecf0d25390eb8d747c2bde98bca"}, | ||||
|   "gettext": {:hex, :gettext, "0.18.0", "406d6b9e0e3278162c2ae1de0a60270452c553536772167e2d701f028116f870", [:mix], [], "hexpm", "c3f850be6367ebe1a08616c2158affe4a23231c70391050bf359d5f92f66a571"}, | ||||
|  | @ -34,6 +35,7 @@ | |||
|   "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"}, | ||||
|   "plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"}, | ||||
|   "postgrex": {:hex, :postgrex, "0.15.5", "aec40306a622d459b01bff890fa42f1430dac61593b122754144ad9033a2152f", [: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", "ed90c81e1525f65a2ba2279dbcebf030d6d13328daa2f8088b9661eb9143af7f"}, | ||||
|   "pow": {:hex, :pow, "1.0.20", "b99993811af5233681bfc521e81ca706d25a56f2be54bad6424db327ce840ab9", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.3.0 and < 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, ">= 2.0.0 and <= 3.0.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, ">= 1.5.0 and < 2.0.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "4b6bd271399ccb353abbdbdc316199fe7fd7ae36bbf47059d53e366831c34fc8"}, | ||||
|   "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"}, | ||||
|   "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, | ||||
|   "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, | ||||
|  |  | |||
|  | @ -6,4 +6,5 @@ set -e | |||
| mix local.hex --force | ||||
| mix local.rebar --force | ||||
| mix deps.get | ||||
| 
 | ||||
| mix test | ||||
		Loading…
	
		Reference in a new issue
	
	 Robert Prehn
						Robert Prehn