Source file contract_user.ml
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
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
type error =
| AlreadyRegistered
| IncorrectPassword
| InvalidPasswordProvided of string
| DoesNotExist
type t =
{ id : string
; email : string
; username : string option
; password : string
; status : string
; admin : bool
; confirmed : bool
; created_at : Ptime.t
; updated_at : Ptime.t
}
exception Exception of string
let name = "user"
module type Sig = sig
val search
: ?sort:[ `Desc | `Asc ]
-> ?filter:string
-> int
-> (t list * int) Lwt.t
val find_opt : user_id:string -> t option Lwt.t
val find : user_id:string -> t Lwt.t
(** [find_by_email email] returns a [User.t] if there is a user with email
address [email]. Raises an [{!Exception}] otherwise. *)
val find_by_email : email:string -> t Lwt.t
(** [find_by_email_opt email] returns a [User.t] if there is a user with email
address [email]. *)
val find_by_email_opt : email:string -> t option Lwt.t
val update_password
: ?password_policy:(string -> (unit, string) Result.t)
-> user:t
-> old_password:string
-> new_password:string
-> new_password_confirmation:string
-> unit
-> (t, string) Result.t Lwt.t
val update_details
: user:t
-> email:string
-> username:string option
-> t Lwt.t
(** Set the password of a user without knowing the old password.
This feature is typically used by admins. *)
val set_password
: ?password_policy:(string -> (unit, string) Result.t)
-> user:t
-> password:string
-> password_confirmation:string
-> unit
-> (t, string) Result.t Lwt.t
val create
: email:string
-> password:string
-> username:string option
-> admin:bool
-> confirmed:bool
-> (t, string) result Lwt.t
(** Create and store a user. *)
val create_user
: email:string
-> password:string
-> username:string option
-> t Lwt.t
(** Create and store a user that is also an admin. *)
val create_admin
: email:string
-> password:string
-> username:string option
-> t Lwt.t
(** Create and store new user.
Provide [password_policy] to check whether the password fulfills certain
criteria. *)
val register_user
: ?password_policy:(string -> (unit, string) result)
-> ?username:string
-> email:string
-> password:string
-> password_confirmation:string
-> unit
-> (t, error) Result.t Lwt.t
(** Find user by email if password matches. *)
val login : email:string -> password:string -> (t, error) Result.t Lwt.t
val register : unit -> Core_container.Service.t
include Core_container.Service.Sig
end
module Hashing = struct
let hash ?count plain =
match count, not (Core_configuration.is_production ()) with
| _, true -> Ok (Bcrypt.hash ~count:4 plain |> Bcrypt.string_of_hash)
| Some count, false ->
if count < 4 || count > 31
then Error "Password hashing count has to be between 4 and 31"
else Ok (Bcrypt.hash ~count plain |> Bcrypt.string_of_hash)
| None, false -> Ok (Bcrypt.hash ~count:10 plain |> Bcrypt.string_of_hash)
;;
let matches ~hash ~plain = Bcrypt.verify plain (Bcrypt.hash_of_string hash)
end
let to_sexp
{ id; email; username; status; admin; confirmed; created_at; updated_at; _ }
=
let open Sexplib0.Sexp_conv in
let open Sexplib0.Sexp in
List
[ List [ Atom "id"; sexp_of_string id ]
; List [ Atom "email"; sexp_of_string email ]
; List [ Atom "username"; sexp_of_option sexp_of_string username ]
; List [ Atom "password"; sexp_of_string "********" ]
; List [ Atom "status"; sexp_of_string status ]
; List [ Atom "admin"; sexp_of_bool admin ]
; List [ Atom "confirmed"; sexp_of_bool confirmed ]
; List [ Atom "created_at"; sexp_of_string (Ptime.to_rfc3339 created_at) ]
; List [ Atom "updated_at"; sexp_of_string (Ptime.to_rfc3339 updated_at) ]
]
;;
let pp fmt t = Sexplib0.Sexp.pp_hum fmt (to_sexp t)
let of_yojson json =
let open Yojson.Safe.Util in
try
let id = json |> member "id" |> to_string in
let email = json |> member "email" |> to_string in
let username = json |> member "username" |> to_string_option in
let password = json |> member "password" |> to_string in
let status = json |> member "status" |> to_string in
let admin = json |> member "admin" |> to_bool in
let confirmed = json |> member "confirmed" |> to_bool in
let created_at = json |> member "created_at" |> to_string in
let updated_at = json |> member "updated_at" |> to_string in
match Ptime.of_rfc3339 created_at, Ptime.of_rfc3339 updated_at with
| Ok (created_at, _, _), Ok (updated_at, _, _) ->
Some
{ id
; email
; username
; password
; status
; admin
; confirmed
; created_at
; updated_at
}
| _ -> None
with
| _ -> None
;;
let to_yojson user =
let created_at = Ptime.to_rfc3339 user.created_at in
let updated_at = Ptime.to_rfc3339 user.updated_at in
let list =
[ "id", `String user.id
; "email", `String user.email
; "password", `String user.password
; "status", `String user.status
; "admin", `Bool user.admin
; "confirmed", `Bool user.confirmed
; "created_at", `String created_at
; "updated_at", `String updated_at
]
in
match user.username with
| Some username -> `Assoc (List.cons ("username", `String username) list)
| None -> `Assoc (List.cons ("username", `Null) list)
;;
let confirm user = { user with confirmed = true }
let set_user_password user new_password =
let hash = new_password |> Hashing.hash in
Result.map (fun hash -> { user with password = hash }) hash
;;
let set_user_details user ~email ~username =
{ user with email = String.lowercase_ascii email; username }
;;
let is_admin user = user.admin
let is_owner user id = String.equal user.id id
let is_confirmed user = user.confirmed
let matches_password password user =
Hashing.matches ~hash:user.password ~plain:password
;;
let default_password_policy password =
if String.length password >= 8
then Ok ()
else Error "Password has to contain at least 8 characters"
;;
let validate_new_password ~password ~password_confirmation ~password_policy =
let is_same =
if String.equal password password_confirmation
then Ok ()
else Error "Password confirmation doesn't match provided password"
in
let complies_with_policy = password_policy password in
match is_same, complies_with_policy with
| Ok (), Ok () -> Ok ()
| Error msg, _ -> Error msg
| _, Error msg -> Error msg
;;
let validate_change_password
user
~old_password
~new_password
~new_password_confirmation
~password_policy
=
let matches_old_password =
match matches_password old_password user with
| true -> Ok ()
| false -> Error "Invalid current password provided"
in
let new_password_valid =
validate_new_password
~password:new_password
~password_confirmation:new_password_confirmation
~password_policy
in
match matches_old_password, new_password_valid with
| Ok (), Ok () -> Ok ()
| Error msg, _ -> Error msg
| _, Error msg -> Error msg
;;
let make ~email ~password ~username ~admin ~confirmed =
let hash = password |> Hashing.hash in
let now = Ptime_clock.now () in
Result.map
(fun hash ->
{ id = Uuidm.v `V4 |> Uuidm.to_string
;
email = String.lowercase_ascii email
; password = hash
; username
; admin
; confirmed
; status = "active"
; created_at = now
; updated_at = now
})
hash
;;