accounts.ex (8096B)
1 defmodule DailyTracker.Accounts do 2 @moduledoc """ 3 The Accounts context. 4 """ 5 6 import Ecto.Query, warn: false 7 alias DailyTracker.Repo 8 9 alias DailyTracker.Accounts.{User, UserToken, UserNotifier} 10 11 ## Database getters 12 13 @doc """ 14 Gets a user by email. 15 16 ## Examples 17 18 iex> get_user_by_email("foo@example.com") 19 %User{} 20 21 iex> get_user_by_email("unknown@example.com") 22 nil 23 24 """ 25 def get_user_by_email(email) when is_binary(email) do 26 Repo.get_by(User, email: email) 27 end 28 29 @doc """ 30 Gets a user by email and password. 31 32 ## Examples 33 34 iex> get_user_by_email_and_password("foo@example.com", "correct_password") 35 %User{} 36 37 iex> get_user_by_email_and_password("foo@example.com", "invalid_password") 38 nil 39 40 """ 41 def get_user_by_email_and_password(email, password) 42 when is_binary(email) and is_binary(password) do 43 user = Repo.get_by(User, email: email) 44 if User.valid_password?(user, password), do: user 45 end 46 47 @doc """ 48 Gets a single user. 49 50 Raises `Ecto.NoResultsError` if the User does not exist. 51 52 ## Examples 53 54 iex> get_user!(123) 55 %User{} 56 57 iex> get_user!(456) 58 ** (Ecto.NoResultsError) 59 60 """ 61 def get_user!(id), do: Repo.get!(User, id) 62 63 ## User registration 64 65 @doc """ 66 Registers a user. 67 68 ## Examples 69 70 iex> register_user(%{field: value}) 71 {:ok, %User{}} 72 73 iex> register_user(%{field: bad_value}) 74 {:error, %Ecto.Changeset{}} 75 76 """ 77 def register_user(attrs) do 78 %User{} 79 |> User.email_changeset(attrs) 80 |> Repo.insert() 81 end 82 83 ## Settings 84 85 @doc """ 86 Checks whether the user is in sudo mode. 87 88 The user is in sudo mode when the last authentication was done no further 89 than 20 minutes ago. The limit can be given as second argument in minutes. 90 """ 91 def sudo_mode?(user, minutes \\ -20) 92 93 def sudo_mode?(%User{authenticated_at: ts}, minutes) when is_struct(ts, DateTime) do 94 DateTime.after?(ts, DateTime.utc_now() |> DateTime.add(minutes, :minute)) 95 end 96 97 def sudo_mode?(_user, _minutes), do: false 98 99 @doc """ 100 Returns an `%Ecto.Changeset{}` for changing the user email. 101 102 See `DailyTracker.Accounts.User.email_changeset/3` for a list of supported options. 103 104 ## Examples 105 106 iex> change_user_email(user) 107 %Ecto.Changeset{data: %User{}} 108 109 """ 110 def change_user_email(user, attrs \\ %{}, opts \\ []) do 111 User.email_changeset(user, attrs, opts) 112 end 113 114 @doc """ 115 Updates the user email using the given token. 116 117 If the token matches, the user email is updated and the token is deleted. 118 """ 119 def update_user_email(user, token) do 120 context = "change:#{user.email}" 121 122 Repo.transact(fn -> 123 with {:ok, query} <- UserToken.verify_change_email_token_query(token, context), 124 %UserToken{sent_to: email} <- Repo.one(query), 125 {:ok, user} <- Repo.update(User.email_changeset(user, %{email: email})), 126 {_count, _result} <- 127 Repo.delete_all(from(UserToken, where: [user_id: ^user.id, context: ^context])) do 128 {:ok, user} 129 else 130 _ -> {:error, :transaction_aborted} 131 end 132 end) 133 end 134 135 @doc """ 136 Returns an `%Ecto.Changeset{}` for changing the user password. 137 138 See `DailyTracker.Accounts.User.password_changeset/3` for a list of supported options. 139 140 ## Examples 141 142 iex> change_user_password(user) 143 %Ecto.Changeset{data: %User{}} 144 145 """ 146 def change_user_password(user, attrs \\ %{}, opts \\ []) do 147 User.password_changeset(user, attrs, opts) 148 end 149 150 @doc """ 151 Updates the user password. 152 153 Returns a tuple with the updated user, as well as a list of expired tokens. 154 155 ## Examples 156 157 iex> update_user_password(user, %{password: ...}) 158 {:ok, {%User{}, [...]}} 159 160 iex> update_user_password(user, %{password: "too short"}) 161 {:error, %Ecto.Changeset{}} 162 163 """ 164 def update_user_password(user, attrs) do 165 user 166 |> User.password_changeset(attrs) 167 |> update_user_and_delete_all_tokens() 168 end 169 170 ## Session 171 172 @doc """ 173 Generates a session token. 174 """ 175 def generate_user_session_token(user) do 176 {token, user_token} = UserToken.build_session_token(user) 177 Repo.insert!(user_token) 178 token 179 end 180 181 @doc """ 182 Gets the user with the given signed token. 183 184 If the token is valid `{user, token_inserted_at}` is returned, otherwise `nil` is returned. 185 """ 186 def get_user_by_session_token(token) do 187 {:ok, query} = UserToken.verify_session_token_query(token) 188 Repo.one(query) 189 end 190 191 @doc """ 192 Gets the user with the given magic link token. 193 """ 194 def get_user_by_magic_link_token(token) do 195 with {:ok, query} <- UserToken.verify_magic_link_token_query(token), 196 {user, _token} <- Repo.one(query) do 197 user 198 else 199 _ -> nil 200 end 201 end 202 203 @doc """ 204 Logs the user in by magic link. 205 206 There are three cases to consider: 207 208 1. The user has already confirmed their email. They are logged in 209 and the magic link is expired. 210 211 2. The user has not confirmed their email and no password is set. 212 In this case, the user gets confirmed, logged in, and all tokens - 213 including session ones - are expired. In theory, no other tokens 214 exist but we delete all of them for best security practices. 215 216 3. The user has not confirmed their email but a password is set. 217 This cannot happen in the default implementation but may be the 218 source of security pitfalls. See the "Mixing magic link and password registration" section of 219 `mix help phx.gen.auth`. 220 """ 221 def login_user_by_magic_link(token) do 222 {:ok, query} = UserToken.verify_magic_link_token_query(token) 223 224 case Repo.one(query) do 225 # Prevent session fixation attacks by disallowing magic links for unconfirmed users with password 226 {%User{confirmed_at: nil, hashed_password: hash}, _token} when not is_nil(hash) -> 227 raise """ 228 magic link log in is not allowed for unconfirmed users with a password set! 229 230 This cannot happen with the default implementation, which indicates that you 231 might have adapted the code to a different use case. Please make sure to read the 232 "Mixing magic link and password registration" section of `mix help phx.gen.auth`. 233 """ 234 235 {%User{confirmed_at: nil} = user, _token} -> 236 user 237 |> User.confirm_changeset() 238 |> update_user_and_delete_all_tokens() 239 240 {user, token} -> 241 Repo.delete!(token) 242 {:ok, {user, []}} 243 244 nil -> 245 {:error, :not_found} 246 end 247 end 248 249 @doc ~S""" 250 Delivers the update email instructions to the given user. 251 252 ## Examples 253 254 iex> deliver_user_update_email_instructions(user, current_email, &url(~p"/users/settings/confirm-email/#{&1}")) 255 {:ok, %{to: ..., body: ...}} 256 257 """ 258 def deliver_user_update_email_instructions(%User{} = user, current_email, update_email_url_fun) 259 when is_function(update_email_url_fun, 1) do 260 {encoded_token, user_token} = UserToken.build_email_token(user, "change:#{current_email}") 261 262 Repo.insert!(user_token) 263 UserNotifier.deliver_update_email_instructions(user, update_email_url_fun.(encoded_token)) 264 end 265 266 @doc """ 267 Delivers the magic link login instructions to the given user. 268 """ 269 def deliver_login_instructions(%User{} = user, magic_link_url_fun) 270 when is_function(magic_link_url_fun, 1) do 271 {encoded_token, user_token} = UserToken.build_email_token(user, "login") 272 Repo.insert!(user_token) 273 UserNotifier.deliver_login_instructions(user, magic_link_url_fun.(encoded_token)) 274 end 275 276 @doc """ 277 Deletes the signed token with the given context. 278 """ 279 def delete_user_session_token(token) do 280 Repo.delete_all(from(UserToken, where: [token: ^token, context: "session"])) 281 :ok 282 end 283 284 ## Token helper 285 286 defp update_user_and_delete_all_tokens(changeset) do 287 Repo.transact(fn -> 288 with {:ok, user} <- Repo.update(changeset) do 289 tokens_to_expire = Repo.all_by(UserToken, user_id: user.id) 290 291 Repo.delete_all(from(t in UserToken, where: t.id in ^Enum.map(tokens_to_expire, & &1.id))) 292 293 {:ok, {user, tokens_to_expire}} 294 end 295 end) 296 end 297 end