Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions lib/atomic_web.ex
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ defmodule AtomicWeb do
end
end

def auth_view do
quote do
use Phoenix.LiveView

import AtomicWeb.Auth.Components.Pitch

unquote(view_helpers())
end
end

def live_view do
quote do
use Phoenix.LiveView,
Expand Down
3 changes: 2 additions & 1 deletion lib/atomic_web/controllers/user_auth.ex
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,8 @@ defmodule AtomicWeb.UserAuth do
def require_finished_user_setup(conn, _opts) do
current_user = conn.assigns[:current_user]

if conn.assigns[:current_user] && not is_nil(current_user.slug) do
if !conn.assigns[:current_user] or
(conn.assigns[:current_user] && not is_nil(current_user.slug)) do
conn
else
conn
Expand Down
30 changes: 11 additions & 19 deletions lib/atomic_web/controllers/user_registration_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,9 @@ defmodule AtomicWeb.UserRegistrationController do
use AtomicWeb, :controller

alias Atomic.Accounts
alias Atomic.Accounts.User

def new(conn, _params) do
changeset = Accounts.change_user_registration(%User{})

conn =
conn
|> assign(:roles, User.roles())

render(conn, "new.html", changeset: changeset)
end

def create(conn, %{"user" => user_params}) do
if user_params["password"] == user_params["password_confirmation"] do
if user_params["password"] == user_params["confirm_password"] do
case Accounts.register_user(user_params) do
{:ok, user} ->
{:ok, _} =
Expand All @@ -25,18 +14,21 @@ defmodule AtomicWeb.UserRegistrationController do
)

conn
|> put_flash(:info, "Registered successfully. Check your email inbox before continuing")
|> render("new.html", changeset: Accounts.change_user_registration(user))
|> put_flash(
:info,
"Registered successfully. Check your email inbox before continuing."
)
|> redirect(to: ~p"/users/register")

{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "new.html", changeset: changeset)
{:error, %Ecto.Changeset{} = _changeset} ->
conn
|> put_flash(:error, "Unable to register. This email may already be registered.")
|> redirect(to: ~p"/users/register")
end
else
conn
|> put_flash(:error, "Passwords don't match.")
|> render("new.html",
changeset: Accounts.change_user_registration(%User{email: user_params["email"]})
)
|> redirect(to: ~p"/users/register")
end
end
end
29 changes: 25 additions & 4 deletions lib/atomic_web/controllers/user_session_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,25 @@ defmodule AtomicWeb.UserSessionController do
alias Atomic.Accounts
alias AtomicWeb.UserAuth

def new(conn, _params) do
render(conn, "new.html", error_message: nil)
def new(conn, %{"user" => user_params}) do
case Accounts.register_user(user_params) do
{:ok, %{user: user, attendee: _}} ->
{:ok, _} =
Accounts.deliver_user_confirmation_instructions(
user,
&url(~p"/users/confirm/#{&1}")
)

conn
|> UserAuth.log_in_user(user, user_params)
|> put_flash(:success, "Registered successfully")
|> redirect(to: ~p"/users/setup")

{:error, _, %Ecto.Changeset{} = _changeset, _} ->
conn
|> put_flash(:error, "Unable to register. This email may already be registered.")
|> redirect(to: ~p"/users/register")
end
end

def create(conn, %{"user" => user_params}) do
Expand All @@ -14,13 +31,17 @@ defmodule AtomicWeb.UserSessionController do

if user do
if is_nil(user.confirmed_at) do
render(conn, "new.html", error_message: "Confirm your email before continuing.")
conn
|> put_flash(:error, "You need to confirm your email address.")
|> redirect(to: ~p"/users/log_in")
else
UserAuth.log_in_user(conn, user, user_params)
end
else
# In order to prevent user enumeration attacks, don't disclose whether the email is registered.
render(conn, "new.html", error_message: "Invalid email or password.")
conn
|> put_flash(:error, "Invalid email or password.")
|> redirect(to: ~p"/users/log_in")
end
end

Expand Down
43 changes: 43 additions & 0 deletions lib/atomic_web/live/auth/components/pitch.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
defmodule AtomicWeb.Auth.Components.Pitch do
@moduledoc false
use AtomicWeb, :component

def pitch(assigns) do
~H"""
<div class="bg-white py-24 sm:py-32">
<div class="mx-auto max-w-2xl px-6 lg:max-w-7xl lg:px-8">
<h2 class="text-base/7 text-primary-600 font-semibold">{gettext("Connect. Organize. Engage.")}</h2>
<p class="text-pretty mt-2 text-4xl font-semibold tracking-tight text-gray-950 sm:text-5xl">{gettext("Your Campus, Connected")}</p>
<div class="mt-10 grid grid-cols-1 gap-4 sm:mt-16 lg:grid-cols-6">
<div class="relative lg:col-span-3">
<div class="absolute inset-px rounded-lg bg-white max-lg:rounded-t-[2rem] lg:rounded-tl-[2rem]"></div>
<div class="relative flex h-full flex-col overflow-hidden rounded-[calc(theme(borderRadius.lg)+1px)] max-lg:rounded-t-[calc(2rem+1px)] lg:rounded-tl-[calc(2rem+1px)]">
<img class="h-[22rem] object-cover object-top" src={~p"/images/pitch/0.png"} alt="" />
<div class="p-10 pt-4">
<h3 class="text-sm/4 text-primary-600 font-semibold">{gettext("Stay in the Loop")}</h3>
<p class="mt-2 text-lg font-medium tracking-tight text-gray-950">{gettext("Everything, all in the same place")}</p>
<p class="text-sm/6 mt-2 max-w-lg text-gray-600">{gettext("Get the latest updates from your favorite student associations in a single, organized feed—no more missed events or scattered information.")}</p>
</div>
</div>
<div class="ring-black/5 pointer-events-none absolute inset-px rounded-lg shadow ring-1 max-lg:rounded-t-[2rem] lg:rounded-tl-[2rem]"></div>
</div>
<div class="relative lg:col-span-3">
<div class="absolute inset-px rounded-lg bg-white lg:rounded-tr-[2rem]"></div>
<div class="relative flex h-full flex-col overflow-hidden rounded-[calc(theme(borderRadius.lg)+1px)] lg:rounded-tr-[calc(2rem+1px)]">
<img class="h-[22rem] object-cover object-left lg:object-right" src={~p"/images/pitch/1.png"} alt="" />
<div class="p-10 pt-4">
<h3 class="text-sm/4 text-primary-600 font-semibold">{gettext("Activities made easy")}</h3>
<p class="mt-2 text-lg font-medium tracking-tight text-gray-950">{gettext("Plan, promote, and track attendance")}</p>
<p class="text-sm/6 mt-2 max-w-lg text-gray-600">
{gettext("Simplify activity organization with participant limits, location tracking, and digital certificates—all in one place.")}
</p>
</div>
</div>
<div class="ring-black/5 pointer-events-none absolute inset-px rounded-lg shadow ring-1 lg:rounded-tr-[2rem]"></div>
</div>
</div>
</div>
</div>
"""
end
end
64 changes: 64 additions & 0 deletions lib/atomic_web/live/auth/user_login_live.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
defmodule AtomicWeb.Auth.UserLoginLive do
use AtomicWeb, :auth_view

import AtomicWeb.Components.Forms

def render(assigns) do
~H"""
<div class="flex min-h-screen">
<div aria-live="assertive" class="pointer-events-none fixed inset-0 z-50 flex flex-col items-end gap-y-2 px-4 py-4 sm:items-start sm:px-6">
<%= for {key, message} <- @flash do %>
<.live_component id={key} module={AtomicWeb.Components.Notification} type={key} message={message} flash={@flash} />
<% end %>
</div>
<div class="flex flex-1 flex-col justify-center px-4 py-12 sm:px-6 lg:flex-none lg:px-20 xl:px-24">
<div class="mx-auto w-full max-w-sm lg:w-96">
<div>
<div class="flex h-16 shrink-0 select-none items-center gap-x-4 pt-4">
<img src={~p"/images/atomic.svg"} class="h-14 w-auto" />
<p class="text-2xl font-semibold text-zinc-400">Atomic</p>
</div>
<h2 class="text-2xl/9 mt-8 font-semibold tracking-tight text-gray-900">{gettext("Sign in to your account")}</h2>
<p class="text-sm/6 mt-2 text-gray-500">
{gettext("Not a member?")}
<.link patch={~p"/users/register"} class="text-primary-600 font-semibold hover:text-primary-700">{gettext("Sign up here")}</.link>
</p>
</div>

<div class="mt-10">
<div>
<.form for={@form} id="login_form" action={~p"/users/log_in"} phx-update="ignore">
<.field field={@form[:email]} type="email" label="Email" required />
<.field field={@form[:password]} type="password" label="Password" required />
<div class="flex justify-between">
<div class="flex gap-3">
<.field field={@form[:remember_me]} type="checkbox" label="Remember me" />
</div>
<.link patch={~p"/users/reset_password"} class="text-primary-600 text-sm font-semibold hover:text-primary-700">
{gettext("Forgot your password?")}
</.link>
</div>

<div>
<.button class="w-full" size={:md}>
{gettext("Log in")}
</.button>
</div>
</.form>
</div>
</div>
</div>
</div>
<div class="relative hidden w-0 flex-1 lg:block">
<img class="size-full absolute inset-0 object-cover" src={~p"/images/backgrounds/0.png"} alt="" />
</div>
</div>
"""
end

def mount(_params, _session, socket) do
email = Phoenix.Flash.get(socket.assigns.flash, :email)
form = to_form(%{"email" => email}, as: "user")
{:ok, assign(socket, form: form), temporary_assigns: [form: form]}
end
end
110 changes: 110 additions & 0 deletions lib/atomic_web/live/auth/user_register_live.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
defmodule AtomicWeb.Auth.UserRegisterLive do
use AtomicWeb, :auth_view

alias Atomic.Accounts
alias Atomic.Accounts.User

import AtomicWeb.Components.Forms

@impl true
def render(assigns) do
~H"""
<div class="flex min-h-screen">
<div aria-live="assertive" class="pointer-events-none fixed inset-0 z-50 flex flex-col items-end gap-y-2 px-4 py-4 sm:items-start sm:px-6">
<%= for {key, message} <- @flash do %>
<.live_component id={key} module={AtomicWeb.Components.Notification} type={key} message={message} flash={@flash} />
<% end %>
</div>
<div class="flex flex-1 flex-col justify-center border px-4 py-12 sm:px-6 lg:flex-none lg:px-20 xl:px-24">
<div class="mx-auto w-full max-w-sm lg:w-96">
<div>
<div class="flex h-16 shrink-0 select-none items-center gap-x-4 pt-4">
<img src={~p"/images/atomic.svg"} class="h-14 w-auto" />
<p class="text-2xl font-semibold text-zinc-400">{gettext("Atomic")}</p>
</div>
<h2 class="text-2xl/9 mt-8 font-semibold tracking-tight text-gray-900">{gettext("Register for an account")}</h2>
<p class="text-sm/6 mt-2 text-gray-500">
{gettext("Already have an account?")}
<.link patch={~p"/users/log_in"} class="text-primary-600 font-semibold hover:text-primary-700">{gettext("Log in")}</.link>
</p>
</div>

<div class="mt-10">
<div>
<.form for={@form} id="register_form" action={~p"/users/register"} phx-update="ignore">
<.field field={@form[:name]} type="text" label="Name" required />
<.field field={@form[:email]} type="email" label="Email" required />
<.field field={@form[:password]} type="password" label="Password" required />
<.field field={@form[:confirm_password]} type="password" label="Confirm Password" required />
<div class="flex justify-between">
<div class="flex gap-1">
<.field field={@form[:terms]} type="checkbox" label="" required />
<p class="text-sm">{gettext("I agree to the")}
<.link navigate={~p"/tos"} target="_blank" class="text-primary-600 font-semibold hover:text-primary-700">{gettext("terms of service")}</.link>
{gettext("and")}
<.link navigate={~p"/privacy"} target="_blank" class="text-primary-600 font-semibold hover:text-primary-700">{gettext("privacy policy.")}</.link></p>
</div>
</div>
<div>
<.button class="mt-4 w-full sm:mt-0" size={:md}>
{gettext("Sign up")}
</.button>
</div>
</.form>
</div>
</div>
</div>
</div>
<div class="relative hidden w-0 flex-1 lg:block">
<.pitch />
</div>
</div>
"""
end

@impl true
def mount(_params, _session, socket) do
changeset = Accounts.change_user_registration(%User{})

socket =
socket
|> assign(trigger_submit: false, check_errors: false)
|> assign_form(changeset)

{:ok, socket, temporary_assigns: [form: nil]}
end

@impl true
def handle_event("save", %{"user" => user_params}, socket) do
case Accounts.register_user(user_params) do
{:ok, user} ->
{:ok, _} =
Accounts.deliver_user_confirmation_instructions(
user,
&url(~p"/users/confirm/#{&1}")
)

changeset = Accounts.change_user_registration(user)
{:noreply, socket |> assign(trigger_submit: true) |> assign_form(changeset)}

{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, socket |> assign(check_errors: true) |> assign_form(changeset)}
end
end

@impl true
def handle_event("validate", %{"user" => user_params}, socket) do
changeset = Accounts.change_user_registration(%User{}, user_params)
{:noreply, assign_form(socket, Map.put(changeset, :action, :validate))}
end

defp assign_form(socket, %Ecto.Changeset{} = changeset) do
form = to_form(changeset, as: "user")

if changeset.valid? do
assign(socket, form: form, check_errors: false)
else
assign(socket, form: form)
end
end
end
9 changes: 4 additions & 5 deletions lib/atomic_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ defmodule AtomicWeb.Router do
## Normal user routes

scope "/", AtomicWeb do
pipe_through :browser
pipe_through [:browser, :require_finished_user_setup]

live_session :user, on_mount: [{AtomicWeb.Hooks, :current_user_state}] do
live "/", HomeLive.Index, :index
Expand All @@ -110,8 +110,7 @@ defmodule AtomicWeb.Router do

pipe_through [
:require_authenticated_user,
:require_confirmed_user,
:require_finished_user_setup
:require_confirmed_user
]

live "/profile/:slug/edit", ProfileLive.Edit, :edit
Expand Down Expand Up @@ -155,10 +154,10 @@ defmodule AtomicWeb.Router do
pipe_through [:browser, :redirect_if_user_is_authenticated]

scope "/users" do
get "/register", UserRegistrationController, :new
live "/register", Auth.UserRegisterLive, :new
post "/register", UserRegistrationController, :create

get "/log_in", UserSessionController, :new
live "/log_in", Auth.UserLoginLive, :new
post "/log_in", UserSessionController, :create

get "/reset_password", UserResetPasswordController, :new
Expand Down
Loading
Loading