···10101111// Load initializes the configuration with viper
1212func Load() {
1313- // Load .env file if it exists
1413 if err := godotenv.Load(); err != nil {
1514 log.Println("No .env file found or error loading it. Using default values and environment variables.")
1615 }
17161818- // Set default configurations
1917 viper.SetDefault("server.port", "8080")
2018 viper.SetDefault("server.host", "localhost")
2119 viper.SetDefault("callback.spotify", "http://localhost:8080/callback/spotify")
···3028 viper.SetDefault("atproto.metadata_url", "http://localhost:8080/metadata")
3129 viper.SetDefault("atproto.callback_url", "/metadata")
32303333- // Configure Viper to read environment variables
3431 viper.AutomaticEnv()
35323636- // Replace dots with underscores for environment variables
3733 viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
38343939- // Set the config name and paths
4035 viper.SetConfigName("config")
4136 viper.SetConfigType("yaml")
4237 viper.AddConfigPath("./config")
4338 viper.AddConfigPath(".")
44394545- // Try to read the config file
4640 if err := viper.ReadInConfig(); err != nil {
4741 if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
4848- // It's not a "file not found" error, so it's a real error
4942 log.Fatalf("Error reading config file: %v", err)
5043 }
5151- // Config file not found, using defaults and environment variables
5244 log.Println("Config file not found, using default values and environment variables")
5345 } else {
5446 log.Println("Using config file:", viper.ConfigFileUsed())
5547 }
56485757- // Check if required values are present
4949+ // check for required settings
5850 requiredVars := []string{"spotify.client_id", "spotify.client_secret"}
5951 missingVars := []string{}
6052
+6-10
db/atproto.go
···39394040func (db *DB) GetATprotoAuthData(state string) (*models.ATprotoAuthData, error) {
4141 var data models.ATprotoAuthData
4242- var dpopPrivateJWKString string // Temporary variable to hold the JSON string
4242+ var dpopPrivateJWKString string
43434444 err := db.QueryRow(`
4545 SELECT state, did, pds_url, authserver_issuer, pkce_verifier, dpop_authserver_nonce, dpop_private_jwk
···5252 &data.AuthServerIssuer,
5353 &data.PKCEVerifier,
5454 &data.DPoPAuthServerNonce,
5555- &dpopPrivateJWKString, // Scan into the temporary string
5555+ &dpopPrivateJWKString,
5656 )
5757 if err != nil {
5858- // Return the original scan error if it occurred
5958 if err == sql.ErrNoRows {
6059 return nil, fmt.Errorf("no auth data found for state %s: %w", state, err)
6160 }
···64636564 key, err := helpers.ParseJWKFromBytes([]byte(dpopPrivateJWKString))
6665 if err != nil {
6767- // Return an error if parsing fails
6866 return nil, fmt.Errorf("failed to parse DPoPPrivateJWK for state %s: %w", state, err)
6967 }
7068 data.DPoPPrivateJWK = key
71697272- return &data, nil // Return nil error on success
7070+ return &data, nil
7371}
74727573func (db *DB) FindOrCreateUserByDID(did string) (*models.User, error) {
···9795 if idErr != nil {
9896 return nil, fmt.Errorf("failed to get last insert id: %w", idErr)
9997 }
100100- // Populate the user struct with the newly created user's data
10198 user.ID = lastID
10299 user.ATProtoDID = &did
103100 user.CreatedAt = now
104101 user.UpdatedAt = now
105105- return &user, nil // Return the created user and nil error
102102+ return &user, nil
106103 } else if err != nil {
107107- // Handle other potential errors from QueryRow
108104 return nil, fmt.Errorf("failed to find user by DID: %w", err)
109105 }
110106111107 return &user, err
112108}
113109114114-// Create or update the current user's ATproto session data.
110110+// create or update the current user's ATproto session data.
115111func (db *DB) SaveATprotoSession(tokenResp *oauth.TokenResponse) error {
116112117113 expiryTime := time.Now().Add(time.Second * time.Duration(tokenResp.ExpiresIn))
···141137142138 rowsAffected, err := result.RowsAffected()
143139 if err != nil {
144144- // Error checking RowsAffected, but the update might have succeeded
140140+ // it's possible the update succeeded here?
145141 return fmt.Errorf("failed to check rows affected after updating atproto session for did %s: %w", tokenResp.Sub, err)
146142 }
147143
+5-11
db/db.go
···1111 "github.com/teal-fm/piper/models"
1212)
13131414-// DB is a wrapper around sql.DB
1514type DB struct {
1615 *sql.DB
1716}
···3635}
37363837func (db *DB) Initialize() error {
3939- // Create users table
4038 _, err := db.Exec(`
4139 CREATE TABLE IF NOT EXISTS users (
4240 id INTEGER PRIMARY KEY AUTOINCREMENT,
···5957 return err
6058 }
61596262- // Create tracks table
6360 _, err = db.Exec(`
6461 CREATE TABLE IF NOT EXISTS tracks (
6562 id INTEGER PRIMARY KEY AUTOINCREMENT,
···115112 return result.LastInsertId()
116113}
117114118118-// Add spotify session to user, returning the updated user
115115+// add spotify session to user, returning the updated user
119116func (db *DB) AddSpotifySession(userID int64, username, email, spotifyId, accessToken, refreshToken string, tokenExpiry time.Time) (*models.User, error) {
120117 now := time.Now()
121118···191188}
192189193190func (db *DB) SaveTrack(userID int64, track *models.Track) (int64, error) {
194194- // Convert the Artist array to a string for storage
191191+ // marshal artist json
195192 artistString := ""
196193 if len(track.Artist) > 0 {
197194 bytes, err := json.Marshal(track.Artist)
···215212}
216213217214func (db *DB) UpdateTrack(trackID int64, track *models.Track) error {
218218- // Convert the Artist array to a string for storage
219219- // In a production environment, you'd want to use proper JSON serialization
215215+ // marshal artist json
220216 artistString := ""
221217 if len(track.Artist) > 0 {
222218 bytes, err := json.Marshal(track.Artist)
···248244}
249245250246func (db *DB) GetRecentTracks(userID int64, limit int) ([]*models.Track, error) {
251251- // convert previous-format artist strings to current-format
252252-253247 rows, err := db.Query(`
254248 SELECT id, name, artist, album, url, timestamp, duration_ms, progress_ms, service_base_url, isrc, has_stamped
255249 FROM tracks
···270264 err := rows.Scan(
271265 &track.PlayID,
272266 &track.Name,
273273- &artistString, // Scan into a string first
267267+ &artistString, // scan to be unmarshaled later
274268 &track.Album,
275269 &track.URL,
276270 &track.Timestamp,
···285279 return nil, err
286280 }
287281288288- // Convert the artist string to the Artist array structure
282282+ // unmarshal artist json
289283 var artists []models.Artist
290284 err = json.Unmarshal([]byte(artistString), &artists)
291285 if err != nil {
+3-7
main.go
···2121func home(w http.ResponseWriter, r *http.Request) {
2222 w.Header().Set("Content-Type", "text/html")
23232424- // Check if user has an active session cookie
2424+ // check if user has an active session cookie
2525 cookie, err := r.Cookie("session")
2626 isLoggedIn := err == nil && cookie != nil
2727- // TODO: Add logic here to fetch user details from DB using session ID
2828- // to check if Spotify is already connected, if desired for finer control.
2929- // For now, we'll just check if *any* session exists.
2727+ // TODO: add logic here to fetch user details from DB using session ID
2828+ // to check if Spotify is already connected
30293130 html := `
3231 <html>
···106105107106// JSON API handlers
108107109109-// jsonResponse returns a JSON response
110108func jsonResponse(w http.ResponseWriter, statusCode int, data any) {
111109 w.Header().Set("Content-Type", "application/json")
112110 w.WriteHeader(statusCode)
···115113 }
116114}
117115118118-// API endpoint for current track
119116func apiCurrentTrack(spotifyService *spotify.SpotifyService) http.HandlerFunc {
120117 return func(w http.ResponseWriter, r *http.Request) {
121118 userID, ok := session.GetUserID(r.Context())
···134131 }
135132}
136133137137-// API endpoint for history
138134func apiTrackHistory(spotifyService *spotify.SpotifyService) http.HandlerFunc {
139135 return func(w http.ResponseWriter, r *http.Request) {
140136 userID, ok := session.GetUserID(r.Context())
-1
models/atproto.go
···11-// Add this struct definition to piper/models/atproto.go
21package models
3243import (
+19-14
models/user.go
···2233import "time"
4455-// User represents a user of the application
55+// an end user of piper
66type User struct {
77- ID int64
88- Username string
99- Email *string // Use pointer for nullable fields
1010- SpotifyID *string // Use pointer for nullable fields
1111- AccessToken *string // Spotify Access Token
1212- RefreshToken *string // Spotify Refresh Token
1313- TokenExpiry *time.Time // Spotify Token Expiry
1414- CreatedAt time.Time
1515- UpdatedAt time.Time
1616- ATProtoDID *string // ATProto DID
1717- ATProtoAccessToken *string // ATProto Access Token
1818- ATProtoRefreshToken *string // ATProto Refresh Token
1919- ATProtoTokenExpiry *time.Time // ATProto Token Expiry
77+ ID int64
88+ Username string
99+ Email *string
1010+1111+ // spotify information
1212+ SpotifyID *string
1313+ AccessToken *string
1414+ RefreshToken *string
1515+ TokenExpiry *time.Time
1616+1717+ // atp info
1818+ ATProtoDID *string
1919+ ATProtoAccessToken *string
2020+ ATProtoRefreshToken *string
2121+ ATProtoTokenExpiry *time.Time
2222+2323+ CreatedAt time.Time
2424+ UpdatedAt time.Time
2025}
+2-3
oauth/atproto/atproto.go
···11-// Modify piper/oauth/atproto/atproto.go
21package atproto
3243import (
···8887 return nil, fmt.Errorf("failed PAR request to %s: %w", ui.AuthServer, err)
8988 }
90899191- // Save state including generated PKCE verifier and DPoP key
9090+ // Save state
9291 data := &models.ATprotoAuthData{
9392 State: parResp.State,
9493 DID: ui.DID,
···171170 }
172171173172 log.Printf("ATProto Callback Success: User %d (DID: %s) authenticated.", userID.ID, data.DID)
174174- return userID.ID, nil // Return the piper user ID
173173+ return userID.ID, nil
175174}
+6-14
oauth/oauth2.go
···11-// Modify piper/oauth/oauth2.go
21package oauth
3243import (
···2221 state string
2322 codeVerifier string
2423 codeChallenge string
2525- // Added TokenReceiver field to handle user lookup/creation based on token
2624 tokenReceiver TokenReceiver
2725}
2826···3836 switch strings.ToLower(provider) {
3937 case "spotify":
4038 endpoint = spotify.Endpoint
4141- // Add other providers like Last.fm here
4239 default:
4343- // Placeholder for unconfigured providers
4040+ // placeholder
4441 log.Printf("Warning: OAuth2 provider '%s' not explicitly configured. Using placeholder endpoints.", provider)
4542 endpoint = oauth2.Endpoint{
4646- AuthURL: "https://example.com/auth", // Replace with actual endpoints if needed
4343+ AuthURL: "https://example.com/auth",
4744 TokenURL: "https://example.com/token",
4845 }
4946 }
···6259 state: GenerateRandomState(),
6360 codeVerifier: codeVerifier,
6461 codeChallenge: codeChallenge,
6565- tokenReceiver: tokenReceiver, // Store the token receiver
6262+ tokenReceiver: tokenReceiver,
6663 }
6764}
68656969-// generateCodeVerifier creates a random code verifier for PKCE
6666+// generate a random code verifier, for PKCE
7067func GenerateCodeVerifier() string {
7168 b := make([]byte, 64)
7269 rand.Read(b)
7370 return base64.RawURLEncoding.EncodeToString(b)
7471}
75727676-// generateCodeChallenge creates a code challenge from the code verifier using S256 method
7373+// generate a code challenge for verification later
7774func GenerateCodeChallenge(verifier string) string {
7875 h := sha256.New()
7976 h.Write([]byte(verifier))
8077 return base64.RawURLEncoding.EncodeToString(h.Sum(nil))
8178}
82798383-// HandleLogin implements the AuthService interface method.
8480func (o *OAuth2Service) HandleLogin(w http.ResponseWriter, r *http.Request) {
8581 opts := []oauth2.AuthCodeOption{
8682 oauth2.SetAuthURLParam("code_challenge", o.codeChallenge),
···128124129125 userId, hasSession := session.GetUserID(r.Context())
130126131131- // Use the token receiver to store the token and get the user ID
127127+ // store token and get uid
132128 userID, err := o.tokenReceiver.SetAccessToken(token.AccessToken, userId, hasSession)
133129 if err != nil {
134130 log.Printf("OAuth2 Callback Info: TokenReceiver did not return a valid user ID for token: %s...", token.AccessToken[:min(10, len(token.AccessToken))])
···138134 return userID, nil
139135}
140136141141-// GetToken remains unchanged
142137func (o *OAuth2Service) GetToken(code string) (*oauth2.Token, error) {
143138 opts := []oauth2.AuthCodeOption{
144139 oauth2.SetAuthURLParam("code_verifier", o.codeVerifier),
···146141 return o.config.Exchange(context.Background(), code, opts...)
147142}
148143149149-// GetClient remains unchanged
150144func (o *OAuth2Service) GetClient(token *oauth2.Token) *http.Client {
151145 return o.config.Client(context.Background(), token)
152146}
153147154154-// RefreshToken remains unchanged
155148func (o *OAuth2Service) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) {
156149 source := o.config.TokenSource(context.Background(), token)
157150 return oauth2.ReuseTokenSource(token, source).Token()
158151}
159152160160-// Helper function
161153func min(a, b int) int {
162154 if a < b {
163155 return a
+9-14
oauth/oauth_manager.go
···12121313// manages multiple oauth client services
1414type OAuthServiceManager struct {
1515- services map[string]AuthService // Changed from *OAuth2Service to AuthService interface
1515+ services map[string]AuthService
1616 sessionManager *session.SessionManager
1717 mu sync.RWMutex
1818}
19192020func NewOAuthServiceManager() *OAuthServiceManager {
2121 return &OAuthServiceManager{
2222- services: make(map[string]AuthService), // Initialize the new map
2222+ services: make(map[string]AuthService),
2323 sessionManager: session.NewSessionManager(),
2424 }
2525}
26262727-// RegisterService registers any service that implements the AuthService interface.
2727+// registers any service that impls AuthService
2828func (m *OAuthServiceManager) RegisterService(name string, service AuthService) {
2929 m.mu.Lock()
3030 defer m.mu.Unlock()
···3232 log.Printf("Registered auth service: %s", name)
3333}
34343535-// GetService retrieves a registered AuthService by name.
3535+// get an AuthService by registered name
3636func (m *OAuthServiceManager) GetService(name string) (AuthService, bool) {
3737 m.mu.RLock()
3838 defer m.mu.RUnlock()
···4747 m.mu.RUnlock()
48484949 if exists {
5050- service.HandleLogin(w, r) // Call interface method
5050+ service.HandleLogin(w, r)
5151 return
5252 }
5353···7070 return
7171 }
72727373- // Call the service's HandleCallback, which now returns the user ID
7474- userID, err := service.HandleCallback(w, r) // Call interface method
7373+ userID, err := service.HandleCallback(w, r)
75747675 if err != nil {
7776 log.Printf("Error handling callback for service '%s': %v", serviceName, err)
···8079 }
81808281 if userID > 0 {
8383- // Create session for the user
8482 session := m.sessionManager.CreateSession(userID)
85838686- // Set session cookie
8784 m.sessionManager.SetSessionCookie(w, session)
88858986 log.Printf("Created session for user %d via service %s", userID, serviceName)
90879191- // Redirect to homepage after successful login and session creation
9288 http.Redirect(w, r, "/", http.StatusSeeOther)
9389 } else {
9490 log.Printf("Callback for service '%s' did not result in a valid user ID.", serviceName)
9595- // Optionally redirect to an error page or show an error message
9696- // For now, just redirecting home, but this might hide errors.
9797- // Consider adding error handling based on why userID might be 0.
9898- http.Redirect(w, r, "/", http.StatusSeeOther) // Or redirect to a login/error page
9191+ // todo: redirect to an error page
9292+ // right now this just redirects home but we don't want this behaviour ideally
9393+ http.Redirect(w, r, "/", http.StatusSeeOther)
9994 }
10095 }
10196}
+6-10
oauth/service.go
···11-// Create piper/oauth/auth_service.go
21package oauth
3243import (
54 "net/http"
65)
7688-// AuthService defines the interface for different authentication services
99-// that can be managed by the OAuthServiceManager.
107type AuthService interface {
1111- // HandleLogin initiates the login flow for the specific service.
88+ // inits the login flow for the service
129 HandleLogin(w http.ResponseWriter, r *http.Request)
1313- // HandleCallback handles the callback from the authentication provider,
1414- // processes the response (e.g., exchanges code for token), finds or creates
1515- // the user in the local system, and returns the user ID.
1616- // Returns 0 if authentication failed or user could not be determined.
1010+ // handles the callback for the provider. is responsible for inserting
1111+ // sessions in the db
1712 HandleCallback(w http.ResponseWriter, r *http.Request) (int64, error)
1813}
19141515+// optional but recommended
2016type TokenReceiver interface {
2121- // SetAccessToken stores the access token for the user and returns the user ID.
2222- // If the user is already logged in, the current ID is provided.
1717+ // stores the access token in the db
1818+ // if there is a session, will associate the token with the session
2319 SetAccessToken(token string, currentId int64, hasSession bool) (int64, error)
2420}
+24-39
service/spotify/spotify.go
···2233import (
44 "encoding/json"
55+ "errors"
56 "fmt"
67 "io"
78 "log"
···3132}
32333334func (s *SpotifyService) SetAccessToken(token string, userId int64, hasSession bool) (int64, error) {
3434- // Identify the user synchronously instead of in a goroutine
3535 userID, err := s.identifyAndStoreUser(token, userId, hasSession)
3636 if err != nil {
3737 log.Printf("Error identifying and storing user: %v", err)
···4141}
42424343func (s *SpotifyService) identifyAndStoreUser(token string, userId int64, hasSession bool) (int64, error) {
4444- // Get Spotify user profile
4544 userProfile, err := s.fetchSpotifyProfile(token)
4645 if err != nil {
4746 log.Printf("Error fetching Spotify profile: %v", err)
···50495150 fmt.Printf("uid: %d hasSession: %t", userId, hasSession)
52515353- // Check if user exists
5452 user, err := s.DB.GetUserBySpotifyID(userProfile.ID)
5553 if err != nil {
5654 // This error might mean DB connection issue, not just user not found.
···7472 }
7573 }
7674 } else {
7777- // Update existing user's token and expiry
7875 err = s.DB.UpdateUserToken(user.ID, token, "", tokenExpiryTime)
7976 if err != nil {
7777+ // for now log and continue
8078 log.Printf("Error updating user token for user ID %d: %v", user.ID, err)
8181- // Consider if we should return 0 or the user ID even if update fails
8282- // Sticking to original behavior: log and continue
8379 } else {
8480 log.Printf("Updated token for existing user: %s (ID: %d)", user.Username, user.ID)
8581 }
8682 }
8787- // Keep the local 'user' object consistent (optional but good practice)
8883 user.AccessToken = &token
8984 user.TokenExpiry = &tokenExpiryTime
90859191- // Store token in memory cache regardless of new/existing user
9286 s.mu.Lock()
9387 s.userTokens[user.ID] = token
9488 s.mu.Unlock()
···10397 Email string `json:"email"`
10498}
10599106106-// LoadAllUsers loads all active users from the database into memory
107100func (s *SpotifyService) LoadAllUsers() error {
108101 users, err := s.DB.GetAllActiveUsers()
109102 if err != nil {
···115108116109 count := 0
117110 for _, user := range users {
118118- // Only load users with valid tokens
111111+ // load users with valid tokens
119112 if user.AccessToken != nil && user.TokenExpiry.After(time.Now()) {
120113 s.userTokens[user.ID] = *user.AccessToken
121114 count++
···126119 return nil
127120}
128121122122+func (s *SpotifyService) refreshTokenInner(user models.User) error {
123123+ // implement token refresh logic here using Spotify's token refresh endpoint
124124+ // this would make a request to Spotify's token endpoint with grant_type=refresh_token
125125+ return errors.New("Not implemented yet")
126126+ // if successful, update the database and in-memory cache
127127+}
128128+129129func (s *SpotifyService) RefreshToken(userID string) error {
130130 s.mu.Lock()
131131 defer s.mu.Unlock()
···139139 return fmt.Errorf("no refresh token for user %s", userID)
140140 }
141141142142- // Implement token refresh logic here using Spotify's token refresh endpoint
143143- // This would make a request to Spotify's token endpoint with grant_type=refresh_token
144144-145145- // If successful, update the database and in-memory cache
146146- // we won't be now so just error out
147147- return fmt.Errorf("token refresh not implemented")
148148- //
149149- //s.userTokens[user.ID] = newToken
150150- //return nil
142142+ return s.refreshTokenInner(*user)
151143}
152144153153-// RefreshExpiredTokens attempts to refresh expired tokens
145145+// attempt to refresh expired tokens
154146func (s *SpotifyService) RefreshExpiredTokens() {
155147 users, err := s.DB.GetUsersWithExpiredTokens()
156148 if err != nil {
···160152161153 refreshed := 0
162154 for _, user := range users {
163163- // Skip users without refresh tokens
155155+ // skip users without refresh tokens
164156 if user.RefreshToken == nil {
165157 continue
166158 }
167159168168- // Implement token refresh logic here using Spotify's token refresh endpoint
169169- // This would make a request to Spotify's token endpoint with grant_type=refresh_token
160160+ err := s.refreshTokenInner(*user)
161161+162162+ if err != nil {
163163+ // just print out errors here for now
164164+ log.Printf("Error from service/spotify/spotify.go when refreshing tokens: %s", err.Error())
165165+ }
170166171171- // If successful, update the database and in-memory cache
172167 refreshed++
173168 }
174169···231226 return
232227 }
233228234234- // Get recent tracks from database
235229 tracks, err := s.DB.GetRecentTracks(userID, 20)
236230 if err != nil {
237231 http.Error(w, "Error retrieving track history", http.StatusInternalServerError)
···252246 return nil, fmt.Errorf("no access token for user %d", userID)
253247 }
254248255255- // Call Spotify API to get currently playing track
256249 req, err := http.NewRequest("GET", "https://api.spotify.com/v1/me/player/currently-playing", nil)
257250 if err != nil {
258251 return nil, err
···266259 }
267260 defer resp.Body.Close()
268261269269- // No track playing
262262+ // nothing playing
270263 if resp.StatusCode == 204 {
271264 return nil, nil
272265 }
273266274274- // Token expired
267267+ // oops, token expired
275268 if resp.StatusCode == 401 {
276269 // attempt to refresh token
277270 if err := s.RefreshToken(strconv.FormatInt(userID, 10)); err != nil {
···282275 }
283276 }
284277285285- // Error response
286278 if resp.StatusCode != 200 {
287279 body, _ := io.ReadAll(resp.Body)
288280 return nil, fmt.Errorf("spotify API error: %s", body)
289281 }
290282291291- // Parse response
292283 var response struct {
293284 Item struct {
294285 Name string `json:"name"`
···320311 return nil, err
321312 }
322313323323- // Extract artist names/ids
324314 var artists []models.Artist
325315 for _, artist := range response.Item.Artists {
326316 artists = append(artists, models.Artist{
···329319 })
330320 }
331321332332- // Create Track model
322322+ // assemble Track
333323 track := &models.Track{
334324 Name: response.Item.Name,
335325 Artist: artists,
···351341 defer ticker.Stop()
352342353343 for range ticker.C {
354354- // Copy userIDs to avoid holding the lock too long
344344+ // copy userIDs to avoid holding the lock too long
355345 s.mu.RLock()
356346 userIDs := make([]int64, 0, len(s.userTokens))
357347 for userID := range s.userTokens {
···359349 }
360350 s.mu.RUnlock()
361351362362- // Check each user's currently playing track
363352 for _, userID := range userIDs {
364353 track, err := s.FetchCurrentTrack(userID)
365354 if err != nil {
···367356 continue
368357 }
369358370370- // No change if no track is playing
371359 if track == nil {
372360 continue
373361 }
374362375375- // Check if this is a new track
376363 s.mu.RLock()
377364 currentTrack := s.userTracks[userID]
378365 s.mu.RUnlock()
···384371 }
385372 }
386373387387- // If track is different or we've played more than either half of the track or 30 seconds since the start
388388- // whichever is greater
374374+ // if flagged true, we have a new track
389375 isNewTrack := currentTrack == nil ||
390376 currentTrack.Name != track.Name ||
391377 // just check the first one for now
···426412 }
427413428414 if isNewTrack {
429429- // Save to database
430415 id, err := s.DB.SaveTrack(userID, track)
431416 if err != nil {
432417 log.Printf("Error saving track for user %d: %v", userID, err)
+10-33
session/session.go
···3131 mu sync.RWMutex
3232}
33333434-// NewSessionManager creates a new session manager
3534func NewSessionManager() *SessionManager {
3636- // Initialize session table if it doesn't exist
3735 database, err := db.New("./data/piper.db")
3836 if err != nil {
3937 log.Printf("Error connecting to database for sessions, falling back to in memory only: %v", err)
···5654 log.Printf("Error creating sessions table: %v", err)
5755 }
58565959- // Create API key manager
6057 apiKeyMgr := apikey.NewApiKeyManager(database)
61586259 return &SessionManager{
···120117 return session, true
121118 }
122119123123- // If not in memory and we have a database, check there
120120+ // if not in memory and we have a database, check there
124121 if sm.db != nil {
125122 session = &Session{ID: sessionID}
126123···189186 http.SetCookie(w, cookie)
190187}
191188192192-// HandleLogout handles user logout
193189func (sm *SessionManager) HandleLogout(w http.ResponseWriter, r *http.Request) {
194190 cookie, err := r.Cookie("session")
195191 if err == nil {
···201197 http.Redirect(w, r, "/", http.StatusSeeOther)
202198}
203199204204-// GetAPIKeyManager returns the API key manager
205200func (sm *SessionManager) GetAPIKeyManager() *apikey.ApiKeyManager {
206201 return sm.apiKeyMgr
207202}
208203209209-// CreateAPIKey creates a new API key for a user
210204func (sm *SessionManager) CreateAPIKey(userID int64, name string, validityDays int) (*apikey.ApiKey, error) {
211205 return sm.apiKeyMgr.CreateApiKey(userID, name, validityDays)
212206}
213207214214-// WithAuth is a middleware that checks if a user is authenticated via cookies or API key
208208+// middleware that checks if a user is authenticated via cookies or API key
215209func WithAuth(handler http.HandlerFunc, sm *SessionManager) http.HandlerFunc {
216210 return func(w http.ResponseWriter, r *http.Request) {
217217- // First try API key authentication (for API requests)
211211+ // first: check API keys
218212 apiKeyStr, apiKeyErr := apikey.ExtractApiKey(r)
219213 if apiKeyErr == nil && apiKeyStr != "" {
220220- // Validate API key
221214 apiKey, valid := sm.apiKeyMgr.GetApiKey(apiKeyStr)
222215 if valid {
223223- // Add user ID to context
224216 ctx := WithUserID(r.Context(), apiKey.UserID)
225217 r = r.WithContext(ctx)
226218227227- // Set a flag in the context that this is an API request
219219+ // set a flag for api requests
228220 ctx = WithAPIRequest(r.Context(), true)
229221 r = r.WithContext(ctx)
230222···233225 }
234226 }
235227236236- // Fall back to cookie authentication (for browser requests)
228228+ // if not found, check cookies for session value
237229 cookie, err := r.Cookie("session")
238230 if err != nil {
239231 http.Redirect(w, r, "/login/spotify", http.StatusSeeOther)
240232 return
241233 }
242234243243- // Verify cookie session
244235 session, exists := sm.GetSession(cookie.Value)
245236 if !exists {
246237 http.Redirect(w, r, "/login/spotify", http.StatusSeeOther)
247238 return
248239 }
249240250250- // Add session information to request context
251241 ctx := WithUserID(r.Context(), session.UserID)
252242 r = r.WithContext(ctx)
253243···255245 }
256246}
257247248248+// middleware that checks if a user is authenticated but doesn't error out if not
258249func WithPossibleAuth(handler http.HandlerFunc, sm *SessionManager) http.HandlerFunc {
259250 return func(w http.ResponseWriter, r *http.Request) {
260251 ctx := r.Context()
261261- authenticated := false // Default to not authenticated
252252+ authenticated := false
262253263263- // 1. Try API key authentication
264254 apiKeyStr, apiKeyErr := apikey.ExtractApiKey(r)
265255 if apiKeyErr == nil && apiKeyStr != "" {
266256 apiKey, valid := sm.apiKeyMgr.GetApiKey(apiKeyStr)
267257 if valid {
268268- // API Key valid: Add UserID, API flag, and set auth status
269258 ctx = WithUserID(ctx, apiKey.UserID)
270259 ctx = WithAPIRequest(ctx, true)
271260 authenticated = true
272272- // Update request context and call handler
273261 r = r.WithContext(WithAuthStatus(ctx, authenticated))
274262 handler(w, r)
275263 return
276264 }
277277- // If API key was provided but invalid, we still proceed without auth
278265 }
279266280280- // 2. If no valid API key, try cookie authentication
281281- if !authenticated { // Only check cookies if API key didn't authenticate
267267+ if !authenticated {
282268 cookie, err := r.Cookie("session")
283283- if err == nil { // Cookie exists
269269+ if err == nil {
284270 session, exists := sm.GetSession(cookie.Value)
285271 if exists {
286286- // Session valid: Add UserID and set auth status
287272 ctx = WithUserID(ctx, session.UserID)
288288- // ctx = WithAPIRequest(ctx, false) // Not strictly needed, default is false
289273 authenticated = true
290274 }
291291- // If session cookie exists but is invalid/expired, we proceed without auth
292275 }
293276 }
294277295295- // 3. Set final auth status (could be true or false) and call handler
296278 r = r.WithContext(WithAuthStatus(ctx, authenticated))
297279 handler(w, r)
298280 }
299281}
300282301301-// WithAPIAuth is a middleware specifically for API-only endpoints (no cookie fallback, returns 401 instead of redirect)
283283+// middleware that only accepts API keys
302284func WithAPIAuth(handler http.HandlerFunc, sm *SessionManager) http.HandlerFunc {
303285 return func(w http.ResponseWriter, r *http.Request) {
304304- // Try API key authentication
305286 apiKeyStr, apiKeyErr := apikey.ExtractApiKey(r)
306287 if apiKeyErr != nil || apiKeyStr == "" {
307288 w.Header().Set("Content-Type", "application/json")
···310291 return
311292 }
312293313313- // Validate API key
314294 apiKey, valid := sm.apiKeyMgr.GetApiKey(apiKeyStr)
315295 if !valid {
316296 w.Header().Set("Content-Type", "application/json")
···319299 return
320300 }
321301322322- // Add user ID to context
323302 ctx := WithUserID(r.Context(), apiKey.UserID)
324324- // Mark as API request
325303 ctx = WithAPIRequest(ctx, true)
326304 r = r.WithContext(ctx)
327305···329307 }
330308}
331309332332-// Context keys
333310type contextKey int
334311335312const (