Files
minstrel/internal/api/me_subsonic_password.go
T
bvandeusenandClaude Opus 5.5 08a2c06b85 docs: api comments say what the endpoints do, not how they came to be (#6277)
Rewrites the internal/api comment blocks to rule 32: task, milestone and
rule ids gone, the stories behind them restated as the constraint they
left, stale references corrected (the stream secret is wired from config,
the timezone handler already refreshes the scheduler, the Android app is
the native client, the design docs cited are not in the repo). Also
rewraps comment lines over 80 columns left by the earlier batches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:23:58 -04:00

84 lines
3.0 KiB
Go

package api
import (
"crypto/rand"
"encoding/base64"
"net/http"
"git.fabledsword.com/bvandeusen/minstrel/internal/apierror"
"git.fabledsword.com/bvandeusen/minstrel/internal/audit"
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
)
// The Subsonic password is for clients that sign in with Subsonic's t/s
// scheme (md5 of the password plus a salt). Checking that needs the password
// itself, so it is stored readable (migration 0003). That is why Minstrel
// generates it rather than letting the user choose one: a generated value
// can never be a login password reused from somewhere else, so a leaked
// users table gives up access to this server's /rest API and nothing more.
const subsonicPasswordBytes = 18 // 24 base64url characters
type subsonicPasswordStatusResp struct {
Enabled bool `json:"enabled"`
}
type subsonicPasswordResp struct {
Password string `json:"password"`
}
// handleGetMySubsonicPassword implements GET /api/me/subsonic-password. It
// reports only whether one is set; the value is shown once, when generated.
func (h *handlers) handleGetMySubsonicPassword(w http.ResponseWriter, r *http.Request) {
user, ok := requireUser(w, r)
if !ok {
return
}
writeJSON(w, http.StatusOK, subsonicPasswordStatusResp{Enabled: user.SubsonicPassword != nil})
}
// handleGenerateMySubsonicPassword implements POST /api/me/subsonic-password:
// replaces any existing Subsonic password with a new random one and returns it.
func (h *handlers) handleGenerateMySubsonicPassword(w http.ResponseWriter, r *http.Request) {
user, ok := requireUser(w, r)
if !ok {
return
}
b := make([]byte, subsonicPasswordBytes)
if _, err := rand.Read(b); err != nil {
h.logger.Error("generate subsonic password: rand failed", "err", err)
writeErr(w, apierror.Internal(err))
return
}
pw := base64.RawURLEncoding.EncodeToString(b)
if err := dbq.New(h.pool).SetSubsonicPassword(r.Context(), dbq.SetSubsonicPasswordParams{
ID: user.ID,
SubsonicPassword: &pw,
}); err != nil {
h.logger.Error("generate subsonic password: update failed", "err", err)
writeErr(w, apierror.Internal(err))
return
}
audit.WriteOrLog(r.Context(), h.pool, h.logger, user.ID, user.ID, audit.ActionSubsonicPasswordSet, nil)
writeJSON(w, http.StatusOK, subsonicPasswordResp{Password: pw})
}
// handleClearMySubsonicPassword implements DELETE /api/me/subsonic-password,
// which turns t/s and p= sign-in off for the account.
func (h *handlers) handleClearMySubsonicPassword(w http.ResponseWriter, r *http.Request) {
user, ok := requireUser(w, r)
if !ok {
return
}
if err := dbq.New(h.pool).SetSubsonicPassword(r.Context(), dbq.SetSubsonicPasswordParams{
ID: user.ID,
SubsonicPassword: nil,
}); err != nil {
h.logger.Error("clear subsonic password: update failed", "err", err)
writeErr(w, apierror.Internal(err))
return
}
audit.WriteOrLog(r.Context(), h.pool, h.logger, user.ID, user.ID, audit.ActionSubsonicPasswordClear, nil)
w.WriteHeader(http.StatusNoContent)
}