# frozen_string_literal: true module Maquina ## # A class representing the User model in the Maquina module. # # == Attributes # # - +email+:: The user's email address (required, unique, automatically normalized). # - +password+:: Encrypted password that must meet complexity requirements. # - +password_expires_at+:: Timestamp indicating when the password expires. # # == Associations # # - +memberships+:: Has many memberships linking users to organizations. # - +active_sessions+:: Has many active sessions that are destroyed when the user is deleted. # # == Included Modules # # - +RetainPasswords+:: Handles password retention functionality. # - +Blockeable+:: Provides blocking/unblocking capabilities. # - +Multifactor+:: Implements multi-factor authentication features. # # == Validations # # - +email+:: Must be present, unique, and in valid email format. # - +password+:: Must contain at least: # * 8 characters # * 1 uppercase letter # * 1 lowercase letter # * 1 number # * 1 special character (@$!%*?&#-=+) # # == Normalizations # # - +email+:: Automatically stripped of whitespace and converted to lowercase. # # == Security # # Uses has_secure_password for password encryption and authentication. # class User < ApplicationRecord include Maquina::RetainPasswords include Maquina::Blockeable include Maquina::Multifactor PASSWORD_COMPLEXITY_REGEX = /\A(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#-=+])[A-Za-z\d@$!%*?&#-=+]{8,}\z/ has_secure_password has_many :memberships, class_name: "Maquina::Membership", foreign_key: :maquina_user_id, inverse_of: :user has_many :active_sessions, class_name: "Maquina::ActiveSession", foreign_key: :maquina_user_id, dependent: :destroy validates :email, presence: true, uniqueness: true, format: {with: URI::MailTo::EMAIL_REGEXP} validates :password, format: {with: PASSWORD_COMPLEXITY_REGEX}, unless: ->(user) { user.password.blank? } normalizes :email, with: ->(email) { email.strip.downcase } # Checks if the user's password has expired # # Returns true if password_expires_at is set and in the past, false otherwise def expired_password? return false if password_expires_at.blank? password_expires_at < Time.zone.now end # Returns the user's default active membership # # A default membership is the first unblocked membership associated with an active organization. # Returns nil if user is management or no valid membership exists. # # Returns: # - Maquina::Membership: the default membership # - nil: if user is management or no valid membership exists def default_membership return nil if management? memberships.detect { |membership| membership.blocked_at.blank? && membership.organization.present? && membership.organization.active? } end end end