프로젝트

일반

사용자정보

Actions

이슈 #313

진행중
CP JL

17-4. 노드 삭제 API

이슈 #313: 17-4. 노드 삭제 API

Chris Park이(가) 약 5시간 전에 추가함.

상태:
해결
우선순위:
높음
담당자:
목표버전:
시작일:
2026/05/19
완료일:
2026/05/22 (약 5달 지연)
진척도:

0%

추정시간:

설명

DELETE /api/nodes/:id


연결된 일감 2 (2 건 진행 중 — 모두 미완료)

다음 일감과 관련됨: 이슈 #375: JWT 전역 인터셉터 구현 (alphea-ai-edge)해결James Lee2026/04/15

Actions
다음 일감과 관련됨: 이슈 #379: NodeService.Heartbeat 구현 (alphea-ai-edge)해결James Lee2026/04/16

Actions

CP Chris Park이(가) 7달 전에 변경 Actions #1

  • 프로젝트을(를) 14에서 Alphea App(으)로 변경되었습니다.
  • 목표버전을(를) 6월 중간 버전(으)로 지정되었습니다.

JL James Lee이(가) 7달 전에 변경 Actions #2

  • 상태을(를) 신규에서 해결(으)로 변경되었습니다.

17-4. 노드 삭제 API 구현 완료

NodeService.DeleteNode RPC 를 구현했습니다. 이번 포팅에서 첫 번째 JWT 인증이 필요한 보호 RPC 입니다.

  • Repository: alphea-ai-edge
  • Commit: commit:server|d9668e6 — feat: add NodeService DeleteNode with JWT-authenticated soft delete
  • 관련 이슈: #254 (새 마이그레이션), #310/#311 (read 쿼리 필터 추가), #312 (upsert 재등록 un-archive 연동)

Proto 정의 (레거시에 없던 신규 RPC)

rpc DeleteNode(DeleteNodeRequest) returns (DeleteNodeResponse);

message DeleteNodeRequest {
  string node_id = 1;
}

message DeleteNodeResponse {
  bool deleted = 1;
}

인증 플로우

  1. 클라이언트가 Authorization: Bearer <node-jwt> 헤더와 함께 DeleteNode 호출
  2. domain::auth::require_auth 가 메타데이터에서 토큰 추출 → verify_access_token → sub (지갑 주소) 반환
  3. nodes::get_by_id 로 노드 조회 (archived 는 NotFound)
  4. 소유권 체크: node.owner_wallet_address 와 JWT sub 대소문자 무관 비교
  5. 불일치 → PermissionDenied
  6. 일치 → nodes::archive 로 archived_at = NOW() 세팅 (물리 삭제 아님)
  7. DeleteNodeResponse { deleted: true }

Soft Delete 설계

물리 삭제 대신 archived_at 컬럼을 세팅하는 soft delete 방식입니다.

  • 읽기 쿼리 필터: list, get_by_id, count_filtered 모두 WHERE archived_at IS NULL 조건 추가 → archived 된 노드는 공개 API 에서 완전히 숨김
  • Partial index: CREATE INDEX ... WHERE archived_at IS NOT NULL — archived 서브셋만 인덱싱해서 인덱스 크기 최소화 (활성 노드 조회는 기존 인덱스로 커버)
  • 재등록 = 자동 un-archive: upsert_registered 가 ON CONFLICT 시 archived_at = NULL 로 리셋. 사용자가 동일 node_id 로 재등록하면 삭제됐던 노드가 자동으로 복원됨 (UX 관점에서 자연스러움)

인증 헬퍼 (domain::auth::require_auth)

pub fn require_auth<T>(req: &Request<T>, jwt_secret: &str) -> Result<String, Status> {
    let token = req
        .metadata()
        .get("authorization")
        .and_then(|v| v.to_str().ok())
        .and_then(|v| v.strip_prefix("Bearer "))
        .ok_or_else(|| Status::unauthenticated("missing authorization header"))?;

    let payload = verify_access_token(token, jwt_secret)
        .map_err(|_| Status::unauthenticated("invalid token"))?;

    Ok(payload.sub)
}

결정: 전역 Tower Layer 인터셉터 대신 핸들러별 명시적 호출 방식 채택.

  • Pro: 구현이 단순하고, allow-list 실수로 public 만들어버리는 버그가 원천 차단됨 (auth 는 opt-in)
  • Con: 핸들러마다 한 줄씩 반복됨
  • 보호 RPC 가 많아지면 Tower Layer 기반 전역 인터셉터로 교체 가능. 현재는 DeleteNode 하나뿐이므로 과도한 추상화 불필요

레거시의 전역 allow-list + 컨텍스트 주입 방식과는 다르지만, 이번 포팅에서 Rust idiom 에 더 부합합니다. #256 에 JWT 인터셉터 추후 작업으로 기록된 내용과 같은 맥락입니다.

E2E 검증 결과 (examples/node_delete.rs)

→ owner:     0x17941d5c72db95ca0998ecae7a87814f903a7479
→ stranger:  0x6c2ae6a68287b3acae6168acfb2906cd1ba931de
→ node_id:   edge-del-b44951cf

✓ owner registered node (token: eyJhbGciOiJIUzI1NiIsInR5...)
✓ stranger blocked: PermissionDenied — caller does not own this node
✓ unauth blocked:   Unauthenticated — missing authorization header
✓ owner deleted:    deleted=true
✓ GetNode post-delete: NotFound — node not found
✓ ListNodes filtered out archived row (total active nodes: 3)
✓ re-registered node is active again: status=2

7개 시나리오 전부 통과:

# 시나리오 결과
1 owner 가 edge 노드 등록 node JWT 발급
2 stranger JWT 로 owner 노드 삭제 시도 PermissionDenied
3 authorization 헤더 없이 삭제 시도 Unauthenticated
4 owner JWT 로 삭제 deleted: true
5 삭제 후 GetNode NotFound
6 삭제 후 ListNodes 총 3건 (archived 제외)
7 동일 node_id 재등록 un-archive, status=2 (ACTIVE)

알려진 한계

  • Delete 되돌리기: 현재는 "재등록 = un-archive" 로만 복원 가능. 명시적 RestoreNode RPC 는 없음 (필요 시 추후 추가)
  • Audit trail: 누가 언제 archive 했는지는 기록되지 않음. 필요 시 archived_at 외에 archived_by 같은 컬럼 추가 가능
  • Archive 전 running jobs: 현재는 무조건 archive. 실행 중인 작업이 있는 노드도 즉시 삭제됨. 실제 운영에서는 draining 과정이 필요할 수 있음 (후속 작업 대상)

JL James Lee이(가) 7달 전에 변경 Actions #3

  • 다음 일감과 관련됨:에 이슈 #375: JWT 전역 인터셉터 구현 (alphea-ai-edge)이(가) 추가되었습니다.

JL James Lee이(가) 7달 전에 변경 Actions #4

DeleteNode 핸들러를 전역 JWT 인터셉터로 전환

commit:server|b2c3d29

#375 에서 도입한 AuthLayer 전역 미들웨어가 Bearer JWT 를 검증하고 AuthContext 를 request extensions 에 주입하도록 변경되면서, DeleteNode 는 더 이상 핸들러 진입 시 JWT 를 재검증하지 않습니다.

변경 전 → 변경 후

// BEFORE
let caller = require_auth(&request, &self.state.cfg.jwt_secret)?;

// AFTER — O(1) extensions 조회
let caller = caller_wallet(&request)?;

owner 체크 (JWT sub vs nodes.owner_wallet_address, case-insensitive) 와 soft-delete (archived_at) 로직은 그대로.

회귀 검증

examples/node_delete.rs 7개 시나리오 전부 통과 — 특히:

  • stranger JWT 로 타인의 node 삭제 시도 → PermissionDenied: caller does not own this node
  • 토큰 없음 → Unauthenticated: missing authorization header (이 메시지는 이제 미들웨어에서 반환)
  • owner soft-delete → deleted=true, GetNode 는 NotFound, ListNodes 는 제외
  • re-register 시 archived_at=NULL 로 복구

관련: #375

JL James Lee이(가) 7달 전에 변경 Actions #5

  • 다음 일감과 관련됨:에 이슈 #379: NodeService.Heartbeat 구현 (alphea-ai-edge)이(가) 추가되었습니다.
Actions

내보내기 PDF Atom