Skip to content

Commit 0505dc8

Browse files
committed
Add OAuth connect/disconnect UI for services
Update token management page to show OAuth connect buttons for services that support OAuth authentication. Distinguish between OAuth and manual token services, showing appropriate UI elements for each type.
1 parent 928dbf7 commit 0505dc8

File tree

4 files changed

+96
-34
lines changed

4 files changed

+96
-34
lines changed

internal/config/unmarshal_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -272,9 +272,9 @@ func TestOAuthAuthConfig_UnmarshalJSON(t *testing.T) {
272272
assert.Equal(t, []string{"example.com"}, config.AllowedDomains)
273273
assert.Equal(t, []string{"https://claude.ai", "https://example.com"}, config.AllowedOrigins)
274274
assert.Equal(t, "test-client-id", config.GoogleClientID)
275-
assert.Equal(t, "test-secret-value", config.GoogleClientSecret)
276-
assert.Equal(t, "this-is-a-very-long-jwt-secret-key", config.JWTSecret)
277-
assert.Equal(t, "exactly-32-bytes-long-encryptkey", config.EncryptionKey)
275+
assert.Equal(t, Secret("test-secret-value"), config.GoogleClientSecret)
276+
assert.Equal(t, Secret("this-is-a-very-long-jwt-secret-key"), config.JWTSecret)
277+
assert.Equal(t, Secret("exactly-32-bytes-long-encryptkey"), config.EncryptionKey)
278278
}
279279

280280
func TestOAuthAuthConfig_ValidationErrors(t *testing.T) {

internal/server/templates.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@ type TokenPageData struct {
2525

2626
// ServiceTokenData represents a single service in the token page
2727
type ServiceTokenData struct {
28-
Name string
29-
DisplayName string
30-
Instructions string
31-
HelpURL string
32-
TokenFormat string
33-
HasToken bool
34-
RequiresToken bool
35-
AuthType string // "oauth", "bearer", or "none"
28+
Name string
29+
DisplayName string
30+
Instructions string
31+
HelpURL string
32+
TokenFormat string
33+
HasToken bool
34+
RequiresToken bool
35+
AuthType string // "oauth", "bearer", or "none"
36+
SupportsOAuth bool // Whether this service supports OAuth authentication
37+
IsOAuthConnected bool // Whether the user has connected OAuth for this service
3638
}

internal/server/templates/tokens.html

Lines changed: 62 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,30 @@
143143
background-color: #c82333;
144144
}
145145

146+
button.oauth {
147+
background-color: #4285f4;
148+
color: white;
149+
}
150+
151+
button.oauth:hover {
152+
background-color: #357ae8;
153+
}
154+
155+
.oauth-status {
156+
display: flex;
157+
align-items: center;
158+
gap: 10px;
159+
margin-top: 15px;
160+
}
161+
162+
.oauth-connected {
163+
color: #2e7d32;
164+
font-size: 14px;
165+
display: flex;
166+
align-items: center;
167+
gap: 5px;
168+
}
169+
146170
.message {
147171
padding: 12px;
148172
margin-bottom: 20px;
@@ -206,25 +230,44 @@ <h2>{{.DisplayName}}</h2>
206230
{{end}}
207231
</div>
208232

209-
<form method="POST" action="/my/tokens/set">
210-
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
211-
<input type="hidden" name="service" value="{{.Name}}">
212-
<input type="password"
213-
name="token"
214-
placeholder="{{if .HasToken}}Enter new token to update{{else}}Enter your {{.DisplayName}} token{{end}}"
215-
{{if .TokenFormat}}pattern="{{.TokenFormat}}" title="Token must match pattern: {{.TokenFormat}}"{{end}}
216-
required>
217-
<button type="submit" class="primary">
218-
{{if .HasToken}}Update Token{{else}}Save Token{{end}}
219-
</button>
220-
</form>
221-
222-
{{if .HasToken}}
223-
<form method="POST" action="/my/tokens/delete" class="delete-form">
224-
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
225-
<input type="hidden" name="service" value="{{.Name}}">
226-
<button type="submit" class="danger">Remove Token</button>
227-
</form>
233+
{{if .SupportsOAuth}}
234+
{{if .IsOAuthConnected}}
235+
<div class="oauth-status">
236+
<span class="oauth-connected">✓ Connected via OAuth</span>
237+
<form method="POST" action="/oauth/disconnect" style="display: inline;">
238+
<input type="hidden" name="service" value="{{.Name}}">
239+
<button type="submit" class="danger">Disconnect {{.DisplayName}}</button>
240+
</form>
241+
</div>
242+
{{else}}
243+
<div class="oauth-status">
244+
<form method="GET" action="/oauth/connect" style="display: inline;">
245+
<input type="hidden" name="service" value="{{.Name}}">
246+
<button type="submit" class="oauth">Connect with {{.DisplayName}}</button>
247+
</form>
248+
</div>
249+
{{end}}
250+
{{else}}
251+
<form method="POST" action="/my/tokens/set">
252+
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
253+
<input type="hidden" name="service" value="{{.Name}}">
254+
<input type="password"
255+
name="token"
256+
placeholder="{{if .HasToken}}Enter new token to update{{else}}Enter your {{.DisplayName}} token{{end}}"
257+
{{if .TokenFormat}}pattern="{{.TokenFormat}}" title="Token must match pattern: {{.TokenFormat}}"{{end}}
258+
required>
259+
<button type="submit" class="primary">
260+
{{if .HasToken}}Update Token{{else}}Save Token{{end}}
261+
</button>
262+
</form>
263+
264+
{{if .HasToken}}
265+
<form method="POST" action="/my/tokens/delete" class="delete-form">
266+
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
267+
<input type="hidden" name="service" value="{{.Name}}">
268+
<button type="submit" class="danger">Remove Token</button>
269+
</form>
270+
{{end}}
228271
{{end}}
229272
{{else}}
230273
<div class="instructions">

internal/server/token_handlers.go

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,17 +86,34 @@ func (h *TokenHandlers) ListTokensHandler(w http.ResponseWriter, r *http.Request
8686
if serverConfig.UserAuthentication.DisplayName != "" {
8787
service.DisplayName = serverConfig.UserAuthentication.DisplayName
8888
}
89-
if serverConfig.UserAuthentication.Type == config.UserAuthTypeManual {
89+
90+
// Check if this service supports OAuth
91+
if serverConfig.UserAuthentication.Type == config.UserAuthTypeOAuth {
92+
service.SupportsOAuth = true
93+
service.Instructions = fmt.Sprintf("Connect your %s account via OAuth", service.DisplayName)
94+
95+
// Check if OAuth is already connected
96+
storedToken, err := h.tokenStore.GetUserToken(r.Context(), userEmail, name)
97+
if err == nil && storedToken.Type == storage.TokenTypeOAuth {
98+
service.IsOAuthConnected = true
99+
service.HasToken = true
100+
}
101+
} else if serverConfig.UserAuthentication.Type == config.UserAuthTypeManual {
90102
if serverConfig.UserAuthentication.Instructions != "" {
91103
service.Instructions = serverConfig.UserAuthentication.Instructions
92104
}
93105
service.HelpURL = serverConfig.UserAuthentication.HelpURL
106+
107+
// Check if manual token exists
108+
_, err := h.tokenStore.GetUserToken(r.Context(), userEmail, name)
109+
service.HasToken = err == nil
94110
}
95111
service.TokenFormat = serverConfig.UserAuthentication.TokenFormat
112+
} else {
113+
// No UserAuthentication config means manual token
114+
_, err := h.tokenStore.GetUserToken(r.Context(), userEmail, name)
115+
service.HasToken = err == nil
96116
}
97-
98-
_, err := h.tokenStore.GetUserToken(r.Context(), userEmail, name)
99-
service.HasToken = err == nil
100117
} else {
101118
// Determine if it's OAuth authenticated or uses bearer tokens
102119
if h.oauthEnabled {

0 commit comments

Comments
 (0)