Packages

WebSocket-first Elixir framework with auto-generated HTTP MCP APIs

Current section

Files

Jump to
dialup lib mix tasks dialup.gen.auth.ex
Raw

lib/mix/tasks/dialup.gen.auth.ex

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