Skip to content

Commit

Permalink
some fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
zoedsoupe committed Oct 26, 2024
1 parent c017d33 commit 787395b
Show file tree
Hide file tree
Showing 9 changed files with 171 additions and 219 deletions.
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
defmodule Arcane.Profiles do
import Ecto.Query

alias Arcane.Profiles.Profile
alias Arcane.Repo

def get_profile(id: id) do
Repo.get(Profile, id)
end

def upsert_profile(attrs) do
changeset = Profile.changeset(%Profile{}, attrs)
def create_profile(user_id: user_id) do
changeset = Profile.changeset(%Profile{}, %{id: user_id})
Repo.insert(changeset, on_conflict: :nothing, conflict_target: [:id])
end

def update_profile(%{"id" => profile_id} = attrs) do
changeset = Profile.update_changeset(attrs)

if changeset.valid? do
updated_at = NaiveDateTime.utc_now()
changes = [{:updated_at, updated_at} | Map.to_list(changeset.changes)]
q = from p in Profile, where: p.id == ^profile_id, select: p

Repo.insert(changeset,
on_conflict: {:replace_all_except, [:id]},
conflict_target: :id
)
case Repo.update_all(q, set: changes) do
{1, [profile]} -> {:ok, profile}
_ -> {:error, :failed_to_update_profile}
end
else
{:error, changeset}
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,11 @@ defmodule Arcane.Profiles.Profile do
|> unique_constraint(:username)
|> foreign_key_constraint(:id)
end

def update_changeset(%{} = params) do
%__MODULE__{}
|> cast(params, [:username, :website, :avatar_url])
|> validate_length(:username, min: 3)
|> validate_length(:website, max: 255)
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -6,45 +6,32 @@ defmodule ArcaneWeb.Components do
use ArcaneWeb, :verified_routes
use Phoenix.Component

alias Phoenix.LiveView.JS

attr :field, Phoenix.HTML.FormField
attr :src, :string
attr :upload, Phoenix.LiveView.UploadConfig, required: true
attr :size, :integer
attr :uploading?, :boolean, default: false

def avatar(%{size: size} = assigns) do
assigns =
assigns
|> Map.put(:height, "#{size}em")
|> Map.put(:width, "#{size}em")
size_str = "height: #{size}em; width: #{size}em;"
assigns = assign(assigns, size: size_str)

~H"""
<div>
<img
:if={@src}
id="avatar-preview"
phx-hook="LivePreview"
src={@src}
<.live_img_preview
:for={entry <- @upload.entries}
entry={entry}
alt="Avatar"
class="avatar-image"
style={[height: @height, width: @width]}
style={@size}
/>
<div :if={is_nil(@src)} class="avatar no-image" style={[height: @height, width: @width]} />
<div :if={@upload.entries == []} class="avatar no-image" style={@size} />
<div style="width: 10em; position: relative;">
<label class="button primary block" for="single">
<%= if @uploading?, do: "Uploading...", else: "Upload" %>
<div style="width: 10em; position: relative; decoration: none;">
<label class="button primary block" for={@upload.ref}>
Upload
</label>
<input
style="position: absolute; visibility: hidden;"
type="file"
<.live_file_input
upload={@upload}
id="single"
accept="image/*"
name={@field.name}
id={@field.id}
value={@field.value}
disabled={@uploading?}
style="position: absolute; visibility: hidden;"
/>
</div>
</div>
Expand Down Expand Up @@ -80,27 +67,16 @@ defmodule ArcaneWeb.Components do
end

attr :form, Phoenix.HTML.Form, required: true
attr :avatar, :string
attr :"trigger-signout", :boolean, default: false

@doc """
We actually need 2 different forms as the first one will keep track of
the profile update data and emit LiveView events and the second one will submit an HTTP request
`DELETE /session` to log out the current user (aka delete session cookies)
"""
def account(assigns) do
~H"""
<.form
for={@form}
class="form-widget"
phx-submit="update-profile"
phx-change="upload-profile"
action={~p"/session"}
phx-trigger-action={Map.get(assigns, :"trigger-signout", false)}
method="delete"
>
<!-- <.avatar src={@avatar} field={@form[:avatar]} size={10} /> -->
<input type="text"
hidden
name={@form[:id].name}
id={@form[:id].id}
value={@form[:id].value}
/>
<.form for={@form} class="form-widget" phx-submit="update-profile" phx-change="upload-profile">
<input type="text" hidden name={@form[:id].name} id={@form[:id].id} value={@form[:id].value} />
<div>
<label for="email">Email</label>
<input
Expand Down Expand Up @@ -135,117 +111,14 @@ defmodule ArcaneWeb.Components do
Update
</button>
</div>
</.form>
<.form for={%{}} action={~p"/session"} method="delete">
<div>
<button type="button" class="button block" phx-click="sign-out">
<button type="submit" class="button block">
Sign Out
</button>
</div>
</.form>
"""
end

@doc """
Renders flash notices.
## Examples
<.flash kind={:info} flash={@flash} />
<.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!</.flash>
"""
attr :id, :string, doc: "the optional id of flash container"
attr :flash, :map, default: %{}, doc: "the map of flash messages to display"
attr :title, :string, default: nil
attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup"
attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container"

slot :inner_block, doc: "the optional inner block that renders the flash message"

def flash(assigns) do
assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end)

~H"""
<div
:if={msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind)}
id={@id}
phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("##{@id}")}
role="alert"
class={[
"flash-container",
@kind == :info && "flash-info",
@kind == :error && "flash-error"
]}
{@rest}
>
<p :if={@title} class="flash-title">
<%= @title %>
</p>
<p class="flash-message"><%= msg %></p>
<button type="button" class="flash-close-button">
X
</button>
</div>
"""
end

@doc """
Shows the flash group with standard titles and content.
## Examples
<.flash_group flash={@flash} />
"""
attr :flash, :map, required: true, doc: "the map of flash messages"
attr :id, :string, default: "flash-group", doc: "the optional id of flash container"

def flash_group(assigns) do
~H"""
<div id={@id} class="flash-group-container">
<.flash kind={:info} title="Success!" flash={@flash} />
<.flash kind={:error} title="Error!" flash={@flash} />
<.flash
id="client-error"
kind={:error}
title="We can't find the internet!"
phx-disconnected={show(".phx-client-error #client-error")}
phx-connected={hide("#client-error")}
class="hidden"
>
Attempting to reconnect...
</.flash>
<.flash
id="server-error"
kind={:error}
title="Something went wrong!"
phx-disconnected={show(".phx-server-error #server-error")}
phx-connected={hide("#server-error")}
class="hidden"
>
Hang in there while we get back on track
</.flash>
</div>
"""
end

def show(js \\ %JS{}, selector) do
JS.show(js,
to: selector,
time: 300,
transition:
{"transition-all ease-out duration-300",
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
"opacity-100 translate-y-0 sm:scale-100"}
)
end

def hide(js \\ %JS{}, selector) do
JS.hide(js,
to: selector,
time: 200,
transition:
{"transition-all ease-in duration-200", "opacity-100 translate-y-0 sm:scale-100",
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"}
)
end
end
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<!-- This is the template that all live views will use -->
<main>
<.flash_group flash={@flash} />
<%= @inner_content %>
</main>
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,19 @@ defmodule ArcaneWeb.SessionController do
import ArcaneWeb.Auth
import Phoenix.LiveView.Controller

alias Arcane.Profiles
alias ArcaneWeb.UserManagementLive
alias Supabase.GoTrue

require Logger

@doc """
THis function is responsible to process the log in request and send tbe
magic link via Supabase/GoTrue
Note that we do `live_render` since there's no state to mantain between
controller and the live view itself (that will do authentication checks).
"""
def create(conn, %{"email" => email}) do
params = %{
email: email,
Expand All @@ -22,65 +30,67 @@ defmodule ArcaneWeb.SessionController do

case GoTrue.sign_in_with_otp(client, params) do
:ok ->
message = "Check your email for the login link!"

conn
|> put_flash(:success, message)
|> live_render(UserManagementLive)
live_render(conn, UserManagementLive)

{:error, error} ->
Logger.error("""
[#{__MODULE__}] => Failed to login user:
ERROR: #{inspect(error, pretty: true)}
""")

message = "Failed to send login link!"

conn
|> put_flash(:error, message)
|> live_render(UserManagementLive)
live_render(conn, UserManagementLive)
end
end

def confirm(conn, %{"token" => token, "type" => "magiclink"}) do
@doc """
Once the user clicks the email link that they'll receive, the link will redirect
to the `/session/confirm` route defined on `ArcaneWeb.Router` and will trigger
this function.
So we create an empty Profile for this user, so the `UserManagementLive` can
correctly show informations about the profile.
Note also that we put the token into the session, as configured in the `ArcaneWeb.Endpoint`
it will set up session cookies to store authentication information locally.
Finally, we redirect back the user to the root page, that will redenr `UserManagementLive`
live view. We could use `live_render`, but it would need to pass all the state and session
mannually to the live view, which is unecessary here since it will happen automatically on
`mount` of the live view.
"""
def confirm(conn, %{"token_hash" => token_hash, "type" => "magiclink"}) do
{:ok, client} = Arcane.Supabase.Client.get_client()

params = %{
token_hash: token,
token_hash: token_hash,
type: :magiclink
}

case GoTrue.verify_otp(client, params) do
{:ok, session} ->
conn
|> put_token_in_session(session.access_token)
|> live_render(UserManagementLive,
session: %{
"user_token" => session.access_token,
"live_socket_id" => get_session(conn, :live_socket_id)
}
)
with {:ok, session} <- GoTrue.verify_otp(client, params),
{:ok, user} <- GoTrue.get_user(client, session) do
Profiles.create_profile(user_id: user.id)

conn
|> put_token_in_session(session.access_token)
|> redirect(to: ~p"/")
else
{:error, error} ->
Logger.error("""
[#{__MODULE__}] => Failed to verify OTP:
ERROR: #{inspect(error, pretty: true)}
""")

message = "Failed to verify login link!"

conn
|> put_flash(:error, message)
|> live_render(UserManagementLive)
redirect(conn, to: ~p"/")
end
end

@doc """
This function clears the local session, which includes the session cookie, so the user
will need to authenticate again on the application.
"""
def signout(conn, _params) do
message = "You have been signed out!"

conn
|> log_out_user(:local)
|> put_flash(:info, message)
|> live_render(UserManagementLive)
end
end
Loading

0 comments on commit 787395b

Please sign in to comment.