// Package api provides the API endpoints for the gpodder API package api import ( "crypto/rand" "database/sql" "encoding/base64" "fmt" "log" "net/http" "strings" "time" "pinepods/gpodder-api/internal/db" "github.com/alexedwards/argon2id" "github.com/fernet/fernet-go" "github.com/gin-gonic/gin" ) // Define the parameters we use for Argon2id type argon2Params struct { memory uint32 iterations uint32 parallelism uint8 saltLength uint32 keyLength uint32 } // AuthMiddleware creates a middleware for authentication func AuthMiddleware(db *db.PostgresDB) gin.HandlerFunc { return func(c *gin.Context) { // Get the username from the URL parameters username := c.Param("username") if username == "" { log.Printf("[ERROR] AuthMiddleware: Username parameter is missing in path") c.JSON(http.StatusBadRequest, gin.H{"error": "Username is required"}) c.Abort() return } // Check if this is an internal API call via X-GPodder-Token gpodderTokenHeader := c.GetHeader("X-GPodder-Token") if gpodderTokenHeader != "" { // Get user data var userID int var gpodderToken sql.NullString var podSyncType string err := db.QueryRow(` SELECT UserID, GpodderToken, Pod_Sync_Type FROM "Users" WHERE LOWER(Username) = LOWER($1) `, username).Scan(&userID, &gpodderToken, &podSyncType) if err != nil { log.Printf("[ERROR] AuthMiddleware: Database error: %v", err) c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or token"}) c.Abort() return } // Check if gpodder sync is enabled if podSyncType != "gpodder" && podSyncType != "both" && podSyncType != "external" { log.Printf("[ERROR] AuthMiddleware: Gpodder API not enabled for user: %s (sync type: %s)", username, podSyncType) c.JSON(http.StatusForbidden, gin.H{"error": "Gpodder API not enabled for this user"}) c.Abort() return } // For internal calls with X-GPodder-Token header, validate token directly if gpodderToken.Valid && gpodderToken.String == gpodderTokenHeader { c.Set("userID", userID) c.Set("username", username) c.Next() return } // If token doesn't match, authentication failed log.Printf("[ERROR] AuthMiddleware: Invalid X-GPodder-Token for user: %s", username) c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) c.Abort() return } // If no token header found, proceed with standard authentication authHeader := c.GetHeader("Authorization") if authHeader == "" { log.Printf("[ERROR] AuthMiddleware: Authorization header is missing") c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"}) c.Abort() return } // Extract credentials parts := strings.Split(authHeader, " ") if len(parts) != 2 || parts[0] != "Basic" { log.Printf("[ERROR] AuthMiddleware: Invalid Authorization header format") c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization header format"}) c.Abort() return } // Decode credentials decoded, err := base64.StdEncoding.DecodeString(parts[1]) if err != nil { log.Printf("[ERROR] AuthMiddleware: Failed to decode base64 credentials: %v", err) c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization header"}) c.Abort() return } // Extract username and password credentials := strings.SplitN(string(decoded), ":", 2) if len(credentials) != 2 { log.Printf("[ERROR] AuthMiddleware: Invalid credentials format") c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials format"}) c.Abort() return } authUsername := credentials[0] password := credentials[1] // Check username match if strings.ToLower(username) != strings.ToLower(authUsername) { log.Printf("[ERROR] AuthMiddleware: Username mismatch - URL: %s, Auth: %s", strings.ToLower(username), strings.ToLower(authUsername)) c.JSON(http.StatusUnauthorized, gin.H{"error": "Username mismatch"}) c.Abort() return } // Query user data var userID int var hashedPassword string var podSyncType string var gpodderToken sql.NullString err = db.QueryRow(` SELECT UserID, Hashed_PW, Pod_Sync_Type, GpodderToken FROM "Users" WHERE LOWER(Username) = LOWER($1) `, username).Scan(&userID, &hashedPassword, &podSyncType, &gpodderToken) if err != nil { if err == sql.ErrNoRows { log.Printf("[ERROR] AuthMiddleware: User not found: %s", username) c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"}) } else { log.Printf("[ERROR] AuthMiddleware: Database error: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) } c.Abort() return } // Check if gpodder sync is enabled if podSyncType != "gpodder" && podSyncType != "both" && podSyncType != "external" { log.Printf("[ERROR] AuthMiddleware: Gpodder API not enabled for user: %s (sync type: %s)", username, podSyncType) c.JSON(http.StatusForbidden, gin.H{"error": "Gpodder API not enabled for this user"}) c.Abort() return } // Flag to track authentication success authenticated := false // Check if this is a gpodder token authentication // Check if this is a gpodder token authentication if gpodderToken.Valid && (gpodderToken.String == password || gpodderToken.String == gpodderTokenHeader) { authenticated = true } // If token auth didn't succeed, try password authentication if !authenticated && verifyPassword(password, hashedPassword) { authenticated = true } // If authentication was successful, set context and continue if authenticated { c.Set("userID", userID) c.Set("username", username) c.Next() return } // Authentication failed log.Printf("[ERROR] AuthMiddleware: Invalid credentials for user: %s", username) c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"}) c.Abort() } } // Helper function to decrypt token func decryptToken(encryptionKey []byte, encryptedToken string) (string, error) { // Ensure the encryptionKey is correctly formatted for fernet // Fernet requires a 32-byte key encoded in base64 keyStr := base64.StdEncoding.EncodeToString(encryptionKey) // Parse the key key, err := fernet.DecodeKey(keyStr) if err != nil { return "", fmt.Errorf("failed to decode key: %w", err) } // Decrypt the token token := []byte(encryptedToken) msg := fernet.VerifyAndDecrypt(token, 0, []*fernet.Key{key}) if msg == nil { return "", fmt.Errorf("failed to decrypt token or token invalid") } return string(msg), nil } // generateSessionToken generates a random token for sessions func generateSessionToken() (string, error) { b := make([]byte, 32) _, err := rand.Read(b) if err != nil { return "", err } return base64.URLEncoding.EncodeToString(b), nil } // createSession creates a new session in the database func createSession(db *db.Database, userID int, userAgent, clientIP string) (string, time.Time, error) { // Generate a random session token token, err := generateSessionToken() if err != nil { return "", time.Time{}, fmt.Errorf("failed to generate session token: %w", err) } // Set expiration time (30 days from now) expires := time.Now().Add(30 * 24 * time.Hour) // Insert session into database _, err = db.Exec(` INSERT INTO "GpodderSessions" (UserID, SessionToken, ExpiresAt, UserAgent, ClientIP) VALUES ($1, $2, $3, $4, $5) `, userID, token, expires, userAgent, clientIP) if err != nil { return "", time.Time{}, fmt.Errorf("failed to create session: %w", err) } return token, expires, nil } // validateSession validates a session token func validateSession(db *db.Database, token string) (int, bool, error) { var userID int var expires time.Time var query string // Format query according to database type if db.IsPostgreSQLDB() { query = `SELECT UserID, ExpiresAt FROM "GpodderSessions" WHERE SessionToken = $1` } else { query = `SELECT UserID, ExpiresAt FROM GpodderSessions WHERE SessionToken = ?` } err := db.QueryRow(query, token).Scan(&userID, &expires) if err != nil { if err == sql.ErrNoRows { return 0, false, nil // Session not found } return 0, false, fmt.Errorf("error validating session: %w", err) } // Check if session has expired if time.Now().After(expires) { // Delete expired session if db.IsPostgreSQLDB() { query = `DELETE FROM "GpodderSessions" WHERE SessionToken = $1` } else { query = `DELETE FROM GpodderSessions WHERE SessionToken = ?` } _, err = db.Exec(query, token) if err != nil { log.Printf("Failed to delete expired session: %v", err) } return 0, false, nil } // Update last active time if db.IsPostgreSQLDB() { query = `UPDATE "GpodderSessions" SET LastActive = CURRENT_TIMESTAMP WHERE SessionToken = $1` } else { query = `UPDATE GpodderSessions SET LastActive = CURRENT_TIMESTAMP WHERE SessionToken = ?` } _, err = db.Exec(query, token) if err != nil { log.Printf("Failed to update session last active time: %v", err) } return userID, true, nil } // deleteSession removes a session from the database func deleteSession(db *db.Database, token string) error { _, err := db.Exec(`DELETE FROM "GpodderSessions" WHERE SessionToken = $1`, token) if err != nil { return fmt.Errorf("failed to delete session: %w", err) } return nil } // deleteUserSessions removes all sessions for a user func deleteUserSessions(db *db.PostgresDB, userID int) error { _, err := db.Exec(`DELETE FROM "GpodderSessions" WHERE UserID = $1`, userID) if err != nil { return fmt.Errorf("failed to delete user sessions: %w", err) } return nil } // handleLogin enhanced with session management func handleLogin(database *db.Database) gin.HandlerFunc { return func(c *gin.Context) { // Use the AuthMiddleware to authenticate the user username := c.Param("username") if username == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Username is required"}) return } // Get the Authorization header authHeader := c.GetHeader("Authorization") if authHeader == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"}) return } // Check if the Authorization header is in the correct format parts := strings.Split(authHeader, " ") if len(parts) != 2 || parts[0] != "Basic" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization header format"}) return } // Decode the base64-encoded credentials decoded, err := base64.StdEncoding.DecodeString(parts[1]) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization header"}) return } // Extract username and password credentials := strings.SplitN(string(decoded), ":", 2) if len(credentials) != 2 { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials format"}) return } authUsername := credentials[0] password := credentials[1] // Verify that the username in the URL matches the one in the Authorization header if strings.ToLower(username) != strings.ToLower(authUsername) { c.JSON(http.StatusUnauthorized, gin.H{"error": "Username mismatch"}) return } // Check if the user exists and the password is correct var userID int var hashedPassword string var podSyncType string // Make sure to use case-insensitive username lookup err = database.QueryRow(` SELECT UserID, Hashed_PW, Pod_Sync_Type FROM "Users" WHERE LOWER(Username) = LOWER($1) `, username).Scan(&userID, &hashedPassword, &podSyncType) if err != nil { if err == sql.ErrNoRows { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"}) } else { log.Printf("Database error during login: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) } return } // Check if gpodder sync is enabled for this user if podSyncType != "gpodder" && podSyncType != "both" { c.JSON(http.StatusForbidden, gin.H{"error": "Gpodder API not enabled for this user"}) return } // Verify password using Pinepods' Argon2 password method if !verifyPassword(password, hashedPassword) { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"}) return } // Create a new session userAgent := c.Request.UserAgent() clientIP := c.ClientIP() sessionToken, expiresAt, err := createSession(database, userID, userAgent, clientIP) if err != nil { log.Printf("Failed to create session: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create session"}) return } log.Printf("[DEBUG] handleLogin: Login successful for user: %s, created session token (first 8 chars): %s...", username, sessionToken[:8]) // Set session cookie c.SetCookie( "sessionid", // name sessionToken, // value int(30*24*time.Hour.Seconds()), // max age in seconds (30 days) "/", // path "", // domain (empty = current domain) c.Request.TLS != nil, // secure (HTTPS only) true, // httpOnly (not accessible via JavaScript) ) log.Printf("[DEBUG] handleLogin: Sending response with session expiry: %s", expiresAt.Format(time.RFC3339)) // Return success with info c.JSON(http.StatusOK, gin.H{ "status": "success", "userid": userID, "username": username, "session_expires": expiresAt.Format(time.RFC3339), }) } } // handleLogout enhanced with session management func handleLogout(database *db.Database) gin.HandlerFunc { return func(c *gin.Context) { // Get username from URL username := c.Param("username") if username == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Username is required"}) return } // Get the session cookie sessionToken, err := c.Cookie("sessionid") if err != nil || sessionToken == "" { // No session cookie, just return success (idempotent operation) c.JSON(http.StatusOK, gin.H{ "status": "logged out", }) return } // Delete the session err = deleteSession(database, sessionToken) if err != nil { log.Printf("Error deleting session: %v", err) // Continue anyway - we still want to invalidate the cookie } // Clear the session cookie c.SetCookie( "sessionid", // name "", // value (empty = delete) -1, // max age (negative = delete) "/", // path "", // domain c.Request.TLS != nil, // secure true, // httpOnly ) c.JSON(http.StatusOK, gin.H{ "status": "logged out", }) } } // SessionMiddleware checks if a user is logged in via session func SessionMiddleware(database *db.Database) gin.HandlerFunc { return func(c *gin.Context) { log.Printf("[DEBUG] SessionMiddleware processing request: %s %s", c.Request.Method, c.Request.URL.Path) // First, try to get user from Authorization header for direct API access authHeader := c.GetHeader("Authorization") if authHeader != "" { log.Printf("[DEBUG] SessionMiddleware: Authorization header found, passing to next middleware") c.Next() return } // No Authorization header, check for session cookie sessionToken, err := c.Cookie("sessionid") if err != nil || sessionToken == "" { log.Printf("[ERROR] SessionMiddleware: No session cookie found: %v", err) c.JSON(http.StatusUnauthorized, gin.H{"error": "Not logged in"}) c.Abort() return } log.Printf("[DEBUG] SessionMiddleware: Found session cookie, validating") // Validate the session userID, valid, err := validateSession(database, sessionToken) if err != nil { log.Printf("[ERROR] SessionMiddleware: Error validating session: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Session error"}) c.Abort() return } if !valid { log.Printf("[ERROR] SessionMiddleware: Invalid or expired session") // Clear the invalid cookie c.SetCookie("sessionid", "", -1, "/", "", c.Request.TLS != nil, true) c.JSON(http.StatusUnauthorized, gin.H{"error": "Session expired"}) c.Abort() return } log.Printf("[DEBUG] SessionMiddleware: Session valid for userID: %d", userID) // Get the username for the user ID var username string err = database.QueryRow(`SELECT Username FROM "Users" WHERE UserID = $1`, userID).Scan(&username) if err != nil { log.Printf("[ERROR] SessionMiddleware: Error getting username for userID %d: %v", userID, err) c.JSON(http.StatusInternalServerError, gin.H{"error": "User data error"}) c.Abort() return } // Check if gpodder sync is enabled for this user var podSyncType string err = database.QueryRow(`SELECT Pod_Sync_Type FROM "Users" WHERE UserID = $1`, userID).Scan(&podSyncType) if err != nil { log.Printf("[ERROR] SessionMiddleware: Error checking sync type for userID %d: %v", userID, err) c.JSON(http.StatusInternalServerError, gin.H{"error": "User data error"}) c.Abort() return } if podSyncType != "gpodder" && podSyncType != "both" { log.Printf("[ERROR] SessionMiddleware: Gpodder API not enabled for user: %s (sync type: %s)", username, podSyncType) c.JSON(http.StatusForbidden, gin.H{"error": "Gpodder API not enabled for this user"}) c.Abort() return } // Set the user information in the context c.Set("userID", userID) c.Set("username", username) // Check if the path username matches the session username pathUsername := c.Param("username") if pathUsername != "" && strings.ToLower(pathUsername) != strings.ToLower(username) { log.Printf("[ERROR] SessionMiddleware: Username mismatch - Path: %s, Session: %s", pathUsername, username) c.JSON(http.StatusForbidden, gin.H{"error": "Username mismatch"}) c.Abort() return } log.Printf("[DEBUG] SessionMiddleware: Session authentication successful for user: %s", username) c.Next() } } // AuthenticationMiddleware with GPodder token handling func AuthenticationMiddleware(database *db.Database) gin.HandlerFunc { return func(c *gin.Context) { log.Printf("[DEBUG] AuthenticationMiddleware processing request: %s %s", c.Request.Method, c.Request.URL.Path) // Handle GPodder API standard .json suffix patterns if strings.HasSuffix(c.Request.URL.Path, ".json") { parts := strings.Split(c.Request.URL.Path, "/") var username string // Handle /episodes/username.json pattern if strings.Contains(c.Request.URL.Path, "/episodes/") && len(parts) >= 3 { usernameWithExt := parts[len(parts)-1] username = strings.TrimSuffix(usernameWithExt, ".json") log.Printf("[DEBUG] AuthenticationMiddleware: Extracted username '%s' from episode actions URL", username) } // Handle /devices/username.json pattern if strings.Contains(c.Request.URL.Path, "/devices/") { for i, part := range parts { if part == "devices" && i+1 < len(parts) { usernameWithExt := parts[i+1] username = strings.TrimSuffix(usernameWithExt, ".json") log.Printf("[DEBUG] AuthenticationMiddleware: Extracted username '%s' from devices URL", username) break } } } // Set username parameter if extracted if username != "" { c.Params = append(c.Params, gin.Param{Key: "username", Value: username}) } } // First try session auth sessionToken, err := c.Cookie("sessionid") if err == nil && sessionToken != "" { log.Printf("[DEBUG] AuthenticationMiddleware: Found session cookie, validating") userID, valid, err := validateSession(database, sessionToken) if err == nil && valid { log.Printf("[DEBUG] AuthenticationMiddleware: Session valid for userID: %d", userID) var username string var query string // Format query according to database type if database.IsPostgreSQLDB() { query = `SELECT Username FROM "Users" WHERE UserID = $1` } else { query = `SELECT Username FROM Users WHERE UserID = ?` } err = database.QueryRow(query, userID).Scan(&username) if err == nil { var podSyncType string // Format query according to database type if database.IsPostgreSQLDB() { query = `SELECT Pod_Sync_Type FROM "Users" WHERE UserID = $1` } else { query = `SELECT Pod_Sync_Type FROM Users WHERE UserID = ?` } err = database.QueryRow(query, userID).Scan(&podSyncType) if err == nil && (podSyncType == "gpodder" || podSyncType == "both") { // Check if the path username matches the session username pathUsername := c.Param("username") if pathUsername == "" || strings.ToLower(pathUsername) == strings.ToLower(username) { log.Printf("[DEBUG] AuthenticationMiddleware: Session auth successful for user: %s", username) c.Set("userID", userID) c.Set("username", username) c.Next() return } else { log.Printf("[ERROR] AuthenticationMiddleware: Session username mismatch - Path: %s, Session: %s", pathUsername, username) } } else { log.Printf("[ERROR] AuthenticationMiddleware: Gpodder not enabled for user: %s", username) } } else { log.Printf("[ERROR] AuthenticationMiddleware: Could not get username for userID %d: %v", userID, err) } } else { log.Printf("[ERROR] AuthenticationMiddleware: Invalid session: %v", err) } } else { log.Printf("[DEBUG] AuthenticationMiddleware: No session cookie, falling back to basic auth") } // Try basic auth if session auth failed log.Printf("[DEBUG] AuthenticationMiddleware: Attempting basic auth") username := c.Param("username") if username == "" { log.Printf("[ERROR] AuthenticationMiddleware: Username parameter is missing in path") c.JSON(http.StatusBadRequest, gin.H{"error": "Username is required"}) return } // Check if this is an internal API call via X-GPodder-Token gpodderTokenHeader := c.GetHeader("X-GPodder-Token") if gpodderTokenHeader != "" { log.Printf("[DEBUG] AuthenticationMiddleware: Found X-GPodder-Token header") // Get user data var userID int var gpodderToken sql.NullString var podSyncType string var query string // Format query according to database type if database.IsPostgreSQLDB() { query = `SELECT UserID, GpodderToken, Pod_Sync_Type FROM "Users" WHERE LOWER(Username) = LOWER($1)` } else { query = `SELECT UserID, GpodderToken, Pod_Sync_Type FROM Users WHERE LOWER(Username) = LOWER(?)` } err := database.QueryRow(query, username).Scan(&userID, &gpodderToken, &podSyncType) if err != nil { log.Printf("[ERROR] AuthenticationMiddleware: Database error: %v", err) c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or token"}) return } // Check if gpodder sync is enabled if podSyncType != "gpodder" && podSyncType != "both" && podSyncType != "external" { log.Printf("[ERROR] AuthenticationMiddleware: Gpodder API not enabled for user: %s (sync type: %s)", username, podSyncType) c.JSON(http.StatusForbidden, gin.H{"error": "Gpodder API not enabled for this user"}) return } // For internal calls with X-GPodder-Token header, validate token directly if gpodderToken.Valid && gpodderToken.String == gpodderTokenHeader { log.Printf("[DEBUG] AuthenticationMiddleware: X-GPodder-Token validated for user: %s", username) c.Set("userID", userID) c.Set("username", username) c.Next() return } // If token doesn't match, authentication failed log.Printf("[ERROR] AuthenticationMiddleware: Invalid X-GPodder-Token for user: %s", username) c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) return } // Standard basic auth handling authHeader := c.GetHeader("Authorization") if authHeader == "" { log.Printf("[ERROR] AuthenticationMiddleware: No Authorization header found") c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"}) return } parts := strings.Split(authHeader, " ") if len(parts) != 2 || parts[0] != "Basic" { log.Printf("[ERROR] AuthenticationMiddleware: Invalid Authorization header format") c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization header format"}) return } decoded, err := base64.StdEncoding.DecodeString(parts[1]) if err != nil { log.Printf("[ERROR] AuthenticationMiddleware: Failed to decode base64 credentials: %v", err) c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization header"}) return } credentials := strings.SplitN(string(decoded), ":", 2) if len(credentials) != 2 { log.Printf("[ERROR] AuthenticationMiddleware: Invalid credentials format") c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials format"}) return } authUsername := credentials[0] password := credentials[1] if strings.ToLower(username) != strings.ToLower(authUsername) { log.Printf("[ERROR] AuthenticationMiddleware: Username mismatch - URL: %s, Auth: %s", strings.ToLower(username), strings.ToLower(authUsername)) c.JSON(http.StatusUnauthorized, gin.H{"error": "Username mismatch"}) return } var userID int var hashedPassword string var podSyncType string var gpodderToken sql.NullString var query string // Format query according to database type if database.IsPostgreSQLDB() { query = `SELECT UserID, Hashed_PW, Pod_Sync_Type, GpodderToken FROM "Users" WHERE LOWER(Username) = LOWER($1)` } else { query = `SELECT UserID, Hashed_PW, Pod_Sync_Type, GpodderToken FROM Users WHERE LOWER(Username) = LOWER(?)` } err = database.QueryRow(query, username).Scan(&userID, &hashedPassword, &podSyncType, &gpodderToken) if err != nil { if err == sql.ErrNoRows { log.Printf("[ERROR] AuthenticationMiddleware: User not found: %s", username) c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"}) } else { log.Printf("[ERROR] AuthenticationMiddleware: Database error: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) } return } if podSyncType != "gpodder" && podSyncType != "both" && podSyncType != "external" { log.Printf("[ERROR] AuthenticationMiddleware: Gpodder API not enabled for user: %s (sync type: %s)", username, podSyncType) c.JSON(http.StatusForbidden, gin.H{"error": "Gpodder API not enabled for this user"}) return } // Flag to track authentication success authenticated := false // Check if this is a gpodder token authentication if gpodderToken.Valid && gpodderToken.String == password { log.Printf("[DEBUG] AuthenticationMiddleware: User authenticated with gpodder token: %s", username) authenticated = true } // If token auth didn't succeed, try password authentication if !authenticated && verifyPassword(password, hashedPassword) { log.Printf("[DEBUG] AuthenticationMiddleware: User authenticated with password: %s", username) authenticated = true } // If authentication was successful, set context and continue if authenticated { c.Set("userID", userID) c.Set("username", username) c.Next() return } // Authentication failed log.Printf("[ERROR] AuthenticationMiddleware: Invalid credentials for user: %s", username) c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"}) } } // verifyPassword verifies a password against a hash using Argon2 // This implementation matches the Pinepods authentication mechanism using alexedwards/argon2id func verifyPassword(password, hashedPassword string) bool { // Use the alexedwards/argon2id package to compare password and hash match, err := argon2id.ComparePasswordAndHash(password, hashedPassword) if err != nil { // Log the error in a production environment return false } return match }