Files
sure/app/models/mobile_device.rb
Dream 6779951194 Fix mobile login "Record not found" for unseeded instances (#916)
* Auto-create mobile OAuth application when missing (#912)

Self-hosted users who set up their instance without running `db:seed`
(or reset their database) got "Record not found" on mobile login because
`MobileDevice.shared_oauth_application` used `find_by!` which raises
when the "Sure Mobile" Doorkeeper application does not exist.

Switch to `find_or_create_by!` so the record is created transparently
on first use, matching the attributes from the seed file.

* Nice Claude Code suggestion

---------

Co-authored-by: Juan José Mata <jjmata@jjmata.com>
2026-02-06 18:04:29 +01:00

96 lines
2.6 KiB
Ruby

class MobileDevice < ApplicationRecord
include Encryptable
# Encrypt device_id if ActiveRecord encryption is configured
if encryption_ready?
encrypts :device_id, deterministic: true
end
belongs_to :user
validates :device_id, presence: true, uniqueness: { scope: :user_id }
validates :device_name, presence: true
validates :device_type, presence: true, inclusion: { in: %w[ios android web] }
before_validation :set_last_seen_at, on: :create
CALLBACK_URL = "sureapp://oauth/callback"
scope :active, -> { where("last_seen_at > ?", 90.days.ago) }
def self.shared_oauth_application
@shared_oauth_application ||= begin
Doorkeeper::Application.find_or_create_by!(name: "Sure Mobile") do |app|
app.redirect_uri = CALLBACK_URL
app.scopes = "read_write"
app.confidential = false
end
rescue ActiveRecord::RecordNotUnique
Doorkeeper::Application.find_by!(name: "Sure Mobile")
end
end
def self.upsert_device!(user, attrs)
device = user.mobile_devices.find_or_initialize_by(device_id: attrs[:device_id])
device.assign_attributes(
device_name: attrs[:device_name],
device_type: attrs[:device_type],
os_version: attrs[:os_version],
app_version: attrs[:app_version],
last_seen_at: Time.current
)
device.save!
device
end
def active?
last_seen_at > 90.days.ago
end
def update_last_seen!
update_column(:last_seen_at, Time.current)
end
def active_tokens
Doorkeeper::AccessToken
.where(mobile_device_id: id)
.where(resource_owner_id: user_id)
.where(revoked_at: nil)
.where("expires_in IS NULL OR created_at + expires_in * interval '1 second' > ?", Time.current)
end
def revoke_all_tokens!
active_tokens.update_all(revoked_at: Time.current)
end
# Issues a fresh Doorkeeper access token for this device, revoking any
# previous tokens. Returns a hash with token details ready for an API
# response or deep-link callback.
def issue_token!
revoke_all_tokens!
access_token = Doorkeeper::AccessToken.create!(
application: self.class.shared_oauth_application,
resource_owner_id: user_id,
mobile_device_id: id,
expires_in: 30.days.to_i,
scopes: "read_write",
use_refresh_token: true
)
{
access_token: access_token.plaintext_token,
refresh_token: access_token.plaintext_refresh_token,
token_type: "Bearer",
expires_in: access_token.expires_in,
created_at: access_token.created_at.to_i
}
end
private
def set_last_seen_at
self.last_seen_at ||= Time.current
end
end