···21func home(w http.ResponseWriter, r *http.Request) {
22 w.Header().Set("Content-Type", "text/html")
2324- // Check if user has an active session cookie
25 cookie, err := r.Cookie("session")
26 isLoggedIn := err == nil && cookie != nil
27- // TODO: Add logic here to fetch user details from DB using session ID
28- // to check if Spotify is already connected, if desired for finer control.
29- // For now, we'll just check if *any* session exists.
3031 html := `
32 <html>
···106107// JSON API handlers
108109-// jsonResponse returns a JSON response
110func jsonResponse(w http.ResponseWriter, statusCode int, data any) {
111 w.Header().Set("Content-Type", "application/json")
112 w.WriteHeader(statusCode)
···115 }
116}
117118-// API endpoint for current track
119func apiCurrentTrack(spotifyService *spotify.SpotifyService) http.HandlerFunc {
120 return func(w http.ResponseWriter, r *http.Request) {
121 userID, ok := session.GetUserID(r.Context())
···134 }
135}
136137-// API endpoint for history
138func apiTrackHistory(spotifyService *spotify.SpotifyService) http.HandlerFunc {
139 return func(w http.ResponseWriter, r *http.Request) {
140 userID, ok := session.GetUserID(r.Context())
···21func home(w http.ResponseWriter, r *http.Request) {
22 w.Header().Set("Content-Type", "text/html")
2324+ // check if user has an active session cookie
25 cookie, err := r.Cookie("session")
26 isLoggedIn := err == nil && cookie != nil
27+ // TODO: add logic here to fetch user details from DB using session ID
28+ // to check if Spotify is already connected
02930 html := `
31 <html>
···105106// JSON API handlers
1070108func jsonResponse(w http.ResponseWriter, statusCode int, data any) {
109 w.Header().Set("Content-Type", "application/json")
110 w.WriteHeader(statusCode)
···113 }
114}
1150116func apiCurrentTrack(spotifyService *spotify.SpotifyService) http.HandlerFunc {
117 return func(w http.ResponseWriter, r *http.Request) {
118 userID, ok := session.GetUserID(r.Context())
···131 }
132}
1330134func apiTrackHistory(spotifyService *spotify.SpotifyService) http.HandlerFunc {
135 return func(w http.ResponseWriter, r *http.Request) {
136 userID, ok := session.GetUserID(r.Context())
-1
models/atproto.go
···1-// Add this struct definition to piper/models/atproto.go
2package models
34import (
···23import "time"
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
0000020}
···23import "time"
45+// an end user of piper
6type User struct {
7+ ID int64
8+ Username string
9+ Email *string
10+11+ // spotify information
12+ SpotifyID *string
13+ AccessToken *string
14+ RefreshToken *string
15+ TokenExpiry *time.Time
16+17+ // atp info
18+ ATProtoDID *string
19+ ATProtoAccessToken *string
20+ ATProtoRefreshToken *string
21+ ATProtoTokenExpiry *time.Time
22+23+ CreatedAt time.Time
24+ UpdatedAt time.Time
25}
+2-3
oauth/atproto/atproto.go
···1-// Modify piper/oauth/atproto/atproto.go
2package atproto
34import (
···88 return nil, fmt.Errorf("failed PAR request to %s: %w", ui.AuthServer, err)
89 }
9091- // Save state including generated PKCE verifier and DPoP key
92 data := &models.ATprotoAuthData{
93 State: parResp.State,
94 DID: ui.DID,
···171 }
172173 log.Printf("ATProto Callback Success: User %d (DID: %s) authenticated.", userID.ID, data.DID)
174- return userID.ID, nil // Return the piper user ID
175}
···01package atproto
23import (
···87 return nil, fmt.Errorf("failed PAR request to %s: %w", ui.AuthServer, err)
88 }
8990+ // Save state
91 data := &models.ATprotoAuthData{
92 State: parResp.State,
93 DID: ui.DID,
···170 }
171172 log.Printf("ATProto Callback Success: User %d (DID: %s) authenticated.", userID.ID, data.DID)
173+ return userID.ID, nil
174}
+6-14
oauth/oauth2.go
···1-// Modify piper/oauth/oauth2.go
2package oauth
34import (
···22 state string
23 codeVerifier string
24 codeChallenge string
25- // Added TokenReceiver field to handle user lookup/creation based on token
26 tokenReceiver TokenReceiver
27}
28···38 switch strings.ToLower(provider) {
39 case "spotify":
40 endpoint = spotify.Endpoint
41- // Add other providers like Last.fm here
42 default:
43- // Placeholder for unconfigured providers
44 log.Printf("Warning: OAuth2 provider '%s' not explicitly configured. Using placeholder endpoints.", provider)
45 endpoint = oauth2.Endpoint{
46- AuthURL: "https://example.com/auth", // Replace with actual endpoints if needed
47 TokenURL: "https://example.com/token",
48 }
49 }
···62 state: GenerateRandomState(),
63 codeVerifier: codeVerifier,
64 codeChallenge: codeChallenge,
65- tokenReceiver: tokenReceiver, // Store the token receiver
66 }
67}
6869-// generateCodeVerifier creates a random code verifier for PKCE
70func GenerateCodeVerifier() string {
71 b := make([]byte, 64)
72 rand.Read(b)
73 return base64.RawURLEncoding.EncodeToString(b)
74}
7576-// generateCodeChallenge creates a code challenge from the code verifier using S256 method
77func GenerateCodeChallenge(verifier string) string {
78 h := sha256.New()
79 h.Write([]byte(verifier))
80 return base64.RawURLEncoding.EncodeToString(h.Sum(nil))
81}
8283-// HandleLogin implements the AuthService interface method.
84func (o *OAuth2Service) HandleLogin(w http.ResponseWriter, r *http.Request) {
85 opts := []oauth2.AuthCodeOption{
86 oauth2.SetAuthURLParam("code_challenge", o.codeChallenge),
···128129 userId, hasSession := session.GetUserID(r.Context())
130131- // Use the token receiver to store the token and get the user ID
132 userID, err := o.tokenReceiver.SetAccessToken(token.AccessToken, userId, hasSession)
133 if err != nil {
134 log.Printf("OAuth2 Callback Info: TokenReceiver did not return a valid user ID for token: %s...", token.AccessToken[:min(10, len(token.AccessToken))])
···138 return userID, nil
139}
140141-// GetToken remains unchanged
142func (o *OAuth2Service) GetToken(code string) (*oauth2.Token, error) {
143 opts := []oauth2.AuthCodeOption{
144 oauth2.SetAuthURLParam("code_verifier", o.codeVerifier),
···146 return o.config.Exchange(context.Background(), code, opts...)
147}
148149-// GetClient remains unchanged
150func (o *OAuth2Service) GetClient(token *oauth2.Token) *http.Client {
151 return o.config.Client(context.Background(), token)
152}
153154-// RefreshToken remains unchanged
155func (o *OAuth2Service) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) {
156 source := o.config.TokenSource(context.Background(), token)
157 return oauth2.ReuseTokenSource(token, source).Token()
158}
159160-// Helper function
161func min(a, b int) int {
162 if a < b {
163 return a
···01package oauth
23import (
···21 state string
22 codeVerifier string
23 codeChallenge string
024 tokenReceiver TokenReceiver
25}
26···36 switch strings.ToLower(provider) {
37 case "spotify":
38 endpoint = spotify.Endpoint
039 default:
40+ // placeholder
41 log.Printf("Warning: OAuth2 provider '%s' not explicitly configured. Using placeholder endpoints.", provider)
42 endpoint = oauth2.Endpoint{
43+ AuthURL: "https://example.com/auth",
44 TokenURL: "https://example.com/token",
45 }
46 }
···59 state: GenerateRandomState(),
60 codeVerifier: codeVerifier,
61 codeChallenge: codeChallenge,
62+ tokenReceiver: tokenReceiver,
63 }
64}
6566+// generate a random code verifier, for PKCE
67func GenerateCodeVerifier() string {
68 b := make([]byte, 64)
69 rand.Read(b)
70 return base64.RawURLEncoding.EncodeToString(b)
71}
7273+// generate a code challenge for verification later
74func GenerateCodeChallenge(verifier string) string {
75 h := sha256.New()
76 h.Write([]byte(verifier))
77 return base64.RawURLEncoding.EncodeToString(h.Sum(nil))
78}
79080func (o *OAuth2Service) HandleLogin(w http.ResponseWriter, r *http.Request) {
81 opts := []oauth2.AuthCodeOption{
82 oauth2.SetAuthURLParam("code_challenge", o.codeChallenge),
···124125 userId, hasSession := session.GetUserID(r.Context())
126127+ // store token and get uid
128 userID, err := o.tokenReceiver.SetAccessToken(token.AccessToken, userId, hasSession)
129 if err != nil {
130 log.Printf("OAuth2 Callback Info: TokenReceiver did not return a valid user ID for token: %s...", token.AccessToken[:min(10, len(token.AccessToken))])
···134 return userID, nil
135}
1360137func (o *OAuth2Service) GetToken(code string) (*oauth2.Token, error) {
138 opts := []oauth2.AuthCodeOption{
139 oauth2.SetAuthURLParam("code_verifier", o.codeVerifier),
···141 return o.config.Exchange(context.Background(), code, opts...)
142}
1430144func (o *OAuth2Service) GetClient(token *oauth2.Token) *http.Client {
145 return o.config.Client(context.Background(), token)
146}
1470148func (o *OAuth2Service) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) {
149 source := o.config.TokenSource(context.Background(), token)
150 return oauth2.ReuseTokenSource(token, source).Token()
151}
1520153func min(a, b int) int {
154 if a < b {
155 return a
+9-14
oauth/oauth_manager.go
···1213// manages multiple oauth client services
14type 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.
28func (m *OAuthServiceManager) RegisterService(name string, service AuthService) {
29 m.mu.Lock()
30 defer m.mu.Unlock()
···32 log.Printf("Registered auth service: %s", name)
33}
3435-// GetService retrieves a registered AuthService by name.
36func (m *OAuthServiceManager) GetService(name string) (AuthService, bool) {
37 m.mu.RLock()
38 defer m.mu.RUnlock()
···47 m.mu.RUnlock()
4849 if exists {
50- service.HandleLogin(w, r) // Call interface method
51 return
52 }
53···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
7576 if err != nil {
77 log.Printf("Error handling callback for service '%s': %v", serviceName, err)
···80 }
8182 if userID > 0 {
83- // Create session for the user
84 session := m.sessionManager.CreateSession(userID)
8586- // Set session cookie
87 m.sessionManager.SetSessionCookie(w, session)
8889 log.Printf("Created session for user %d via service %s", userID, serviceName)
9091- // 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}
···1213// manages multiple oauth client services
14type OAuthServiceManager struct {
15+ services map[string]AuthService
16 sessionManager *session.SessionManager
17 mu sync.RWMutex
18}
1920func NewOAuthServiceManager() *OAuthServiceManager {
21 return &OAuthServiceManager{
22+ services: make(map[string]AuthService),
23 sessionManager: session.NewSessionManager(),
24 }
25}
2627+// registers any service that impls AuthService
28func (m *OAuthServiceManager) RegisterService(name string, service AuthService) {
29 m.mu.Lock()
30 defer m.mu.Unlock()
···32 log.Printf("Registered auth service: %s", name)
33}
3435+// get an AuthService by registered name
36func (m *OAuthServiceManager) GetService(name string) (AuthService, bool) {
37 m.mu.RLock()
38 defer m.mu.RUnlock()
···47 m.mu.RUnlock()
4849 if exists {
50+ service.HandleLogin(w, r)
51 return
52 }
53···70 return
71 }
7273+ userID, err := service.HandleCallback(w, r)
07475 if err != nil {
76 log.Printf("Error handling callback for service '%s': %v", serviceName, err)
···79 }
8081 if userID > 0 {
082 session := m.sessionManager.CreateSession(userID)
83084 m.sessionManager.SetSessionCookie(w, session)
8586 log.Printf("Created session for user %d via service %s", userID, serviceName)
87088 http.Redirect(w, r, "/", http.StatusSeeOther)
89 } else {
90 log.Printf("Callback for service '%s' did not result in a valid user ID.", serviceName)
91+ // todo: redirect to an error page
92+ // right now this just redirects home but we don't want this behaviour ideally
93+ http.Redirect(w, r, "/", http.StatusSeeOther)
094 }
95 }
96}
+6-10
oauth/service.go
···1-// Create piper/oauth/auth_service.go
2package oauth
34import (
5 "net/http"
6)
78-// AuthService defines the interface for different authentication services
9-// that can be managed by the OAuthServiceManager.
10type 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}
19020type 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}
···01package oauth
23import (
4 "net/http"
5)
6007type AuthService interface {
8+ // inits the login flow for the service
9 HandleLogin(w http.ResponseWriter, r *http.Request)
10+ // handles the callback for the provider. is responsible for inserting
11+ // sessions in the db
0012 HandleCallback(w http.ResponseWriter, r *http.Request) (int64, error)
13}
1415+// optional but recommended
16type TokenReceiver interface {
17+ // stores the access token in the db
18+ // if there is a session, will associate the token with the session
19 SetAccessToken(token string, currentId int64, hasSession bool) (int64, error)
20}
+24-39
service/spotify/spotify.go
···23import (
4 "encoding/json"
05 "fmt"
6 "io"
7 "log"
···31}
3233func (s *SpotifyService) SetAccessToken(token string, userId int64, hasSession bool) (int64, error) {
34- // 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)
···41}
4243func (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)
···5051 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.
···74 }
75 }
76 } else {
77- // Update existing user's token and expiry
78 err = s.DB.UpdateUserToken(user.ID, token, "", tokenExpiryTime)
79 if err != nil {
080 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()
···103 Email string `json:"email"`
104}
105106-// LoadAllUsers loads all active users from the database into memory
107func (s *SpotifyService) LoadAllUsers() error {
108 users, err := s.DB.GetAllActiveUsers()
109 if err != nil {
···115116 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++
···126 return nil
127}
1280000000129func (s *SpotifyService) RefreshToken(userID string) error {
130 s.mu.Lock()
131 defer s.mu.Unlock()
···139 return fmt.Errorf("no refresh token for user %s", userID)
140 }
141142- // Implement token refresh logic here using Spotify's token refresh endpoint
143- // This would make a request to Spotify's token endpoint with grant_type=refresh_token
144-145- // If successful, update the database and in-memory cache
146- // we won't be now so just error out
147- return fmt.Errorf("token refresh not implemented")
148- //
149- //s.userTokens[user.ID] = newToken
150- //return nil
151}
152153-// RefreshExpiredTokens attempts to refresh expired tokens
154func (s *SpotifyService) RefreshExpiredTokens() {
155 users, err := s.DB.GetUsersWithExpiredTokens()
156 if err != nil {
···160161 refreshed := 0
162 for _, user := range users {
163- // Skip users without refresh tokens
164 if user.RefreshToken == nil {
165 continue
166 }
167168- // Implement token refresh logic here using Spotify's token refresh endpoint
169- // This would make a request to Spotify's token endpoint with grant_type=refresh_token
0000170171- // If successful, update the database and in-memory cache
172 refreshed++
173 }
174···231 return
232 }
233234- // Get recent tracks from database
235 tracks, err := s.DB.GetRecentTracks(userID, 20)
236 if err != nil {
237 http.Error(w, "Error retrieving track history", http.StatusInternalServerError)
···252 return nil, fmt.Errorf("no access token for user %d", userID)
253 }
254255- // Call Spotify API to get currently playing track
256 req, err := http.NewRequest("GET", "https://api.spotify.com/v1/me/player/currently-playing", nil)
257 if err != nil {
258 return nil, err
···266 }
267 defer resp.Body.Close()
268269- // No track playing
270 if resp.StatusCode == 204 {
271 return nil, nil
272 }
273274- // Token expired
275 if resp.StatusCode == 401 {
276 // attempt to refresh token
277 if err := s.RefreshToken(strconv.FormatInt(userID, 10)); err != nil {
···282 }
283 }
284285- // Error response
286 if resp.StatusCode != 200 {
287 body, _ := io.ReadAll(resp.Body)
288 return nil, fmt.Errorf("spotify API error: %s", body)
289 }
290291- // Parse response
292 var response struct {
293 Item struct {
294 Name string `json:"name"`
···320 return nil, err
321 }
322323- // Extract artist names/ids
324 var artists []models.Artist
325 for _, artist := range response.Item.Artists {
326 artists = append(artists, models.Artist{
···329 })
330 }
331332- // Create Track model
333 track := &models.Track{
334 Name: response.Item.Name,
335 Artist: artists,
···351 defer ticker.Stop()
352353 for range ticker.C {
354- // Copy userIDs to avoid holding the lock too long
355 s.mu.RLock()
356 userIDs := make([]int64, 0, len(s.userTokens))
357 for userID := range s.userTokens {
···359 }
360 s.mu.RUnlock()
361362- // Check each user's currently playing track
363 for _, userID := range userIDs {
364 track, err := s.FetchCurrentTrack(userID)
365 if err != nil {
···367 continue
368 }
369370- // No change if no track is playing
371 if track == nil {
372 continue
373 }
374375- // Check if this is a new track
376 s.mu.RLock()
377 currentTrack := s.userTracks[userID]
378 s.mu.RUnlock()
···384 }
385 }
386387- // If track is different or we've played more than either half of the track or 30 seconds since the start
388- // whichever is greater
389 isNewTrack := currentTrack == nil ||
390 currentTrack.Name != track.Name ||
391 // just check the first one for now
···426 }
427428 if isNewTrack {
429- // Save to database
430 id, err := s.DB.SaveTrack(userID, track)
431 if err != nil {
432 log.Printf("Error saving track for user %d: %v", userID, err)
···23import (
4 "encoding/json"
5+ "errors"
6 "fmt"
7 "io"
8 "log"
···32}
3334func (s *SpotifyService) SetAccessToken(token string, userId int64, hasSession bool) (int64, error) {
035 userID, err := s.identifyAndStoreUser(token, userId, hasSession)
36 if err != nil {
37 log.Printf("Error identifying and storing user: %v", err)
···41}
4243func (s *SpotifyService) identifyAndStoreUser(token string, userId int64, hasSession bool) (int64, error) {
044 userProfile, err := s.fetchSpotifyProfile(token)
45 if err != nil {
46 log.Printf("Error fetching Spotify profile: %v", err)
···4950 fmt.Printf("uid: %d hasSession: %t", userId, hasSession)
51052 user, err := s.DB.GetUserBySpotifyID(userProfile.ID)
53 if err != nil {
54 // This error might mean DB connection issue, not just user not found.
···72 }
73 }
74 } else {
075 err = s.DB.UpdateUserToken(user.ID, token, "", tokenExpiryTime)
76 if err != nil {
77+ // for now log and continue
78 log.Printf("Error updating user token for user ID %d: %v", user.ID, err)
0079 } else {
80 log.Printf("Updated token for existing user: %s (ID: %d)", user.Username, user.ID)
81 }
82 }
083 user.AccessToken = &token
84 user.TokenExpiry = &tokenExpiryTime
85086 s.mu.Lock()
87 s.userTokens[user.ID] = token
88 s.mu.Unlock()
···97 Email string `json:"email"`
98}
990100func (s *SpotifyService) LoadAllUsers() error {
101 users, err := s.DB.GetAllActiveUsers()
102 if err != nil {
···108109 count := 0
110 for _, user := range users {
111+ // load users with valid tokens
112 if user.AccessToken != nil && user.TokenExpiry.After(time.Now()) {
113 s.userTokens[user.ID] = *user.AccessToken
114 count++
···119 return nil
120}
121122+func (s *SpotifyService) refreshTokenInner(user models.User) error {
123+ // implement token refresh logic here using Spotify's token refresh endpoint
124+ // this would make a request to Spotify's token endpoint with grant_type=refresh_token
125+ return errors.New("Not implemented yet")
126+ // if successful, update the database and in-memory cache
127+}
128+129func (s *SpotifyService) RefreshToken(userID string) error {
130 s.mu.Lock()
131 defer s.mu.Unlock()
···139 return fmt.Errorf("no refresh token for user %s", userID)
140 }
141142+ return s.refreshTokenInner(*user)
00000000143}
144145+// attempt to refresh expired tokens
146func (s *SpotifyService) RefreshExpiredTokens() {
147 users, err := s.DB.GetUsersWithExpiredTokens()
148 if err != nil {
···152153 refreshed := 0
154 for _, user := range users {
155+ // skip users without refresh tokens
156 if user.RefreshToken == nil {
157 continue
158 }
159160+ err := s.refreshTokenInner(*user)
161+162+ if err != nil {
163+ // just print out errors here for now
164+ log.Printf("Error from service/spotify/spotify.go when refreshing tokens: %s", err.Error())
165+ }
1660167 refreshed++
168 }
169···226 return
227 }
2280229 tracks, err := s.DB.GetRecentTracks(userID, 20)
230 if err != nil {
231 http.Error(w, "Error retrieving track history", http.StatusInternalServerError)
···246 return nil, fmt.Errorf("no access token for user %d", userID)
247 }
2480249 req, err := http.NewRequest("GET", "https://api.spotify.com/v1/me/player/currently-playing", nil)
250 if err != nil {
251 return nil, err
···259 }
260 defer resp.Body.Close()
261262+ // nothing playing
263 if resp.StatusCode == 204 {
264 return nil, nil
265 }
266267+ // oops, token expired
268 if resp.StatusCode == 401 {
269 // attempt to refresh token
270 if err := s.RefreshToken(strconv.FormatInt(userID, 10)); err != nil {
···275 }
276 }
2770278 if resp.StatusCode != 200 {
279 body, _ := io.ReadAll(resp.Body)
280 return nil, fmt.Errorf("spotify API error: %s", body)
281 }
2820283 var response struct {
284 Item struct {
285 Name string `json:"name"`
···311 return nil, err
312 }
3130314 var artists []models.Artist
315 for _, artist := range response.Item.Artists {
316 artists = append(artists, models.Artist{
···319 })
320 }
321322+ // assemble Track
323 track := &models.Track{
324 Name: response.Item.Name,
325 Artist: artists,
···341 defer ticker.Stop()
342343 for range ticker.C {
344+ // copy userIDs to avoid holding the lock too long
345 s.mu.RLock()
346 userIDs := make([]int64, 0, len(s.userTokens))
347 for userID := range s.userTokens {
···349 }
350 s.mu.RUnlock()
3510352 for _, userID := range userIDs {
353 track, err := s.FetchCurrentTrack(userID)
354 if err != nil {
···356 continue
357 }
3580359 if track == nil {
360 continue
361 }
3620363 s.mu.RLock()
364 currentTrack := s.userTracks[userID]
365 s.mu.RUnlock()
···371 }
372 }
373374+ // if flagged true, we have a new track
0375 isNewTrack := currentTrack == nil ||
376 currentTrack.Name != track.Name ||
377 // just check the first one for now
···412 }
413414 if isNewTrack {
0415 id, err := s.DB.SaveTrack(userID, track)
416 if err != nil {
417 log.Printf("Error saving track for user %d: %v", userID, err)
+10-33
session/session.go
···31 mu sync.RWMutex
32}
3334-// NewSessionManager creates a new session manager
35func NewSessionManager() *SessionManager {
36- // Initialize session table if it doesn't exist
37 database, err := db.New("./data/piper.db")
38 if err != nil {
39 log.Printf("Error connecting to database for sessions, falling back to in memory only: %v", err)
···56 log.Printf("Error creating sessions table: %v", err)
57 }
5859- // Create API key manager
60 apiKeyMgr := apikey.NewApiKeyManager(database)
6162 return &SessionManager{
···120 return session, true
121 }
122123- // If not in memory and we have a database, check there
124 if sm.db != nil {
125 session = &Session{ID: sessionID}
126···189 http.SetCookie(w, cookie)
190}
191192-// HandleLogout handles user logout
193func (sm *SessionManager) HandleLogout(w http.ResponseWriter, r *http.Request) {
194 cookie, err := r.Cookie("session")
195 if err == nil {
···201 http.Redirect(w, r, "/", http.StatusSeeOther)
202}
203204-// GetAPIKeyManager returns the API key manager
205func (sm *SessionManager) GetAPIKeyManager() *apikey.ApiKeyManager {
206 return sm.apiKeyMgr
207}
208209-// CreateAPIKey creates a new API key for a user
210func (sm *SessionManager) CreateAPIKey(userID int64, name string, validityDays int) (*apikey.ApiKey, error) {
211 return sm.apiKeyMgr.CreateApiKey(userID, name, validityDays)
212}
213214-// WithAuth is a middleware that checks if a user is authenticated via cookies or API key
215func WithAuth(handler http.HandlerFunc, sm *SessionManager) http.HandlerFunc {
216 return func(w http.ResponseWriter, r *http.Request) {
217- // First try API key authentication (for API requests)
218 apiKeyStr, apiKeyErr := apikey.ExtractApiKey(r)
219 if apiKeyErr == nil && apiKeyStr != "" {
220- // Validate API key
221 apiKey, valid := sm.apiKeyMgr.GetApiKey(apiKeyStr)
222 if valid {
223- // Add user ID to context
224 ctx := WithUserID(r.Context(), apiKey.UserID)
225 r = r.WithContext(ctx)
226227- // Set a flag in the context that this is an API request
228 ctx = WithAPIRequest(r.Context(), true)
229 r = r.WithContext(ctx)
230···233 }
234 }
235236- // Fall back to cookie authentication (for browser requests)
237 cookie, err := r.Cookie("session")
238 if err != nil {
239 http.Redirect(w, r, "/login/spotify", http.StatusSeeOther)
240 return
241 }
242243- // Verify cookie session
244 session, exists := sm.GetSession(cookie.Value)
245 if !exists {
246 http.Redirect(w, r, "/login/spotify", http.StatusSeeOther)
247 return
248 }
249250- // Add session information to request context
251 ctx := WithUserID(r.Context(), session.UserID)
252 r = r.WithContext(ctx)
253···255 }
256}
2570258func 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
262263- // 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 }
279280- // 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 }
294295- // 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}
300301-// 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) {
304- // Try API key authentication
305 apiKeyStr, apiKeyErr := apikey.ExtractApiKey(r)
306 if apiKeyErr != nil || apiKeyStr == "" {
307 w.Header().Set("Content-Type", "application/json")
···310 return
311 }
312313- // Validate API key
314 apiKey, valid := sm.apiKeyMgr.GetApiKey(apiKeyStr)
315 if !valid {
316 w.Header().Set("Content-Type", "application/json")
···319 return
320 }
321322- // Add user ID to context
323 ctx := WithUserID(r.Context(), apiKey.UserID)
324- // Mark as API request
325 ctx = WithAPIRequest(ctx, true)
326 r = r.WithContext(ctx)
327···329 }
330}
331332-// Context keys
333type contextKey int
334335const (
···31 mu sync.RWMutex
32}
33034func NewSessionManager() *SessionManager {
035 database, err := db.New("./data/piper.db")
36 if err != nil {
37 log.Printf("Error connecting to database for sessions, falling back to in memory only: %v", err)
···54 log.Printf("Error creating sessions table: %v", err)
55 }
56057 apiKeyMgr := apikey.NewApiKeyManager(database)
5859 return &SessionManager{
···117 return session, true
118 }
119120+ // if not in memory and we have a database, check there
121 if sm.db != nil {
122 session = &Session{ID: sessionID}
123···186 http.SetCookie(w, cookie)
187}
1880189func (sm *SessionManager) HandleLogout(w http.ResponseWriter, r *http.Request) {
190 cookie, err := r.Cookie("session")
191 if err == nil {
···197 http.Redirect(w, r, "/", http.StatusSeeOther)
198}
1990200func (sm *SessionManager) GetAPIKeyManager() *apikey.ApiKeyManager {
201 return sm.apiKeyMgr
202}
2030204func (sm *SessionManager) CreateAPIKey(userID int64, name string, validityDays int) (*apikey.ApiKey, error) {
205 return sm.apiKeyMgr.CreateApiKey(userID, name, validityDays)
206}
207208+// middleware that checks if a user is authenticated via cookies or API key
209func WithAuth(handler http.HandlerFunc, sm *SessionManager) http.HandlerFunc {
210 return func(w http.ResponseWriter, r *http.Request) {
211+ // first: check API keys
212 apiKeyStr, apiKeyErr := apikey.ExtractApiKey(r)
213 if apiKeyErr == nil && apiKeyStr != "" {
0214 apiKey, valid := sm.apiKeyMgr.GetApiKey(apiKeyStr)
215 if valid {
0216 ctx := WithUserID(r.Context(), apiKey.UserID)
217 r = r.WithContext(ctx)
218219+ // set a flag for api requests
220 ctx = WithAPIRequest(r.Context(), true)
221 r = r.WithContext(ctx)
222···225 }
226 }
227228+ // if not found, check cookies for session value
229 cookie, err := r.Cookie("session")
230 if err != nil {
231 http.Redirect(w, r, "/login/spotify", http.StatusSeeOther)
232 return
233 }
2340235 session, exists := sm.GetSession(cookie.Value)
236 if !exists {
237 http.Redirect(w, r, "/login/spotify", http.StatusSeeOther)
238 return
239 }
2400241 ctx := WithUserID(r.Context(), session.UserID)
242 r = r.WithContext(ctx)
243···245 }
246}
247248+// middleware that checks if a user is authenticated but doesn't error out if not
249func WithPossibleAuth(handler http.HandlerFunc, sm *SessionManager) http.HandlerFunc {
250 return func(w http.ResponseWriter, r *http.Request) {
251 ctx := r.Context()
252+ authenticated := false
2530254 apiKeyStr, apiKeyErr := apikey.ExtractApiKey(r)
255 if apiKeyErr == nil && apiKeyStr != "" {
256 apiKey, valid := sm.apiKeyMgr.GetApiKey(apiKeyStr)
257 if valid {
0258 ctx = WithUserID(ctx, apiKey.UserID)
259 ctx = WithAPIRequest(ctx, true)
260 authenticated = true
0261 r = r.WithContext(WithAuthStatus(ctx, authenticated))
262 handler(w, r)
263 return
264 }
0265 }
266267+ if !authenticated {
0268 cookie, err := r.Cookie("session")
269+ if err == nil {
270 session, exists := sm.GetSession(cookie.Value)
271 if exists {
0272 ctx = WithUserID(ctx, session.UserID)
0273 authenticated = true
274 }
0275 }
276 }
2770278 r = r.WithContext(WithAuthStatus(ctx, authenticated))
279 handler(w, r)
280 }
281}
282283+// middleware that only accepts API keys
284func WithAPIAuth(handler http.HandlerFunc, sm *SessionManager) http.HandlerFunc {
285 return func(w http.ResponseWriter, r *http.Request) {
0286 apiKeyStr, apiKeyErr := apikey.ExtractApiKey(r)
287 if apiKeyErr != nil || apiKeyStr == "" {
288 w.Header().Set("Content-Type", "application/json")
···291 return
292 }
2930294 apiKey, valid := sm.apiKeyMgr.GetApiKey(apiKeyStr)
295 if !valid {
296 w.Header().Set("Content-Type", "application/json")
···299 return
300 }
3010302 ctx := WithUserID(r.Context(), apiKey.UserID)
0303 ctx = WithAPIRequest(ctx, true)
304 r = r.WithContext(ctx)
305···307 }
308}
3090310type contextKey int
311312const (