Phase 3: OpenSubsonic core (response envelope, error codes 42-44, getOpenSubsonicExtensions) and apiKeyAuthentication extension with tokenInfo, web API key management

This commit is contained in:
2026-08-23 08:04:01 -05:00
parent 3b4883780b
commit e38d31d384
17 changed files with 763 additions and 37 deletions
+35 -14
View File
@@ -9,18 +9,22 @@ import (
)
type APIKey struct {
ID string
UserID string
Name string
KeyHash string
CreatedAt time.Time
LastUsedAt sql.NullTime
RevokedAt sql.NullTime
ID string
UserID string
Name string
KeyHash string
ClientDescription string
CreatedAt time.Time
LastUsedAt sql.NullTime
RevokedAt sql.NullTime
}
func CreateAPIKey(db *sql.DB, userID, name, keyHash string) (*APIKey, error) {
const apiKeySelect = `SELECT id, user_id, name, key_hash, client_description, created_at, last_used_at, revoked_at FROM api_keys`
func CreateAPIKey(db *sql.DB, userID, name, keyHash, clientDescription string) (*APIKey, error) {
id := idgen.New("ak")
_, err := db.Exec(`INSERT INTO api_keys (id, user_id, name, key_hash) VALUES (?, ?, ?, ?)`, id, userID, name, keyHash)
_, err := db.Exec(`INSERT INTO api_keys (id, user_id, name, key_hash, client_description) VALUES (?, ?, ?, ?, ?)`,
id, userID, name, keyHash, clientDescription)
if err != nil {
return nil, err
}
@@ -28,18 +32,20 @@ func CreateAPIKey(db *sql.DB, userID, name, keyHash string) (*APIKey, error) {
}
func GetAPIKey(db *sql.DB, id string) (*APIKey, error) {
return scanAPIKey(db.QueryRow(`SELECT id, user_id, name, key_hash, created_at, last_used_at, revoked_at FROM api_keys WHERE id = ?`, id))
return scanAPIKey(db.QueryRow(apiKeySelect+` WHERE id = ?`, id))
}
// GetAPIKeyByHash looks up a non-revoked key by its hash, for
// authentication. Revoked keys never match, by design.
func GetAPIKeyByHash(db *sql.DB, keyHash string) (*APIKey, error) {
return scanAPIKey(db.QueryRow(`SELECT id, user_id, name, key_hash, created_at, last_used_at, revoked_at FROM api_keys WHERE key_hash = ? AND revoked_at IS NULL`, keyHash))
return scanAPIKey(db.QueryRow(apiKeySelect+` WHERE key_hash = ? AND revoked_at IS NULL`, keyHash))
}
func scanAPIKey(row *sql.Row) (*APIKey, error) {
var k APIKey
var createdAt string
var lastUsed, revoked sql.NullString
err := row.Scan(&k.ID, &k.UserID, &k.Name, &k.KeyHash, &createdAt, &lastUsed, &revoked)
err := row.Scan(&k.ID, &k.UserID, &k.Name, &k.KeyHash, &k.ClientDescription, &createdAt, &lastUsed, &revoked)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
@@ -61,17 +67,32 @@ func scanAPIKey(row *sql.Row) (*APIKey, error) {
}
func ListAPIKeysForUser(db *sql.DB, userID string) ([]*APIKey, error) {
rows, err := db.Query(`SELECT id, user_id, name, key_hash, created_at, last_used_at, revoked_at FROM api_keys WHERE user_id = ? ORDER BY created_at DESC`, userID)
rows, err := db.Query(apiKeySelect+` WHERE user_id = ? ORDER BY created_at DESC`, userID)
if err != nil {
return nil, err
}
defer rows.Close()
return scanAPIKeys(rows)
}
// ListAllAPIKeys returns every API key for every user, for admin
// management.
func ListAllAPIKeys(db *sql.DB) ([]*APIKey, error) {
rows, err := db.Query(apiKeySelect + ` ORDER BY created_at DESC`)
if err != nil {
return nil, err
}
defer rows.Close()
return scanAPIKeys(rows)
}
func scanAPIKeys(rows *sql.Rows) ([]*APIKey, error) {
var out []*APIKey
for rows.Next() {
var k APIKey
var createdAt string
var lastUsed, revoked sql.NullString
if err := rows.Scan(&k.ID, &k.UserID, &k.Name, &k.KeyHash, &createdAt, &lastUsed, &revoked); err != nil {
if err := rows.Scan(&k.ID, &k.UserID, &k.Name, &k.KeyHash, &k.ClientDescription, &createdAt, &lastUsed, &revoked); err != nil {
return nil, err
}
k.CreatedAt, _ = parseTime(createdAt)