···469469 Templates: templates,
470470 }).Methods("GET")
471471472472- router.Handle("/auth/oauth/login", &uihandlers.LoginSubmitHandler{
473473- Refresher: refresher,
474474- Directory: oauthApp.Directory(),
475475- SessionStore: sessionStore,
476476- }).Methods("POST")
472472+ router.Handle("/auth/oauth/login", &uihandlers.LoginSubmitHandler{}).Methods("POST")
477473478474 // Public routes (with optional auth for navbar)
479475 // SECURITY: Public pages use read-only DB
-95
pkg/appview/handlers/auth.go
···11package handlers
2233import (
44- "fmt"
54 "html/template"
65 "net/http"
77- "time"
88-99- "atcr.io/pkg/auth/oauth"
1010- "github.com/bluesky-social/indigo/atproto/identity"
1111- "github.com/bluesky-social/indigo/atproto/syntax"
126)
137148// LoginHandler shows the OAuth login form
···38323933// LoginSubmitHandler processes the login form submission
4034type LoginSubmitHandler struct {
4141- Refresher *oauth.Refresher
4242- Directory identity.Directory
4343- SessionStore UISessionStore
4444-}
4545-4646-// UISessionStore is the interface for UI session management
4747-type UISessionStore interface {
4848- CreateWithOAuth(did, handle, pdsEndpoint, oauthSessionID string, duration time.Duration) (string, error)
4935}
50365137func (h *LoginSubmitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
···6551 return
6652 }
67536868- // Attempt silent login first
6969- if h.Refresher != nil && h.Directory != nil && h.SessionStore != nil {
7070- // Parse handle
7171- handleSyntax, err := syntax.ParseHandle(handle)
7272- if err == nil {
7373- // Resolve handle to identity (DID + PDS endpoint)
7474- ident, err := h.Directory.LookupHandle(r.Context(), handleSyntax)
7575- if err == nil {
7676- did := ident.DID.String()
7777-7878- // Try to get existing OAuth session
7979- session, err := h.Refresher.GetSession(r.Context(), did)
8080- if err == nil {
8181- // Check if the session has all required scopes
8282- requiredScopes := oauth.GetDefaultScopes()
8383- sessionScopes := session.Data.Scopes
8484-8585- if !hasAllScopes(sessionScopes, requiredScopes) {
8686- fmt.Printf("DEBUG [auth]: Session scopes mismatch for %s. Required: %v, Have: %v. Forcing re-auth.\n",
8787- handle, requiredScopes, sessionScopes)
8888- } else {
8989- // Found valid OAuth session with all required scopes! Create UI session silently
9090- fmt.Printf("DEBUG [auth]: Silent login successful for %s (DID: %s)\n", handle, did)
9191-9292- // Get PDS endpoint from identity
9393- pdsEndpoint := ident.PDSEndpoint()
9494-9595- // Get OAuth sessionID from refresher
9696- sessionID := h.Refresher.GetSessionID(did)
9797-9898- uiSessionID, err := h.SessionStore.CreateWithOAuth(did, handle, pdsEndpoint, sessionID, 30*24*time.Hour)
9999- if err == nil {
100100- // Set session cookie
101101- http.SetCookie(w, &http.Cookie{
102102- Name: "atcr_session",
103103- Value: uiSessionID,
104104- Path: "/",
105105- MaxAge: 30 * 86400, // 30 days
106106- HttpOnly: true,
107107- Secure: true,
108108- SameSite: http.SameSiteLaxMode,
109109- })
110110-111111- // Redirect to return URL
112112- fmt.Printf("DEBUG [auth]: Silent login complete, redirecting to %s\n", returnTo)
113113- http.Redirect(w, r, returnTo, http.StatusFound)
114114- return
115115- }
116116-117117- fmt.Printf("WARNING [auth]: Failed to create UI session during silent login: %v\n", err)
118118- }
119119- } else {
120120- fmt.Printf("DEBUG [auth]: No valid OAuth session found for %s: %v\n", handle, err)
121121- }
122122- } else {
123123- fmt.Printf("DEBUG [auth]: Failed to resolve handle %s: %v\n", handle, err)
124124- }
125125- } else {
126126- fmt.Printf("DEBUG [auth]: Failed to parse handle %s: %v\n", handle, err)
127127- }
128128- }
129129-130130- // Silent login failed or not configured - proceed with full OAuth flow
131131- fmt.Printf("DEBUG [auth]: Proceeding with full OAuth flow for %s\n", handle)
132132-13354 // Store return_to in cookie so callback can use it
13455 http.SetCookie(w, &http.Cookie{
13556 Name: "oauth_return_to",
···14465 // Redirect to OAuth authorize with handle
14566 http.Redirect(w, r, "/auth/oauth/authorize?handle="+handle, http.StatusFound)
14667}
147147-148148-// hasAllScopes checks if grantedScopes contains all requiredScopes
149149-func hasAllScopes(grantedScopes, requiredScopes []string) bool {
150150- grantedSet := make(map[string]bool)
151151- for _, scope := range grantedScopes {
152152- grantedSet[scope] = true
153153- }
154154-155155- for _, required := range requiredScopes {
156156- if !grantedSet[required] {
157157- return false
158158- }
159159- }
160160-161161- return true
162162-}
+4-9
pkg/atproto/profile.go
···11111212// EnsureProfile checks if a user's profile exists and creates it if needed
1313// This should be called during authentication (OAuth exchange or token service)
1414-// If defaultHoldEndpoint is provided and profile doesn't exist, creates profile with that default
1414+// If defaultHoldEndpoint is provided, creates profile with that default (or empty if not provided)
1515func EnsureProfile(ctx context.Context, client *Client, defaultHoldEndpoint string) error {
1616 // Check if profile already exists
1717 profile, err := client.GetRecord(ctx, SailorProfileCollection, ProfileRKey)
···2020 return nil
2121 }
22222323- // Profile doesn't exist
2424- // Only create if we have a default hold endpoint to set
2525- if defaultHoldEndpoint == "" {
2626- // No default configured, don't create empty profile
2727- return nil
2828- }
2929-3030- // Create new profile with default hold
2323+ // Profile doesn't exist - create it
2424+ // defaultHoldEndpoint can be empty string (user will need to configure it later)
3125 newProfile := NewSailorProfileRecord(defaultHoldEndpoint)
32263327 _, err = client.PutRecord(ctx, SailorProfileCollection, ProfileRKey, newProfile)
···3529 return fmt.Errorf("failed to create sailor profile: %w", err)
3630 }
37313232+ fmt.Printf("DEBUG [profile]: Created sailor profile with defaultHold=%s\n", defaultHoldEndpoint)
3833 return nil
3934}
4035
+5-2
pkg/auth/oauth/server.go
···267267 // Create authenticated atproto client using the indigo session's API client
268268 client := atproto.NewClientWithIndigoClient(pdsEndpoint, did, session.APIClient())
269269270270- // Ensure sailor profile exists (creates with default hold on first login)
270270+ // Ensure sailor profile exists (creates with default hold if configured, or empty profile if not)
271271+ fmt.Printf("DEBUG [oauth/server]: Ensuring profile exists for %s (defaultHold=%s)\n", did, s.defaultHoldEndpoint)
271272 if err := atproto.EnsureProfile(ctx, client, s.defaultHoldEndpoint); err != nil {
272273 fmt.Printf("WARNING [oauth/server]: Failed to ensure profile for %s: %v\n", did, err)
273273- // Continue anyway - profile creation is not critical
274274+ // Continue anyway - profile creation is not critical for avatar fetch
275275+ } else {
276276+ fmt.Printf("DEBUG [oauth/server]: Profile ensured for %s\n", did)
274277 }
275278276279 // Fetch user's profile record from PDS (contains blob references)