defmodule Mix.Tasks.Coh.Install do
@moduledoc """
Configure the Coherence User Model for your Phoenix application. Coherence
is composed of a number of modules that can be enabled with this installer.
This installer will normally do the following unless given an option not to do so:
* Append the :coherence configuration to your `config/config.exs` file.
* Generate appropriate migration files.
* Generate appropriate view files.
* Generate appropriate template files.
* Generate a `web/coherence_web.ex` file.
* Generate a `web/coherence_messages.ex` file.
* Generate a `web/models/user.ex` file if one does not already exist.
## Install Examples
# Install with only the `authenticatable` option
mix coh.install
# Install all the options except `confirmable` and `invitable`
mix coh.install --full
# Install all the options except `invitable`
mix coh.install --full-confirmable
# Install all the options except `confirmable`
mix coh.install --full-invitable
# Install the `full` options except `lockable` and `trackable`
mix coh.install --full --no-lockable --no-trackable
## Reinstall Examples
# Reinstall with defaults (--silent --no-migrations --no-config --confirm-once)
mix coh.install --reinstall
# Confirm to overwrite files, show instructions, and generate migrations
mix coh.install --reinstall --no-confirm-once --with-migrations
## Option list
A Coherence configuration will be appended to your `config/config.exs` file unless
the `--no-config` option is given.
A `--model="SomeModule tablename"` option can be given to override the default User module.
A `--repo=CustomRepo` option can be given to override the default Repo module
A `--router=CustomRouter` option can be given to override the default Router module
A `--web-path="lib/my_project/web"` option can be given to specify the web path
A `--default` option will include only `authenticatable`
A `--full` option will include options `authenticatable`, `recoverable`, `lockable`, `trackable`, `unlockable_with_token`, `registerable`
A `--full-confirmable` option will include the `--full` options in addition to the `--confirmable` option
A `--full-invitable` option will include the `--full` options in addition to the `--invitable` option
An `--authenticatable` option provides authentication support to your User model.
A `--recoverable` option provides the ability to request a password reset email.
A `--lockable` option provides login locking after too many failed login attempts.
An `--unlockable-with-token` option provides the ability to request an unlock email.
A `--trackable` option provides login count, current login timestamp, current login ip, last login timestamp, last login ip in your User model.
A `--trackable-table` option provides `trackable` fields in the `trackables` table.
A `--confirmable` option provides support for confirmation email before the account can be logged in.
An `--invitable` option provides support for invitation emails, allowing the new user to create their account including password creation.
A `--registerable` option provides support for new users to register for an account
A `--rememberable` option provides a remember me? check box for persistent logins
A `--migration-path` option to set the migration path
A `--module` option to override the module
A `--web-module` option to override the web module
A `--installed-options` option to list the previous install options
A `--reinstall` option to reinstall the coherence boilerplate based on your existing configuration options
A `--silent` option to disable printing instructions
A `--confirm-once` option to only confirm overwriting existing files once
A `--with-migrations` option to reinstall migrations. only valid for --reinstall option
A `--layout` (false) generate layout template and view
A `--user-active-field` (false) add active field to user schema and disable logins when set to false.
A `--password-hashing-alg` (Comeonin.Bcrypt) add a different password hashing algorithm
## Disable Options
* `--no-config` -- Don't append to your `config/config.exs` file.
* `--no-web` -- Don't create the `coherence_web.ex` file.
* `--no-messages` -- Don't create the `coherence_messages.ex` file.
* `--no-views` -- Don't create the `web/views/coherence/` files.
* `--no-migrations` -- Don't create the migration files.
* `--no-templates` -- Don't create the `web/templates/coherence` files.
* `--no-boilerplate` -- Don't create any of the boilerplate files.
* `--no-models` -- Don't generate the model file.
* `--no-confirm` -- Don't confirm overwriting files.
"""
use Mix.Task
import Macro, only: [camelize: 1, underscore: 1]
import Mix.Generator
import Mix.Ecto
import Coherence.Mix.Utils
@shortdoc "Configure the Coherence Package"
@all_options ~w(authenticatable recoverable lockable trackable trackable_table rememberable) ++
~w(unlockable_with_token confirmable invitable registerable)
@all_options_atoms Enum.map(@all_options, &String.to_atom(&1))
@default_options ~w(authenticatable)
@full_options @all_options -- ~w(confirmable invitable rememberable trackable_table)
@full_confirmable @all_options -- ~w(invitable rememberable trackable_table)
@full_invitable @all_options -- ~w(confirmable rememberable trackable_table)
# the options that default to true, and can be disabled with --no-option
@default_booleans ~w(config web messages views migrations templates models emails boilerplate confirm)
# all boolean_options
@boolean_options @default_booleans ++
~w(default full full_confirmable full_invitable) ++ @all_options
# options that will set use_email? to true
@email_options Enum.map(
~w(recoverable unlockable_with_token confirmable invitable),
&String.to_atom(&1)
)
@config_file "config/config.exs"
@config_marker_start "%% Coherence Configuration %%"
@config_marker_end "%% End Coherence Configuration %%"
@switches [
user: :string,
repo: :string,
migration_path: :string,
model: :string,
log_only: :boolean,
confirm_once: :boolean,
module: :string,
installed_options: :boolean,
reinstall: :boolean,
silent: :boolean,
with_migrations: :boolean,
router: :string,
web_path: :string,
web_module: :string,
binary_id: :boolean,
layout: :boolean,
user_active_field: :boolean,
password_hashing_alg: :string
] ++ Enum.map(@boolean_options, &{String.to_atom(&1), :boolean})
@switch_names Enum.map(@switches, &elem(&1, 0))
@new_user_migration_fields ["add :name, :string", "add :email, :string"]
@new_user_constraints ["create unique_index(:users, [:email])"]
@spec run(command_line_args :: [binary]) :: any
def run(args) do
{opts, parsed, unknown} = OptionParser.parse(args, switches: @switches)
verify_deprecated!(opts)
verify_args!(parsed, unknown)
{bin_opts, opts} = parse_options(opts)
opts
|> do_config(bin_opts)
|> do_run
end
defp do_run(%{reinstall: true} = config) do
["--no-config"]
|> check_confirm_once(config)
|> check_silent(config)
|> check_migrations(config)
|> get_config_options()
|> run
end
defp do_run(%{installed_options: true} = config) do
print_installed_options(config)
end
defp do_run(%{confirm_once: true} = config) do
if Mix.shell().yes?("Are you sure you want overwrite any existing files?") do
config
|> Map.put(:confirm, false)
|> Map.delete(:confirm_once)
|> do_run
end
end
defp do_run(%{with_migrations: true}),
do: Mix.raise("--with-migrations only valid with --reinstall")
defp do_run(config) do
config
|> validate_project_structure
|> get_existing_model
|> gen_coherence_config
|> gen_migration
|> gen_model
|> gen_layout_template
|> gen_invitable_migration
|> gen_rememberable_migration
|> gen_trackable_migration
|> gen_invitable_schema
|> gen_rememberable_schema
|> gen_trackable_schema
|> gen_schemas_module
|> gen_coherence_web
|> gen_coherence_messages
|> gen_coherence_views
|> gen_coherence_templates
|> gen_coherence_mailer
|> gen_redirects
|> gen_responders
# work around for config file not getting recompiled
|> touch_config
|> print_instructions
end
defp validate_project_structure(%{web_path: web_path} = config) do
case File.lstat(web_path) do
{:ok, %{type: :directory}} ->
config
_ ->
if Mix.shell().yes?(
"Cannot find web path #{web_path}. Are you sure you want to continue?"
) do
config
else
Mix.raise("Cannot find web path #{web_path}")
end
end
end
defp check_confirm(options, %{confirm: true}), do: options
defp check_confirm(options, _), do: ["--no-confirm" | options]
defp check_confirm_once(options, %{confirm_once: false} = config),
do: check_confirm(options, config)
defp check_confirm_once(options, _), do: ["--confirm-once" | options]
defp check_silent(options, %{silent: false}), do: options
defp check_silent(options, _), do: ["--silent" | options]
defp check_migrations(options, %{with_migrations: true}), do: options
defp check_migrations(options, _), do: ["--no-migrations" | options]
defp gen_coherence_config(config) do
from_email =
if config[:use_email?] do
~s| email_from_name: "Your Name",\n| <> ~s| email_from_email: "yourname@example.com",\n|
else
""
end
config_block = """
# #{@config_marker_start} Don't remove this line
config :coherence,
user_schema: #{config[:user_schema]},
repo: #{config[:repo]},
module: #{config[:base]},
web_module: #{config[:web_base]},
router: #{config[:router]},
password_hashing_alg: #{config[:password_hashing_alg]},
messages_backend: #{config[:web_base]}.Coherence.Messages,#{layout_field(config)}
registration_permitted_attributes: ["email","name","password","current_password","password_confirmation"],
invitation_permitted_attributes: ["name","email"],
password_reset_permitted_attributes: ["reset_password_token","password","password_confirmation"],
session_permitted_attributes: ["remember","email","password"],
"""
(config_block <> from_email <> " opts: #{inspect(config[:opts])}\n")
|> swoosh_config(config)
|> add_end_marker
|> write_config(config)
|> log_config
end
defp layout_field(%{layout: true} = config),
do: ~s(\n layout: {#{config.web_base}.Coherence.LayoutView, "app.html"},)
defp layout_field(_),
do: ""
# defp user_active_field(%{user_active_field?: true}),
# do: "\n user_active_field: true,"
# defp user_active_field(_),
# do: ""
defp swoosh_config(string, %{web_base: web_base, use_email?: true}) do
string <>
"\n" <>
"""
config :coherence, #{web_base}.Coherence.Mailer,
adapter: Swoosh.Adapters.Sendgrid,
api_key: "your api key here"
"""
end
defp swoosh_config(string, _), do: string
defp add_end_marker(string) do
string <> "# #{@config_marker_end}\n"
end
defp write_config(string, %{config: true, confirm: confirm?} = config) do
log_config? =
if File.exists?(@config_file) do
source = File.read!(@config_file)
confirmed =
if String.contains?(source, @config_marker_start) do
confirm? &&
Mix.shell().yes?(
"Your config file already contains Coherence configuration. Are you sure you want to add another?"
)
else
true
end
if confirmed do
# File.write!(@config_file, source <> "\n" <> string)
File.write!(@config_file, format_string!(source <> "\n" <> string))
shell_info(config, "Your config/config.exs file was updated.")
false
else
shell_info(config, "Configuration was not added!")
true
end
else
shell_info(config, "Could not find #{@config_file}. Configuration was not added!")
true
end
Enum.into([config_string: string, log_config?: log_config?], config)
end
defp write_config(string, config),
do: Enum.into([log_config?: true, config_string: string], config)
defp shell_info(%{silent: true} = config, _message), do: config
defp shell_info(config, message) do
Mix.shell().info(message)
config
end
defp log_config(%{log_config?: false} = config) do
save_instructions(config, "")
end
defp log_config(%{config_string: string} = config) do
verb = if config[:log_config] == :appended, do: "has been", else: "should be"
instructions = """
The following #{verb} added to your #{@config_file} file.
"""
save_instructions(config, instructions <> string)
end
defp touch_config(config) do
File.touch(@config_file)
config
end
defp module_to_string(module) when is_atom(module) do
module
|> Module.split()
|> Enum.reverse()
|> hd
|> to_string
end
defp module_to_string(module) when is_binary(module) do
module
|> String.split(".")
|> Enum.reverse()
|> hd
end
################
# Models
defp get_existing_model(%{user_schema: _} = config) do
config
|> get_compiled_model
|> find_existing_model(lib_path())
end
defp get_existing_model(config), do: config
defp get_compiled_model(%{user_schema: user_schema} = config) do
user_schema = Module.concat(user_schema, nil)
Map.put(config, :model_found?, ensure_compiled?(user_schema))
end
defp ensure_compiled?(module) do
case Code.ensure_compiled(module) do
{:module, _} -> true
{:error, _} -> false
end
end
def find_existing_model(%{model_found?: false, user_schema: user_schema} = config, path) do
user_schema = Module.concat(user_schema, nil)
model =
user_schema
|> Module.split()
|> List.last()
[path, "**", "user.ex"]
|> Path.join()
|> Path.wildcard()
|> model_file_and_module(model)
|> set_user_schema(config)
end
def find_existing_model(config, _path), do: config
defp model_file_and_module(files, model) do
Enum.reduce(files, [], fn fname, acc ->
case File.read(fname) do
{:ok, contents} ->
case Regex.run(~r/defmodule\s*(.+\.#{model}) /, contents) do
nil -> acc
[_, module] -> [{contents, module} | acc]
end
{:error, _} ->
acc
end
end)
end
defp set_user_schema([], config), do: config
defp set_user_schema([{contents, module} | list], config) do
case Regex.run(~r/schema\s+"(.*)"/, contents) do
nil ->
set_user_schema(list, config)
[_, table_name] ->
config
|> Map.put(:model_found?, true)
|> Map.put(:user_schema, module)
|> Map.put(:user_table_name, table_name)
end
end
defp gen_model(
%{
user_schema: user_schema,
boilerplate: true,
models: true,
model_found?: false,
web_path: web_path
} = config
) do
name =
user_schema
|> String.split(".")
|> List.last()
|> String.downcase()
binding = Kernel.binding() ++ [user_table_name: config[:user_table_name]] ++ config.binding
copy_from(
paths(),
"priv/templates/coh.install/models/coherence",
"",
binding,
[
{:eex, "user.ex", Path.join([lib_path(), "coherence", "#{name}.ex"])}
],
config
)
config
end
defp gen_model(config), do: config
################
# Migrations
defp create_or_alter_model(config, name) do
table_name = config[:user_table_name]
# user_schema = Module.concat user_schema, nil
if config[:model_found?] do
{:alter, "add_coherence_to_#{name}", [], []}
else
fields =
Enum.map(
@new_user_migration_fields,
&String.replace(&1, ":users", ":#{table_name}")
)
constraints =
Enum.map(
@new_user_constraints,
&String.replace(&1, ":users", ":#{table_name}")
)
{:create, "create_coherence_#{name}", fields, constraints}
end
end
# defp model_exists?(model, path) do
# end
defp add_timestamp(acc, %{model_found?: false}), do: acc ++ ["", "timestamps()"]
defp add_timestamp(acc, _), do: acc
defp get_field_list(initial_fields, config) do
schema_fields = schema_fields(config)
Enum.reduce(config[:opts], initial_fields, fn opt, acc ->
case schema_fields[opt] do
nil -> acc
list -> acc ++ list
end
end)
end
defp gen_migration(%{migrations: true, boilerplate: true} = config) do
table_name = config[:user_table_name]
name =
config[:user_schema]
|> module_to_string
|> String.downcase()
{verb, migration_name, initial_fields, constraints} = create_or_alter_model(config, name)
do_gen_migration(config, migration_name, fn repo, _path, file, name ->
field_list = get_field_list(initial_fields, config)
adds =
field_list
|> add_timestamp(config)
|> Enum.map(&(" " <> &1))
|> Enum.join("\n")
constraints =
constraints
|> Enum.map(&(" " <> &1))
|> Enum.join("\n")
statement =
case verb do
:alter -> "#{verb} table(:#{table_name}) do"
:create -> gen_table_statement(table_name)
end
change = """
#{statement}
#{adds}
end
#{constraints}
"""
assigns = [mod: Module.concat([repo, Migrations, camelize(name)]), change: change]
create_file(file, migration_template(assigns))
end)
end
defp gen_migration(config), do: config
defp gen_invitable_migration(%{invitable: true, migrations: true, boilerplate: true} = config) do
do_gen_migration(config, "create_coherence_invitable", fn repo, _path, file, name ->
change = """
#{gen_table_statement(:invitations)}
add :name, :string
add :email, :string
add :token, :string
timestamps()
end
create unique_index(:invitations, [:email])
create index(:invitations, [:token])
"""
assigns = [mod: Module.concat([repo, Migrations, camelize(name)]), change: change]
create_file(file, migration_template(assigns))
end)
end
defp gen_invitable_migration(config), do: config
defp gen_rememberable_migration(
%{rememberable: true, migrations: true, boilerplate: true} = config
) do
table_name = config[:user_table_name]
do_gen_migration(config, "create_coherence_rememberable", fn repo, _path, file, name ->
change = """
#{gen_table_statement(:rememberables)}
add :series_hash, :string
add :token_hash, :string
add :token_created_at, :utc_datetime
add :user_id, #{gen_reference(table_name)}
timestamps()
end
create index(:rememberables, [:user_id])
create index(:rememberables, [:series_hash])
create index(:rememberables, [:token_hash])
create unique_index(:rememberables, [:user_id, :series_hash, :token_hash])
"""
assigns = [mod: Module.concat([repo, Migrations, camelize(name)]), change: change]
create_file(file, migration_template(assigns))
end)
end
defp gen_rememberable_migration(config), do: config
defp gen_trackable_migration(
%{trackable_table: true, migrations: true, boilerplate: true} = config
) do
table_name = config[:user_table_name]
do_gen_migration(config, "create_coherence_trackable", fn repo, _path, file, name ->
change = """
#{gen_table_statement(:trackables)}
add :action, :string
add :sign_in_count, :integer, default: 0
add :current_sign_in_at, :utc_datetime
add :last_sign_in_at, :utc_datetime
add :current_sign_in_ip, :string
add :last_sign_in_ip, :string
add :user_id, #{gen_reference(table_name)}
timestamps()
end
create index(:trackables, [:user_id])
create index(:trackables, [:action])
"""
assigns = [mod: Module.concat([repo, Migrations, camelize(name)]), change: change]
create_file(file, migration_template(assigns))
end)
end
defp gen_trackable_migration(config), do: config
defp do_gen_migration(%{timestamp: current_timestamp} = config, name, fun) do
repo =
config[:repo]
|> String.split(".")
|> Module.concat()
ensure_repo(repo, [])
path =
case config[:migration_path] do
path when is_binary(path) ->
path
_ ->
Path.relative_to(migrations_path(repo), Mix.Project.app_path())
end
file = Path.join(path, "#{current_timestamp}_#{underscore(name)}.exs")
fun.(repo, path, file, name)
Map.put(config, :timestamp, current_timestamp + 1)
end
defp gen_table_statement(table_name) do
if use_binary_id?() do
"""
create table(:#{table_name}, primary_key: false) do
add :id, :binary_id, primary_key: true
"""
else
"""
create table(:#{table_name}) do
"""
end
end
defp gen_reference(table_name) do
type_hint = if use_binary_id?(), do: ", type: :binary_id", else: ""
"references(:#{table_name}, on_delete: :delete_all#{type_hint})"
end
################
# Schema
defp gen_trackable_schema(%{trackable_table: true, boilerplate: true, models: true} = config) do
gen_schema_schema(config, "trackable.ex")
end
defp gen_trackable_schema(config), do: config
defp gen_invitable_schema(%{invitable: true, boilerplate: true, models: true} = config) do
gen_schema_schema(config, "invitation.ex")
end
defp gen_invitable_schema(config), do: config
defp gen_rememberable_schema(%{rememberable: true, boilerplate: true, models: true} = config) do
gen_schema_schema(config, "rememberable.ex")
end
defp gen_rememberable_schema(config), do: config
def gen_schema_schema(config, file_name) do
copy_from(
paths(),
"priv/templates/coh.install/models/coherence",
lib_path("coherence"),
config.binding,
[
{:eex, file_name, file_name}
],
config
)
config
end
def gen_schemas_module(%{boilerplate: true, models: true} = config) do
schema_list =
[
{Invitation, config[:invitable]},
{Rememberable, config[:rememberable]},
{Trackable, config[:trackable_table]}
]
|> Enum.filter(&elem(&1, 1))
|> Enum.map(&Module.concat([config.base, Coherence, elem(&1, 0)]))
binding = [
{:schema_list, schema_list |> inspect},
{:trackable?, config[:trackable_table]} | config.binding
]
copy_from(
paths(),
"priv/templates/coh.install/models/coherence",
lib_path("coherence"),
binding,
[
{:eex, "schemas.ex", "schemas.ex"}
],
config
)
config
end
def gen_schemas_module(config), do: config
################
# Web
defp gen_coherence_web(
%{web: true, boilerplate: true, binding: binding, web_path: web_path} = config
) do
copy_from(
paths(),
"priv/templates/coh.install",
"",
binding,
[
{:eex, "coherence_web.ex", Path.join(web_path, "coherence_web.ex")}
],
config
)
config
end
defp gen_coherence_web(config), do: config
################
# Messages
defp gen_coherence_messages(
%{messages: true, boilerplate: true, binding: binding, web_path: web_path} = config
) do
copy_from(
paths(),
"priv/templates/coh.install",
"",
binding,
[
{:eex, "coherence_messages.ex", Path.join(web_path, "coherence_messages.ex")}
],
config
)
config
end
defp gen_coherence_messages(config), do: config
defp gen_redirects(%{boilerplate: true, binding: binding, web_path: web_path} = config) do
copy_from(
paths(),
"priv/templates/coh.install/controllers/coherence",
"",
binding,
[
{:eex, "redirects.ex", Path.join(web_path, "controllers/coherence/redirects.ex")}
],
config
)
config
end
defp gen_redirects(config), do: config
defp gen_responders(%{boilerplate: true, binding: binding, web_path: web_path} = config) do
copy_from(
paths(),
"priv/templates/coh.install/controllers/coherence/responders",
"",
binding,
[
{:eex, "html.ex", Path.join(web_path, "controllers/coherence/responders/html.ex")},
{:eex, "json.ex", Path.join(web_path, "controllers/coherence/responders/json.ex")}
],
config
)
config
end
defp gen_responders(config), do: config
################
# Views
@view_files [
all: "coherence_view.ex",
confirmable: "confirmation_view.ex",
use_email?: "email_view.ex",
invitable: "invitation_view.ex",
all: "layout_view.ex",
all: "coherence_view_helpers.ex",
recoverable: "password_view.ex",
registerable: "registration_view.ex",
authenticatable: "session_view.ex",
unlockable_with_token: "unlock_view.ex"
]
def view_files, do: @view_files
def gen_coherence_views(
%{views: true, boilerplate: true, binding: binding, web_path: web_path} = config
) do
files =
@view_files
|> Enum.filter(&validate_option(config, elem(&1, 0)))
|> Enum.map(&elem(&1, 1))
|> Enum.map(&{:eex, &1, Path.join(web_path, "views/coherence/#{&1}")})
copy_from(
paths(),
"priv/templates/coh.install/views/coherence",
"",
[{:otp_app, Mix.Phoenix.otp_app()} | binding],
files,
config
)
config
end
def gen_coherence_views(config), do: config
@template_files [
email: {:use_email?, ~w(confirmation invitation password unlock)},
invitation: {:invitable, ~w(edit new)},
# layout: {:all, ~w(app email)},
layout: {:all, ~w(email)},
password: {:recoverable, ~w(edit new)},
registration: {:registerable, ~w(new edit form show)},
session: {:authenticatable, ~w(new)},
unlock: {:unlockable_with_token, ~w(new)},
confirmation: {:confirmable, ~w(new)}
]
def template_files, do: @template_files
defp validate_option(_, :all), do: true
defp validate_option(%{use_email?: true}, :use_email?), do: true
defp validate_option(%{opts: opts}, opt) do
if opt in opts, do: true, else: false
end
################
# Templates
def gen_coherence_templates(%{templates: true, boilerplate: true, binding: binding} = config) do
for {name, {opt, files}} <- @template_files do
if validate_option(config, opt), do: copy_templates(binding, name, files, config)
end
config
end
def gen_coherence_templates(config), do: config
def gen_layout_template(%{layout: true, templates: true, boilerplate: true} = config) do
copy_templates(config.binding, :layout, ["app"], config)
config
end
def gen_layout_template(config), do: config
defp copy_templates(binding, name, file_list, %{web_path: web_path} = config) do
files =
for fname <- file_list do
fname = "#{fname}.html.eex"
{:eex, fname, Path.join(web_path, "templates/coherence/#{name}/#{fname}")}
end
copy_from(
paths(),
"priv/templates/coh.install/templates/coherence/#{name}",
"",
binding,
files,
config
)
end
################
# Mailer
defp gen_coherence_mailer(
%{binding: binding, use_email?: true, boilerplate: true, web_path: web_path} = config
) do
copy_from(
paths(),
"priv/templates/coh.install/emails/coherence",
"",
binding,
[
{:eex, "coherence_mailer.ex",
Path.join(web_path, "emails/coherence/coherence_mailer.ex")},
{:eex, "user_email.ex", Path.join(web_path, "emails/coherence/user_email.ex")}
],
config
)
config
end
defp gen_coherence_mailer(config), do: config
################
# Instructions
defp seeds_instructions(%{repo: repo, user_schema: user_schema, authenticatable: true} = config) do
user_schema = to_string(user_schema)
repo = to_string(repo)
block = """
You might want to add the following to your priv/repo/seeds.exs file.
#{repo}.delete_all(#{user_schema})
%#{user_schema}{}
|> #{user_schema}.changeset(%{
name: "Test User",
email: "testuser@example.com",
password: "secret",
password_confirmation: "secret"
})
|> #{repo}.insert!()
"""
confirm = if config[:confirmable], do: "|> Coherence.Controller.confirm!()\n", else: ""
block <> confirm
end
defp seeds_instructions(_config), do: ""
defp schema_instructions(%{base: base, found_model?: false}),
do: """
Add the following items to your User model (Phoenix v1.2).
defmodule #{base}.User do
use Ecto.Schema
# Add this
use Coherence.Schema
schema "users" do
field :name, :string
field :email, :string
# Add this
coherence_schema()
timestamps()
end
def changeset(model, params \\ %{}) do
model
|> cast(params, [:name, :email] ++ coherence_fields)
|> validate_required([:name, :email])
|> unique_constraint(:email)
# Add this
|> validate_coherence(params)
end
def changeset(model, params, :password) do
model
|> cast(
params,
~w(password password_confirmation reset_password_token reset_password_sent_at)
)
|> validate_coherence_password_reset(params)
end
end
"""
defp schema_instructions(_), do: ""
defp mix_instructions(%{base: base}),
do: """
Add :coherence to your applications list in mix.exs.
def application do
[mod: {#{base}, []},
extra_applications: [..., :coherence]]
end
"""
defp router_instructions(config) do
router = config[:router]
web_base = config[:web_base]
"""
Add the following to your router.ex file.
defmodule #{router} do
use #{web_base}, :router
# Add this
use Coherence.Router
pipeline :browser do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(:fetch_flash)
plug(:protect_from_forgery)
plug(:put_secure_browser_headers)
# Add this
plug(Coherence.Authentication.Session)
end
# Add this block
pipeline :protected do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(:fetch_flash)
plug(:protect_from_forgery)
plug(:put_secure_browser_headers)
plug(Coherence.Authentication.Session, protected: true)
end
# Add this block
scope "/" do
pipe_through(:browser)
coherence_routes()
end
# Add this block
scope "/" do
pipe_through(:protected)
coherence_routes(:protected)
end
scope "/", #{web_base} do
pipe_through(:browser)
get("/", PageController, :index)
# Add public routes below
end
scope "/", #{web_base} do
pipe_through(:protected)
# Add protected routes below
end
end
"""
end
defp migrate_instructions(%{migrations: true, boilerplate: true}) do
"""
Don't forget to run the new migrations and seeds with:
$ mix ecto.setup
"""
end
defp migrate_instructions(_), do: ""
defp print_instructions(%{silent: true} = config), do: config
defp print_instructions(%{instructions: instructions} = config) do
Mix.shell().info(instructions)
Mix.shell().info(mix_instructions(config))
Mix.shell().info(router_instructions(config))
Mix.shell().info(schema_instructions(config))
Mix.shell().info(seeds_instructions(config))
Mix.shell().info(migrate_instructions(config))
config
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
embed_template(:migration, """
defmodule <%= inspect @mod %> do
use Ecto.Migration
def change do
<%= @change %>
end
end
""")
################
# Utilities
defp pad(i) when i < 10, do: <<?0, ?0 + i>>
defp pad(i), do: to_string(i)
defp do_default_config(config, opts) do
@default_booleans
|> list_to_atoms
|> Enum.reduce(config, fn opt, acc ->
Map.put(acc, opt, Keyword.get(opts, opt, true))
end)
end
defp list_to_atoms(list), do: Enum.map(list, &String.to_atom(&1))
defp paths do
[".", :coherence]
end
defp save_instructions(config, instructions) do
update_in(config, [:instructions], &(&1 <> instructions))
end
################
# Installer Configuration
defp do_config(opts, []) do
do_config(opts, list_to_atoms(@default_options))
end
defp do_config(opts, bin_opts) do
binding =
Mix.Project.config()
|> Keyword.fetch!(:app)
|> Atom.to_string()
|> Mix.Phoenix.inflect()
base = opts[:module] || binding[:base]
web_base = opts[:web_module] || base <> "Web"
opts = Keyword.put(opts, :base, base)
repo = opts[:repo] || "#{base}.Repo"
router = opts[:router] || "#{web_base}.Router"
web_path = opts[:web_path] || web_path()
web_module = web_base <> ".Coherence"
password_hashing_alg = opts[:password_hashing_alg] || "Comeonin.Bcrypt"
binding =
binding
|> Keyword.put(:base, base)
|> Keyword.put(:web_base, web_base)
|> Keyword.put(:web_path, web_path)
|> Keyword.put(:web_module, web_module)
|> Keyword.put(:otp_app, Mix.Phoenix.otp_app())
|> Keyword.put(:use_binary_id?, use_binary_id?())
|> Keyword.put(:user_active_field?, opts[:user_active_field])
{user_schema, user_table_name} = parse_model(opts[:model], base, opts)
opts_map = do_bin_opts(bin_opts)
user_email = Enum.any?(bin_opts, &(&1 in @email_options))
the_timestamp = String.to_integer(timestamp())
[
instructions: "",
base: base,
use_email?: user_email,
user_schema: user_schema,
user_table_name: user_table_name,
repo: repo,
router: router,
opts: bin_opts,
binding: binding,
log_only: opts[:log_only],
migration_path: opts[:migration_path],
module: opts[:module],
timestamp: the_timestamp,
installed_options: opts[:installed_options],
confirm: opts[:confirm],
confirm_once: opts[:confirm_once],
reinstall: opts[:reinstall],
silent: opts[:silent],
with_migrations: opts[:with_migrations],
web_path: web_path,
web_base: web_base,
web_module: web_module,
use_binary_id?: binding[:use_binary_id?],
layout: opts[:layout] || false,
user_active_field?: binding[:user_active_field?],
password_hashing_alg: password_hashing_alg
]
|> Enum.into(opts_map)
|> do_default_config(opts)
end
defp do_bin_opts(bin_opts) do
bin_opts
|> Enum.map(&{&1, true})
|> Enum.into(%{})
end
defp parse_model(model, _base, opts) when is_binary(model) do
case String.split(model, " ", trim: true) do
[model, table] ->
{prefix_model(model, opts), String.to_atom(table)}
[_] ->
Mix.raise("""
The mix coh.install --model option expects both singular and plural names. For example:
mix coh.install --model="Account accounts"
""")
end
end
defp parse_model(_, base, _) do
{"#{base}.Coherence.User", :users}
end
defp prefix_model(model, opts) do
module = opts[:module] || opts[:base]
if String.starts_with?(model, module) do
model
else
module <> "." <> model
end
end
defp option_reduce({:default, true}, {acc_bin, acc}),
do: {list_to_atoms(@default_options) ++ acc_bin, acc}
defp option_reduce({:full, true}, {acc_bin, acc}),
do: {list_to_atoms(@full_options) ++ acc_bin, acc}
defp option_reduce({:full_confirmable, true}, {acc_bin, acc}),
do: {list_to_atoms(@full_confirmable) ++ acc_bin, acc}
defp option_reduce({:full_invitable, true}, {acc_bin, acc}),
do: {list_to_atoms(@full_invitable) ++ acc_bin, acc}
defp option_reduce({:trackable_table, true}, {acc_bin, acc}),
do: {[:trackable_table | acc_bin] -- [:trackable], acc}
defp option_reduce({name, true}, {acc_bin, acc}) when name in @all_options_atoms,
do: {[name | acc_bin], acc}
defp option_reduce({name, false}, {acc_bin, acc}) when name in @all_options_atoms,
do: {acc_bin -- [name], acc}
defp option_reduce(opt, {acc_bin, acc}),
do: {acc_bin, [opt | acc]}
defp parse_options(opts) do
{opts_bin, opts} = Enum.reduce(opts, {[], []}, &option_reduce(&1, &2))
opts_bin = Enum.uniq(opts_bin)
opts_names = Enum.map(opts, &elem(&1, 0))
with [] <- Enum.filter(opts_bin, &(not (&1 in @switch_names))),
[] <- Enum.filter(opts_names, &(not (&1 in @switch_names))) do
{opts_bin, opts}
else
list -> raise_option_errors(list)
end
end
def all_options, do: @all_options_atoms
def print_installed_options(_config) do
["mix coh.install"]
|> list_config_options(Application.get_env(:coherence, :opts, []))
|> Enum.reverse()
|> Enum.join(" ")
|> Mix.shell().info
end
def list_config_options(acc, opts) do
Enum.reduce(opts, acc, &config_option/2)
end
def get_config_options([]) do
Mix.raise("""
Could not find coherence configuration.
""")
end
def get_config_options(opts) do
:coherence
|> Application.get_env(:opts, [])
|> get_config_options(opts)
end
def get_config_options([], _opts) do
Mix.raise("""
Could not find coherence configuration for re-installation. Please remove the --reinstall option to do a fresh install.
""")
end
def get_config_options(config_opts, opts) do
Enum.reduce(config_opts, opts, &config_option/2)
end
defp config_option(opt, acc) when is_atom(opt) do
str = opt |> Atom.to_string() |> String.replace("_", "-")
["--" <> str | acc]
end
defp config_option(opt, acc) when is_tuple(opt) do
str = opt |> elem(0) |> Atom.to_string() |> String.replace("_", "-")
["--" <> str | acc]
end
@doc """
Copies files from source dir to target dir
according to the given map.
Files are evaluated against EEx according to
the given binding.
"""
def copy_from(apps, source_dir, target_dir, binding, mapping, config) when is_list(mapping) do
roots = Enum.map(apps, &to_app_source(&1, source_dir))
create_opts = if config[:confirm], do: [], else: [force: true]
for {format, source_file_path, target_file_path} <- mapping do
source =
Enum.find_value(roots, fn root ->
source = Path.join(root, source_file_path)
if File.exists?(source), do: source
end) || raise("could not find #{source_file_path} in any of the sources")
target = Path.join(target_dir, target_file_path)
contents =
case format do
:text -> File.read!(source)
:eex -> EEx.eval_file(source, binding)
end
Mix.Generator.create_file(target, contents, create_opts)
end
end
defp to_app_source(path, source_dir) when is_binary(path),
do: Path.join(path, source_dir)
defp to_app_source(app, source_dir) when is_atom(app),
do: Application.app_dir(app, source_dir)
defp lib_path(path \\ "") do
Path.join(["lib", to_string(Mix.Phoenix.otp_app()), path])
end
defp web_path(path \\ "") do
otp_app = to_string(Mix.Phoenix.otp_app())
Path.join(["lib", otp_app <> "_web", path])
end
defp use_binary_id? do
binary_ids? =
Mix.Phoenix.otp_app()
|> Application.get_env(:generators, [])
|> Keyword.get(:binary_id)
Coherence.Config.use_binary_id?() || binary_ids?
end
defp verify_deprecated!(opts) do
if opts[:controllers] do
Mix.raise("--controllers not supported.
Please use mix coh.gen.controllers instead.")
end
end
end