use crate::protos::auth::common::v1::{Code, ErrorDetail}; use crate::protos::google::rpc::Status as RpcStatus; use prost::Message; use prost_types::Any; use tonic::Status; use crate::errors::AuthError; pub fn code_of(error: &AuthError) -> Code { match error { AuthError::Expired => Code::ExpiredChallenge, AuthError::InvalidPayload => Code::InvalidPayload, AuthError::InvalidProof => Code::InvalidProof, AuthError::MalformedRequest => Code::MalformedRequest, AuthError::InvalidUsername => Code::InvalidUsername, AuthError::InvalidDisplayname => Code::InvalidDisplayname, AuthError::InvalidEmail => Code::InvalidEmail, AuthError::EmailUnchanged => Code::EmailUnchanged, AuthError::InvalidBirthDate => Code::InvalidBirthDate, AuthError::UnderMinimumAge { .. } => Code::UnderMinimumAge, AuthError::InvalidPassword => Code::InvalidPassword, AuthError::PasswordHash(_) => Code::Internal, AuthError::UsernameTaken => Code::UsernameTaken, AuthError::EmailTaken => Code::EmailTaken, AuthError::InvalidEmailCode => Code::InvalidEmailCode, AuthError::EmailCodeAttemptsExceeded => Code::EmailCodeAttemptsExceeded, AuthError::InvalidResetToken => Code::InvalidResetToken, AuthError::Unauthenticated => Code::Unauthenticated, AuthError::Forbidden => Code::Forbidden, AuthError::BadRequest(_) => Code::MalformedRequest, AuthError::Conflict(_) => Code::VersionMismatch, AuthError::InvalidCredentials => Code::InvalidCredentials, AuthError::LoginForbidden { .. } => Code::LoginForbidden, AuthError::AccountUnavailable { .. } => Code::UserDisabled, AuthError::MfaRequired => Code::MfaRequired, AuthError::TotpNotConfigured => Code::MfaNotConfigured, AuthError::TotpAlreadyEnabled => Code::TotpAlreadyEnabled, AuthError::TotpCodeInvalid => Code::InvalidCredentials, AuthError::TotpStepReused => Code::TotpStepReused, AuthError::RecoveryCodeInvalid => Code::InvalidCredentials, AuthError::RecoveryCodesExhausted => Code::RecoveryCodesExhausted, AuthError::InvalidAccessToken => Code::InvalidAccessToken, AuthError::AccessTokenNotFound => Code::AccessKeyNotFound, AuthError::InvalidAccessKeyName => Code::InvalidName, AuthError::InvalidScope => Code::InvalidScope, AuthError::InvalidExpiry => Code::InvalidExpiry, AuthError::TooManyAccessKeys => Code::TooManyAccessKeys, AuthError::SshKeyInvalid => Code::InvalidPublicKey, AuthError::InvalidPermission { .. } => Code::InvalidPermission, AuthError::OrganizationNotFound => Code::OrganizationNotFound, AuthError::OrganizationMemberNotFound => Code::OrganizationMemberNotFound, AuthError::OrganizationRoleNotFound => Code::OrganizationRoleNotFound, AuthError::OrganizationInviteNotFound => Code::OrganizationInviteNotFound, AuthError::OrganizationSlugTaken => Code::OrganizationSlugTaken, AuthError::OrganizationMemberExists => Code::OrganizationMemberExists, AuthError::OrganizationRoleKeyTaken => Code::OrganizationRoleKeyTaken, AuthError::OrganizationRoleInUse => Code::OrganizationRoleInUse, AuthError::OrganizationRoleSystem => Code::OrganizationRoleSystem, AuthError::OrganizationInviteExhausted => Code::OrganizationInviteExhausted, AuthError::OrganizationInviteExpired => Code::OrganizationInviteExpired, AuthError::OrganizationOwnerRequired => Code::OrganizationOwnerRequired, AuthError::OrganizationHasMembers => Code::OrganizationHasMembers, AuthError::OrganizationInactive => Code::OrganizationInactive, AuthError::OrganizationMemberBanned => Code::OrganizationMemberBanned, AuthError::VersionMismatch { .. } => Code::VersionMismatch, AuthError::SshKeyTaken => Code::SshKeyTaken, AuthError::SshKeyNameInvalid => Code::InvalidName, AuthError::SshKeyNotFound => Code::SshKeyNotFound, AuthError::AgreementNotFound { .. } => Code::AgreementNotFound, AuthError::AgreementNotAccepted { .. } => Code::AgreementNotAccepted, AuthError::DeletionAlreadyRequested => Code::DeletionAlreadyRequested, AuthError::DeletionNotRequested => Code::DeletionNotRequested, AuthError::RateLimited { .. } => Code::RateLimited, AuthError::UserNotFound => Code::UserNotFound, AuthError::AppNotFound => Code::NotFound, AuthError::SessionNotFound => Code::SessionNotFound, AuthError::TokenNotFound => Code::TokenNotFound, AuthError::TokenExpired => Code::TokenExpired, AuthError::TokenRevoked => Code::TokenRevoked, AuthError::SecurityVersionStale { .. } => Code::SecurityVersionStale, AuthError::ScopeInsufficient { .. } => Code::ScopeInsufficient, AuthError::Unavailable(_) => Code::Unavailable, AuthError::Internal(_) => Code::Internal, AuthError::Database(_) => Code::DatabaseError, AuthError::Cache(_) => Code::CacheError, AuthError::Session(_) => Code::CacheError, AuthError::Email(_) => Code::EmailError, AuthError::Serialization(_) => Code::SerializationError, } } pub fn tonic_code(code: Code) -> tonic::Code { match code { Code::Unspecified => tonic::Code::Unknown, Code::InvalidArgument | Code::MalformedRequest | Code::ExpiredChallenge | Code::InvalidProof | Code::InvalidPayload | Code::InvalidEmail | Code::InvalidUsername | Code::InvalidDisplayname | Code::InvalidBirthDate | Code::InvalidPassword | Code::InvalidEmailCode | Code::InvalidResetToken | Code::InvalidScope | Code::InvalidExpiry | Code::InvalidName | Code::InvalidPublicKey => tonic::Code::InvalidArgument, Code::Unauthenticated | Code::InvalidCredentials | Code::InvalidAccessToken | Code::TokenExpired | Code::TokenRevoked | Code::SecurityVersionStale | Code::MfaRequired => tonic::Code::Unauthenticated, Code::Forbidden | Code::LoginForbidden | Code::UserDisabled | Code::UnderMinimumAge | Code::ScopeInsufficient | Code::AgreementNotAccepted => tonic::Code::PermissionDenied, Code::MfaNotConfigured | Code::TooManyAccessKeys | Code::TotpAlreadyEnabled | Code::TotpStepReused | Code::DeletionAlreadyRequested | Code::DeletionNotRequested | Code::OrganizationRoleInUse | Code::OrganizationRoleSystem | Code::OrganizationInviteExhausted | Code::OrganizationInviteExpired | Code::OrganizationOwnerRequired => tonic::Code::FailedPrecondition, Code::Conflict | Code::UsernameTaken | Code::EmailTaken | Code::SshKeyTaken | Code::OrganizationSlugTaken | Code::OrganizationMemberExists | Code::OrganizationRoleKeyTaken | Code::OrganizationHasMembers | Code::VersionMismatch | Code::EmailUnchanged => tonic::Code::AlreadyExists, Code::NotFound | Code::UserNotFound | Code::SessionNotFound | Code::AccessKeyNotFound | Code::SshKeyNotFound | Code::AgreementNotFound | Code::OrganizationNotFound | Code::OrganizationMemberNotFound | Code::OrganizationRoleNotFound | Code::OrganizationInviteNotFound | Code::TokenNotFound => tonic::Code::NotFound, Code::OrganizationInactive | Code::OrganizationMemberBanned => { tonic::Code::PermissionDenied } Code::InvalidPermission => tonic::Code::InvalidArgument, Code::RateLimited | Code::EmailCodeAttemptsExceeded | Code::RecoveryCodesExhausted | Code::PayloadTooLarge => tonic::Code::ResourceExhausted, Code::Internal | Code::DatabaseError | Code::CacheError | Code::SerializationError | Code::EmailError => tonic::Code::Internal, Code::Unavailable => tonic::Code::Unavailable, } } pub fn detail(error: &AuthError, request_id: Option<&str>) -> ErrorDetail { let retry_after_ms = match error { AuthError::RateLimited { retry_after_secs } => (*retry_after_secs).min(3600) as u32 * 1000, _ => 0, }; let current_security_version = match error { AuthError::SecurityVersionStale { current } => *current, _ => 0, }; ErrorDetail { code: code_of(error) as i32, message: error.message(), retry_after_ms, current_security_version, request_id: request_id.unwrap_or_default().to_owned(), offending_fields: Vec::new(), } } /// 业务错误详情的 type_url(放进 `google.rpc.Status.details`)。 pub const DETAIL_TYPE_URL: &str = "type.googleapis.com/auth.common.v1.ErrorDetail"; /// 组装 `grpc-status-details-bin`。 /// /// 按 gRPC 规范,这个 trailer 必须是 `google.rpc.Status`:外层 `code` 与 `grpc-status` /// 一致,业务错误码放在 `details` 的 `Any` 里。直接把自定义 message 塞进去会被严格客户端 /// (grpcurl、Go 的 status.FromError)解析成错误的 code。 fn with_details(code: tonic::Code, message: String, detail: ErrorDetail) -> Status { let outer = RpcStatus { code: code as i32, message: message.clone(), details: vec![Any { type_url: DETAIL_TYPE_URL.to_owned(), value: detail.encode_to_vec(), }], }; Status::with_details(code, message, outer.encode_to_vec().into()) } pub fn status(error: &AuthError, request_id: Option<&str>) -> Status { let code = code_of(error); let detail = detail(error, request_id); with_details(tonic_code(code), error.message(), detail) } pub fn status_of(code: Code, message: impl Into, request_id: Option<&str>) -> Status { let detail = ErrorDetail { code: code as i32, message: message.into(), retry_after_ms: 0, current_security_version: 0, request_id: request_id.unwrap_or_default().to_owned(), offending_fields: Vec::new(), }; let message = detail.message.clone(); with_details(tonic_code(code), message, detail) } pub fn unauthenticated(message: impl Into, request_id: Option<&str>) -> Status { status_of(Code::Unauthenticated, message, request_id) } pub fn invalid_argument(message: impl Into, request_id: Option<&str>) -> Status { status_of(Code::InvalidArgument, message, request_id) } pub fn forbidden(message: impl Into, request_id: Option<&str>) -> Status { status_of(Code::Forbidden, message, request_id) } pub fn unavailable(message: impl Into, request_id: Option<&str>) -> Status { status_of(Code::Unavailable, message, request_id) }