@@ -2,20 +2,56 @@ defmodule AlgoraWeb.SignInLive do
22 @ moduledoc false
33 use AlgoraWeb , :live_view
44
5+ alias Algora.Accounts.User
6+ alias Swoosh.Email
7+
58 def render ( assigns ) do
69 ~H"""
7- < div class = "min-h-[calc(100vh-64px)] flex flex-col justify-center " >
8- < div class = "mb-[64px] mx-auto max-w-3xl p-12 sm:mx-auto sm:w-full sm:max-w-sm sm:p-24 " >
9- < h2 class = "text-center text-3xl font-extrabold text-gray-50 " >
10- Algora Console
11- </ h2 >
12- < . link
13- href = { @ authorize_url }
14- rel = "noopener "
15- class = "mt-8 flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-600 focus:outline-none focus:ring-2 focus:ring-indigo-400 focus:ring-offset-2 "
16- >
17- Sign in with GitHub
18- </ . link >
10+ < div class = "mx-auto max-w-sm " >
11+ < div class = "min-h-[calc(100vh-64px)] flex flex-col justify-center " >
12+ < div class = "mb mx-auto max-w-3xl p-4 sm:mx-auto sm:w-full sm:max-w-sm " >
13+ < h2 class = "text-center text-3xl font-extrabold text-gray-50 p-4 " >
14+ Algora Console
15+ </ h2 >
16+ < . header class = "text-center p-4 " >
17+ Sign in with Github
18+ <: subtitle > Sign in and link your Github Account</: subtitle >
19+ </ . header >
20+ < . link
21+ href = { @ authorize_url }
22+ rel = "noopener "
23+ class = "mt-4 flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-600 focus:outline-none focus:ring-2 focus:ring-indigo-400 focus:ring-offset-2 "
24+ >
25+ Authorize with GitHub
26+ </ . link >
27+ </ div >
28+ < div class = "border-t border-gray-800 my-4 " > </ div >
29+ < . header class = "text-center p-4 " >
30+ Sign in with Email
31+ <: subtitle :if = { ! @ secret_code } > We'll send a login code to your inbox</: subtitle >
32+ <: subtitle :if = { @ secret_code } > We sent a login code to your inbox</: subtitle >
33+ </ . header >
34+
35+ < . simple_form for = { @ form } id = "send_login_code_form " phx-submit = "send_login_code " >
36+ < . input :if = { ! @ secret_code } field = { @ form [ :email ] } type = "email " placeholder = "Email " required />
37+ < . input
38+ :if = { @ secret_code }
39+ field = { @ form [ :login_code ] }
40+ type = "text "
41+ placeholder = "Enter Login Code "
42+ required
43+ />
44+ <: actions >
45+ < . button
46+ if = { ! @ secret_code }
47+ phx-disable-with = "Sending... "
48+ class = "mt-4 flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-600 focus:outline-none focus:ring-2 focus:ring-indigo-400 focus:ring-offset-2 "
49+ >
50+ < span :if = { ! @ secret_code } > Send login code</ span >
51+ < div :if = { @ secret_code } > Login with code</ div >
52+ </ . button >
53+ </: actions >
54+ </ . simple_form >
1955 </ div >
2056 </ div >
2157 """
@@ -28,6 +64,87 @@ defmodule AlgoraWeb.SignInLive do
2864 return_to -> Algora.Github . authorize_url ( % { return_to: return_to } )
2965 end
3066
31- { :ok , assign ( socket , authorize_url: authorize_url ) }
67+ changeset = User . login_changeset ( % User { } , % { } )
68+
69+ { :ok ,
70+ socket
71+ |> assign ( authorize_url: authorize_url )
72+ |> assign ( secret_code: nil )
73+ |> assign_form ( changeset ) }
74+ end
75+
76+ def handle_event ( "send_login_code" , % { "user" => % { "email" => email } } , socket ) do
77+ code = Nanoid . generate ( )
78+
79+ case Algora.Accounts . get_user_by_email ( email ) do
80+ % User { } = user ->
81+ changeset = User . login_changeset ( % User { } , % { } )
82+
83+ case send_login_code_to_user ( user , code ) do
84+ { :ok , _id } ->
85+ { :noreply ,
86+ socket
87+ |> assign ( :secret_code , code )
88+ |> assign ( :user , user )
89+ |> assign_form ( changeset )
90+ |> put_flash ( :info , "Login code sent to #{ email } !" ) }
91+
92+ { :error , _reason } ->
93+ # capture_error reason
94+ { :noreply , put_flash ( socket , :error , "We had trouble sending mail to #{ email } . Please try again" ) }
95+ end
96+
97+ nil ->
98+ throttle ( )
99+ { :noreply , put_flash ( socket , :error , "Email address not found." ) }
100+ end
101+ end
102+
103+ def handle_event ( "send_login_code" , % { "user" => % { "login_code" => code } } , socket ) do
104+ if Plug.Crypto . secure_compare ( code , socket . assigns . secret_code ) do
105+ user = socket . assigns . user
106+ token = AlgoraWeb.UserAuth . generate_login_code ( user . email )
107+ path = AlgoraWeb.UserAuth . login_path ( user . email , token )
108+
109+ { :noreply ,
110+ socket
111+ |> redirect ( to: path )
112+ |> put_flash ( :info , "Logged in successfully!" ) }
113+ else
114+ throttle ( )
115+ { :noreply , put_flash ( socket , :error , "Invalid login code" ) }
116+ end
32117 end
118+
119+ defp assign_form ( socket , % Ecto.Changeset { } = changeset ) do
120+ assign ( socket , :form , to_form ( changeset ) )
121+ end
122+
123+ @ from_name "Algora"
124+ 125+
126+ defp send_login_code_to_user ( user , code ) do
127+ email =
128+ Email . new ( )
129+ |> Email . to ( { user . display_name , user . email } )
130+ |> Email . from ( { @ from_name , @ from_email } )
131+ |> Email . subject ( "Login code for Algora" )
132+ |> Email . text_body ( """
133+ Here is your login code for Algora!
134+
135+ #{ code }
136+
137+ If you didn't request this link, you can safely ignore this email.
138+
139+ --------------------------------------------------------------------------------
140+
141+ For correspondence, please email the Algora founders at [email protected] and [email protected] 142+
143+ © 2025 Algora PBC.
144+ """ )
145+
146+ Algora.Mailer . deliver ( email )
147+ end
148+
149+ defp throttle , do: :timer . sleep ( 1000 )
33150end
0 commit comments