883 lines
26 KiB
Elixir
883 lines
26 KiB
Elixir
defmodule Mix.Tasks.Phx.Gen.Auth do
|
|
@shortdoc "Generates authentication logic for a resource"
|
|
|
|
@moduledoc """
|
|
Generates authentication logic and related views for a resource.
|
|
|
|
$ mix phx.gen.auth Accounts User users
|
|
|
|
The first argument is the context module followed by the schema module
|
|
and its plural name (used as the schema table name).
|
|
|
|
Additional information and security considerations are detailed in the
|
|
[`mix phx.gen.auth` guide](mix_phx_gen_auth.html).
|
|
|
|
## LiveView vs conventional Controllers & Views
|
|
|
|
Authentication views can either be generated to use LiveView by passing
|
|
the `--live` option, or they can use conventional Phoenix
|
|
Controllers & Views by passing `--no-live`.
|
|
|
|
If neither of these options are provided, a prompt will be displayed.
|
|
|
|
Using the `--live` option is advised if you plan on using LiveView
|
|
elsewhere in your application. The user experience when navigating between
|
|
LiveViews can be tightly controlled, allowing you to let your users navigate
|
|
to authentication views without necessarily triggering a new HTTP request
|
|
each time (which would result in a full page load).
|
|
|
|
## Password hashing
|
|
|
|
The password hashing mechanism defaults to `bcrypt` for
|
|
Unix systems and `pbkdf2` for Windows systems. Both
|
|
systems use the [Comeonin interface](https://hexdocs.pm/comeonin/).
|
|
|
|
The password hashing mechanism can be overridden with the
|
|
`--hashing-lib` option. The following values are supported:
|
|
|
|
* `bcrypt` - [bcrypt_elixir](https://hex.pm/packages/bcrypt_elixir)
|
|
* `pbkdf2` - [pbkdf2_elixir](https://hex.pm/packages/pbkdf2_elixir)
|
|
* `argon2` - [argon2_elixir](https://hex.pm/packages/argon2_elixir)
|
|
|
|
We recommend developers to consider using `argon2`, which
|
|
is the most robust of all 3. The downside is that `argon2`
|
|
is quite CPU and memory intensive, and you will need more
|
|
powerful instances to run your applications on.
|
|
|
|
For more information about choosing these libraries, see the
|
|
[Comeonin project](https://github.com/riverrun/comeonin).
|
|
|
|
## Web namespace
|
|
|
|
By default, the controllers and HTML view will be namespaced by the schema name.
|
|
You can customize the web module namespace by passing the `--web` flag with a
|
|
module name, for example:
|
|
|
|
$ mix phx.gen.auth Accounts User users --web Warehouse
|
|
|
|
Which would generate the controllers, views, templates and associated tests nested in the `MyAppWeb.Warehouse` namespace:
|
|
|
|
* `lib/my_app_web/controllers/warehouse/user_auth.ex`
|
|
* `lib/my_app_web/controllers/warehouse/user_confirmation_controller.ex`
|
|
* `lib/my_app_web/controllers/warehouse/user_confirmation_html.ex`
|
|
* `lib/my_app_web/controllers/warehouse/user_confirmation_html/new.html.heex`
|
|
* `test/my_app_web/controllers/warehouse/user_auth_test.exs`
|
|
* `test/my_app_web/controllers/warehouse/user_confirmation_controller_test.exs`
|
|
* and so on...
|
|
|
|
## Multiple invocations
|
|
|
|
You can invoke this generator multiple times. This is typically useful
|
|
if you have distinct resources that go through distinct authentication
|
|
workflows:
|
|
|
|
$ mix phx.gen.auth Store User users
|
|
$ mix phx.gen.auth Backoffice Admin admins
|
|
|
|
## Binary ids
|
|
|
|
The `--binary-id` option causes the generated migration to use
|
|
`binary_id` for its primary key and foreign keys.
|
|
|
|
## Default options
|
|
|
|
This generator uses default options provided in the `:generators`
|
|
configuration of your application. These are the defaults:
|
|
|
|
config :your_app, :generators,
|
|
binary_id: false,
|
|
sample_binary_id: "11111111-1111-1111-1111-111111111111"
|
|
|
|
You can override those options per invocation by providing corresponding
|
|
switches, e.g. `--no-binary-id` to use normal ids despite the default
|
|
configuration.
|
|
|
|
## Custom table names
|
|
|
|
By default, the table name for the migration and schema will be
|
|
the plural name provided for the resource. To customize this value,
|
|
a `--table` option may be provided. For example:
|
|
|
|
$ mix phx.gen.auth Accounts User users --table accounts_users
|
|
|
|
This will cause the generated tables to be named `"accounts_users"` and `"accounts_users_tokens"`.
|
|
"""
|
|
|
|
use Mix.Task
|
|
|
|
alias Mix.Phoenix.{Context, Schema}
|
|
alias Mix.Tasks.Phx.Gen
|
|
alias Mix.Tasks.Phx.Gen.Auth.{HashingLibrary, Injector, Migration}
|
|
|
|
@switches [
|
|
web: :string,
|
|
binary_id: :boolean,
|
|
hashing_lib: :string,
|
|
table: :string,
|
|
merge_with_existing_context: :boolean,
|
|
prefix: :string,
|
|
live: :boolean,
|
|
compile: :boolean
|
|
]
|
|
|
|
@doc false
|
|
def run(args, test_opts \\ []) do
|
|
if Mix.Project.umbrella?() do
|
|
Mix.raise("mix phx.gen.auth can only be run inside an application directory")
|
|
end
|
|
|
|
Mix.Phoenix.ensure_live_view_compat!(__MODULE__)
|
|
|
|
{opts, parsed} = OptionParser.parse!(args, strict: @switches)
|
|
validate_args!(parsed)
|
|
hashing_library = build_hashing_library!(opts)
|
|
|
|
context_args = OptionParser.to_argv(opts, switches: @switches) ++ parsed
|
|
{context, schema} = Gen.Context.build(context_args, __MODULE__)
|
|
|
|
context = put_live_option(context)
|
|
Gen.Context.prompt_for_code_injection(context)
|
|
|
|
if "--no-compile" not in args do
|
|
# Needed so we can get the ecto adapter and ensure other
|
|
# libraries are loaded.
|
|
Mix.Task.run("compile")
|
|
validate_required_dependencies!()
|
|
end
|
|
|
|
ecto_adapter =
|
|
Keyword.get_lazy(
|
|
test_opts,
|
|
:ecto_adapter,
|
|
fn -> get_ecto_adapter!(schema) end
|
|
)
|
|
|
|
migration = Migration.build(ecto_adapter)
|
|
|
|
binding = [
|
|
context: context,
|
|
schema: schema,
|
|
migration: migration,
|
|
hashing_library: hashing_library,
|
|
web_app_name: web_app_name(context),
|
|
web_namespace: context.web_module,
|
|
endpoint_module: Module.concat([context.web_module, Endpoint]),
|
|
auth_module:
|
|
Module.concat([context.web_module, schema.web_namespace, "#{inspect(schema.alias)}Auth"]),
|
|
router_scope: router_scope(context),
|
|
web_path_prefix: web_path_prefix(schema),
|
|
test_case_options: test_case_options(ecto_adapter),
|
|
live?: Keyword.fetch!(context.opts, :live)
|
|
]
|
|
|
|
paths = generator_paths()
|
|
|
|
prompt_for_conflicts(context)
|
|
|
|
context
|
|
|> copy_new_files(binding, paths)
|
|
|> inject_conn_case_helpers(paths, binding)
|
|
|> inject_config(hashing_library)
|
|
|> maybe_inject_mix_dependency(hashing_library)
|
|
|> inject_routes(paths, binding)
|
|
|> maybe_inject_router_import(binding)
|
|
|> maybe_inject_router_plug()
|
|
|> maybe_inject_app_layout_menu()
|
|
|> Gen.Notifier.maybe_print_mailer_installation_instructions()
|
|
|> print_shell_instructions()
|
|
end
|
|
|
|
defp web_app_name(%Context{} = context) do
|
|
context.web_module
|
|
|> inspect()
|
|
|> Phoenix.Naming.underscore()
|
|
end
|
|
|
|
defp validate_args!([_, _, _]), do: :ok
|
|
|
|
defp validate_args!(_) do
|
|
raise_with_help("Invalid arguments")
|
|
end
|
|
|
|
defp validate_required_dependencies! do
|
|
unless Code.ensure_loaded?(Ecto.Adapters.SQL) do
|
|
raise_with_help("mix phx.gen.auth requires ecto_sql", :phx_generator_args)
|
|
end
|
|
|
|
if generated_with_no_html?() do
|
|
raise_with_help("mix phx.gen.auth requires phoenix_html", :phx_generator_args)
|
|
end
|
|
end
|
|
|
|
defp generated_with_no_html? do
|
|
Mix.Project.config()
|
|
|> Keyword.get(:deps, [])
|
|
|> Enum.any?(fn
|
|
{:phoenix_html, _} -> true
|
|
{:phoenix_html, _, _} -> true
|
|
_ -> false
|
|
end)
|
|
|> Kernel.not()
|
|
end
|
|
|
|
defp build_hashing_library!(opts) do
|
|
opts
|
|
|> Keyword.get_lazy(:hashing_lib, &default_hashing_library_option/0)
|
|
|> HashingLibrary.build()
|
|
|> case do
|
|
{:ok, hashing_library} ->
|
|
hashing_library
|
|
|
|
{:error, {:unknown_library, unknown_library}} ->
|
|
raise_with_help(
|
|
"Unknown value for --hashing-lib #{inspect(unknown_library)}",
|
|
:hashing_lib
|
|
)
|
|
end
|
|
end
|
|
|
|
defp default_hashing_library_option do
|
|
case :os.type() do
|
|
{:unix, _} -> "bcrypt"
|
|
{:win32, _} -> "pbkdf2"
|
|
end
|
|
end
|
|
|
|
defp prompt_for_conflicts(context) do
|
|
context
|
|
|> files_to_be_generated()
|
|
|> Mix.Phoenix.prompt_for_conflicts()
|
|
end
|
|
|
|
defp files_to_be_generated(%Context{schema: schema, context_app: context_app} = context) do
|
|
singular = schema.singular
|
|
web_pre = Mix.Phoenix.web_path(context_app)
|
|
web_test_pre = Mix.Phoenix.web_test_path(context_app)
|
|
migrations_pre = Mix.Phoenix.context_app_path(context_app, "priv/repo/migrations")
|
|
web_path = to_string(schema.web_path)
|
|
controller_pre = Path.join([web_pre, "controllers", web_path])
|
|
|
|
default_files = [
|
|
"migration.ex": [migrations_pre, "#{timestamp()}_create_#{schema.table}_auth_tables.exs"],
|
|
"notifier.ex": [context.dir, "#{singular}_notifier.ex"],
|
|
"schema.ex": [context.dir, "#{singular}.ex"],
|
|
"schema_token.ex": [context.dir, "#{singular}_token.ex"],
|
|
"auth.ex": [web_pre, web_path, "#{singular}_auth.ex"],
|
|
"auth_test.exs": [web_test_pre, web_path, "#{singular}_auth_test.exs"],
|
|
"session_controller.ex": [controller_pre, "#{singular}_session_controller.ex"],
|
|
"session_controller_test.exs": [
|
|
web_test_pre,
|
|
"controllers",
|
|
web_path,
|
|
"#{singular}_session_controller_test.exs"
|
|
]
|
|
]
|
|
|
|
case Keyword.fetch(context.opts, :live) do
|
|
{:ok, true} ->
|
|
live_files = [
|
|
"registration_live.ex": [web_pre, "live", web_path, "#{singular}_registration_live.ex"],
|
|
"registration_live_test.exs": [
|
|
web_test_pre,
|
|
"live",
|
|
web_path,
|
|
"#{singular}_registration_live_test.exs"
|
|
],
|
|
"login_live.ex": [web_pre, "live", web_path, "#{singular}_login_live.ex"],
|
|
"login_live_test.exs": [
|
|
web_test_pre,
|
|
"live",
|
|
web_path,
|
|
"#{singular}_login_live_test.exs"
|
|
],
|
|
"reset_password_live.ex": [
|
|
web_pre,
|
|
"live",
|
|
web_path,
|
|
"#{singular}_reset_password_live.ex"
|
|
],
|
|
"reset_password_live_test.exs": [
|
|
web_test_pre,
|
|
"live",
|
|
web_path,
|
|
"#{singular}_reset_password_live_test.exs"
|
|
],
|
|
"forgot_password_live.ex": [
|
|
web_pre,
|
|
"live",
|
|
web_path,
|
|
"#{singular}_forgot_password_live.ex"
|
|
],
|
|
"forgot_password_live_test.exs": [
|
|
web_test_pre,
|
|
"live",
|
|
web_path,
|
|
"#{singular}_forgot_password_live_test.exs"
|
|
],
|
|
"settings_live.ex": [web_pre, "live", web_path, "#{singular}_settings_live.ex"],
|
|
"settings_live_test.exs": [
|
|
web_test_pre,
|
|
"live",
|
|
web_path,
|
|
"#{singular}_settings_live_test.exs"
|
|
],
|
|
"confirmation_live.ex": [web_pre, "live", web_path, "#{singular}_confirmation_live.ex"],
|
|
"confirmation_live_test.exs": [
|
|
web_test_pre,
|
|
"live",
|
|
web_path,
|
|
"#{singular}_confirmation_live_test.exs"
|
|
],
|
|
"confirmation_instructions_live.ex": [
|
|
web_pre,
|
|
"live",
|
|
web_path,
|
|
"#{singular}_confirmation_instructions_live.ex"
|
|
],
|
|
"confirmation_instructions_live_test.exs": [
|
|
web_test_pre,
|
|
"live",
|
|
web_path,
|
|
"#{singular}_confirmation_instructions_live_test.exs"
|
|
]
|
|
]
|
|
|
|
remap_files(default_files ++ live_files)
|
|
|
|
_ ->
|
|
non_live_files = [
|
|
"confirmation_html.ex": [controller_pre, "#{singular}_confirmation_html.ex"],
|
|
"confirmation_new.html.heex": [
|
|
controller_pre,
|
|
"#{singular}_confirmation_html",
|
|
"new.html.heex"
|
|
],
|
|
"confirmation_edit.html.heex": [
|
|
controller_pre,
|
|
"#{singular}_confirmation_html",
|
|
"edit.html.heex"
|
|
],
|
|
"confirmation_controller.ex": [controller_pre, "#{singular}_confirmation_controller.ex"],
|
|
"confirmation_controller_test.exs": [
|
|
web_test_pre,
|
|
"controllers",
|
|
web_path,
|
|
"#{singular}_confirmation_controller_test.exs"
|
|
],
|
|
"registration_new.html.heex": [
|
|
controller_pre,
|
|
"#{singular}_registration_html",
|
|
"new.html.heex"
|
|
],
|
|
"registration_controller.ex": [controller_pre, "#{singular}_registration_controller.ex"],
|
|
"registration_controller_test.exs": [
|
|
web_test_pre,
|
|
"controllers",
|
|
web_path,
|
|
"#{singular}_registration_controller_test.exs"
|
|
],
|
|
"registration_html.ex": [controller_pre, "#{singular}_registration_html.ex"],
|
|
"reset_password_html.ex": [controller_pre, "#{singular}_reset_password_html.ex"],
|
|
"reset_password_controller.ex": [
|
|
controller_pre,
|
|
"#{singular}_reset_password_controller.ex"
|
|
],
|
|
"reset_password_controller_test.exs": [
|
|
web_test_pre,
|
|
"controllers",
|
|
web_path,
|
|
"#{singular}_reset_password_controller_test.exs"
|
|
],
|
|
"reset_password_edit.html.heex": [
|
|
controller_pre,
|
|
"#{singular}_reset_password_html",
|
|
"edit.html.heex"
|
|
],
|
|
"reset_password_new.html.heex": [
|
|
controller_pre,
|
|
"#{singular}_reset_password_html",
|
|
"new.html.heex"
|
|
],
|
|
"session_html.ex": [controller_pre, "#{singular}_session_html.ex"],
|
|
"session_new.html.heex": [controller_pre, "#{singular}_session_html", "new.html.heex"],
|
|
"settings_html.ex": [web_pre, "controllers", web_path, "#{singular}_settings_html.ex"],
|
|
"settings_controller.ex": [controller_pre, "#{singular}_settings_controller.ex"],
|
|
"settings_edit.html.heex": [
|
|
controller_pre,
|
|
"#{singular}_settings_html",
|
|
"edit.html.heex"
|
|
],
|
|
"settings_controller_test.exs": [
|
|
web_test_pre,
|
|
"controllers",
|
|
web_path,
|
|
"#{singular}_settings_controller_test.exs"
|
|
]
|
|
]
|
|
|
|
remap_files(default_files ++ non_live_files)
|
|
end
|
|
end
|
|
|
|
defp remap_files(files) do
|
|
for {source, dest} <- files, do: {:eex, to_string(source), Path.join(dest)}
|
|
end
|
|
|
|
defp copy_new_files(%Context{} = context, binding, paths) do
|
|
files = files_to_be_generated(context)
|
|
Mix.Phoenix.copy_from(paths, "priv/templates/phx.gen.auth", binding, files)
|
|
inject_context_functions(context, paths, binding)
|
|
inject_tests(context, paths, binding)
|
|
inject_context_test_fixtures(context, paths, binding)
|
|
|
|
context
|
|
end
|
|
|
|
defp inject_context_functions(%Context{file: file} = context, paths, binding) do
|
|
Gen.Context.ensure_context_file_exists(context, paths, binding)
|
|
|
|
paths
|
|
|> Mix.Phoenix.eval_from("priv/templates/phx.gen.auth/context_functions.ex", binding)
|
|
|> prepend_newline()
|
|
|> inject_before_final_end(file)
|
|
end
|
|
|
|
defp inject_tests(%Context{test_file: test_file} = context, paths, binding) do
|
|
Gen.Context.ensure_test_file_exists(context, paths, binding)
|
|
|
|
paths
|
|
|> Mix.Phoenix.eval_from("priv/templates/phx.gen.auth/test_cases.exs", binding)
|
|
|> prepend_newline()
|
|
|> inject_before_final_end(test_file)
|
|
end
|
|
|
|
defp inject_context_test_fixtures(
|
|
%Context{test_fixtures_file: test_fixtures_file} = context,
|
|
paths,
|
|
binding
|
|
) do
|
|
Gen.Context.ensure_test_fixtures_file_exists(context, paths, binding)
|
|
|
|
paths
|
|
|> Mix.Phoenix.eval_from("priv/templates/phx.gen.auth/context_fixtures_functions.ex", binding)
|
|
|> prepend_newline()
|
|
|> inject_before_final_end(test_fixtures_file)
|
|
end
|
|
|
|
defp inject_conn_case_helpers(%Context{} = context, paths, binding) do
|
|
test_file = "test/support/conn_case.ex"
|
|
|
|
paths
|
|
|> Mix.Phoenix.eval_from("priv/templates/phx.gen.auth/conn_case.exs", binding)
|
|
|> inject_before_final_end(test_file)
|
|
|
|
context
|
|
end
|
|
|
|
defp inject_routes(%Context{context_app: ctx_app} = context, paths, binding) do
|
|
web_prefix = Mix.Phoenix.web_path(ctx_app)
|
|
file_path = Path.join(web_prefix, "router.ex")
|
|
|
|
paths
|
|
|> Mix.Phoenix.eval_from("priv/templates/phx.gen.auth/routes.ex", binding)
|
|
|> inject_before_final_end(file_path)
|
|
|
|
context
|
|
end
|
|
|
|
defp maybe_inject_mix_dependency(%Context{context_app: ctx_app} = context, %HashingLibrary{
|
|
mix_dependency: mix_dependency
|
|
}) do
|
|
file_path = Mix.Phoenix.context_app_path(ctx_app, "mix.exs")
|
|
|
|
file = File.read!(file_path)
|
|
|
|
case Injector.mix_dependency_inject(file, mix_dependency) do
|
|
{:ok, new_file} ->
|
|
print_injecting(file_path)
|
|
File.write!(file_path, new_file)
|
|
|
|
:already_injected ->
|
|
:ok
|
|
|
|
{:error, :unable_to_inject} ->
|
|
Mix.shell().info("""
|
|
|
|
Add your #{mix_dependency} dependency to #{file_path}:
|
|
|
|
defp deps do
|
|
[
|
|
#{mix_dependency},
|
|
...
|
|
]
|
|
end
|
|
""")
|
|
end
|
|
|
|
context
|
|
end
|
|
|
|
defp maybe_inject_router_import(%Context{context_app: ctx_app} = context, binding) do
|
|
web_prefix = Mix.Phoenix.web_path(ctx_app)
|
|
file_path = Path.join(web_prefix, "router.ex")
|
|
auth_module = Keyword.fetch!(binding, :auth_module)
|
|
inject = "import #{inspect(auth_module)}"
|
|
use_line = "use #{inspect(context.web_module)}, :router"
|
|
|
|
help_text = """
|
|
Add your #{inspect(auth_module)} import to #{Path.relative_to_cwd(file_path)}:
|
|
|
|
defmodule #{inspect(context.web_module)}.Router do
|
|
#{use_line}
|
|
|
|
# Import authentication plugs
|
|
#{inject}
|
|
|
|
...
|
|
end
|
|
"""
|
|
|
|
with {:ok, file} <- read_file(file_path),
|
|
{:ok, new_file} <-
|
|
Injector.inject_unless_contains(
|
|
file,
|
|
inject,
|
|
&String.replace(&1, use_line, "#{use_line}\n\n #{&2}")
|
|
) do
|
|
print_injecting(file_path, " - imports")
|
|
File.write!(file_path, new_file)
|
|
else
|
|
:already_injected ->
|
|
:ok
|
|
|
|
{:error, :unable_to_inject} ->
|
|
Mix.shell().info("""
|
|
|
|
#{help_text}
|
|
""")
|
|
|
|
{:error, {:file_read_error, _}} ->
|
|
print_injecting(file_path)
|
|
print_unable_to_read_file_error(file_path, help_text)
|
|
end
|
|
|
|
context
|
|
end
|
|
|
|
defp maybe_inject_router_plug(%Context{context_app: ctx_app} = context) do
|
|
web_prefix = Mix.Phoenix.web_path(ctx_app)
|
|
file_path = Path.join(web_prefix, "router.ex")
|
|
help_text = Injector.router_plug_help_text(file_path, context)
|
|
|
|
with {:ok, file} <- read_file(file_path),
|
|
{:ok, new_file} <- Injector.router_plug_inject(file, context) do
|
|
print_injecting(file_path, " - plug")
|
|
File.write!(file_path, new_file)
|
|
else
|
|
:already_injected ->
|
|
:ok
|
|
|
|
{:error, :unable_to_inject} ->
|
|
Mix.shell().info("""
|
|
|
|
#{help_text}
|
|
""")
|
|
|
|
{:error, {:file_read_error, _}} ->
|
|
print_injecting(file_path)
|
|
print_unable_to_read_file_error(file_path, help_text)
|
|
end
|
|
|
|
context
|
|
end
|
|
|
|
defp maybe_inject_app_layout_menu(%Context{} = context) do
|
|
schema = context.schema
|
|
|
|
if file_path = get_layout_html_path(context) do
|
|
case Injector.app_layout_menu_inject(schema, File.read!(file_path)) do
|
|
{:ok, new_content} ->
|
|
print_injecting(file_path)
|
|
File.write!(file_path, new_content)
|
|
|
|
:already_injected ->
|
|
:ok
|
|
|
|
{:error, :unable_to_inject} ->
|
|
Mix.shell().info("""
|
|
|
|
#{Injector.app_layout_menu_help_text(file_path, schema)}
|
|
""")
|
|
end
|
|
else
|
|
{_dup, inject} = Injector.app_layout_menu_code_to_inject(schema)
|
|
|
|
missing =
|
|
context
|
|
|> potential_layout_file_paths()
|
|
|> Enum.map_join("\n", &" * #{&1}")
|
|
|
|
Mix.shell().error("""
|
|
|
|
Unable to find an application layout file to inject user menu items.
|
|
|
|
Missing files:
|
|
|
|
#{missing}
|
|
|
|
Please ensure this phoenix app was not generated with
|
|
--no-html. If you have changed the name of your application
|
|
layout file, please add the following code to it where you'd
|
|
like the #{schema.singular} menu items to be rendered.
|
|
|
|
#{inject}
|
|
""")
|
|
end
|
|
|
|
context
|
|
end
|
|
|
|
defp get_layout_html_path(%Context{} = context) do
|
|
context
|
|
|> potential_layout_file_paths()
|
|
|> Enum.find(&File.exists?/1)
|
|
end
|
|
|
|
defp potential_layout_file_paths(%Context{context_app: ctx_app}) do
|
|
web_prefix = Mix.Phoenix.web_path(ctx_app)
|
|
|
|
for file_name <- ~w(root.html.heex app.html.heex) do
|
|
Path.join([web_prefix, "components", "layouts", file_name])
|
|
end
|
|
end
|
|
|
|
defp inject_config(context, %HashingLibrary{} = hashing_library) do
|
|
file_path =
|
|
if Mix.Phoenix.in_umbrella?(File.cwd!()) do
|
|
Path.expand("../../")
|
|
else
|
|
File.cwd!()
|
|
end
|
|
|> Path.join("config/test.exs")
|
|
|
|
file =
|
|
case read_file(file_path) do
|
|
{:ok, file} -> file
|
|
{:error, {:file_read_error, _}} -> "use Mix.Config\n"
|
|
end
|
|
|
|
case Injector.test_config_inject(file, hashing_library) do
|
|
{:ok, new_file} ->
|
|
print_injecting(file_path)
|
|
File.write!(file_path, new_file)
|
|
|
|
:already_injected ->
|
|
:ok
|
|
|
|
{:error, :unable_to_inject} ->
|
|
help_text = Injector.test_config_help_text(file_path, hashing_library)
|
|
|
|
Mix.shell().info("""
|
|
|
|
#{help_text}
|
|
""")
|
|
end
|
|
|
|
context
|
|
end
|
|
|
|
defp print_shell_instructions(%Context{} = context) do
|
|
Mix.shell().info("""
|
|
|
|
Please re-fetch your dependencies with the following command:
|
|
|
|
$ mix deps.get
|
|
|
|
Remember to update your repository by running migrations:
|
|
|
|
$ mix ecto.migrate
|
|
|
|
Once you are ready, visit "/#{context.schema.plural}/register"
|
|
to create your account and then access "/dev/mailbox" to
|
|
see the account confirmation email.
|
|
""")
|
|
|
|
context
|
|
end
|
|
|
|
defp router_scope(%Context{schema: schema} = context) do
|
|
prefix = Module.concat(context.web_module, schema.web_namespace)
|
|
|
|
if schema.web_namespace do
|
|
~s|"/#{schema.web_path}", #{inspect(prefix)}, as: :#{schema.web_path}|
|
|
else
|
|
~s|"/", #{inspect(context.web_module)}|
|
|
end
|
|
end
|
|
|
|
defp web_path_prefix(%Schema{web_path: nil}), do: ""
|
|
defp web_path_prefix(%Schema{web_path: web_path}), do: "/" <> web_path
|
|
|
|
# The paths to look for template files for generators.
|
|
#
|
|
# Defaults to checking the current app's `priv` directory,
|
|
# and falls back to phx_gen_auth's `priv` directory.
|
|
defp generator_paths do
|
|
[".", :phoenix]
|
|
end
|
|
|
|
defp inject_before_final_end(content_to_inject, file_path) do
|
|
with {:ok, file} <- read_file(file_path),
|
|
{:ok, new_file} <- Injector.inject_before_final_end(file, content_to_inject) do
|
|
print_injecting(file_path)
|
|
File.write!(file_path, new_file)
|
|
else
|
|
:already_injected ->
|
|
:ok
|
|
|
|
{:error, {:file_read_error, _}} ->
|
|
print_injecting(file_path)
|
|
|
|
print_unable_to_read_file_error(
|
|
file_path,
|
|
"""
|
|
|
|
Please add the following to the end of your equivalent
|
|
#{Path.relative_to_cwd(file_path)} module:
|
|
|
|
#{indent_spaces(content_to_inject, 2)}
|
|
"""
|
|
)
|
|
end
|
|
end
|
|
|
|
defp read_file(file_path) do
|
|
case File.read(file_path) do
|
|
{:ok, file} -> {:ok, file}
|
|
{:error, reason} -> {:error, {:file_read_error, reason}}
|
|
end
|
|
end
|
|
|
|
defp indent_spaces(string, number_of_spaces)
|
|
when is_binary(string) and is_integer(number_of_spaces) do
|
|
indent = String.duplicate(" ", number_of_spaces)
|
|
|
|
string
|
|
|> String.split("\n")
|
|
|> Enum.map_join("\n", &(indent <> &1))
|
|
end
|
|
|
|
defp timestamp do
|
|
{{y, m, d}, {hh, mm, ss}} = :calendar.universal_time()
|
|
"#{y}#{pad(m)}#{pad(d)}#{pad(hh)}#{pad(mm)}#{pad(ss)}"
|
|
end
|
|
|
|
defp pad(i) when i < 10, do: <<?0, ?0 + i>>
|
|
defp pad(i), do: to_string(i)
|
|
|
|
defp prepend_newline(string) when is_binary(string), do: "\n" <> string
|
|
|
|
defp get_ecto_adapter!(%Schema{repo: repo}) do
|
|
if Code.ensure_loaded?(repo) do
|
|
repo.__adapter__()
|
|
else
|
|
Mix.raise("Unable to find #{inspect(repo)}")
|
|
end
|
|
end
|
|
|
|
defp print_injecting(file_path, suffix \\ []) do
|
|
Mix.shell().info([:green, "* injecting ", :reset, Path.relative_to_cwd(file_path), suffix])
|
|
end
|
|
|
|
defp print_unable_to_read_file_error(file_path, help_text) do
|
|
Mix.shell().error(
|
|
"""
|
|
|
|
Unable to read file #{Path.relative_to_cwd(file_path)}.
|
|
|
|
#{help_text}
|
|
"""
|
|
|> indent_spaces(2)
|
|
)
|
|
end
|
|
|
|
@doc false
|
|
def raise_with_help(msg) do
|
|
raise_with_help(msg, :general)
|
|
end
|
|
|
|
defp raise_with_help(msg, :general) do
|
|
Mix.raise("""
|
|
#{msg}
|
|
|
|
mix phx.gen.auth expects a context module name, followed by
|
|
the schema module and its plural name (used as the schema
|
|
table name).
|
|
|
|
For example:
|
|
|
|
mix phx.gen.auth Accounts User users
|
|
|
|
The context serves as the API boundary for the given resource.
|
|
Multiple resources may belong to a context and a resource may be
|
|
split over distinct contexts (such as Accounts.User and Payments.User).
|
|
""")
|
|
end
|
|
|
|
defp raise_with_help(msg, :phx_generator_args) do
|
|
Mix.raise("""
|
|
#{msg}
|
|
|
|
mix phx.gen.auth must be installed into a Phoenix 1.5 app that
|
|
contains ecto and html templates.
|
|
|
|
mix phx.new my_app
|
|
mix phx.new my_app --umbrella
|
|
mix phx.new my_app --database mysql
|
|
|
|
Apps generated with --no-ecto or --no-html are not supported.
|
|
""")
|
|
end
|
|
|
|
defp raise_with_help(msg, :hashing_lib) do
|
|
Mix.raise("""
|
|
#{msg}
|
|
|
|
mix phx.gen.auth supports the following values for --hashing-lib
|
|
|
|
* bcrypt
|
|
* pbkdf2
|
|
* argon2
|
|
|
|
Visit https://github.com/riverrun/comeonin for more information
|
|
on choosing a library.
|
|
""")
|
|
end
|
|
|
|
defp test_case_options(Ecto.Adapters.Postgres), do: ", async: true"
|
|
defp test_case_options(adapter) when is_atom(adapter), do: ""
|
|
|
|
defp put_live_option(schema) do
|
|
opts =
|
|
case Keyword.fetch(schema.opts, :live) do
|
|
{:ok, _live?} ->
|
|
schema.opts
|
|
|
|
_ ->
|
|
Mix.shell().info("""
|
|
An authentication system can be created in two different ways:
|
|
- Using Phoenix.LiveView (default)
|
|
- Using Phoenix.Controller only\
|
|
""")
|
|
|
|
if Mix.shell().yes?("Do you want to create a LiveView based authentication system?") do
|
|
Keyword.put_new(schema.opts, :live, true)
|
|
else
|
|
Keyword.put_new(schema.opts, :live, false)
|
|
end
|
|
end
|
|
|
|
Map.put(schema, :opts, opts)
|
|
end
|
|
end
|