package sihl-user

  1. Overview
  2. Docs
User service implementations for Sihl

Install

Dune Dependency

Authors

Maintainers

Sources

1.0.0-rc2.tar.gz
md5=ca63753c880ab1d043bc3653909f049f
sha512=55579ced17dc7a80e1e12e45c0ecfc673d194dbbaefc4eb77f525e9d3f6eae4e34773334014a413d7ca8f883a9e56fdd6ef47d80b4fd066230f31021738153fb

doc/src/sihl-user/sihl_user.ml.html

Source file sihl_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
include Sihl.Contract.User

let log_src = Logs.Src.create ("sihl.service." ^ Sihl.Contract.User.name)

module Logs = (val Logs.src_log log_src : Logs.LOG)

module Make (Repo : User_repo.Sig) : Sihl.Contract.User.Sig = struct
  let find_opt user_id = Repo.get user_id

  let find user_id =
    let%lwt m_user = find_opt user_id in
    match m_user with
    | Some user -> Lwt.return user
    | None ->
      Logs.err (fun m -> m "User not found with id %s" user_id);
      raise (Sihl.Contract.User.Exception "User not found")
  ;;

  let find_by_email_opt email = Repo.get_by_email email

  let find_by_email email =
    let%lwt user = find_by_email_opt email in
    match user with
    | Some user -> Lwt.return user
    | None ->
      Logs.err (fun m -> m "User not found with email %s" email);
      raise (Sihl.Contract.User.Exception "User not found")
  ;;

  let search ?(sort = `Desc) ?filter ?(limit = 50) ?(offset = 0) () =
    Repo.search sort filter ~limit ~offset
  ;;

  let update_details ~user:_ ~email:_ ~username:_ = failwith "update()"

  let update ?email ?username ?name ?given_name ?status user =
    let updated =
      { user with
        email = Option.value ~default:user.email email
      ; username =
          (match username with
          | Some username -> Some username
          | None -> user.username)
      ; name =
          (match name with
          | Some name -> Some name
          | None -> user.name)
      ; given_name =
          (match given_name with
          | Some given_name -> Some given_name
          | None -> user.given_name)
      ; status = Option.value ~default:user.status status
      }
    in
    let%lwt () = Repo.update updated in
    find user.id
  ;;

  let update_password
      ?(password_policy = default_password_policy)
      user
      ~old_password
      ~new_password
      ~new_password_confirmation
    =
    match
      validate_change_password
        user
        ~old_password
        ~new_password
        ~new_password_confirmation
        ~password_policy
    with
    | Ok () ->
      let updated_user =
        match set_user_password user new_password with
        | Ok user -> user
        | Error msg ->
          Logs.err (fun m ->
              m "Can not update password of user '%s': %s" user.email msg);
          raise (Sihl.Contract.User.Exception msg)
      in
      let%lwt () = Repo.update updated_user in
      find user.id |> Lwt.map Result.ok
    | Error msg -> Lwt.return @@ Error msg
  ;;

  let set_password
      ?(password_policy = default_password_policy)
      user
      ~password
      ~password_confirmation
    =
    let%lwt result =
      validate_new_password ~password ~password_confirmation ~password_policy
      |> Lwt.return
    in
    match result with
    | Error msg -> Lwt.return @@ Error msg
    | Ok () ->
      let%lwt result = Repo.get user.id in
      (* Re-fetch user to make sure that we have an up-to-date model *)
      let%lwt user =
        match result with
        | Some user -> Lwt.return user
        | None -> raise (Sihl.Contract.User.Exception "Failed to create user")
      in
      let updated_user =
        match set_user_password user password with
        | Ok user -> user
        | Error msg ->
          Logs.err (fun m ->
              m "Can not set password of user %s: %s" user.email msg);
          raise (Sihl.Contract.User.Exception msg)
      in
      let%lwt () = Repo.update updated_user in
      find user.id |> Lwt.map Result.ok
  ;;

  let create ~email ~password ~username ~name ~given_name ~admin ~confirmed =
    let user =
      make ~email ~password ~username ~name ~given_name ~admin ~confirmed
    in
    match user with
    | Ok user ->
      let%lwt () = Repo.insert user in
      let%lwt user = find user.id in
      Lwt.return (Ok user)
    | Error msg -> raise (Sihl.Contract.User.Exception msg)
  ;;

  let create_user ?username ?name ?given_name ~password email =
    let%lwt user =
      create
        ~password
        ~username
        ~name
        ~given_name
        ~admin:false
        ~confirmed:false
        ~email
    in
    match user with
    | Ok user -> Lwt.return user
    | Error msg -> raise (Sihl.Contract.User.Exception msg)
  ;;

  let create_admin ?username ?name ?given_name ~password email =
    let%lwt user = Repo.get_by_email email in
    let%lwt () =
      match user with
      | Some _ ->
        Logs.err (fun m ->
            m "Can not create admin '%s' since the email is already taken" email);
        raise (Sihl.Contract.User.Exception "Email already taken")
      | None -> Lwt.return ()
    in
    let%lwt user =
      create
        ~password
        ~username
        ~name
        ~given_name
        ~admin:true
        ~confirmed:true
        ~email
    in
    match user with
    | Ok user -> Lwt.return user
    | Error msg ->
      Logs.err (fun m -> m "Can not create admin '%s': %s" email msg);
      raise (Sihl.Contract.User.Exception msg)
  ;;

  let register_user
      ?(password_policy = default_password_policy)
      ?username
      ?name
      ?given_name
      email
      ~password
      ~password_confirmation
    =
    match
      validate_new_password ~password ~password_confirmation ~password_policy
    with
    | Error msg -> Lwt_result.fail @@ `Invalid_password_provided msg
    | Ok () ->
      let%lwt user = find_by_email_opt email in
      (match user with
      | None ->
        create_user ?username ?name ?given_name ~password email
        |> Lwt.map Result.ok
      | Some _ -> Lwt_result.fail `Already_registered)
  ;;

  let login email ~password =
    let open Sihl.Contract.User in
    let%lwt user = find_by_email_opt email in
    match user with
    | None -> Lwt_result.fail `Does_not_exist
    | Some user ->
      if matches_password password user
      then Lwt_result.return user
      else Lwt_result.fail `Incorrect_password
  ;;

  let start () = Lwt.return ()
  let stop () = Lwt.return ()

  let create_admin_cmd =
    Sihl.Command.make
      ~name:"user.admin"
      ~help:"<email> <password>"
      ~description:"Creates a user with admin privileges."
      (fun args ->
        match args with
        | [ email; password ] ->
          let%lwt () = start () in
          create_admin ~password email |> Lwt.map ignore |> Lwt.map Option.some
        | _ -> Lwt.return None)
  ;;

  let lifecycle =
    Sihl.Container.create_lifecycle
      Sihl.Contract.User.name
      ~dependencies:(fun () -> Repo.lifecycles)
      ~start
      ~stop
  ;;

  let register () =
    Repo.register_migration ();
    Repo.register_cleaner ();
    Sihl.Container.Service.create ~commands:[ create_admin_cmd ] lifecycle
  ;;

  module Web = struct
    let user_from_session = Web.user_from_session find_opt
    let user_from_token = Web.user_from_token find_opt
  end
end

module PostgreSql =
  Make (User_repo.MakePostgreSql (Sihl.Database.Migration.PostgreSql))

module MariaDb = Make (User_repo.MakeMariaDb (Sihl.Database.Migration.MariaDb))

module Password_reset = struct
  module MakePostgreSql (TokenService : Sihl.Contract.Token.Sig) =
    Password_reset.Make (PostgreSql) (TokenService)

  module MakeMariaDb (TokenService : Sihl.Contract.Token.Sig) =
    Password_reset.Make (MariaDb) (TokenService)
end
OCaml

Innovation. Community. Security.