From 938be908267d4377935729484682eaf6342d17e9 Mon Sep 17 00:00:00 2001 From: elijah Date: Fri, 10 Jul 2015 17:11:44 -0700 Subject: initial commit. --- Gemfile | 3 ++ README.md | 75 ++++++++++++++++++++++++++++++++++++++ app/models/reserved_username.rb | 15 ++++++++ config.yml | 27 ++++++++++++++ lib/reserve_usernames.rb | 12 ++++++ lib/reserve_usernames/callbacks.rb | 73 +++++++++++++++++++++++++++++++++++++ lib/reserve_usernames/engine.rb | 4 ++ lib/reserve_usernames/validate.rb | 27 ++++++++++++++ lib/reserve_usernames/version.rb | 5 +++ reserve_usernames.gemspec | 15 ++++++++ 10 files changed, 256 insertions(+) create mode 100644 Gemfile create mode 100644 README.md create mode 100644 app/models/reserved_username.rb create mode 100644 config.yml create mode 100644 lib/reserve_usernames.rb create mode 100644 lib/reserve_usernames/callbacks.rb create mode 100644 lib/reserve_usernames/engine.rb create mode 100644 lib/reserve_usernames/validate.rb create mode 100644 lib/reserve_usernames/version.rb create mode 100644 reserve_usernames.gemspec 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 -- cgit v1.2.3