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>
84 lines
3.0 KiB
Go
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)
|
|
}
|