Class: TimeOffRequests::GoogleAuthService

Inherits:
Object
  • Object
show all
Defined in:
app/services/time_off_requests/google_auth_service.rb

Overview

Service object: google auth service.

Defined Under Namespace

Classes: RefreshExhaustedError

Class Method Summary collapse

Class Method Details

.authorize(employee) ⇒ Object



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
# File 'app/services/time_off_requests/google_auth_service.rb', line 58

def self.authorize(employee)
  auth = employee.&.authentications&.google_auth&.first
  return unless auth

  client = Signet::OAuth2::Client.new(
    client_id: Heatwave::Configuration.fetch(:omniauth, :google_oauth2_id),
    client_secret: Heatwave::Configuration.fetch(:omniauth, :google_oauth2_secret),
    access_token: auth.google_auth_access_token,
    refresh_token: auth.google_auth_refresh_token,
    expires_at: auth.google_auth_expires_at,
    token_credential_uri: 'https://oauth2.googleapis.com/token'
  )

  # Refresh ahead of expiry rather than waiting for Google to 401. We ask the
  # record, not Signet: Signet#expired? is false whenever expires_at is nil,
  # which is every row written before that column existed.
  if auth.google_auth_needs_refresh?
    refreshed_client = refresh_token(employee, client)
    return nil unless refreshed_client

    return refreshed_client
  end

  client
end

.check_status(employee, skip_api_check: false) ⇒ Object

Check if employee has valid Google Calendar authorization
Returns a hash with :connected (boolean) and :needs_reconnect (boolean)

Parameters:

  • employee (Employee)

    the employee to check

  • skip_api_check (Boolean) (defaults to: false)

    if true, only use cached status (fast but may be stale)



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'app/services/time_off_requests/google_auth_service.rb', line 11

def self.check_status(employee, skip_api_check: false)
  return { connected: false, needs_reconnect: false, message: 'No account linked' } unless employee&.

  auth = employee..authentications.google_auth&.first
  return { connected: false, needs_reconnect: false, message: 'Google account not connected' } unless auth

  # Check if we have refresh token (required for calendar access)
  if auth.google_auth_refresh_token.blank?
    update_cached_status(auth, 'needs_reconnect')
    return { connected: false, needs_reconnect: true, message: 'Google account needs to be reconnected' }
  end

  # If skip_api_check, use cached status if available and recent (within 24 hours)
  if skip_api_check && auth.respond_to?(:google_calendar_status) &&
     auth.google_calendar_status.present? && auth.google_calendar_status_checked_at&.>(24.hours.ago)
    case auth.google_calendar_status
    when 'connected'
      return { connected: true, needs_reconnect: false, message: 'Connected' }
    when 'needs_reconnect'
      return { connected: false, needs_reconnect: true, message: 'Google authorization expired - please reconnect' }
    end
  end

  # Actually try to refresh the token to verify it works
  # This is the only reliable way to know if the credentials are still valid
  refreshed_client = refresh_token(employee)
  if refreshed_client
    update_cached_status(auth, 'connected')
    { connected: true, needs_reconnect: false, message: 'Connected' }
  else
    update_cached_status(auth, 'needs_reconnect')
    { connected: false, needs_reconnect: true, message: 'Google authorization expired - please reconnect' }
  end
end

.refresh_token(employee, client = nil) ⇒ Object



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'app/services/time_off_requests/google_auth_service.rb', line 113

def self.refresh_token(employee, client = nil)
  auth = employee..authentications.google_auth&.first
  return unless auth

  client ||= Signet::OAuth2::Client.new(
    client_id: Heatwave::Configuration.fetch(:omniauth, :google_oauth2_id),
    client_secret: Heatwave::Configuration.fetch(:omniauth, :google_oauth2_secret),
    refresh_token: auth.google_auth_refresh_token,
    token_credential_uri: 'https://oauth2.googleapis.com/token'
  )

  begin
    client.refresh!
    auth.update!(google_auth_access_token: client.access_token, google_auth_expires_at: client.expires_at)
    Rails.logger.info("Google OAuth token refreshed successfully for #{employee.email}")
    client
  rescue StandardError => e
    Rails.logger.error("Failed to refresh Google OAuth token: #{e.message}")
    nil
  end
end

.report_refresh_exhausted(employee, context = {}) ⇒ nil

Report a grant that no amount of retrying will fix — revoked, or consent
that never covered the scope. Nobody sees a log line, and the symptom is
silent: calendars read as empty and PTO stops syncing for that employee
until they reconnect. That needs to page someone, so it goes to AppSignal.

Parameters:

  • employee (Employee)
  • context (Hash) (defaults to: {})

    extra ErrorReporting context (e.g. custom_data)

Returns:

  • (nil)


96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'app/services/time_off_requests/google_auth_service.rb', line 96

def self.report_refresh_exhausted(employee, context = {})
  # Employee id, not email: the message is the AppSignal grouping key and ends
  # up in incident titles, so it stays free of PII. party_id below carries the
  # correlation, and the id keeps one broken grant from merging with the next.
  ErrorReporting.error(
    RefreshExhaustedError.new(
      "Google token refresh limit reached for employee #{employee&.id} — the account must be reconnected"
    ),
    {
      party_id: employee&.id,
      reason: 'google_refresh_exhausted',
      source: defined?(Sidekiq) && Sidekiq.server? ? :background : :web
    }.merge(context)
  )
  nil
end

.update_cached_status(auth, status) ⇒ Object

Update the cached Google Calendar status on the authentication record



47
48
49
50
51
52
53
54
55
56
# File 'app/services/time_off_requests/google_auth_service.rb', line 47

def self.update_cached_status(auth, status)
  return unless auth.respond_to?(:google_calendar_status)

  auth.update_columns(
    google_calendar_status: status,
    google_calendar_status_checked_at: Time.current
  )
rescue StandardError => e
  Rails.logger.error("Failed to update cached Google Calendar status: #{e.message}")
end