Class: Panda::Core::User

Inherits:
ApplicationRecord show all
Includes:
HasMetadata, HasUUID
Defined in:
app/models/panda/core/user.rb

Class Method Summary collapse

Instance Method Summary collapse

Methods included from HasMetadata

#metadata_value, #remove_metadata, #set_metadata, #set_metadata_attribute

Class Method Details

.admin_columnObject

Determine which column stores admin flag (supports legacy admin and new is_admin)



33
34
35
36
# File 'app/models/panda/core/user.rb', line 33

def self.admin_column
  # Prefer canonical `admin` if available, otherwise fall back to legacy `is_admin`
  @admin_column ||= column_names.include?("admin") ? "admin" : "is_admin"
end

.find_or_create_from_auth_hash(auth_hash) ⇒ Object



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'app/models/panda/core/user.rb', line 51

def self.find_or_create_from_auth_hash(auth_hash)
  user = find_by(email: auth_hash.info.email.downcase)

  avatar_url = auth_hash.info.image
  if user
    has_stored_avatar = begin
      user.avatar.attached?
    rescue
      false
    end

    if avatar_url.present?
      # Update image_url with latest OAuth URL only if no local avatar is stored
      user.update_column(:image_url, avatar_url) unless has_stored_avatar

      # Skip OAuth avatar download when user has a manually uploaded avatar
      # (indicated by oauth_avatar_url being nil while an avatar is attached)
      manually_uploaded = has_stored_avatar && user.oauth_avatar_url.nil?
      unless manually_uploaded
        # Try to download and store avatar if URL changed or no avatar attached
        if avatar_url != user.oauth_avatar_url || !has_stored_avatar
          AttachAvatarService.call(user: user, avatar_url: avatar_url)
        end
      end
    end
    return user
  end

  # Check if user creation is restricted (e.g. invite-only mode)
  restrict = Panda::Core.config.restrict_user_creation
  restricted = restrict.respond_to?(:call) ? restrict.call(auth_hash) : restrict
  if restricted
    user = new(email: auth_hash.info.email.downcase, name: auth_hash.info.name || "Unknown User")
    user.errors.add(:base, "No account exists for this email address. Please contact your administrator.")
    return user
  end

  attributes = {
    :email => auth_hash.info.email.downcase,
    :name => auth_hash.info.name || "Unknown User",
    :image_url => avatar_url,
    admin_column => User.count.zero? # First user is admin
  }

  user = create!(attributes)

  # Attach avatar for new user (will clear image_url on success)
  if avatar_url.present?
    AttachAvatarService.call(user: user, avatar_url: avatar_url)
  end

  user
end

Instance Method Details

#accept_invitation!Object



145
146
147
148
149
150
# File 'app/models/panda/core/user.rb', line 145

def accept_invitation!
  update!(
    invitation_accepted_at: Time.current,
    invitation_token: nil
  )
end

#active_for_authentication?Boolean

Returns:

  • (Boolean)


121
122
123
# File 'app/models/panda/core/user.rb', line 121

def active_for_authentication?
  enabled?
end

#adminObject Also known as: is_admin

Support both legacy admin and new is_admin columns



111
112
113
# File 'app/models/panda/core/user.rb', line 111

def admin
  self[self.class.admin_column]
end

#admin=(value) ⇒ Object Also known as: is_admin=



115
116
117
# File 'app/models/panda/core/user.rb', line 115

def admin=(value)
  self[self.class.admin_column] = ActiveRecord::Type::Boolean.new.cast(value)
end

#admin?Boolean

Admin status check

Returns:

  • (Boolean)


106
107
108
# File 'app/models/panda/core/user.rb', line 106

def admin?
  ActiveRecord::Type::Boolean.new.cast(admin)
end

#avatar_url(size: nil) ⇒ String?

Returns the URL for the user's avatar Prefers Active Storage attachment over OAuth provider URL

Parameters:

  • size (Symbol) (defaults to: nil)

    The variant size (:thumb, :small, :medium, :large, or nil for original)

Returns:

  • (String, nil)

    The avatar URL or nil if no avatar available



164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'app/models/panda/core/user.rb', line 164

def avatar_url(size: nil)
  return self[:image_url].presence unless avatar.attached?

  helpers = Rails.application.routes.url_helpers
  if size && [:thumb, :small, :medium, :large].include?(size)
    helpers.rails_representation_path(avatar.variant(size), only_path: true)
  else
    helpers.rails_blob_path(avatar.blob, only_path: true)
  end
rescue => e
  Rails.logger.error("Error generating avatar URL for user #{id}: #{e.message}\n#{e.backtrace&.first(3)&.join("\n")}")
  self[:image_url].presence
end

#disable!Object



129
130
131
# File 'app/models/panda/core/user.rb', line 129

def disable!
  update!(enabled: false)
end

#enable!Object



125
126
127
# File 'app/models/panda/core/user.rb', line 125

def enable!
  update!(enabled: true)
end

#enabled?Boolean

Returns:

  • (Boolean)


133
134
135
# File 'app/models/panda/core/user.rb', line 133

def enabled?
  self[:enabled] != false
end

#invite!(invited_by:) ⇒ Object



137
138
139
140
141
142
143
# File 'app/models/panda/core/user.rb', line 137

def invite!(invited_by:)
  update!(
    invitation_token: SecureRandom.urlsafe_base64(32),
    invitation_sent_at: Time.current,
    invited_by: invited_by
  )
end

#track_login!(request) ⇒ Object



152
153
154
155
156
157
158
# File 'app/models/panda/core/user.rb', line 152

def track_login!(request)
  update!(
    last_login_at: Time.current,
    last_login_ip: request.remote_ip,
    login_count: ( || 0) + 1
  )
end