some ai generated code from claude that does not work

This commit is contained in:
2026-04-08 02:03:43 -04:00
parent 3c9910a723
commit 90d7eab7d0
19 changed files with 1206 additions and 53 deletions

View File

@@ -15,4 +15,9 @@ defmodule MixerWeb.AuthOverrides do
set :text, "⬡ Mixer"
set :text_class, "text-3xl font-bold tracking-tight"
end
# Inject the username field into the password registration form
override AshAuthentication.Phoenix.Components.Password do
set :register_extra_component, &MixerWeb.AuthComponents.username_register_field/1
end
end

View File

@@ -0,0 +1,51 @@
defmodule MixerWeb.AuthComponents do
@moduledoc """
Extra components injected into AshAuthentication.Phoenix forms.
"""
use Phoenix.Component
@doc """
Renders a username input field inside the password registration form.
Receives `form` (an `AshPhoenix.Form`) as an assign via the
`register_extra_component` override.
"""
def username_register_field(assigns) do
field = assigns.form[:username]
assigns =
assigns
|> assign(:field_id, field.id)
|> assign(:field_name, field.name)
|> assign(:field_value, field.value || "")
|> assign(:field_errors, field.errors)
~H"""
<div class="mt-2 mb-2">
<label for={@field_id} class="block text-sm font-medium text-base-content mb-1">
Username
</label>
<div class="flex">
<span class="input rounded-r-none border-r-0 text-base-content/50 select-none">@</span>
<input
type="text"
id={@field_id}
name={@field_name}
value={@field_value}
class={"input w-full rounded-l-none #{if @field_errors != [], do: "input-error", else: ""}"}
placeholder="your_handle"
autocomplete="username"
required
/>
</div>
<p :if={@field_errors != []} class="mt-1 text-xs text-error">
{@field_errors |> List.first() |> elem(0)}
</p>
<p :if={@field_errors == []} class="mt-1 text-xs text-base-content/50">
330 characters · letters, numbers, underscores
</p>
</div>
"""
end
end

View File

@@ -2,6 +2,11 @@
id="app"
data-current-user-id={if @current_user, do: @current_user.id, else: ""}
data-current-user-email={if @current_user, do: @current_user.email, else: ""}
data-current-user-username={if @current_user, do: @current_user.username || "", else: ""}
data-current-user-display-name={
if @current_user, do: @current_user.display_name || "", else: ""
}
data-current-user-avatar-url={if @current_user, do: @current_user.avatar_url || "", else: ""}
data-asset-host={@media_host}
data-page={@page}
data-tweet-id={@tweet_id || ""}

View File

@@ -2,6 +2,7 @@ defmodule MixerWeb.UploadController do
use MixerWeb, :controller
alias Mixer.Posts.MediaUploader
alias Mixer.Accounts.AvatarUploader
def create(conn, %{"file" => %Plug.Upload{} = upload}) do
actor = conn.assigns[:current_user]
@@ -46,4 +47,48 @@ defmodule MixerWeb.UploadController do
|> put_status(:bad_request)
|> json(%{error: "no file provided"})
end
# ── Avatar upload ──────────────────────────────────────────────────────────
def upload_avatar(conn, %{"file" => %Plug.Upload{} = upload}) do
actor = conn.assigns[:current_user]
unless actor do
conn
|> put_status(:unauthorized)
|> json(%{error: "authentication required"})
else
scope = %{user_id: actor.id}
case AvatarUploader.store({upload, scope}) do
{:ok, _file_name} ->
# The thumb is always stored as avatars/:user_id/thumb.webp
thumb_key = "avatars/#{actor.id}/thumb.webp"
actor
|> Ash.Changeset.for_update(:update_avatar, %{avatar_url: thumb_key}, actor: actor)
|> Ash.update()
|> case do
{:ok, _user} ->
json(conn, %{success: true, avatarUrl: thumb_key})
{:error, error} ->
conn
|> put_status(:unprocessable_entity)
|> json(%{success: false, error: inspect(error)})
end
{:error, reason} ->
conn
|> put_status(:unprocessable_entity)
|> json(%{success: false, error: reason})
end
end
end
def upload_avatar(conn, _params) do
conn
|> put_status(:bad_request)
|> json(%{error: "no file provided"})
end
end

View File

@@ -0,0 +1,184 @@
defmodule MixerWeb.MagicSignInLive do
@moduledoc """
Custom magic-link sign-in LiveView that collects a username for new users.
When a user clicks their magic link, this page is shown instead of the
default auto-submit. If the user is brand new (no account) or has no
username set yet, we ask them to choose one before completing sign-in.
"""
use AshAuthentication.Phoenix.Overrides.Overridable,
root_class: "CSS class for the root `div` element.",
magic_sign_in_id: "Element ID for the `MagicSignIn` LiveComponent."
use AshAuthentication.Phoenix.Web, :live_view
alias AshAuthentication.Info
alias AshPhoenix.Form
alias Phoenix.LiveView.{Rendered, Socket}
import AshAuthentication.Phoenix.Components.Helpers, only: [auth_path: 5]
import PhoenixHTMLHelpers.Form, only: [hidden_input: 3, submit: 2]
import Slug
@doc false
@impl true
def mount(params, session, socket) do
overrides =
session
|> Map.get("overrides", [AshAuthentication.Phoenix.Overrides.Default])
resource = session["resource"]
strategy_name = session["strategy"]
token = params["token"] || params["magic_link"]
strategy = Info.strategy!(resource, strategy_name)
subject_name = Info.authentication_subject_name!(resource)
domain = Info.authentication_domain!(resource)
# Determine whether this user needs to pick a username
needs_username? = needs_username?(token, resource)
form =
resource
|> Form.for_action(strategy.sign_in_action_name,
domain: domain,
as: subject_name |> to_string(),
id: "#{subject_name}-#{strategy_name}-sign-in-form" |> slugify(),
context: %{strategy: strategy, private: %{ash_authentication?: true}}
)
socket =
socket
|> assign(overrides: overrides)
|> assign(:token, token)
|> assign(:strategy, strategy)
|> assign(:subject_name, subject_name)
|> assign(:resource, resource)
|> assign(:needs_username?, needs_username?)
|> assign(:form, form)
|> assign(:trigger_action, false)
|> assign(:current_tenant, session["tenant"])
|> assign(:auth_routes_prefix, session["auth_routes_prefix"])
{:ok, socket}
end
@doc false
@impl true
@spec handle_params(map, String.t(), Socket.t()) :: {:noreply, Socket.t()}
def handle_params(_params, _uri, socket), do: {:noreply, socket}
@doc false
@impl true
@spec render(Socket.assigns()) :: Rendered.t()
def render(assigns) do
~H"""
<div class={override_for(@overrides, :root_class)}>
<.live_component
module={AshAuthentication.Phoenix.Components.Banner}
id="magic-sign-in-banner"
overrides={@overrides}
/>
<div style="max-width: 400px; margin: 0 auto; padding: 1rem;">
<.form
:let={form}
for={@form}
phx-submit="submit"
phx-trigger-action={@trigger_action}
action={
auth_path(
@socket,
@subject_name,
@auth_routes_prefix,
@strategy,
:sign_in
)
}
method="POST"
>
{hidden_input(form, :token, value: @token)}
<%!-- Username field — only shown for new or username-less users --%>
<div :if={@needs_username?} class="mt-2 mb-4">
<label
for={form[:username].id}
class="block text-sm font-medium text-base-content mb-1"
>
Choose a username
</label>
<div class="flex">
<span class="input rounded-r-none border-r-0 text-base-content/50 select-none">
@
</span>
<input
type="text"
id={form[:username].id}
name={form[:username].name}
value={form[:username].value || ""}
class={"input w-full rounded-l-none #{if form[:username].errors != [], do: "input-error", else: ""}"}
placeholder="your_handle"
autocomplete="username"
required
/>
</div>
<p
:if={form[:username].errors != []}
class="mt-1 text-xs text-error"
>
{form[:username].errors |> List.first() |> elem(0)}
</p>
<p :if={form[:username].errors == []} class="mt-1 text-xs text-base-content/50">
330 characters · letters, numbers, underscores
</p>
</div>
{submit("Sign in",
class: "btn btn-primary w-full mt-2",
phx_disable_with: "Signing in…"
)}
</.form>
</div>
</div>
"""
end
@doc false
@impl true
@spec handle_event(String.t(), map(), Socket.t()) :: {:noreply, Socket.t()}
def handle_event("submit", params, socket) do
subject_name =
socket.assigns.subject_name
|> to_string()
|> slugify()
form_params = Map.get(params, subject_name, %{})
form = Form.validate(socket.assigns.form, form_params)
socket =
socket
|> assign(:form, form)
|> assign(:trigger_action, form.valid?)
{:noreply, socket}
end
# ── Helpers ──────────────────────────────────────────────────────────────────
# Returns true if the user is new or has no username set yet.
defp needs_username?(nil, _resource), do: true
defp needs_username?(token, resource) do
with {:ok, claims} <- AshAuthentication.Jwt.peek(token),
subject when is_binary(subject) <- Map.get(claims, "sub"),
{:ok, user} <- AshAuthentication.subject_to_user(subject, resource) do
is_nil(user.username)
else
_ ->
# Unknown / new user — ask for username to be safe
true
end
end
end

View File

@@ -47,6 +47,7 @@ defmodule MixerWeb.Router do
post "/rpc/run", AshTypescriptRpcController, :run
post "/rpc/validate", AshTypescriptRpcController, :validate
post "/upload", UploadController, :create
post "/upload/avatar", UploadController, :upload_avatar
auth_routes AuthController, Mixer.Accounts.User, path: "/auth"
sign_out_route AuthController
@@ -74,6 +75,7 @@ defmodule MixerWeb.Router do
# Remove this if you do not use the magic link strategy.
magic_sign_in_route(Mixer.Accounts.User, :magic_link,
live_view: MixerWeb.MagicSignInLive,
auth_routes_prefix: "/auth",
overrides: [MixerWeb.AuthOverrides, Elixir.AshAuthentication.Phoenix.Overrides.DaisyUI]
)