package postgres import ( "context" "database/sql" "encoding/json" "time" "armature/models" ) // APITokenStore implements store.APITokenStore for PostgreSQL. type APITokenStore struct { db *sql.DB } // NewAPITokenStore creates a new PostgreSQL API token store. func NewAPITokenStore(db *sql.DB) *APITokenStore { return &APITokenStore{db: db} } func (s *APITokenStore) Create(ctx context.Context, token *models.APIToken) error { permsJSON, err := json.Marshal(token.Permissions) if err != nil { return err } return s.db.QueryRowContext(ctx, `INSERT INTO api_tokens (user_id, name, token_hash, prefix, permissions, expires_at, created_by) VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7) RETURNING id, created_at`, token.UserID, token.Name, token.TokenHash, token.Prefix, string(permsJSON), token.ExpiresAt, token.CreatedBy, ).Scan(&token.ID, &token.CreatedAt) } func (s *APITokenStore) GetByHash(ctx context.Context, tokenHash string) (*models.APIToken, error) { row := s.db.QueryRowContext(ctx, `SELECT id, user_id, name, token_hash, prefix, permissions, expires_at, last_used_at, created_at, created_by FROM api_tokens WHERE token_hash = $1 AND (expires_at IS NULL OR expires_at > NOW())`, tokenHash) var t models.APIToken var permsJSON []byte err := row.Scan(&t.ID, &t.UserID, &t.Name, &t.TokenHash, &t.Prefix, &permsJSON, &t.ExpiresAt, &t.LastUsedAt, &t.CreatedAt, &t.CreatedBy) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } if err := json.Unmarshal(permsJSON, &t.Permissions); err != nil { t.Permissions = []string{} } return &t, nil } func (s *APITokenStore) ListForUser(ctx context.Context, userID string) ([]models.APIToken, error) { rows, err := s.db.QueryContext(ctx, `SELECT id, user_id, name, prefix, permissions, expires_at, last_used_at, created_at, created_by FROM api_tokens WHERE user_id = $1 ORDER BY created_at DESC`, userID) if err != nil { return nil, err } defer rows.Close() var tokens []models.APIToken for rows.Next() { var t models.APIToken var permsJSON []byte if err := rows.Scan(&t.ID, &t.UserID, &t.Name, &t.Prefix, &permsJSON, &t.ExpiresAt, &t.LastUsedAt, &t.CreatedAt, &t.CreatedBy); err != nil { return nil, err } if err := json.Unmarshal(permsJSON, &t.Permissions); err != nil { t.Permissions = []string{} } tokens = append(tokens, t) } if tokens == nil { tokens = []models.APIToken{} } return tokens, nil } func (s *APITokenStore) Revoke(ctx context.Context, id, userID string) (int64, error) { res, err := s.db.ExecContext(ctx, `DELETE FROM api_tokens WHERE id = $1 AND user_id = $2`, id, userID) if err != nil { return 0, err } return res.RowsAffected() } func (s *APITokenStore) RevokeByID(ctx context.Context, id string) (int64, error) { res, err := s.db.ExecContext(ctx, `DELETE FROM api_tokens WHERE id = $1`, id) if err != nil { return 0, err } return res.RowsAffected() } func (s *APITokenStore) CleanExpired(ctx context.Context) (int64, error) { res, err := s.db.ExecContext(ctx, `DELETE FROM api_tokens WHERE expires_at IS NOT NULL AND expires_at < NOW()`) if err != nil { return 0, err } return res.RowsAffected() } func (s *APITokenStore) UpdateLastUsed(ctx context.Context, id string) error { _, err := s.db.ExecContext(ctx, `UPDATE api_tokens SET last_used_at = $1 WHERE id = $2`, time.Now(), id) return err }