Skip to content

Commit 0801ac3

Browse files
committed
Code for step 2
1 parent 7f01391 commit 0801ac3

37 files changed

+2572
-0
lines changed

config/test.exs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
use Mix.Config
22

3+
# Only in tests, remove the complexity from the password hashing algorithm
4+
config :bcrypt_elixir, :log_rounds, 1
5+
36
# Configure your database
47
#
58
# The MIX_TEST_PARTITION environment variable can be used

lib/mjml_demo/accounts.ex

Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
defmodule MjmlDemo.Accounts do
2+
@moduledoc """
3+
The Accounts context.
4+
"""
5+
6+
import Ecto.Query, warn: false
7+
alias MjmlDemo.Repo
8+
alias MjmlDemo.Accounts.{User, UserToken, UserNotifier}
9+
10+
## Database getters
11+
12+
@doc """
13+
Gets a user by email.
14+
15+
## Examples
16+
17+
iex> get_user_by_email("foo@example.com")
18+
%User{}
19+
20+
iex> get_user_by_email("unknown@example.com")
21+
nil
22+
23+
"""
24+
def get_user_by_email(email) when is_binary(email) do
25+
Repo.get_by(User, email: email)
26+
end
27+
28+
@doc """
29+
Gets a user by email and password.
30+
31+
## Examples
32+
33+
iex> get_user_by_email_and_password("foo@example.com", "correct_password")
34+
%User{}
35+
36+
iex> get_user_by_email_and_password("foo@example.com", "invalid_password")
37+
nil
38+
39+
"""
40+
def get_user_by_email_and_password(email, password)
41+
when is_binary(email) and is_binary(password) do
42+
user = Repo.get_by(User, email: email)
43+
if User.valid_password?(user, password), do: user
44+
end
45+
46+
@doc """
47+
Gets a single user.
48+
49+
Raises `Ecto.NoResultsError` if the User does not exist.
50+
51+
## Examples
52+
53+
iex> get_user!(123)
54+
%User{}
55+
56+
iex> get_user!(456)
57+
** (Ecto.NoResultsError)
58+
59+
"""
60+
def get_user!(id), do: Repo.get!(User, id)
61+
62+
## User registration
63+
64+
@doc """
65+
Registers a user.
66+
67+
## Examples
68+
69+
iex> register_user(%{field: value})
70+
{:ok, %User{}}
71+
72+
iex> register_user(%{field: bad_value})
73+
{:error, %Ecto.Changeset{}}
74+
75+
"""
76+
def register_user(attrs) do
77+
%User{}
78+
|> User.registration_changeset(attrs)
79+
|> Repo.insert()
80+
end
81+
82+
@doc """
83+
Returns an `%Ecto.Changeset{}` for tracking user changes.
84+
85+
## Examples
86+
87+
iex> change_user_registration(user)
88+
%Ecto.Changeset{data: %User{}}
89+
90+
"""
91+
def change_user_registration(%User{} = user, attrs \\ %{}) do
92+
User.registration_changeset(user, attrs, hash_password: false)
93+
end
94+
95+
## Settings
96+
97+
@doc """
98+
Returns an `%Ecto.Changeset{}` for changing the user email.
99+
100+
## Examples
101+
102+
iex> change_user_email(user)
103+
%Ecto.Changeset{data: %User{}}
104+
105+
"""
106+
def change_user_email(user, attrs \\ %{}) do
107+
User.email_changeset(user, attrs)
108+
end
109+
110+
@doc """
111+
Emulates that the email will change without actually changing
112+
it in the database.
113+
114+
## Examples
115+
116+
iex> apply_user_email(user, "valid password", %{email: ...})
117+
{:ok, %User{}}
118+
119+
iex> apply_user_email(user, "invalid password", %{email: ...})
120+
{:error, %Ecto.Changeset{}}
121+
122+
"""
123+
def apply_user_email(user, password, attrs) do
124+
user
125+
|> User.email_changeset(attrs)
126+
|> User.validate_current_password(password)
127+
|> Ecto.Changeset.apply_action(:update)
128+
end
129+
130+
@doc """
131+
Updates the user email using the given token.
132+
133+
If the token matches, the user email is updated and the token is deleted.
134+
The confirmed_at date is also updated to the current time.
135+
"""
136+
def update_user_email(user, token) do
137+
context = "change:#{user.email}"
138+
139+
with {:ok, query} <- UserToken.verify_change_email_token_query(token, context),
140+
%UserToken{sent_to: email} <- Repo.one(query),
141+
{:ok, _} <- Repo.transaction(user_email_multi(user, email, context)) do
142+
:ok
143+
else
144+
_ -> :error
145+
end
146+
end
147+
148+
defp user_email_multi(user, email, context) do
149+
changeset = user |> User.email_changeset(%{email: email}) |> User.confirm_changeset()
150+
151+
Ecto.Multi.new()
152+
|> Ecto.Multi.update(:user, changeset)
153+
|> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, [context]))
154+
end
155+
156+
@doc """
157+
Delivers the update email instructions to the given user.
158+
159+
## Examples
160+
161+
iex> deliver_update_email_instructions(user, current_email, &Routes.user_update_email_url(conn, :edit, &1))
162+
{:ok, %{to: ..., body: ...}}
163+
164+
"""
165+
def deliver_update_email_instructions(%User{} = user, current_email, update_email_url_fun)
166+
when is_function(update_email_url_fun, 1) do
167+
{encoded_token, user_token} = UserToken.build_email_token(user, "change:#{current_email}")
168+
169+
Repo.insert!(user_token)
170+
UserNotifier.deliver_update_email_instructions(user, update_email_url_fun.(encoded_token))
171+
end
172+
173+
@doc """
174+
Returns an `%Ecto.Changeset{}` for changing the user password.
175+
176+
## Examples
177+
178+
iex> change_user_password(user)
179+
%Ecto.Changeset{data: %User{}}
180+
181+
"""
182+
def change_user_password(user, attrs \\ %{}) do
183+
User.password_changeset(user, attrs, hash_password: false)
184+
end
185+
186+
@doc """
187+
Updates the user password.
188+
189+
## Examples
190+
191+
iex> update_user_password(user, "valid password", %{password: ...})
192+
{:ok, %User{}}
193+
194+
iex> update_user_password(user, "invalid password", %{password: ...})
195+
{:error, %Ecto.Changeset{}}
196+
197+
"""
198+
def update_user_password(user, password, attrs) do
199+
changeset =
200+
user
201+
|> User.password_changeset(attrs)
202+
|> User.validate_current_password(password)
203+
204+
Ecto.Multi.new()
205+
|> Ecto.Multi.update(:user, changeset)
206+
|> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, :all))
207+
|> Repo.transaction()
208+
|> case do
209+
{:ok, %{user: user}} -> {:ok, user}
210+
{:error, :user, changeset, _} -> {:error, changeset}
211+
end
212+
end
213+
214+
## Session
215+
216+
@doc """
217+
Generates a session token.
218+
"""
219+
def generate_user_session_token(user) do
220+
{token, user_token} = UserToken.build_session_token(user)
221+
Repo.insert!(user_token)
222+
token
223+
end
224+
225+
@doc """
226+
Gets the user with the given signed token.
227+
"""
228+
def get_user_by_session_token(token) do
229+
{:ok, query} = UserToken.verify_session_token_query(token)
230+
Repo.one(query)
231+
end
232+
233+
@doc """
234+
Deletes the signed token with the given context.
235+
"""
236+
def delete_session_token(token) do
237+
Repo.delete_all(UserToken.token_and_context_query(token, "session"))
238+
:ok
239+
end
240+
241+
## Confirmation
242+
243+
@doc """
244+
Delivers the confirmation email instructions to the given user.
245+
246+
## Examples
247+
248+
iex> deliver_user_confirmation_instructions(user, &Routes.user_confirmation_url(conn, :confirm, &1))
249+
{:ok, %{to: ..., body: ...}}
250+
251+
iex> deliver_user_confirmation_instructions(confirmed_user, &Routes.user_confirmation_url(conn, :confirm, &1))
252+
{:error, :already_confirmed}
253+
254+
"""
255+
def deliver_user_confirmation_instructions(%User{} = user, confirmation_url_fun)
256+
when is_function(confirmation_url_fun, 1) do
257+
if user.confirmed_at do
258+
{:error, :already_confirmed}
259+
else
260+
{encoded_token, user_token} = UserToken.build_email_token(user, "confirm")
261+
Repo.insert!(user_token)
262+
UserNotifier.deliver_confirmation_instructions(user, confirmation_url_fun.(encoded_token))
263+
end
264+
end
265+
266+
@doc """
267+
Confirms a user by the given token.
268+
269+
If the token matches, the user account is marked as confirmed
270+
and the token is deleted.
271+
"""
272+
def confirm_user(token) do
273+
with {:ok, query} <- UserToken.verify_email_token_query(token, "confirm"),
274+
%User{} = user <- Repo.one(query),
275+
{:ok, %{user: user}} <- Repo.transaction(confirm_user_multi(user)) do
276+
{:ok, user}
277+
else
278+
_ -> :error
279+
end
280+
end
281+
282+
defp confirm_user_multi(user) do
283+
Ecto.Multi.new()
284+
|> Ecto.Multi.update(:user, User.confirm_changeset(user))
285+
|> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, ["confirm"]))
286+
end
287+
288+
## Reset password
289+
290+
@doc """
291+
Delivers the reset password email to the given user.
292+
293+
## Examples
294+
295+
iex> deliver_user_reset_password_instructions(user, &Routes.user_reset_password_url(conn, :edit, &1))
296+
{:ok, %{to: ..., body: ...}}
297+
298+
"""
299+
def deliver_user_reset_password_instructions(%User{} = user, reset_password_url_fun)
300+
when is_function(reset_password_url_fun, 1) do
301+
{encoded_token, user_token} = UserToken.build_email_token(user, "reset_password")
302+
Repo.insert!(user_token)
303+
UserNotifier.deliver_reset_password_instructions(user, reset_password_url_fun.(encoded_token))
304+
end
305+
306+
@doc """
307+
Gets the user by reset password token.
308+
309+
## Examples
310+
311+
iex> get_user_by_reset_password_token("validtoken")
312+
%User{}
313+
314+
iex> get_user_by_reset_password_token("invalidtoken")
315+
nil
316+
317+
"""
318+
def get_user_by_reset_password_token(token) do
319+
with {:ok, query} <- UserToken.verify_email_token_query(token, "reset_password"),
320+
%User{} = user <- Repo.one(query) do
321+
user
322+
else
323+
_ -> nil
324+
end
325+
end
326+
327+
@doc """
328+
Resets the user password.
329+
330+
## Examples
331+
332+
iex> reset_user_password(user, %{password: "new long password", password_confirmation: "new long password"})
333+
{:ok, %User{}}
334+
335+
iex> reset_user_password(user, %{password: "valid", password_confirmation: "not the same"})
336+
{:error, %Ecto.Changeset{}}
337+
338+
"""
339+
def reset_user_password(user, attrs) do
340+
Ecto.Multi.new()
341+
|> Ecto.Multi.update(:user, User.password_changeset(user, attrs))
342+
|> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, :all))
343+
|> Repo.transaction()
344+
|> case do
345+
{:ok, %{user: user}} -> {:ok, user}
346+
{:error, :user, changeset, _} -> {:error, changeset}
347+
end
348+
end
349+
end

0 commit comments

Comments
 (0)