initial commit. master
authorelijah <elijah@riseup.net>
Sat, 11 Jul 2015 00:11:44 +0000 (17:11 -0700)
committerelijah <elijah@riseup.net>
Sat, 11 Jul 2015 00:11:44 +0000 (17:11 -0700)
Gemfile [new file with mode: 0644]
README.md [new file with mode: 0644]
app/models/reserved_username.rb [new file with mode: 0644]
config.yml [new file with mode: 0644]
lib/reserve_usernames.rb [new file with mode: 0644]
lib/reserve_usernames/callbacks.rb [new file with mode: 0644]
lib/reserve_usernames/engine.rb [new file with mode: 0644]
lib/reserve_usernames/validate.rb [new file with mode: 0644]
lib/reserve_usernames/version.rb [new file with mode: 0644]
reserve_usernames.gemspec [new file with mode: 0644]

diff --git a/Gemfile b/Gemfile
new file mode 100644 (file)
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 (file)
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 (file)
index 0000000..899b319
--- /dev/null
@@ -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 (file)
index 0000000..ffb08f7
--- /dev/null
@@ -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 (file)
index 0000000..6fc6d2c
--- /dev/null
@@ -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 (file)
index 0000000..a4bc2de
--- /dev/null
@@ -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 (file)
index 0000000..a4bdbfa
--- /dev/null
@@ -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 (file)
index 0000000..669c2fb
--- /dev/null
@@ -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 (file)
index 0000000..b92ea68
--- /dev/null
@@ -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 (file)
index 0000000..66bb55e
--- /dev/null
@@ -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