2026-02-21 21:40:53 +00:00
|
|
|
defmodule BerrypodWeb.Setup.Recover do
|
|
|
|
|
use BerrypodWeb, :live_view
|
|
|
|
|
|
|
|
|
|
require Logger
|
|
|
|
|
|
|
|
|
|
alias Berrypod.{Accounts, Setup}
|
|
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
|
def mount(_params, _session, socket) do
|
|
|
|
|
cond do
|
|
|
|
|
get_user(socket) ->
|
|
|
|
|
{:ok, push_navigate(socket, to: ~p"/admin")}
|
|
|
|
|
|
|
|
|
|
not Accounts.has_admin?() ->
|
|
|
|
|
{:ok, push_navigate(socket, to: ~p"/setup")}
|
|
|
|
|
|
|
|
|
|
true ->
|
|
|
|
|
Logger.warning("Account recovery requested. Setup secret: #{Setup.setup_secret()}")
|
|
|
|
|
|
|
|
|
|
{:ok,
|
|
|
|
|
socket
|
|
|
|
|
|> assign(:page_title, "Account recovery")
|
|
|
|
|
|> assign(:require_secret, Application.get_env(:berrypod, :env, :dev) == :prod)
|
|
|
|
|
|> assign(:form, to_form(%{}, as: :recover))}
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
defp get_user(socket) do
|
|
|
|
|
case socket.assigns do
|
|
|
|
|
%{current_scope: %{user: user}} when not is_nil(user) -> user
|
|
|
|
|
_ -> nil
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
|
def handle_event("recover", %{"recover" => params}, socket) do
|
|
|
|
|
secret = params["secret"] || ""
|
|
|
|
|
password = params["password"] || ""
|
|
|
|
|
|
|
|
|
|
cond do
|
|
|
|
|
socket.assigns.require_secret and
|
|
|
|
|
not Plug.Crypto.secure_compare(secret, Setup.setup_secret()) ->
|
|
|
|
|
{:noreply, put_flash(socket, :error, "Wrong setup secret")}
|
|
|
|
|
|
|
|
|
|
String.length(password) < 12 ->
|
|
|
|
|
{:noreply, put_flash(socket, :error, "Password must be at least 12 characters")}
|
|
|
|
|
|
|
|
|
|
true ->
|
|
|
|
|
user = Accounts.get_first_admin()
|
|
|
|
|
|
|
|
|
|
case Accounts.update_user_password(user, %{password: password}) do
|
|
|
|
|
{:ok, {_user, _tokens}} ->
|
|
|
|
|
token = Accounts.generate_login_token(user)
|
|
|
|
|
{:noreply, redirect(socket, to: ~p"/recover/login/#{token}")}
|
|
|
|
|
|
|
|
|
|
{:error, changeset} ->
|
|
|
|
|
message =
|
|
|
|
|
changeset
|
|
|
|
|
|> Ecto.Changeset.traverse_errors(fn {msg, _} -> msg end)
|
|
|
|
|
|> Enum.flat_map(fn {_field, msgs} -> msgs end)
|
|
|
|
|
|> Enum.join(", ")
|
|
|
|
|
|
|
|
|
|
{:noreply, put_flash(socket, :error, message)}
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
|
def render(assigns) do
|
|
|
|
|
~H"""
|
|
|
|
|
<Layouts.app flash={@flash} current_scope={@current_scope}>
|
2026-03-04 17:12:21 +00:00
|
|
|
<div class="setup-page">
|
|
|
|
|
<div class="setup-header">
|
|
|
|
|
<.header>
|
|
|
|
|
Account recovery
|
|
|
|
|
<:subtitle>
|
|
|
|
|
Reset your admin password using the setup secret from your server logs.
|
|
|
|
|
</:subtitle>
|
|
|
|
|
</.header>
|
|
|
|
|
</div>
|
2026-02-21 21:40:53 +00:00
|
|
|
|
|
|
|
|
<div class="admin-alert admin-alert-info">
|
|
|
|
|
<.icon name="hero-information-circle" class="size-6 shrink-0" />
|
|
|
|
|
<p>A recovery secret has been printed to your server logs.</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<.form for={@form} phx-submit="recover">
|
|
|
|
|
<.input
|
|
|
|
|
:if={@require_secret}
|
|
|
|
|
name="recover[secret]"
|
|
|
|
|
value=""
|
|
|
|
|
type="password"
|
|
|
|
|
label="Setup secret"
|
|
|
|
|
autocomplete="off"
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
<.input
|
|
|
|
|
name="recover[password]"
|
|
|
|
|
value=""
|
|
|
|
|
type="password"
|
|
|
|
|
label="New password"
|
|
|
|
|
autocomplete="new-password"
|
|
|
|
|
required
|
|
|
|
|
/>
|
2026-03-04 17:12:21 +00:00
|
|
|
<p class="setup-hint">Minimum 12 characters</p>
|
|
|
|
|
<.button variant="primary" class="admin-btn-block">
|
2026-02-21 21:40:53 +00:00
|
|
|
Reset password and log in
|
|
|
|
|
</.button>
|
|
|
|
|
</.form>
|
|
|
|
|
|
2026-03-04 17:12:21 +00:00
|
|
|
<p class="setup-footer">
|
|
|
|
|
<.link navigate={~p"/users/log-in"} class="admin-link">
|
2026-02-21 21:40:53 +00:00
|
|
|
Back to login
|
|
|
|
|
</.link>
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</Layouts.app>
|
|
|
|
|
"""
|
|
|
|
|
end
|
|
|
|
|
end
|