defmodule Mix.Tasks.Dialup.Gen.Auth do @moduledoc """ Generates authentication scaffolding for a Dialup application. mix dialup.gen.auth Accounts User users ## Options * `--app` — OTP application name (defaults to `Mix.Project` app) * `--force` — overwrite existing files """ use Mix.Task @shortdoc "Generates authentication scaffolding" @switches [app: :string, force: :boolean] @aliases [f: :force] @impl Mix.Task def run(args) do Mix.Task.run("compile") {opts, positional} = OptionParser.parse!(args, strict: @switches, aliases: @aliases) case positional do [context, schema, table] -> generate(context, schema, table, opts) _ -> Mix.raise(""" mix dialup.gen.auth expects: mix dialup.gen.auth Context Schema table Example: mix dialup.gen.auth Accounts User users """) end end defp generate(context, schema, table, opts) do otp_app = opts[:app] || app_name!() app_module = otp_app |> to_string() |> Macro.camelize() context_module = Module.concat([app_module, context]) schema_module = Module.concat([context_module, schema]) force? = Keyword.get(opts, :force, false) bindings = [ otp_app: otp_app, app_module: app_module, context: context, context_module: context_module, context_mod: module_str(context_module), schema: schema, schema_module: schema_module, schema_mod: module_str(schema_module), schema_underscore: Macro.underscore(schema), table: table, migration_timestamp: next_migration_timestamp() ] files = [ {"accounts.ex", "lib/#{otp_app}/#{Macro.underscore(context)}/#{Macro.underscore(context)}.ex"}, {"user.ex", "lib/#{otp_app}/#{Macro.underscore(context)}/#{Macro.underscore(schema)}.ex"}, {"user_token.ex", "lib/#{otp_app}/#{Macro.underscore(context)}/#{Macro.underscore(schema)}_token.ex"}, {"notifier.ex", "lib/#{otp_app}/#{Macro.underscore(context)}/notifier.ex"}, {"migration.ex", "priv/repo/migrations/#{bindings[:migration_timestamp]}_create_#{table}_auth.exs"}, {"log_in_page.ex", "lib/app/log_in/page.ex"}, {"register_page.ex", "lib/app/register/page.ex"}, {"settings_page.ex", "lib/app/settings/page.ex"}, {"reset_password_request_page.ex", "lib/app/reset_password/page.ex"}, {"reset_password_page.ex", "lib/app/reset_password/[token]/page.ex"}, {"auth_snippet.ex", "lib/#{otp_app}_auth_snippet.ex"} ] for {template, dest} <- files do write_template(template, dest, bindings, force?) end Mix.shell().info(""" Add to your app module: use Dialup, auth_accounts: #{inspect(context_module)}, plugs: [{Dialup.Auth.Plug, accounts: #{inspect(context_module)}}] Run migrations and configure your Repo before using auth pages. """) :ok end defp write_template(template, dest, bindings, force?) do if File.exists?(dest) and not force? do Mix.raise("Refusing to overwrite #{dest}. Pass --force to overwrite.") else source = template_path(template) content = EEx.eval_file(source, bindings, trim: true) File.mkdir_p!(Path.dirname(dest)) File.write!(dest, content) Mix.shell().info("* creating #{dest}") end end defp app_name! do Mix.Project.config()[:app] || Mix.raise("Could not determine OTP app. Pass --app APP_NAME.") end defp next_migration_timestamp do migrations_dir = Path.join(File.cwd!(), "priv/repo/migrations") base = utc_timestamp() if File.exists?(migrations_dir) do max_existing = migrations_dir |> File.ls!() |> Enum.filter(&String.match?(&1, ~r/^\d{14}_/)) |> Enum.map(&String.slice(&1, 0, 14)) |> Enum.max(fn -> base end) if max_existing >= base, do: increment_timestamp(max_existing), else: base else base end end defp utc_timestamp do {{y, m, d}, {h, min, s}} = :calendar.universal_time() Enum.map([y, m, d, h, min, s], &pad/1) |> Enum.join() end defp increment_timestamp(ts), do: ts |> String.to_integer() |> Kernel.+(1) |> pad_number() defp pad_number(n), do: n |> Integer.to_string() |> String.pad_leading(14, "0") defp pad(n) when n < 10, do: "0#{n}" defp pad(n), do: to_string(n) defp module_str(module), do: module |> inspect() |> String.replace_prefix("Elixir.", "") defp template_path(name) do priv_dir = case :code.priv_dir(:dialup) do {:error, :bad_name} -> __ENV__.file |> Path.dirname() |> Path.join("../../../priv") |> Path.expand() dir -> dir end Path.join(priv_dir, "templates/dialup.gen.auth/#{name}.eex") end end