summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Gemfile3
-rw-r--r--README.md75
-rw-r--r--app/models/reserved_username.rb15
-rw-r--r--config.yml27
-rw-r--r--lib/reserve_usernames.rb12
-rw-r--r--lib/reserve_usernames/callbacks.rb73
-rw-r--r--lib/reserve_usernames/engine.rb4
-rw-r--r--lib/reserve_usernames/validate.rb27
-rw-r--r--lib/reserve_usernames/version.rb5
-rw-r--r--reserve_usernames.gemspec15
10 files changed, 256 insertions, 0 deletions
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..b4e2a20
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,3 @@
+source "https://rubygems.org"
+
+gemspec
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e517ee0
--- /dev/null
+++ b/README.md
@@ -0,0 +1,75 @@
+Reserve Usernames
+==============================
+
+This is an engine that adds support to leap_web for checking an external REST
+API to see if a username is available. Once the account is created, the
+username is reserved via the same external API.
+
+TODO:
+
+* support the ability for a user to change their username
+* sanitize error messages before displaying
+
+See the file config.yml for the development and production values used for
+accessing the remote API.
+
+This gem is distributed on the same license as leap_web.
+
+Notes
+--------------------------------
+
+This engine uses ActiveResource, which maps a ActiveRecord-like interface to a
+REST API. So, the remote API must conform to the API expected by
+ActiveResource.
+
+* http://rubydoc.info/gems/activeresource/3.2.18/frames
+* http://api.rubyonrails.org/v3.2.18/files/activeresource/README_rdoc.html
+* https://github.com/rails/activeresource
+
+ActiveResource exceptions:
+
+ 301, 302, 303, 307 - ActiveResource::Redirection
+ 400 - ActiveResource::BadRequest
+ 401 - ActiveResource::UnauthorizedAccess
+ 403 - ActiveResource::ForbiddenAccess
+ 404 - ActiveResource::ResourceNotFound
+ 405 - ActiveResource::MethodNotAllowed
+ 409 - ActiveResource::ResourceConflict
+ 410 - ActiveResource::ResourceGone
+ 422 - ActiveResource::ResourceInvalid (rescued by save as validation errors)
+ 401..499 - ActiveResource::ClientError
+ 500..599 - ActiveResource::ServerError
+ Other - ActiveResource::ConnectionError
+
+Errors must be returned by the remote API in this format:
+
+ {
+ "errors": [
+ "fieldname1 message about fieldname1",
+ "fieldname2 message about fieldname2"
+ ]
+ }
+
+Here is the code that parses these errors (active_resource/validations.rb):
+
+ class Errors < ActiveModel::Errors
+ def from_array(messages, save_cache = false)
+ clear unless save_cache
+ humanized_attributes = Hash[@base.attributes.keys.map { |attr_name| [attr_name.humanize, attr_name] }]
+ messages.each do |message|
+ attr_message = humanized_attributes.keys.detect do |attr_name|
+ if message[0, attr_name.size + 1] == "#{attr_name} "
+ add humanized_attributes[attr_name], message[(attr_name.size + 1)..-1]
+ end
+ end
+
+ self[:base] << message if attr_message.nil?
+ end
+ end
+
+ # Grabs errors from a json response.
+ def from_json(json, save_cache = false)
+ array = Array.wrap(ActiveSupport::JSON.decode(json)['errors']) rescue []
+ from_array array, save_cache
+ end
+ end \ No newline at end of file
diff --git a/app/models/reserved_username.rb b/app/models/reserved_username.rb
new file mode 100644
index 0000000..899b319
--- /dev/null
+++ b/app/models/reserved_username.rb
@@ -0,0 +1,15 @@
+require 'active_resource'
+
+class ReservedUsername < ActiveResource::Base
+ self.include_root_in_json = false
+
+ self.site = "%s://%s:%s@%s:%s" % [
+ ReserveUsernames::CONFIG['api_proto'],
+ ReserveUsernames::CONFIG['api_username'],
+ ReserveUsernames::CONFIG['api_password'],
+ ReserveUsernames::CONFIG['api_host'],
+ ReserveUsernames::CONFIG['api_port']
+ ]
+ self.element_name = ReserveUsernames::CONFIG['api_path']
+
+end
diff --git a/config.yml b/config.yml
new file mode 100644
index 0000000..ffb08f7
--- /dev/null
+++ b/config.yml
@@ -0,0 +1,27 @@
+development:
+ domain: example.org
+ api_path: api/reserved_usernames
+ api_username: admin
+ api_password: password
+ api_host: localhost
+ api_port: 3000
+ api_proto: http
+
+test:
+ domain: example.org
+ api_path: api/reserved_usernames
+ api_username: admin
+ api_password: password
+ api_host: localhost
+ api_port: 3000
+ api_proto: http
+
+production:
+ domain: example.org
+ api_path: api/reserved_usernames
+ api_username: xxxxxxxxxxxxxxxx
+ api_password: xxxxxxxxxxxxxxxx
+ api_host: api.example.org
+ api_port: 443
+ api_proto: https
+
diff --git a/lib/reserve_usernames.rb b/lib/reserve_usernames.rb
new file mode 100644
index 0000000..6fc6d2c
--- /dev/null
+++ b/lib/reserve_usernames.rb
@@ -0,0 +1,12 @@
+module ReserveUsernames
+ CONFIG = YAML.load_file(File.expand_path('../../config.yml', __FILE__))[Rails.env]
+end
+
+require 'reserve_usernames/engine'
+require 'reserve_usernames/callbacks'
+require 'reserve_usernames/validate'
+
+ActiveSupport.on_load(:identity) do
+ include ReserveUsernames::Validate
+ include ReserveUsernames::Callbacks
+end
diff --git a/lib/reserve_usernames/callbacks.rb b/lib/reserve_usernames/callbacks.rb
new file mode 100644
index 0000000..a4bc2de
--- /dev/null
+++ b/lib/reserve_usernames/callbacks.rb
@@ -0,0 +1,73 @@
+module ReserveUsernames::Callbacks
+ def self.included(base)
+ base.class_eval do
+
+ around_create :create_reservation
+ around_destroy :destroy_reservation
+
+ protected
+
+ def create_reservation
+ if username
+ Rails.logger.info("ReserveUsernames - creating reservation for username '#{username}'.")
+ ru = ReservedUsername.create(:username => username, :reserved_by => owner_string)
+ ru.encode
+ unless ru.persisted?
+ if has_errors?(ru)
+ Rails.logger.error("ReserveUsernames - failed to reserve username (#{ru.errors.error_messages}).")
+ return false
+ else
+ Rails.logger.error("ReserveUsernames - failed to reserve username (remote error).")
+ raise RuntimeError.new('Failed to reserve username. (remote username reservation error)')
+ end
+ end
+ end
+ yield
+ if self.persisted?
+ ru.confirmed = true
+ ru.save
+ return false if has_errors?(ru)
+ end
+ end
+
+ def destroy_reservation
+ ru = ReservedUsername.find(username)
+ ru.deleted = true
+ ru.save
+ yield
+ if self.destroyed?
+ ReservedUsername.delete(username)
+ end
+ rescue ActiveResource::ResourceNotFound
+ end
+
+ def username
+ address ? address.split('@').first : nil
+ end
+
+ #
+ # a label to identify the owner
+ #
+ def owner_string
+ if user && user.id
+ user.id
+ else
+ username
+ end
+ end
+
+ def has_errors?(record)
+ if record.errors.any?
+ record.errors.each do |attr, msg|
+ Rails.logger.error("ReserveUsernames - REMOTE ERROR #{attr}: #{msg}")
+ self.errors.add(attr, msg + " (remote username reservation error)")
+ end
+ true
+ else
+ false
+ end
+ end
+
+ end
+ end
+end
diff --git a/lib/reserve_usernames/engine.rb b/lib/reserve_usernames/engine.rb
new file mode 100644
index 0000000..a4bdbfa
--- /dev/null
+++ b/lib/reserve_usernames/engine.rb
@@ -0,0 +1,4 @@
+module ReserveUsernames
+ class Engine < ::Rails::Engine
+ end
+end
diff --git a/lib/reserve_usernames/validate.rb b/lib/reserve_usernames/validate.rb
new file mode 100644
index 0000000..669c2fb
--- /dev/null
+++ b/lib/reserve_usernames/validate.rb
@@ -0,0 +1,27 @@
+module ReserveUsernames::Validate
+ def self.included(base)
+ base.class_eval do
+
+ validate :check_if_address_is_taken
+
+ protected
+
+ #
+ # Only query ReservedUsername if this is a new record.
+ # Otherwise, validation will always fail because the address
+ # has been taken... by this identity.
+ #
+ def check_if_address_is_taken
+ if new_record? && address
+ username = address.split('@').first
+ if ReservedUsername.find(username)
+ Rails.logger.info("ReserveUsernames - #{username} is taken.")
+ errors.add :address, :taken
+ end
+ end
+ rescue ActiveResource::ResourceNotFound
+ end
+
+ end
+ end
+end
diff --git a/lib/reserve_usernames/version.rb b/lib/reserve_usernames/version.rb
new file mode 100644
index 0000000..b92ea68
--- /dev/null
+++ b/lib/reserve_usernames/version.rb
@@ -0,0 +1,5 @@
+module ReserveUsernames
+ unless defined?(VERSION)
+ VERSION = "0.0.1"
+ end
+end
diff --git a/reserve_usernames.gemspec b/reserve_usernames.gemspec
new file mode 100644
index 0000000..66bb55e
--- /dev/null
+++ b/reserve_usernames.gemspec
@@ -0,0 +1,15 @@
+$:.push File.expand_path("../lib", __FILE__)
+
+require "reserve_usernames/version"
+
+Gem::Specification.new do |s|
+ s.name = "reserve_usernames"
+ s.version = ReserveUsernames::VERSION
+ s.authors = ["LEAP"]
+ s.summary = "Connects to a external REST API in order to reserve usernames"
+
+ s.files = Dir["{app,config,db,lib}/**/*"] + ["README.md"]
+
+ s.add_dependency "rails", "~> 3.2.18"
+ s.add_dependency "activeresource", "~> 3.2.18"
+end