package httpapi import ( "errors" "net/http" "strings" "time" "data-terminal-cloud/internal/idempotency" "data-terminal-cloud/internal/opsaccess" ) func CreateOpsSensitiveAccessRequestHandler(service *opsaccess.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, ok := webPrincipal(w, r) if !ok { return } tenantID, key, ok := opsCreateHeaders(w, r) if !ok { return } var input struct { ConversationID string `json:"conversation_id"` Capabilities []string `json:"capabilities"` Reason string `json:"reason"` ExpiresAt time.Time `json:"expires_at"` } if !decodeWebJSON(w, r, &input, "临时敏感数据访问申请参数不正确") { return } item, created, err := service.CreateRequest(r.Context(), principal, tenantID, input.ConversationID, input.Capabilities, input.Reason, input.ExpiresAt, key, RequestID(r.Context())) if err != nil { writeOpsAccessError(w, r, err) return } status := http.StatusCreated if !created { status = http.StatusOK w.Header().Set("Idempotency-Replayed", "true") } w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, status, item) }) } func ListOpsSensitiveAccessRequestsHandler(service *opsaccess.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, ok := webPrincipal(w, r) if !ok { return } tenantID, ok := opsTenantHeader(w, r) if !ok { return } items, err := service.ListRequests(r.Context(), principal, tenantID) if err != nil { writeOpsAccessError(w, r, err) return } w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusOK, map[string]any{"items": items}) }) } func ReviewOpsSensitiveAccessRequestHandler(service *opsaccess.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, ok := webPrincipal(w, r) if !ok { return } tenantID, key, ok := opsCreateHeaders(w, r) if !ok { return } var input struct { Decision string `json:"decision"` Reason string `json:"reason"` } if !decodeWebJSON(w, r, &input, "临时访问审批参数不正确") { return } item, created, err := service.ReviewRequest(r.Context(), principal, tenantID, r.PathValue("id"), input.Decision, input.Reason, key, RequestID(r.Context())) if err != nil { writeOpsAccessError(w, r, err) return } if !created { w.Header().Set("Idempotency-Replayed", "true") } w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusOK, item) }) } func GetOpsSensitiveConversationHandler(service *opsaccess.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, ok := webPrincipal(w, r) if !ok { return } tenantID, ok := opsTenantHeader(w, r) if !ok { return } grantID := strings.TrimSpace(r.Header.Get("X-Ops-Access-Grant")) if grantID == "" { writeValidation(w, r, "临时访问凭据缺失", FieldError{Field: "X-Ops-Access-Grant", Message: "必须提供已批准且未过期的临时访问申请编号"}) return } item, err := service.GetSensitiveConversation(r.Context(), principal, tenantID, r.PathValue("id"), grantID, RequestID(r.Context())) if err != nil { writeOpsAccessError(w, r, err) return } w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusOK, item) }) } func CreateOpsSensitiveRecordingAccessHandler(service *opsaccess.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, ok := webPrincipal(w, r) if !ok { return } tenantID, ok := opsTenantHeader(w, r) if !ok { return } var input struct { GrantID string `json:"grant_id"` } if !decodeWebJSON(w, r, &input, "录音临时访问参数不正确") { return } if strings.TrimSpace(input.GrantID) == "" { writeValidation(w, r, "录音临时访问参数不正确", FieldError{Field: "grant_id", Message: "必须提供已批准且未过期的临时访问申请编号"}) return } item, err := service.CreateRecordingAccess(r.Context(), principal, tenantID, r.PathValue("id"), input.GrantID, RequestID(r.Context())) if err != nil { writeOpsAccessError(w, r, err) return } w.Header().Set("Cache-Control", "no-store") WriteJSON(w, r, http.StatusCreated, item) }) } func writeOpsAccessError(w http.ResponseWriter, r *http.Request, err error) { switch { case errors.Is(err, opsaccess.ErrInvalid): writeValidation(w, r, "临时敏感数据访问参数不正确", FieldError{Field: "temporary_access", Message: "通话、能力、原因或有效时间不符合要求"}) case errors.Is(err, opsaccess.ErrAccessDenied): WriteError(w, r, http.StatusForbidden, "TEMPORARY_ACCESS_DENIED", "临时访问未批准、已过期、能力不匹配或不属于当前申请人") case errors.Is(err, opsaccess.ErrNotFound): WriteError(w, r, http.StatusNotFound, "RESOURCE_NOT_FOUND", "指定临时访问申请、通话或录音不存在") case errors.Is(err, opsaccess.ErrConflict): WriteError(w, r, http.StatusConflict, "TEMPORARY_ACCESS_STATE_CONFLICT", "已有未结束申请、申请人尝试自批或当前状态不允许操作") case errors.Is(err, idempotency.ErrKeyConflict): WriteError(w, r, http.StatusConflict, "IDEMPOTENCY_KEY_CONFLICT", "重复请求识别码已被不同请求使用") case errors.Is(err, idempotency.ErrInProgress): WriteError(w, r, http.StatusConflict, "IDEMPOTENCY_IN_PROGRESS", "相同请求正在处理中") default: WriteError(w, r, http.StatusInternalServerError, "INTERNAL_ERROR", "临时敏感数据访问处理失败") } }