···45// User represents a user of the application
6type User struct {
7- ID int64 `json:"id"`
8- Username string `json:"username"`
9- Email string `json:"email"`
10- SpotifyID string `json:"spotify_id"`
11- AccessToken string `json:"-"` // Not exposed in JSON
12- RefreshToken string `json:"-"` // Not exposed in JSON
13- TokenExpiry time.Time `json:"-"` // Not exposed in JSON
14- CreatedAt time.Time `json:"created_at"`
15- UpdatedAt time.Time `json:"updated_at"`
16-}0000
···45// User represents a user of the application
6type User struct {
7+ ID int64
8+ Username string
9+ Email *string // Use pointer for nullable fields
10+ SpotifyID *string // Use pointer for nullable fields
11+ AccessToken *string // Spotify Access Token
12+ RefreshToken *string // Spotify Refresh Token
13+ TokenExpiry *time.Time // Spotify Token Expiry
14+ CreatedAt time.Time
15+ UpdatedAt time.Time
16+ ATProtoDID *string // ATProto DID
17+ ATProtoAccessToken *string // ATProto Access Token
18+ ATProtoRefreshToken *string // ATProto Refresh Token
19+ ATProtoTokenExpiry *time.Time // ATProto Token Expiry
20+}
···1+// Modify piper/oauth/oauth_manager.go
2package oauth
34import (
···10 "github.com/teal-fm/piper/session"
11)
1213+// manages multiple oauth client services
0000014type OAuthServiceManager struct {
15+ services map[string]AuthService // Changed from *OAuth2Service to AuthService interface
16 sessionManager *session.SessionManager
17 mu sync.RWMutex
18}
1920func NewOAuthServiceManager() *OAuthServiceManager {
21 return &OAuthServiceManager{
22+ services: make(map[string]AuthService), // Initialize the new map
23 sessionManager: session.NewSessionManager(),
24 }
25}
2627+// RegisterService registers any service that implements the AuthService interface.
28+func (m *OAuthServiceManager) RegisterService(name string, service AuthService) {
29 m.mu.Lock()
30 defer m.mu.Unlock()
31+ m.services[name] = service
32+ log.Printf("Registered auth service: %s", name)
33}
3435+// GetService retrieves a registered AuthService by name.
36+func (m *OAuthServiceManager) GetService(name string) (AuthService, bool) {
37 m.mu.RLock()
38 defer m.mu.RUnlock()
39+ service, exists := m.services[name]
40 return service, exists
41}
4243func (m *OAuthServiceManager) HandleLogin(serviceName string) http.HandlerFunc {
44 return func(w http.ResponseWriter, r *http.Request) {
45 m.mu.RLock()
46+ service, exists := m.services[serviceName]
47 m.mu.RUnlock()
4849+ if exists {
50+ service.HandleLogin(w, r) // Call interface method
51 return
52 }
5354+ log.Printf("Auth service '%s' not found for login request", serviceName)
55+ http.Error(w, fmt.Sprintf("Auth service '%s' not found", serviceName), http.StatusNotFound)
56 }
57}
5859+func (m *OAuthServiceManager) HandleCallback(serviceName string) http.HandlerFunc {
60 return func(w http.ResponseWriter, r *http.Request) {
61 m.mu.RLock()
62+ service, exists := m.services[serviceName]
63 m.mu.RUnlock()
6465+ log.Printf("Logging in with service %s", serviceName)
6667+ if !exists {
68+ log.Printf("Auth service '%s' not found for callback request", serviceName)
0069 http.Error(w, fmt.Sprintf("OAuth service '%s' not found", serviceName), http.StatusNotFound)
70 return
71 }
7273+ // Call the service's HandleCallback, which now returns the user ID
74+ userID, err := service.HandleCallback(w, r) // Call interface method
75+76+ if err != nil {
77+ log.Printf("Error handling callback for service '%s': %v", serviceName, err)
78+ http.Error(w, fmt.Sprintf("Error handling callback for service '%s'", serviceName), http.StatusInternalServerError)
79+ return
80+ }
81+82 if userID > 0 {
83 // Create session for the user
84 session := m.sessionManager.CreateSession(userID)
···86 // Set session cookie
87 m.sessionManager.SetSessionCookie(w, session)
8889+ log.Printf("Created session for user %d via service %s", userID, serviceName)
09091+ // Redirect to homepage after successful login and session creation
92+ http.Redirect(w, r, "/", http.StatusSeeOther)
93+ } else {
94+ log.Printf("Callback for service '%s' did not result in a valid user ID.", serviceName)
95+ // Optionally redirect to an error page or show an error message
96+ // For now, just redirecting home, but this might hide errors.
97+ // Consider adding error handling based on why userID might be 0.
98+ http.Redirect(w, r, "/", http.StatusSeeOther) // Or redirect to a login/error page
99+ }
100 }
101}
+24
oauth/service.go
···000000000000000000000000
···1+// Create piper/oauth/auth_service.go
2+package oauth
3+4+import (
5+ "net/http"
6+)
7+8+// AuthService defines the interface for different authentication services
9+// that can be managed by the OAuthServiceManager.
10+type AuthService interface {
11+ // HandleLogin initiates the login flow for the specific service.
12+ HandleLogin(w http.ResponseWriter, r *http.Request)
13+ // HandleCallback handles the callback from the authentication provider,
14+ // processes the response (e.g., exchanges code for token), finds or creates
15+ // the user in the local system, and returns the user ID.
16+ // Returns 0 if authentication failed or user could not be determined.
17+ HandleCallback(w http.ResponseWriter, r *http.Request) (int64, error)
18+}
19+20+type TokenReceiver interface {
21+ // SetAccessToken stores the access token for the user and returns the user ID.
22+ // If the user is already logged in, the current ID is provided.
23+ SetAccessToken(token string, currentId int64, hasSession bool) (int64, error)
24+}
+44-33
service/spotify/spotify.go
···30 }
31}
3233-// SetAccessToken is called from OAuth callback and now identifies the user
34-// SetAccessToken is called from OAuth callback and now identifies the user
35-func (s *SpotifyService) SetAccessToken(token string) int64 {
36 // Identify the user synchronously instead of in a goroutine
37- userID := s.identifyAndStoreUser(token)
38- return userID
000039}
4041-func (s *SpotifyService) identifyAndStoreUser(token string) int64 {
42 // Get Spotify user profile
43 userProfile, err := s.fetchSpotifyProfile(token)
44 if err != nil {
45 log.Printf("Error fetching Spotify profile: %v", err)
46- return 0
47 }
004849 // Check if user exists
50 user, err := s.DB.GetUserBySpotifyID(userProfile.ID)
51 if err != nil {
52- log.Printf("Error checking for user: %v", err)
53- return 0
054 }
5556- // If user doesn't exist, create them
0057 if user == nil {
58- user = &models.User{
59- Username: userProfile.DisplayName,
60- Email: userProfile.Email,
61- SpotifyID: userProfile.ID,
62- AccessToken: token,
63- TokenExpiry: time.Now().Add(1 * time.Hour), // Spotify tokens last ~1 hour
64- }
65-66- userID, err := s.DB.CreateUser(user)
67- if err != nil {
68- log.Printf("Error creating user: %v", err)
69- return 0
70 }
71- user.ID = userID
72 } else {
73- // Update token
74- err = s.DB.UpdateUserToken(user.ID, token, "", time.Now().Add(1*time.Hour))
75 if err != nil {
76- log.Printf("Error updating user token: %v", err)
000077 }
78 }
0007980- // Store token in memory
81 s.mu.Lock()
82 s.userTokens[user.ID] = token
83 s.mu.Unlock()
8485- log.Printf("User authenticated: %s (ID: %d)", user.Username, user.ID)
86- return user.ID
87}
8889type spotifyProfile struct {
···105 count := 0
106 for _, user := range users {
107 // Only load users with valid tokens
108- if user.AccessToken != "" && user.TokenExpiry.After(time.Now()) {
109- s.userTokens[user.ID] = user.AccessToken
110 count++
111 }
112 }
···124 return fmt.Errorf("error loading user: %v", err)
125 }
126127- if user.RefreshToken == "" {
128 return fmt.Errorf("no refresh token for user %s", userID)
129 }
130···150 refreshed := 0
151 for _, user := range users {
152 // Skip users without refresh tokens
153- if user.RefreshToken == "" {
154 continue
155 }
156
···30 }
31}
3233+func (s *SpotifyService) SetAccessToken(token string, userId int64, hasSession bool) (int64, error) {
0034 // Identify the user synchronously instead of in a goroutine
35+ userID, err := s.identifyAndStoreUser(token, userId, hasSession)
36+ if err != nil {
37+ log.Printf("Error identifying and storing user: %v", err)
38+ return 0, err
39+ }
40+ return userID, nil
41}
4243+func (s *SpotifyService) identifyAndStoreUser(token string, userId int64, hasSession bool) (int64, error) {
44 // Get Spotify user profile
45 userProfile, err := s.fetchSpotifyProfile(token)
46 if err != nil {
47 log.Printf("Error fetching Spotify profile: %v", err)
48+ return 0, err
49 }
50+51+ fmt.Printf("uid: %d hasSession: %t", userId, hasSession)
5253 // Check if user exists
54 user, err := s.DB.GetUserBySpotifyID(userProfile.ID)
55 if err != nil {
56+ // This error might mean DB connection issue, not just user not found.
57+ log.Printf("Error checking for user by Spotify ID %s: %v", userProfile.ID, err)
58+ return 0, err
59 }
6061+ tokenExpiryTime := time.Now().Add(1 * time.Hour) // Spotify tokens last ~1 hour
62+63+ // We don't intend users to log in via spotify!
64 if user == nil {
65+ if !hasSession {
66+ log.Printf("User does not seem to exist")
67+ return 0, fmt.Errorf("user does not seem to exist")
68+ } else {
69+ // overwrite prev user
70+ user, err = s.DB.AddSpotifySession(userId, userProfile.DisplayName, userProfile.Email, userProfile.ID, token, "", tokenExpiryTime)
71+ if err != nil {
72+ log.Printf("Error adding Spotify session for user ID %d: %v", userId, err)
73+ return 0, err
74+ }
0075 }
076 } else {
77+ // Update existing user's token and expiry
78+ err = s.DB.UpdateUserToken(user.ID, token, "", tokenExpiryTime)
79 if err != nil {
80+ log.Printf("Error updating user token for user ID %d: %v", user.ID, err)
81+ // Consider if we should return 0 or the user ID even if update fails
82+ // Sticking to original behavior: log and continue
83+ } else {
84+ log.Printf("Updated token for existing user: %s (ID: %d)", user.Username, user.ID)
85 }
86 }
87+ // Keep the local 'user' object consistent (optional but good practice)
88+ user.AccessToken = &token
89+ user.TokenExpiry = &tokenExpiryTime
9091+ // Store token in memory cache regardless of new/existing user
92 s.mu.Lock()
93 s.userTokens[user.ID] = token
94 s.mu.Unlock()
9596+ log.Printf("User authenticated via Spotify: %s (ID: %d)", user.Username, user.ID)
97+ return user.ID, nil
98}
99100type spotifyProfile struct {
···116 count := 0
117 for _, user := range users {
118 // Only load users with valid tokens
119+ if user.AccessToken != nil && user.TokenExpiry.After(time.Now()) {
120+ s.userTokens[user.ID] = *user.AccessToken
121 count++
122 }
123 }
···135 return fmt.Errorf("error loading user: %v", err)
136 }
137138+ if user.RefreshToken == nil {
139 return fmt.Errorf("no refresh token for user %s", userID)
140 }
141···161 refreshed := 0
162 for _, user := range users {
163 // Skip users without refresh tokens
164+ if user.RefreshToken == nil {
165 continue
166 }
167
+56-4
session/session.go
···13 "github.com/teal-fm/piper/db/apikey"
14)
15016type Session struct {
17- ID string
18- UserID int64
19- CreatedAt time.Time
20- ExpiresAt time.Time
00021}
2223type SessionManager struct {
···251 }
252}
2530000000000000000000000000000000000000000000254// WithAPIAuth is a middleware specifically for API-only endpoints (no cookie fallback, returns 401 instead of redirect)
255func WithAPIAuth(handler http.HandlerFunc, sm *SessionManager) http.HandlerFunc {
256 return func(w http.ResponseWriter, r *http.Request) {
···288const (
289 userIDKey contextKey = iota
290 apiRequestKey
0291)
292293func WithUserID(ctx context.Context, userID int64) context.Context {
···297func GetUserID(ctx context.Context) (int64, bool) {
298 userID, ok := ctx.Value(userIDKey).(int64)
299 return userID, ok
0000300}
301302func WithAPIRequest(ctx context.Context, isAPI bool) context.Context {
···13 "github.com/teal-fm/piper/db/apikey"
14)
1516+// session/session.go
17type Session struct {
18+ ID string
19+ UserID int64
20+ ATprotoDID string
21+ ATprotoAccessToken string
22+ ATprotoRefreshToken string
23+ CreatedAt time.Time
24+ ExpiresAt time.Time
25}
2627type SessionManager struct {
···255 }
256}
257258+func WithPossibleAuth(handler http.HandlerFunc, sm *SessionManager) http.HandlerFunc {
259+ return func(w http.ResponseWriter, r *http.Request) {
260+ ctx := r.Context()
261+ authenticated := false // Default to not authenticated
262+263+ // 1. Try API key authentication
264+ apiKeyStr, apiKeyErr := apikey.ExtractApiKey(r)
265+ if apiKeyErr == nil && apiKeyStr != "" {
266+ apiKey, valid := sm.apiKeyMgr.GetApiKey(apiKeyStr)
267+ if valid {
268+ // API Key valid: Add UserID, API flag, and set auth status
269+ ctx = WithUserID(ctx, apiKey.UserID)
270+ ctx = WithAPIRequest(ctx, true)
271+ authenticated = true
272+ // Update request context and call handler
273+ r = r.WithContext(WithAuthStatus(ctx, authenticated))
274+ handler(w, r)
275+ return
276+ }
277+ // If API key was provided but invalid, we still proceed without auth
278+ }
279+280+ // 2. If no valid API key, try cookie authentication
281+ if !authenticated { // Only check cookies if API key didn't authenticate
282+ cookie, err := r.Cookie("session")
283+ if err == nil { // Cookie exists
284+ session, exists := sm.GetSession(cookie.Value)
285+ if exists {
286+ // Session valid: Add UserID and set auth status
287+ ctx = WithUserID(ctx, session.UserID)
288+ // ctx = WithAPIRequest(ctx, false) // Not strictly needed, default is false
289+ authenticated = true
290+ }
291+ // If session cookie exists but is invalid/expired, we proceed without auth
292+ }
293+ }
294+295+ // 3. Set final auth status (could be true or false) and call handler
296+ r = r.WithContext(WithAuthStatus(ctx, authenticated))
297+ handler(w, r)
298+ }
299+}
300+301// WithAPIAuth is a middleware specifically for API-only endpoints (no cookie fallback, returns 401 instead of redirect)
302func WithAPIAuth(handler http.HandlerFunc, sm *SessionManager) http.HandlerFunc {
303 return func(w http.ResponseWriter, r *http.Request) {
···335const (
336 userIDKey contextKey = iota
337 apiRequestKey
338+ authStatusKey
339)
340341func WithUserID(ctx context.Context, userID int64) context.Context {
···345func GetUserID(ctx context.Context) (int64, bool) {
346 userID, ok := ctx.Value(userIDKey).(int64)
347 return userID, ok
348+}
349+350+func WithAuthStatus(ctx context.Context, isAuthed bool) context.Context {
351+ return context.WithValue(ctx, authStatusKey, isAuthed)
352}
353354func WithAPIRequest(ctx context.Context, isAPI bool) context.Context {