This commit is contained in:
Chris McCord 2021-11-09 06:20:10 -05:00
parent 4f474b6462
commit ff7b064660
13 changed files with 201 additions and 68 deletions

View file

@ -74,6 +74,16 @@ defmodule LiveBeats.Accounts do
Repo.one(query) Repo.one(query)
end end
def change_settings(%User{} = user, attrs) do
User.settings_changeset(user, attrs)
end
def update_settings(%User{} = user, attrs) do
user
|> change_settings(attrs)
|> Repo.update()
end
defp update_github_token(%User{} = user, new_token) do defp update_github_token(%User{} = user, new_token) do
identity = identity =
Repo.one!(from(i in Identity, where: i.user_id == ^user.id and i.provider == "github")) Repo.one!(from(i in Identity, where: i.user_id == ^user.id and i.provider == "github"))

View file

@ -10,6 +10,7 @@ defmodule LiveBeats.Accounts.User do
field :username, :string field :username, :string
field :confirmed_at, :naive_datetime field :confirmed_at, :naive_datetime
field :role, :string, default: "subscriber" field :role, :string, default: "subscriber"
field :profile_tagline, :string
has_many :identities, Identity has_many :identities, Identity
@ -21,16 +22,21 @@ defmodule LiveBeats.Accounts.User do
""" """
def github_registration_changeset(info, primary_email, emails, token) do def github_registration_changeset(info, primary_email, emails, token) do
%{"login" => username} = info %{"login" => username} = info
identity_changeset = Identity.github_registration_changeset(info, primary_email, emails, token)
identity_changeset =
Identity.github_registration_changeset(info, primary_email, emails, token)
if identity_changeset.valid? do if identity_changeset.valid? do
params = %{ params = %{
"username" => username, "username" => username,
"email" => primary_email, "email" => primary_email,
"name" => get_change(identity_changeset, :provider_name), "name" => get_change(identity_changeset, :provider_name)
} }
%User{} %User{}
|> cast(params, [:email, :name, :username]) |> cast(params, [:email, :name, :username])
|> validate_required([:email, :name, :username]) |> validate_required([:email, :name, :username])
|> validate_username()
|> validate_email() |> validate_email()
|> put_assoc(:identities, [identity_changeset]) |> put_assoc(:identities, [identity_changeset])
else else
@ -41,6 +47,13 @@ defmodule LiveBeats.Accounts.User do
end end
end end
def settings_changeset(%User{} = user, params) do
user
|> cast(params, [:username])
|> validate_required([:username])
|> validate_username()
end
defp validate_email(changeset) do defp validate_email(changeset) do
changeset changeset
|> validate_required([:email]) |> validate_required([:email])
@ -49,4 +62,21 @@ defmodule LiveBeats.Accounts.User do
|> unsafe_validate_unique(:email, LiveBeats.Repo) |> unsafe_validate_unique(:email, LiveBeats.Repo)
|> unique_constraint(:email) |> unique_constraint(:email)
end end
defp validate_username(changeset) do
changeset
|> validate_format(:username, ~r/^[a-z0-9_-]{2,32}$/)
|> unsafe_validate_unique(:username, LiveBeats.Repo)
|> unique_constraint(:username)
|> prepare_changes(fn changeset ->
case fetch_change(changeset, :profile_tagline) do
{:ok, _} ->
changeset
:error ->
username = get_field(changeset, :username)
put_change(changeset, :profile_tagline, "#{username}'s beats")
end
end)
end
end end

View file

@ -0,0 +1,11 @@
defmodule LiveBeatsWeb.RedirectController do
use LiveBeatsWeb, :controller
def redirect_authenticated(conn, _) do
if conn.assigns.current_user do
LiveBeatsWeb.UserAuth.redirect_if_user_is_authenticated(conn, [])
else
redirect(conn, to: Routes.sign_in_path(conn, :index))
end
end
end

View file

@ -43,6 +43,7 @@ defmodule LiveBeatsWeb.UserAuth do
""" """
def log_in_user(conn, user) do def log_in_user(conn, user) do
user_return_to = get_session(conn, :user_return_to) user_return_to = get_session(conn, :user_return_to)
conn = assign(conn, :current_user, user)
conn conn
|> renew_session() |> renew_session()
@ -107,7 +108,7 @@ defmodule LiveBeatsWeb.UserAuth do
conn conn
|> put_flash(:error, "You must log in to access this page.") |> put_flash(:error, "You must log in to access this page.")
|> maybe_store_return_to() |> maybe_store_return_to()
|> redirect(to: Routes.home_path(conn, :index)) |> redirect(to: Routes.sign_in_path(conn, :index))
|> halt() |> halt()
end end
end end
@ -134,5 +135,5 @@ defmodule LiveBeatsWeb.UserAuth do
defp maybe_store_return_to(conn), do: conn defp maybe_store_return_to(conn), do: conn
defp signed_in_path(_conn), do: "/" def signed_in_path(conn), do: Routes.song_index_path(conn, :index, conn.assigns.current_user.username)
end end

View file

@ -2,8 +2,13 @@ defmodule LiveBeatsWeb.LiveHelpers do
import Phoenix.LiveView import Phoenix.LiveView
import Phoenix.LiveView.Helpers import Phoenix.LiveView.Helpers
alias LiveBeatsWeb.Router.Helpers, as: Routes
alias Phoenix.LiveView.JS alias Phoenix.LiveView.JS
def home_path(socket) do
Routes.song_index_path(socket, :index, socket.assigns.current_user.username)
end
def spinner(assigns) do def spinner(assigns) do
~H""" ~H"""
<svg class="inline-block animate-spin h-2.5 w-2.5 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <svg class="inline-block animate-spin h-2.5 w-2.5 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">

View file

@ -0,0 +1,93 @@
defmodule LiveBeatsWeb.SettingsLive do
use LiveBeatsWeb, :live_view
alias LiveBeats.Accounts
def render(assigns) do
~H"""
<.title_bar>
Profile Settings
</.title_bar>
<div class="max-w-3xl mx-auto mt-6">
<.form let={f} for={@changeset} phx-change="validate" phx-submit="save" class="space-y-8 divide-y divide-gray-200">
<div class="space-y-8 divide-y divide-gray-200">
<div>
<div>
<p class="mt-1 text-sm text-gray-500">
This information will be displayed publicly so be careful what you share.
</p>
</div>
<div class="mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
<div class="sm:col-span-4">
<label for="username" class="block text-sm font-medium text-gray-700">
Username
</label>
<div class="mt-1 flex rounded-md shadow-sm">
<span class="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 bg-gray-50 text-gray-500 sm:text-sm">
<%= URI.parse(LiveBeatsWeb.Endpoint.url()).host %>/
</span>
<%= text_input f, :username, class: "flex-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full min-w-0 rounded-none rounded-r-md sm:text-sm border-gray-300" %>
<.error field={:username} input_name="user[username]" errors={@changeset.errors} class="pt-2 pl-4 pr-4 ml-2 text-center" />
</div>
</div>
<div class="sm:col-span-4">
<label for="username" class="block text-sm font-medium text-gray-700">
Email (from GitHub)
</label>
<div class="mt-1 flex rounded-md shadow-sm">
<%= text_input f, :email, disabled: true, class: "flex-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full min-w-0 rounded-md sm:text-sm border-gray-300 bg-gray-50" %>
</div>
</div>
<div class="sm:col-span-4">
<label for="about" class="block text-sm font-medium text-gray-700">
Profile Tagline
</label>
<div class="mt-1">
<%= text_input f, :profile_tagline, class: "flex-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full min-w-0 rounded-md sm:text-sm border-gray-300" %>
<.error field={:profile_tagline} input_name="user[profile_tagline]" errors={@changeset.errors} class="pt-2 pl-4 pr-4 ml-2 text-center" />
</div>
<p class="text-sm text-gray-500">Write a short tagline for your beats page.</p>
</div>
</div>
</div>
</div>
<div class="pt-5">
<div class="flex justify-end">
<button type="submit" class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Save
</button>
</div>
</div>
</.form>
</div>
"""
end
def mount(_parmas, _session, socket) do
changeset = Accounts.change_settings(socket.assigns.current_user, %{})
{:ok, assign(socket, changeset: changeset)}
end
def handle_event("validate", %{"user" => params}, socket) do
changeset = Accounts.change_settings(socket.assigns.current_user, params)
{:noreply, assign(socket, changeset: changeset)}
end
def handle_event("save", %{"user" => params}, socket) do
case Accounts.update_settings(socket.assigns.current_user, params) do
{:ok, user} ->
{:noreply,
socket
|> assign(current_user: user)
|> put_flash(:info, "settings updated!")}
{:error, changeset} ->
{:noreply, assign(socket, changeset: changeset)}
end
end
end

View file

@ -130,7 +130,7 @@ defmodule LiveBeatsWeb.SongLive.Index do
LayoutComponent.show_modal(UploadFormComponent, %{ LayoutComponent.show_modal(UploadFormComponent, %{
id: :new, id: :new,
confirm: {"Save", type: "submit", form: "song-form"}, confirm: {"Save", type: "submit", form: "song-form"},
patch_to: Routes.song_index_path(socket, :index), patch_to: home_path(socket),
song: socket.assigns.song, song: socket.assigns.song,
title: socket.assigns.page_title, title: socket.assigns.page_title,
current_user: socket.assigns.current_user current_user: socket.assigns.current_user

View file

@ -30,8 +30,8 @@ defmodule LiveBeatsWeb.SongLive.SongEntryComponent do
class="block w-full border-0 p-0 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-sm"/> class="block w-full border-0 p-0 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-sm"/>
</div> </div>
<div class="col-span-full sm:grid sm:grid-cols-2 sm:gap-2 sm:items-start"> <div class="col-span-full sm:grid sm:grid-cols-2 sm:gap-2 sm:items-start">
<.error input_name={"songs[#{@ref}][title]"} field={:title} errors={@errors}/> <.error input_name={"songs[#{@ref}][title]"} field={:title} errors={@errors} class="-mt-1"/>
<.error input_name={"songs[#{@ref}][artist]"} field={:artist} errors={@errors}/> <.error input_name={"songs[#{@ref}][artist]"} field={:artist} errors={@errors} class="-mt-1"/>
</div> </div>
<div style={"width: #{@progress}%;"} class="col-span-full bg-purple-500 dark:bg-purple-400 h-1.5 w-0 p-0"> <div style={"width: #{@progress}%;"} class="col-span-full bg-purple-500 dark:bg-purple-400 h-1.5 w-0 p-0">
</div> </div>

View file

@ -1,7 +1,8 @@
defmodule LiveBeatsWeb.Router do defmodule LiveBeatsWeb.Router do
use LiveBeatsWeb, :router use LiveBeatsWeb, :router
import LiveBeatsWeb.UserAuth, only: [redirect_if_user_is_authenticated: 2] import LiveBeatsWeb.UserAuth,
only: [fetch_current_user: 2, redirect_if_user_is_authenticated: 2]
pipeline :browser do pipeline :browser do
plug :accepts, ["html"] plug :accepts, ["html"]
@ -10,48 +11,19 @@ defmodule LiveBeatsWeb.Router do
plug :put_root_layout, {LiveBeatsWeb.LayoutView, :root} plug :put_root_layout, {LiveBeatsWeb.LayoutView, :root}
plug :protect_from_forgery plug :protect_from_forgery
plug :put_secure_browser_headers plug :put_secure_browser_headers
plug :fetch_current_user
end end
pipeline :api do pipeline :api do
plug :accepts, ["json"] plug :accepts, ["json"]
end end
scope "/", LiveBeatsWeb do
pipe_through :browser
get "/files/:id", FileController, :show
delete "/signout", OAuthCallbackController, :sign_out
live_session :default, on_mount: [{LiveBeatsWeb.UserAuth, :current_user}, LiveBeatsWeb.Nav] do
live "/signin", SignInLive, :index
end
live_session :authenticated, on_mount: [{LiveBeatsWeb.UserAuth, :ensure_authenticated}, LiveBeatsWeb.Nav] do
live "/", HomeLive, :index
live "/songs", SongLive.Index, :index
live "/songs/new", SongLive.Index, :new
end
end
scope "/", LiveBeatsWeb do scope "/", LiveBeatsWeb do
pipe_through [:browser, :redirect_if_user_is_authenticated] pipe_through [:browser, :redirect_if_user_is_authenticated]
get "/oauth/callbacks/:provider", OAuthCallbackController, :new get "/oauth/callbacks/:provider", OAuthCallbackController, :new
end end
# Other scopes may use custom stacks.
# scope "/api", LiveBeatsWeb do
# pipe_through :api
# end
# Enables LiveDashboard only for development
#
# If you want to use the LiveDashboard in production, you should put
# it behind authentication and allow only admins to access it.
# If your application does not have an admins-only section yet,
# you can use Plug.BasicAuth to set up some basic authentication
# as long as you are also using SSL (which you should anyway).
if Mix.env() in [:dev, :test] do if Mix.env() in [:dev, :test] do
import Phoenix.LiveDashboard.Router import Phoenix.LiveDashboard.Router
@ -61,10 +33,6 @@ defmodule LiveBeatsWeb.Router do
end end
end end
# Enables the Swoosh mailbox preview in development.
#
# Note that preview only shows emails that were sent by the same
# node running the Phoenix server.
if Mix.env() == :dev do if Mix.env() == :dev do
scope "/dev" do scope "/dev" do
pipe_through :browser pipe_through :browser
@ -72,4 +40,24 @@ defmodule LiveBeatsWeb.Router do
forward "/mailbox", Plug.Swoosh.MailboxPreview forward "/mailbox", Plug.Swoosh.MailboxPreview
end end
end end
scope "/", LiveBeatsWeb do
pipe_through :browser
get "/", RedirectController, :redirect_authenticated
get "/files/:id", FileController, :show
delete "/signout", OAuthCallbackController, :sign_out
live_session :default, on_mount: [{LiveBeatsWeb.UserAuth, :current_user}, LiveBeatsWeb.Nav] do
live "/signin", SignInLive, :index
end
live_session :authenticated,
on_mount: [{LiveBeatsWeb.UserAuth, :ensure_authenticated}, LiveBeatsWeb.Nav] do
live "/songs/new", SongLive.Index, :new
live "/:user_id", SongLive.Index, :index
live "/profile/settings", SettingsLive, :edit
end
end
end end

View file

@ -38,18 +38,6 @@
<nav class="px-2"> <nav class="px-2">
<div class="space-y-1"> <div class="space-y-1">
<%= live_redirect to: Routes.home_path(@conn, :index),
class: "bg-gray-100 text-gray-900 group flex items-center px-2 py-2 text-base leading-5 font-medium rounded-md" do %>
<.icon name={:home} outlined class="text-gray-500 mr-3 flex-shrink-0 h-6 w-6"/>
Home
<% end %>
<a href="#"
class="text-gray-600 hover:text-gray-900 hover:bg-gray-50 group flex items-center px-2 py-2 text-base leading-5 font-medium rounded-md">
<.icon name={:music_note} outlined class="text-gray-400 group-hover:text-gray-500 mr-3 flex-shrink-0 h-6 w-6"/>
My tasks
</a>
<%= if @current_user do %> <%= if @current_user do %>
<!-- User account dropdown --> <!-- User account dropdown -->
<div class="px-3 mt-6 relative inline-block text-left w-full"> <div class="px-3 mt-6 relative inline-block text-left w-full">
@ -207,16 +195,22 @@
<nav class="px-3 mt-6"> <nav class="px-3 mt-6">
<div class="space-y-1"> <div class="space-y-1">
<%= live_redirect to: Routes.home_path(@conn, :index), <%= if @current_user do %>
class: "bg-gray-200 text-gray-900 group flex items-center px-2 py-2 text-sm font-medium rounded-md" do %> <.link
<.icon name={:home} outlined class="text-gray-500 mr-3 flex-shrink-0 h-6 w-6"/> redirect_to={Routes.song_index_path(@conn, :index, @current_user.username)}
Home class="text-gray-700 hover:text-gray-900 hover:bg-gray-50 group flex items-center px-2 py-2 text-sm font-medium rounded-md"
<% end %> >
<.icon name={:music_note} outlined class="text-gray-400 group-hover:text-gray-500 mr-3 flex-shrink-0 h-6 w-6"/>
My Songs
</.link>
<%= live_redirect to: Routes.song_index_path(@conn, :index), <.link
class: "text-gray-700 hover:text-gray-900 hover:bg-gray-50 group flex items-center px-2 py-2 text-sm font-medium rounded-md" do %> redirect_to={Routes.settings_path(@conn, :edit)}
<.icon name={:music_note} outlined class="text-gray-400 group-hover:text-gray-500 mr-3 flex-shrink-0 h-6 w-6"/> class="text-gray-700 hover:text-gray-900 hover:bg-gray-50 group flex items-center px-2 py-2 text-sm font-medium rounded-md"
My Songs >
<.icon name={:adjustments} outlined class="text-gray-400 group-hover:text-gray-500 mr-3 flex-shrink-0 h-6 w-6"/>
Settings
</.link>
<% end %> <% end %>
<%= unless @current_user do %> <%= unless @current_user do %>

View file

@ -22,16 +22,16 @@ defmodule LiveBeatsWeb.ErrorHelpers do
~H""" ~H"""
<%= for error <- @error_values do %> <%= for error <- @error_values do %>
<div <span
phx-feedback-for={@input_name} phx-feedback-for={@input_name}
class={"invalid-feedback -mt-1 pl-2 text-sm text-white bg-red-600 rounded-md #{@class}"} class={"invalid-feedback inline-block pl-2 pr-2 text-sm text-white bg-red-600 rounded-md #{@class}"}
> >
<%= translate_error(error) %> <%= translate_error(error) %>
</div> </span>
<% end %> <% end %>
<%= if Enum.empty?(@error_values) do %> <%= if Enum.empty?(@error_values) do %>
<div class={"invalid-feedback h-0 #{@class}"}></div> <span class={"invalid-feedback inline-block h-0 #{@class}"}></span>
<% end %> <% end %>
""" """
end end

View file

@ -10,6 +10,7 @@ defmodule LiveBeats.Repo.Migrations.CreateUserAuth do
add :name, :string add :name, :string
add :role, :string, null: false add :role, :string, null: false
add :confirmed_at, :naive_datetime add :confirmed_at, :naive_datetime
add :profile_tagline, :string
timestamps() timestamps()
end end

View file

@ -86,7 +86,7 @@ defmodule LiveBeatsWeb.UserAuthTest do
test "redirects if user is not authenticated", %{conn: conn} do test "redirects if user is not authenticated", %{conn: conn} do
conn = conn |> fetch_flash() |> UserAuth.require_authenticated_user([]) conn = conn |> fetch_flash() |> UserAuth.require_authenticated_user([])
assert conn.halted assert conn.halted
assert redirected_to(conn) == Routes.home_path(conn, :index) assert redirected_to(conn) == Routes.song_index_path(conn, :index)
assert get_flash(conn, :error) == "You must log in to access this page." assert get_flash(conn, :error) == "You must log in to access this page."
end end