-
-
Notifications
You must be signed in to change notification settings - Fork 153
/
context.ex
245 lines (193 loc) · 6.91 KB
/
context.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
defmodule Pow.Ecto.Context do
@moduledoc """
Handles pow users context for user.
## Usage
This module will be used by pow by default. If you
wish to have control over context methods, you can
do configure `lib/my_project/users/users.ex`
the following way:
defmodule MyApp.Users do
use Pow.Ecto.Context,
repo: MyApp.Repo,
user: MyApp.Users.User
def create(params) do
pow_create(params)
end
end
Remember to update configuration with `users_context: MyApp.Users`.
The following Pow methods can be accessed:
* `pow_authenticate/1`
* `pow_create/1`
* `pow_update/2`
* `pow_delete/1`
* `pow_get_by/1`
## Configuration options
* `:repo` - the ecto repo module (required)
* `:user` - the user schema module (required)
* `:repo_opts` - keyword list options for the repo, `:prefix` can be set here
"""
alias Pow.Config
alias Pow.Ecto.Schema
@type user :: map()
@type changeset :: map()
@callback authenticate(map()) :: user() | nil
@callback create(map()) :: {:ok, user()} | {:error, changeset()}
@callback update(user(), map()) :: {:ok, user()} | {:error, changeset()}
@callback delete(user()) :: {:ok, user()} | {:error, changeset()}
@callback get_by(Keyword.t() | map()) :: user() | nil
@doc false
defmacro __using__(config) do
quote do
@behaviour unquote(__MODULE__)
@pow_config unquote(config)
def authenticate(params), do: pow_authenticate(params)
def create(params), do: pow_create(params)
def update(user, params), do: pow_update(user, params)
def delete(user), do: pow_delete(user)
def get_by(clauses), do: pow_get_by(clauses)
def pow_authenticate(params) do
unquote(__MODULE__).authenticate(params, @pow_config)
end
def pow_create(params) do
unquote(__MODULE__).create(params, @pow_config)
end
def pow_update(user, params) do
unquote(__MODULE__).update(user, params, @pow_config)
end
def pow_delete(user) do
unquote(__MODULE__).delete(user, @pow_config)
end
def pow_get_by(clauses) do
unquote(__MODULE__).get_by(clauses, @pow_config)
end
defoverridable unquote(__MODULE__)
end
end
@doc """
Finds a user based on the user id, and verifies the password on the user.
User schema module and repo module will be fetched from the config. The user
id field is fetched from the user schema module.
The method will return nil if either the fetched user, or password is nil.
To prevent timing attacks, a blank user struct will be passed to the
`verify_password/2` method for the user schema module to ensure that the
the response time will be equal as when a password is verified.
"""
@spec authenticate(map(), Config.t()) :: user() | nil
def authenticate(params, config) do
user_mod = Config.user!(config)
user_id_field = user_mod.pow_user_id_field()
login_value = params[Atom.to_string(user_id_field)]
password = params["password"]
do_authenticate(user_id_field, login_value, password, config)
end
defp do_authenticate(_user_id_field, nil, _password, _config), do: nil
defp do_authenticate(user_id_field, login_value, password, config) do
[{user_id_field, login_value}]
|> get_by(config)
|> verify_password(password, config)
end
defp verify_password(nil, _password, config) do
user_mod = Config.user!(config)
user = struct(user_mod, password_hash: nil)
user_mod.verify_password(user, "")
nil
end
defp verify_password(_user, nil, _config), do: false
defp verify_password(user, password, _config) do
case user.__struct__.verify_password(user, password) do
true -> user
_ -> nil
end
end
@doc """
Creates a new user.
User schema module and repo module will be fetched from config.
"""
@spec create(map(), Config.t()) :: {:ok, user()} | {:error, changeset()}
def create(params, config) do
user_mod = Config.user!(config)
user_mod
|> struct()
|> user_mod.changeset(params)
|> do_insert(config)
end
@doc """
Updates the user.
User schema module will be fetched from provided user and repo will be
fetched from the config.
"""
@spec update(user(), map(), Config.t()) :: {:ok, user()} | {:error, changeset()}
def update(user, params, config) do
user
|> user.__struct__.changeset(params)
|> do_update(config)
end
@doc """
Deletes the user.
Repo module will be fetched from the config.
"""
@spec delete(user(), Config.t()) :: {:ok, user()} | {:error, changeset()}
def delete(user, config) do
opts = repo_opts(config, [:prefix])
Config.repo!(config).delete(user, opts)
end
@doc """
Retrieves an user by the provided clauses.
User schema module and repo module will be fetched from the config.
"""
@spec get_by(Keyword.t() | map(), Config.t()) :: user() | nil
def get_by(clauses, config) do
user_mod = Config.user!(config)
clauses = normalize_user_id_field_value(user_mod, clauses)
opts = repo_opts(config, [:prefix])
Config.repo!(config).get_by(user_mod, clauses, opts)
end
defp normalize_user_id_field_value(user_mod, clauses) do
user_id_field = user_mod.pow_user_id_field()
Enum.map(clauses, fn
{^user_id_field, value} when is_binary(value) -> {user_id_field, Schema.normalize_user_id_field_value(value)}
any -> any
end)
end
@doc """
Inserts a changeset to the database.
If succesful, the returned row will be reloaded from the database.
"""
@spec do_insert(changeset(), Config.t()) :: {:ok, user()} | {:error, changeset()}
def do_insert(changeset, config) do
opts = repo_opts(config, [:prefix])
changeset
|> Config.repo!(config).insert(opts)
|> reload_after_write(config)
end
@doc """
Updates a changeset in the database.
If succesful, the returned row will be reloaded from the database.
"""
@spec do_update(changeset(), Config.t()) :: {:ok, user()} | {:error, changeset()}
def do_update(changeset, config) do
opts = repo_opts(config, [:prefix])
changeset
|> Config.repo!(config).update(opts)
|> reload_after_write(config)
end
defp reload_after_write({:error, changeset}, _config), do: {:error, changeset}
defp reload_after_write({:ok, struct}, config) do
# When ecto updates/inserts, has_many :through associations are set to nil.
# So we'll just reload when writes happen.
opts = repo_opts(config, [:prefix])
struct = Config.repo!(config).get!(struct.__struct__, struct.id, opts)
{:ok, struct}
end
# TODO: Remove by 1.1.0
@deprecated "Use `Pow.Config.repo!/1` instead"
defdelegate repo(config), to: Config, as: :repo!
defp repo_opts(config, opts) do
config
|> Config.get(:repo_opts, [])
|> Keyword.take(opts)
end
# TODO: Remove by 1.1.0
@deprecated "Use `Pow.Config.user!/1` instead"
defdelegate user_schema_mod(config), to: Config, as: :user!
end