Skip to content

Commit

Permalink
feat(user): add feature to sync from and to cognito
Browse files Browse the repository at this point in the history
  • Loading branch information
ProGM committed Mar 30, 2023
1 parent c0ef734 commit d0ba22a
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 12 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# Changelog
All notable changes to this project made by Monade Team are documented in this file. For info refer to [email protected]

## [UNRELEASED] - 2023-03-30
## [1.0.0] - 2023-03-30
### Added
- `sync_from_cognito!` to create users in the local database from cognito
- `sync_to_cognito!` to create cognito users based from the local database

### Changed
- [BREAKING] Switched from explicit `aws_access_key`/`aws_secret_access_key` to a more flexible `aws_client_credentials`

Expand Down
49 changes: 49 additions & 0 deletions lib/cognito_rails/model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,55 @@ module Model
end
end

# rubocop:disable Metrics/BlockLength
class_methods do
# @return [Array<ActiveRecord::Base>] all users
# @raise [CognitoRails::Error] if failed to fetch users
# @raise [ActiveRecord::RecordInvalid] if failed to save user
def sync_from_cognito!
response = User.all
response.users.map do |user_data|
sync_user!(user_data)
end
end

# @return [Array<ActiveRecord::Base>] all users
# @raise [CognitoRails::Error] if failed to fetch users
# @raise [ActiveRecord::RecordInvalid] if failed to save user
def sync_to_cognito!
find_each.map do |user|
user.init_cognito_user
user.save!
end
end

private

def sync_user!(user_data)
external_id = user_data.username
return if external_id.blank?

user = find_or_initialize_by(_cognito_attribute_name => external_id)
user.email = User.extract_cognito_attribute(user_data.attributes, :email)
user.phone = User.extract_cognito_attribute(user_data.attributes, :phone_number) if user.respond_to?(:phone)
_cognito_resolve_custom_attribute(user, user_data)

user.save!
user
end

def _cognito_resolve_custom_attribute(user, user_data)
_cognito_custom_attributes.each do |attribute|
next if attribute[:value].is_a?(String)

value = User.extract_cognito_attribute(user_data.attributes, attribute[:name])
next unless value

user[attribute[:name].gsub('custom:', '')] = value
end
end
end

# @return [String]
def cognito_external_id
self[self.class._cognito_attribute_name]
Expand Down
28 changes: 18 additions & 10 deletions lib/cognito_rails/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,15 @@ def self.find(id, user_class = nil)
)
user = new(user_class: user_class)
user.id = result.username
user.email = result.user_attributes.find { |attribute| attribute[:name] == 'email' }[:value]
user.phone = result.user_attributes.find { |attribute| attribute[:name] == 'phone_number' }&.dig(:value)
user.email = extract_cognito_attribute(result.user_attributes, :email)
user.phone = extract_cognito_attribute(result.user_attributes, :phone_number)
user
end

def self.all
cognito_client.list_users(user_pool_id: CognitoRails::Config.aws_user_pool_id)
end

# @param attributes [Hash]
# @option attributes [String] :email
# @option attributes [String] :password
Expand Down Expand Up @@ -138,6 +142,18 @@ def destroy!
destroy || (raise ActiveRecord::RecordInvalid, self)
end

# @return [Aws::CognitoIdentityProvider::Client]
# @raise [RuntimeError]
def self.cognito_client
@cognito_client ||= Aws::CognitoIdentityProvider::Client.new(
{ region: CognitoRails::Config.aws_region }.merge(CognitoRails::Config.aws_client_credentials)
)
end

def self.extract_cognito_attribute(attributes, column)
attributes.find { |attribute| attribute[:name] == column.to_s }&.dig(:value)
end

private

# @return [Aws::CognitoIdentityProvider::Client]
Expand All @@ -155,14 +171,6 @@ def verify_phone?
user_class._cognito_verify_phone
end

# @return [Aws::CognitoIdentityProvider::Client]
# @raise [RuntimeError]
def self.cognito_client
@cognito_client ||= Aws::CognitoIdentityProvider::Client.new(
{ region: CognitoRails::Config.aws_region }.merge(CognitoRails::Config.aws_client_credentials)
)
end

# @return [Array<Hash>]
def general_user_attributes
[
Expand Down
2 changes: 1 addition & 1 deletion lib/cognito_rails/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

module CognitoRails
# @return [String] gem version
VERSION = '0.1.0'
VERSION = '1.0.0'
end
36 changes: 36 additions & 0 deletions spec/cognito_rails/user_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,42 @@
end
end

context 'class methods' do
before do
expect(CognitoRails::User).to receive(:cognito_client).at_least(:once).and_return(fake_cognito_client)
end

it '#sync_from_cognito!' do
expect(fake_cognito_client).to receive(:list_users).and_return(
OpenStruct.new(
users: [
build_cognito_user_data('[email protected]'),
build_cognito_user_data('[email protected]')
],
pagination_token: nil
)
)

expect do
users = User.sync_from_cognito!

expect(users).to be_a(Array)
expect(users.size).to eq(2)
expect(users.first).to be_a(User)
end.to change { User.count }.by(2)

expect(User.pluck(:email)).to match_array(['[email protected]', '[email protected]'])
expect(User.pluck(:name)).to match_array(['Giovanni', 'Giovanni'])
end

it '#sync_to_cognito!' do
User.create!(email: sample_cognito_email)

expect_any_instance_of(User).to receive(:init_cognito_user).exactly(1).times
User.sync_to_cognito!
end
end

context 'admin' do
before do
expect(CognitoRails::User).to receive(:cognito_client).at_least(:once).and_return(fake_cognito_client)
Expand Down
20 changes: 20 additions & 0 deletions spec/support/cognito_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,24 @@ module CognitoRails::Helpers
client
end
end

def build_cognito_user_data(email)
OpenStruct.new(
username: SecureRandom.uuid,
user_status: 'CONFIRMED',
enabled: true,
user_last_modified_date: Time.now,
attributes: [
OpenStruct.new(
name: 'email',
value: email
),
OpenStruct.new(
name: 'custom:name',
value: 'Giovanni'
)
],
mfa_options: []
)
end
end

0 comments on commit d0ba22a

Please sign in to comment.