package database import ( "database/sql" "errors" "time" "git.noroutes.net/NoRoutes/vibetunes/internal/idgen" ) type APIKey struct { ID string UserID string Name string KeyHash string ClientDescription string CreatedAt time.Time LastUsedAt sql.NullTime RevokedAt sql.NullTime } 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, client_description) VALUES (?, ?, ?, ?, ?)`, id, userID, name, keyHash, clientDescription) if err != nil { return nil, err } return GetAPIKey(db, id) } func GetAPIKey(db *sql.DB, id string) (*APIKey, error) { 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(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, &k.ClientDescription, &createdAt, &lastUsed, &revoked) if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } if err != nil { return nil, err } k.CreatedAt, _ = parseTime(createdAt) if lastUsed.Valid { if t, err := parseTime(lastUsed.String); err == nil { k.LastUsedAt = sql.NullTime{Time: t, Valid: true} } } if revoked.Valid { if t, err := parseTime(revoked.String); err == nil { k.RevokedAt = sql.NullTime{Time: t, Valid: true} } } return &k, nil } func ListAPIKeysForUser(db *sql.DB, userID string) ([]*APIKey, error) { 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, &k.ClientDescription, &createdAt, &lastUsed, &revoked); err != nil { return nil, err } k.CreatedAt, _ = parseTime(createdAt) if lastUsed.Valid { if t, err := parseTime(lastUsed.String); err == nil { k.LastUsedAt = sql.NullTime{Time: t, Valid: true} } } if revoked.Valid { if t, err := parseTime(revoked.String); err == nil { k.RevokedAt = sql.NullTime{Time: t, Valid: true} } } out = append(out, &k) } return out, rows.Err() } func TouchAPIKey(db *sql.DB, id string) error { _, err := db.Exec(`UPDATE api_keys SET last_used_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?`, id) return err } func RevokeAPIKey(db *sql.DB, id string) error { _, err := db.Exec(`UPDATE api_keys SET revoked_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?`, id) return err }