test-web / test (push) Successful in 2m4s
test-go / test (push) Successful in 2m23s
test-go / integration (push) Successful in 5m28s
release / Build signed APK (releases and dev) (push) Successful in 6m29s
release / Build + push container image (push) Successful in 29s
release / Verify release artifacts (tag releases only) (push) Skipped
users.api_token held each user's apiKey in plaintext and was looked up by equality, so a leaked row or backup handed out working keys. Migration 0063 replaces it with api_token_hash (sha256, hex), computed in place from the existing keys so every Subsonic client keeps working. The key can no longer be read back: GET /api/me/api-token is gone, and POST returns the new key once. Settings shows it right after Regenerate with a copy button and a "won't be shown again" note. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
61 lines
1.6 KiB
Go
61 lines
1.6 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"testing"
|
|
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/auth"
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
func newMeTokenRouter(h *handlers) chi.Router {
|
|
r := chi.NewRouter()
|
|
r.Post("/api/me/api-token", h.handleRegenerateMyAPIToken)
|
|
return r
|
|
}
|
|
|
|
func TestRegenerateAPIToken_IssuesNewToken(t *testing.T) {
|
|
if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
|
|
t.Skip("MINSTREL_TEST_DATABASE_URL not set")
|
|
}
|
|
h, pool := testHandlers(t)
|
|
user := seedUser(t, pool, "tok2", "pw", false)
|
|
oldHash := user.ApiTokenHash
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/me/api-token", nil)
|
|
req = withUser(req, user)
|
|
rec := httptest.NewRecorder()
|
|
newMeTokenRouter(h).ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
var resp apiTokenResp
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if resp.APIToken == "" {
|
|
t.Fatalf("api_token is empty")
|
|
}
|
|
// The DB holds the new key's hash, never the key, and the old key no
|
|
// longer matches.
|
|
var dbHash string
|
|
if err := pool.QueryRow(context.Background(),
|
|
"SELECT api_token_hash FROM users WHERE id = $1", user.ID).Scan(&dbHash); err != nil {
|
|
t.Fatalf("read token hash: %v", err)
|
|
}
|
|
if dbHash != auth.HashAPIToken(resp.APIToken) {
|
|
t.Errorf("DB api_token_hash = %q, want hash of the returned key", dbHash)
|
|
}
|
|
if dbHash == oldHash {
|
|
t.Errorf("old key hash still in DB after regenerate")
|
|
}
|
|
if dbHash == resp.APIToken {
|
|
t.Errorf("DB holds the raw key")
|
|
}
|
|
}
|