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