feat: add admin edit endpoint for server moderation (#315)
## Summary
Implements `PUT /v0/servers/{id}` admin edit endpoint to address part of
issue #182, providing an API-based solution for server moderation
without requiring direct database access.
- **New endpoint**: `PUT /v0/servers/{id}` for editing existing servers
- **Security**: Uses existing JWT auth with `PermissionActionEdit`
permissions
- **Anti-undelete protection**: Added `ServerStatusDeleted` status that
cannot be reverted
- **Full implementation**: Database, service, API, and comprehensive
test coverage
## Key Use Cases
- Mark malicious servers as `deleted` for takedown
- Update server metadata for moderation actions
Related to: https://github.com/modelcontextprotocol/registry/issues/182
(but that is not _done_ yet because we need admins to be able to login
(probably with #283), and need to add docs on how to do the takedown,
and probably guidelines on what we will/won't takedown)
This commit is contained in:
parent
36fcdff044
commit
6431f23107
3 changed files with 681 additions and 23 deletions
|
|
@ -28,6 +28,8 @@ type Database interface {
|
|||
Publish(ctx context.Context, serverDetail model.ServerDetail, publisherExtensions map[string]interface{}, registryMetadata model.RegistryMetadata) (*model.ServerRecord, error)
|
||||
// UpdateLatestFlag updates the is_latest flag for a specific server record
|
||||
UpdateLatestFlag(ctx context.Context, id string, isLatest bool) error
|
||||
// UpdateServer updates an existing server record with new server details
|
||||
UpdateServer(ctx context.Context, id string, serverDetail model.ServerDetail, publisherExtensions map[string]interface{}) (*model.ServerRecord, error)
|
||||
// ImportSeed imports initial data from a seed file
|
||||
ImportSeed(ctx context.Context, seedFilePath string) error
|
||||
// Close closes the database connection
|
||||
|
|
|
|||
|
|
@ -246,6 +246,29 @@ func (db *MemoryDB) UpdateLatestFlag(ctx context.Context, id string, isLatest bo
|
|||
return ErrNotFound
|
||||
}
|
||||
|
||||
// UpdateServer updates an existing server record with new server details
|
||||
func (db *MemoryDB) UpdateServer(ctx context.Context, id string, serverDetail model.ServerDetail, publisherExtensions map[string]interface{}) (*model.ServerRecord, error) {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
db.mu.Lock()
|
||||
defer db.mu.Unlock()
|
||||
|
||||
entry, exists := db.entries[id]
|
||||
if !exists {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
// Update the server details
|
||||
entry.ServerJSON = serverDetail
|
||||
entry.PublisherExtensions = publisherExtensions
|
||||
entry.RegistryMetadata.UpdatedAt = time.Now()
|
||||
|
||||
// Return the updated record
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
// Close closes the database connection
|
||||
// For an in-memory database, this is a no-op
|
||||
func (db *MemoryDB) Close() error {
|
||||
|
|
|
|||
|
|
@ -531,6 +531,96 @@ func (db *PostgreSQL) UpdateLatestFlag(ctx context.Context, id string, isLatest
|
|||
return nil
|
||||
}
|
||||
|
||||
// UpdateServer updates an existing server record with new server details
|
||||
func (db *PostgreSQL) UpdateServer(ctx context.Context, id string, serverDetail model.ServerDetail, publisherExtensions map[string]interface{}) (*model.ServerRecord, error) {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
// First check if the server exists
|
||||
_, err := db.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Start a transaction for atomic updates
|
||||
tx, err := db.conn.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to begin transaction: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if rollbackErr := tx.Rollback(ctx); rollbackErr != nil && !errors.Is(rollbackErr, pgx.ErrTxClosed) {
|
||||
log.Printf("Failed to rollback transaction: %v", rollbackErr)
|
||||
}
|
||||
}()
|
||||
|
||||
// Prepare JSON data
|
||||
repositoryJSON, err := json.Marshal(serverDetail.Repository)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal repository: %w", err)
|
||||
}
|
||||
|
||||
packagesJSON, err := json.Marshal(serverDetail.Packages)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal packages: %w", err)
|
||||
}
|
||||
|
||||
remotesJSON, err := json.Marshal(serverDetail.Remotes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal remotes: %w", err)
|
||||
}
|
||||
|
||||
publisherExtensionsJSON, err := json.Marshal(publisherExtensions)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal publisher extensions: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// Update servers table
|
||||
serverQuery := `
|
||||
UPDATE servers
|
||||
SET name = $1, description = $2, status = $3, repository = $4,
|
||||
version = $5, packages = $6, remotes = $7, updated_at = $8
|
||||
WHERE id = $9
|
||||
`
|
||||
|
||||
_, err = tx.Exec(ctx, serverQuery,
|
||||
serverDetail.Name,
|
||||
serverDetail.Description,
|
||||
serverDetail.Status,
|
||||
repositoryJSON,
|
||||
serverDetail.VersionDetail.Version,
|
||||
packagesJSON,
|
||||
remotesJSON,
|
||||
now,
|
||||
id,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update server: %w", err)
|
||||
}
|
||||
|
||||
// Update server_extensions table
|
||||
extQuery := `
|
||||
UPDATE server_extensions
|
||||
SET updated_at = $1, publisher_extensions = $2
|
||||
WHERE server_id = $3
|
||||
`
|
||||
|
||||
_, err = tx.Exec(ctx, extQuery, now, publisherExtensionsJSON, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update server extensions: %w", err)
|
||||
}
|
||||
|
||||
// Commit transaction
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, fmt.Errorf("failed to commit transaction: %w", err)
|
||||
}
|
||||
|
||||
// Return the updated record
|
||||
return db.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
// Close closes the database connection
|
||||
func (db *PostgreSQL) Close() error {
|
||||
return db.conn.Close(context.Background())
|
||||
|
|
|
|||
Loading…
Reference in a new issue