feat: add device deletion endpoint with cascade cleanup and admin UI integration
Add DELETE /admin/devices/{id} endpoint with cascade deletion of device records, WireGuard peers, IP allocations, and device access profile settings. Update device status to 'deleted' and set deleted_at timestamp while preserving revoked_at if already set.
Add deleteDevice API method and delete button to devices page with query invalidation for both devices and device-profile lists. Record admin.device.deleted audit
This commit is contained in:
@@ -196,3 +196,26 @@ func (h *Handler) Rotate(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
apiutil.JSON(w, http.StatusOK, map[string]any{"ok": true})
|
||||
}
|
||||
|
||||
func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
deviceID, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
apiutil.Error(w, http.StatusBadRequest, "invalid_device_id", "invalid device id")
|
||||
return
|
||||
}
|
||||
if err := h.service.Delete(r.Context(), deviceID); err != nil {
|
||||
apiutil.Error(w, http.StatusInternalServerError, "device_delete_failed", "unable to delete device")
|
||||
return
|
||||
}
|
||||
if claims, ok := requestctx.ClaimsFromContext(r.Context()); ok {
|
||||
_ = h.audit.Record(r.Context(), audit.Entry{
|
||||
ActorUserID: &claims.UserID,
|
||||
EntityType: "device",
|
||||
EntityID: &deviceID,
|
||||
EventType: "admin.device.deleted",
|
||||
Status: "success",
|
||||
Message: "admin deleted device",
|
||||
})
|
||||
}
|
||||
apiutil.JSON(w, http.StatusOK, map[string]any{"ok": true})
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ type Repository interface {
|
||||
GetSelectedProfileID(ctx context.Context, deviceID uuid.UUID) (*uuid.UUID, error)
|
||||
SetSelectedProfileID(ctx context.Context, deviceID uuid.UUID, profileID uuid.UUID) error
|
||||
Revoke(ctx context.Context, deviceID uuid.UUID) error
|
||||
Delete(ctx context.Context, deviceID uuid.UUID) error
|
||||
Rotate(ctx context.Context, deviceID uuid.UUID) error
|
||||
}
|
||||
|
||||
@@ -292,6 +293,44 @@ func (r *PGRepository) Revoke(ctx context.Context, deviceID uuid.UUID) error {
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (r *PGRepository) Delete(ctx context.Context, deviceID uuid.UUID) error {
|
||||
tx, err := r.db.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
if _, err := tx.Exec(ctx, `
|
||||
update devices
|
||||
set status = 'deleted', deleted_at = now(), revoked_at = coalesce(revoked_at, now()), updated_at = now()
|
||||
where id = $1 and deleted_at is null
|
||||
`, deviceID); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
update wireguard_peers
|
||||
set deleted_at = now(), updated_at = now()
|
||||
where device_id = $1 and deleted_at is null
|
||||
`, deviceID); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
update ip_allocations
|
||||
set status = 'released', released_at = coalesce(released_at, now()), updated_at = now()
|
||||
where device_id = $1 and status = 'allocated'
|
||||
`, deviceID); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
delete from settings
|
||||
where category = 'device_access_profile' and key = $1
|
||||
`, deviceID.String()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (r *PGRepository) Rotate(ctx context.Context, deviceID uuid.UUID) error {
|
||||
_, err := r.db.Exec(ctx, `
|
||||
update wireguard_peers
|
||||
|
||||
@@ -189,6 +189,10 @@ func (s *Service) Revoke(ctx context.Context, deviceID uuid.UUID) error {
|
||||
return s.repo.Revoke(ctx, deviceID)
|
||||
}
|
||||
|
||||
func (s *Service) Delete(ctx context.Context, deviceID uuid.UUID) error {
|
||||
return s.repo.Delete(ctx, deviceID)
|
||||
}
|
||||
|
||||
func (s *Service) Rotate(ctx context.Context, deviceID uuid.UUID) error {
|
||||
return s.repo.Rotate(ctx, deviceID)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user