File: hex.user.ex

package info (click to toggle)
erlang-hex 2.0.6-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,204 kB
  • sloc: erlang: 2,950; sh: 203; makefile: 10
file content (349 lines) | stat: -rw-r--r-- 9,600 bytes parent folder | download
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
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
defmodule Mix.Tasks.Hex.User do
  use Mix.Task

  @shortdoc "Manages your Hex user account"

  @moduledoc """
  Hex user tasks.

  ## Register a new user

      $ mix hex.user register

  ## Print the current user

      $ mix hex.user whoami

  ## Authorize a new user

  Authorizes a new user on the local machine by generating a new API key and
  storing it in the Hex config.

      $ mix hex.user auth [--key-name KEY_NAME]

  ### Command line options

    * `--key-name KEY_NAME` - By default Hex will base the key name on your machine's
      hostname, use this option to give your own name.

  ## Deauthorize the user

  Deauthorizes the user from the local machine by removing the API key from the Hex config.

      $ mix hex.user deauth

  ## Generate user key

  Generates an unencrypted API key for your account. Keys generated by this command will be owned
  by you and will give access to your private resources, do not share this key with anyone. For
  keys that will be shared by organization members use `mix hex.organization key` instead. By
  default this command sets the `api:write` permission which allows write access to the API,
  it can be overridden with the `--permission` flag.

      $ mix hex.user key generate

  ### Command line options

    * `--key-name KEY_NAME` - By default Hex will base the key name on your machine's
      hostname, use this option to give your own name.

    * `--permission PERMISSION` - Sets the permissions on the key, this option can be given
      multiple times, possible values are:
      * `api:read` - API read access.
      * `api:write` - API write access.
      * `repository:ORGANIZATION_NAME` - Access to given organization repository.
      * `repositories` - Access to repositories for all organizations you are member of.

  ## Revoke key

  Removes given key from account.

  The key can no longer be used to authenticate API requests.

      $ mix hex.user key revoke KEY_NAME

  ## Revoke all keys

  Revoke all keys from your account.

      $ mix hex.user key revoke --all

  ## List keys

  Lists all keys associated with your account.

      $ mix hex.user key list

  ## Reset user account password

  Starts the process for resetting account password.

      $ mix hex.user reset_password account

  ## Reset local password

  Updates the local password for your local authentication credentials.

      $ mix hex.user reset_password local
  """
  @behaviour Hex.Mix.TaskDescription

  @switches [
    all: :boolean,
    key_name: :string,
    permission: [:string, :keep]
  ]

  @impl true
  def run(args) do
    Hex.start()
    {opts, args} = OptionParser.parse!(args, strict: @switches)

    case args do
      ["register"] ->
        register()

      ["whoami"] ->
        whoami()

      ["auth"] ->
        auth(opts)

      ["deauth"] ->
        deauth()

      ["key", "generate"] ->
        key_generate(opts)

      ["key", "revoke", key_name] ->
        key_revoke(key_name)

      ["key", "revoke"] ->
        if opts[:all], do: key_revoke_all(), else: invalid_args()

      ["key", "list"] ->
        key_list()

      ["reset_password", "account"] ->
        reset_account_password()

      ["reset_password", "local"] ->
        reset_local_password()

      _ ->
        invalid_args()
    end
  end

  @impl true
  def tasks() do
    [
      {"register", "Register a new user"},
      {"whoami", "Prints the current user"},
      {"auth", "Authorize a new user"},
      {"deauth", "Deauthorize the user"},
      {"key generate", "Generate user key"},
      {"key revoke KEY_NAME", "Removes given key from account"},
      {"key revoke --all", "Revoke all keys"},
      {"key list", "Lists all keys associated with your account"},
      {"reset_password account", "Reset user account password"},
      {"reset_password local", "Reset local password"}
    ]
  end

  defp invalid_args() do
    Mix.raise("""
    Invalid arguments, expected one of:

    mix hex.user register
    mix hex.user whoami
    mix hex.user auth
    mix hex.user deauth
    mix hex.user key generate
    mix hex.user key revoke KEY_NAME
    mix hex.user key revoke --all
    mix hex.user key list
    mix hex.user reset_password account
    mix hex.user reset_password local
    """)
  end

  defp whoami() do
    auth = Mix.Tasks.Hex.auth_info(:read)

    case Hex.API.User.me(auth) do
      {:ok, {code, body, _}} when code in 200..299 ->
        Hex.Shell.info(body["username"])

      other ->
        Hex.Shell.error("Failed to auth")
        Hex.Utils.print_error_result(other)
    end
  end

  defp reset_account_password() do
    name = Hex.Shell.prompt("Username or Email:") |> String.trim()

    case Hex.API.User.password_reset(name) do
      {:ok, {code, _, _}} when code in 200..299 ->
        Hex.Shell.info(
          "We’ve sent you an email containing a link that will allow you to reset " <>
            "your account password for the next 24 hours. Please check your spam folder if the " <>
            "email doesn’t appear within a few minutes."
        )

      other ->
        Hex.Shell.error("Initiating password reset for #{name} failed")
        Hex.Utils.print_error_result(other)
    end
  end

  defp reset_local_password() do
    encrypted_key = Hex.State.fetch!(:api_key_write)
    read_key = Hex.State.fetch!(:api_key_read)

    unless encrypted_key do
      Mix.raise("No authorized user found. Run `mix hex.user auth`")
    end

    decrypted_key = Mix.Tasks.Hex.prompt_decrypt_key(encrypted_key, "Current local password")
    Mix.Tasks.Hex.prompt_encrypt_key(decrypted_key, read_key, "New local password")
    Hex.Shell.info("Password changed")
  end

  defp deauth() do
    Mix.Tasks.Hex.update_keys(nil, nil)
    deauth_organizations()

    Hex.Shell.info(
      "Authentication credentials removed from the local machine. " <>
        "To authenticate again, run `mix hex.user auth` " <>
        "or create a new user with `mix hex.user register`"
    )
  end

  defp deauth_organizations() do
    Hex.State.fetch!(:repos)
    |> Enum.reject(fn {name, _config} -> String.starts_with?(name, "hexpm:") end)
    |> Map.new()
    |> Hex.Config.update_repos()
  end

  defp register() do
    Hex.Shell.info("""
    By registering an account on Hex.pm you accept all our \
    policies and terms of service found at:
    https://hex.pm/policies/codeofconduct
    https://hex.pm/policies/termsofservice
    https://hex.pm/policies/privacy
    """)

    username = Hex.Shell.prompt("Username:") |> String.trim()
    email = Hex.Shell.prompt("Email:") |> String.trim()
    password = Mix.Tasks.Hex.password_get("Account password:") |> String.trim()

    confirm = Mix.Tasks.Hex.password_get("Account password (confirm):") |> String.trim()

    if password != confirm do
      Mix.raise("Entered passwords do not match")
    end

    Hex.Shell.info("Registering...")
    create_user(username, email, password)
  end

  defp create_user(username, email, password) do
    case Hex.API.User.new(username, email, password) do
      {:ok, {code, _, _}} when code in 200..299 ->
        Mix.Tasks.Hex.generate_all_user_keys(username, password)

        Hex.Shell.info(
          "You are required to confirm your email to access your account, " <>
            "a confirmation email has been sent to #{email}"
        )

      other ->
        Hex.Shell.error("Registration of user #{username} failed")
        Hex.Utils.print_error_result(other)
    end
  end

  defp auth(opts) do
    Mix.Tasks.Hex.auth(opts)
  end

  defp key_revoke_all() do
    auth = Mix.Tasks.Hex.auth_info(:write)

    Hex.Shell.info("Revoking all keys...")

    case Hex.API.Key.delete_all(auth) do
      {:ok, {code, %{"name" => _, "authing_key" => true}, _headers}} when code in 200..299 ->
        Mix.Tasks.Hex.User.run(["deauth"])

      other ->
        Hex.Shell.error("Key revocation failed")
        Hex.Utils.print_error_result(other)
    end
  end

  defp key_revoke(key) do
    auth = Mix.Tasks.Hex.auth_info(:write)

    Hex.Shell.info("Revoking key #{key}...")

    case Hex.API.Key.delete(key, auth) do
      {:ok, {200, %{"name" => ^key, "authing_key" => true}, _headers}} ->
        Mix.Tasks.Hex.User.run(["deauth"])
        :ok

      {:ok, {code, _body, _headers}} when code in 200..299 ->
        :ok

      other ->
        Hex.Shell.error("Key revocation failed")
        Hex.Utils.print_error_result(other)
    end
  end

  # TODO: print permissions
  defp key_list() do
    auth = Mix.Tasks.Hex.auth_info(:read)

    case Hex.API.Key.get(auth) do
      {:ok, {code, body, _headers}} when code in 200..299 ->
        values =
          Enum.map(body, fn %{"name" => name, "inserted_at" => time} ->
            [name, time]
          end)

        Mix.Tasks.Hex.print_table(["Name", "Created at"], values)

      other ->
        Hex.Shell.error("Key fetching failed")
        Hex.Utils.print_error_result(other)
    end
  end

  defp key_generate(opts) do
    username = Hex.Shell.prompt("Username:") |> String.trim()
    password = Mix.Tasks.Hex.password_get("Account password:") |> String.trim()
    key_name = Mix.Tasks.Hex.general_key_name(opts[:key_name])
    permissions = Keyword.get_values(opts, :permission)
    permissions = Mix.Tasks.Hex.convert_permissions(permissions) || [%{"domain" => "api"}]
    Hex.Shell.info("Generating key...")

    result =
      Mix.Tasks.Hex.generate_user_key(
        key_name,
        permissions,
        user: username,
        pass: password
      )

    case result do
      {:ok, secret} -> Hex.Shell.info(secret)
      :error -> :ok
    end
  end
end