Show connected OAuth provider on the settings page

Settings previously had no signal of how an account signs in. Records which provider (GitHub/Google) a brokered OAuth sign-in used on the User record, and shows it under a new "Connected accounts" card; falls back to a "no OAuth connection" message for email/password accounts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Seto Elkahfi committed Jul 5, 2026 at 10:40 UTC a0cb76ee96230acfd6693d65dfc3acf350339c2a
7 files changed +63 -5
app/controllers/oauth/github_controller.rb
+4
@@ -9,6 +9,10 @@ module Oauth
9 "GitHub"
10 end
11
12 + def provider_key
13 + "github"
14 + end
15 +
16 def authorize_url
17 SmbcloudAuthService.github_authorize_url(redirect_uri: github_auth_callback_url)
18 end
app/controllers/oauth/google_controller.rb
+4
@@ -9,6 +9,10 @@ module Oauth
9 "Google"
10 end
11
12 + def provider_key
13 + "google"
14 + end
15 +
16 def authorize_url
17 SmbcloudAuthService.google_authorize_url(redirect_uri: google_auth_callback_url)
18 end
app/controllers/oauth/provider_controller.rb
+6 -1
@@ -10,6 +10,7 @@ module Oauth
10 #
11 # Subclasses supply the provider specifics:
12 # provider_label — human name used in flash/log messages ("GitHub")
13 + # provider_key — lowercase key stored on User#oauth_provider ("github")
14 # authorize_url — the smbCloud authorize URL carrying our callback
15 class ProviderController < ApplicationController
16 before_action :redirect_if_signed_in
@@ -32,7 +33,11 @@ module Oauth
33 end
34
35 profile = SmbcloudAuthService.me(access_token: access_token)
35 - user = User.find_or_create_from_smbcloud(profile.merge(access_token: access_token))
36 + user = User.find_or_create_from_smbcloud(
37 + profile,
38 + access_token: access_token,
39 + oauth_provider: provider_key
40 + )
41
42 reset_session
43 session[:user_id] = user.id
app/models/user.rb
+16 -3
@@ -84,6 +84,14 @@ class User < ApplicationRecord
84 "https://ui-avatars.com/api/?name=#{encoded_name}&background=0057B8&color=fff&size=128"
85 end
86
87 + OAUTH_PROVIDER_LABELS = { "github" => "GitHub", "google" => "Google" }.freeze
88 +
89 + # Human label for the OAuth provider this account last signed in with, or
90 + # nil if it has only ever used email/password sign-in.
91 + def oauth_provider_label
92 + OAUTH_PROVIDER_LABELS[oauth_provider]
93 + end
94 +
95 # ---------------------------------------------------------------------------
96 # smbCloud Auth integration
97 # ---------------------------------------------------------------------------
@@ -92,11 +100,15 @@ class User < ApplicationRecord
100 # 1. SmbcloudAuthService.me(access_token:)
101 # → { id: Integer, email: String, created_at: String, updated_at: String }
102 # 2. The access_token passed through from login.
103 + # 3. `oauth_provider`, when this upsert followed a brokered "Continue with
104 + # <provider>" sign-in (e.g. "github", "google") — nil for plain
105 + # email/password sign-in. Recorded so Settings can show which OAuth
106 + # connection, if any, the account last used.
107 #
108 # Accepts either string or symbol keys.
109 #
110 # Returns the persisted User. Raises ActiveRecord::RecordInvalid on failure.
99 - def self.find_or_create_from_smbcloud(userinfo, access_token: nil)
111 + def self.find_or_create_from_smbcloud(userinfo, access_token: nil, oauth_provider: nil)
112 smbcloud_id = Integer(userinfo[:id] || userinfo["id"])
113 email = (userinfo[:email] || userinfo["email"]).to_s.strip.downcase
114
@@ -118,8 +130,9 @@ class User < ApplicationRecord
130 end
131
132 # Always sync email and token in case they changed on the auth server.
121 - user.email = email if email.present?
122 - user.access_token = access_token if access_token.present?
133 + user.email = email if email.present?
134 + user.access_token = access_token if access_token.present?
135 + user.oauth_provider = oauth_provider if oauth_provider.present?
136
137 # Derive a username only for new records.
138 user.username = generate_username_from_email(email) if user.new_record?
app/views/users/settings.html.erb
+21
@@ -32,6 +32,27 @@
32 </div>
33 </div>
34
35 + <div class="card mt-6">
36 + <div class="px-6 py-4 border-b border-surface-600">
37 + <h2 class="text-sm font-semibold text-gray-100">Connected accounts</h2>
38 + </div>
39 + <div class="px-6 py-6">
40 + <% if @user.oauth_provider_label %>
41 + <div class="flex items-center justify-between">
42 + <div class="flex items-center gap-3">
43 + <span class="inline-flex h-2 w-2 rounded-full bg-green-500"></span>
44 + <p class="text-sm text-gray-300">
45 + Signed in with <span class="font-medium text-gray-100"><%= @user.oauth_provider_label %></span>
46 + </p>
47 + </div>
48 + <span class="text-xs text-gray-500">Connected</span>
49 + </div>
50 + <% else %>
51 + <p class="text-sm text-gray-400">No OAuth connection. You're signing in with email and password.</p>
52 + <% end %>
53 + </div>
54 + </div>
55 +
56 <div class="card mt-6">
57 <div class="px-6 py-4 border-b border-surface-600">
58 <h2 class="text-sm font-semibold text-gray-100">Billing</h2>
db/migrate/20250101000012_add_oauth_provider_to_users.rb new
+10
@@ -0,0 +1,10 @@
1 +# frozen_string_literal: true
2 +
3 +# Records which OAuth provider (if any) a user last signed in with, so
4 +# Settings can show "Connected via GitHub/Google" for accounts that used the
5 +# brokered social sign-in flow. Left blank for email/password-only accounts.
6 +class AddOauthProviderToUsers < ActiveRecord::Migration[8.1]
7 + def change
8 + add_column :users, :oauth_provider, :string
9 + end
10 +end
db/schema.rb
+2 -1
@@ -10,7 +10,7 @@
10 #
11 # It's strongly recommended that you check this file into your version control system.
12
13 -ActiveRecord::Schema[8.1].define(version: 2025_01_01_000011) do
13 +ActiveRecord::Schema[8.1].define(version: 2025_01_01_000012) do
14 # These are extensions that must be enabled in order to support this database
15 enable_extension "pg_catalog.plpgsql"
16
@@ -146,6 +146,7 @@ ActiveRecord::Schema[8.1].define(version: 2025_01_01_000011) do
146 t.datetime "created_at", null: false
147 t.string "display_name"
148 t.string "email", null: false
149 + t.string "oauth_provider"
150 t.integer "smbcloud_id", null: false
151 t.datetime "updated_at", null: false
152 t.string "username", null: false