From e81c083f3654e08f2b62c3cffe45f9f607039dd7 Mon Sep 17 00:00:00 2001 From: Auston Bunsen Date: Fri, 3 Apr 2026 17:02:37 -0400 Subject: [PATCH 1/2] Sync SDK with API: add landing pages, credential profiles, fix missing fields - Add employee_id, organization_name, created_at to Card model - Add metadata to Template model - Add LandingPage model and list/create/update methods - Add CredentialProfile model and CredentialProfiles service - Update README issue example to match doc params - Update feature matrix with new endpoints - Bump version to 0.4.0 --- README.md | 94 +++++++++++++- lib/accessgrid/access_cards.rb | 6 +- lib/accessgrid/console.rb | 71 ++++++++++- lib/accessgrid/version.rb | 2 +- spec/console_spec.rb | 225 +++++++++++++++++++++++++++++++++ spec/models/card_spec.rb | 20 ++- spec/models/template_spec.rb | 11 +- 7 files changed, 419 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index dd5676d..782217c 100644 --- a/README.md +++ b/README.md @@ -43,15 +43,25 @@ client = AccessGrid.new(account_id, secret_key) card = client.access_cards.issue( card_template_id: card_template_id, employee_id: "123456789", - card_number: "16187", tag_id: "DDEADB33FB00B5", full_name: "Employee name", email: "employee@yourwebsite.com", phone_number: "+19547212241", classification: "full_time", - start_date: "2025-01-31T22:46:25.601Z", - expiration_date: "2025-04-30T22:46:25.601Z", - employee_photo: "[image_in_base64_encoded_format]" + department: "Engineering", + location: "San Francisco", + site_name: "HQ Building A", + workstation: "4F-207", + mail_stop: "MS-401", + company_address: "123 Main St, San Francisco, CA 94105", + start_date: Time.now.utc.iso8601(3), + expiration_date: 3.months.from_now.utc.iso8601(3), + employee_photo: "[image_in_base64_encoded_format]", + title: "Engineering Manager", + metadata: { + "department": "engineering", + "badge_type": "contractor" + } ) # Provision is an alias for issue (for backwards compatibility) @@ -323,6 +333,77 @@ puts "Completed registration for org: #{result.name}" puts "Status: #{result.status}" ``` +### Landing Pages + +#### List landing pages + +```ruby +landing_pages = client.console.list_landing_pages + +landing_pages.each do |page| + puts "ID: #{page.id}, Name: #{page.name}, Kind: #{page.kind}" + puts " Password Protected: #{page.password_protected}" + puts " Logo URL: #{page.logo_url}" if page.logo_url +end +``` + +#### Create a landing page + +```ruby +landing_page = client.console.create_landing_page( + name: "Miami Office Access Pass", + kind: "universal", + additional_text: "Welcome to the Miami Office", + bg_color: "#f1f5f9", + allow_immediate_download: true +) + +puts "Landing page created: #{landing_page.id}" +puts "Name: #{landing_page.name}, Kind: #{landing_page.kind}" +``` + +#### Update a landing page + +```ruby +landing_page = client.console.update_landing_page( + landing_page_id: "0xlandingpage1d", + name: "Updated Miami Office Access Pass", + additional_text: "Welcome! Tap below to get your access pass.", + bg_color: "#e2e8f0" +) + +puts "Landing page updated: #{landing_page.id}" +puts "Name: #{landing_page.name}" +``` + +### Credential Profiles + +#### List credential profiles + +```ruby +profiles = client.console.credential_profiles.list + +profiles.each do |profile| + puts "ID: #{profile.id}, Name: #{profile.name}, AID: #{profile.aid}" +end +``` + +#### Create a credential profile + +```ruby +profile = client.console.credential_profiles.create( + name: 'Main Office Profile', + app_name: 'KEY-ID-main', + keys: [ + { value: 'your_32_char_hex_master_key_here' }, + { value: 'your_32_char_hex__read_key__here' } + ] +) + +puts "Profile created: #{profile.id}" +puts "AID: #{profile.aid}" +``` + ## Configuration The SDK can be configured with a custom API endpoint: @@ -402,6 +483,11 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/access | GET /v1/console/webhooks | `console.webhooks.list()` | Y | | POST /v1/console/webhooks | `console.webhooks.create()` | Y | | DELETE /v1/console/webhooks/{id} | `console.webhooks.delete()` | Y | +| GET /v1/console/landing-pages | `console.list_landing_pages()` | Y | +| POST /v1/console/landing-pages | `console.create_landing_page()` | Y | +| PUT /v1/console/landing-pages/{id} | `console.update_landing_page()` | Y | +| GET /v1/console/credential-profiles | `console.credential_profiles.list()` | Y | +| POST /v1/console/credential-profiles | `console.credential_profiles.create()` | Y | | POST /v1/console/hid/orgs | `console.hid.orgs.create()` | Y | | POST /v1/console/hid/orgs/activate | `console.hid.orgs.activate()` | Y | | GET /v1/console/hid/orgs | `console.hid.orgs.list()` | Y | diff --git a/lib/accessgrid/access_cards.rb b/lib/accessgrid/access_cards.rb index a801515..4f0c39d 100644 --- a/lib/accessgrid/access_cards.rb +++ b/lib/accessgrid/access_cards.rb @@ -66,7 +66,8 @@ def manage_state(card_id, action) class Card attr_reader :id, :state, :url, :install_url, :details, :full_name, :expiration_date, :card_template_id, :card_number, :site_code, - :file_data, :direct_install_url, :devices, :metadata, :temporary + :file_data, :direct_install_url, :devices, :metadata, :temporary, + :employee_id, :organization_name, :created_at def initialize(data) data ||= {} @@ -86,6 +87,9 @@ def initialize(data) @devices = data.fetch('devices', []) @metadata = data.fetch('metadata', {}) @temporary = data.fetch('temporary', nil) + @employee_id = data.fetch('employee_id', nil) + @organization_name = data.fetch('organization_name', nil) + @created_at = data.fetch('created_at', nil) end def to_s diff --git a/lib/accessgrid/console.rb b/lib/accessgrid/console.rb index b0c8f5d..24f1aab 100644 --- a/lib/accessgrid/console.rb +++ b/lib/accessgrid/console.rb @@ -4,12 +4,13 @@ module AccessGrid # Manages enterprise template and logging operations. class Console - attr_reader :webhooks, :hid + attr_reader :webhooks, :hid, :credential_profiles def initialize(client) @client = client @webhooks = Webhooks.new(client) @hid = HID.new(client) + @credential_profiles = CredentialProfiles.new(client) end def create_template(params) @@ -73,6 +74,22 @@ def ios_preflight(card_template_id:, access_pass_ex_id:) IosPreflight.new(response) end + def list_landing_pages + response = @client.make_request(:get, '/v1/console/landing-pages') + pages = response.is_a?(Array) ? response : response.fetch('landing_pages', []) + pages.map { |page| LandingPage.new(page) } + end + + def create_landing_page(**params) + response = @client.make_request(:post, '/v1/console/landing-pages', params) + LandingPage.new(response) + end + + def update_landing_page(landing_page_id:, **params) + response = @client.make_request(:put, "/v1/console/landing-pages/#{landing_page_id}", params) + LandingPage.new(response) + end + private def transform_template_params(params) @@ -91,7 +108,8 @@ def transform_template_params(params) class Template attr_reader :id, :name, :platform, :protocol, :use_case, :created_at, :last_published_at, :issued_keys_count, :active_keys_count, - :allowed_device_counts, :support_settings, :terms_settings, :style_settings + :allowed_device_counts, :support_settings, :terms_settings, :style_settings, + :metadata def initialize(data) @id = data['id'] @@ -107,6 +125,7 @@ def initialize(data) @support_settings = data['support_settings'] @terms_settings = data['terms_settings'] @style_settings = data['style_settings'] + @metadata = data['metadata'] || {} end end @@ -205,6 +224,54 @@ def initialize(data) end end + # Represents a landing page configuration. + class LandingPage + attr_reader :id, :name, :created_at, :kind, :password_protected, :logo_url + + def initialize(data) + @id = data['id'] + @name = data['name'] + @created_at = data['created_at'] + @kind = data['kind'] + @password_protected = data['password_protected'] + @logo_url = data['logo_url'] + end + end + + # Represents a credential profile configuration. + class CredentialProfile + attr_reader :id, :aid, :name, :apple_id, :created_at, :card_storage, :keys, :files + + def initialize(data) + @id = data['id'] + @aid = data['aid'] + @name = data['name'] + @apple_id = data['apple_id'] + @created_at = data['created_at'] + @card_storage = data['card_storage'] + @keys = data['keys'] || [] + @files = data['files'] || [] + end + end + + # Manages credential profile operations. + class CredentialProfiles + def initialize(client) + @client = client + end + + def create(**params) + response = @client.make_request(:post, '/v1/console/credential-profiles', params) + CredentialProfile.new(response) + end + + def list + response = @client.make_request(:get, '/v1/console/credential-profiles') + profiles = response.is_a?(Array) ? response : response.fetch('credential_profiles', []) + profiles.map { |profile| CredentialProfile.new(profile) } + end + end + # Manages webhook operations. class Webhooks def initialize(client) diff --git a/lib/accessgrid/version.rb b/lib/accessgrid/version.rb index 83fe190..9ed4b5d 100644 --- a/lib/accessgrid/version.rb +++ b/lib/accessgrid/version.rb @@ -2,5 +2,5 @@ # lib/accessgrid/version.rb module AccessGrid - VERSION = '0.2.0' + VERSION = '0.4.0' end diff --git a/spec/console_spec.rb b/spec/console_spec.rb index 5dadec2..aec0e0d 100644 --- a/spec/console_spec.rb +++ b/spec/console_spec.rb @@ -745,6 +745,231 @@ end end + describe '#list_landing_pages' do + let(:landing_pages_response) do + [ + { + id: 'lp_1', + name: 'Miami Office', + created_at: '2025-01-01T00:00:00Z', + kind: 'universal', + password_protected: false, + logo_url: 'https://example.com/logo.png' + }, + { + id: 'lp_2', + name: 'NYC Office', + created_at: '2025-01-02T00:00:00Z', + kind: 'universal', + password_protected: true, + logo_url: nil + } + ] + end + + it 'returns a list of landing pages' do + stub_api_request( + :get, + '/v1/console/landing-pages', + body: landing_pages_response, + query: generate_sig_payload(id: :'landing-pages') + ) + + pages = console.list_landing_pages + + expect(pages).to be_an(Array) + expect(pages.length).to eq(2) + expect(pages.first).to be_a(AccessGrid::LandingPage) + expect(pages.first.id).to eq('lp_1') + expect(pages.first.name).to eq('Miami Office') + expect(pages.first.kind).to eq('universal') + expect(pages.first.password_protected).to eq(false) + expect(pages.first.logo_url).to eq('https://example.com/logo.png') + end + + it 'returns empty array when no landing pages' do + stub_api_request( + :get, + '/v1/console/landing-pages', + body: [], + query: generate_sig_payload(id: :'landing-pages') + ) + + pages = console.list_landing_pages + expect(pages).to eq([]) + end + end + + describe '#create_landing_page' do + let(:create_response) do + { + id: 'lp_new', + name: 'Miami Office Access Pass', + created_at: '2025-06-01T00:00:00Z', + kind: 'universal', + password_protected: false, + logo_url: nil + } + end + + it 'creates a landing page' do + request_body = { + name: 'Miami Office Access Pass', + kind: 'universal', + additional_text: 'Welcome to the Miami Office', + bg_color: '#f1f5f9', + allow_immediate_download: true + } + + stub_api_request( + :post, + '/v1/console/landing-pages', + body: create_response, + request_body: request_body + ) + + page = console.create_landing_page(**request_body) + + expect(page).to be_a(AccessGrid::LandingPage) + expect(page.id).to eq('lp_new') + expect(page.name).to eq('Miami Office Access Pass') + expect(page.kind).to eq('universal') + end + end + + describe '#update_landing_page' do + let(:update_response) do + { + id: 'lp_1', + name: 'Updated Miami Office', + created_at: '2025-06-01T00:00:00Z', + kind: 'universal', + password_protected: false, + logo_url: nil + } + end + + it 'updates a landing page' do + request_body = { + name: 'Updated Miami Office', + additional_text: 'Welcome!', + bg_color: '#e2e8f0' + } + + stub_api_request( + :put, + '/v1/console/landing-pages/lp_1', + body: update_response, + request_body: request_body + ) + + page = console.update_landing_page(landing_page_id: 'lp_1', **request_body) + + expect(page).to be_a(AccessGrid::LandingPage) + expect(page.id).to eq('lp_1') + expect(page.name).to eq('Updated Miami Office') + end + end + + describe 'credential_profiles' do + let(:profiles_service) { console.credential_profiles } + + describe '#create' do + let(:create_response) do + { + id: 'cp_123', + aid: 'F0394148', + name: 'Main Office Profile', + apple_id: 'pass.com.example', + created_at: '2025-06-01T00:00:00Z', + card_storage: 2048, + keys: [{ 'value' => 'abcdef1234567890abcdef1234567890' }], + files: [] + } + end + + it 'creates a credential profile' do + request_body = { + name: 'Main Office Profile', + app_name: 'KEY-ID-main', + keys: [{ value: 'abcdef1234567890abcdef1234567890' }] + } + + stub_api_request( + :post, + '/v1/console/credential-profiles', + body: create_response, + request_body: request_body + ) + + profile = profiles_service.create(**request_body) + + expect(profile).to be_a(AccessGrid::CredentialProfile) + expect(profile.id).to eq('cp_123') + expect(profile.aid).to eq('F0394148') + expect(profile.name).to eq('Main Office Profile') + expect(profile.keys).to eq([{ 'value' => 'abcdef1234567890abcdef1234567890' }]) + end + end + + describe '#list' do + let(:list_response) do + [ + { + id: 'cp_1', + aid: 'F0394148', + name: 'Profile A', + apple_id: nil, + created_at: '2025-01-01T00:00:00Z', + card_storage: 2048, + keys: [], + files: [] + }, + { + id: 'cp_2', + aid: 'F0394149', + name: 'Profile B', + apple_id: nil, + created_at: '2025-01-02T00:00:00Z', + card_storage: 4096, + keys: [], + files: [] + } + ] + end + + it 'returns a list of credential profiles' do + stub_api_request( + :get, + '/v1/console/credential-profiles', + body: list_response, + query: generate_sig_payload(id: :'credential-profiles') + ) + + profiles = profiles_service.list + + expect(profiles).to be_an(Array) + expect(profiles.length).to eq(2) + expect(profiles.first).to be_a(AccessGrid::CredentialProfile) + expect(profiles.first.id).to eq('cp_1') + expect(profiles.first.aid).to eq('F0394148') + expect(profiles.last.id).to eq('cp_2') + end + + it 'returns empty array when no profiles' do + stub_api_request( + :get, + '/v1/console/credential-profiles', + body: [], + query: generate_sig_payload(id: :'credential-profiles') + ) + + profiles = profiles_service.list + expect(profiles).to eq([]) + end + end + end + describe 'HID orgs' do let(:org_response) do { diff --git a/spec/models/card_spec.rb b/spec/models/card_spec.rb index 1a910b1..03885ec 100644 --- a/spec/models/card_spec.rb +++ b/spec/models/card_spec.rb @@ -23,7 +23,10 @@ { 'type' => 'watch', 'id' => 'dev_2' } ], 'metadata' => { 'department' => 'Sales', 'badge_id' => 'B001' }, - 'temporary' => true + 'temporary' => true, + 'employee_id' => 'emp_789', + 'organization_name' => 'Acme Corp', + 'created_at' => '2025-06-01T00:00:00Z' } end @@ -91,6 +94,18 @@ it 'sets temporary' do expect(card.temporary).to eq(true) end + + it 'sets employee_id' do + expect(card.employee_id).to eq('emp_789') + end + + it 'sets organization_name' do + expect(card.organization_name).to eq('Acme Corp') + end + + it 'sets created_at' do + expect(card.created_at).to eq('2025-06-01T00:00:00Z') + end end context 'with minimal data' do @@ -114,6 +129,9 @@ expect(card.direct_install_url).to be_nil expect(card.details).to be_nil expect(card.temporary).to be_nil + expect(card.employee_id).to be_nil + expect(card.organization_name).to be_nil + expect(card.created_at).to be_nil end it 'defaults devices to empty array' do diff --git a/spec/models/template_spec.rb b/spec/models/template_spec.rb index 830d781..3f572a2 100644 --- a/spec/models/template_spec.rb +++ b/spec/models/template_spec.rb @@ -28,7 +28,8 @@ 'style_settings' => { 'background_color' => '#FFFFFF', 'label_color' => '#000000' - } + }, + 'metadata' => { 'version' => '2.1', 'approval_status' => 'approved' } } end @@ -94,6 +95,10 @@ 'label_color' => '#000000' }) end + + it 'sets metadata' do + expect(template.metadata).to eq({ 'version' => '2.1', 'approval_status' => 'approved' }) + end end context 'with minimal data' do @@ -119,6 +124,10 @@ expect(template.terms_settings).to be_nil expect(template.style_settings).to be_nil end + + it 'defaults metadata to empty hash' do + expect(template.metadata).to eq({}) + end end context 'with nil data' do From 0cf43ce356f198dce26aa26f73cb0af615954fe5 Mon Sep 17 00:00:00 2001 From: Auston Bunsen Date: Fri, 3 Apr 2026 17:34:49 -0400 Subject: [PATCH 2/2] Update Gemfile.lock for version 0.4.0 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index e65e3d7..f4821e3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - accessgrid (0.2.0) + accessgrid (0.4.0) base64 GEM