pcli 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +13 -0
  3. data/CHANGELOG.md +5 -0
  4. data/Gemfile +12 -0
  5. data/Gemfile.lock +176 -0
  6. data/Rakefile +12 -0
  7. data/exe/pcli +6 -0
  8. data/lib/pcli/api_request.rb +60 -0
  9. data/lib/pcli/api_response.rb +82 -0
  10. data/lib/pcli/command_output.rb +23 -0
  11. data/lib/pcli/container.rb +54 -0
  12. data/lib/pcli/depends.rb +43 -0
  13. data/lib/pcli/main.rb +22 -0
  14. data/lib/pcli/options.rb +41 -0
  15. data/lib/pcli/output/padded.rb +32 -0
  16. data/lib/pcli/output/server_error.rb +24 -0
  17. data/lib/pcli/output.rb +6 -0
  18. data/lib/pcli/pl.rb +15 -0
  19. data/lib/pcli/services/all_commands.rb +52 -0
  20. data/lib/pcli/services/all_steps.rb +23 -0
  21. data/lib/pcli/services/api.rb +82 -0
  22. data/lib/pcli/services/api_manager.rb +28 -0
  23. data/lib/pcli/services/app.rb +31 -0
  24. data/lib/pcli/services/authenticate.rb +60 -0
  25. data/lib/pcli/services/client.rb +24 -0
  26. data/lib/pcli/services/cmd.rb +13 -0
  27. data/lib/pcli/services/commander.rb +38 -0
  28. data/lib/pcli/services/commands/login.rb +22 -0
  29. data/lib/pcli/services/commands/logout.rb +24 -0
  30. data/lib/pcli/services/commands/templates/change.rb +95 -0
  31. data/lib/pcli/services/commands/templates/list.rb +52 -0
  32. data/lib/pcli/services/commands/user/change.rb +114 -0
  33. data/lib/pcli/services/commands/user/password.rb +77 -0
  34. data/lib/pcli/services/commands/user/show.rb +47 -0
  35. data/lib/pcli/services/commands/user/totp.rb +78 -0
  36. data/lib/pcli/services/commands/users/change.rb +134 -0
  37. data/lib/pcli/services/commands/users/create.rb +67 -0
  38. data/lib/pcli/services/commands/users/list.rb +52 -0
  39. data/lib/pcli/services/commands/users/remove.rb +85 -0
  40. data/lib/pcli/services/prompt.rb +21 -0
  41. data/lib/pcli/services/qr.rb +18 -0
  42. data/lib/pcli/services/steps/authenticate.rb +17 -0
  43. data/lib/pcli/services/steps/connect.rb +35 -0
  44. data/lib/pcli/services/steps/greeting.rb +18 -0
  45. data/lib/pcli/services/steps/main.rb +51 -0
  46. data/lib/pcli/services/steps/valediction.rb +21 -0
  47. data/lib/pcli/simple_spinner_bar.rb +38 -0
  48. data/lib/pcli/step.rb +69 -0
  49. data/lib/pcli/util/cli.rb +32 -0
  50. data/lib/pcli/util/hash.rb +25 -0
  51. data/lib/pcli/version.rb +5 -0
  52. data/lib/pcli.rb +24 -0
  53. metadata +389 -0
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ module Commands
6
+ module User
7
+ class Password < Dry::CLI::Command
8
+ include Depends.on(
9
+ 'api',
10
+ 'api_manager',
11
+ 'input',
12
+ 'output',
13
+ 'screen',
14
+ 'prompt'
15
+ )
16
+
17
+ desc 'Rotate the password of the logged-in administrator.'
18
+
19
+ def call(*)
20
+ spinner = nil
21
+ response = api_manager.ensure_authenticated do
22
+ spinner = SimpleSpinnerBar.start('Retrieving user...', output)
23
+ r = api.me
24
+ if r.failure?
25
+ spinner.failure
26
+ end
27
+ r
28
+ end
29
+
30
+ if response.success?
31
+ spinner.success
32
+ else
33
+ output.puts
34
+ Output::ServerError.show(response, output, screen)
35
+ return CommandOutput.continue
36
+ end
37
+
38
+ output.puts
39
+ output.puts "This action will regenerate #{response.json['name']}'s password and display it once."
40
+ output.puts
41
+
42
+ if prompt.no?(Pl.red('Are you sure you want to continue?'))
43
+ output.puts 'Command aborted.'
44
+ return CommandOutput.continue
45
+ end
46
+
47
+ output.puts
48
+ spinner = nil
49
+ response = api_manager.ensure_authenticated do
50
+ spinner = SimpleSpinnerBar.start('Regenerating password...', output)
51
+ r = api.rotate_password
52
+ if r.failure?
53
+ spinner.failure
54
+ end
55
+ r
56
+ end
57
+
58
+ if response.success?
59
+ spinner.success("Password #{Pl.green('regenerated')}")
60
+ output.puts
61
+ output.puts Pl.yellow('New password: ') + response.json['password']
62
+ output.puts
63
+ output.print 'Copy the password and press any key when finished...'
64
+ input.gets
65
+ output.clear_screen
66
+ else
67
+ output.puts
68
+ Output::ServerError.show(response, output, screen)
69
+ end
70
+
71
+ CommandOutput.continue
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ module Commands
6
+ module User
7
+ class Show < Dry::CLI::Command
8
+ include Depends.on(
9
+ 'api',
10
+ 'api_manager',
11
+ 'output',
12
+ 'screen'
13
+ )
14
+
15
+ desc 'Display information about the logged-in administrator.'
16
+
17
+ def call(*)
18
+ response = api_manager.ensure_authenticated do
19
+ spinner = SimpleSpinnerBar.start('Retrieving user...', output)
20
+ r = api.me
21
+ if r.failure?
22
+ spinner.failure
23
+ else
24
+ spinner.success
25
+ end
26
+ r
27
+ end
28
+
29
+ if response.success?
30
+ output.puts
31
+ output.puts TTY::Table.new(rows: [
32
+ [Pl.bold('ID'), response.json['id']],
33
+ [Pl.bold('Name'), response.json['name']],
34
+ [Pl.bold('Username'), response.json['username']]
35
+ ]).render(:ascii)
36
+ else
37
+ output.puts
38
+ Output::ServerError.show(response, output, screen)
39
+ end
40
+
41
+ CommandOutput.continue
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ module Commands
6
+ module User
7
+ class Totp < Dry::CLI::Command
8
+ include Depends.on(
9
+ 'api',
10
+ 'api_manager',
11
+ 'input',
12
+ 'output',
13
+ 'screen',
14
+ 'prompt',
15
+ 'qr'
16
+ )
17
+
18
+ desc 'Rotate the TOTP secret of the logged-in administrator.'
19
+
20
+ def call(*)
21
+ spinner = nil
22
+ response = api_manager.ensure_authenticated do
23
+ spinner = SimpleSpinnerBar.start('Retrieving user...', output)
24
+ r = api.me
25
+ if r.failure?
26
+ spinner.failure
27
+ else
28
+ spinner.success
29
+ end
30
+ r
31
+ end
32
+
33
+ if response.failure?
34
+ output.puts
35
+ Output::ServerError.show(response, output, screen)
36
+ return CommandOutput.continue
37
+ end
38
+
39
+ output.puts
40
+ output.puts "This action will regenerate #{response.json['name']}'s TOTP secret and display the corresponding QR code once."
41
+ output.puts
42
+
43
+ if prompt.no?(Pl.red('Are you sure you want to continue?'))
44
+ output.puts 'Command aborted.'
45
+ return CommandOutput.continue
46
+ end
47
+
48
+ output.puts
49
+ response = api_manager.ensure_authenticated do
50
+ spinner = SimpleSpinnerBar.start('Regenerating TOTP...', output)
51
+ r = api.rotate_totp
52
+ if r.failure?
53
+ spinner.failure
54
+ end
55
+ r
56
+ end
57
+
58
+ if response.success?
59
+ spinner.success("TOTP #{Pl.green('regenerated')}")
60
+ output.puts
61
+ output.puts Pl.yellow('New TOTP code:')
62
+ qr.show(response.json['totpUrl'])
63
+ output.puts
64
+ output.print 'Scan the QR with your authenticator app and press any key when finished...'
65
+ input.gets
66
+ output.clear_screen
67
+ else
68
+ output.puts
69
+ Output::ServerError.show(response, output, screen)
70
+ end
71
+
72
+ CommandOutput.continue
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ module Commands
6
+ module Users
7
+ class Change < Dry::CLI::Command
8
+ @field_options = []
9
+
10
+ class << self
11
+ attr_reader :field_options
12
+
13
+ def field_option(name, *args, **kw_args)
14
+ @field_options << name
15
+ option name, *args, **kw_args
16
+ end
17
+ end
18
+
19
+ include Depends.on(
20
+ 'api',
21
+ 'api_manager',
22
+ 'output',
23
+ 'screen',
24
+ 'prompt'
25
+ )
26
+
27
+ desc 'Change name or username of an administrator.'
28
+
29
+ argument :user, desc: 'The user id or username to change.'
30
+
31
+ field_option :name, type: :boolean, default: nil, desc: 'Whether to change the name'
32
+ field_option :username, type: :boolean, default: nil, desc: 'Whether to change the username'
33
+
34
+ option :all, type: :boolean, default: false, desc: 'Change all fields'
35
+
36
+ def call(user: nil, all:, **args)
37
+ field_args = args.slice(*self.class.field_options)
38
+
39
+ spinner = nil
40
+ response = api_manager.ensure_authenticated do
41
+ spinner = SimpleSpinnerBar.start('Retrieving users...', output)
42
+ r = api.users
43
+ if r.failure?
44
+ spinner.failure
45
+ else
46
+ spinner.success
47
+ end
48
+ r
49
+ end
50
+
51
+ if response.failure?
52
+ output.puts
53
+ Output::ServerError.show(response, output, screen)
54
+ return
55
+ end
56
+
57
+ users = response.json
58
+
59
+ unless user
60
+ choices = users.map { |u| { name: "#{u['name']} (#{u['username']})", value: u['id'] } }
61
+ user = prompt.select('Which user would you like to change?', choices)
62
+ end
63
+
64
+ user = users.find { |u| u['id'] === user || u['username'].downcase === user.to_s.downcase }
65
+ unless user
66
+ output.puts(Pl.yellow('That user does not exist.'))
67
+ return CommandOutput.continue
68
+ end
69
+
70
+ fields = nil
71
+
72
+ if all == false && field_args.empty?
73
+ fields = prompt
74
+ .multi_select('Which fields do you want to change?', self.class.field_options)
75
+ .map { |n| [n, true] }
76
+ .to_h
77
+ else
78
+ begin
79
+ fields = Util::Cli.analyze_fields_flags(all, self.class.field_options, field_args)
80
+ rescue Util::Cli::FieldsFlagsError => e
81
+ output.puts Pl.yellow(e.message)
82
+ return CommandOutput.continue
83
+ end
84
+ end
85
+
86
+ payload = {}
87
+
88
+ if fields[:name]
89
+ name = prompt.ask('Name', default: user['name'])
90
+ payload['name'] = name if name != user['name']
91
+ end
92
+
93
+ if fields[:username]
94
+ username = prompt.ask('Username', default: user['username'])
95
+ payload['username'] = username if username != user['username']
96
+ end
97
+
98
+ if payload.empty?
99
+ output.puts Pl.yellow('Nothing to change!')
100
+ return CommandOutput.continue
101
+ end
102
+
103
+ response = api_manager.ensure_authenticated do
104
+ spinner = SimpleSpinnerBar.start('Changing user...', output)
105
+ r = api.change_user(user['id'], payload)
106
+ if r.failure?
107
+ spinner.failure
108
+ end
109
+ r
110
+ end
111
+
112
+ if response.success?
113
+ spinner.success("User #{Pl.green('changed')}")
114
+
115
+ output.puts
116
+ output.puts TTY::Table.new(rows: [
117
+ [Pl.bold('ID'), response.json['id']],
118
+ [Pl.bold('Name'), response.json['name']],
119
+ [Pl.bold('Username'), response.json['username']]
120
+ ]).render(:ascii)
121
+ else
122
+ spinner.failure
123
+
124
+ output.puts
125
+ Output::ServerError.show(response, output, screen)
126
+ end
127
+
128
+ CommandOutput.continue
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ module Commands
6
+ module Users
7
+ class Create < Dry::CLI::Command
8
+ include Depends.on(
9
+ 'api',
10
+ 'api_manager',
11
+ 'input',
12
+ 'output',
13
+ 'screen',
14
+ 'prompt'
15
+ )
16
+
17
+ desc 'Create a new administrator.'
18
+
19
+ def call(**args)
20
+ payload = {}
21
+
22
+ name = prompt.ask('Name')
23
+ payload['name'] = name
24
+
25
+ username = prompt.ask('Username')
26
+ payload['username'] = username
27
+
28
+ spinner = nil
29
+ response = api_manager.ensure_authenticated do
30
+ spinner = SimpleSpinnerBar.start('Creating user...', output)
31
+ r = api.create_user(payload)
32
+ if r.failure?
33
+ spinner.failure
34
+ end
35
+ r
36
+ end
37
+
38
+ if response.success?
39
+ spinner.success("User #{Pl.green('created')}")
40
+
41
+ output.puts
42
+ output.puts Pl.yellow('New password: ') + response.json['password']
43
+ output.puts
44
+ output.print 'Copy the password and press any key when finished...'
45
+ input.gets
46
+ output.clear_screen
47
+
48
+ output.puts
49
+ output.puts TTY::Table.new(rows: [
50
+ [Pl.bold('ID'), response.json['id']],
51
+ [Pl.bold('Name'), response.json['name']],
52
+ [Pl.bold('Username'), response.json['username']]
53
+ ]).render(:ascii)
54
+ else
55
+ spinner.failure
56
+
57
+ output.puts
58
+ Output::ServerError.show(response, output, screen)
59
+ end
60
+
61
+ CommandOutput.continue
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ module Commands
6
+ module Users
7
+ class List < Dry::CLI::Command
8
+ include Depends.on(
9
+ 'api',
10
+ 'api_manager',
11
+ 'output',
12
+ 'screen'
13
+ )
14
+
15
+ desc 'Get a list of all admin users.'
16
+
17
+ def call(*)
18
+ spinner = nil
19
+ response = api_manager.ensure_authenticated do
20
+ spinner = SimpleSpinnerBar.start('Retrieving users...', output)
21
+ r = api.users
22
+ if r.failure?
23
+ spinner.failure
24
+ else
25
+ spinner.success
26
+ end
27
+ r
28
+ end
29
+
30
+ if response.success?
31
+ spinner.success
32
+ output.puts
33
+ output.puts TTY::Table.new(
34
+ header: [
35
+ Pl.bold('ID'), Pl.bold('Username'), Pl.bold('Name')
36
+ ],
37
+ rows: response.json.map { |u| [u['id'], u['username'], u['name']] }
38
+ ).render(:ascii)
39
+ else
40
+ spinner.failure
41
+
42
+ output.puts
43
+ Output::ServerError.show(response, output, screen)
44
+ end
45
+
46
+ CommandOutput.continue
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ module Commands
6
+ module Users
7
+ class Remove < Dry::CLI::Command
8
+ include Depends.on(
9
+ 'api',
10
+ 'api_manager',
11
+ 'input',
12
+ 'output',
13
+ 'screen',
14
+ 'prompt'
15
+ )
16
+
17
+ desc 'Remove an administrator.'
18
+
19
+ argument :user, desc: 'The user to delete.'
20
+
21
+ def call(user: nil, **args)
22
+ spinner = nil
23
+ response = api_manager.ensure_authenticated do
24
+ spinner = SimpleSpinnerBar.start('Retrieving users...', output)
25
+ r = api.users
26
+ if r.failure?
27
+ spinner.failure
28
+ else
29
+ spinner.success
30
+ end
31
+ r
32
+ end
33
+
34
+ if response.failure?
35
+ output.puts
36
+ Output::ServerError.show(response, output, screen)
37
+ return
38
+ end
39
+
40
+ users = response.json
41
+
42
+ unless user
43
+ choices = users.map { |u| { name: "#{u['name']} (#{u['username']})", value: u['id'] } }
44
+ user = prompt.select('Which user would you like to remove?', choices)
45
+ end
46
+
47
+ user = users.find { |u| u['id'] === user || u['username'].downcase === user.to_s.downcase }
48
+ unless user
49
+ output.puts(Pl.yellow('That user does not exist.'))
50
+ return CommandOutput.continue
51
+ end
52
+
53
+ output.puts
54
+ output.puts "This action will permanently remove #{user['name']} as an administrator."
55
+ output.puts
56
+
57
+ if prompt.no?(Pl.red('Are you sure you want to continue?'))
58
+ output.puts 'Command aborted.'
59
+ return CommandOutput.continue
60
+ end
61
+
62
+ spinner = nil
63
+ response = api_manager.ensure_authenticated do
64
+ spinner = SimpleSpinnerBar.start('Removing user...', output)
65
+ r = api.remove_user(user['id'])
66
+ if r.failure?
67
+ spinner.failure
68
+ end
69
+ r
70
+ end
71
+
72
+ if response.success?
73
+ spinner.success("User #{Pl.green('removed')}")
74
+ else
75
+ output.puts
76
+ Output::ServerError.show(response, output, screen)
77
+ end
78
+
79
+ CommandOutput.continue
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ class Prompt
6
+ delegate_missing_to :base
7
+
8
+ def initialize(input:, output:)
9
+ @base = TTY::Prompt.new(input: input, output: output)
10
+ end
11
+
12
+ def self.dependencies
13
+ %w[input output]
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :base
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ class Qr
6
+ include Depends.on('cmd', 'output', 'which')
7
+
8
+ def show(content)
9
+ if which.exist?('qr')
10
+ out, err = cmd.new(pty: true, printer: :null).run(" qr '#{content}'")
11
+ output.puts out
12
+ else
13
+ output.puts RQRCode::QRCode.new(code).as_ansi
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ module Steps
6
+ class Authenticate < Step
7
+ include Depends.on('authenticate')
8
+
9
+ spaced
10
+
11
+ def run(_prev)
12
+ authenticate.run ? success : failure
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ module Steps
6
+ class Connect < Step
7
+ include Depends.on(
8
+ 'config.endpoint',
9
+ 'api',
10
+ 'output',
11
+ 'screen'
12
+ )
13
+
14
+ spaced
15
+
16
+ def run(_prev)
17
+ spinner = SimpleSpinnerBar.start("Connecting to #{endpoint}", output)
18
+
19
+ response = api.info
20
+
21
+ if response.success?
22
+ v = response.json['version']
23
+ spinner.success("#{Pl.green('Connected')} to #{endpoint}, #{Pl.yellow("v#{v}")}")
24
+ success
25
+ else
26
+ spinner.failure
27
+ output.puts
28
+ Output::ServerError.show(response, output, screen)
29
+ failure
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ module Steps
6
+ class Greeting < Step
7
+ include Depends.on('output')
8
+
9
+ spaced
10
+
11
+ def run(_prev)
12
+ output.puts "The Ponsqb Admin CLI #{Pl.green("v#{Pcli::VERSION}")}, at your service!"
13
+ success
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end