Require a Google client
GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET must be set, so the app no
longer has a path for a server without one. The Fly demos set
placeholders, since anyone who opens them shares the one demo user.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LG4iASZnHb1yEM1JAASiWn
diff --git a/AGENTS.md b/AGENTS.md
index 52ebd61..b86ff2d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -85,11 +85,11 @@ fly.toml — Fly config for the main demo and PR previews
- `DATABASE_URL` — SQLite path (default: `:memory:`)
- `SESSION_SECRET` — session signing key (auto-generated if unset)
-- `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` — a Google OAuth web client
- with the YouTube Data API enabled, for connecting Google. Its redirect
- URI is `<app URL>/settings/google/callback`. Without them YouTube lists
- are not offered. A consent screen left in Testing mode expires refresh
- tokens after 7 days.
+- `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` (required) — a Google OAuth
+ web client with the YouTube Data API enabled, for connecting Google.
+ Its redirect URI is `<app URL>/settings/google/callback`. A consent
+ screen left in Testing mode expires refresh tokens after 7 days. The
+ Fly demos set placeholders in `fly.toml`, since anyone can open them.
Tests set `DATABASE_URL=:memory:` and a dummy Google client in
`test/test_helper.rb`, and swap `RankKing::Google.transport` so no
diff --git a/fly.toml b/fly.toml
index f8e8383..1f9ced0 100644
--- a/fly.toml
+++ b/fly.toml
@@ -16,6 +16,14 @@ primary_region = 'sjc'
[build]
+# The app requires a Google client. The demos get a placeholder rather
+# than the real one: anyone who opens them is the one demo user, so a
+# Google account connected there would be everyone's. Connect Google on
+# a demo fails at Google's consent screen.
+[env]
+ GOOGLE_CLIENT_ID = 'demo'
+ GOOGLE_CLIENT_SECRET = 'demo'
+
[processes]
app = 'rackup -o 0.0.0.0 demo.ru'
diff --git a/lib/rank_king/config.rb b/lib/rank_king/config.rb
index 89124f1..ffaae54 100644
--- a/lib/rank_king/config.rb
+++ b/lib/rank_king/config.rb
@@ -22,10 +22,9 @@ module RankKing
new(
database_url: env.fetch("DATABASE_URL", ":memory:"),
session_secret: env.fetch("SESSION_SECRET") { SecureRandom.hex(64) },
- # A Google OAuth client, for connecting YouTube. Without one the
- # settings page says so and YouTube lists cannot be made.
- google_client_id: env["GOOGLE_CLIENT_ID"],
- google_client_secret: env["GOOGLE_CLIENT_SECRET"],
+ # A Google OAuth client, for connecting YouTube.
+ google_client_id: env.fetch("GOOGLE_CLIENT_ID"),
+ google_client_secret: env.fetch("GOOGLE_CLIENT_SECRET"),
)
end
end
diff --git a/lib/rank_king/google.rb b/lib/rank_king/google.rb
index e3dcb4b..eee06bd 100644
--- a/lib/rank_king/google.rb
+++ b/lib/rank_king/google.rb
@@ -34,9 +34,6 @@ module RankKing
@transport ||= ->(method, url, params, access_token) { request(method, url, params, access_token) }
end
- #: () -> bool
- def self.configured? = !!(CONFIG.google_client_id && CONFIG.google_client_secret)
-
# Where to send the user to grant read access to their YouTube
# account. prompt=consent makes Google send a refresh token every time.
#: (redirect_uri: String, state: String) -> String
@@ -115,7 +112,7 @@ module RankKing
#: () -> Hash[Symbol, String]
def self.client
- { client_id: CONFIG.google_client_id.to_s, client_secret: CONFIG.google_client_secret.to_s }
+ { client_id: CONFIG.google_client_id, client_secret: CONFIG.google_client_secret }
end
#: (Symbol method, String url, Hash[Symbol, String] params, String? access_token) -> Hash[String, untyped]
diff --git a/lib/rank_king/sources/youtube.rb b/lib/rank_king/sources/youtube.rb
index c29ce4b..3cbaaf2 100644
--- a/lib/rank_king/sources/youtube.rb
+++ b/lib/rank_king/sources/youtube.rb
@@ -28,9 +28,9 @@ module RankKing
def self.fields(user)
account = user.google_account
playlists = account && Google.playlists(Google.access_token(account.refresh_token))
- Views::Sources::YouTubeFields.new(playlists:, configured: Google.configured?)
+ Views::Sources::YouTubeFields.new(playlists:)
rescue Google::Error => e
- Views::Sources::YouTubeFields.new(playlists: nil, configured: true, error: e.message)
+ Views::Sources::YouTubeFields.new(playlists: nil, error: e.message)
end
# Follows the playlist chosen on the new-list form and fetches it.
diff --git a/lib/rank_king/views/settings.rb b/lib/rank_king/views/settings.rb
index f0835f6..a5c63e8 100644
--- a/lib/rank_king/views/settings.rb
+++ b/lib/rank_king/views/settings.rb
@@ -9,9 +9,8 @@ module RankKing
# The user's own settings, as against a list's. A future setting is a
# new section here.
class Settings < Phlex::HTML
- def initialize(user:, google_configured:, csrf:)
+ def initialize(user:, csrf:)
@user = user
- @google_configured = google_configured
@csrf = csrf
end
@@ -23,11 +22,9 @@ module RankKing
h2(class: "type-label") { "Google" }
if @user.google_account
render_connected
- elsif @google_configured
+ else
p { "Connect Google to make lists from your YouTube playlists, private ones included." }
a(href: "/settings/google/connect", class: "btn", data_variant: "primary") { "Connect Google" }
- else
- p { "This server has no Google client set up. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET to connect Google." }
end
end
end
diff --git a/lib/rank_king/views/sources/youtube.rb b/lib/rank_king/views/sources/youtube.rb
index 43128a2..675ced5 100644
--- a/lib/rank_king/views/sources/youtube.rb
+++ b/lib/rank_king/views/sources/youtube.rb
@@ -9,9 +9,8 @@ module RankKing
# of the user's playlists, once they have connected Google. playlists
# is nil until then.
class YouTubeFields < Phlex::HTML
- def initialize(playlists:, configured:, error: nil)
+ def initialize(playlists:, error: nil)
@playlists = playlists
- @configured = configured
@error = error
end
@@ -30,14 +29,12 @@ module RankKing
end
small { "Private playlists are listed too. YouTube does not share Watch Later." }
end
- elsif @configured
+ else
p do
plain "Connect Google in "
a(href: "/settings") { "your settings" }
plain " to make a list from a YouTube playlist."
end
- else
- p { "This server has no Google client set up, so YouTube lists cannot be made yet." }
end
end
end
diff --git a/lib/rank_king/web.rb b/lib/rank_king/web.rb
index 982fd9b..7027849 100644
--- a/lib/rank_king/web.rb
+++ b/lib/rank_king/web.rb
@@ -41,7 +41,7 @@ module RankKing
r.on "settings" do
r.get true do
- Views::Settings.new(user:, google_configured: Google.configured?, csrf: method(:csrf_token)).call
+ Views::Settings.new(user:, csrf: method(:csrf_token)).call
end
# Connecting Google is the OAuth dance: off to Google with a state
@@ -50,8 +50,6 @@ module RankKing
redirect_uri = "#{r.base_url}/settings/google/callback"
r.get "connect" do
- r.halt 404 unless Google.configured?
-
state = SecureRandom.hex(16)
session["google_state"] = state
r.redirect Google.authorize_url(redirect_uri:, state:)
diff --git a/sig/rank_king/config.rbs b/sig/rank_king/config.rbs
index 1625670..672805c 100644
--- a/sig/rank_king/config.rbs
+++ b/sig/rank_king/config.rbs
@@ -5,9 +5,9 @@ module RankKing
class Config
attr_reader database_url: String
attr_reader session_secret: String
- attr_reader google_client_id: String?
- attr_reader google_client_secret: String?
+ attr_reader google_client_id: String
+ attr_reader google_client_secret: String
- def self.new: (database_url: String, session_secret: String, google_client_id: String?, google_client_secret: String?) -> instance
+ def self.new: (database_url: String, session_secret: String, google_client_id: String, google_client_secret: String) -> instance
end
end
diff --git a/sig/rank_king/views.rbs b/sig/rank_king/views.rbs
index 8513dba..d8f44f7 100644
--- a/sig/rank_king/views.rbs
+++ b/sig/rank_king/views.rbs
@@ -9,7 +9,7 @@ module RankKing
end
class Settings
- def initialize: (user: User, google_configured: bool, csrf: Method) -> void
+ def initialize: (user: User, csrf: Method) -> void
def call: () -> String
end
@@ -30,7 +30,7 @@ module RankKing
end
class YouTubeFields
- def initialize: (playlists: Array[[String, String]]?, configured: bool, ?error: String?) -> void
+ def initialize: (playlists: Array[[String, String]]?, ?error: String?) -> void
def call: () -> String
end
end
diff --git a/test/test_youtube.rb b/test/test_youtube.rb
index 33799bd..f3a6810 100644
--- a/test/test_youtube.rb
+++ b/test/test_youtube.rb
@@ -87,17 +87,6 @@ class TestYouTube < Minitest::Test
assert_includes last_response.body, %(href="/settings/google/connect")
end
- def test_settings_says_when_google_is_not_set_up
- unconfigured do
- get "/settings"
- refute_includes last_response.body, "/settings/google/connect"
- assert_includes last_response.body, "GOOGLE_CLIENT_ID"
-
- get "/settings/google/connect"
- assert_equal 404, last_response.status
- end
- end
-
def test_connecting_google_keeps_a_refresh_token
get "/settings/google/connect"
assert last_response.redirect?
@@ -282,17 +271,6 @@ class TestYouTube < Minitest::Test
def user = RankKing::User.first(login: LOGIN)
- # As if the server had no Google client.
- def unconfigured
- google = RankKing::Google.singleton_class
- google.alias_method :__configured?, :configured?
- google.define_method(:configured?) { false }
- yield
- ensure
- google.alias_method :configured?, :__configured?
- google.remove_method :__configured?
- end
-
def connect_google
get "/settings/google/connect"
state = URI.decode_www_form(URI(last_response.location).query).to_h["state"]