punk 0.0.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +9 -0
  3. data/.github/workflows/ship.yml +28 -0
  4. data/.github/workflows/test.yml +45 -0
  5. data/.rdoc_options +23 -0
  6. data/.rgignore +1 -0
  7. data/.rspec +2 -0
  8. data/.rubocop.yml +243 -0
  9. data/Gemfile +6 -6
  10. data/Gemfile.lock +18 -30
  11. data/README.md +8 -0
  12. data/Rakefile +7 -9
  13. data/VERSION +1 -1
  14. data/app/migrations/001_lets_punk.rb +3 -0
  15. data/app/routes/hello.rb +4 -0
  16. data/bin/punk +0 -1
  17. data/env/.gitignore +3 -0
  18. data/env/spec/test.sh +3 -0
  19. data/env/test.sh +5 -0
  20. data/lib/punk/actions/.keep +0 -0
  21. data/lib/punk/actions/groups/list.rb +24 -0
  22. data/lib/punk/actions/sessions/clear.rb +21 -0
  23. data/lib/punk/actions/sessions/create.rb +64 -0
  24. data/lib/punk/actions/sessions/list.rb +18 -0
  25. data/lib/punk/actions/sessions/verify.rb +24 -0
  26. data/lib/punk/actions/tenants/list.rb +18 -0
  27. data/lib/punk/actions/users/list_group.rb +18 -0
  28. data/lib/punk/actions/users/list_tenant.rb +18 -0
  29. data/lib/punk/actions/users/show.rb +18 -0
  30. data/lib/punk/commands/http.rb +3 -3
  31. data/lib/punk/commands/list.rb +12 -6
  32. data/lib/punk/config/defaults.json +3 -0
  33. data/lib/punk/config/schema.json +3 -0
  34. data/lib/punk/core/app.rb +6 -8
  35. data/lib/punk/core/commander.rb +9 -6
  36. data/lib/punk/core/exec.rb +2 -0
  37. data/lib/punk/core/load.rb +0 -1
  38. data/lib/punk/framework/command.rb +5 -1
  39. data/lib/punk/framework/plugins/validation.rb +0 -14
  40. data/lib/punk/framework/runnable.rb +1 -1
  41. data/lib/punk/helpers/loggable.rb +1 -1
  42. data/lib/punk/migrations/001_punk.rb +103 -0
  43. data/lib/punk/models/.keep +0 -0
  44. data/lib/punk/models/group.rb +20 -0
  45. data/lib/punk/models/group_user_metadata.rb +17 -0
  46. data/lib/punk/models/identity.rb +29 -0
  47. data/lib/punk/models/session.rb +89 -0
  48. data/lib/punk/models/tenant.rb +19 -0
  49. data/lib/punk/models/tenant_user_metadata.rb +17 -0
  50. data/lib/punk/models/user.rb +31 -0
  51. data/lib/punk/routes/groups.rb +31 -0
  52. data/lib/punk/routes/plivo.rb +4 -0
  53. data/lib/punk/routes/sessions.rb +108 -0
  54. data/lib/punk/routes/swagger.rb +9 -0
  55. data/lib/punk/routes/tenants.rb +29 -0
  56. data/lib/punk/routes/users.rb +36 -0
  57. data/lib/punk/services/.keep +0 -0
  58. data/lib/punk/services/challenge_claim.rb +46 -0
  59. data/lib/punk/services/create_identities.rb +25 -0
  60. data/lib/punk/services/generate_swagger.rb +25 -0
  61. data/lib/punk/services/prove_claim.rb +29 -0
  62. data/lib/punk/services/secret.rb +9 -0
  63. data/lib/punk/templates/groups/list.jbuilder +7 -0
  64. data/lib/punk/templates/plivo.slim +16 -0
  65. data/lib/punk/templates/sessions/list.jbuilder +6 -0
  66. data/lib/punk/templates/sessions/pending.jbuilder +4 -0
  67. data/lib/punk/templates/tenants/list.jbuilder +7 -0
  68. data/lib/punk/templates/tenants/list.slim +8 -0
  69. data/lib/punk/templates/users/list.jbuilder +7 -0
  70. data/lib/punk/templates/users/list.rcsv +4 -0
  71. data/lib/punk/templates/users/show.jbuilder +5 -0
  72. data/lib/punk/views/groups/list.rb +22 -0
  73. data/lib/punk/views/plivo_store.rb +15 -0
  74. data/lib/punk/views/sessions/list.rb +22 -0
  75. data/lib/punk/views/sessions/pending.rb +28 -0
  76. data/lib/punk/views/tenants/list.rb +22 -0
  77. data/lib/punk/views/users/list.rb +22 -0
  78. data/lib/punk/views/users/show.rb +22 -0
  79. data/lib/punk/workers/.keep +0 -0
  80. data/lib/punk/workers/expire_sessions.rb +9 -0
  81. data/lib/punk/workers/geocode_session_worker.rb +48 -0
  82. data/lib/punk/workers/identify_session_worker.rb +45 -0
  83. data/lib/punk/workers/secret.rb +18 -0
  84. data/lib/punk/workers/send_email_worker.rb +51 -0
  85. data/lib/punk/workers/send_sms_worker.rb +40 -0
  86. data/punk.gemspec +149 -16
  87. data/schema.psql +345 -0
  88. data/spec/actions/groups/punk/list_groups_action_spec.rb +36 -0
  89. data/spec/actions/sessions/punk/clear_session_action_spec.rb +29 -0
  90. data/spec/actions/sessions/punk/create_session_action_spec.rb +33 -0
  91. data/spec/actions/sessions/punk/list_sessions_action_spec.rb +26 -0
  92. data/spec/actions/sessions/punk/verify_session_action_spec.rb +59 -0
  93. data/spec/actions/tenants/punk/list_tenants_action_spec.rb +25 -0
  94. data/spec/actions/users/punk/list_group_users_action_spec.rb +26 -0
  95. data/spec/actions/users/punk/list_tenant_users_action_spec.rb +26 -0
  96. data/spec/factories/group.rb +12 -0
  97. data/spec/factories/group_user_metadata.rb +10 -0
  98. data/spec/factories/identity.rb +19 -0
  99. data/spec/factories/session.rb +12 -0
  100. data/spec/factories/tenant.rb +10 -0
  101. data/spec/factories/tenant_user_metadata.rb +10 -0
  102. data/spec/factories/user.rb +12 -0
  103. data/spec/lib/commands/auth_spec.rb +11 -0
  104. data/spec/lib/commands/generate_spec.rb +7 -0
  105. data/spec/lib/commands/http_spec.rb +23 -0
  106. data/spec/lib/commands/list_spec.rb +7 -0
  107. data/spec/lib/commands/swagger_spec.rb +7 -0
  108. data/spec/lib/engine/punk_env_spec.rb +13 -0
  109. data/spec/lib/engine/punk_exec_spec.rb +9 -0
  110. data/spec/lib/engine/punk_init_spec.rb +9 -0
  111. data/spec/lib/engine/punk_store_spec.rb +10 -0
  112. data/spec/lib/punk.env +7 -0
  113. data/spec/models/punk/group_spec.rb +50 -0
  114. data/spec/models/punk/group_user_metadata_spec.rb +61 -0
  115. data/spec/models/punk/identity_spec.rb +61 -0
  116. data/spec/models/punk/session_spec.rb +156 -0
  117. data/spec/models/punk/tenant_spec.rb +51 -0
  118. data/spec/models/punk/tenant_user_metadata_spec.rb +61 -0
  119. data/spec/models/punk/user_spec.rb +115 -0
  120. data/spec/routes/groups/get_groups_spec.rb +33 -0
  121. data/spec/routes/plivo/get_plivo_spec.rb +11 -0
  122. data/spec/routes/sessions/delete_session_spec.rb +11 -0
  123. data/spec/routes/sessions/get_sessions_spec.rb +30 -0
  124. data/spec/routes/sessions/patch_session_spec.rb +11 -0
  125. data/spec/routes/sessions/post_session_spec.rb +11 -0
  126. data/spec/routes/swagger/get_swagger_spec.rb +12 -0
  127. data/spec/routes/tenants/get_tenants_spec.rb +31 -0
  128. data/spec/routes/users/get_users_spec.rb +60 -0
  129. data/spec/services/punk/challenge_claim_service_spec.rb +7 -0
  130. data/spec/services/punk/create_identities_service_spec.rb +14 -0
  131. data/spec/services/punk/generate_swagger_service_spec.rb +7 -0
  132. data/spec/services/punk/prove_claim_service_spec.rb +7 -0
  133. data/spec/services/punk/secret_service_spec.rb +7 -0
  134. data/spec/spec_helper.rb +122 -0
  135. data/spec/vcr_cassettes/PUNK_GeocodeSessionWorker/updates_the_session_data.yml +57 -0
  136. data/spec/vcr_cassettes/PUNK_IdentifySessionWorker/updates_the_session_data.yml +112 -0
  137. data/spec/views/punk/plivo_store_spec.rb +7 -0
  138. data/spec/views/sessions/punk/list_sessions_view_spec.rb +7 -0
  139. data/spec/views/sessions/punk/pending_session_view_spec.rb +7 -0
  140. data/spec/views/tenants/punk/list_tenants_view_spec.rb +7 -0
  141. data/spec/views/users/punk/list_groups_view_spec.rb +7 -0
  142. data/spec/views/users/punk/list_users_view_spec.rb +7 -0
  143. data/spec/workers/punk/expire_sessions_worker_spec.rb +31 -0
  144. data/spec/workers/punk/geocode_session_worker_spec.rb +14 -0
  145. data/spec/workers/punk/identify_session_worker_spec.rb +15 -0
  146. data/spec/workers/punk/secret_worker_spec.rb +20 -0
  147. data/spec/workers/punk/send_email_worker_spec.rb +46 -0
  148. data/spec/workers/punk/send_sms_worker_spec.rb +33 -0
  149. metadata +169 -13
  150. data/lib/punk/views/all.rb +0 -4
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @resource Groups
4
+ #
5
+ # Each tenant can have many groups.
6
+ PUNK.route('groups') do
7
+ require_session!
8
+ require_tenant!
9
+
10
+ # Retrieve the list of groups visible to the authenticated user for a specific tenant.
11
+ # @path [GET] /groups
12
+ # @parameter tenant_id(required) [string] An email address or mobile phone number.
13
+ # @response [Array<Group>] 200 List of groups
14
+ # @method get
15
+ # @example 200
16
+ # [{
17
+ # "id": "deadbeef-1234-5678-abcd-000000000000",
18
+ # "name": "Cool Group",
19
+ # "icon": "https://some.image/url"
20
+ # }]
21
+ # @example 401
22
+ # {
23
+ # "message": "You must specify a tenant.",
24
+ # "errors": ["Cannot find tenant"]
25
+ # }
26
+ #
27
+ # route: GET /groups
28
+ get do
29
+ perform PUNK::ListGroupsAction, user: current_user, tenant: current_tenant
30
+ end
31
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # route: GET /plivo
4
+ PUNK.route('plivo') { present PUNK::PlivoStore }
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @resource Sessions
4
+ #
5
+ # Handle the authentication flow.
6
+ PUNK.route('sessions') do
7
+ # Challenge the claim of someone having a particular email address or phone number.
8
+ # @path [POST] /sessions
9
+ # @parameter claim(required) [string] An email address or mobile phone number.
10
+ # @response [Session] 201 Pending session created
11
+ # @response [Error] 400 Invalid claim, or the user is already authenticated
12
+ # @method post
13
+ # @example 201
14
+ # {
15
+ # "slug": "deadbeef-1234-5678-abcd-000000000000",
16
+ # "message": "A code has been sent to your email address."
17
+ # }
18
+ # @example 400
19
+ # {
20
+ # "message": "Validation failed",
21
+ # "errors": ["Claim is not an email or phone."]
22
+ # }
23
+ #
24
+ # route: POST /sessions
25
+ post do
26
+ require_anonymous!
27
+ perform PUNK::CreateSessionAction, args.merge(remote_addr: request.ip || PUNK::Session.default_values[:remote_addr].to_s, user_agent: request.env['HTTP_USER_AGENT'] || PUNK::Session.default_values[:user_agent])
28
+ end
29
+
30
+ # route: GET /sessions/current
31
+ on "current" do
32
+ require_session!
33
+ get do
34
+ perform PUNK::ShowUserAction, user: current_user
35
+ end
36
+ end
37
+
38
+ # route: PATCH /sessions/:slug
39
+ on :id do |slug|
40
+ require_anonymous!
41
+ user_session = PUNK::Session.find(slug: slug)
42
+ # Verify a pending session by providing proof of access to the email address or phone number.
43
+ # @path [PATCH] /sessions/{slug}
44
+ # @parameter secret(required) [string] The verification code sent by email or sms.
45
+ # @response [Info] 200 Session verified and cookie created
46
+ # @response [Error] 400 Invalid secret
47
+ # @method patch
48
+ # @example 200
49
+ # {
50
+ # "message": "You are now logged in."
51
+ # }
52
+ # @example 400
53
+ # {
54
+ # "message": "Validation failed",
55
+ # "errors": ["Secret does not match."]
56
+ # }
57
+ patch do
58
+ view = perform PUNK::VerifySessionAction, args.merge(session: user_session)
59
+ request.session[:session_id] = user_session.id if user_session&.active?
60
+ view
61
+ end
62
+ end
63
+
64
+ # Allow the current user to access their active sessions.
65
+ # @path [GET] /sessions
66
+ # @response [Array<Session>] 200 List of sessions
67
+ # @response [Error] 401 The user was not authenticated
68
+ # @method get
69
+ # @example 200
70
+ # [{
71
+ # "id": "deadbeef-1234-5678-abcd-000000000000",
72
+ # "client": {...}
73
+ # }]
74
+ # @example 401
75
+ # {
76
+ # "message": "you are not authenticated.",
77
+ # "errors": ["cannot find session"]
78
+ # }
79
+ #
80
+ # route: GET /tenants
81
+ get do
82
+ require_session!
83
+ perform PUNK::ListSessionsAction, user: current_user
84
+ end
85
+
86
+ # Allow the current user to logout.
87
+ # @path [DELETE] /sessions
88
+ # @response [Info] 200 Session destroyed and cookie cleared
89
+ # @response [Error] 401 The user was not authenticated
90
+ # @method destroy
91
+ # @example 200
92
+ # {
93
+ # "message": "You have logged out."
94
+ # }
95
+ # @example 401
96
+ # {
97
+ # "message": "You are not authenticated.",
98
+ # "errors": ["No session exists"]
99
+ # }
100
+ #
101
+ # route: DELETE /sessions
102
+ delete do
103
+ require_session!
104
+ view = perform PUNK::ClearSessionAction, session: current_session
105
+ clear_session if current_session&.deleted?
106
+ view
107
+ end
108
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # route: GET /swagger.json
4
+ PUNK.route('swagger') do
5
+ result = PUNK::GenerateSwaggerService.run.result
6
+ response.status = 200
7
+ response['Content-Type'] = 'application/json'
8
+ result
9
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @resource Tenants
4
+ #
5
+ # All resources in the system are relative to a particular tenant application.
6
+ PUNK.route('tenants') do
7
+ require_session!
8
+
9
+ # Retrieve the list of tenants visible to the authenticated user.
10
+ # @path [GET] /tenants
11
+ # @response [Array<Tenant>] 200 List of tenants
12
+ # @method get
13
+ # @example 200
14
+ # [{
15
+ # "id": "deadbeef-1234-5678-abcd-000000000000",
16
+ # "name": "Cool Tenant",
17
+ # "icon": "https://some.image/url"
18
+ # }]
19
+ # @example 401
20
+ # {
21
+ # "message": "You are not authenticated.",
22
+ # "errors": ["Cannot find session"]
23
+ # }
24
+ #
25
+ # route: GET /tenants
26
+ get do
27
+ perform PUNK::ListTenantsAction, user: current_user
28
+ end
29
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @resource Users
4
+ #
5
+ # Users can belong to many tenants and many groups.
6
+ PUNK.route('users') do
7
+ require_session!
8
+ require_tenant!
9
+
10
+ # Retrieve the list of users visible to the authenticated user for a specific tenant or group.
11
+ # @path [GET] /users
12
+ # @parameter tenant_id(required) [string] The ID of a tenant visible to the authenticated user.
13
+ # @parameter group_id [string] The ID of a group that belongs to the tenant.
14
+ # @response [Array<User>] 200 List of users
15
+ # @method get
16
+ # @example 200
17
+ # [{
18
+ # "id": "deadbeef-1234-5678-abcd-000000000000",
19
+ # "name": "John Smith",
20
+ # "icon": "https://some.image/url"
21
+ # }]
22
+ # @example 401
23
+ # {
24
+ # "message": "You must specify a tenant.",
25
+ # "errors": ["Cannot find tenant"]
26
+ # }
27
+ #
28
+ # route: GET /users
29
+ get do
30
+ if args[:group_id]
31
+ perform PUNK::ListGroupUsersAction, group: current_user.groups_dataset[tenant: current_tenant, id: args[:group_id]]
32
+ else
33
+ perform PUNK::ListTenantUsersAction, tenant: current_tenant
34
+ end
35
+ end
36
+ end
File without changes
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rbnacl'
4
+
5
+ module PUNK
6
+ class ChallengeClaimService < Service
7
+ args :session
8
+
9
+ def validate
10
+ validates_not_null :session
11
+ validates_not_empty :session
12
+ return if session.blank?
13
+ validates_type Session, :session
14
+ validates_state :session, :created
15
+ validates_event :session, :challenge
16
+ end
17
+
18
+ def process
19
+ secret = SecretService.run.result
20
+ salt = RbNaCl::Random.random_bytes(RbNaCl::PasswordHash::SCrypt::SALTBYTES)
21
+ hash = RbNaCl::PasswordHash.scrypt(secret, salt, 1_048_576, 16_777_216)
22
+ session.update(salt: salt, hash: hash)
23
+ session.challenge!
24
+ identity = session.identity
25
+ case identity.claim_type
26
+ when :email
27
+ SendEmailWorker.perform_async(
28
+ from: 'GroupFire Accounts <noreply@groupfire.com>',
29
+ to: identity.claim,
30
+ subject: '[GroupFire] Verification Code',
31
+ template: 'verify',
32
+ tags: [:auth],
33
+ variables: {
34
+ name: identity.user&.name || 'New User',
35
+ secret: secret
36
+ }
37
+ )
38
+ when :phone
39
+ SendSmsWorker.perform_async(
40
+ to: identity.claim,
41
+ body: "Your GroupFire verification code is: #{secret}."
42
+ )
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PUNK
4
+ class CreateIdentitiesService < Service
5
+ def process
6
+ User.each do |user|
7
+ if user.email.present?
8
+ Identity.find_or_create(claim: user.email) do |i|
9
+ i.claim_type = :email
10
+ i.user = user
11
+ end
12
+ end
13
+ if user.phone.present?
14
+ Identity.find_or_create(claim: user.phone) do |i|
15
+ i.claim_type = :phone
16
+ i.user = user
17
+ end
18
+ end
19
+ rescue Sequel::ValidationFailed => e
20
+ logger.warn e.message
21
+ end
22
+ nil
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PUNK
4
+ class GenerateSwaggerService < Service
5
+ def process
6
+ path = File.join(PUNK.get.app.path, '..', 'www', 'swagger.json')
7
+ raise InternalServerError, 'swagger.json already exists' if File.exist?(path) && !PUNK.env.test?
8
+ require 'swagger_yard'
9
+ require_relative '../helpers/swagger'
10
+ SwaggerYard.register_custom_yard_tags!
11
+ SwaggerYard.configure do |config|
12
+ config.api_version = PUNK.version
13
+ config.title = PUNK.get.app.name
14
+ config.description = PUNK.get.app.description
15
+ config.api_base_path = PUNK.get.app.url
16
+ config.controller_path = [File.join(PUNK.get.app.path, 'routes', '**', '*'), File.join(__dir__, '..', 'routes', '**', '*')]
17
+ config.model_path = [File.join(__dir__, '..', 'models', '**', '*'), File.join(__dir__, '..', 'views', '**', '*'), File.join(PUNK.get.app.path, 'models', '**', '*')]
18
+ end
19
+ spec = SwaggerYard::OpenAPI.new
20
+ blob = JSON.pretty_generate(spec.to_h)
21
+ File.open(path, "w") { |f| f << blob } unless PUNK.env.test?
22
+ blob
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rbnacl'
4
+
5
+ module PUNK
6
+ class ProveClaimService < Service
7
+ args :session, :secret
8
+
9
+ def validate
10
+ validates_not_null :session
11
+ validates_not_empty :session
12
+ return if session.blank?
13
+ session.timeout?
14
+ validates_type Session, :session
15
+ validates_state :session, :pending
16
+ validates_event :session, :verify
17
+ end
18
+
19
+ def process
20
+ session.increment_attempts
21
+ session.reload
22
+ raise BadRequest, "Too many attempts" if session.attempt_count >= 3
23
+ hash = RbNaCl::PasswordHash.scrypt(secret, session.salt, 1_048_576, 16_777_216)
24
+ proven = (session[:hash] == hash)
25
+ session.verify! if proven
26
+ proven
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PUNK
4
+ class SecretService < Service
5
+ def process
6
+ (SecureRandom.random_number(900_000) + 100_000).to_s
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ json.array!(groups) do |group|
4
+ json.id group.id
5
+ json.name group.name
6
+ json.icon group.icon
7
+ end
@@ -0,0 +1,16 @@
1
+ doctype html
2
+ html
3
+ head
4
+ title Plivo
5
+ body
6
+ h2 Plivo
7
+ - plivo = PUNK.cache.get(:plivo) || []
8
+ - plivo.each do |message|
9
+ h4= message[:sent]
10
+ dl
11
+ dt from
12
+ dd= message[:from]
13
+ dt to
14
+ dd= message[:to]
15
+ dt body
16
+ dd= message[:body]
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ json.array!(sessions) do |session|
4
+ json.id session.id
5
+ json.data session.data
6
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ json.slug session.slug
4
+ json.message message
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ json.array!(tenants) do |tenant|
4
+ json.id tenant.id
5
+ json.name tenant.name
6
+ json.icon tenant.icon
7
+ end
@@ -0,0 +1,8 @@
1
+ doctype html
2
+ html
3
+ head
4
+ title Tenants
5
+ body
6
+ - tenants.each do |tenant|
7
+ h1= tenant.name
8
+ img src= tenant.icon
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ json.array!(users) do |user|
4
+ json.id user.id
5
+ json.name user.name
6
+ json.icon user.icon
7
+ end
@@ -0,0 +1,4 @@
1
+ csv << ['ID','NAME','ICON','EMAIL','PHONE','TITLE','COMPANY','WEBSITE','LOCATION']
2
+ users.each do |user|
3
+ csv << [user.id, user.name, user.icon, user.email, user.phone, user.profile[:professional][:title], user.profile[:professional][:company], user.profile[:professional][:website], user.profile[:professional][:location]]
4
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ json.id user.id
4
+ json.name user.name
5
+ json.icon user.icon