feat(web): web-initiated YouTube OAuth connect flow (ADR-006)
Add GET /oauth/youtube/connect and /oauth/youtube/callback, mounted inside the login + registration guard so CurrentUserID is always set and every connection binds to the authenticated tapir user. - connect: generate a per-user CSRF state (single-use, short TTL, bound to the user), redirect to Google consent with access_type=offline and prompt=consent so a refresh token comes back. - callback: verify the state belongs to this user, exchange the code via the existing auth.Exchange, persist the refresh token under a PER-USER ref (web.YouTubeTokenRef = "youtube/<userID>/refresh_token") so tenants never collide, then UpsertConnection (provider=youtube, status=active). Any failure renders a clean error page and leaves no half-written state. Reuses auth.Exchange and adds auth.AuthCodeURL (offline + consent) rather than the CLI's listener/terminal flow (ADR-006: web flow, not CLI). The ConnectHandler depends on a narrow web.Connections port, not the concrete store. Wired in cmdServe only when YT client credentials are present; TAPIR_YT_CONNECT_REDIRECT_URL configures the callback URL. Per-user token-ref scheme documented in docs/homelab-integration.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,10 @@ type App struct {
|
||||
Identity Identity
|
||||
Auth Auth
|
||||
Log *slog.Logger
|
||||
// Connect runs the web-initiated YouTube OAuth connect flow. Optional: when
|
||||
// nil (e.g. dev without YouTube client credentials), the /oauth/youtube/*
|
||||
// routes are not mounted.
|
||||
Connect *ConnectHandler
|
||||
}
|
||||
|
||||
func (a *App) logger() *slog.Logger {
|
||||
@@ -58,6 +62,13 @@ func (a *App) Router() http.Handler {
|
||||
app.HandleFunc("GET /register", a.handleRegisterForm)
|
||||
app.HandleFunc("POST /register", a.handleRegister)
|
||||
|
||||
// Web-initiated YouTube connect (ADR-006). Gated like every app route, so
|
||||
// CurrentUserID is set and the connection binds to the authenticated user.
|
||||
if a.Connect != nil {
|
||||
app.HandleFunc("GET /oauth/youtube/connect", a.Connect.handleConnect)
|
||||
app.HandleFunc("GET /oauth/youtube/callback", a.Connect.handleCallback)
|
||||
}
|
||||
|
||||
// Two layers: Auth.Middleware requires a Dex session (you must be logged in);
|
||||
// registrationGate requires a tapir user (else → /register) and stashes the
|
||||
// resolved user_id. /register lives inside the auth guard but is exempt from
|
||||
|
||||
Reference in New Issue
Block a user