Files
TermorServer/internal/httpapi/middleware.go
benya f8880aa9a4 feat: add scan status and cover art endpoints
Track scanner status for the web API and Subsonic-compatible scan endpoints, add authenticated cover art serving, and wire album artwork into the web UI. Keep Subsonic auth limited to legacy password mode for now so behavior stays honest with the current bcrypt-based user storage.
2026-04-02 22:37:10 +03:00

113 lines
2.8 KiB
Go

package httpapi
import (
"context"
"log"
"net/http"
"strings"
"time"
"github.com/benya/temporserv/internal/auth"
)
type contextKey string
const currentUserKey contextKey = "currentUser"
func requestLogger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
startedAt := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(startedAt))
})
}
func recoverer(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if recover() != nil {
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
func cors(origins string) func(http.Handler) http.Handler {
allowed := strings.Split(origins, ",")
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
for _, candidate := range allowed {
if strings.TrimSpace(candidate) == origin {
w.Header().Set("Access-Control-Allow-Origin", origin)
break
}
}
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
}
func (a app) requireAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, err := a.auth.CurrentUser(r.Context(), r.Header.Get("Authorization"))
if err != nil {
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
return
}
ctx := context.WithValue(r.Context(), currentUserKey, user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func currentUserFromContext(r *http.Request) auth.User {
user, ok := r.Context().Value(currentUserKey).(auth.User)
if !ok {
return auth.User{}
}
return user
}
func (a app) requireSubsonicAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, err := a.auth.CurrentUserBySubsonicAuth(
r.Context(),
r.URL.Query().Get("u"),
r.URL.Query().Get("p"),
r.URL.Query().Get("t"),
r.URL.Query().Get("s"),
)
if err != nil {
writeJSON(w, http.StatusUnauthorized, map[string]any{
"subsonic-response": map[string]any{
"status": "failed",
"version": "1.16.1",
"type": "temporserv",
"serverVersion": "0.1.0",
"openSubsonic": true,
"error": map[string]any{
"code": 40,
"message": "Wrong username or password",
},
},
})
return
}
ctx := context.WithValue(r.Context(), currentUserKey, user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}