Class: YouTube::OauthService

Inherits:
Object
  • Object
show all
Defined in:
app/services/youtube/oauth_service.rb

Overview

Handles Google/YouTube OAuth 2.0 token exchange and refresh.

YouTube uses standard OAuth 2.0 Authorization Code flow:

  1. Redirect user to authorization_url
  2. Google redirects back with ?code=...
  3. Exchange code for access_token + refresh_token
  4. When token expires (~1 hour), use refresh_token to get a new pair

Reference: https://developers.google.com/youtube/v3/guides/authentication

Defined Under Namespace

Classes: TokenRefreshError

Constant Summary collapse

AUTHORIZE_URL =
'https://accounts.google.com/o/oauth2/v2/auth'
TOKEN_URL =
'https://oauth2.googleapis.com/token'
PROVIDER =
'youtube'
SCOPES =

captions.list / captions.insert / captions.delete only accept youtube.force-ssl or
youtubepartner per API docs — the broader youtube scope is not sufficient for those endpoints.
After changing scopes, users must disconnect and complete OAuth again so the refresh token includes them.

[
  'https://www.googleapis.com/auth/youtube.force-ssl',
  'https://www.googleapis.com/auth/youtube',
  'https://www.googleapis.com/auth/youtube.upload',
  'https://www.googleapis.com/auth/youtube.readonly'
].freeze

Instance Method Summary collapse

Constructor Details

#initialize(account: nil) ⇒ OauthService

Returns a new instance of OauthService.



31
32
33
34
35
# File 'app/services/youtube/oauth_service.rb', line 31

def initialize(account: nil)
  @account       = 
  @client_id     = Heatwave::Configuration.fetch(:omniauth, :google_oauth2_id)
  @client_secret = Heatwave::Configuration.fetch(:omniauth, :google_oauth2_secret)
end

Instance Method Details

#access_token!Object

Get a valid access token, refreshing if expired.

Raises:



89
90
91
92
93
94
95
# File 'app/services/youtube/oauth_service.rb', line 89

def access_token!
  credential = OauthCredential.for(PROVIDER, account: @account)
  raise TokenRefreshError, 'No YouTube credential found' unless credential

  refresh! if credential.token_expired?
  credential.reload.access_token
end

#authorization_url(state: nil) ⇒ Object

URL to redirect the user to for authorization.
access_type=offline ensures we get a refresh_token.
prompt=consent forces re-consent to always get a refresh_token.



40
41
42
43
44
45
46
47
48
49
50
51
# File 'app/services/youtube/oauth_service.rb', line 40

def authorization_url(state: nil)
  params = {
    response_type: 'code',
    client_id: @client_id,
    redirect_uri: redirect_uri,
    scope: SCOPES.join(' '),
    access_type: 'offline',
    prompt: 'consent'
  }
  params[:state] = state if state.present?
  "#{AUTHORIZE_URL}?#{params.to_query}"
end

#connected?Boolean

Returns:

  • (Boolean)


97
98
99
# File 'app/services/youtube/oauth_service.rb', line 97

def connected?
  OauthCredential.for(PROVIDER, account: @account).present?
end

#connection_statusObject

Lightweight status check (DB only, no HTTP calls). Returns a hash:
{ connected: true/false, healthy: true/false, credential: OauthCredential or nil }

healthy = connected AND token not expired (refresh worker keeps it fresh)



105
106
107
108
109
110
# File 'app/services/youtube/oauth_service.rb', line 105

def connection_status
  credential = OauthCredential.for(PROVIDER, account: @account)
  return { connected: false, healthy: false, credential: nil } unless credential

  { connected: true, healthy: credential.token_fresh?, credential: credential }
end

#disconnect!Object



112
113
114
# File 'app/services/youtube/oauth_service.rb', line 112

def disconnect!
  OauthCredential.where(provider: PROVIDER, account_id: @account&.id).destroy_all
end

#exchange_code!(code) ⇒ Object

Exchange the authorization code for tokens and persist them.



54
55
56
57
58
59
60
61
62
63
64
# File 'app/services/youtube/oauth_service.rb', line 54

def exchange_code!(code)
  response = token_request(
    grant_type: 'authorization_code',
    code: code,
    redirect_uri: redirect_uri,
    client_id: @client_id,
    client_secret: @client_secret
  )

  persist_tokens!(response)
end

#healthy?Boolean

Full health check — attempts token refresh if expired. Use on admin pages,
not on every video page load (makes HTTP call to Google).

Returns:

  • (Boolean)


118
119
120
121
122
123
124
125
126
# File 'app/services/youtube/oauth_service.rb', line 118

def healthy?
  credential = OauthCredential.for(PROVIDER, account: @account)
  return false unless credential&.access_token.present?

  refresh! if credential.token_expired?
  true
rescue TokenRefreshError, StandardError
  false
end

#refresh!Object

Refresh the stored access token.

Raises:



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'app/services/youtube/oauth_service.rb', line 67

def refresh!
  credential = OauthCredential.for(PROVIDER, account: @account)
  raise TokenRefreshError, 'No YouTube credential found' unless credential
  raise TokenRefreshError, 'No refresh token available' if credential.refresh_token.blank?

  response = token_request(
    grant_type: 'refresh_token',
    refresh_token: credential.refresh_token,
    client_id: @client_id,
    client_secret: @client_secret
  )

  attrs = {
    access_token: response['access_token'],
    expires_at: response['expires_in'] ? response['expires_in'].to_i.seconds.from_now : nil
  }
  attrs[:refresh_token] = response['refresh_token'] if response['refresh_token'].present?
  credential.update!(attrs)
  credential
end