feat: add sqlite-backed auth and library services
Bootstrap SQLite on server startup with embedded migrations and development seed data. Replace placeholder auth and library responses with database-backed services, bearer sessions, and repository-driven API handlers.
This commit is contained in:
@@ -1,12 +1,19 @@
|
||||
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()
|
||||
@@ -51,3 +58,24 @@ func cors(origins string) func(http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -14,7 +16,17 @@ import (
|
||||
"github.com/benya/temporserv/internal/subsonic"
|
||||
)
|
||||
|
||||
func NewRouter(cfg config.Config) http.Handler {
|
||||
type app struct {
|
||||
auth *auth.Service
|
||||
library *library.Service
|
||||
}
|
||||
|
||||
func NewRouter(cfg config.Config, database *sql.DB) http.Handler {
|
||||
application := app{
|
||||
auth: auth.NewService(database),
|
||||
library: library.NewService(database),
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Use(requestLogger)
|
||||
r.Use(recoverer)
|
||||
@@ -29,22 +41,13 @@ func NewRouter(cfg config.Config) http.Handler {
|
||||
})
|
||||
|
||||
r.Route("/api", func(api chi.Router) {
|
||||
api.Get("/me", func(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, auth.DemoUser())
|
||||
})
|
||||
api.Get("/home", func(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, library.DemoHome())
|
||||
})
|
||||
api.Get("/tracks", func(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"items": library.DemoTracks(),
|
||||
})
|
||||
})
|
||||
api.Post("/auth/login", func(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"token": "dev-token",
|
||||
"user": auth.DemoUser(),
|
||||
})
|
||||
api.Post("/auth/login", application.login)
|
||||
|
||||
api.Group(func(private chi.Router) {
|
||||
private.Use(application.requireAuth)
|
||||
private.Get("/me", application.me)
|
||||
private.Get("/home", application.home)
|
||||
private.Get("/tracks", application.tracks)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -55,6 +58,8 @@ func NewRouter(cfg config.Config) http.Handler {
|
||||
rest.Get("/getLicense.view", func(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, subsonic.PingResponse())
|
||||
})
|
||||
rest.Get("/getArtists.view", application.subsonicArtists)
|
||||
rest.Get("/getRandomSongs.view", application.subsonicRandomSongs)
|
||||
})
|
||||
|
||||
fs := http.FileServer(http.Dir("./web"))
|
||||
@@ -63,6 +68,71 @@ func NewRouter(cfg config.Config) http.Handler {
|
||||
return r
|
||||
}
|
||||
|
||||
func (a app) login(w http.ResponseWriter, r *http.Request) {
|
||||
var payload struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
|
||||
return
|
||||
}
|
||||
|
||||
session, err := a.auth.Login(r.Context(), payload.Username, payload.Password)
|
||||
if err != nil {
|
||||
if errors.Is(err, auth.ErrInvalidCredentials) {
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid credentials"})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "login failed"})
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, session)
|
||||
}
|
||||
|
||||
func (a app) me(w http.ResponseWriter, r *http.Request) {
|
||||
user := currentUserFromContext(r)
|
||||
writeJSON(w, http.StatusOK, user)
|
||||
}
|
||||
|
||||
func (a app) home(w http.ResponseWriter, r *http.Request) {
|
||||
home, err := a.library.Home(r.Context())
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to load home"})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, home)
|
||||
}
|
||||
|
||||
func (a app) tracks(w http.ResponseWriter, r *http.Request) {
|
||||
items, err := a.library.Tracks(r.Context(), 200)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to load tracks"})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
||||
}
|
||||
|
||||
func (a app) subsonicArtists(w http.ResponseWriter, r *http.Request) {
|
||||
artists, err := a.library.Artists(r.Context(), 1000)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, subsonic.PingResponse())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, subsonic.ArtistsResponse(artists))
|
||||
}
|
||||
|
||||
func (a app) subsonicRandomSongs(w http.ResponseWriter, r *http.Request) {
|
||||
tracks, err := a.library.Tracks(r.Context(), 20)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, subsonic.PingResponse())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, subsonic.RandomSongsResponse(tracks))
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, payload any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
@@ -78,4 +148,3 @@ func spaFallback(next http.Handler) http.HandlerFunc {
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user