diff options
Diffstat (limited to 'users/app/models')
-rw-r--r-- | users/app/models/account_settings.rb | 36 | ||||
-rw-r--r-- | users/app/models/email.rb | 35 | ||||
-rw-r--r-- | users/app/models/identity.rb | 82 | ||||
-rw-r--r-- | users/app/models/local_email.rb | 68 | ||||
-rw-r--r-- | users/app/models/login_format_validation.rb | 19 | ||||
-rw-r--r-- | users/app/models/remote_email.rb | 14 | ||||
-rw-r--r-- | users/app/models/session.rb | 6 | ||||
-rw-r--r-- | users/app/models/signup_service.rb | 9 | ||||
-rw-r--r-- | users/app/models/token.rb | 4 | ||||
-rw-r--r-- | users/app/models/user.rb | 71 |
10 files changed, 210 insertions, 134 deletions
diff --git a/users/app/models/account_settings.rb b/users/app/models/account_settings.rb new file mode 100644 index 0000000..27fa227 --- /dev/null +++ b/users/app/models/account_settings.rb @@ -0,0 +1,36 @@ +class AccountSettings + + def initialize(user) + @user = user + end + + def update(attrs) + if attrs[:password_verifier].present? + update_login(attrs[:login]) + @user.update_attributes attrs.slice(:password_verifier, :password_salt) + end + # TODO: move into identity controller + update_pgp_key(attrs[:public_key]) if attrs.has_key? :public_key + @user.save && save_identities + end + + protected + + def update_login(login) + return unless login.present? + @old_identity = Identity.for(@user) + @user.login = login + @new_identity = Identity.for(@user) # based on the new login + @old_identity.destination = @user.email_address # alias old -> new + end + + def update_pgp_key(key) + @new_identity ||= Identity.for(@user) + @new_identity.set_key(:pgp, key) + end + + def save_identities + @new_identity.try(:save) && @old_identity.try(:save) + end + +end diff --git a/users/app/models/email.rb b/users/app/models/email.rb index 6d82f2a..1bcff1c 100644 --- a/users/app/models/email.rb +++ b/users/app/models/email.rb @@ -1,33 +1,22 @@ -module Email - extend ActiveSupport::Concern +class Email < String + include ActiveModel::Validations - included do - validates :email, - :format => { - :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/, - :message => "needs to be a valid email address" - } - end - - def initialize(attributes = nil, &block) - attributes = {:email => attributes} if attributes.is_a? String - super(attributes, &block) - end - - def to_s - email - end - - def ==(other) - other.is_a?(Email) ? self.email == other.email : self.email == other - end + validates :email, + :format => { + :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/, + :message => "needs to be a valid email address" + } def to_partial_path "emails/email" end def to_param - email + to_s + end + + def email + self end end diff --git a/users/app/models/identity.rb b/users/app/models/identity.rb new file mode 100644 index 0000000..355f67a --- /dev/null +++ b/users/app/models/identity.rb @@ -0,0 +1,82 @@ +class Identity < CouchRest::Model::Base + + use_database :identities + + belongs_to :user + + property :address, LocalEmail + property :destination, Email + property :keys, HashWithIndifferentAccess + + validate :unique_forward + validate :alias_available + + design do + view :by_user_id + view :by_address_and_destination + view :by_address + view :pgp_key_by_email, + map: <<-EOJS + function(doc) { + if (doc.type != 'Identity') { + return; + } + emit(doc.address, doc.keys["pgp"]); + } + EOJS + + end + + def self.for(user, attributes = {}) + find_for(user, attributes) || build_for(user, attributes) + end + + def self.find_for(user, attributes = {}) + attributes.reverse_merge! attributes_from_user(user) + find_by_address_and_destination [attributes[:address], attributes[:destination]] + end + + def self.build_for(user, attributes = {}) + attributes.reverse_merge! attributes_from_user(user) + Identity.new(attributes) + end + + def self.create_for(user, attributes = {}) + identity = build_for(user, attributes) + identity.save + identity + end + + def self.attributes_from_user(user) + { user_id: user.id, + address: user.email_address, + destination: user.email_address + } + end + + def keys + read_attribute('keys') || HashWithIndifferentAccess.new + end + + def set_key(type, value) + return if keys[type] == value + write_attribute('keys', keys.merge(type => value)) + end + + protected + + def unique_forward + same = Identity.find_by_address_and_destination([address, destination]) + if same && same != self + errors.add :base, "This alias already exists" + end + end + + def alias_available + same = Identity.find_by_address(address) + if same && same.user != self.user + errors.add :base, "This email has already been taken" + end + end + +end diff --git a/users/app/models/local_email.rb b/users/app/models/local_email.rb index 69cba01..c1f7c11 100644 --- a/users/app/models/local_email.rb +++ b/users/app/models/local_email.rb @@ -1,63 +1,39 @@ -class LocalEmail - include CouchRest::Model::Embeddable - include Email +class LocalEmail < Email - property :username, String - before_validation :strip_domain_if_needed - - validates :username, - :presence => true, - :format => { :with => /\A([^@\s]+)(@#{APP_CONFIG[:domain]})?\Z/i, :message => "needs to be a valid login or email address @#{APP_CONFIG[:domain]}"} - - validate :unique_on_server - validate :unique_alias_for_user - validate :differs_from_login - - validates :casted_by, :presence => true - - def email - return '' if username.nil? - username + '@' + APP_CONFIG[:domain] + def self.domain + APP_CONFIG[:domain] end - def email=(value) - return if value.blank? - self.username = value - strip_domain_if_needed + validates :email, + :format => { + :with => /@#{domain}\Z/i, + :message => "needs to end in @#{domain}" + } + + def initialize(s) + super + append_domain_if_needed end def to_key - [username] + [handle] end - protected - - def unique_on_server - has_email = User.find_by_login_or_alias(username) - if has_email && has_email != self.casted_by - errors.add :username, "has already been taken" - end + def handle + gsub(/@#{domain}/i, '') end - def unique_alias_for_user - aliases = self.casted_by.email_aliases - if aliases.select{|a|a.username == self.username}.count > 1 - errors.add :username, "is already your alias" - end + def domain + LocalEmail.domain end - def differs_from_login - # If this has not changed but the email let's mark the email invalid instead. - return if self.persisted? - user = self.casted_by - if user.login == self.username - errors.add :username, "may not be the same as your email address" - end - end + protected - def strip_domain_if_needed - self.username.gsub! /@#{APP_CONFIG[:domain]}/i, '' + def append_domain_if_needed + unless self.index('@') + self << '@' + domain + end end end diff --git a/users/app/models/login_format_validation.rb b/users/app/models/login_format_validation.rb new file mode 100644 index 0000000..1d02bd1 --- /dev/null +++ b/users/app/models/login_format_validation.rb @@ -0,0 +1,19 @@ +module LoginFormatValidation + extend ActiveSupport::Concern + + included do + # Have multiple regular expression validations so we can get specific error messages: + validates :login, + :format => { :with => /\A.{2,}\z/, + :message => "Login must have at least two characters"} + validates :login, + :format => { :with => /\A[a-z\d_\.-]+\z/, + :message => "Only lowercase letters, digits, . - and _ allowed."} + validates :login, + :format => { :with => /\A[a-z].*\z/, + :message => "Login must begin with a lowercase letter"} + validates :login, + :format => { :with => /\A.*[a-z\d]\z/, + :message => "Login must end with a letter or digit"} + end +end diff --git a/users/app/models/remote_email.rb b/users/app/models/remote_email.rb deleted file mode 100644 index 4fe7425..0000000 --- a/users/app/models/remote_email.rb +++ /dev/null @@ -1,14 +0,0 @@ -class RemoteEmail - include CouchRest::Model::Embeddable - include Email - - property :email, String - - def username - email.spilt('@').first - end - - def domain - email.split('@').last - end -end diff --git a/users/app/models/session.rb b/users/app/models/session.rb index a9fdb1b..0d7e10e 100644 --- a/users/app/models/session.rb +++ b/users/app/models/session.rb @@ -1,12 +1,10 @@ class Session < SRP::Session include ActiveModel::Validations + include LoginFormatValidation attr_accessor :login - validates :login, - :presence => true, - :format => { :with => /\A[A-Za-z\d_]+\z/, - :message => "Only letters, digits and _ allowed" } + validates :login, :presence => true def initialize(user = nil, aa = nil) super(user, aa) if user diff --git a/users/app/models/signup_service.rb b/users/app/models/signup_service.rb new file mode 100644 index 0000000..f316ca9 --- /dev/null +++ b/users/app/models/signup_service.rb @@ -0,0 +1,9 @@ +class SignupService + + def register(attrs) + User.create(attrs).tap do |user| + Identity.create_for user + end + end + +end diff --git a/users/app/models/token.rb b/users/app/models/token.rb index cc62778..514b97f 100644 --- a/users/app/models/token.rb +++ b/users/app/models/token.rb @@ -6,6 +6,10 @@ class Token < CouchRest::Model::Base validates :user_id, presence: true + def user + User.find(self.user_id) + end + def initialize(*args) super self.id = SecureRandom.urlsafe_base64(32).gsub(/^_*/, '') diff --git a/users/app/models/user.rb b/users/app/models/user.rb index 413b4ac..c1988f3 100644 --- a/users/app/models/user.rb +++ b/users/app/models/user.rb @@ -1,4 +1,5 @@ class User < CouchRest::Model::Base + include LoginFormatValidation use_database :users @@ -6,11 +7,6 @@ class User < CouchRest::Model::Base property :password_verifier, String, :accessible => true property :password_salt, String, :accessible => true - property :email_forward, String, :accessible => true - property :email_aliases, [LocalEmail] - - property :public_key, :accessible => true - property :enabled, TrueClass, :default => true validates :login, :password_salt, :password_verifier, @@ -20,20 +16,6 @@ class User < CouchRest::Model::Base :uniqueness => true, :if => :serverside? - # Have multiple regular expression validations so we can get specific error messages: - validates :login, - :format => { :with => /\A.{2,}\z/, - :message => "Login must have at least two characters"} - validates :login, - :format => { :with => /\A[a-z\d_\.-]+\z/, - :message => "Only lowercase letters, digits, . - and _ allowed."} - validates :login, - :format => { :with => /\A[a-z].*\z/, - :message => "Login must begin with a lowercase letter"} - validates :login, - :format => { :with => /\A.*[a-z\d]\z/, - :message => "Login must end with a letter or digit"} - validate :login_is_unique_alias validates :password_salt, :password_verifier, @@ -43,10 +25,6 @@ class User < CouchRest::Model::Base :confirmation => true, :format => { :with => /.{8}.*/, :message => "needs to be at least 8 characters long" } - validates :email_forward, - :allow_blank => true, - :format => { :with => /\A(([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,}))?\Z/, :message => "needs to be a valid email address"} - timestamps! design do @@ -54,19 +32,6 @@ class User < CouchRest::Model::Base load_views(own_path.join('..', 'designs', 'user')) view :by_login view :by_created_at - view :pgp_key_by_handle, - map: <<-EOJS - function(doc) { - if (doc.type != 'User') { - return; - } - emit(doc.login, doc.public_key); - doc.email_aliases.forEach(function(alias){ - emit(alias.username, doc.public_key); - }); - } - EOJS - end # end of design class << self @@ -105,16 +70,30 @@ class User < CouchRest::Model::Base APP_CONFIG['admins'].include? self.login end - # this currently only adds the first email address submitted. - # All the ui needs for now. - def email_aliases_attributes=(attrs) - email_aliases.build(attrs.values.first) if attrs - end - def most_recent_tickets(count=3) Ticket.for_user(self).limit(count).all #defaults to having most recent updated first end + # DEPRECATED + # + # Please set the key on the identity directly + # WARNING: This will not be serialized with the user record! + # It is only a workaround for the key form. + def public_key=(value) + identity.set_key(:pgp, value) + end + + # DEPRECATED + # + # Please access identity.keys[:pgp] directly + def public_key + identity.keys[:pgp] + end + + def identity + @identity ||= Identity.for(self) + end + protected ## @@ -122,12 +101,10 @@ class User < CouchRest::Model::Base ## def login_is_unique_alias - has_alias = User.find_by_login_or_alias(username) - return if has_alias.nil? - if has_alias != self + alias_identity = Identity.find_by_address(self.email_address) + return if alias_identity.blank? + if alias_identity.user != self errors.add(:login, "has already been taken") - elsif has_alias.login != self.login - errors.add(:login, "may not be the same as one of your aliases") end end |