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,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ class AllCommands
6
+ include Depends.on(
7
+ user_show: 'commands.user.show',
8
+ user_change: 'commands.user.change',
9
+ user_password: 'commands.user.password',
10
+ user_totp: 'commands.user.totp',
11
+ users_list: 'commands.users.list',
12
+ users_create: 'commands.users.create',
13
+ users_change: 'commands.users.change',
14
+ users_remove: 'commands.users.remove',
15
+ templates_list: 'commands.templates.list',
16
+ templates_change: 'commands.templates.change',
17
+ login: 'commands.login',
18
+ logout: 'commands.logout',
19
+ )
20
+
21
+ def registry
22
+ me = self
23
+
24
+ Module.new do
25
+ extend Dry::CLI::Registry
26
+
27
+ register 'user', aliases: %w[me myself profile] do |p|
28
+ p.register 'show', me.user_show, aliases: %w[sh view]
29
+ p.register 'change', me.user_change, aliases: %w[ch chng update up modify mod]
30
+ p.register 'password', me.user_password, aliases: %w[passwd pass pwd pw]
31
+ p.register 'totp', me.user_totp, aliases: %w[code otp]
32
+ end
33
+
34
+ register 'users', aliases: %w[admins u] do |p|
35
+ p.register 'list', me.users_list, aliases: %w[l all index]
36
+ p.register 'change', me.users_change, aliases: %w[ch chng update up modify mod]
37
+ p.register 'create', me.users_create, aliases: %w[make new n]
38
+ p.register 'remove', me.users_remove, aliases: %w[rem r delete del d destroy]
39
+ end
40
+
41
+ register 'templates', aliases: %w[temps temp tmp t] do |p|
42
+ p.register 'list', me.templates_list, aliases: %w[l all index]
43
+ p.register 'change', me.templates_change, aliases: %w[ch chng update up modify mod]
44
+ end
45
+
46
+ register 'login', me.login, aliases: %w[signin authenticate auth]
47
+ register 'logout', me.logout, aliases: %w[signout deauthenticate deauth unauthenticate unauth]
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ class AllSteps
6
+ include Depends.on(
7
+ 'steps.greeting',
8
+ 'steps.connect',
9
+ 'steps.main',
10
+ 'steps.valediction'
11
+ )
12
+
13
+ def all
14
+ [
15
+ greeting,
16
+ connect,
17
+ main,
18
+ valediction
19
+ ]
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ class Api
6
+ include Depends.on('client')
7
+ attr_accessor :token
8
+
9
+ def send(request)
10
+ client.send(request)
11
+ end
12
+
13
+ def info
14
+ send ApiRequest.new('info')
15
+ end
16
+
17
+ def auth(username, password, totp)
18
+ send ApiRequest.new('admin/auth')
19
+ .method(:post)
20
+ .params(
21
+ username: username,
22
+ password: password,
23
+ totp: totp
24
+ )
25
+ end
26
+
27
+ def me
28
+ send authenticated(ApiRequest.new('admin/me'))
29
+ end
30
+
31
+ def change_me(payload)
32
+ send authenticated(ApiRequest.new('admin/me')
33
+ .method(:patch)
34
+ .params(payload))
35
+ end
36
+
37
+ def rotate_password
38
+ send authenticated(ApiRequest.new('admin/me/password').method(:post))
39
+ end
40
+
41
+ def rotate_totp
42
+ send authenticated(ApiRequest.new('admin/me/totp').method(:post))
43
+ end
44
+
45
+ def users
46
+ send authenticated(ApiRequest.new('admin/admins'))
47
+ end
48
+
49
+ def change_user(id, payload)
50
+ send authenticated(ApiRequest.new("admin/admins/#{id}"))
51
+ .method(:patch)
52
+ .params(payload)
53
+ end
54
+
55
+ def create_user(payload)
56
+ send authenticated(ApiRequest.new('admin/admins'))
57
+ .method(:post)
58
+ .params(payload)
59
+ end
60
+
61
+ def remove_user(id)
62
+ send authenticated(ApiRequest.new("admin/admins/#{id}")).method(:delete)
63
+ end
64
+
65
+ def templates
66
+ send authenticated(ApiRequest.new('admin/templates'))
67
+ end
68
+
69
+ def change_template(id, payload)
70
+ send authenticated(ApiRequest.new("admin/templates/#{id}"))
71
+ .method(:patch)
72
+ .params(payload)
73
+ end
74
+
75
+ private
76
+
77
+ def authenticated(request)
78
+ request.header('Authorization', "Bearer #{token}")
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ class ApiManager
6
+ include Depends.on('output', 'authenticate', 'api')
7
+
8
+ def ensure_authenticated(&block)
9
+ unless api.token
10
+ return ensure_authenticated(&block) if authenticate.run(true)
11
+ end
12
+
13
+ response = block.call
14
+
15
+ return if response === false
16
+
17
+ if response.failure? && response.known_error? && response.error.type == 'unauthenticated'
18
+ output.puts
19
+ output.puts(Pl.dim("You've been logged out. Please log in again."))
20
+ output.puts
21
+ return ensure_authenticated(&block) if authenticate.run(false)
22
+ else
23
+ response
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ class App
6
+ include Depends.on(
7
+ 'output',
8
+ steps: 'all_steps'
9
+ )
10
+
11
+ def run
12
+ result = nil
13
+ prev_space = false
14
+ steps.all.each.with_index do |step, i|
15
+ break if result&.halt? && !step.ensured?
16
+
17
+ output.puts if i.positive? && step.spaced? && !prev_space
18
+
19
+ result = step.run(result)
20
+
21
+ if i < steps.all.count - 1 && step.spaced?
22
+ prev_space = true
23
+ output.puts
24
+ else
25
+ prev_space = false
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ class Authenticate
6
+ include ActionView::Helpers::DateHelper
7
+
8
+ include Depends.on(
9
+ 'output', 'prompt', 'api', 'screen'
10
+ )
11
+
12
+ def run(should_prompt = true)
13
+ if should_prompt
14
+ output.puts Pl.dim('Please login.')
15
+ output.puts
16
+ end
17
+
18
+ username = prompt.ask('Username:')
19
+ password = prompt.mask('Password:')
20
+ totp = prompt.ask('TOTP Code:')
21
+ output.puts
22
+
23
+ spinner = SimpleSpinnerBar.start('Authenticating...', output)
24
+ response = api.auth(username, password, totp)
25
+
26
+ if response.success?
27
+ api.token = response.json['token']
28
+ expires_at = DateTime.parse(response.json['expiresAt'])
29
+ time = distance_of_time_in_words(DateTime.now, expires_at)
30
+
31
+ spinner.success("#{Pl.green('Authenticated')} for the next #{Pl.yellow(time)}")
32
+ true
33
+ else
34
+ spinner.failure
35
+
36
+ output.puts
37
+ if response.known_error?
38
+ if response.error.type == 'invalid_credentials'
39
+ output.puts Pl.yellow('Invalid credentials!')
40
+ output.puts
41
+ return run(false)
42
+ end
43
+
44
+ puts 'Server:'
45
+ Output::Padded.show([
46
+ "#{Pl.yellow(response.error.title)} #{Pl.dim("(#{response.error.status})")}",
47
+ '',
48
+ response.error.message
49
+ ], output, screen)
50
+ else
51
+ output.puts "Server #{Pl.yellow("(#{response.code})")}:"
52
+ output.puts Output::Padded.show(response.body, output, screen)
53
+ end
54
+
55
+ false
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ class Client
6
+ include Depends.on('config.endpoint')
7
+
8
+ def send(request)
9
+ r = HTTP.send(
10
+ request.method,
11
+ URI.join(endpoint, request.path),
12
+ body: request.params.to_json,
13
+ headers: request.headers
14
+ )
15
+ ApiResponse.new(
16
+ r.code,
17
+ r.status.success?,
18
+ r.status.reason,
19
+ r.to_s
20
+ )
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ class Cmd
6
+ include Depends.on('output')
7
+
8
+ def new(**options)
9
+ TTY::Command.new(output: output, **options)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ class CLI
5
+ def exit(code); end
6
+
7
+ def perform_registry(arguments)
8
+ result = registry.get(arguments)
9
+ return usage(result) unless result.found?
10
+
11
+ command, args = parse(result.command, result.arguments, result.names)
12
+
13
+ result.before_callbacks.run(command, args)
14
+ output = command.call(**args)
15
+ result.after_callbacks.run(command, args)
16
+
17
+ output
18
+ end
19
+ end
20
+ end
21
+
22
+ module Pcli
23
+ module Services
24
+ class Commander
25
+ include Depends.on('output', commands: 'all_commands')
26
+
27
+ def initialize(**args)
28
+ super
29
+
30
+ @cli = Dry::CLI.new(commands.registry)
31
+ end
32
+
33
+ def run(cmd)
34
+ @cli.call(arguments: cmd.split, out: output, err: output)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,22 @@
1
+ module Pcli
2
+ module Services
3
+ module Commands
4
+ class Login < Dry::CLI::Command
5
+ include Depends.on('authenticate', 'api', 'prompt', 'output')
6
+
7
+ desc 'Log in.'
8
+
9
+ def call(*)
10
+ if api.token && !prompt.yes?('You are already logged in. Would you like to log out?')
11
+ output.puts('Aborted.')
12
+ return
13
+ end
14
+
15
+ authenticate.run(true)
16
+
17
+ CommandOutput.continue
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ module Pcli
2
+ module Services
3
+ module Commands
4
+ class Logout < Dry::CLI::Command
5
+ include Depends.on('authenticate', 'api', 'output')
6
+
7
+ desc 'Log out.'
8
+
9
+ def call(*)
10
+ unless api.token
11
+ output.puts('You are not logged in.')
12
+ return
13
+ end
14
+
15
+ api.token = nil
16
+
17
+ output.puts(Pl.green('Logged out.'))
18
+
19
+ CommandOutput.continue
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ module Commands
6
+ module Templates
7
+ class Change < Dry::CLI::Command
8
+ include Depends.on(
9
+ 'api',
10
+ 'api_manager',
11
+ 'output',
12
+ 'screen',
13
+ 'editor',
14
+ 'prompt'
15
+ )
16
+
17
+ desc 'Change or view a template.'
18
+
19
+ argument :template, desc: 'The template id to change.'
20
+
21
+ def call(template: nil, **args)
22
+ response = api_manager.ensure_authenticated do
23
+ spinner = SimpleSpinnerBar.start('Retrieving templates...', output)
24
+ r = api.templates
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
+ end
37
+
38
+ templates = response.json
39
+
40
+ unless template
41
+ choices = templates.map { |t| { name: "#{t['name']} (#{t['id']})", value: t['id'] } }
42
+ template = prompt.select('Which template would you like to view/change?', choices)
43
+ end
44
+
45
+ template = templates.find { |t| t['id'] === template }
46
+ unless template
47
+ output.puts(Pl.yellow('That template does not exist.'))
48
+ return CommandOutput.continue
49
+ end
50
+
51
+ file = Tempfile.new('pcli_open_template')
52
+ file.write(template['templateXML'])
53
+ file.close
54
+
55
+ unless editor.open(file.path)
56
+ output.puts('Aborted.')
57
+ return CommandOutput.continue
58
+ end
59
+
60
+ file.open
61
+ new_template = file.read
62
+
63
+ if template['templateXML'] == new_template
64
+ output.puts('Nothing to change.')
65
+ return CommandOutput.continue
66
+ end
67
+
68
+ unless prompt.yes?('Save changes?')
69
+ output.puts('Aborted.')
70
+ return CommandOutput.continue
71
+ end
72
+
73
+ response = api_manager.ensure_authenticated do
74
+ spinner = SimpleSpinnerBar.start('Changing template...', output)
75
+ r = api.change_template(template['id'], { 'templateXML' => new_template })
76
+ if r.failure?
77
+ spinner.failure
78
+ else
79
+ spinner.success("Template #{Pl.green('changed')}")
80
+ end
81
+ r
82
+ end
83
+
84
+ if response.failure?
85
+ output.puts
86
+ Output::ServerError.show(response, output, screen)
87
+ end
88
+
89
+ CommandOutput.continue
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ module Commands
6
+ module Templates
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 templates.'
16
+
17
+ def call(*)
18
+ spinner = nil
19
+ response = api_manager.ensure_authenticated do
20
+ spinner = SimpleSpinnerBar.start('Retrieving templates...', output)
21
+ r = api.templates
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('Name')
36
+ ],
37
+ rows: response.json.map { |u| [u['id'], 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,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pcli
4
+ module Services
5
+ module Commands
6
+ module User
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_manager',
21
+ 'api',
22
+ 'output',
23
+ 'screen',
24
+ 'prompt'
25
+ )
26
+
27
+ desc 'Change name or username of the logged-in administrator.'
28
+
29
+ field_option :name, type: :boolean, default: nil, desc: 'Whether to change the name'
30
+ field_option :username, type: :boolean, default: nil, desc: 'Whether to change the username'
31
+ option :all, type: :boolean, default: false, desc: 'Change all fields'
32
+
33
+ def call(all:, **args)
34
+ field_args = args.slice(*self.class.field_options)
35
+
36
+ response = api_manager.ensure_authenticated do
37
+ spinner = SimpleSpinnerBar.start('Retrieving user...', output)
38
+ r = api.me
39
+ if r.failure?
40
+ spinner.failure
41
+ else
42
+ spinner.success
43
+ end
44
+ r
45
+ end
46
+
47
+ if response.success?
48
+ spinner.success
49
+ else
50
+ spinner.failure
51
+
52
+ output.puts
53
+ Output::ServerError.show(response, output, screen)
54
+ end
55
+
56
+ fields = nil
57
+
58
+ if all == false && field_args.empty?
59
+ fields = prompt
60
+ .multi_select('Which fields do you want to change?', self.class.field_options)
61
+ .map { |n| [n, true] }
62
+ .to_h
63
+ else
64
+ begin
65
+ fields = Util::Cli.analyze_fields_flags(all, self.class.field_options, field_args)
66
+ rescue Util::Cli::FieldsFlagsError => e
67
+ output.puts Pl.yellow(e.message)
68
+ return CommandOutput.continue
69
+ end
70
+ end
71
+
72
+ payload = {}
73
+
74
+ if fields[:name]
75
+ name = prompt.ask('Name', default: response.json['name'])
76
+ payload['name'] = name if name != response.json['name']
77
+ end
78
+
79
+ if fields[:username]
80
+ username = prompt.ask('Username', default: response.json['username'])
81
+ payload['username'] = username if username != response.json['username']
82
+ end
83
+
84
+ if payload.empty?
85
+ output.puts Pl.yellow('Nothing to change!')
86
+ return CommandOutput.continue
87
+ end
88
+
89
+ spinner = SimpleSpinnerBar.start('Changing user...', output)
90
+ response = api_manager.ensure_authenticated { api.change_me(payload) }
91
+
92
+ if response.success?
93
+ spinner.success("User #{Pl.green('changed')}")
94
+
95
+ output.puts
96
+ output.puts TTY::Table.new(rows: [
97
+ [Pl.bold('ID'), response.json['id']],
98
+ [Pl.bold('Name'), response.json['name']],
99
+ [Pl.bold('Username'), response.json['username']]
100
+ ]).render(:ascii)
101
+ else
102
+ spinner.failure
103
+
104
+ output.puts
105
+ Output::ServerError.show(response, output, screen)
106
+ end
107
+
108
+ CommandOutput.continue
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end