Skip to content
Clone

refactor: remove unused modules and clean up imports for improved maintainability

ZhenYi committed
parent d13fe56 commit ef0b83a Browse files

153 files changed

+4070 −12024

This change is very large, so some diffs are not shown. Clone the repository to see the full change.

File tree 153
Modified lib/auth/src/api/error.rs View lib/auth/src/api/error.rs
@@ -41,6 +41,8 @@
4141 AuthError::InvalidResetToken => Code::InvalidResetToken,
4242 AuthError::Unauthenticated => Code::Unauthenticated,
4343 AuthError::Forbidden => Code::Forbidden,
44 AuthError::BadRequest(_) => Code::MalformedRequest,
45 AuthError::Conflict(_) => Code::VersionMismatch,
4446 AuthError::InvalidCredentials => Code::InvalidCredentials,
4547 AuthError::LoginForbidden { .. } => Code::LoginForbidden,
4648 AuthError::AccountUnavailable { .. } => Code::UserDisabled,
Deleted lib/auth/src/api/organization.rs

Diff too large to display.

Modified lib/auth/src/handlers/identity/mod.rs View lib/auth/src/handlers/identity/mod.rs
@@ -2,7 +2,6 @@
22pub mod login;
33pub mod logout;
44pub mod me;
5pub mod organization;
65pub mod password_reset;
76pub mod register;
87pub mod ssh_key;
Deleted lib/auth/src/services/organization/error.rs

Diff too large to display.

Deleted lib/auth/src/services/organization/mod.rs

Diff too large to display.

Deleted lib/auth/src/services/organization/service.rs

Diff too large to display.

Modified lib/auth/src/services/lifecycle.rs View lib/auth/src/services/lifecycle.rs
@@ -6,7 +6,7 @@
66
77use crate::AuthService;
88use crate::errors::{AuthError, AuthResult};
9use crate::models::space::{OrganizationStatus, organization_members, organizations};
9use crate::models::space::{SpaceStatus, space_members, spaces};
1010use crate::models::users::{user_mfa, user_mfa_backup_codes, user_sessions, user_ssh_keys, users};
1111
1212impl AuthService {
@@ -31,10 +31,10 @@
3131 if user.deletion_requested_at.is_some() || user.deleted_at.is_some() {
3232 return Err(AuthError::DeletionAlreadyRequested);
3333 }
34 let owned = organizations::Entity::find()
35 .filter(organizations::Column::OwnerUserId.eq(user_id))
36 .filter(organizations::Column::DeletedAt.is_null())
37 .filter(organizations::Column::Status.ne(OrganizationStatus::Archived))
34 let owned = spaces::Entity::find()
35 .filter(spaces::Column::OwnerUserId.eq(user_id))
36 .filter(spaces::Column::DeletedAt.is_null())
37 .filter(spaces::Column::Status.ne(SpaceStatus::Archived))
3838 .count(&txn)
3939 .await?;
4040 if owned > 0 {
@@ -87,10 +87,10 @@
8787
8888 let now = Utc::now();
8989 revoke_credentials(&txn, user_id, now).await?;
90 organization_members::Entity::update_many()
91 .col_expr(organization_members::Column::LeftAt, Expr::value(now))
92 .filter(organization_members::Column::UserId.eq(user_id))
93 .filter(organization_members::Column::LeftAt.is_null())
90 space_members::Entity::update_many()
91 .col_expr(space_members::Column::LeftAt, Expr::value(now))
92 .filter(space_members::Column::UserId.eq(user_id))
93 .filter(space_members::Column::LeftAt.is_null())
9494 .exec(&txn)
9595 .await?;
9696
Modified lib/auth/src/services/mod.rs View lib/auth/src/services/mod.rs
@@ -3,7 +3,6 @@
33pub mod lifecycle;
44pub mod me;
55pub mod notifications;
6pub mod organization;
76pub mod preferences;
87pub mod profile;
98pub mod reports;
Modified lib/auth/src/api.rs View lib/auth/src/api.rs
@@ -8,7 +8,6 @@
88use crate::protos::auth::internal::v1::authority_service_server::AuthorityService;
99use crate::protos::auth::v1::identity_service_server::IdentityService;
1010use crate::protos::auth::v1::mfa_service_server::MfaService;
11use crate::protos::auth::v1::organization_service_server::OrganizationService;
1211use crate::protos::auth::v1::session_service_server::SessionService;
1312use crate::protos::auth::v1::ssh_key_service_server::SshKeyService;
1413
@@ -18,7 +17,6 @@
1817pub mod error;
1918pub mod identity;
2019pub mod mfa;
21pub mod organization;
2220pub mod session;
2321pub mod ssh_key;
2422pub mod store;
@@ -36,10 +34,6 @@
3634 crate::api::session::SessionApi::new(self.clone())
3735 }
3836
39 fn organization(&self) -> crate::api::organization::OrganizationApi {
40 crate::api::organization::OrganizationApi::new(self.clone())
41 }
42
4337 fn ssh_key(&self) -> crate::api::ssh_key::SshKeyApi {
4438 crate::api::ssh_key::SshKeyApi::new(self.clone())
4539 }
@@ -511,856 +505,5 @@
511505 .into_inner()
512506 .user
513507 .ok_or(KenAuthError::NotFound)
514 }
515
516 async fn ensure_bot_member(
517 &self,
518 organization_id: &str,
519 user_id: i64,
520 role_keys: Vec<String>,
521 role_ids: Vec<String>,
522 ) -> Result<proto::v1::OrganizationMemberInfo, KenAuthError> {
523 let client = self.organization();
524 client
525 .ensure_bot_member(self.write_request(proto::v1::EnsureBotMemberRequest {
526 organization_id: organization_id.to_string(),
527 user_id,
528 role_keys,
529 role_ids,
530 }))
531 .await
532 .map_err(KenAuthError::from_status)?
533 .into_inner()
534 .member
535 .ok_or(KenAuthError::NotFound)
536 }
537
538 async fn get_organization(
539 &self,
540 actor_user_id: i64,
541 organization_id: &str,
542 slug: &str,
543 ) -> Result<proto::v1::GetOrganizationResponse, KenAuthError> {
544 let client = self.organization();
545 client
546 .get_organization(self.read_request(proto::v1::GetOrganizationRequest {
547 actor_user_id,
548 organization_id: organization_id.to_string(),
549 slug: slug.to_string(),
550 }))
551 .await
552 .map_err(KenAuthError::from_status)
553 .map(|response| response.into_inner())
554 }
555
556 async fn get_member(
557 &self,
558 actor_user_id: i64,
559 organization_id: &str,
560 user_id: i64,
561 ) -> Result<Option<proto::v1::OrganizationMemberInfo>, KenAuthError> {
562 let client = self.organization();
563 match client
564 .get_member(self.read_request(proto::v1::GetMemberRequest {
565 actor_user_id,
566 organization_id: organization_id.to_string(),
567 member_id: String::new(),
568 user_id,
569 }))
570 .await
571 {
572 Ok(response) => Ok(response.into_inner().member),
573 Err(status) if status.code() == tonic::Code::NotFound => Ok(None),
574 Err(status) => Err(KenAuthError::from_status(status)),
575 }
576 }
577
578 async fn list_members(
579 &self,
580 actor_user_id: i64,
581 organization_id: &str,
582 include_inactive: bool,
583 include_banned: bool,
584 page: proto::common::v1::PageRequest,
585 ) -> Result<proto::v1::ListMembersResponse, KenAuthError> {
586 let client = self.organization();
587 client
588 .list_members(self.read_request(proto::v1::ListMembersRequest {
589 actor_user_id,
590 organization_id: organization_id.to_string(),
591 include_inactive,
592 include_banned,
593 page: Some(page),
594 }))
595 .await
596 .map_err(KenAuthError::from_status)
597 .map(|response| response.into_inner())
598 }
599
600 async fn list_organizations(
601 &self,
602 actor_user_id: i64,
603 user_id: i64,
604 include_archived: bool,
605 page: proto::common::v1::PageRequest,
606 ) -> Result<proto::v1::ListOrganizationsResponse, KenAuthError> {
607 let client = self.organization();
608 client
609 .list_organizations(self.read_request(proto::v1::ListOrganizationsRequest {
610 actor_user_id,
611 user_id,
612 include_archived,
613 page: Some(page),
614 }))
615 .await
616 .map_err(KenAuthError::from_status)
617 .map(|response| response.into_inner())
618 }
619
620 async fn list_public_organizations(
621 &self,
622 query: &str,
623 page: proto::common::v1::PageRequest,
624 ) -> Result<proto::v1::ListPublicOrganizationsResponse, KenAuthError> {
625 let client = self.organization();
626 client
627 .list_public_organizations(self.read_request(
628 proto::v1::ListPublicOrganizationsRequest {
629 query: query.to_string(),
630 page: Some(page),
631 },
632 ))
633 .await
634 .map_err(KenAuthError::from_status)
635 .map(|response| response.into_inner())
636 }
637
638 async fn check_permissions(
639 &self,
640 actor_user_id: i64,
641 organization_id: &str,
642 user_id: i64,
643 permissions: Vec<String>,
644 ) -> Result<proto::v1::CheckPermissionsResponse, KenAuthError> {
645 let client = self.organization();
646 client
647 .check_permissions(self.read_request(proto::v1::CheckPermissionsRequest {
648 actor_user_id,
649 organization_id: organization_id.to_string(),
650 user_id,
651 permissions,
652 }))
653 .await
654 .map_err(KenAuthError::from_status)
655 .map(|response| response.into_inner())
656 }
657
658 async fn create_organization(
659 &self,
660 actor_user_id: i64,
661 slug: &str,
662 name: &str,
663 description: &str,
664 avatar_url: &str,
665 join_policy: Option<i32>,
666 allow_join: Option<bool>,
667 require_email_verified: Option<bool>,
668 require_mfa: Option<bool>,
669 welcome_message: Option<String>,
670 member_must_enable_2fa: Option<bool>,
671 allow_invite: Option<bool>,
672 allow_join_level: Option<i32>,
673 ) -> Result<proto::v1::CreateOrganizationResponse, KenAuthError> {
674 let client = self.organization();
675 client
676 .create_organization(self.write_request(proto::v1::CreateOrganizationRequest {
677 actor_user_id,
678 slug: slug.to_string(),
679 name: name.to_string(),
680 description: description.to_string(),
681 avatar_url: avatar_url.to_string(),
682 join_policy,
683 allow_join,
684 require_email_verified,
685 require_mfa,
686 welcome_message,
687 member_must_enable_2fa,
688 allow_invite,
689 allow_join_level,
690 }))
691 .await
692 .map_err(KenAuthError::from_status)
693 .map(|response| response.into_inner())
694 }
695
696 #[allow(clippy::too_many_arguments)]
697 async fn update_organization(
698 &self,
699 actor_user_id: i64,
700 organization_id: &str,
701 name: Option<String>,
702 description: Option<String>,
703 avatar_url: Option<String>,
704 expected_version: i32,
705 join_policy: Option<i32>,
706 allow_join: Option<bool>,
707 require_email_verified: Option<bool>,
708 require_mfa: Option<bool>,
709 welcome_message: Option<String>,
710 member_must_enable_2fa: Option<bool>,
711 allow_invite: Option<bool>,
712 allow_join_level: Option<i32>,
713 ) -> Result<proto::v1::OrganizationInfo, KenAuthError> {
714 let client = self.organization();
715 client
716 .update_organization(self.write_request(proto::v1::UpdateOrganizationRequest {
717 actor_user_id,
718 organization_id: organization_id.to_string(),
719 name,
720 description,
721 avatar_url,
722 expected_version,
723 join_policy,
724 allow_join,
725 require_email_verified,
726 require_mfa,
727 welcome_message,
728 member_must_enable_2fa,
729 allow_invite,
730 allow_join_level,
731 }))
732 .await
733 .map_err(KenAuthError::from_status)?
734 .into_inner()
735 .organization
736 .ok_or(KenAuthError::NotFound)
737 }
738
739 async fn archive_organization(
740 &self,
741 actor_user_id: i64,
742 organization_id: &str,
743 reason: &str,
744 ) -> Result<(), KenAuthError> {
745 let client = self.organization();
746 client
747 .archive_organization(self.write_request(proto::v1::ArchiveOrganizationRequest {
748 actor_user_id,
749 organization_id: organization_id.to_string(),
750 reason: reason.to_string(),
751 }))
752 .await
753 .map_err(KenAuthError::from_status)?;
754 Ok(())
755 }
756
757 async fn restore_organization(
758 &self,
759 actor_user_id: i64,
760 organization_id: &str,
761 ) -> Result<(), KenAuthError> {
762 let client = self.organization();
763 client
764 .restore_organization(self.write_request(proto::v1::RestoreOrganizationRequest {
765 actor_user_id,
766 organization_id: organization_id.to_string(),
767 }))
768 .await
769 .map_err(KenAuthError::from_status)?;
770 Ok(())
771 }
772
773 async fn delete_organization(
774 &self,
775 actor_user_id: i64,
776 organization_id: &str,
777 force: bool,
778 reason: &str,
779 ) -> Result<(), KenAuthError> {
780 let client = self.organization();
781 client
782 .delete_organization(self.write_request(proto::v1::DeleteOrganizationRequest {
783 actor_user_id,
784 organization_id: organization_id.to_string(),
785 force,
786 reason: reason.to_string(),
787 }))
788 .await
789 .map_err(KenAuthError::from_status)?;
790 Ok(())
791 }
792
793 async fn add_member(
794 &self,
795 actor_user_id: i64,
796 organization_id: &str,
797 user_id: i64,
798 role: i32,
799 role_keys: Vec<String>,
800 ) -> Result<(proto::v1::OrganizationMemberInfo, bool), KenAuthError> {
801 let client = self.organization();
802 let response = client
803 .add_member(self.write_request(proto::v1::AddMemberRequest {
804 actor_user_id,
805 organization_id: organization_id.to_string(),
806 user_id,
807 role,
808 role_keys,
809 }))
810 .await
811 .map_err(KenAuthError::from_status)?
812 .into_inner();
813 let member = response.member.ok_or(KenAuthError::NotFound)?;
814 Ok((member, response.created))
815 }
816
817 async fn remove_member(
818 &self,
819 actor_user_id: i64,
820 organization_id: &str,
821 user_id: i64,
822 reason: &str,
823 ) -> Result<bool, KenAuthError> {
824 let client = self.organization();
825 client
826 .remove_member(self.write_request(proto::v1::RemoveMemberRequest {
827 actor_user_id,
828 organization_id: organization_id.to_string(),
829 member_id: String::new(),
830 user_id,
831 reason: reason.to_string(),
832 }))
833 .await
834 .map_err(KenAuthError::from_status)
835 .map(|response| response.into_inner().removed)
836 }
837
838 async fn leave_organization(
839 &self,
840 actor_user_id: i64,
841 organization_id: &str,
842 ) -> Result<bool, KenAuthError> {
843 let client = self.organization();
844 client
845 .leave_organization(self.write_request(proto::v1::LeaveOrganizationRequest {
846 actor_user_id,
847 organization_id: organization_id.to_string(),
848 }))
849 .await
850 .map_err(KenAuthError::from_status)
851 .map(|response| response.into_inner().left)
852 }
853
854 #[allow(clippy::too_many_arguments)]
855 async fn update_member(
856 &self,
857 actor_user_id: i64,
858 organization_id: &str,
859 user_id: i64,
860 alias_name: Option<Option<String>>,
861 alias_avatar_url: Option<Option<String>>,
862 message_notifications: Option<String>,
863 muted: Option<bool>,
864 mute_until: Option<Option<prost_types::Timestamp>>,
865 mobile_push: Option<bool>,
866 suppress_everyone: Option<bool>,
867 suppress_roles: Option<bool>,
868 hide_muted_channels: Option<bool>,
869 ) -> Result<proto::v1::OrganizationMemberInfo, KenAuthError> {
870 let (clear_mute_until, mute_until) = match mute_until {
871 Some(None) => (Some(true), None),
872 Some(Some(value)) => (None, Some(value)),
873 None => (None, None),
874 };
875 let client = self.organization();
876 client
877 .update_member(self.write_request(proto::v1::UpdateMemberRequest {
878 actor_user_id,
879 organization_id: organization_id.to_string(),
880 member_id: String::new(),
881 alias_name: alias_name.map(|value| value.unwrap_or_default()),
882 alias_avatar_url: alias_avatar_url.map(|value| value.unwrap_or_default()),
883 message_notifications,
884 muted,
885 clear_mute_until,
886 mute_until,
887 mobile_push,
888 suppress_everyone,
889 suppress_roles,
890 hide_muted_channels,
891 expected_version: 0,
892 user_id,
893 }))
894 .await
895 .map_err(KenAuthError::from_status)?
896 .into_inner()
897 .member
898 .ok_or(KenAuthError::NotFound)
899 }
900
901 async fn ban_member(
902 &self,
903 actor_user_id: i64,
904 organization_id: &str,
905 user_id: i64,
906 reason: &str,
907 ) -> Result<proto::v1::OrganizationMemberInfo, KenAuthError> {
908 let client = self.organization();
909 client
910 .ban_member(self.write_request(proto::v1::BanMemberRequest {
911 actor_user_id,
912 organization_id: organization_id.to_string(),
913 member_id: String::new(),
914 reason: reason.to_string(),
915 user_id,
916 }))
917 .await
918 .map_err(KenAuthError::from_status)?
919 .into_inner()
920 .member
921 .ok_or(KenAuthError::NotFound)
922 }
923
924 async fn unban_member(
925 &self,
926 actor_user_id: i64,
927 organization_id: &str,
928 user_id: i64,
929 ) -> Result<proto::v1::OrganizationMemberInfo, KenAuthError> {
930 let client = self.organization();
931 client
932 .unban_member(self.write_request(proto::v1::UnbanMemberRequest {
933 actor_user_id,
934 organization_id: organization_id.to_string(),
935 member_id: String::new(),
936 user_id,
937 }))
938 .await
939 .map_err(KenAuthError::from_status)?
940 .into_inner()
941 .member
942 .ok_or(KenAuthError::NotFound)
943 }
944
945 #[allow(clippy::too_many_arguments)]
946 async fn update_ban(
947 &self,
948 actor_user_id: i64,
949 organization_id: &str,
950 user_id: i64,
951 reason: Option<Option<String>>,
952 expires_at: Option<Option<prost_types::Timestamp>>,
953 allow_speak: Option<bool>,
954 allow_push_repo: Option<bool>,
955 allow_comment_any: Option<bool>,
956 ) -> Result<proto::v1::OrganizationMemberInfo, KenAuthError> {
957 let (clear_expires_at, expires_at) = match expires_at {
958 Some(None) => (Some(true), None),
959 Some(Some(value)) => (None, Some(value)),
960 None => (None, None),
961 };
962 let client = self.organization();
963 client
964 .update_ban(self.write_request(proto::v1::UpdateBanRequest {
965 actor_user_id,
966 organization_id: organization_id.to_string(),
967 user_id,
968 reason: reason.map(|value| value.unwrap_or_default()),
969 clear_expires_at,
970 expires_at,
971 allow_agent_join: None,
972 allow_speak,
973 allow_push_repo,
974 allow_comment_any,
975 }))
976 .await
977 .map_err(KenAuthError::from_status)?
978 .into_inner()
979 .ban
980 .ok_or(KenAuthError::NotFound)
981 }
982
983 #[allow(clippy::too_many_arguments)]
984 async fn create_role(
985 &self,
986 actor_user_id: i64,
987 organization_id: &str,
988 key: &str,
989 name: &str,
990 description: &str,
991 rank: i32,
992 self_assignable: bool,
993 permissions: Vec<String>,
994 ) -> Result<proto::v1::OrganizationRoleInfo, KenAuthError> {
995 let client = self.organization();
996 client
997 .create_role(self.write_request(proto::v1::CreateRoleRequest {
998 actor_user_id,
999 organization_id: organization_id.to_string(),
1000 key: key.to_string(),
1001 name: name.to_string(),
1002 description: description.to_string(),
1003 rank,
1004 default_role: false,
1005 permissions: permissions.into_iter().map(allow_grant).collect(),
1006 self_assignable,
1007 }))
1008 .await
1009 .map_err(KenAuthError::from_status)?
1010 .into_inner()
1011 .role
1012 .ok_or(KenAuthError::NotFound)
1013 }
1014
1015 #[allow(clippy::too_many_arguments)]
1016 async fn update_role(
1017 &self,
1018 actor_user_id: i64,
1019 organization_id: &str,
1020 role_id: &str,
1021 name: Option<String>,
1022 description: Option<String>,
1023 rank: Option<i32>,
1024 self_assignable: Option<bool>,
1025 expected_version: i32,
1026 ) -> Result<proto::v1::OrganizationRoleInfo, KenAuthError> {
1027 let client = self.organization();
1028 client
1029 .update_role(self.write_request(proto::v1::UpdateRoleRequest {
1030 actor_user_id,
1031 organization_id: organization_id.to_string(),
1032 role_id: role_id.to_string(),
1033 name,
1034 description,
1035 rank,
1036 default_role: None,
1037 expected_version,
1038 self_assignable,
1039 }))
1040 .await
1041 .map_err(KenAuthError::from_status)?
1042 .into_inner()
1043 .role
1044 .ok_or(KenAuthError::NotFound)
1045 }
1046
1047 async fn set_role_permissions(
1048 &self,
1049 actor_user_id: i64,
1050 organization_id: &str,
1051 role_id: &str,
1052 permissions: Vec<String>,
1053 ) -> Result<proto::v1::OrganizationRoleInfo, KenAuthError> {
1054 let client = self.organization();
1055 client
1056 .set_role_permissions(self.write_request(proto::v1::SetRolePermissionsRequest {
1057 actor_user_id,
1058 organization_id: organization_id.to_string(),
1059 role_id: role_id.to_string(),
1060 permissions: permissions.into_iter().map(allow_grant).collect(),
1061 }))
1062 .await
1063 .map_err(KenAuthError::from_status)?
1064 .into_inner()
1065 .role
1066 .ok_or(KenAuthError::NotFound)
1067 }
1068
1069 async fn delete_role(
1070 &self,
1071 actor_user_id: i64,
1072 organization_id: &str,
1073 role_id: &str,
1074 ) -> Result<bool, KenAuthError> {
1075 let client = self.organization();
1076 client
1077 .delete_role(self.write_request(proto::v1::DeleteRoleRequest {
1078 actor_user_id,
1079 organization_id: organization_id.to_string(),
1080 role_id: role_id.to_string(),
1081 }))
1082 .await
1083 .map_err(KenAuthError::from_status)
1084 .map(|response| response.into_inner().deleted)
1085 }
1086
1087 async fn assign_role(
1088 &self,
1089 actor_user_id: i64,
1090 organization_id: &str,
1091 user_id: i64,
1092 role_id: &str,
1093 ) -> Result<proto::v1::OrganizationMemberInfo, KenAuthError> {
1094 let client = self.organization();
1095 client
1096 .assign_role(self.write_request(proto::v1::AssignRoleRequest {
1097 actor_user_id,
1098 organization_id: organization_id.to_string(),
1099 member_id: String::new(),
1100 role_id: role_id.to_string(),
1101 user_id,
1102 }))
1103 .await
1104 .map_err(KenAuthError::from_status)?
1105 .into_inner()
1106 .member
1107 .ok_or(KenAuthError::NotFound)
1108 }
1109
1110 async fn unassign_role(
1111 &self,
1112 actor_user_id: i64,
1113 organization_id: &str,
1114 user_id: i64,
1115 role_id: &str,
1116 ) -> Result<proto::v1::OrganizationMemberInfo, KenAuthError> {
1117 let client = self.organization();
1118 client
1119 .unassign_role(self.write_request(proto::v1::UnassignRoleRequest {
1120 actor_user_id,
1121 organization_id: organization_id.to_string(),
1122 member_id: String::new(),
1123 role_id: role_id.to_string(),
1124 user_id,
1125 }))
1126 .await
1127 .map_err(KenAuthError::from_status)?
1128 .into_inner()
1129 .member
1130 .ok_or(KenAuthError::NotFound)
1131 }
1132
1133 #[allow(clippy::too_many_arguments)]
1134 async fn create_invite(
1135 &self,
1136 actor_user_id: i64,
1137 organization_id: &str,
1138 method: i32,
1139 max_uses: i32,
1140 expires_at: Option<prost_types::Timestamp>,
1141 role_ids: Vec<String>,
1142 target_user_id: i64,
1143 email: &str,
1144 ) -> Result<(proto::v1::OrganizationInviteInfo, String), KenAuthError> {
1145 let client = self.organization();
1146 let response = client
1147 .create_invite(self.write_request(proto::v1::CreateInviteRequest {
1148 actor_user_id,
1149 organization_id: organization_id.to_string(),
1150 method,
1151 max_uses,
1152 expires_at,
1153 role_id: role_ids.first().cloned().unwrap_or_default(),
1154 target_user_id,
1155 email: email.to_string(),
1156 role_ids,
1157 }))
1158 .await
1159 .map_err(KenAuthError::from_status)?
1160 .into_inner();
1161 let invite = response.invite.ok_or(KenAuthError::NotFound)?;
1162 Ok((invite, response.token))
1163 }
1164
1165 async fn revoke_invite(
1166 &self,
1167 actor_user_id: i64,
1168 organization_id: &str,
1169 invite_id: &str,
1170 ) -> Result<bool, KenAuthError> {
1171 let client = self.organization();
1172 client
1173 .revoke_invite(self.write_request(proto::v1::RevokeInviteRequest {
1174 actor_user_id,
1175 organization_id: organization_id.to_string(),
1176 invite_id: invite_id.to_string(),
1177 }))
1178 .await
1179 .map_err(KenAuthError::from_status)
1180 .map(|response| response.into_inner().revoked)
1181 }
1182
1183 async fn redeem_invite(
1184 &self,
1185 actor_user_id: i64,
1186 token: &str,
1187 ) -> Result<proto::v1::RedeemInviteResponse, KenAuthError> {
1188 let client = self.organization();
1189 client
1190 .redeem_invite(self.write_request(proto::v1::RedeemInviteRequest {
1191 actor_user_id,
1192 token: token.to_string(),
1193 }))
1194 .await
1195 .map_err(KenAuthError::from_status)
1196 .map(|response| response.into_inner())
1197 }
1198
1199 async fn create_join_rule(
1200 &self,
1201 actor_user_id: i64,
1202 organization_id: &str,
1203 role_id: &str,
1204 trigger: &str,
1205 matcher: &str,
1206 enabled: bool,
1207 ) -> Result<proto::v1::JoinRuleInfo, KenAuthError> {
1208 let client = self.organization();
1209 client
1210 .create_join_rule(self.write_request(proto::v1::CreateJoinRuleRequest {
1211 actor_user_id,
1212 organization_id: organization_id.to_string(),
1213 role_id: role_id.to_string(),
1214 trigger: trigger.to_string(),
1215 matcher: matcher.to_string(),
1216 enabled,
1217 }))
1218 .await
1219 .map_err(KenAuthError::from_status)?
1220 .into_inner()
1221 .rule
1222 .ok_or(KenAuthError::NotFound)
1223 }
1224
1225 #[allow(clippy::too_many_arguments)]
1226 async fn update_join_rule(
1227 &self,
1228 actor_user_id: i64,
1229 organization_id: &str,
1230 rule_id: &str,
1231 role_id: Option<String>,
1232 trigger: Option<String>,
1233 matcher: Option<String>,
1234 enabled: Option<bool>,
1235 ) -> Result<proto::v1::JoinRuleInfo, KenAuthError> {
1236 let client = self.organization();
1237 client
1238 .update_join_rule(self.write_request(proto::v1::UpdateJoinRuleRequest {
1239 actor_user_id,
1240 organization_id: organization_id.to_string(),
1241 rule_id: rule_id.to_string(),
1242 role_id,
1243 trigger,
1244 matcher,
1245 enabled,
1246 }))
1247 .await
1248 .map_err(KenAuthError::from_status)?
1249 .into_inner()
1250 .rule
1251 .ok_or(KenAuthError::NotFound)
1252 }
1253
1254 async fn delete_join_rule(
1255 &self,
1256 actor_user_id: i64,
1257 organization_id: &str,
1258 rule_id: &str,
1259 ) -> Result<bool, KenAuthError> {
1260 let client = self.organization();
1261 client
1262 .delete_join_rule(self.write_request(proto::v1::DeleteJoinRuleRequest {
1263 actor_user_id,
1264 organization_id: organization_id.to_string(),
1265 rule_id: rule_id.to_string(),
1266 }))
1267 .await
1268 .map_err(KenAuthError::from_status)
1269 .map(|response| response.into_inner().deleted)
1270 }
1271
1272 async fn create_join_request(
1273 &self,
1274 actor_user_id: i64,
1275 organization_id: &str,
1276 note: &str,
1277 ) -> Result<proto::v1::JoinRequestInfo, KenAuthError> {
1278 let client = self.organization();
1279 client
1280 .create_join_request(self.write_request(proto::v1::CreateJoinRequestRequest {
1281 actor_user_id,
1282 organization_id: organization_id.to_string(),
1283 note: note.to_string(),
1284 }))
1285 .await
1286 .map_err(KenAuthError::from_status)?
1287 .into_inner()
1288 .request
1289 .ok_or(KenAuthError::NotFound)
1290 }
1291
1292 async fn withdraw_join_request(
1293 &self,
1294 actor_user_id: i64,
1295 organization_id: &str,
1296 request_id: &str,
1297 ) -> Result<proto::v1::JoinRequestInfo, KenAuthError> {
1298 let client = self.organization();
1299 client
1300 .withdraw_join_request(self.write_request(proto::v1::WithdrawJoinRequestRequest {
1301 actor_user_id,
1302 organization_id: organization_id.to_string(),
1303 request_id: request_id.to_string(),
1304 }))
1305 .await
1306 .map_err(KenAuthError::from_status)?
1307 .into_inner()
1308 .request
1309 .ok_or(KenAuthError::NotFound)
1310 }
1311
1312 async fn approve_join_request(
1313 &self,
1314 actor_user_id: i64,
1315 organization_id: &str,
1316 request_id: &str,
1317 ) -> Result<
1318 (
1319 proto::v1::JoinRequestInfo,
1320 Option<proto::v1::OrganizationMemberInfo>,
1321 ),
1322 KenAuthError,
1323 > {
1324 let client = self.organization();
1325 let response = client
1326 .approve_join_request(self.write_request(proto::v1::ApproveJoinRequestRequest {
1327 actor_user_id,
1328 organization_id: organization_id.to_string(),
1329 request_id: request_id.to_string(),
1330 }))
1331 .await
1332 .map_err(KenAuthError::from_status)?
1333 .into_inner();
1334 let request = response.request.ok_or(KenAuthError::NotFound)?;
1335 Ok((request, response.member))
1336 }
1337
1338 async fn reject_join_request(
1339 &self,
1340 actor_user_id: i64,
1341 organization_id: &str,
1342 request_id: &str,
1343 note: &str,
1344 ) -> Result<proto::v1::JoinRequestInfo, KenAuthError> {
1345 let client = self.organization();
1346 client
1347 .reject_join_request(self.write_request(proto::v1::RejectJoinRequestRequest {
1348 actor_user_id,
1349 organization_id: organization_id.to_string(),
1350 request_id: request_id.to_string(),
1351 note: note.to_string(),
1352 }))
1353 .await
1354 .map_err(KenAuthError::from_status)?
1355 .into_inner()
1356 .request
1357 .ok_or(KenAuthError::NotFound)
1358 }
1359}
1360
1361fn allow_grant(permission: String) -> proto::v1::OrganizationPermissionGrant {
1362 proto::v1::OrganizationPermissionGrant {
1363 permission,
1364 effect: proto::v1::PermissionEffect::Allow as i32,
1365508 }
1366509}
Modified lib/auth/src/errors.rs View lib/auth/src/errors.rs
@@ -49,6 +49,10 @@
4949 Unauthenticated,
5050 #[error("forbidden")]
5151 Forbidden,
52 #[error("{0}")]
53 BadRequest(String),
54 #[error("{0}")]
55 Conflict(String),
5256 #[error("username or password is incorrect")]
5357 InvalidCredentials,
5458 #[error("login is forbidden{}", reason.as_deref().map(|reason| format!(": {reason}")).unwrap_or_default())]
@@ -187,6 +191,7 @@
187191 | Self::SecurityVersionStale { .. }
188192 | Self::MfaRequired => StatusCode::UNAUTHORIZED,
189193 Self::Forbidden
194 | Self::BadRequest(_)
190195 | Self::LoginForbidden { .. }
191196 | Self::UnderMinimumAge { .. }
192197 | Self::AccountUnavailable { .. }
@@ -206,6 +211,7 @@
206211 | Self::AgreementNotFound { .. } => StatusCode::NOT_FOUND,
207212 Self::Unavailable(_) => StatusCode::SERVICE_UNAVAILABLE,
208213 Self::UsernameTaken
214 | Self::Conflict(_)
209215 | Self::EmailTaken
210216 | Self::SshKeyTaken
211217 | Self::OrganizationSlugTaken
@@ -257,6 +263,8 @@
257263 Self::InvalidResetToken => "invalid_reset_token",
258264 Self::Unauthenticated => "unauthenticated",
259265 Self::Forbidden => "forbidden",
266 Self::BadRequest(_) => "bad_request",
267 Self::Conflict(_) => "conflict",
260268 Self::InvalidCredentials => "invalid_credentials",
261269 Self::LoginForbidden { .. } => "login_forbidden",
262270 Self::AccountUnavailable { .. } => "account_unavailable",
@@ -489,3 +497,43 @@
489497 }
490498 }
491499}
500
501impl From<space::errors::SpaceError> for AuthError {
502 fn from(error: space::errors::SpaceError) -> Self {
503 use space::errors::SpaceError as E;
504 match error {
505 E::BadRequest(message) => Self::BadRequest(message),
506 E::Conflict(message) => Self::Conflict(message),
507 E::Unauthorized => Self::Unauthenticated,
508 E::Forbidden
509 | E::JoinNotAllowed(_)
510 | E::JoinRequirementNotMet(_)
511 | E::JoinRequestCooldown(_)
512 | E::SpaceInactive
513 | E::SpaceMemberBanned => Self::Forbidden,
514 E::MalformedRequest => Self::MalformedRequest,
515 E::InvalidEmail => Self::InvalidEmail,
516 E::InvalidPermission { permission } => Self::InvalidPermission { permission },
517 E::NotFound | E::SpaceNotFound => Self::OrganizationNotFound,
518 E::SpaceMemberNotFound => Self::OrganizationMemberNotFound,
519 E::SpaceRoleNotFound => Self::OrganizationRoleNotFound,
520 E::SpaceInviteNotFound => Self::OrganizationInviteNotFound,
521 E::SpaceSlugTaken => Self::OrganizationSlugTaken,
522 E::SpaceMemberExists => Self::OrganizationMemberExists,
523 E::SpaceRoleKeyTaken => Self::OrganizationRoleKeyTaken,
524 E::SpaceRoleInUse => Self::OrganizationRoleInUse,
525 E::SpaceRoleSystem => Self::OrganizationRoleSystem,
526 E::SpaceInviteExhausted => Self::OrganizationInviteExhausted,
527 E::SpaceInviteExpired => Self::OrganizationInviteExpired,
528 E::SpaceOwnerRequired => Self::OrganizationOwnerRequired,
529 E::SpaceHasMembers => Self::OrganizationHasMembers,
530 E::VersionMismatch { current } => Self::VersionMismatch { current },
531 E::UserNotFound => Self::UserNotFound,
532 E::ScopeInsufficient { required } => Self::ScopeInsufficient { required },
533 E::RateLimited(_) => Self::RateLimited {
534 retry_after_secs: 0,
535 },
536 E::Internal(message) => Self::Internal(message),
537 }
538 }
539}
Modified lib/auth/src/openapi.rs View lib/auth/src/openapi.rs
@@ -22,75 +22,8 @@
2222 crate::handlers::identity::ssh_key::ssh_key_list,
2323 crate::handlers::identity::ssh_key::ssh_key_add,
2424 crate::handlers::identity::ssh_key::ssh_key_delete,
25 crate::handlers::identity::organization::create_organization,
26 crate::handlers::identity::organization::list_organizations,
27 crate::handlers::identity::organization::get_organization,
28 crate::handlers::identity::organization::update_organization,
29 crate::handlers::identity::organization::archive_organization,
30 crate::handlers::identity::organization::restore_organization,
31 crate::handlers::identity::organization::delete_organization,
32 crate::handlers::identity::organization::transfer_ownership,
33 crate::handlers::identity::organization::leave_organization,
34 crate::handlers::identity::organization::list_members,
35 crate::handlers::identity::organization::get_member,
36 crate::handlers::identity::organization::update_member,
37 crate::handlers::identity::organization::update_self_member,
38 crate::handlers::identity::organization::add_member,
39 crate::handlers::identity::organization::remove_member,
40 crate::handlers::identity::organization::set_member_role,
41 crate::handlers::identity::organization::ban_member,
42 crate::handlers::identity::organization::unban_member,
43 crate::handlers::identity::organization::set_member_permissions,
44 crate::handlers::identity::organization::list_bans,
45 crate::handlers::identity::organization::get_ban,
46 crate::handlers::identity::organization::update_ban,
47 crate::handlers::identity::organization::unban_by_user,
48 crate::handlers::identity::organization::list_roles,
49 crate::handlers::identity::organization::get_role,
50 crate::handlers::identity::organization::create_role,
51 crate::handlers::identity::organization::update_role,
52 crate::handlers::identity::organization::delete_role,
53 crate::handlers::identity::organization::set_role_permissions,
54 crate::handlers::identity::organization::assign_role,
55 crate::handlers::identity::organization::unassign_role,
56 crate::handlers::identity::organization::join_role,
57 crate::handlers::identity::organization::leave_role,
58 crate::handlers::identity::organization::check_permission,
59 crate::handlers::identity::organization::create_invite,
60 crate::handlers::identity::organization::list_invites,
61 crate::handlers::identity::organization::revoke_invite,
62 crate::handlers::identity::organization::redeem_invite,
6325 ),
6426 components(schemas(
65 crate::services::organization::types::OrganizationSummary,
66 crate::services::organization::types::MemberSummary,
67 crate::services::organization::types::RoleSummary,
68 crate::services::organization::types::InviteSummary,
69 crate::services::organization::types::PermissionGrant,
70 crate::services::organization::types::PermissionCheck,
71 crate::services::organization::types::CreateOrganizationBody,
72 crate::services::organization::types::UpdateOrganizationBody,
73 crate::services::organization::types::AddMemberBody,
74 crate::services::organization::types::SetMemberRoleBody,
75 crate::services::organization::types::BanMemberBody,
76 crate::services::organization::types::CheckPermissionBody,
77 crate::services::organization::types::CreateRoleBody,
78 crate::services::organization::types::SetRolePermissionsBody,
79 crate::handlers::identity::organization::OrgPath,
80 crate::handlers::identity::organization::OrgMemberPath,
81 crate::handlers::identity::organization::OrgRolePath,
82 crate::handlers::identity::organization::OrgInvitePath,
83 crate::handlers::identity::organization::OrgListQuery,
84 crate::handlers::identity::organization::MemberListQuery,
85 crate::handlers::identity::organization::UpdateRoleBody,
86 crate::handlers::identity::organization::TransferBody,
87 crate::handlers::identity::organization::ReasonBody,
88 crate::handlers::identity::organization::DeleteOrgQuery,
89 crate::handlers::identity::organization::CreateInviteBody,
90 crate::handlers::identity::organization::RedeemInviteBody,
91 crate::handlers::identity::organization::CreateInviteResponse,
92 crate::handlers::identity::organization::RedeemInviteResponse,
93 crate::handlers::identity::organization::AssignRoleBody,
9427 ))
9528)]
9629pub struct ApiDoc;
Modified lib/auth/src/routes.rs View lib/auth/src/routes.rs
@@ -58,156 +58,4 @@
5858 "/auth/ssh-keys/{key_id}",
5959 web::delete().to(crate::handlers::identity::ssh_key::ssh_key_delete),
6060 );
61 cfg.route(
62 "/organizations",
63 web::post().to(crate::handlers::identity::organization::create_organization),
64 );
65 cfg.route(
66 "/organizations",
67 web::get().to(crate::handlers::identity::organization::list_organizations),
68 );
69 cfg.route(
70 "/organizations/invites/redeem",
71 web::post().to(crate::handlers::identity::organization::redeem_invite),
72 );
73 cfg.route(
74 "/organizations/{organization_id}",
75 web::get().to(crate::handlers::identity::organization::get_organization),
76 );
77 cfg.route(
78 "/organizations/{organization_id}",
79 web::patch().to(crate::handlers::identity::organization::update_organization),
80 );
81 cfg.route(
82 "/organizations/{organization_id}",
83 web::delete().to(crate::handlers::identity::organization::delete_organization),
84 );
85 cfg.route(
86 "/organizations/{organization_id}/archive",
87 web::post().to(crate::handlers::identity::organization::archive_organization),
88 );
89 cfg.route(
90 "/organizations/{organization_id}/restore",
91 web::post().to(crate::handlers::identity::organization::restore_organization),
92 );
93 cfg.route(
94 "/organizations/{organization_id}/transfer",
95 web::post().to(crate::handlers::identity::organization::transfer_ownership),
96 );
97 cfg.route(
98 "/organizations/{organization_id}/leave",
99 web::post().to(crate::handlers::identity::organization::leave_organization),
100 );
101 cfg.route(
102 "/organizations/{organization_id}/members",
103 web::get().to(crate::handlers::identity::organization::list_members),
104 );
105 cfg.route(
106 "/organizations/{organization_id}/members",
107 web::post().to(crate::handlers::identity::organization::add_member),
108 );
109 cfg.route(
110 "/organizations/{organization_id}/members/@me",
111 web::patch().to(crate::handlers::identity::organization::update_self_member),
112 );
113 cfg.route(
114 "/organizations/{organization_id}/members/{member_id}",
115 web::get().to(crate::handlers::identity::organization::get_member),
116 );
117 cfg.route(
118 "/organizations/{organization_id}/members/{member_id}",
119 web::patch().to(crate::handlers::identity::organization::update_member),
120 );
121 cfg.route(
122 "/organizations/{organization_id}/members/{member_id}",
123 web::delete().to(crate::handlers::identity::organization::remove_member),
124 );
125 cfg.route(
126 "/organizations/{organization_id}/members/{member_id}/role",
127 web::patch().to(crate::handlers::identity::organization::set_member_role),
128 );
129 cfg.route(
130 "/organizations/{organization_id}/members/{member_id}/ban",
131 web::post().to(crate::handlers::identity::organization::ban_member),
132 );
133 cfg.route(
134 "/organizations/{organization_id}/members/{member_id}/unban",
135 web::post().to(crate::handlers::identity::organization::unban_member),
136 );
137 cfg.route(
138 "/organizations/{organization_id}/members/{member_id}/permissions",
139 web::put().to(crate::handlers::identity::organization::set_member_permissions),
140 );
141 cfg.route(
142 "/organizations/{organization_id}/bans",
143 web::get().to(crate::handlers::identity::organization::list_bans),
144 );
145 cfg.route(
146 "/organizations/{organization_id}/bans/{user_id}",
147 web::get().to(crate::handlers::identity::organization::get_ban),
148 );
149 cfg.route(
150 "/organizations/{organization_id}/bans/{user_id}",
151 web::patch().to(crate::handlers::identity::organization::update_ban),
152 );
153 cfg.route(
154 "/organizations/{organization_id}/bans/{user_id}",
155 web::delete().to(crate::handlers::identity::organization::unban_by_user),
156 );
157 cfg.route(
158 "/organizations/{organization_id}/roles",
159 web::get().to(crate::handlers::identity::organization::list_roles),
160 );
161 cfg.route(
162 "/organizations/{organization_id}/roles",
163 web::post().to(crate::handlers::identity::organization::create_role),
164 );
165 cfg.route(
166 "/organizations/{organization_id}/roles/assign",
167 web::post().to(crate::handlers::identity::organization::assign_role),
168 );
169 cfg.route(
170 "/organizations/{organization_id}/roles/unassign",
171 web::post().to(crate::handlers::identity::organization::unassign_role),
172 );
173 cfg.route(
174 "/organizations/{organization_id}/roles/{role_id}",
175 web::get().to(crate::handlers::identity::organization::get_role),
176 );
177 cfg.route(
178 "/organizations/{organization_id}/roles/{role_id}",
179 web::patch().to(crate::handlers::identity::organization::update_role),
180 );
181 cfg.route(
182 "/organizations/{organization_id}/roles/{role_id}/join",
183 web::post().to(crate::handlers::identity::organization::join_role),
184 );
185 cfg.route(
186 "/organizations/{organization_id}/roles/{role_id}/join",
187 web::delete().to(crate::handlers::identity::organization::leave_role),
188 );
189 cfg.route(
190 "/organizations/{organization_id}/roles/{role_id}",
191 web::delete().to(crate::handlers::identity::organization::delete_role),
192 );
193 cfg.route(
194 "/organizations/{organization_id}/roles/{role_id}/permissions",
195 web::put().to(crate::handlers::identity::organization::set_role_permissions),
196 );
197 cfg.route(
198 "/organizations/{organization_id}/permissions/check",
199 web::post().to(crate::handlers::identity::organization::check_permission),
200 );
201 cfg.route(
202 "/organizations/{organization_id}/invites",
203 web::post().to(crate::handlers::identity::organization::create_invite),
204 );
205 cfg.route(
206 "/organizations/{organization_id}/invites",
207 web::get().to(crate::handlers::identity::organization::list_invites),
208 );
209 cfg.route(
210 "/organizations/{organization_id}/invites/{invite_id}",
211 web::delete().to(crate::handlers::identity::organization::revoke_invite),
212 );
21361}
Modified lib/auth/Cargo.toml View lib/auth/Cargo.toml
@@ -23,6 +23,7 @@
2323redis = { workspace = true }
2424queue = { workspace = true }
2525session = { workspace = true }
26space = { workspace = true }
2627tokio-stream = { version = "0.1", features = ["net"] }
2728actix-web = "4.16.0"
2829aes-gcm = "0.11.1"
Modified lib/auth/OpenApi.Auth.json View lib/auth/OpenApi.Auth.json
@@ -1254,2190 +1254,10 @@
12541254 }
12551255 }
12561256 }
1257 },
1258 "/api/v1/organizations": {
1259 "get": {
1260 "tags": [
1261 "auth"
1262 ],
1263 "summary": "列示我加入的组织",
1264 "description": "默认只返回 ACTIVE 组织;`include_archived=true` 时一并返回已归档组织。",
1265 "operationId": "auth_organization_list",
1266 "responses": {
1267 "200": {
1268 "description": "组织列表",
1269 "content": {
1270 "application/json": {
1271 "schema": {
1272 "$ref": "#/components/schemas/Envelope_Vec_OrganizationSummary"
1273 }
1274 }
1275 }
1276 },
1277 "401": {
1278 "description": "会话未绑定账号",
1279 "content": {
1280 "application/json": {
1281 "schema": {
1282 "$ref": "#/components/schemas/Envelope_NoData"
1283 }
1284 }
1285 }
1286 }
1287 }
1288 },
1289 "post": {
1290 "tags": [
1291 "auth"
1292 ],
1293 "summary": "新建组织",
1294 "description": "创建者自动成为 OWNER 并写入 owner/admin/member 三个内置角色。slug 全局唯一且只允许小写字母、数字与连字符。",
1295 "operationId": "auth_organization_create",
1296 "requestBody": {
1297 "content": {
1298 "application/json": {
1299 "schema": {
1300 "$ref": "#/components/schemas/CreateOrganizationBody"
1301 }
1302 }
1303 },
1304 "required": true
1305 },
1306 "responses": {
1307 "200": {
1308 "description": "组织已创建",
1309 "content": {
1310 "application/json": {
1311 "schema": {
1312 "$ref": "#/components/schemas/Envelope_OrganizationSummary"
1313 }
1314 }
1315 }
1316 },
1317 "401": {
1318 "description": "会话未绑定账号",
1319 "content": {
1320 "application/json": {
1321 "schema": {
1322 "$ref": "#/components/schemas/Envelope_NoData"
1323 }
1324 }
1325 }
1326 },
1327 "409": {
1328 "description": "slug 已被占用",
1329 "content": {
1330 "application/json": {
1331 "schema": {
1332 "$ref": "#/components/schemas/Envelope_NoData"
1333 }
1334 }
1335 }
1336 }
1337 }
1338 }
1339 },
1340 "/api/v1/organizations/invites/redeem": {
1341 "post": {
1342 "tags": [
1343 "auth"
1344 ],
1345 "summary": "兑换邀请",
1346 "description": "uses 递增与兑换记录写入同事务。定向邀请只能由目标用户兑换。",
1347 "operationId": "auth_organization_invite_redeem",
1348 "requestBody": {
1349 "content": {
1350 "application/json": {
1351 "schema": {
1352 "$ref": "#/components/schemas/RedeemInviteBody"
1353 }
1354 }
1355 },
1356 "required": true
1357 },
1358 "responses": {
1359 "200": {
1360 "description": "已加入",
1361 "content": {
1362 "application/json": {
1363 "schema": {
1364 "$ref": "#/components/schemas/Envelope_RedeemInviteResponse"
1365 }
1366 }
1367 }
1368 },
1369 "403": {
1370 "description": "定向邀请目标不匹配",
1371 "content": {
1372 "application/json": {
1373 "schema": {
1374 "$ref": "#/components/schemas/Envelope_NoData"
1375 }
1376 }
1377 }
1378 },
1379 "409": {
1380 "description": "邀请已用尽",
1381 "content": {
1382 "application/json": {
1383 "schema": {
1384 "$ref": "#/components/schemas/Envelope_NoData"
1385 }
1386 }
1387 }
1388 }
1389 }
1390 }
1391 },
1392 "/api/v1/organizations/{organization_id}": {
1393 "get": {
1394 "tags": [
1395 "auth"
1396 ],
1397 "summary": "组织详情",
1398 "operationId": "auth_organization_get",
1399 "parameters": [
1400 {
1401 "name": "organization_id",
1402 "in": "path",
1403 "description": "组织 id",
1404 "required": true,
1405 "schema": {
1406 "type": "string",
1407 "format": "uuid"
1408 }
1409 }
1410 ],
1411 "responses": {
1412 "200": {
1413 "description": "组织详情",
1414 "content": {
1415 "application/json": {
1416 "schema": {
1417 "$ref": "#/components/schemas/Envelope_OrganizationSummary"
1418 }
1419 }
1420 }
1421 },
1422 "404": {
1423 "description": "组织不存在",
1424 "content": {
1425 "application/json": {
1426 "schema": {
1427 "$ref": "#/components/schemas/Envelope_NoData"
1428 }
1429 }
1430 }
1431 }
1432 }
1433 },
1434 "delete": {
1435 "tags": [
1436 "auth"
1437 ],
1438 "summary": "删除组织",
1439 "description": "仍存在活跃成员且未指定 `force=true` 时返回 409。删除会清理成员、角色与邀请。",
1440 "operationId": "auth_organization_delete",
1441 "parameters": [
1442 {
1443 "name": "organization_id",
1444 "in": "path",
1445 "description": "组织 id",
1446 "required": true,
1447 "schema": {
1448 "type": "string",
1449 "format": "uuid"
1450 }
1451 },
1452 {
1453 "name": "force",
1454 "in": "query",
1455 "description": "是否连成员一并清理",
1456 "required": false,
1457 "schema": {
1458 "type": "boolean"
1459 }
1460 },
1461 {
1462 "name": "reason",
1463 "in": "query",
1464 "description": "删除原因,最长 512 字符",
1465 "required": false,
1466 "schema": {
1467 "type": "string"
1468 }
1469 }
1470 ],
1471 "responses": {
1472 "200": {
1473 "description": "已删除",
1474 "content": {
1475 "application/json": {
1476 "schema": {
1477 "$ref": "#/components/schemas/Envelope_bool"
1478 }
1479 }
1480 }
1481 },
1482 "403": {
1483 "description": "权限不足",
1484 "content": {
1485 "application/json": {
1486 "schema": {
1487 "$ref": "#/components/schemas/Envelope_NoData"
1488 }
1489 }
1490 }
1491 },
1492 "409": {
1493 "description": "仍有活跃成员",
1494 "content": {
1495 "application/json": {
1496 "schema": {
1497 "$ref": "#/components/schemas/Envelope_NoData"
1498 }
1499 }
1500 }
1501 }
1502 }
1503 },
1504 "patch": {
1505 "tags": [
1506 "auth"
1507 ],
1508 "summary": "修改组织",
1509 "description": "需要 `org.update`。`expected_version` 与当前 version 不一致时返回 409。",
1510 "operationId": "auth_organization_update",
1511 "parameters": [
1512 {
1513 "name": "organization_id",
1514 "in": "path",
1515 "description": "组织 id",
1516 "required": true,
1517 "schema": {
1518 "type": "string",
1519 "format": "uuid"
1520 }
1521 }
1522 ],
1523 "requestBody": {
1524 "content": {
1525 "application/json": {
1526 "schema": {
1527 "$ref": "#/components/schemas/UpdateOrganizationBody"
1528 }
1529 }
1530 },
1531 "required": true
1532 },
1533 "responses": {
1534 "200": {
1535 "description": "已更新",
1536 "content": {
1537 "application/json": {
1538 "schema": {
1539 "$ref": "#/components/schemas/Envelope_OrganizationSummary"
1540 }
1541 }
1542 }
1543 },
1544 "403": {
1545 "description": "权限不足",
1546 "content": {
1547 "application/json": {
1548 "schema": {
1549 "$ref": "#/components/schemas/Envelope_NoData"
1550 }
1551 }
1552 }
1553 },
1554 "409": {
1555 "description": "版本冲突",
1556 "content": {
1557 "application/json": {
1558 "schema": {
1559 "$ref": "#/components/schemas/Envelope_NoData"
1560 }
1561 }
1562 }
1563 }
1564 }
1565 }
1566 },
1567 "/api/v1/organizations/{organization_id}/archive": {
1568 "post": {
1569 "tags": [
1570 "auth"
1571 ],
1572 "summary": "归档组织",
1573 "description": "需要 `org.archive`。归档后全部权限判定返回 false,可恢复。",
1574 "operationId": "auth_organization_archive",
1575 "parameters": [
1576 {
1577 "name": "organization_id",
1578 "in": "path",
1579 "description": "组织 id",
1580 "required": true,
1581 "schema": {
1582 "type": "string",
1583 "format": "uuid"
1584 }
1585 }
1586 ],
1587 "requestBody": {
1588 "content": {
1589 "application/json": {
1590 "schema": {
1591 "$ref": "#/components/schemas/ReasonBody"
1592 }
1593 }
1594 },
1595 "required": true
1596 },
1597 "responses": {
1598 "200": {
1599 "description": "已归档",
1600 "content": {
1601 "application/json": {
1602 "schema": {
1603 "$ref": "#/components/schemas/Envelope_OrganizationSummary"
1604 }
1605 }
1606 }
1607 },
1608 "403": {
1609 "description": "权限不足",
1610 "content": {
1611 "application/json": {
1612 "schema": {
1613 "$ref": "#/components/schemas/Envelope_NoData"
1614 }
1615 }
1616 }
1617 }
1618 }
1619 }
1620 },
1621 "/api/v1/organizations/{organization_id}/bans": {
1622 "get": {
1623 "tags": [
1624 "auth"
1625 ],
1626 "summary": "封禁列表",
1627 "description": "需要 `member.ban`。按封禁时间倒序。",
1628 "operationId": "auth_organization_ban_list",
1629 "parameters": [
1630 {
1631 "name": "organization_id",
1632 "in": "path",
1633 "description": "组织 id",
1634 "required": true,
1635 "schema": {
1636 "type": "string",
1637 "format": "uuid"
1638 }
1639 },
1640 {
1641 "name": "page",
1642 "in": "query",
1643 "description": "页码,从 1 开始",
1644 "required": false,
1645 "schema": {
1646 "type": "integer",
1647 "format": "int64",
1648 "minimum": 0
1649 }
1650 },
1651 {
1652 "name": "page_size",
1653 "in": "query",
1654 "description": "每页条数",
1655 "required": false,
1656 "schema": {
1657 "type": "integer",
1658 "format": "int64",
1659 "minimum": 0
1660 }
1661 }
1662 ],
1663 "responses": {
1664 "200": {
1665 "description": "封禁列表",
1666 "content": {
1667 "application/json": {
1668 "schema": {
1669 "$ref": "#/components/schemas/Envelope_Vec_MemberSummary"
1670 }
1671 }
1672 }
1673 },
1674 "403": {
1675 "description": "权限不足",
1676 "content": {
1677 "application/json": {
1678 "schema": {
1679 "$ref": "#/components/schemas/Envelope_NoData"
1680 }
1681 }
1682 }
1683 }
1684 }
1685 }
1686 },
1687 "/api/v1/organizations/{organization_id}/bans/{user_id}": {
1688 "get": {
1689 "tags": [
1690 "auth"
1691 ],
1692 "summary": "封禁详情",
1693 "description": "需要 `member.ban`。未封禁返回 404。",
1694 "operationId": "auth_organization_ban_get",
1695 "parameters": [
1696 {
1697 "name": "organization_id",
1698 "in": "path",
1699 "description": "组织 id",
1700 "required": true,
1701 "schema": {
1702 "type": "string",
1703 "format": "uuid"
1704 }
1705 },
1706 {
1707 "name": "user_id",
1708 "in": "path",
1709 "description": "被封禁用户 id",
1710 "required": true,
1711 "schema": {
1712 "type": "integer",
1713 "format": "int64"
1714 }
1715 }
1716 ],
1717 "responses": {
1718 "200": {
1719 "description": "封禁详情",
1720 "content": {
1721 "application/json": {
1722 "schema": {
1723 "$ref": "#/components/schemas/Envelope_MemberSummary"
1724 }
1725 }
1726 }
1727 },
1728 "404": {
1729 "description": "未封禁或成员不存在",
1730 "content": {
1731 "application/json": {
1732 "schema": {
1733 "$ref": "#/components/schemas/Envelope_NoData"
1734 }
1735 }
1736 }
1737 }
1738 }
1739 },
1740 "delete": {
1741 "tags": [
1742 "auth"
1743 ],
1744 "summary": "按用户解封",
1745 "description": "需要 `member.ban`。等价于按会籍 id 解封,便于按用户维度操作。",
1746 "operationId": "auth_organization_ban_delete",
1747 "parameters": [
1748 {
1749 "name": "organization_id",
1750 "in": "path",
1751 "description": "组织 id",
1752 "required": true,
1753 "schema": {
1754 "type": "string",
1755 "format": "uuid"
1756 }
1757 },
1758 {
1759 "name": "user_id",
1760 "in": "path",
1761 "description": "被解封用户 id",
1762 "required": true,
1763 "schema": {
1764 "type": "integer",
1765 "format": "int64"
1766 }
1767 }
1768 ],
1769 "responses": {
1770 "200": {
1771 "description": "已解封",
1772 "content": {
1773 "application/json": {
1774 "schema": {
1775 "$ref": "#/components/schemas/Envelope_MemberSummary"
1776 }
1777 }
1778 }
1779 },
1780 "404": {
1781 "description": "成员不存在",
1782 "content": {
1783 "application/json": {
1784 "schema": {
1785 "$ref": "#/components/schemas/Envelope_NoData"
1786 }
1787 }
1788 }
1789 }
1790 }
1791 },
1792 "patch": {
1793 "tags": [
1794 "auth"
1795 ],
1796 "summary": "更新封禁",
1797 "description": "需要 `member.ban`。改原因、到期时间与四个放行位;未封禁返回 404。",
1798 "operationId": "auth_organization_ban_update",
1799 "parameters": [
1800 {
1801 "name": "organization_id",
1802 "in": "path",
1803 "description": "组织 id",
1804 "required": true,
1805 "schema": {
1806 "type": "string",
1807 "format": "uuid"
1808 }
1809 },
1810 {
1811 "name": "user_id",
1812 "in": "path",
1813 "description": "被封禁用户 id",
1814 "required": true,
1815 "schema": {
1816 "type": "integer",
1817 "format": "int64"
1818 }
1819 }
1820 ],
1821 "requestBody": {
1822 "content": {
1823 "application/json": {
1824 "schema": {
1825 "$ref": "#/components/schemas/UpdateBanBody"
1826 }
1827 }
1828 },
1829 "required": true
1830 },
1831 "responses": {
1832 "200": {
1833 "description": "已更新",
1834 "content": {
1835 "application/json": {
1836 "schema": {
1837 "$ref": "#/components/schemas/Envelope_MemberSummary"
1838 }
1839 }
1840 }
1841 },
1842 "404": {
1843 "description": "未封禁或成员不存在",
1844 "content": {
1845 "application/json": {
1846 "schema": {
1847 "$ref": "#/components/schemas/Envelope_NoData"
1848 }
1849 }
1850 }
1851 }
1852 }
1853 }
1854 },
1855 "/api/v1/organizations/{organization_id}/invites": {
1856 "get": {
1857 "tags": [
1858 "auth"
1859 ],
1860 "summary": "列示邀请",
1861 "description": "需要 `invite.view`。不含 token 明文。",
1862 "operationId": "auth_organization_invite_list",
1863 "parameters": [
1864 {
1865 "name": "organization_id",
1866 "in": "path",
1867 "description": "组织 id",
1868 "required": true,
1869 "schema": {
1870 "type": "string",
1871 "format": "uuid"
1872 }
1873 },
1874 {
1875 "name": "include_revoked",
1876 "in": "query",
1877 "description": "包含已撤销邀请",
1878 "required": false,
1879 "schema": {
1880 "type": "boolean"
1881 }
1882 }
1883 ],
1884 "responses": {
1885 "200": {
1886 "description": "邀请列表",
1887 "content": {
1888 "application/json": {
1889 "schema": {
1890 "$ref": "#/components/schemas/Envelope_Vec_InviteSummary"
1891 }
1892 }
1893 }
1894 },
1895 "403": {
1896 "description": "权限不足",
1897 "content": {
1898 "application/json": {
1899 "schema": {
1900 "$ref": "#/components/schemas/Envelope_NoData"
1901 }
1902 }
1903 }
1904 }
1905 }
1906 },
1907 "post": {
1908 "tags": [
1909 "auth"
1910 ],
1911 "summary": "创建邀请",
1912 "description": "需要 `invite.create`。明文 token 只在创建响应里返回一次,之后只存 sha256。",
1913 "operationId": "auth_organization_invite_create",
1914 "parameters": [
1915 {
1916 "name": "organization_id",
1917 "in": "path",
1918 "description": "组织 id",
1919 "required": true,
1920 "schema": {
1921 "type": "string",
1922 "format": "uuid"
1923 }
1924 }
1925 ],
1926 "requestBody": {
1927 "content": {
1928 "application/json": {
1929 "schema": {
1930 "$ref": "#/components/schemas/CreateInviteBody"
1931 }
1932 }
1933 },
1934 "required": true
1935 },
1936 "responses": {
1937 "200": {
1938 "description": "邀请已创建",
1939 "content": {
1940 "application/json": {
1941 "schema": {
1942 "$ref": "#/components/schemas/Envelope_CreateInviteResponse"
1943 }
1944 }
1945 }
1946 },
1947 "403": {
1948 "description": "权限不足",
1949 "content": {
1950 "application/json": {
1951 "schema": {
1952 "$ref": "#/components/schemas/Envelope_NoData"
1953 }
1954 }
1955 }
1956 }
1957 }
1958 }
1959 },
1960 "/api/v1/organizations/{organization_id}/invites/{invite_id}": {
1961 "delete": {
1962 "tags": [
1963 "auth"
1964 ],
1965 "summary": "撤销邀请",
1966 "description": "需要 `invite.revoke`。已撤销的邀请再次撤销返回 false。",
1967 "operationId": "auth_organization_invite_revoke",
1968 "parameters": [
1969 {
1970 "name": "organization_id",
1971 "in": "path",
1972 "description": "组织 id",
1973 "required": true,
1974 "schema": {
1975 "type": "string",
1976 "format": "uuid"
1977 }
1978 },
1979 {
1980 "name": "invite_id",
1981 "in": "path",
1982 "description": "邀请 id",
1983 "required": true,
1984 "schema": {
1985 "type": "string",
1986 "format": "uuid"
1987 }
1988 }
1989 ],
1990 "responses": {
1991 "200": {
1992 "description": "已撤销",
1993 "content": {
1994 "application/json": {
1995 "schema": {
1996 "$ref": "#/components/schemas/Envelope_bool"
1997 }
1998 }
1999 }
2000 },
2001 "404": {
2002 "description": "邀请不存在",
2003 "content": {
2004 "application/json": {
2005 "schema": {
2006 "$ref": "#/components/schemas/Envelope_NoData"
2007 }
2008 }
2009 }
2010 }
2011 }
2012 }
2013 },
2014 "/api/v1/organizations/{organization_id}/leave": {
2015 "post": {
2016 "tags": [
2017 "auth"
2018 ],
2019 "summary": "退出组织",
2020 "description": "OWNER 必须先转移所有权,否则返回 409。",
2021 "operationId": "auth_organization_leave",
2022 "parameters": [
2023 {
2024 "name": "organization_id",
2025 "in": "path",
2026 "description": "组织 id",
2027 "required": true,
2028 "schema": {
2029 "type": "string",
2030 "format": "uuid"
2031 }
2032 }
2033 ],
2034 "responses": {
2035 "200": {
2036 "description": "已退出",
2037 "content": {
2038 "application/json": {
2039 "schema": {
2040 "$ref": "#/components/schemas/Envelope_bool"
2041 }
2042 }
2043 }
2044 },
2045 "409": {
2046 "description": "OWNER 需先转移所有权",
2047 "content": {
2048 "application/json": {
2049 "schema": {
2050 "$ref": "#/components/schemas/Envelope_NoData"
2051 }
2052 }
2053 }
2054 }
2055 }
2056 }
2057 },
2058 "/api/v1/organizations/{organization_id}/members": {
2059 "get": {
2060 "tags": [
2061 "auth"
2062 ],
2063 "summary": "列示组织成员",
2064 "description": "需要 `member.view`。默认只返回活跃成员。",
2065 "operationId": "auth_organization_member_list",
2066 "parameters": [
2067 {
2068 "name": "organization_id",
2069 "in": "path",
2070 "description": "组织 id",
2071 "required": true,
2072 "schema": {
2073 "type": "string",
2074 "format": "uuid"
2075 }
2076 },
2077 {
2078 "name": "include_inactive",
2079 "in": "query",
2080 "description": "包含已退出成员",
2081 "required": false,
2082 "schema": {
2083 "type": "boolean"
2084 }
2085 },
2086 {
2087 "name": "include_banned",
2088 "in": "query",
2089 "description": "包含已封禁成员",
2090 "required": false,
2091 "schema": {
2092 "type": "boolean"
2093 }
2094 }
2095 ],
2096 "responses": {
2097 "200": {
2098 "description": "成员列表",
2099 "content": {
2100 "application/json": {
2101 "schema": {
2102 "$ref": "#/components/schemas/Envelope_Vec_MemberSummary"
2103 }
2104 }
2105 }
2106 },
2107 "403": {
2108 "description": "权限不足",
2109 "content": {
2110 "application/json": {
2111 "schema": {
2112 "$ref": "#/components/schemas/Envelope_NoData"
2113 }
2114 }
2115 }
2116 }
2117 }
2118 },
2119 "post": {
2120 "tags": [
2121 "auth"
2122 ],
2123 "summary": "直接添加成员",
2124 "description": "需要 `member.add`。已退出的会籍按复活处理,已封禁的返回 403。",
2125 "operationId": "auth_organization_member_add",
2126 "parameters": [
2127 {
2128 "name": "organization_id",
2129 "in": "path",
2130 "description": "组织 id",
2131 "required": true,
2132 "schema": {
2133 "type": "string",
2134 "format": "uuid"
2135 }
2136 }
2137 ],
2138 "requestBody": {
2139 "content": {
2140 "application/json": {
2141 "schema": {
2142 "$ref": "#/components/schemas/AddMemberBody"
2143 }
2144 }
2145 },
2146 "required": true
2147 },
2148 "responses": {
2149 "200": {
2150 "description": "成员已添加",
2151 "content": {
2152 "application/json": {
2153 "schema": {
2154 "$ref": "#/components/schemas/Envelope_MemberSummary"
2155 }
2156 }
2157 }
2158 },
2159 "403": {
2160 "description": "权限不足或成员已封禁",
2161 "content": {
2162 "application/json": {
2163 "schema": {
2164 "$ref": "#/components/schemas/Envelope_NoData"
2165 }
2166 }
2167 }
2168 },
2169 "404": {
2170 "description": "用户不存在",
2171 "content": {
2172 "application/json": {
2173 "schema": {
2174 "$ref": "#/components/schemas/Envelope_NoData"
2175 }
2176 }
2177 }
2178 }
2179 }
2180 }
2181 },
2182 "/api/v1/organizations/{organization_id}/members/@me": {
2183 "patch": {
2184 "tags": [
2185 "auth"
2186 ],
2187 "summary": "更新自己的会籍资料",
2188 "description": "只允许修改自己的别名、头像与通知偏好;无需 `member.role.update`。",
2189 "operationId": "auth_organization_member_update_self",
2190 "parameters": [
2191 {
2192 "name": "organization_id",
2193 "in": "path",
2194 "description": "组织 id",
2195 "required": true,
2196 "schema": {
2197 "type": "string",
2198 "format": "uuid"
2199 }
2200 }
2201 ],
2202 "requestBody": {
2203 "content": {
2204 "application/json": {
2205 "schema": {
2206 "$ref": "#/components/schemas/UpdateMemberBody"
2207 }
2208 }
2209 },
2210 "required": true
2211 },
2212 "responses": {
2213 "200": {
2214 "description": "已更新",
2215 "content": {
2216 "application/json": {
2217 "schema": {
2218 "$ref": "#/components/schemas/Envelope_MemberSummary"
2219 }
2220 }
2221 }
2222 },
2223 "403": {
2224 "description": "不是活跃成员",
2225 "content": {
2226 "application/json": {
2227 "schema": {
2228 "$ref": "#/components/schemas/Envelope_NoData"
2229 }
2230 }
2231 }
2232 }
2233 }
2234 }
2235 },
2236 "/api/v1/organizations/{organization_id}/members/{member_id}": {
2237 "get": {
2238 "tags": [
2239 "auth"
2240 ],
2241 "summary": "成员详情",
2242 "description": "需要 `member.view`。包含别名、通知偏好与封禁附加信息。",
2243 "operationId": "auth_organization_member_get",
2244 "parameters": [
2245 {
2246 "name": "organization_id",
2247 "in": "path",
2248 "description": "组织 id",
2249 "required": true,
2250 "schema": {
2251 "type": "string",
2252 "format": "uuid"
2253 }
2254 },
2255 {
2256 "name": "member_id",
2257 "in": "path",
2258 "description": "会籍 id",
2259 "required": true,
2260 "schema": {
2261 "type": "string",
2262 "format": "uuid"
2263 }
2264 }
2265 ],
2266 "responses": {
2267 "200": {
2268 "description": "成员详情",
2269 "content": {
2270 "application/json": {
2271 "schema": {
2272 "$ref": "#/components/schemas/Envelope_MemberSummary"
2273 }
2274 }
2275 }
2276 },
2277 "404": {
2278 "description": "成员不存在",
2279 "content": {
2280 "application/json": {
2281 "schema": {
2282 "$ref": "#/components/schemas/Envelope_NoData"
2283 }
2284 }
2285 }
2286 }
2287 }
2288 },
2289 "delete": {
2290 "tags": [
2291 "auth"
2292 ],
2293 "summary": "移除成员",
2294 "description": "需要 `member.remove`。OWNER 不能移除,需先转移所有权。会籍行保留用于复活。",
2295 "operationId": "auth_organization_member_remove",
2296 "parameters": [
2297 {
2298 "name": "organization_id",
2299 "in": "path",
2300 "description": "组织 id",
2301 "required": true,
2302 "schema": {
2303 "type": "string",
2304 "format": "uuid"
2305 }
2306 },
2307 {
2308 "name": "member_id",
2309 "in": "path",
2310 "description": "会籍 id",
2311 "required": true,
2312 "schema": {
2313 "type": "string",
2314 "format": "uuid"
2315 }
2316 },
2317 {
2318 "name": "reason",
2319 "in": "query",
2320 "description": "移除原因",
2321 "required": false,
2322 "schema": {
2323 "type": "string"
2324 }
2325 }
2326 ],
2327 "responses": {
2328 "200": {
2329 "description": "已移除",
2330 "content": {
2331 "application/json": {
2332 "schema": {
2333 "$ref": "#/components/schemas/Envelope_bool"
2334 }
2335 }
2336 }
2337 },
2338 "403": {
2339 "description": "权限不足或目标是 OWNER",
2340 "content": {
2341 "application/json": {
2342 "schema": {
2343 "$ref": "#/components/schemas/Envelope_NoData"
2344 }
2345 }
2346 }
2347 }
2348 }
2349 },
2350 "patch": {
2351 "tags": [
2352 "auth"
2353 ],
2354 "summary": "更新成员资料",
2355 "description": "需要 `member.role.update`。修改别名、头像、通知偏好与免打扰;封禁中的会籍走封禁接口。",
2356 "operationId": "auth_organization_member_update",
2357 "parameters": [
2358 {
2359 "name": "organization_id",
2360 "in": "path",
2361 "description": "组织 id",
2362 "required": true,
2363 "schema": {
2364 "type": "string",
2365 "format": "uuid"
2366 }
2367 },
2368 {
2369 "name": "member_id",
2370 "in": "path",
2371 "description": "会籍 id",
2372 "required": true,
2373 "schema": {
2374 "type": "string",
2375 "format": "uuid"
2376 }
2377 }
2378 ],
2379 "requestBody": {
2380 "content": {
2381 "application/json": {
2382 "schema": {
2383 "$ref": "#/components/schemas/UpdateMemberBody"
2384 }
2385 }
2386 },
2387 "required": true
2388 },
2389 "responses": {
2390 "200": {
2391 "description": "已更新",
2392 "content": {
2393 "application/json": {
2394 "schema": {
2395 "$ref": "#/components/schemas/Envelope_MemberSummary"
2396 }
2397 }
2398 }
2399 },
2400 "403": {
2401 "description": "权限不足或目标已封禁",
2402 "content": {
2403 "application/json": {
2404 "schema": {
2405 "$ref": "#/components/schemas/Envelope_NoData"
2406 }
2407 }
2408 }
2409 },
2410 "409": {
2411 "description": "版本冲突",
2412 "content": {
2413 "application/json": {
2414 "schema": {
2415 "$ref": "#/components/schemas/Envelope_NoData"
2416 }
2417 }
2418 }
2419 }
2420 }
2421 }
2422 },
2423 "/api/v1/organizations/{organization_id}/members/{member_id}/ban": {
2424 "post": {
2425 "tags": [
2426 "auth"
2427 ],
2428 "summary": "封禁成员",
2429 "description": "写 banned_at 并清空角色绑定与直挂权限。封禁优先于一切授权。",
2430 "operationId": "auth_organization_member_ban",
2431 "parameters": [
2432 {
2433 "name": "organization_id",
2434 "in": "path",
2435 "description": "组织 id",
2436 "required": true,
2437 "schema": {
2438 "type": "string",
2439 "format": "uuid"
2440 }
2441 },
2442 {
2443 "name": "member_id",
2444 "in": "path",
2445 "description": "会籍 id",
2446 "required": true,
2447 "schema": {
2448 "type": "string",
2449 "format": "uuid"
2450 }
2451 }
2452 ],
2453 "requestBody": {
2454 "content": {
2455 "application/json": {
2456 "schema": {
2457 "$ref": "#/components/schemas/BanMemberBody"
2458 }
2459 }
2460 },
2461 "required": true
2462 },
2463 "responses": {
2464 "200": {
2465 "description": "已封禁",
2466 "content": {
2467 "application/json": {
2468 "schema": {
2469 "$ref": "#/components/schemas/Envelope_MemberSummary"
2470 }
2471 }
2472 }
2473 },
2474 "403": {
2475 "description": "权限不足",
2476 "content": {
2477 "application/json": {
2478 "schema": {
2479 "$ref": "#/components/schemas/Envelope_NoData"
2480 }
2481 }
2482 }
2483 }
2484 }
2485 }
2486 },
2487 "/api/v1/organizations/{organization_id}/members/{member_id}/permissions": {
2488 "put": {
2489 "tags": [
2490 "auth"
2491 ],
2492 "summary": "设置成员直挂权限",
2493 "description": "整表替换语义。需要 `role.permission.update`,OWNER 不接受直挂覆盖。",
2494 "operationId": "auth_organization_member_permissions",
2495 "parameters": [
2496 {
2497 "name": "organization_id",
2498 "in": "path",
2499 "description": "组织 id",
2500 "required": true,
2501 "schema": {
2502 "type": "string",
2503 "format": "uuid"
2504 }
2505 },
2506 {
2507 "name": "member_id",
2508 "in": "path",
2509 "description": "会籍 id",
2510 "required": true,
2511 "schema": {
2512 "type": "string",
2513 "format": "uuid"
2514 }
2515 }
2516 ],
2517 "requestBody": {
2518 "content": {
2519 "application/json": {
2520 "schema": {
2521 "$ref": "#/components/schemas/SetRolePermissionsBody"
2522 }
2523 }
2524 },
2525 "required": true
2526 },
2527 "responses": {
2528 "200": {
2529 "description": "已更新",
2530 "content": {
2531 "application/json": {
2532 "schema": {
2533 "$ref": "#/components/schemas/Envelope_Vec_String"
2534 }
2535 }
2536 }
2537 },
2538 "403": {
2539 "description": "权限不足",
2540 "content": {
2541 "application/json": {
2542 "schema": {
2543 "$ref": "#/components/schemas/Envelope_NoData"
2544 }
2545 }
2546 }
2547 }
2548 }
2549 }
2550 },
2551 "/api/v1/organizations/{organization_id}/members/{member_id}/role": {
2552 "patch": {
2553 "tags": [
2554 "auth"
2555 ],
2556 "summary": "改粗粒度角色",
2557 "description": "只接受 MEMBER 与 ADMIN。OWNER 只能通过转移所有权变更。",
2558 "operationId": "auth_organization_member_role",
2559 "parameters": [
2560 {
2561 "name": "organization_id",
2562 "in": "path",
2563 "description": "组织 id",
2564 "required": true,
2565 "schema": {
2566 "type": "string",
2567 "format": "uuid"
2568 }
2569 },
2570 {
2571 "name": "member_id",
2572 "in": "path",
2573 "description": "会籍 id",
2574 "required": true,
2575 "schema": {
2576 "type": "string",
2577 "format": "uuid"
2578 }
2579 }
2580 ],
2581 "requestBody": {
2582 "content": {
2583 "application/json": {
2584 "schema": {
2585 "$ref": "#/components/schemas/SetMemberRoleBody"
2586 }
2587 }
2588 },
2589 "required": true
2590 },
2591 "responses": {
2592 "200": {
2593 "description": "已更新",
2594 "content": {
2595 "application/json": {
2596 "schema": {
2597 "$ref": "#/components/schemas/Envelope_MemberSummary"
2598 }
2599 }
2600 }
2601 },
2602 "403": {
2603 "description": "权限不足",
2604 "content": {
2605 "application/json": {
2606 "schema": {
2607 "$ref": "#/components/schemas/Envelope_NoData"
2608 }
2609 }
2610 }
2611 },
2612 "409": {
2613 "description": "版本冲突",
2614 "content": {
2615 "application/json": {
2616 "schema": {
2617 "$ref": "#/components/schemas/Envelope_NoData"
2618 }
2619 }
2620 }
2621 }
2622 }
2623 }
2624 },
2625 "/api/v1/organizations/{organization_id}/members/{member_id}/unban": {
2626 "post": {
2627 "tags": [
2628 "auth"
2629 ],
2630 "summary": "解封成员",
2631 "description": "清空 banned_at。已清空的角色不会自动恢复。",
2632 "operationId": "auth_organization_member_unban",
2633 "parameters": [
2634 {
2635 "name": "organization_id",
2636 "in": "path",
2637 "description": "组织 id",
2638 "required": true,
2639 "schema": {
2640 "type": "string",
2641 "format": "uuid"
2642 }
2643 },
2644 {
2645 "name": "member_id",
2646 "in": "path",
2647 "description": "会籍 id",
2648 "required": true,
2649 "schema": {
2650 "type": "string",
2651 "format": "uuid"
2652 }
2653 }
2654 ],
2655 "responses": {
2656 "200": {
2657 "description": "已解封",
2658 "content": {
2659 "application/json": {
2660 "schema": {
2661 "$ref": "#/components/schemas/Envelope_MemberSummary"
2662 }
2663 }
2664 }
2665 },
2666 "403": {
2667 "description": "权限不足",
2668 "content": {
2669 "application/json": {
2670 "schema": {
2671 "$ref": "#/components/schemas/Envelope_NoData"
2672 }
2673 }
2674 }
2675 }
2676 }
2677 }
2678 },
2679 "/api/v1/organizations/{organization_id}/permissions/check": {
2680 "post": {
2681 "tags": [
2682 "auth"
2683 ],
2684 "summary": "单权限判定",
2685 "description": "拒绝不作为错误返回:`allowed=false` 附 `reason` 与 `matched_by`,便于调用方聚合。",
2686 "operationId": "auth_organization_permission_check",
2687 "parameters": [
2688 {
2689 "name": "organization_id",
2690 "in": "path",
2691 "description": "组织 id",
2692 "required": true,
2693 "schema": {
2694 "type": "string",
2695 "format": "uuid"
2696 }
2697 }
2698 ],
2699 "requestBody": {
2700 "content": {
2701 "application/json": {
2702 "schema": {
2703 "$ref": "#/components/schemas/CheckPermissionBody"
2704 }
2705 }
2706 },
2707 "required": true
2708 },
2709 "responses": {
2710 "200": {
2711 "description": "判定结果",
2712 "content": {
2713 "application/json": {
2714 "schema": {
2715 "$ref": "#/components/schemas/Envelope_PermissionCheck"
2716 }
2717 }
2718 }
2719 },
2720 "403": {
2721 "description": "查询他人权限但不是平台管理员",
2722 "content": {
2723 "application/json": {
2724 "schema": {
2725 "$ref": "#/components/schemas/Envelope_NoData"
2726 }
2727 }
2728 }
2729 }
2730 }
2731 }
2732 },
2733 "/api/v1/organizations/{organization_id}/restore": {
2734 "post": {
2735 "tags": [
2736 "auth"
2737 ],
2738 "summary": "恢复归档组织",
2739 "description": "仅 OWNER 或平台管理员可执行。",
2740 "operationId": "auth_organization_restore",
2741 "parameters": [
2742 {
2743 "name": "organization_id",
2744 "in": "path",
2745 "description": "组织 id",
2746 "required": true,
2747 "schema": {
2748 "type": "string",
2749 "format": "uuid"
2750 }
2751 }
2752 ],
2753 "responses": {
2754 "200": {
2755 "description": "已恢复",
2756 "content": {
2757 "application/json": {
2758 "schema": {
2759 "$ref": "#/components/schemas/Envelope_OrganizationSummary"
2760 }
2761 }
2762 }
2763 },
2764 "403": {
2765 "description": "权限不足",
2766 "content": {
2767 "application/json": {
2768 "schema": {
2769 "$ref": "#/components/schemas/Envelope_NoData"
2770 }
2771 }
2772 }
2773 }
2774 }
2775 }
2776 },
2777 "/api/v1/organizations/{organization_id}/roles": {
2778 "get": {
2779 "tags": [
2780 "auth"
2781 ],
2782 "summary": "列示组织角色",
2783 "description": "需要 `role.view`。含内置 owner/admin/member 三个系统角色。",
2784 "operationId": "auth_organization_role_list",
2785 "parameters": [
2786 {
2787 "name": "organization_id",
2788 "in": "path",
2789 "description": "组织 id",
2790 "required": true,
2791 "schema": {
2792 "type": "string",
2793 "format": "uuid"
2794 }
2795 }
2796 ],
2797 "responses": {
2798 "200": {
2799 "description": "角色列表",
2800 "content": {
2801 "application/json": {
2802 "schema": {
2803 "$ref": "#/components/schemas/Envelope_Vec_RoleSummary"
2804 }
2805 }
2806 }
2807 },
2808 "403": {
2809 "description": "权限不足",
2810 "content": {
2811 "application/json": {
2812 "schema": {
2813 "$ref": "#/components/schemas/Envelope_NoData"
2814 }
2815 }
2816 }
2817 }
2818 }
2819 },
2820 "post": {
2821 "tags": [
2822 "auth"
2823 ],
2824 "summary": "新建角色",
2825 "description": "需要 `role.create`。key 在组织内唯一,权限键必须在白名单内。",
2826 "operationId": "auth_organization_role_create",
2827 "parameters": [
2828 {
2829 "name": "organization_id",
2830 "in": "path",
2831 "description": "组织 id",
2832 "required": true,
2833 "schema": {
2834 "type": "string",
2835 "format": "uuid"
2836 }
2837 }
2838 ],
2839 "requestBody": {
2840 "content": {
2841 "application/json": {
2842 "schema": {
2843 "$ref": "#/components/schemas/CreateRoleBody"
2844 }
2845 }
2846 },
2847 "required": true
2848 },
2849 "responses": {
2850 "200": {
2851 "description": "角色已创建",
2852 "content": {
2853 "application/json": {
2854 "schema": {
2855 "$ref": "#/components/schemas/Envelope_RoleSummary"
2856 }
2857 }
2858 }
2859 },
2860 "409": {
2861 "description": "key 已占用",
2862 "content": {
2863 "application/json": {
2864 "schema": {
2865 "$ref": "#/components/schemas/Envelope_NoData"
2866 }
2867 }
2868 }
2869 }
2870 }
2871 }
2872 },
2873 "/api/v1/organizations/{organization_id}/roles/assign": {
2874 "post": {
2875 "tags": [
2876 "auth"
2877 ],
2878 "summary": "绑定角色到成员",
2879 "description": "需要 `role.assign`。重复绑定幂等。",
2880 "operationId": "auth_organization_role_assign",
2881 "parameters": [
2882 {
2883 "name": "organization_id",
2884 "in": "path",
2885 "description": "组织 id",
2886 "required": true,
2887 "schema": {
2888 "type": "string",
2889 "format": "uuid"
2890 }
2891 }
2892 ],
2893 "requestBody": {
2894 "content": {
2895 "application/json": {
2896 "schema": {
2897 "$ref": "#/components/schemas/AssignRoleBody"
2898 }
2899 }
2900 },
2901 "required": true
2902 },
2903 "responses": {
2904 "200": {
2905 "description": "已绑定",
2906 "content": {
2907 "application/json": {
2908 "schema": {
2909 "$ref": "#/components/schemas/Envelope_Vec_String"
2910 }
2911 }
2912 }
2913 },
2914 "404": {
2915 "description": "成员或角色不存在",
2916 "content": {
2917 "application/json": {
2918 "schema": {
2919 "$ref": "#/components/schemas/Envelope_NoData"
2920 }
2921 }
2922 }
2923 }
2924 }
2925 }
2926 },
2927 "/api/v1/organizations/{organization_id}/roles/unassign": {
2928 "post": {
2929 "tags": [
2930 "auth"
2931 ],
2932 "summary": "解绑成员角色",
2933 "description": "需要 `role.assign`。系统角色不可解绑。",
2934 "operationId": "auth_organization_role_unassign",
2935 "parameters": [
2936 {
2937 "name": "organization_id",
2938 "in": "path",
2939 "description": "组织 id",
2940 "required": true,
2941 "schema": {
2942 "type": "string",
2943 "format": "uuid"
2944 }
2945 }
2946 ],
2947 "requestBody": {
2948 "content": {
2949 "application/json": {
2950 "schema": {
2951 "$ref": "#/components/schemas/AssignRoleBody"
2952 }
2953 }
2954 },
2955 "required": true
2956 },
2957 "responses": {
2958 "200": {
2959 "description": "已解绑",
2960 "content": {
2961 "application/json": {
2962 "schema": {
2963 "$ref": "#/components/schemas/Envelope_Vec_String"
2964 }
2965 }
2966 }
2967 },
2968 "403": {
2969 "description": "系统角色不可解绑",
2970 "content": {
2971 "application/json": {
2972 "schema": {
2973 "$ref": "#/components/schemas/Envelope_NoData"
2974 }
2975 }
2976 }
2977 }
2978 }
2979 }
2980 },
2981 "/api/v1/organizations/{organization_id}/roles/{role_id}": {
2982 "get": {
2983 "tags": [
2984 "auth"
2985 ],
2986 "summary": "角色详情",
2987 "description": "需要 `role.view`。",
2988 "operationId": "auth_organization_role_get",
2989 "parameters": [
2990 {
2991 "name": "organization_id",
2992 "in": "path",
2993 "description": "组织 id",
2994 "required": true,
2995 "schema": {
2996 "type": "string",
2997 "format": "uuid"
2998 }
2999 },
3000 {
3001 "name": "role_id",
3002 "in": "path",
3003 "description": "角色 id",
3004 "required": true,
3005 "schema": {
3006 "type": "string",
3007 "format": "uuid"
3008 }
3009 }
3010 ],
3011 "responses": {
3012 "200": {
3013 "description": "角色详情",
3014 "content": {
3015 "application/json": {
3016 "schema": {
3017 "$ref": "#/components/schemas/Envelope_RoleSummary"
3018 }
3019 }
3020 }
3021 },
3022 "404": {
3023 "description": "角色不存在",
3024 "content": {
3025 "application/json": {
3026 "schema": {
3027 "$ref": "#/components/schemas/Envelope_NoData"
3028 }
3029 }
3030 }
3031 }
3032 }
3033 },
3034 "delete": {
3035 "tags": [
3036 "auth"
3037 ],
3038 "summary": "删除角色",
3039 "description": "需要 `role.delete`。系统角色不可删;仍有成员绑定时返回 409。",
3040 "operationId": "auth_organization_role_delete",
3041 "parameters": [
3042 {
3043 "name": "organization_id",
3044 "in": "path",
3045 "description": "组织 id",
3046 "required": true,
3047 "schema": {
3048 "type": "string",
3049 "format": "uuid"
3050 }
3051 },
3052 {
3053 "name": "role_id",
3054 "in": "path",
3055 "description": "角色 id",
3056 "required": true,
3057 "schema": {
3058 "type": "string",
3059 "format": "uuid"
3060 }
3061 }
3062 ],
3063 "responses": {
3064 "200": {
3065 "description": "已删除",
3066 "content": {
3067 "application/json": {
3068 "schema": {
3069 "$ref": "#/components/schemas/Envelope_bool"
3070 }
3071 }
3072 }
3073 },
3074 "409": {
3075 "description": "系统角色或仍被绑定",
3076 "content": {
3077 "application/json": {
3078 "schema": {
3079 "$ref": "#/components/schemas/Envelope_NoData"
3080 }
3081 }
3082 }
3083 }
3084 }
3085 },
3086 "patch": {
3087 "tags": [
3088 "auth"
3089 ],
3090 "summary": "修改角色",
3091 "description": "需要 `role.update`。OWNER 系统角色不允许改 rank 与 default_role。",
3092 "operationId": "auth_organization_role_update",
3093 "parameters": [
3094 {
3095 "name": "organization_id",
3096 "in": "path",
3097 "description": "组织 id",
3098 "required": true,
3099 "schema": {
3100 "type": "string",
3101 "format": "uuid"
3102 }
3103 },
3104 {
3105 "name": "role_id",
3106 "in": "path",
3107 "description": "角色 id",
3108 "required": true,
3109 "schema": {
3110 "type": "string",
3111 "format": "uuid"
3112 }
3113 }
3114 ],
3115 "requestBody": {
3116 "content": {
3117 "application/json": {
3118 "schema": {
3119 "$ref": "#/components/schemas/UpdateRoleBody"
3120 }
3121 }
3122 },
3123 "required": true
3124 },
3125 "responses": {
3126 "200": {
3127 "description": "已更新",
3128 "content": {
3129 "application/json": {
3130 "schema": {
3131 "$ref": "#/components/schemas/Envelope_RoleSummary"
3132 }
3133 }
3134 }
3135 },
3136 "409": {
3137 "description": "版本冲突",
3138 "content": {
3139 "application/json": {
3140 "schema": {
3141 "$ref": "#/components/schemas/Envelope_NoData"
3142 }
3143 }
3144 }
3145 }
3146 }
3147 }
3148 },
3149 "/api/v1/organizations/{organization_id}/roles/{role_id}/join": {
3150 "post": {
3151 "tags": [
3152 "auth"
3153 ],
3154 "summary": "自助加入角色",
3155 "description": "只允许 `self_assignable = true` 的非内置角色。",
3156 "operationId": "auth_organization_role_join",
3157 "parameters": [
3158 {
3159 "name": "organization_id",
3160 "in": "path",
3161 "description": "组织 id",
3162 "required": true,
3163 "schema": {
3164 "type": "string",
3165 "format": "uuid"
3166 }
3167 },
3168 {
3169 "name": "role_id",
3170 "in": "path",
3171 "description": "角色 id",
3172 "required": true,
3173 "schema": {
3174 "type": "string",
3175 "format": "uuid"
3176 }
3177 }
3178 ],
3179 "responses": {
3180 "200": {
3181 "description": "已加入",
3182 "content": {
3183 "application/json": {
3184 "schema": {
3185 "$ref": "#/components/schemas/Envelope_MemberSummary"
3186 }
3187 }
3188 }
3189 },
3190 "403": {
3191 "description": "角色不可自助加入或不是活跃成员",
3192 "content": {
3193 "application/json": {
3194 "schema": {
3195 "$ref": "#/components/schemas/Envelope_NoData"
3196 }
3197 }
3198 }
3199 }
3200 }
3201 },
3202 "delete": {
3203 "tags": [
3204 "auth"
3205 ],
3206 "summary": "自助退出角色",
3207 "description": "只允许 `self_assignable = true` 的非内置角色;重复退出视为成功。",
3208 "operationId": "auth_organization_role_leave",
3209 "parameters": [
3210 {
3211 "name": "organization_id",
3212 "in": "path",
3213 "description": "组织 id",
3214 "required": true,
3215 "schema": {
3216 "type": "string",
3217 "format": "uuid"
3218 }
3219 },
3220 {
3221 "name": "role_id",
3222 "in": "path",
3223 "description": "角色 id",
3224 "required": true,
3225 "schema": {
3226 "type": "string",
3227 "format": "uuid"
3228 }
3229 }
3230 ],
3231 "responses": {
3232 "200": {
3233 "description": "已退出",
3234 "content": {
3235 "application/json": {
3236 "schema": {
3237 "$ref": "#/components/schemas/Envelope_MemberSummary"
3238 }
3239 }
3240 }
3241 },
3242 "403": {
3243 "description": "角色不可自助加入或不是活跃成员",
3244 "content": {
3245 "application/json": {
3246 "schema": {
3247 "$ref": "#/components/schemas/Envelope_NoData"
3248 }
3249 }
3250 }
3251 }
3252 }
3253 }
3254 },
3255 "/api/v1/organizations/{organization_id}/roles/{role_id}/permissions": {
3256 "put": {
3257 "tags": [
3258 "auth"
3259 ],
3260 "summary": "设置角色权限",
3261 "description": "整体替换语义,是角色权限的唯一写入口。需要 `role.permission.update`。",
3262 "operationId": "auth_organization_role_permissions",
3263 "parameters": [
3264 {
3265 "name": "organization_id",
3266 "in": "path",
3267 "description": "组织 id",
3268 "required": true,
3269 "schema": {
3270 "type": "string",
3271 "format": "uuid"
3272 }
3273 },
3274 {
3275 "name": "role_id",
3276 "in": "path",
3277 "description": "角色 id",
3278 "required": true,
3279 "schema": {
3280 "type": "string",
3281 "format": "uuid"
3282 }
3283 }
3284 ],
3285 "requestBody": {
3286 "content": {
3287 "application/json": {
3288 "schema": {
3289 "$ref": "#/components/schemas/SetRolePermissionsBody"
3290 }
3291 }
3292 },
3293 "required": true
3294 },
3295 "responses": {
3296 "200": {
3297 "description": "已更新",
3298 "content": {
3299 "application/json": {
3300 "schema": {
3301 "$ref": "#/components/schemas/Envelope_RoleSummary"
3302 }
3303 }
3304 }
3305 },
3306 "403": {
3307 "description": "权限不足或系统 OWNER 角色",
3308 "content": {
3309 "application/json": {
3310 "schema": {
3311 "$ref": "#/components/schemas/Envelope_NoData"
3312 }
3313 }
3314 }
3315 }
3316 }
3317 }
3318 },
3319 "/api/v1/organizations/{organization_id}/transfer": {
3320 "post": {
3321 "tags": [
3322 "auth"
3323 ],
3324 "summary": "转移 OWNER",
3325 "description": "目标必须是活跃成员。原 Owner 降为 ADMIN。",
3326 "operationId": "auth_organization_transfer",
3327 "parameters": [
3328 {
3329 "name": "organization_id",
3330 "in": "path",
3331 "description": "组织 id",
3332 "required": true,
3333 "schema": {
3334 "type": "string",
3335 "format": "uuid"
3336 }
3337 }
3338 ],
3339 "requestBody": {
3340 "content": {
3341 "application/json": {
3342 "schema": {
3343 "$ref": "#/components/schemas/TransferBody"
3344 }
3345 }
3346 },
3347 "required": true
3348 },
3349 "responses": {
3350 "200": {
3351 "description": "已转移",
3352 "content": {
3353 "application/json": {
3354 "schema": {
3355 "$ref": "#/components/schemas/Envelope_OrganizationSummary"
3356 }
3357 }
3358 }
3359 },
3360 "403": {
3361 "description": "权限不足",
3362 "content": {
3363 "application/json": {
3364 "schema": {
3365 "$ref": "#/components/schemas/Envelope_NoData"
3366 }
3367 }
3368 }
3369 },
3370 "404": {
3371 "description": "目标不是活跃成员",
3372 "content": {
3373 "application/json": {
3374 "schema": {
3375 "$ref": "#/components/schemas/Envelope_NoData"
3376 }
3377 }
3378 }
3379 }
3380 }
3381 }
33821257 }
33831258 },
33841259 "components": {
33851260 "schemas": {
3386 "AddMemberBody": {
3387 "type": "object",
3388 "required": [
3389 "user_id"
3390 ],
3391 "properties": {
3392 "role": {
3393 "oneOf": [
3394 {
3395 "type": "null"
3396 },
3397 {
3398 "$ref": "#/components/schemas/OrgMemberRole"
3399 }
3400 ]
3401 },
3402 "role_keys": {
3403 "type": "array",
3404 "items": {
3405 "type": "string"
3406 }
3407 },
3408 "user_id": {
3409 "type": "integer",
3410 "format": "int64"
3411 }
3412 }
3413 },
3414 "AssignRoleBody": {
3415 "type": "object",
3416 "required": [
3417 "member_id",
3418 "role_key"
3419 ],
3420 "properties": {
3421 "member_id": {
3422 "type": "string",
3423 "format": "uuid"
3424 },
3425 "role_key": {
3426 "type": "string"
3427 }
3428 }
3429 },
3430 "BanMemberBody": {
3431 "type": "object",
3432 "properties": {
3433 "reason": {
3434 "type": [
3435 "string",
3436 "null"
3437 ]
3438 }
3439 }
3440 },
34411261 "ChallengeResponse": {
34421262 "type": "object",
34431263 "required": [
@@ -3447,2032 +1267,27 @@
34471267 "expires_at"
34481268 ],
34491269 "properties": {
3450 "context": {
3451 "type": "string"
3452 },
3453 "difficulty": {
3454 "type": "integer",
3455 "format": "int32",
3456 "minimum": 0
3457 },
3458 "expires_at": {
3459 "type": "integer",
3460 "format": "int64",
3461 "minimum": 0
3462 },
3463 "issued_at": {
3464 "type": "integer",
3465 "format": "int64",
3466 "minimum": 0
3467 }
3468 }
3469 },
3470 "CheckPermissionBody": {
3471 "type": "object",
3472 "required": [
3473 "permission"
3474 ],
3475 "properties": {
3476 "permission": {
3477 "type": "string"
3478 },
3479 "user_id": {
3480 "type": [
3481 "integer",
3482 "null"
3483 ],
3484 "format": "int64"
3485 }
3486 }
3487 },
3488 "CreateInviteBody": {
3489 "type": "object",
3490 "properties": {
3491 "max_uses": {
3492 "type": "integer",
3493 "format": "int32"
3494 },
3495 "role_id": {
3496 "type": [
3497 "string",
3498 "null"
3499 ],
3500 "format": "uuid"
3501 },
3502 "target_user_id": {
3503 "type": [
3504 "integer",
3505 "null"
3506 ],
3507 "format": "int64"
3508 },
3509 "ttl_secs": {
3510 "type": [
3511 "integer",
3512 "null"
3513 ],
3514 "format": "int64",
3515 "minimum": 0
3516 }
3517 }
3518 },
3519 "CreateInviteResponse": {
3520 "type": "object",
3521 "required": [
3522 "invite",
3523 "token"
3524 ],
3525 "properties": {
3526 "invite": {
3527 "$ref": "#/components/schemas/InviteSummary"
3528 },
3529 "token": {
3530 "type": "string"
3531 }
3532 }
3533 },
3534 "CreateOrganizationBody": {
3535 "type": "object",
3536 "required": [
3537 "slug",
3538 "name"
3539 ],
3540 "properties": {
3541 "allow_invite": {
3542 "type": [
3543 "boolean",
3544 "null"
3545 ]
3546 },
3547 "allow_join": {
3548 "type": [
3549 "boolean",
3550 "null"
3551 ]
3552 },
3553 "allow_join_level": {
3554 "type": [
3555 "integer",
3556 "null"
3557 ],
3558 "format": "int32"
3559 },
3560 "avatar_url": {
3561 "type": [
3562 "string",
3563 "null"
3564 ]
3565 },
3566 "description": {
3567 "type": [
3568 "string",
3569 "null"
3570 ]
3571 },
3572 "join_policy": {
3573 "oneOf": [
3574 {
3575 "type": "null"
3576 },
3577 {
3578 "$ref": "#/components/schemas/OrganizationJoinPolicy"
3579 }
3580 ]
3581 },
3582 "member_must_enable_2fa": {
3583 "type": [
3584 "boolean",
3585 "null"
3586 ]
3587 },
3588 "name": {
3589 "type": "string"
3590 },
3591 "require_email_verified": {
3592 "type": [
3593 "boolean",
3594 "null"
3595 ]
3596 },
3597 "require_mfa": {
3598 "type": [
3599 "boolean",
3600 "null"
3601 ]
3602 },
3603 "slug": {
3604 "type": "string"
3605 },
3606 "welcome_message": {
3607 "type": [
3608 "string",
3609 "null"
3610 ]
3611 }
3612 }
3613 },
3614 "CreateRoleBody": {
3615 "type": "object",
3616 "required": [
3617 "key",
3618 "name"
3619 ],
3620 "properties": {
3621 "default_role": {
3622 "type": "boolean"
3623 },
3624 "description": {
3625 "type": [
3626 "string",
3627 "null"
3628 ]
3629 },
3630 "key": {
3631 "type": "string"
3632 },
3633 "name": {
3634 "type": "string"
3635 },
3636 "permissions": {
3637 "type": "array",
3638 "items": {
3639 "$ref": "#/components/schemas/PermissionGrant"
3640 }
3641 },
3642 "rank": {
3643 "type": "integer",
3644 "format": "int32"
3645 },
3646 "self_assignable": {
3647 "type": "boolean"
3648 }
3649 }
3650 },
3651 "DeleteOrgQuery": {
3652 "type": "object",
3653 "properties": {
3654 "force": {
3655 "type": "boolean"
3656 },
3657 "reason": {
3658 "type": [
3659 "string",
3660 "null"
3661 ]
3662 }
3663 }
3664 },
3665 "Envelope_ChallengeResponse": {
3666 "type": "object",
3667 "required": [
3668 "code",
3669 "message"
3670 ],
3671 "properties": {
3672 "code": {
3673 "type": "integer",
3674 "format": "int32"
3675 },
3676 "data": {
3677 "type": "object",
3678 "required": [
3679 "context",
3680 "difficulty",
3681 "issued_at",
3682 "expires_at"
3683 ],
3684 "properties": {
3685 "context": {
3686 "type": "string"
3687 },
3688 "difficulty": {
3689 "type": "integer",
3690 "format": "int32",
3691 "minimum": 0
3692 },
3693 "expires_at": {
3694 "type": "integer",
3695 "format": "int64",
3696 "minimum": 0
3697 },
3698 "issued_at": {
3699 "type": "integer",
3700 "format": "int64",
3701 "minimum": 0
3702 }
3703 }
3704 },
3705 "error": {
3706 "type": [
3707 "string",
3708 "null"
3709 ]
3710 },
3711 "error_code": {
3712 "type": [
3713 "string",
3714 "null"
3715 ]
3716 },
3717 "message": {
3718 "type": "string"
3719 },
3720 "request_id": {
3721 "type": "string"
3722 }
3723 }
3724 },
3725 "Envelope_CreateInviteResponse": {
3726 "type": "object",
3727 "required": [
3728 "code",
3729 "message"
3730 ],
3731 "properties": {
3732 "code": {
3733 "type": "integer",
3734 "format": "int32"
3735 },
3736 "data": {
3737 "type": "object",
3738 "required": [
3739 "invite",
3740 "token"
3741 ],
3742 "properties": {
3743 "invite": {
3744 "$ref": "#/components/schemas/InviteSummary"
3745 },
3746 "token": {
3747 "type": "string"
3748 }
3749 }
3750 },
3751 "error": {
3752 "type": [
3753 "string",
3754 "null"
3755 ]
3756 },
3757 "error_code": {
3758 "type": [
3759 "string",
3760 "null"
3761 ]
3762 },
3763 "message": {
3764 "type": "string"
3765 },
3766 "request_id": {
3767 "type": "string"
3768 }
3769 }
3770 },
3771 "Envelope_LoginCodeResponse": {
3772 "type": "object",
3773 "required": [
3774 "code",
3775 "message"
3776 ],
3777 "properties": {
3778 "code": {
3779 "type": "integer",
3780 "format": "int32"
3781 },
3782 "data": {
3783 "type": "object",
3784 "required": [
3785 "expires_in_secs"
3786 ],
3787 "properties": {
3788 "expires_in_secs": {
3789 "type": "integer",
3790 "format": "int64",
3791 "minimum": 0
3792 }
3793 }
3794 },
3795 "error": {
3796 "type": [
3797 "string",
3798 "null"
3799 ]
3800 },
3801 "error_code": {
3802 "type": [
3803 "string",
3804 "null"
3805 ]
3806 },
3807 "message": {
3808 "type": "string"
3809 },
3810 "request_id": {
3811 "type": "string"
3812 }
3813 }
3814 },
3815 "Envelope_LogoutResponse": {
3816 "type": "object",
3817 "required": [
3818 "code",
3819 "message"
3820 ],
3821 "properties": {
3822 "code": {
3823 "type": "integer",
3824 "format": "int32"
3825 },
3826 "data": {
3827 "type": "object",
3828 "properties": {
3829 "user_id": {
3830 "type": [
3831 "integer",
3832 "null"
3833 ],
3834 "format": "int64"
3835 }
3836 }
3837 },
3838 "error": {
3839 "type": [
3840 "string",
3841 "null"
3842 ]
3843 },
3844 "error_code": {
3845 "type": [
3846 "string",
3847 "null"
3848 ]
3849 },
3850 "message": {
3851 "type": "string"
3852 },
3853 "request_id": {
3854 "type": "string"
3855 }
3856 }
3857 },
3858 "Envelope_MeResponse": {
3859 "type": "object",
3860 "required": [
3861 "code",
3862 "message"
3863 ],
3864 "properties": {
3865 "code": {
3866 "type": "integer",
3867 "format": "int32"
3868 },
3869 "data": {
3870 "type": "object",
3871 "required": [
3872 "user_id",
3873 "username",
3874 "email",
3875 "is_admin",
3876 "created_at",
3877 "version"
3878 ],
3879 "properties": {
3880 "avatar_url": {
3881 "type": [
3882 "string",
3883 "null"
3884 ]
3885 },
3886 "created_at": {
3887 "type": "string",
3888 "format": "date-time"
3889 },
3890 "displayname": {
3891 "type": [
3892 "string",
3893 "null"
3894 ]
3895 },
3896 "email": {
3897 "type": "string"
3898 },
3899 "email_verified_at": {
3900 "type": [
3901 "string",
3902 "null"
3903 ],
3904 "format": "date-time"
3905 },
3906 "is_admin": {
3907 "type": "boolean"
3908 },
3909 "user_id": {
3910 "type": "integer",
3911 "format": "int64"
3912 },
3913 "username": {
3914 "type": "string"
3915 },
3916 "version": {
3917 "type": "integer",
3918 "format": "int32"
3919 }
3920 }
3921 },
3922 "error": {
3923 "type": [
3924 "string",
3925 "null"
3926 ]
3927 },
3928 "error_code": {
3929 "type": [
3930 "string",
3931 "null"
3932 ]
3933 },
3934 "message": {
3935 "type": "string"
3936 },
3937 "request_id": {
3938 "type": "string"
3939 }
3940 }
3941 },
3942 "Envelope_MemberSummary": {
3943 "type": "object",
3944 "required": [
3945 "code",
3946 "message"
3947 ],
3948 "properties": {
3949 "code": {
3950 "type": "integer",
3951 "format": "int32"
3952 },
3953 "data": {
3954 "type": "object",
3955 "required": [
3956 "id",
3957 "organization_id",
3958 "user_id",
3959 "username",
3960 "role",
3961 "role_keys",
3962 "permissions",
3963 "joined_at",
3964 "join_method",
3965 "message_notifications",
3966 "muted",
3967 "mobile_push",
3968 "suppress_everyone",
3969 "suppress_roles",
3970 "hide_muted_channels",
3971 "ban_allow_agent_join",
3972 "ban_allow_speak",
3973 "ban_allow_push_repo",
3974 "ban_allow_comment_any",
3975 "version"
3976 ],
3977 "properties": {
3978 "alias_avatar_url": {
3979 "type": [
3980 "string",
3981 "null"
3982 ]
3983 },
3984 "alias_name": {
3985 "type": [
3986 "string",
3987 "null"
3988 ]
3989 },
3990 "ban_allow_agent_join": {
3991 "type": "boolean"
3992 },
3993 "ban_allow_comment_any": {
3994 "type": "boolean"
3995 },
3996 "ban_allow_push_repo": {
3997 "type": "boolean"
3998 },
3999 "ban_allow_speak": {
4000 "type": "boolean"
4001 },
4002 "ban_expires_at": {
4003 "type": [
4004 "string",
4005 "null"
4006 ],
4007 "format": "date-time"
4008 },
4009 "ban_reason": {
4010 "type": [
4011 "string",
4012 "null"
4013 ]
4014 },
4015 "banned_at": {
4016 "type": [
4017 "string",
4018 "null"
4019 ],
4020 "format": "date-time"
4021 },
4022 "displayname": {
4023 "type": [
4024 "string",
4025 "null"
4026 ]
4027 },
4028 "hide_muted_channels": {
4029 "type": "boolean"
4030 },
4031 "id": {
4032 "type": "string",
4033 "format": "uuid"
4034 },
4035 "invited_by": {
4036 "type": [
4037 "integer",
4038 "null"
4039 ],
4040 "format": "int64"
4041 },
4042 "join_method": {
4043 "$ref": "#/components/schemas/OrgJoinMethod"
4044 },
4045 "joined_at": {
4046 "type": "string",
4047 "format": "date-time"
4048 },
4049 "last_active_at": {
4050 "type": [
4051 "string",
4052 "null"
4053 ],
4054 "format": "date-time"
4055 },
4056 "left_at": {
4057 "type": [
4058 "string",
4059 "null"
4060 ],
4061 "format": "date-time"
4062 },
4063 "message_notifications": {
4064 "$ref": "#/components/schemas/MessageNotifications"
4065 },
4066 "mobile_push": {
4067 "type": "boolean"
4068 },
4069 "mute_until": {
4070 "type": [
4071 "string",
4072 "null"
4073 ],
4074 "format": "date-time"
4075 },
4076 "muted": {
4077 "type": "boolean"
4078 },
4079 "organization_id": {
4080 "type": "string",
4081 "format": "uuid"
4082 },
4083 "permissions": {
4084 "type": "array",
4085 "items": {
4086 "type": "string"
4087 }
4088 },
4089 "role": {
4090 "$ref": "#/components/schemas/OrgMemberRole"
4091 },
4092 "role_keys": {
4093 "type": "array",
4094 "items": {
4095 "type": "string"
4096 }
4097 },
4098 "suppress_everyone": {
4099 "type": "boolean"
4100 },
4101 "suppress_roles": {
4102 "type": "boolean"
4103 },
4104 "user_id": {
4105 "type": "integer",
4106 "format": "int64"
4107 },
4108 "username": {
4109 "type": "string"
4110 },
4111 "version": {
4112 "type": "integer",
4113 "format": "int32"
4114 }
4115 }
4116 },
4117 "error": {
4118 "type": [
4119 "string",
4120 "null"
4121 ]
4122 },
4123 "error_code": {
4124 "type": [
4125 "string",
4126 "null"
4127 ]
4128 },
4129 "message": {
4130 "type": "string"
4131 },
4132 "request_id": {
4133 "type": "string"
4134 }
4135 }
4136 },
4137 "Envelope_NoData": {
4138 "type": "object",
4139 "required": [
4140 "code",
4141 "message"
4142 ],
4143 "properties": {
4144 "code": {
4145 "type": "integer",
4146 "format": "int32"
4147 },
4148 "data": {
4149 "default": null
4150 },
4151 "error": {
4152 "type": [
4153 "string",
4154 "null"
4155 ]
4156 },
4157 "error_code": {
4158 "type": [
4159 "string",
4160 "null"
4161 ]
4162 },
4163 "message": {
4164 "type": "string"
4165 },
4166 "request_id": {
4167 "type": "string"
4168 }
4169 }
4170 },
4171 "Envelope_OrganizationSummary": {
4172 "type": "object",
4173 "required": [
4174 "code",
4175 "message"
4176 ],
4177 "properties": {
4178 "code": {
4179 "type": "integer",
4180 "format": "int32"
4181 },
4182 "data": {
4183 "type": "object",
4184 "required": [
4185 "id",
4186 "slug",
4187 "name",
4188 "status",
4189 "owner_user_id",
4190 "member_count",
4191 "version",
4192 "created_at",
4193 "updated_at",
4194 "join_policy",
4195 "allow_join",
4196 "require_email_verified",
4197 "require_mfa",
4198 "member_must_enable_2fa",
4199 "allow_invite",
4200 "allow_join_level"
4201 ],
4202 "properties": {
4203 "allow_invite": {
4204 "type": "boolean"
4205 },
4206 "allow_join": {
4207 "type": "boolean"
4208 },
4209 "allow_join_level": {
4210 "type": "integer",
4211 "format": "int32"
4212 },
4213 "archived_at": {
4214 "type": [
4215 "string",
4216 "null"
4217 ],
4218 "format": "date-time"
4219 },
4220 "avatar_url": {
4221 "type": [
4222 "string",
4223 "null"
4224 ]
4225 },
4226 "created_at": {
4227 "type": "string",
4228 "format": "date-time"
4229 },
4230 "description": {
4231 "type": [
4232 "string",
4233 "null"
4234 ]
4235 },
4236 "id": {
4237 "type": "string",
4238 "format": "uuid"
4239 },
4240 "join_policy": {
4241 "$ref": "#/components/schemas/OrganizationJoinPolicy"
4242 },
4243 "member_count": {
4244 "type": "integer",
4245 "format": "int64"
4246 },
4247 "member_must_enable_2fa": {
4248 "type": "boolean"
4249 },
4250 "my_role": {
4251 "oneOf": [
4252 {
4253 "type": "null"
4254 },
4255 {
4256 "$ref": "#/components/schemas/OrgMemberRole"
4257 }
4258 ]
4259 },
4260 "name": {
4261 "type": "string"
4262 },
4263 "owner_user_id": {
4264 "type": "integer",
4265 "format": "int64"
4266 },
4267 "require_email_verified": {
4268 "type": "boolean"
4269 },
4270 "require_mfa": {
4271 "type": "boolean"
4272 },
4273 "slug": {
4274 "type": "string"
4275 },
4276 "status": {
4277 "$ref": "#/components/schemas/OrganizationStatus"
4278 },
4279 "updated_at": {
4280 "type": "string",
4281 "format": "date-time"
4282 },
4283 "version": {
4284 "type": "integer",
4285 "format": "int32"
4286 },
4287 "welcome_message": {
4288 "type": [
4289 "string",
4290 "null"
4291 ]
4292 }
4293 }
4294 },
4295 "error": {
4296 "type": [
4297 "string",
4298 "null"
4299 ]
4300 },
4301 "error_code": {
4302 "type": [
4303 "string",
4304 "null"
4305 ]
4306 },
4307 "message": {
4308 "type": "string"
4309 },
4310 "request_id": {
4311 "type": "string"
4312 }
4313 }
4314 },
4315 "Envelope_PasswordResetConfirmResponse": {
4316 "type": "object",
4317 "required": [
4318 "code",
4319 "message"
4320 ],
4321 "properties": {
4322 "code": {
4323 "type": "integer",
4324 "format": "int32"
4325 },
4326 "data": {
4327 "type": "object",
4328 "required": [
4329 "user_id"
4330 ],
4331 "properties": {
4332 "user_id": {
4333 "type": "integer",
4334 "format": "int64"
4335 }
4336 }
4337 },
4338 "error": {
4339 "type": [
4340 "string",
4341 "null"
4342 ]
4343 },
4344 "error_code": {
4345 "type": [
4346 "string",
4347 "null"
4348 ]
4349 },
4350 "message": {
4351 "type": "string"
4352 },
4353 "request_id": {
4354 "type": "string"
4355 }
4356 }
4357 },
4358 "Envelope_PasswordResetResponse": {
4359 "type": "object",
4360 "required": [
4361 "code",
4362 "message"
4363 ],
4364 "properties": {
4365 "code": {
4366 "type": "integer",
4367 "format": "int32"
4368 },
4369 "data": {
4370 "type": "object",
4371 "required": [
4372 "expires_in_secs"
4373 ],
4374 "properties": {
4375 "expires_in_secs": {
4376 "type": "integer",
4377 "format": "int64",
4378 "minimum": 0
4379 }
4380 }
4381 },
4382 "error": {
4383 "type": [
4384 "string",
4385 "null"
4386 ]
4387 },
4388 "error_code": {
4389 "type": [
4390 "string",
4391 "null"
4392 ]
4393 },
4394 "message": {
4395 "type": "string"
4396 },
4397 "request_id": {
4398 "type": "string"
4399 }
4400 }
4401 },
4402 "Envelope_PermissionCheck": {
4403 "type": "object",
4404 "required": [
4405 "code",
4406 "message"
4407 ],
4408 "properties": {
4409 "code": {
4410 "type": "integer",
4411 "format": "int32"
4412 },
4413 "data": {
4414 "type": "object",
4415 "required": [
4416 "allowed",
4417 "reason",
4418 "matched_by",
4419 "organization_version"
4420 ],
4421 "properties": {
4422 "allowed": {
4423 "type": "boolean"
4424 },
4425 "matched_by": {
4426 "type": "string"
4427 },
4428 "organization_version": {
4429 "type": "integer",
4430 "format": "int32"
4431 },
4432 "reason": {
4433 "type": "string"
4434 }
4435 }
4436 },
4437 "error": {
4438 "type": [
4439 "string",
4440 "null"
4441 ]
4442 },
4443 "error_code": {
4444 "type": [
4445 "string",
4446 "null"
4447 ]
4448 },
4449 "message": {
4450 "type": "string"
4451 },
4452 "request_id": {
4453 "type": "string"
4454 }
4455 }
4456 },
4457 "Envelope_RedeemInviteResponse": {
4458 "type": "object",
4459 "required": [
4460 "code",
4461 "message"
4462 ],
4463 "properties": {
4464 "code": {
4465 "type": "integer",
4466 "format": "int32"
4467 },
4468 "data": {
4469 "type": "object",
4470 "required": [
4471 "organization_id",
4472 "member_id",
4473 "joined"
4474 ],
4475 "properties": {
4476 "joined": {
4477 "type": "boolean"
4478 },
4479 "member_id": {
4480 "type": "string",
4481 "format": "uuid"
4482 },
4483 "organization_id": {
4484 "type": "string",
4485 "format": "uuid"
4486 }
4487 }
4488 },
4489 "error": {
4490 "type": [
4491 "string",
4492 "null"
4493 ]
4494 },
4495 "error_code": {
4496 "type": [
4497 "string",
4498 "null"
4499 ]
4500 },
4501 "message": {
4502 "type": "string"
4503 },
4504 "request_id": {
4505 "type": "string"
4506 }
4507 }
4508 },
4509 "Envelope_RegisterCodeResponse": {
4510 "type": "object",
4511 "required": [
4512 "code",
4513 "message"
4514 ],
4515 "properties": {
4516 "code": {
4517 "type": "integer",
4518 "format": "int32"
4519 },
4520 "data": {
4521 "type": "object",
4522 "required": [
4523 "expires_in_secs"
4524 ],
4525 "properties": {
4526 "expires_in_secs": {
4527 "type": "integer",
4528 "format": "int64",
4529 "minimum": 0
4530 }
4531 }
4532 },
4533 "error": {
4534 "type": [
4535 "string",
4536 "null"
4537 ]
4538 },
4539 "error_code": {
4540 "type": [
4541 "string",
4542 "null"
4543 ]
4544 },
4545 "message": {
4546 "type": "string"
4547 },
4548 "request_id": {
4549 "type": "string"
4550 }
4551 }
4552 },
4553 "Envelope_RoleSummary": {
4554 "type": "object",
4555 "required": [
4556 "code",
4557 "message"
4558 ],
4559 "properties": {
4560 "code": {
4561 "type": "integer",
4562 "format": "int32"
4563 },
4564 "data": {
4565 "type": "object",
4566 "required": [
4567 "id",
4568 "organization_id",
4569 "key",
4570 "name",
4571 "rank",
4572 "system",
4573 "default_role",
4574 "self_assignable",
4575 "permissions",
4576 "member_count",
4577 "created_at",
4578 "updated_at",
4579 "version",
4580 "position"
4581 ],
4582 "properties": {
4583 "color": {
4584 "type": [
4585 "string",
4586 "null"
4587 ]
4588 },
4589 "created_at": {
4590 "type": "string",
4591 "format": "date-time"
4592 },
4593 "default_role": {
4594 "type": "boolean"
4595 },
4596 "description": {
4597 "type": [
4598 "string",
4599 "null"
4600 ]
4601 },
4602 "emoji": {
4603 "type": [
4604 "string",
4605 "null"
4606 ]
4607 },
4608 "icon_hash": {
4609 "type": [
4610 "string",
4611 "null"
4612 ]
4613 },
4614 "id": {
4615 "type": "string",
4616 "format": "uuid"
4617 },
4618 "key": {
4619 "type": "string"
4620 },
4621 "member_count": {
4622 "type": "integer",
4623 "format": "int64"
4624 },
4625 "name": {
4626 "type": "string"
4627 },
4628 "organization_id": {
4629 "type": "string",
4630 "format": "uuid"
4631 },
4632 "permissions": {
4633 "type": "array",
4634 "items": {
4635 "$ref": "#/components/schemas/PermissionGrant"
4636 }
4637 },
4638 "position": {
4639 "type": "integer",
4640 "format": "int64"
4641 },
4642 "rank": {
4643 "type": "integer",
4644 "format": "int32"
4645 },
4646 "self_assignable": {
4647 "type": "boolean"
4648 },
4649 "system": {
4650 "type": "boolean"
4651 },
4652 "updated_at": {
4653 "type": "string",
4654 "format": "date-time"
4655 },
4656 "version": {
4657 "type": "integer",
4658 "format": "int32"
4659 }
4660 }
4661 },
4662 "error": {
4663 "type": [
4664 "string",
4665 "null"
4666 ]
4667 },
4668 "error_code": {
4669 "type": [
4670 "string",
4671 "null"
4672 ]
4673 },
4674 "message": {
4675 "type": "string"
4676 },
4677 "request_id": {
4678 "type": "string"
4679 }
4680 }
4681 },
4682 "Envelope_SshKeySummary": {
4683 "type": "object",
4684 "required": [
4685 "code",
4686 "message"
4687 ],
4688 "properties": {
4689 "code": {
4690 "type": "integer",
4691 "format": "int32"
4692 },
4693 "data": {
4694 "type": "object",
4695 "required": [
4696 "id",
4697 "name",
4698 "algorithm",
4699 "fingerprint",
4700 "public_key",
4701 "created_at"
4702 ],
4703 "properties": {
4704 "algorithm": {
4705 "$ref": "#/components/schemas/SshKeyAlgorithm"
4706 },
4707 "created_at": {
4708 "type": "string",
4709 "format": "date-time"
4710 },
4711 "description": {
4712 "type": [
4713 "string",
4714 "null"
4715 ]
4716 },
4717 "expires_at": {
4718 "type": [
4719 "string",
4720 "null"
4721 ],
4722 "format": "date-time"
4723 },
4724 "fingerprint": {
4725 "type": "string"
4726 },
4727 "id": {
4728 "type": "string",
4729 "format": "uuid"
4730 },
4731 "last_used_at": {
4732 "type": [
4733 "string",
4734 "null"
4735 ],
4736 "format": "date-time"
4737 },
4738 "name": {
4739 "type": "string"
4740 },
4741 "public_key": {
4742 "type": "string"
4743 },
4744 "revoked_at": {
4745 "type": [
4746 "string",
4747 "null"
4748 ],
4749 "format": "date-time"
4750 }
4751 }
4752 },
4753 "error": {
4754 "type": [
4755 "string",
4756 "null"
4757 ]
4758 },
4759 "error_code": {
4760 "type": [
4761 "string",
4762 "null"
4763 ]
4764 },
4765 "message": {
4766 "type": "string"
4767 },
4768 "request_id": {
4769 "type": "string"
4770 }
4771 }
4772 },
4773 "Envelope_UserLoginResponse": {
4774 "type": "object",
4775 "required": [
4776 "code",
4777 "message"
4778 ],
4779 "properties": {
4780 "code": {
4781 "type": "integer",
4782 "format": "int32"
4783 },
4784 "data": {
4785 "type": "object",
4786 "required": [
4787 "user_id",
4788 "username",
4789 "mfa_required"
4790 ],
4791 "properties": {
4792 "challenge": {
4793 "type": [
4794 "string",
4795 "null"
4796 ]
4797 },
4798 "displayname": {
4799 "type": [
4800 "string",
4801 "null"
4802 ]
4803 },
4804 "mfa_required": {
4805 "type": "boolean"
4806 },
4807 "user_id": {
4808 "type": "integer",
4809 "format": "int64"
4810 },
4811 "username": {
4812 "type": "string"
4813 }
4814 }
4815 },
4816 "error": {
4817 "type": [
4818 "string",
4819 "null"
4820 ]
4821 },
4822 "error_code": {
4823 "type": [
4824 "string",
4825 "null"
4826 ]
4827 },
4828 "message": {
4829 "type": "string"
4830 },
4831 "request_id": {
4832 "type": "string"
4833 }
4834 }
4835 },
4836 "Envelope_UserRegisterResponse": {
4837 "type": "object",
4838 "required": [
4839 "code",
4840 "message"
4841 ],
4842 "properties": {
4843 "code": {
4844 "type": "integer",
4845 "format": "int32"
4846 },
4847 "data": {
4848 "type": "object",
4849 "required": [
4850 "user_id",
4851 "username"
4852 ],
4853 "properties": {
4854 "displayname": {
4855 "type": [
4856 "string",
4857 "null"
4858 ]
4859 },
4860 "user_id": {
4861 "type": "integer",
4862 "format": "int64"
4863 },
4864 "username": {
4865 "type": "string"
4866 }
4867 }
4868 },
4869 "error": {
4870 "type": [
4871 "string",
4872 "null"
4873 ]
4874 },
4875 "error_code": {
4876 "type": [
4877 "string",
4878 "null"
4879 ]
4880 },
4881 "message": {
4882 "type": "string"
4883 },
4884 "request_id": {
4885 "type": "string"
4886 }
4887 }
4888 },
4889 "Envelope_Vec_InviteSummary": {
4890 "type": "object",
4891 "required": [
4892 "code",
4893 "message"
4894 ],
4895 "properties": {
4896 "code": {
4897 "type": "integer",
4898 "format": "int32"
4899 },
4900 "data": {
4901 "type": "array",
4902 "items": {
4903 "type": "object",
4904 "required": [
4905 "id",
4906 "organization_id",
4907 "method",
4908 "created_by",
4909 "max_uses",
4910 "uses",
4911 "created_at"
4912 ],
4913 "properties": {
4914 "created_at": {
4915 "type": "string",
4916 "format": "date-time"
4917 },
4918 "created_by": {
4919 "type": "integer",
4920 "format": "int64"
4921 },
4922 "email": {
4923 "type": [
4924 "string",
4925 "null"
4926 ]
4927 },
4928 "expires_at": {
4929 "type": [
4930 "string",
4931 "null"
4932 ],
4933 "format": "date-time"
4934 },
4935 "id": {
4936 "type": "string",
4937 "format": "uuid"
4938 },
4939 "max_uses": {
4940 "type": "integer",
4941 "format": "int32"
4942 },
4943 "method": {
4944 "$ref": "#/components/schemas/OrgJoinMethod"
4945 },
4946 "organization_id": {
4947 "type": "string",
4948 "format": "uuid"
4949 },
4950 "revoked_at": {
4951 "type": [
4952 "string",
4953 "null"
4954 ],
4955 "format": "date-time"
4956 },
4957 "role_id": {
4958 "type": [
4959 "string",
4960 "null"
4961 ],
4962 "format": "uuid"
4963 },
4964 "target_user_id": {
4965 "type": [
4966 "integer",
4967 "null"
4968 ],
4969 "format": "int64"
4970 },
4971 "uses": {
4972 "type": "integer",
4973 "format": "int32"
4974 }
4975 }
4976 }
4977 },
4978 "error": {
4979 "type": [
4980 "string",
4981 "null"
4982 ]
4983 },
4984 "error_code": {
4985 "type": [
4986 "string",
4987 "null"
4988 ]
4989 },
4990 "message": {
4991 "type": "string"
4992 },
4993 "request_id": {
4994 "type": "string"
4995 }
4996 }
4997 },
4998 "Envelope_Vec_MemberSummary": {
4999 "type": "object",
5000 "required": [
5001 "code",
5002 "message"
5003 ],
5004 "properties": {
5005 "code": {
5006 "type": "integer",
5007 "format": "int32"
5008 },
5009 "data": {
5010 "type": "array",
5011 "items": {
5012 "type": "object",
5013 "required": [
5014 "id",
5015 "organization_id",
5016 "user_id",
5017 "username",
5018 "role",
5019 "role_keys",
5020 "permissions",
5021 "joined_at",
5022 "join_method",
5023 "message_notifications",
5024 "muted",
5025 "mobile_push",
5026 "suppress_everyone",
5027 "suppress_roles",
5028 "hide_muted_channels",
5029 "ban_allow_agent_join",
5030 "ban_allow_speak",
5031 "ban_allow_push_repo",
5032 "ban_allow_comment_any",
5033 "version"
5034 ],
5035 "properties": {
5036 "alias_avatar_url": {
5037 "type": [
5038 "string",
5039 "null"
5040 ]
5041 },
5042 "alias_name": {
5043 "type": [
5044 "string",
5045 "null"
5046 ]
5047 },
5048 "ban_allow_agent_join": {
5049 "type": "boolean"
5050 },
5051 "ban_allow_comment_any": {
5052 "type": "boolean"
5053 },
5054 "ban_allow_push_repo": {
5055 "type": "boolean"
5056 },
5057 "ban_allow_speak": {
5058 "type": "boolean"
5059 },
5060 "ban_expires_at": {
5061 "type": [
5062 "string",
5063 "null"
5064 ],
5065 "format": "date-time"
5066 },
5067 "ban_reason": {
5068 "type": [
5069 "string",
5070 "null"
5071 ]
5072 },
5073 "banned_at": {
5074 "type": [
5075 "string",
5076 "null"
5077 ],
5078 "format": "date-time"
5079 },
5080 "displayname": {
5081 "type": [
5082 "string",
5083 "null"
5084 ]
5085 },
5086 "hide_muted_channels": {
5087 "type": "boolean"
5088 },
5089 "id": {
5090 "type": "string",
5091 "format": "uuid"
5092 },
5093 "invited_by": {
5094 "type": [
5095 "integer",
5096 "null"
5097 ],
5098 "format": "int64"
5099 },
5100 "join_method": {
5101 "$ref": "#/components/schemas/OrgJoinMethod"
5102 },
5103 "joined_at": {
5104 "type": "string",
5105 "format": "date-time"
5106 },
5107 "last_active_at": {
5108 "type": [
5109 "string",
5110 "null"
5111 ],
5112 "format": "date-time"
5113 },
5114 "left_at": {
5115 "type": [
5116 "string",
5117 "null"
5118 ],
5119 "format": "date-time"
5120 },
5121 "message_notifications": {
5122 "$ref": "#/components/schemas/MessageNotifications"
5123 },
5124 "mobile_push": {
5125 "type": "boolean"
5126 },
5127 "mute_until": {
5128 "type": [
5129 "string",
5130 "null"
5131 ],
5132 "format": "date-time"
5133 },
5134 "muted": {
5135 "type": "boolean"
5136 },
5137 "organization_id": {
5138 "type": "string",
5139 "format": "uuid"
5140 },
5141 "permissions": {
5142 "type": "array",
5143 "items": {
5144 "type": "string"
5145 }
5146 },
5147 "role": {
5148 "$ref": "#/components/schemas/OrgMemberRole"
5149 },
5150 "role_keys": {
5151 "type": "array",
5152 "items": {
5153 "type": "string"
5154 }
5155 },
5156 "suppress_everyone": {
5157 "type": "boolean"
5158 },
5159 "suppress_roles": {
5160 "type": "boolean"
5161 },
5162 "user_id": {
5163 "type": "integer",
5164 "format": "int64"
5165 },
5166 "username": {
5167 "type": "string"
5168 },
5169 "version": {
5170 "type": "integer",
5171 "format": "int32"
5172 }
5173 }
5174 }
5175 },
5176 "error": {
5177 "type": [
5178 "string",
5179 "null"
5180 ]
5181 },
5182 "error_code": {
5183 "type": [
5184 "string",
5185 "null"
5186 ]
5187 },
5188 "message": {
5189 "type": "string"
5190 },
5191 "request_id": {
5192 "type": "string"
5193 }
5194 }
5195 },
5196 "Envelope_Vec_OrganizationSummary": {
5197 "type": "object",
5198 "required": [
5199 "code",
5200 "message"
5201 ],
5202 "properties": {
5203 "code": {
5204 "type": "integer",
5205 "format": "int32"
5206 },
5207 "data": {
5208 "type": "array",
5209 "items": {
5210 "type": "object",
5211 "required": [
5212 "id",
5213 "slug",
5214 "name",
5215 "status",
5216 "owner_user_id",
5217 "member_count",
5218 "version",
5219 "created_at",
5220 "updated_at",
5221 "join_policy",
5222 "allow_join",
5223 "require_email_verified",
5224 "require_mfa",
5225 "member_must_enable_2fa",
5226 "allow_invite",
5227 "allow_join_level"
5228 ],
5229 "properties": {
5230 "allow_invite": {
5231 "type": "boolean"
5232 },
5233 "allow_join": {
5234 "type": "boolean"
5235 },
5236 "allow_join_level": {
5237 "type": "integer",
5238 "format": "int32"
5239 },
5240 "archived_at": {
5241 "type": [
5242 "string",
5243 "null"
5244 ],
5245 "format": "date-time"
5246 },
5247 "avatar_url": {
5248 "type": [
5249 "string",
5250 "null"
5251 ]
5252 },
5253 "created_at": {
5254 "type": "string",
5255 "format": "date-time"
5256 },
5257 "description": {
5258 "type": [
5259 "string",
5260 "null"
5261 ]
5262 },
5263 "id": {
5264 "type": "string",
5265 "format": "uuid"
5266 },
5267 "join_policy": {
5268 "$ref": "#/components/schemas/OrganizationJoinPolicy"
5269 },
5270 "member_count": {
5271 "type": "integer",
5272 "format": "int64"
5273 },
5274 "member_must_enable_2fa": {
5275 "type": "boolean"
5276 },
5277 "my_role": {
5278 "oneOf": [
5279 {
5280 "type": "null"
5281 },
5282 {
5283 "$ref": "#/components/schemas/OrgMemberRole"
5284 }
5285 ]
5286 },
5287 "name": {
5288 "type": "string"
5289 },
5290 "owner_user_id": {
5291 "type": "integer",
5292 "format": "int64"
5293 },
5294 "require_email_verified": {
5295 "type": "boolean"
5296 },
5297 "require_mfa": {
5298 "type": "boolean"
5299 },
5300 "slug": {
5301 "type": "string"
5302 },
5303 "status": {
5304 "$ref": "#/components/schemas/OrganizationStatus"
5305 },
5306 "updated_at": {
5307 "type": "string",
5308 "format": "date-time"
5309 },
5310 "version": {
5311 "type": "integer",
5312 "format": "int32"
5313 },
5314 "welcome_message": {
5315 "type": [
5316 "string",
5317 "null"
5318 ]
5319 }
5320 }
5321 }
5322 },
5323 "error": {
5324 "type": [
5325 "string",
5326 "null"
5327 ]
5328 },
5329 "error_code": {
5330 "type": [
5331 "string",
5332 "null"
5333 ]
5334 },
5335 "message": {
1270 "context": {
53361271 "type": "string"
53371272 },
5338 "request_id": {
5339 "type": "string"
5340 }
5341 }
5342 },
5343 "Envelope_Vec_RoleSummary": {
5344 "type": "object",
5345 "required": [
5346 "code",
5347 "message"
5348 ],
5349 "properties": {
5350 "code": {
1273 "difficulty": {
53511274 "type": "integer",
5352 "format": "int32"
1275 "format": "int32",
1276 "minimum": 0
53531277 },
5354 "data": {
5355 "type": "array",
5356 "items": {
5357 "type": "object",
5358 "required": [
5359 "id",
5360 "organization_id",
5361 "key",
5362 "name",
5363 "rank",
5364 "system",
5365 "default_role",
5366 "self_assignable",
5367 "permissions",
5368 "member_count",
5369 "created_at",
5370 "updated_at",
5371 "version",
5372 "position"
5373 ],
5374 "properties": {
5375 "color": {
5376 "type": [
5377 "string",
5378 "null"
5379 ]
5380 },
5381 "created_at": {
5382 "type": "string",
5383 "format": "date-time"
5384 },
5385 "default_role": {
5386 "type": "boolean"
5387 },
5388 "description": {
5389 "type": [
5390 "string",
5391 "null"
5392 ]
5393 },
5394 "emoji": {
5395 "type": [
5396 "string",
5397 "null"
5398 ]
5399 },
5400 "icon_hash": {
5401 "type": [
5402 "string",
5403 "null"
5404 ]
5405 },
5406 "id": {
5407 "type": "string",
5408 "format": "uuid"
5409 },
5410 "key": {
5411 "type": "string"
5412 },
5413 "member_count": {
5414 "type": "integer",
5415 "format": "int64"
5416 },
5417 "name": {
5418 "type": "string"
5419 },
5420 "organization_id": {
5421 "type": "string",
5422 "format": "uuid"
5423 },
5424 "permissions": {
5425 "type": "array",
5426 "items": {
5427 "$ref": "#/components/schemas/PermissionGrant"
5428 }
5429 },
5430 "position": {
5431 "type": "integer",
5432 "format": "int64"
5433 },
5434 "rank": {
5435 "type": "integer",
5436 "format": "int32"
5437 },
5438 "self_assignable": {
5439 "type": "boolean"
5440 },
5441 "system": {
5442 "type": "boolean"
5443 },
5444 "updated_at": {
5445 "type": "string",
5446 "format": "date-time"
5447 },
5448 "version": {
5449 "type": "integer",
5450 "format": "int32"
5451 }
5452 }
5453 }
1278 "expires_at": {
1279 "type": "integer",
1280 "format": "int64",
1281 "minimum": 0
54541282 },
5455 "error": {
5456 "type": [
5457 "string",
5458 "null"
5459 ]
5460 },
5461 "error_code": {
5462 "type": [
5463 "string",
5464 "null"
5465 ]
5466 },
5467 "message": {
5468 "type": "string"
5469 },
5470 "request_id": {
5471 "type": "string"
1283 "issued_at": {
1284 "type": "integer",
1285 "format": "int64",
1286 "minimum": 0
54721287 }
54731288 }
54741289 },
5475 "Envelope_Vec_SshKeySummary": {
1290 "Envelope_ChallengeResponse": {
54761291 "type": "object",
54771292 "required": [
54781293 "code",
@@ -5484,65 +1299,31 @@
54841299 "format": "int32"
54851300 },
54861301 "data": {
5487 "type": "array",
5488 "items": {
5489 "type": "object",
5490 "required": [
5491 "id",
5492 "name",
5493 "algorithm",
5494 "fingerprint",
5495 "public_key",
5496 "created_at"
5497 ],
5498 "properties": {
5499 "algorithm": {
5500 "$ref": "#/components/schemas/SshKeyAlgorithm"
5501 },
5502 "created_at": {
5503 "type": "string",
5504 "format": "date-time"
5505 },
5506 "description": {
5507 "type": [
5508 "string",
5509 "null"
5510 ]
5511 },
5512 "expires_at": {
5513 "type": [
5514 "string",
5515 "null"
5516 ],
5517 "format": "date-time"
5518 },
5519 "fingerprint": {
5520 "type": "string"
5521 },
5522 "id": {
5523 "type": "string",
5524 "format": "uuid"
5525 },
5526 "last_used_at": {
5527 "type": [
5528 "string",
5529 "null"
5530 ],
5531 "format": "date-time"
5532 },
5533 "name": {
5534 "type": "string"
5535 },
5536 "public_key": {
5537 "type": "string"
5538 },
5539 "revoked_at": {
5540 "type": [
5541 "string",
5542 "null"
5543 ],
5544 "format": "date-time"
5545 }
1302 "type": "object",
1303 "required": [
1304 "context",
1305 "difficulty",
1306 "issued_at",
1307 "expires_at"
1308 ],
1309 "properties": {
1310 "context": {
1311 "type": "string"
1312 },
1313 "difficulty": {
1314 "type": "integer",
1315 "format": "int32",
1316 "minimum": 0
1317 },
1318 "expires_at": {
1319 "type": "integer",
1320 "format": "int64",
1321 "minimum": 0
1322 },
1323 "issued_at": {
1324 "type": "integer",
1325 "format": "int64",
1326 "minimum": 0
55461327 }
55471328 }
55481329 },
@@ -5566,7 +1347,7 @@
55661347 }
55671348 }
55681349 },
5569 "Envelope_Vec_String": {
1350 "Envelope_LoginCodeResponse": {
55701351 "type": "object",
55711352 "required": [
55721353 "code",
@@ -5578,9 +1359,16 @@
55781359 "format": "int32"
55791360 },
55801361 "data": {
5581 "type": "array",
5582 "items": {
5583 "type": "string"
1362 "type": "object",
1363 "required": [
1364 "expires_in_secs"
1365 ],
1366 "properties": {
1367 "expires_in_secs": {
1368 "type": "integer",
1369 "format": "int64",
1370 "minimum": 0
1371 }
55841372 }
55851373 },
55861374 "error": {
@@ -5603,7 +1391,7 @@
56031391 }
56041392 }
56051393 },
5606 "Envelope_bool": {
1394 "Envelope_LogoutResponse": {
56071395 "type": "object",
56081396 "required": [
56091397 "code",
@@ -5615,7 +1403,16 @@
56151403 "format": "int32"
56161404 },
56171405 "data": {
5618 "type": "boolean"
1406 "type": "object",
1407 "properties": {
1408 "user_id": {
1409 "type": [
1410 "integer",
1411 "null"
1412 ],
1413 "format": "int64"
1414 }
1415 }
56191416 },
56201417 "error": {
56211418 "type": [
@@ -5637,973 +1434,792 @@
56371434 }
56381435 }
56391436 },
5640 "InviteSummary": {
1437 "Envelope_MeResponse": {
56411438 "type": "object",
56421439 "required": [
5643 "id",
5644 "organization_id",
5645 "method",
5646 "created_by",
5647 "max_uses",
5648 "uses",
5649 "created_at"
1440 "code",
1441 "message"
56501442 ],
56511443 "properties": {
5652 "created_at": {
5653 "type": "string",
5654 "format": "date-time"
5655 },
5656 "created_by": {
5657 "type": "integer",
5658 "format": "int64"
5659 },
5660 "email": {
5661 "type": [
5662 "string",
5663 "null"
5664 ]
5665 },
5666 "expires_at": {
5667 "type": [
5668 "string",
5669 "null"
5670 ],
5671 "format": "date-time"
5672 },
5673 "id": {
5674 "type": "string",
5675 "format": "uuid"
5676 },
5677 "max_uses": {
1444 "code": {
56781445 "type": "integer",
56791446 "format": "int32"
56801447 },
5681 "method": {
5682 "$ref": "#/components/schemas/OrgJoinMethod"
5683 },
5684 "organization_id": {
5685 "type": "string",
5686 "format": "uuid"
5687 },
5688 "revoked_at": {
5689 "type": [
5690 "string",
5691 "null"
1448 "data": {
1449 "type": "object",
1450 "required": [
1451 "user_id",
1452 "username",
1453 "email",
1454 "is_admin",
1455 "created_at",
1456 "version"
56921457 ],
5693 "format": "date-time"
1458 "properties": {
1459 "avatar_url": {
1460 "type": [
1461 "string",
1462 "null"
1463 ]
1464 },
1465 "created_at": {
1466 "type": "string",
1467 "format": "date-time"
1468 },
1469 "displayname": {
1470 "type": [
1471 "string",
1472 "null"
1473 ]
1474 },
1475 "email": {
1476 "type": "string"
1477 },
1478 "email_verified_at": {
1479 "type": [
1480 "string",
1481 "null"
1482 ],
1483 "format": "date-time"
1484 },
1485 "is_admin": {
1486 "type": "boolean"
1487 },
1488 "user_id": {
1489 "type": "integer",
1490 "format": "int64"
1491 },
1492 "username": {
1493 "type": "string"
1494 },
1495 "version": {
1496 "type": "integer",
1497 "format": "int32"
1498 }
1499 }
56941500 },
5695 "role_id": {
5696 "type": [
5697 "string",
5698 "null"
5699 ],
5700 "format": "uuid"
5701 },
5702 "target_user_id": {
5703 "type": [
5704 "integer",
5705 "null"
5706 ],
5707 "format": "int64"
5708 },
5709 "uses": {
5710 "type": "integer",
5711 "format": "int32"
5712 }
5713 }
5714 },
5715 "LoginCodeRequest": {
5716 "type": "object",
5717 "required": [
5718 "email",
5719 "pow_payload"
5720 ],
5721 "properties": {
5722 "email": {
5723 "type": "string"
5724 },
5725 "pow_payload": {
5726 "$ref": "#/components/schemas/PowProofRequest"
5727 }
5728 }
5729 },
5730 "LoginCodeResponse": {
5731 "type": "object",
5732 "required": [
5733 "expires_in_secs"
5734 ],
5735 "properties": {
5736 "expires_in_secs": {
5737 "type": "integer",
5738 "format": "int64",
5739 "minimum": 0
5740 }
5741 }
5742 },
5743 "LoginCodeVerifyRequest": {
5744 "type": "object",
5745 "required": [
5746 "email",
5747 "email_code",
5748 "pow_payload"
5749 ],
5750 "properties": {
5751 "email": {
5752 "type": "string"
5753 },
5754 "email_code": {
5755 "type": "string"
5756 },
5757 "pow_payload": {
5758 "$ref": "#/components/schemas/PowProofRequest"
5759 }
5760 }
5761 },
5762 "LoginVerifyRequest": {
5763 "type": "object",
5764 "required": [
5765 "challenge",
5766 "code"
5767 ],
5768 "properties": {
5769 "challenge": {
5770 "type": "string"
5771 },
5772 "code": {
5773 "type": "string"
5774 }
5775 }
5776 },
5777 "LogoutResponse": {
5778 "type": "object",
5779 "properties": {
5780 "user_id": {
5781 "type": [
5782 "integer",
5783 "null"
5784 ],
5785 "format": "int64"
5786 }
5787 }
5788 },
5789 "MeResponse": {
5790 "type": "object",
5791 "required": [
5792 "user_id",
5793 "username",
5794 "email",
5795 "is_admin",
5796 "created_at",
5797 "version"
5798 ],
5799 "properties": {
5800 "avatar_url": {
5801 "type": [
5802 "string",
5803 "null"
5804 ]
5805 },
5806 "created_at": {
5807 "type": "string",
5808 "format": "date-time"
5809 },
5810 "displayname": {
1501 "error": {
58111502 "type": [
58121503 "string",
58131504 "null"
58141505 ]
58151506 },
5816 "email": {
5817 "type": "string"
5818 },
5819 "email_verified_at": {
5820 "type": [
5821 "string",
5822 "null"
5823 ],
5824 "format": "date-time"
5825 },
5826 "is_admin": {
5827 "type": "boolean"
5828 },
5829 "user_id": {
5830 "type": "integer",
5831 "format": "int64"
1507 "error_code": {
1508 "type": [
1509 "string",
1510 "null"
1511 ]
58321512 },
5833 "username": {
1513 "message": {
58341514 "type": "string"
58351515 },
5836 "version": {
5837 "type": "integer",
5838 "format": "int32"
1516 "request_id": {
1517 "type": "string"
58391518 }
58401519 }
58411520 },
5842 "MemberListQuery": {
1521 "Envelope_NoData": {
58431522 "type": "object",
1523 "required": [
1524 "code",
1525 "message"
1526 ],
58441527 "properties": {
5845 "include_banned": {
1528 "code": {
1529 "type": "integer",
1530 "format": "int32"
1531 },
1532 "data": {
1533 "default": null
1534 },
1535 "error": {
58461536 "type": [
5847 "boolean",
1537 "string",
58481538 "null"
58491539 ]
58501540 },
5851 "include_inactive": {
1541 "error_code": {
58521542 "type": [
5853 "boolean",
1543 "string",
58541544 "null"
58551545 ]
58561546 },
5857 "page": {
5858 "type": [
5859 "integer",
5860 "null"
5861 ],
5862 "format": "int64",
5863 "minimum": 0
1547 "message": {
1548 "type": "string"
58641549 },
5865 "page_size": {
5866 "type": [
5867 "integer",
5868 "null"
5869 ],
5870 "format": "int64",
5871 "minimum": 0
1550 "request_id": {
1551 "type": "string"
58721552 }
58731553 }
58741554 },
5875 "MemberSummary": {
1555 "Envelope_PasswordResetConfirmResponse": {
58761556 "type": "object",
58771557 "required": [
5878 "id",
5879 "organization_id",
5880 "user_id",
5881 "username",
5882 "role",
5883 "role_keys",
5884 "permissions",
5885 "joined_at",
5886 "join_method",
5887 "message_notifications",
5888 "muted",
5889 "mobile_push",
5890 "suppress_everyone",
5891 "suppress_roles",
5892 "hide_muted_channels",
5893 "ban_allow_agent_join",
5894 "ban_allow_speak",
5895 "ban_allow_push_repo",
5896 "ban_allow_comment_any",
5897 "version"
1558 "code",
1559 "message"
58981560 ],
58991561 "properties": {
5900 "alias_avatar_url": {
1562 "code": {
1563 "type": "integer",
1564 "format": "int32"
1565 },
1566 "data": {
1567 "type": "object",
1568 "required": [
1569 "user_id"
1570 ],
1571 "properties": {
1572 "user_id": {
1573 "type": "integer",
1574 "format": "int64"
1575 }
1576 }
1577 },
1578 "error": {
59011579 "type": [
59021580 "string",
59031581 "null"
59041582 ]
59051583 },
5906 "alias_name": {
1584 "error_code": {
59071585 "type": [
59081586 "string",
59091587 "null"
59101588 ]
59111589 },
5912 "ban_allow_agent_join": {
5913 "type": "boolean"
1590 "message": {
1591 "type": "string"
59141592 },
5915 "ban_allow_comment_any": {
5916 "type": "boolean"
1593 "request_id": {
1594 "type": "string"
1595 }
1596 }
1597 },
1598 "Envelope_PasswordResetResponse": {
1599 "type": "object",
1600 "required": [
1601 "code",
1602 "message"
1603 ],
1604 "properties": {
1605 "code": {
1606 "type": "integer",
1607 "format": "int32"
59171608 },
5918 "ban_allow_push_repo": {
5919 "type": "boolean"
5920 },
5921 "ban_allow_speak": {
5922 "type": "boolean"
5923 },
5924 "ban_expires_at": {
5925 "type": [
5926 "string",
5927 "null"
1609 "data": {
1610 "type": "object",
1611 "required": [
1612 "expires_in_secs"
59281613 ],
5929 "format": "date-time"
1614 "properties": {
1615 "expires_in_secs": {
1616 "type": "integer",
1617 "format": "int64",
1618 "minimum": 0
1619 }
1620 }
59301621 },
5931 "ban_reason": {
1622 "error": {
59321623 "type": [
59331624 "string",
59341625 "null"
59351626 ]
59361627 },
5937 "banned_at": {
5938 "type": [
5939 "string",
5940 "null"
5941 ],
5942 "format": "date-time"
5943 },
5944 "displayname": {
1628 "error_code": {
59451629 "type": [
59461630 "string",
59471631 "null"
59481632 ]
59491633 },
5950 "hide_muted_channels": {
5951 "type": "boolean"
1634 "message": {
1635 "type": "string"
59521636 },
5953 "id": {
5954 "type": "string",
5955 "format": "uuid"
1637 "request_id": {
1638 "type": "string"
1639 }
1640 }
1641 },
1642 "Envelope_RegisterCodeResponse": {
1643 "type": "object",
1644 "required": [
1645 "code",
1646 "message"
1647 ],
1648 "properties": {
1649 "code": {
1650 "type": "integer",
1651 "format": "int32"
59561652 },
5957 "invited_by": {
5958 "type": [
5959 "integer",
5960 "null"
1653 "data": {
1654 "type": "object",
1655 "required": [
1656 "expires_in_secs"
59611657 ],
5962 "format": "int64"
1658 "properties": {
1659 "expires_in_secs": {
1660 "type": "integer",
1661 "format": "int64",
1662 "minimum": 0
1663 }
1664 }
59631665 },
5964 "join_method": {
5965 "$ref": "#/components/schemas/OrgJoinMethod"
5966 },
5967 "joined_at": {
5968 "type": "string",
5969 "format": "date-time"
5970 },
5971 "last_active_at": {
1666 "error": {
59721667 "type": [
59731668 "string",
59741669 "null"
5975 ],
5976 "format": "date-time"
1670 ]
59771671 },
5978 "left_at": {
1672 "error_code": {
59791673 "type": [
59801674 "string",
59811675 "null"
5982 ],
5983 "format": "date-time"
1676 ]
59841677 },
5985 "message_notifications": {
5986 "$ref": "#/components/schemas/MessageNotifications"
1678 "message": {
1679 "type": "string"
59871680 },
5988 "mobile_push": {
5989 "type": "boolean"
1681 "request_id": {
1682 "type": "string"
1683 }
1684 }
1685 },
1686 "Envelope_SshKeySummary": {
1687 "type": "object",
1688 "required": [
1689 "code",
1690 "message"
1691 ],
1692 "properties": {
1693 "code": {
1694 "type": "integer",
1695 "format": "int32"
59901696 },
5991 "mute_until": {
5992 "type": [
5993 "string",
5994 "null"
1697 "data": {
1698 "type": "object",
1699 "required": [
1700 "id",
1701 "name",
1702 "algorithm",
1703 "fingerprint",
1704 "public_key",
1705 "created_at"
59951706 ],
5996 "format": "date-time"
5997 },
5998 "muted": {
5999 "type": "boolean"
6000 },
6001 "organization_id": {
6002 "type": "string",
6003 "format": "uuid"
6004 },
6005 "permissions": {
6006 "type": "array",
6007 "items": {
6008 "type": "string"
6009 }
6010 },
6011 "role": {
6012 "$ref": "#/components/schemas/OrgMemberRole"
6013 },
6014 "role_keys": {
6015 "type": "array",
6016 "items": {
6017 "type": "string"
1707 "properties": {
1708 "algorithm": {
1709 "$ref": "#/components/schemas/SshKeyAlgorithm"
1710 },
1711 "created_at": {
1712 "type": "string",
1713 "format": "date-time"
1714 },
1715 "description": {
1716 "type": [
1717 "string",
1718 "null"
1719 ]
1720 },
1721 "expires_at": {
1722 "type": [
1723 "string",
1724 "null"
1725 ],
1726 "format": "date-time"
1727 },
1728 "fingerprint": {
1729 "type": "string"
1730 },
1731 "id": {
1732 "type": "string",
1733 "format": "uuid"
1734 },
1735 "last_used_at": {
1736 "type": [
1737 "string",
1738 "null"
1739 ],
1740 "format": "date-time"
1741 },
1742 "name": {
1743 "type": "string"
1744 },
1745 "public_key": {
1746 "type": "string"
1747 },
1748 "revoked_at": {
1749 "type": [
1750 "string",
1751 "null"
1752 ],
1753 "format": "date-time"
1754 }
60181755 }
60191756 },
6020 "suppress_everyone": {
6021 "type": "boolean"
1757 "error": {
1758 "type": [
1759 "string",
1760 "null"
1761 ]
60221762 },
6023 "suppress_roles": {
6024 "type": "boolean"
1763 "error_code": {
1764 "type": [
1765 "string",
1766 "null"
1767 ]
60251768 },
6026 "user_id": {
6027 "type": "integer",
6028 "format": "int64"
1769 "message": {
1770 "type": "string"
60291771 },
6030 "username": {
1772 "request_id": {
60311773 "type": "string"
6032 },
6033 "version": {
6034 "type": "integer",
6035 "format": "int32"
60361774 }
60371775 }
60381776 },
6039 "MessageNotifications": {
6040 "type": "string",
6041 "description": "会籍的通知偏好,对应旧 `space_members.message_notifications`。\n旧表用 0/1/2 存,这里用字符串存,便于跨服务读写时直接透传。",
6042 "enum": [
6043 "all",
6044 "mentions",
6045 "nothing"
6046 ]
6047 },
6048 "NoData": {
6049 "default": null
6050 },
6051 "OrgInvitePath": {
1777 "Envelope_UserLoginResponse": {
60521778 "type": "object",
60531779 "required": [
6054 "organization_id",
6055 "invite_id"
1780 "code",
1781 "message"
60561782 ],
60571783 "properties": {
6058 "invite_id": {
6059 "type": "string",
6060 "format": "uuid"
1784 "code": {
1785 "type": "integer",
1786 "format": "int32"
1787 },
1788 "data": {
1789 "type": "object",
1790 "required": [
1791 "user_id",
1792 "username",
1793 "mfa_required"
1794 ],
1795 "properties": {
1796 "challenge": {
1797 "type": [
1798 "string",
1799 "null"
1800 ]
1801 },
1802 "displayname": {
1803 "type": [
1804 "string",
1805 "null"
1806 ]
1807 },
1808 "mfa_required": {
1809 "type": "boolean"
1810 },
1811 "user_id": {
1812 "type": "integer",
1813 "format": "int64"
1814 },
1815 "username": {
1816 "type": "string"
1817 }
1818 }
60611819 },
6062 "organization_id": {
6063 "type": "string",
6064 "format": "uuid"
6065 }
6066 }
6067 },
6068 "OrgJoinMethod": {
6069 "type": "string",
6070 "enum": [
6071 "direct",
6072 "invite_link",
6073 "invite_targeted",
6074 "invite_email"
6075 ]
6076 },
6077 "OrgListQuery": {
6078 "type": "object",
6079 "properties": {
6080 "include_archived": {
1820 "error": {
60811821 "type": [
6082 "boolean",
1822 "string",
60831823 "null"
60841824 ]
60851825 },
6086 "page": {
1826 "error_code": {
60871827 "type": [
6088 "integer",
1828 "string",
60891829 "null"
6090 ],
6091 "format": "int64",
6092 "minimum": 0
1830 ]
1831 },
1832 "message": {
1833 "type": "string"
60931834 },
6094 "page_size": {
6095 "type": [
6096 "integer",
6097 "null"
6098 ],
6099 "format": "int64",
6100 "minimum": 0
1835 "request_id": {
1836 "type": "string"
61011837 }
61021838 }
61031839 },
6104 "OrgMemberPath": {
1840 "Envelope_UserRegisterResponse": {
61051841 "type": "object",
61061842 "required": [
6107 "organization_id",
6108 "member_id"
1843 "code",
1844 "message"
61091845 ],
61101846 "properties": {
6111 "member_id": {
6112 "type": "string",
6113 "format": "uuid"
1847 "code": {
1848 "type": "integer",
1849 "format": "int32"
1850 },
1851 "data": {
1852 "type": "object",
1853 "required": [
1854 "user_id",
1855 "username"
1856 ],
1857 "properties": {
1858 "displayname": {
1859 "type": [
1860 "string",
1861 "null"
1862 ]
1863 },
1864 "user_id": {
1865 "type": "integer",
1866 "format": "int64"
1867 },
1868 "username": {
1869 "type": "string"
1870 }
1871 }
1872 },
1873 "error": {
1874 "type": [
1875 "string",
1876 "null"
1877 ]
1878 },
1879 "error_code": {
1880 "type": [
1881 "string",
1882 "null"
1883 ]
61141884 },
6115 "organization_id": {
6116 "type": "string",
6117 "format": "uuid"
6118 }
6119 }
6120 },
6121 "OrgMemberRole": {
6122 "type": "string",
6123 "enum": [
6124 "member",
6125 "admin",
6126 "owner"
6127 ]
6128 },
6129 "OrgPath": {
6130 "type": "object",
6131 "required": [
6132 "organization_id"
6133 ],
6134 "properties": {
6135 "organization_id": {
6136 "type": "string",
6137 "format": "uuid"
6138 }
6139 }
6140 },
6141 "OrgRolePath": {
6142 "type": "object",
6143 "required": [
6144 "organization_id",
6145 "role_id"
6146 ],
6147 "properties": {
6148 "organization_id": {
6149 "type": "string",
6150 "format": "uuid"
1885 "message": {
1886 "type": "string"
61511887 },
6152 "role_id": {
6153 "type": "string",
6154 "format": "uuid"
1888 "request_id": {
1889 "type": "string"
61551890 }
61561891 }
61571892 },
6158 "OrganizationJoinPolicy": {
6159 "type": "string",
6160 "enum": [
6161 "open",
6162 "approval",
6163 "invite_only",
6164 "private"
6165 ]
6166 },
6167 "OrganizationStatus": {
6168 "type": "string",
6169 "enum": [
6170 "active",
6171 "suspended",
6172 "archived"
6173 ]
6174 },
6175 "OrganizationSummary": {
1893 "Envelope_Vec_SshKeySummary": {
61761894 "type": "object",
61771895 "required": [
6178 "id",
6179 "slug",
6180 "name",
6181 "status",
6182 "owner_user_id",
6183 "member_count",
6184 "version",
6185 "created_at",
6186 "updated_at",
6187 "join_policy",
6188 "allow_join",
6189 "require_email_verified",
6190 "require_mfa",
6191 "member_must_enable_2fa",
6192 "allow_invite",
6193 "allow_join_level"
1896 "code",
1897 "message"
61941898 ],
61951899 "properties": {
6196 "allow_invite": {
6197 "type": "boolean"
1900 "code": {
1901 "type": "integer",
1902 "format": "int32"
61981903 },
6199 "allow_join": {
6200 "type": "boolean"
1904 "data": {
1905 "type": "array",
1906 "items": {
1907 "type": "object",
1908 "required": [
1909 "id",
1910 "name",
1911 "algorithm",
1912 "fingerprint",
1913 "public_key",
1914 "created_at"
1915 ],
1916 "properties": {
1917 "algorithm": {
1918 "$ref": "#/components/schemas/SshKeyAlgorithm"
1919 },
1920 "created_at": {
1921 "type": "string",
1922 "format": "date-time"
1923 },
1924 "description": {
1925 "type": [
1926 "string",
1927 "null"
1928 ]
1929 },
1930 "expires_at": {
1931 "type": [
1932 "string",
1933 "null"
1934 ],
1935 "format": "date-time"
1936 },
1937 "fingerprint": {
1938 "type": "string"
1939 },
1940 "id": {
1941 "type": "string",
1942 "format": "uuid"
1943 },
1944 "last_used_at": {
1945 "type": [
1946 "string",
1947 "null"
1948 ],
1949 "format": "date-time"
1950 },
1951 "name": {
1952 "type": "string"
1953 },
1954 "public_key": {
1955 "type": "string"
1956 },
1957 "revoked_at": {
1958 "type": [
1959 "string",
1960 "null"
1961 ],
1962 "format": "date-time"
1963 }
1964 }
1965 }
62011966 },
6202 "allow_join_level": {
6203 "type": "integer",
6204 "format": "int32"
6205 },
6206 "archived_at": {
6207 "type": [
6208 "string",
6209 "null"
6210 ],
6211 "format": "date-time"
6212 },
6213 "avatar_url": {
1967 "error": {
62141968 "type": [
62151969 "string",
62161970 "null"
62171971 ]
62181972 },
6219 "created_at": {
6220 "type": "string",
6221 "format": "date-time"
6222 },
6223 "description": {
1973 "error_code": {
62241974 "type": [
62251975 "string",
62261976 "null"
62271977 ]
62281978 },
6229 "id": {
6230 "type": "string",
6231 "format": "uuid"
6232 },
6233 "join_policy": {
6234 "$ref": "#/components/schemas/OrganizationJoinPolicy"
6235 },
6236 "member_count": {
6237 "type": "integer",
6238 "format": "int64"
6239 },
6240 "member_must_enable_2fa": {
6241 "type": "boolean"
6242 },
6243 "my_role": {
6244 "oneOf": [
6245 {
6246 "type": "null"
6247 },
6248 {
6249 "$ref": "#/components/schemas/OrgMemberRole"
6250 }
6251 ]
6252 },
6253 "name": {
1979 "message": {
62541980 "type": "string"
62551981 },
6256 "owner_user_id": {
6257 "type": "integer",
6258 "format": "int64"
6259 },
6260 "require_email_verified": {
6261 "type": "boolean"
6262 },
6263 "require_mfa": {
6264 "type": "boolean"
6265 },
6266 "slug": {
1982 "request_id": {
62671983 "type": "string"
6268 },
6269 "status": {
6270 "$ref": "#/components/schemas/OrganizationStatus"
6271 },
6272 "updated_at": {
6273 "type": "string",
6274 "format": "date-time"
6275 },
6276 "version": {
6277 "type": "integer",
6278 "format": "int32"
6279 },
6280 "welcome_message": {
6281 "type": [
6282 "string",
6283 "null"
6284 ]
62851984 }
62861985 }
62871986 },
6288 "PasswordResetConfirmRequest": {
1987 "LoginCodeRequest": {
62891988 "type": "object",
62901989 "required": [
6291 "token",
6292 "password_hash",
1990 "email",
62931991 "pow_payload"
62941992 ],
62951993 "properties": {
6296 "password_hash": {
1994 "email": {
62971995 "type": "string"
62981996 },
62991997 "pow_payload": {
63001998 "$ref": "#/components/schemas/PowProofRequest"
6301 },
6302 "token": {
6303 "type": "string"
63041999 }
63052000 }
63062001 },
6307 "PasswordResetConfirmResponse": {
2002 "LoginCodeResponse": {
63082003 "type": "object",
63092004 "required": [
6310 "user_id"
2005 "expires_in_secs"
63112006 ],
63122007 "properties": {
6313 "user_id": {
2008 "expires_in_secs": {
63142009 "type": "integer",
6315 "format": "int64"
2010 "format": "int64",
2011 "minimum": 0
63162012 }
63172013 }
63182014 },
6319 "PasswordResetRequest": {
2015 "LoginCodeVerifyRequest": {
63202016 "type": "object",
63212017 "required": [
63222018 "email",
2019 "email_code",
63232020 "pow_payload"
63242021 ],
63252022 "properties": {
63262023 "email": {
63272024 "type": "string"
63282025 },
2026 "email_code": {
2027 "type": "string"
2028 },
63292029 "pow_payload": {
63302030 "$ref": "#/components/schemas/PowProofRequest"
63312031 }
63322032 }
63332033 },
6334 "PasswordResetResponse": {
2034 "LoginVerifyRequest": {
63352035 "type": "object",
63362036 "required": [
6337 "expires_in_secs"
2037 "challenge",
2038 "code"
63382039 ],
63392040 "properties": {
6340 "expires_in_secs": {
6341 "type": "integer",
6342 "format": "int64",
6343 "minimum": 0
6344 }
6345 }
6346 },
6347 "PermissionCheck": {
6348 "type": "object",
6349 "required": [
6350 "allowed",
6351 "reason",
6352 "matched_by",
6353 "organization_version"
6354 ],
6355 "properties": {
6356 "allowed": {
6357 "type": "boolean"
6358 },
6359 "matched_by": {
2041 "challenge": {
63602042 "type": "string"
63612043 },
6362 "organization_version": {
6363 "type": "integer",
6364 "format": "int32"
6365 },
6366 "reason": {
2044 "code": {
63672045 "type": "string"
63682046 }
63692047 }
63702048 },
6371 "PermissionEffect": {
6372 "type": "string",
6373 "enum": [
6374 "allow",
6375 "deny"
6376 ]
6377 },
6378 "PermissionGrant": {
2049 "LogoutResponse": {
63792050 "type": "object",
6380 "required": [
6381 "permission",
6382 "effect"
6383 ],
63842051 "properties": {
6385 "effect": {
6386 "$ref": "#/components/schemas/PermissionEffect"
6387 },
6388 "permission": {
6389 "type": "string"
2052 "user_id": {
2053 "type": [
2054 "integer",
2055 "null"
2056 ],
2057 "format": "int64"
63902058 }
63912059 }
63922060 },
6393 "PowProofRequest": {
2061 "MeResponse": {
63942062 "type": "object",
63952063 "required": [
6396 "nonce",
6397 "payload"
2064 "user_id",
2065 "username",
2066 "email",
2067 "is_admin",
2068 "created_at",
2069 "version"
63982070 ],
63992071 "properties": {
6400 "nonce": {
6401 "type": "integer",
6402 "format": "int64",
6403 "minimum": 0
2072 "avatar_url": {
2073 "type": [
2074 "string",
2075 "null"
2076 ]
2077 },
2078 "created_at": {
2079 "type": "string",
2080 "format": "date-time"
64042081 },
6405 "payload": {
6406 "type": "array",
6407 "items": {
6408 "type": "integer",
6409 "format": "int32",
6410 "minimum": 0
6411 }
6412 }
6413 }
6414 },
6415 "ReasonBody": {
6416 "type": "object",
6417 "properties": {
6418 "reason": {
2082 "displayname": {
64192083 "type": [
64202084 "string",
64212085 "null"
64222086 ]
6423 }
6424 }
6425 },
6426 "RedeemInviteBody": {
6427 "type": "object",
6428 "required": [
6429 "token"
6430 ],
6431 "properties": {
6432 "token": {
2087 },
2088 "email": {
64332089 "type": "string"
6434 }
6435 }
6436 },
6437 "RedeemInviteResponse": {
6438 "type": "object",
6439 "required": [
6440 "organization_id",
6441 "member_id",
6442 "joined"
6443 ],
6444 "properties": {
6445 "joined": {
2090 },
2091 "email_verified_at": {
2092 "type": [
2093 "string",
2094 "null"
2095 ],
2096 "format": "date-time"
2097 },
2098 "is_admin": {
64462099 "type": "boolean"
64472100 },
6448 "member_id": {
6449 "type": "string",
6450 "format": "uuid"
2101 "user_id": {
2102 "type": "integer",
2103 "format": "int64"
2104 },
2105 "username": {
2106 "type": "string"
64512107 },
6452 "organization_id": {
6453 "type": "string",
6454 "format": "uuid"
2108 "version": {
2109 "type": "integer",
2110 "format": "int32"
64552111 }
64562112 }
64572113 },
6458 "RegisterCodeRequest": {
2114 "NoData": {
2115 "default": null
2116 },
2117 "PasswordResetConfirmRequest": {
64592118 "type": "object",
64602119 "required": [
6461 "email",
2120 "token",
2121 "password_hash",
64622122 "pow_payload"
64632123 ],
64642124 "properties": {
6465 "email": {
2125 "password_hash": {
64662126 "type": "string"
64672127 },
64682128 "pow_payload": {
64692129 "$ref": "#/components/schemas/PowProofRequest"
2130 },
2131 "token": {
2132 "type": "string"
64702133 }
64712134 }
64722135 },
6473 "RegisterCodeResponse": {
2136 "PasswordResetConfirmResponse": {
64742137 "type": "object",
64752138 "required": [
6476 "expires_in_secs"
2139 "user_id"
64772140 ],
64782141 "properties": {
6479 "expires_in_secs": {
2142 "user_id": {
64802143 "type": "integer",
6481 "format": "int64",
6482 "minimum": 0
2144 "format": "int64"
64832145 }
64842146 }
64852147 },
6486 "RoleSummary": {
2148 "PasswordResetRequest": {
64872149 "type": "object",
64882150 "required": [
6489 "id",
6490 "organization_id",
6491 "key",
6492 "name",
6493 "rank",
6494 "system",
6495 "default_role",
6496 "self_assignable",
6497 "permissions",
6498 "member_count",
6499 "created_at",
6500 "updated_at",
6501 "version",
6502 "position"
2151 "email",
2152 "pow_payload"
65032153 ],
65042154 "properties": {
6505 "color": {
6506 "type": [
6507 "string",
6508 "null"
6509 ]
6510 },
6511 "created_at": {
6512 "type": "string",
6513 "format": "date-time"
6514 },
6515 "default_role": {
6516 "type": "boolean"
6517 },
6518 "description": {
6519 "type": [
6520 "string",
6521 "null"
6522 ]
6523 },
6524 "emoji": {
6525 "type": [
6526 "string",
6527 "null"
6528 ]
6529 },
6530 "icon_hash": {
6531 "type": [
6532 "string",
6533 "null"
6534 ]
6535 },
6536 "id": {
6537 "type": "string",
6538 "format": "uuid"
6539 },
6540 "key": {
6541 "type": "string"
6542 },
6543 "member_count": {
6544 "type": "integer",
6545 "format": "int64"
6546 },
6547 "name": {
2155 "email": {
65482156 "type": "string"
65492157 },
6550 "organization_id": {
6551 "type": "string",
6552 "format": "uuid"
2158 "pow_payload": {
2159 "$ref": "#/components/schemas/PowProofRequest"
2160 }
2161 }
2162 },
2163 "PasswordResetResponse": {
2164 "type": "object",
2165 "required": [
2166 "expires_in_secs"
2167 ],
2168 "properties": {
2169 "expires_in_secs": {
2170 "type": "integer",
2171 "format": "int64",
2172 "minimum": 0
2173 }
2174 }
2175 },
2176 "PowProofRequest": {
2177 "type": "object",
2178 "required": [
2179 "nonce",
2180 "payload"
2181 ],
2182 "properties": {
2183 "nonce": {
2184 "type": "integer",
2185 "format": "int64",
2186 "minimum": 0
65532187 },
6554 "permissions": {
2188 "payload": {
65552189 "type": "array",
65562190 "items": {
6557 "$ref": "#/components/schemas/PermissionGrant"
2191 "type": "integer",
2192 "format": "int32",
2193 "minimum": 0
65582194 }
6559 },
6560 "position": {
6561 "type": "integer",
6562 "format": "int64"
6563 },
6564 "rank": {
6565 "type": "integer",
6566 "format": "int32"
6567 },
6568 "self_assignable": {
6569 "type": "boolean"
6570 },
6571 "system": {
6572 "type": "boolean"
6573 },
6574 "updated_at": {
6575 "type": "string",
6576 "format": "date-time"
6577 },
6578 "version": {
6579 "type": "integer",
6580 "format": "int32"
65812195 }
65822196 }
65832197 },
6584 "SetMemberRoleBody": {
2198 "RegisterCodeRequest": {
65852199 "type": "object",
65862200 "required": [
6587 "role"
2201 "email",
2202 "pow_payload"
65882203 ],
65892204 "properties": {
6590 "expected_version": {
6591 "type": "integer",
6592 "format": "int32"
2205 "email": {
2206 "type": "string"
65932207 },
6594 "role": {
6595 "$ref": "#/components/schemas/OrgMemberRole"
2208 "pow_payload": {
2209 "$ref": "#/components/schemas/PowProofRequest"
65962210 }
65972211 }
65982212 },
6599 "SetRolePermissionsBody": {
2213 "RegisterCodeResponse": {
66002214 "type": "object",
2215 "required": [
2216 "expires_in_secs"
2217 ],
66012218 "properties": {
6602 "permissions": {
6603 "type": "array",
6604 "items": {
6605 "$ref": "#/components/schemas/PermissionGrant"
6606 }
2219 "expires_in_secs": {
2220 "type": "integer",
2221 "format": "int64",
2222 "minimum": 0
66072223 }
66082224 }
66092225 },
@@ -6700,250 +2316,6 @@
67002316 "null"
67012317 ],
67022318 "format": "date-time"
6703 }
6704 }
6705 },
6706 "TransferBody": {
6707 "type": "object",
6708 "required": [
6709 "target_user_id"
6710 ],
6711 "properties": {
6712 "target_user_id": {
6713 "type": "integer",
6714 "format": "int64"
6715 }
6716 }
6717 },
6718 "UpdateBanBody": {
6719 "type": "object",
6720 "description": "封禁更新:原因、到期时间与四个放行位。缺失表示不变,null 表示清空。",
6721 "properties": {
6722 "allow_agent_join": {
6723 "type": [
6724 "boolean",
6725 "null"
6726 ]
6727 },
6728 "allow_comment_any": {
6729 "type": [
6730 "boolean",
6731 "null"
6732 ]
6733 },
6734 "allow_push_repo": {
6735 "type": [
6736 "boolean",
6737 "null"
6738 ]
6739 },
6740 "allow_speak": {
6741 "type": [
6742 "boolean",
6743 "null"
6744 ]
6745 },
6746 "expires_at": {
6747 "type": [
6748 "string",
6749 "null"
6750 ],
6751 "format": "date-time"
6752 },
6753 "reason": {
6754 "type": [
6755 "string",
6756 "null"
6757 ]
6758 }
6759 }
6760 },
6761 "UpdateMemberBody": {
6762 "type": "object",
6763 "description": "会籍更新:管理端可改别名、通知偏好、免打扰与封禁附加信息。\n所有字段缺失表示不变,显式 null 表示清空(仅限 Option 字段)。",
6764 "properties": {
6765 "alias_avatar_url": {
6766 "type": [
6767 "string",
6768 "null"
6769 ]
6770 },
6771 "alias_name": {
6772 "type": [
6773 "string",
6774 "null"
6775 ]
6776 },
6777 "expected_version": {
6778 "type": "integer",
6779 "format": "int32"
6780 },
6781 "hide_muted_channels": {
6782 "type": [
6783 "boolean",
6784 "null"
6785 ]
6786 },
6787 "message_notifications": {
6788 "oneOf": [
6789 {
6790 "type": "null"
6791 },
6792 {
6793 "$ref": "#/components/schemas/MessageNotifications"
6794 }
6795 ]
6796 },
6797 "mobile_push": {
6798 "type": [
6799 "boolean",
6800 "null"
6801 ]
6802 },
6803 "mute_until": {
6804 "type": [
6805 "string",
6806 "null"
6807 ],
6808 "format": "date-time"
6809 },
6810 "muted": {
6811 "type": [
6812 "boolean",
6813 "null"
6814 ]
6815 },
6816 "suppress_everyone": {
6817 "type": [
6818 "boolean",
6819 "null"
6820 ]
6821 },
6822 "suppress_roles": {
6823 "type": [
6824 "boolean",
6825 "null"
6826 ]
6827 }
6828 }
6829 },
6830 "UpdateOrganizationBody": {
6831 "type": "object",
6832 "properties": {
6833 "allow_invite": {
6834 "type": [
6835 "boolean",
6836 "null"
6837 ]
6838 },
6839 "allow_join": {
6840 "type": [
6841 "boolean",
6842 "null"
6843 ]
6844 },
6845 "allow_join_level": {
6846 "type": [
6847 "integer",
6848 "null"
6849 ],
6850 "format": "int32"
6851 },
6852 "avatar_url": {
6853 "type": [
6854 "string",
6855 "null"
6856 ]
6857 },
6858 "description": {
6859 "type": [
6860 "string",
6861 "null"
6862 ]
6863 },
6864 "expected_version": {
6865 "type": "integer",
6866 "format": "int32"
6867 },
6868 "join_policy": {
6869 "oneOf": [
6870 {
6871 "type": "null"
6872 },
6873 {
6874 "$ref": "#/components/schemas/OrganizationJoinPolicy"
6875 }
6876 ]
6877 },
6878 "member_must_enable_2fa": {
6879 "type": [
6880 "boolean",
6881 "null"
6882 ]
6883 },
6884 "name": {
6885 "type": [
6886 "string",
6887 "null"
6888 ]
6889 },
6890 "require_email_verified": {
6891 "type": [
6892 "boolean",
6893 "null"
6894 ]
6895 },
6896 "require_mfa": {
6897 "type": [
6898 "boolean",
6899 "null"
6900 ]
6901 },
6902 "welcome_message": {
6903 "type": [
6904 "string",
6905 "null"
6906 ]
6907 }
6908 }
6909 },
6910 "UpdateRoleBody": {
6911 "type": "object",
6912 "properties": {
6913 "default_role": {
6914 "type": [
6915 "boolean",
6916 "null"
6917 ]
6918 },
6919 "description": {
6920 "type": [
6921 "string",
6922 "null"
6923 ]
6924 },
6925 "expected_version": {
6926 "type": "integer",
6927 "format": "int32"
6928 },
6929 "name": {
6930 "type": [
6931 "string",
6932 "null"
6933 ]
6934 },
6935 "rank": {
6936 "type": [
6937 "integer",
6938 "null"
6939 ],
6940 "format": "int32"
6941 },
6942 "self_assignable": {
6943 "type": [
6944 "boolean",
6945 "null"
6946 ]
69472319 }
69482320 }
69492321 },
Modified lib/chat/src/services/dms.rs View lib/chat/src/services/dms.rs
@@ -4,7 +4,7 @@
44use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
55use serde::{Deserialize, Serialize};
66use sha2::{Digest, Sha256};
7use space::services::spaces::UserDto;
7use space::services::dto::UserDto;
88use time::OffsetDateTime;
99use utoipa::ToSchema;
1010use uuid::Uuid;
Modified lib/chat/src/services/messages.rs View lib/chat/src/services/messages.rs
@@ -11,7 +11,7 @@
1111};
1212use serde::{Deserialize, Serialize};
1313use serde_json::json;
14use space::services::spaces::UserDto;
14use space::services::dto::UserDto;
1515use time::OffsetDateTime;
1616use utoipa::{IntoParams, ToSchema};
1717use uuid::Uuid;
Modified lib/chat/src/services/presence.rs View lib/chat/src/services/presence.rs
@@ -17,7 +17,7 @@
1717#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
1818pub struct PresenceDto {
1919 pub user_id: i64,
20 pub user: Option<space::services::spaces::UserDto>,
20 pub user: Option<space::services::dto::UserDto>,
2121 pub status: PresenceStatus,
2222 pub activities: serde_json::Value,
2323 pub last_seen_at: Option<OffsetDateTime>,
@@ -463,8 +463,8 @@
463463 let members =
464464 space::authority::list_members(&self.state, caller, space.id, false, false).await?;
465465 let mut uids: Vec<i64> = members.into_iter().map(|m| m.user_id).collect();
466 if !uids.contains(&space.owner_id) {
467 uids.push(space.owner_id);
466 if !uids.contains(&space.owner_user_id) {
467 uids.push(space.owner_user_id);
468468 }
469469 let mut presences = self.get_presences(&uids).await?;
470470 // 隐私:关掉"展示在线状态"的成员对他人一律表现为离线,本人照常。
Modified lib/chat/src/services/reactions.rs View lib/chat/src/services/reactions.rs
@@ -20,7 +20,7 @@
2020#[derive(Debug, Clone, Serialize, ToSchema)]
2121pub struct ReactionUserDto {
2222 pub emoji: String,
23 pub user: space::services::spaces::UserDto,
23 pub user: space::services::dto::UserDto,
2424 pub created_at: OffsetDateTime,
2525}
2626
Modified lib/chat/src/services/room_admin.rs View lib/chat/src/services/room_admin.rs
@@ -2,7 +2,7 @@
22use models::room::{room_bans, rooms};
33use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set};
44use serde::{Deserialize, Serialize};
5use space::services::spaces::UserDto;
5use space::services::dto::UserDto;
66use time::OffsetDateTime;
77use utoipa::ToSchema;
88use uuid::Uuid;
Modified lib/chat/src/services/rooms.rs View lib/chat/src/services/rooms.rs
@@ -12,7 +12,7 @@
1212 Set, TransactionTrait,
1313};
1414use serde::{Deserialize, Serialize};
15use space::services::spaces::UserDto;
15use space::services::dto::UserDto;
1616use space::share::authz::{MANAGE_INVITES, effective_permissions};
1717use time::OffsetDateTime;
1818use utoipa::{IntoParams, ToSchema};
@@ -84,7 +84,7 @@
8484
8585#[derive(Debug, Clone, Serialize, ToSchema)]
8686pub struct RoomMemberDto {
87 pub user: space::services::spaces::UserDto,
87 pub user: space::services::dto::UserDto,
8888 pub role: RoomMemberRole,
8989 pub nick: Option<String>,
9090 pub joined_at: OffsetDateTime,
@@ -793,7 +793,7 @@
793793 "room_id": room_id,
794794 "invite_id": id,
795795 "accept_code": accept_code,
796 "space_name": space.lower_name,
796 "space_name": space.slug,
797797 });
798798 let pending = export::notify::PendingNotifications::default();
799799 notify_user(
Modified lib/chat/src/services/service.rs View lib/chat/src/services/service.rs
@@ -4,7 +4,7 @@
44use config::AppConfig;
55use export::AppState;
66use export::database::Database;
7use space::services::spaces::UserDto;
7use space::services::dto::UserDto;
88use storage::BlobStore;
99
1010use crate::errors::ChatError;
Modified lib/chat/src/services/threads.rs View lib/chat/src/services/threads.rs
@@ -8,7 +8,7 @@
88 QueryFilter, QueryOrder, QuerySelect, Set, TransactionTrait,
99};
1010use serde::{Deserialize, Serialize};
11use space::services::spaces::UserDto;
11use space::services::dto::UserDto;
1212use time::OffsetDateTime;
1313use utoipa::{IntoParams, ToSchema};
1414use uuid::Uuid;
Modified lib/chat/src/share/authz.rs View lib/chat/src/share/authz.rs
@@ -9,7 +9,7 @@
99 space: &spaces::Model,
1010 uid: i64,
1111) -> Result<bool, ChatError> {
12 if space.owner_id == uid {
12 if space.owner_user_id == uid {
1313 return Ok(true);
1414 }
1515 let perms = effective_permissions(&service.state, space, uid).await?;
Modified lib/chat/src/share/lookup.rs View lib/chat/src/share/lookup.rs
@@ -28,7 +28,7 @@
2828 space::authority::space_by_id(&service.state, 0, space_id)
2929 .await
3030 .ok()
31 .map(|space| space.lower_name)
31 .map(|space| space.slug)
3232}
3333
3434pub async fn lookup_room(service: &ChatService, room_id: Uuid) -> Result<rooms::Model, ChatError> {
Modified lib/chat/src/share/mention.rs View lib/chat/src/share/mention.rs
@@ -1,6 +1,5 @@
11use models::room::room_members;
2use models::space::space_groups;
3use models::space::space_members::MemberStatus;
2use models::space::{MemberStatus, space_groups};
43use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
54use uuid::Uuid;
65
@@ -78,17 +77,17 @@
7877 let candidate = export::users::find_by_username(&service.state, lower).await?;
7978 if let Some(space) = &ctx.space {
8079 if let Some(candidate) = &candidate {
81 if space.owner_id == candidate.user_id {
80 if space.owner_user_id == candidate.user_id {
8281 return Ok(Resolved::User(candidate.user_id));
8382 }
8483 let member = space::authority::member(
8584 &service.state,
86 space.owner_id,
85 space.owner_user_id,
8786 space.id,
8887 candidate.user_id,
8988 )
9089 .await?;
91 if member.is_some_and(|m| m.status == MemberStatus::Active) {
90 if member.is_some_and(|m| m.status() == MemberStatus::Active) {
9291 return Ok(Resolved::User(candidate.user_id));
9392 }
9493 }
Modified lib/chat/src/share/notify.rs View lib/chat/src/share/notify.rs
@@ -1,7 +1,7 @@
11use models::chat::room_seq;
22use models::chat::room_user_settings::{self, RoomNotificationLevel};
33use models::room::room_members;
4use models::space::space_members::MessageNotifications;
4use models::space::MessageNotifications;
55use models::users::user_notifications::{self, NotificationType};
66use models::users::user_recent_mentions;
77use sea_orm::{
@@ -156,7 +156,11 @@
156156 if let Some(space_id) = space_id
157157 && let Some(member) = space::authority::member(&service.state, uid, space_id, uid).await?
158158 {
159 if mute_active(member.muted, member.mute_until) && !(mentioned && !everyone) {
159 if mute_active(
160 member.muted,
161 space::authority::to_time_opt(member.mute_until),
162 ) && !(mentioned && !everyone)
163 {
160164 return Ok(NotifyDecision::Skip);
161165 }
162166 return Ok(match member.message_notifications {
@@ -292,7 +296,10 @@
292296 if let Some(space_id) = space_id
293297 && let Some(member) = space::authority::member(&service.state, uid, space_id, uid).await?
294298 {
295 if mute_active(member.muted, member.mute_until) {
299 if mute_active(
300 member.muted,
301 space::authority::to_time_opt(member.mute_until),
302 ) {
296303 return Ok(false);
297304 }
298305 return Ok(!matches!(
Modified lib/chat/src/openapi.rs View lib/chat/src/openapi.rs
@@ -120,7 +120,7 @@
120120 export::pagination::Page<crate::services::rooms::RoomInviteListItemDto>,
121121 AppResponseData<export::rest::NoData>,
122122 export::rest::NoData,
123 space::services::spaces::UserDto,
123 space::services::dto::UserDto,
124124 models::chat::messages::AuthorType,
125125 models::chat::messages::ChatMessageType,
126126 models::chat::attachments::AttachmentStatus,
Modified lib/deletion/src/lib.rs View lib/deletion/src/lib.rs
@@ -5,9 +5,9 @@
55use models::repo::repos;
66use models::room::rooms;
77use models::space::{
8 space_bans, space_extras, space_group_permissions, space_groups, space_invite,
9 space_invite_groups, space_join_request, space_join_rule, space_member_extras,
10 space_member_groups, space_warn,
8 SpaceStatus, space_extras, space_group_permissions, space_groups, space_invites,
9 space_join_requests, space_join_rules, space_member_extras, space_member_groups, space_members,
10 space_warn, spaces,
1111};
1212use models::system::{account_deletion, space_deletion};
1313use models::users::{
@@ -16,7 +16,9 @@
1616 user_recent_mentions, user_saved_messages,
1717};
1818use proto::KenShellClient;
19use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QuerySelect, Set};
19use sea_orm::{
20 ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QuerySelect, Set,
21};
2022use serde::Serialize;
2123use time::OffsetDateTime;
2224use uuid::Uuid;
@@ -125,26 +127,15 @@
125127 return Ok(status_of_account(&existing));
126128 }
127129
128 let client = client(state)?;
129 let organizations = client
130 .list_organizations(
131 user_id,
132 user_id,
133 true,
134 proto::auth::common::v1::PageRequest {
135 page_size: 100,
136 page_token: String::new(),
137 },
138 )
139 .await
140 .map_err(ken_auth)?
141 .organizations;
142 for organization in organizations
143 .iter()
144 .filter(|organization| organization.owner_user_id == user_id)
145 {
146 let space_id = Uuid::parse_str(&organization.id)
147 .map_err(|_| DeletionError::Config("organization id is not a uuid".into()))?;
130 let owned: Vec<Uuid> = spaces::Entity::find()
131 .filter(spaces::Column::OwnerUserId.eq(user_id))
132 .filter(spaces::Column::DeletedAt.is_null())
133 .select_only()
134 .column(spaces::Column::Id)
135 .into_tuple()
136 .all(db)
137 .await?;
138 for space_id in owned {
148139 let pending = space_deletion::Entity::find()
149140 .filter(space_deletion::Column::SpaceId.eq(space_id))
150141 .filter(space_deletion::Column::Status.eq("pending"))
@@ -156,6 +147,7 @@
156147 ));
157148 }
158149 }
150 let client = client(state)?;
159151 let status = client.get_user_status(user_id).await.map_err(ken_auth)?;
160152 if status.deletion_requested_at.is_none() && status.deleted_at.is_none() {
161153 client
@@ -233,18 +225,18 @@
233225 reason: &str,
234226) -> Result<SpaceDeletionStatus, DeletionError> {
235227 let db = state.database.write();
236 let client = client(state)?;
237 let organization = client
238 .get_organization(actor, &space_id.to_string(), "")
239 .await
240 .map_err(ken_auth)?
241 .organization
242 .ok_or_else(|| DeletionError::NotFound("organization not found".into()))?;
243 if organization.status != proto::auth::v1::OrganizationStatus::Archived as i32 {
244 client
245 .archive_organization(actor, &space_id.to_string(), reason)
246 .await
247 .map_err(ken_auth)?;
228 let space = spaces::Entity::find_by_id(space_id)
229 .one(db)
230 .await?
231 .ok_or_else(|| DeletionError::NotFound("space not found".into()))?;
232 if space.status != SpaceStatus::Archived {
233 let mut active: spaces::ActiveModel = space.into();
234 active.status = Set(SpaceStatus::Archived);
235 active.archived_at = Set(Some(sea_orm::prelude::DateTimeUtc::from(
236 std::time::SystemTime::now(),
237 )));
238 active.suspended_reason = Set(Some(reason.to_string()));
239 active.update(db).await?;
248240 }
249241
250242 let now = now();
@@ -289,16 +281,19 @@
289281
290282pub async fn cancel_space_deletion(
291283 state: &AppState,
292 actor: i64,
284 _actor: i64,
293285 space_id: Uuid,
294286) -> Result<(), DeletionError> {
295 client(state)?
296 .restore_organization(actor, &space_id.to_string())
297 .await
298 .map_err(ken_auth)?;
287 let db = state.database.write();
288 if let Some(space) = spaces::Entity::find_by_id(space_id).one(db).await? {
289 let mut active: spaces::ActiveModel = space.into();
290 active.status = Set(SpaceStatus::Active);
291 active.archived_at = Set(None);
292 active.update(db).await?;
293 }
299294 space_deletion::Entity::delete_many()
300295 .filter(space_deletion::Column::SpaceId.eq(space_id))
301 .exec(state.database.write())
296 .exec(db)
302297 .await?;
303298 Ok(())
304299}
@@ -373,38 +368,27 @@
373368}
374369
375370async fn purge_space(state: &AppState, row: &space_deletion::Model) -> Result<(), DeletionError> {
376 let client = client(state)?;
377 // Never delete an organization whose ownership changed after the request.
378 let organization = match client
379 .get_organization(0, &row.space_id.to_string(), "")
380 .await
381 {
382 Ok(response) => match response.organization {
383 Some(organization) => organization,
384 None => {
385 space_deletion::Entity::delete_by_id(row.id)
386 .exec(state.database.write())
387 .await?;
388 return Ok(());
389 }
390 },
391 Err(KenAuthError::NotFound) => {
392 space_deletion::Entity::delete_by_id(row.id)
393 .exec(state.database.write())
394 .await?;
395 return Ok(());
396 }
397 Err(error) => return Err(ken_auth(error)),
371 // Never delete a space whose ownership changed after the request.
372 let space = {
373 let db = state.database.write();
374 spaces::Entity::find_by_id(row.space_id).one(db).await?
398375 };
399 if organization.owner_user_id != row.requested_by {
400 client
401 .restore_organization(organization.owner_user_id, &row.space_id.to_string())
402 .await
403 .map_err(ken_auth)?;
376 let Some(space) = space else {
404377 space_deletion::Entity::delete_by_id(row.id)
405378 .exec(state.database.write())
406379 .await?;
407380 return Ok(());
381 };
382 if space.owner_user_id != row.requested_by {
383 let db = state.database.write();
384 let mut active: spaces::ActiveModel = space.into();
385 active.status = Set(SpaceStatus::Active);
386 active.archived_at = Set(None);
387 active.update(db).await?;
388 space_deletion::Entity::delete_by_id(row.id)
389 .exec(db)
390 .await?;
391 return Ok(());
408392 }
409393
410394 // 1. KenShell is the owner of repository/placement/LFS bytes.
@@ -421,22 +405,15 @@
421405 }
422406 }
423407
424 // 2. Local KenApi resources become unreachable and org-scoped metadata is removed.
408 // 2. Local KenApi resources become unreachable and space-scoped metadata is removed.
425409 archive_local_space(state, row.space_id).await?;
426410
427 // 3. The authority plane removes the organization for good.
428 client
429 .delete_organization(
430 row.requested_by,
431 &row.space_id.to_string(),
432 true,
433 "scheduled space deletion",
434 )
435 .await
436 .map_err(ken_auth)?;
411 // 3. The space row and its members/roles/invites cascade away.
412 let db = state.database.write();
413 spaces::Entity::delete_by_id(row.space_id).exec(db).await?;
437414
438415 space_deletion::Entity::delete_by_id(row.id)
439 .exec(state.database.write())
416 .exec(db)
440417 .await?;
441418 tracing::info!(space_id = %row.space_id, "space purged");
442419 Ok(())
@@ -527,20 +504,6 @@
527504 .exec(db)
528505 .await?;
529506 }
530 let invite_ids: Vec<Uuid> = space_invite::Entity::find()
531 .filter(space_invite::Column::SpaceId.eq(space_id))
532 .select_only()
533 .column(space_invite::Column::Id)
534 .into_tuple()
535 .all(db)
536 .await?;
537 if !invite_ids.is_empty() {
538 space_invite_groups::Entity::delete_many()
539 .filter(space_invite_groups::Column::InviteId.is_in(invite_ids))
540 .exec(db)
541 .await?;
542 }
543
544507 space_member_groups::Entity::delete_many()
545508 .filter(space_member_groups::Column::SpaceId.eq(space_id))
546509 .exec(db)
@@ -549,24 +512,20 @@
549512 .filter(space_groups::Column::SpaceId.eq(space_id))
550513 .exec(db)
551514 .await?;
552 space_invite::Entity::delete_many()
553 .filter(space_invite::Column::SpaceId.eq(space_id))
515 space_invites::Entity::delete_many()
516 .filter(space_invites::Column::OrganizationId.eq(space_id))
554517 .exec(db)
555518 .await?;
556 space_join_request::Entity::delete_many()
557 .filter(space_join_request::Column::SpaceId.eq(space_id))
519 space_join_requests::Entity::delete_many()
520 .filter(space_join_requests::Column::OrganizationId.eq(space_id))
558521 .exec(db)
559522 .await?;
560 space_join_rule::Entity::delete_many()
561 .filter(space_join_rule::Column::SpaceId.eq(space_id))
523 space_join_rules::Entity::delete_many()
524 .filter(space_join_rules::Column::OrganizationId.eq(space_id))
562525 .exec(db)
563526 .await?;
564527 space_warn::Entity::delete_many()
565528 .filter(space_warn::Column::SpaceId.eq(space_id))
566 .exec(db)
567 .await?;
568 space_bans::Entity::delete_many()
569 .filter(space_bans::Column::SpaceId.eq(space_id))
570529 .exec(db)
571530 .await?;
572531 space_extras::Entity::delete_by_id(space_id)
@@ -583,32 +542,24 @@
583542 state: &AppState,
584543 row: &account_deletion::Model,
585544) -> Result<(), DeletionError> {
586 let client = client(state)?;
587 let organizations = client
588 .list_organizations(
589 row.user_id,
590 row.user_id,
591 true,
592 proto::auth::common::v1::PageRequest {
593 page_size: 100,
594 page_token: String::new(),
595 },
596 )
597 .await
598 .map_err(ken_auth)?
599 .organizations;
600 if organizations
601 .iter()
602 .any(|organization| organization.owner_user_id == row.user_id)
603 {
545 let db = state.database.write();
546 let owned = spaces::Entity::find()
547 .filter(spaces::Column::OwnerUserId.eq(row.user_id))
548 .filter(spaces::Column::DeletedAt.is_null())
549 .count(db)
550 .await?;
551 if owned > 0 {
604552 return Err(DeletionError::Conflict(
605553 "owned spaces are still being purged".into(),
606554 ));
607555 }
608556
609 let db = state.database.write();
610557 let now = now();
611558
559 space_members::Entity::delete_many()
560 .filter(space_members::Column::UserId.eq(row.user_id))
561 .exec(db)
562 .await?;
612563 user_preferences::Entity::delete_many()
613564 .filter(user_preferences::Column::UserId.eq(row.user_id))
614565 .exec(db)
@@ -673,6 +624,7 @@
673624 active.update(db).await?;
674625 }
675626
627 let client = client(state)?;
676628 match client.purge_account(row.user_id).await {
677629 Ok(()) | Err(KenAuthError::NotFound) => {}
678630 Err(error) => return Err(ken_auth(error)),
Modified lib/export/src/auth_api.rs View lib/export/src/auth_api.rs
@@ -177,288 +177,4 @@
177177 displayname: &str,
178178 system: bool,
179179 ) -> Result<proto::common::v1::UserPublic, KenAuthError>;
180 async fn ensure_bot_member(
181 &self,
182 organization_id: &str,
183 user_id: i64,
184 role_keys: Vec<String>,
185 role_ids: Vec<String>,
186 ) -> Result<proto::v1::OrganizationMemberInfo, KenAuthError>;
187 async fn get_organization(
188 &self,
189 actor_user_id: i64,
190 organization_id: &str,
191 slug: &str,
192 ) -> Result<proto::v1::GetOrganizationResponse, KenAuthError>;
193 async fn get_member(
194 &self,
195 actor_user_id: i64,
196 organization_id: &str,
197 user_id: i64,
198 ) -> Result<Option<proto::v1::OrganizationMemberInfo>, KenAuthError>;
199 async fn list_members(
200 &self,
201 actor_user_id: i64,
202 organization_id: &str,
203 include_inactive: bool,
204 include_banned: bool,
205 page: proto::common::v1::PageRequest,
206 ) -> Result<proto::v1::ListMembersResponse, KenAuthError>;
207 async fn list_organizations(
208 &self,
209 actor_user_id: i64,
210 user_id: i64,
211 include_archived: bool,
212 page: proto::common::v1::PageRequest,
213 ) -> Result<proto::v1::ListOrganizationsResponse, KenAuthError>;
214 async fn list_public_organizations(
215 &self,
216 query: &str,
217 page: proto::common::v1::PageRequest,
218 ) -> Result<proto::v1::ListPublicOrganizationsResponse, KenAuthError>;
219 async fn check_permissions(
220 &self,
221 actor_user_id: i64,
222 organization_id: &str,
223 user_id: i64,
224 permissions: Vec<String>,
225 ) -> Result<proto::v1::CheckPermissionsResponse, KenAuthError>;
226 async fn create_organization(
227 &self,
228 actor_user_id: i64,
229 slug: &str,
230 name: &str,
231 description: &str,
232 avatar_url: &str,
233 join_policy: Option<i32>,
234 allow_join: Option<bool>,
235 require_email_verified: Option<bool>,
236 require_mfa: Option<bool>,
237 welcome_message: Option<String>,
238 member_must_enable_2fa: Option<bool>,
239 allow_invite: Option<bool>,
240 allow_join_level: Option<i32>,
241 ) -> Result<proto::v1::CreateOrganizationResponse, KenAuthError>;
242 async fn update_organization(
243 &self,
244 actor_user_id: i64,
245 organization_id: &str,
246 name: Option<String>,
247 description: Option<String>,
248 avatar_url: Option<String>,
249 expected_version: i32,
250 join_policy: Option<i32>,
251 allow_join: Option<bool>,
252 require_email_verified: Option<bool>,
253 require_mfa: Option<bool>,
254 welcome_message: Option<String>,
255 member_must_enable_2fa: Option<bool>,
256 allow_invite: Option<bool>,
257 allow_join_level: Option<i32>,
258 ) -> Result<proto::v1::OrganizationInfo, KenAuthError>;
259 async fn archive_organization(
260 &self,
261 actor_user_id: i64,
262 organization_id: &str,
263 reason: &str,
264 ) -> Result<(), KenAuthError>;
265 async fn restore_organization(
266 &self,
267 actor_user_id: i64,
268 organization_id: &str,
269 ) -> Result<(), KenAuthError>;
270 async fn delete_organization(
271 &self,
272 actor_user_id: i64,
273 organization_id: &str,
274 force: bool,
275 reason: &str,
276 ) -> Result<(), KenAuthError>;
277 async fn add_member(
278 &self,
279 actor_user_id: i64,
280 organization_id: &str,
281 user_id: i64,
282 role: i32,
283 role_keys: Vec<String>,
284 ) -> Result<(proto::v1::OrganizationMemberInfo, bool), KenAuthError>;
285 async fn remove_member(
286 &self,
287 actor_user_id: i64,
288 organization_id: &str,
289 user_id: i64,
290 reason: &str,
291 ) -> Result<bool, KenAuthError>;
292 async fn leave_organization(
293 &self,
294 actor_user_id: i64,
295 organization_id: &str,
296 ) -> Result<bool, KenAuthError>;
297 async fn update_member(
298 &self,
299 actor_user_id: i64,
300 organization_id: &str,
301 user_id: i64,
302 alias_name: Option<Option<String>>,
303 alias_avatar_url: Option<Option<String>>,
304 message_notifications: Option<String>,
305 muted: Option<bool>,
306 mute_until: Option<Option<prost_types::Timestamp>>,
307 mobile_push: Option<bool>,
308 suppress_everyone: Option<bool>,
309 suppress_roles: Option<bool>,
310 hide_muted_channels: Option<bool>,
311 ) -> Result<proto::v1::OrganizationMemberInfo, KenAuthError>;
312 async fn ban_member(
313 &self,
314 actor_user_id: i64,
315 organization_id: &str,
316 user_id: i64,
317 reason: &str,
318 ) -> Result<proto::v1::OrganizationMemberInfo, KenAuthError>;
319 async fn unban_member(
320 &self,
321 actor_user_id: i64,
322 organization_id: &str,
323 user_id: i64,
324 ) -> Result<proto::v1::OrganizationMemberInfo, KenAuthError>;
325 async fn update_ban(
326 &self,
327 actor_user_id: i64,
328 organization_id: &str,
329 user_id: i64,
330 reason: Option<Option<String>>,
331 expires_at: Option<Option<prost_types::Timestamp>>,
332 allow_speak: Option<bool>,
333 allow_push_repo: Option<bool>,
334 allow_comment_any: Option<bool>,
335 ) -> Result<proto::v1::OrganizationMemberInfo, KenAuthError>;
336 async fn create_role(
337 &self,
338 actor_user_id: i64,
339 organization_id: &str,
340 key: &str,
341 name: &str,
342 description: &str,
343 rank: i32,
344 self_assignable: bool,
345 permissions: Vec<String>,
346 ) -> Result<proto::v1::OrganizationRoleInfo, KenAuthError>;
347 async fn update_role(
348 &self,
349 actor_user_id: i64,
350 organization_id: &str,
351 role_id: &str,
352 name: Option<String>,
353 description: Option<String>,
354 rank: Option<i32>,
355 self_assignable: Option<bool>,
356 expected_version: i32,
357 ) -> Result<proto::v1::OrganizationRoleInfo, KenAuthError>;
358 async fn set_role_permissions(
359 &self,
360 actor_user_id: i64,
361 organization_id: &str,
362 role_id: &str,
363 permissions: Vec<String>,
364 ) -> Result<proto::v1::OrganizationRoleInfo, KenAuthError>;
365 async fn delete_role(
366 &self,
367 actor_user_id: i64,
368 organization_id: &str,
369 role_id: &str,
370 ) -> Result<bool, KenAuthError>;
371 async fn assign_role(
372 &self,
373 actor_user_id: i64,
374 organization_id: &str,
375 user_id: i64,
376 role_id: &str,
377 ) -> Result<proto::v1::OrganizationMemberInfo, KenAuthError>;
378 async fn unassign_role(
379 &self,
380 actor_user_id: i64,
381 organization_id: &str,
382 user_id: i64,
383 role_id: &str,
384 ) -> Result<proto::v1::OrganizationMemberInfo, KenAuthError>;
385 async fn create_invite(
386 &self,
387 actor_user_id: i64,
388 organization_id: &str,
389 method: i32,
390 max_uses: i32,
391 expires_at: Option<prost_types::Timestamp>,
392 role_ids: Vec<String>,
393 target_user_id: i64,
394 email: &str,
395 ) -> Result<(proto::v1::OrganizationInviteInfo, String), KenAuthError>;
396 async fn revoke_invite(
397 &self,
398 actor_user_id: i64,
399 organization_id: &str,
400 invite_id: &str,
401 ) -> Result<bool, KenAuthError>;
402 async fn redeem_invite(
403 &self,
404 actor_user_id: i64,
405 token: &str,
406 ) -> Result<proto::v1::RedeemInviteResponse, KenAuthError>;
407 async fn create_join_rule(
408 &self,
409 actor_user_id: i64,
410 organization_id: &str,
411 role_id: &str,
412 trigger: &str,
413 matcher: &str,
414 enabled: bool,
415 ) -> Result<proto::v1::JoinRuleInfo, KenAuthError>;
416 async fn update_join_rule(
417 &self,
418 actor_user_id: i64,
419 organization_id: &str,
420 rule_id: &str,
421 role_id: Option<String>,
422 trigger: Option<String>,
423 matcher: Option<String>,
424 enabled: Option<bool>,
425 ) -> Result<proto::v1::JoinRuleInfo, KenAuthError>;
426 async fn delete_join_rule(
427 &self,
428 actor_user_id: i64,
429 organization_id: &str,
430 rule_id: &str,
431 ) -> Result<bool, KenAuthError>;
432 async fn create_join_request(
433 &self,
434 actor_user_id: i64,
435 organization_id: &str,
436 note: &str,
437 ) -> Result<proto::v1::JoinRequestInfo, KenAuthError>;
438 async fn withdraw_join_request(
439 &self,
440 actor_user_id: i64,
441 organization_id: &str,
442 request_id: &str,
443 ) -> Result<proto::v1::JoinRequestInfo, KenAuthError>;
444 async fn approve_join_request(
445 &self,
446 actor_user_id: i64,
447 organization_id: &str,
448 request_id: &str,
449 ) -> Result<
450 (
451 proto::v1::JoinRequestInfo,
452 Option<proto::v1::OrganizationMemberInfo>,
453 ),
454 KenAuthError,
455 >;
456
457 async fn reject_join_request(
458 &self,
459 actor_user_id: i64,
460 organization_id: &str,
461 request_id: &str,
462 note: &str,
463 ) -> Result<proto::v1::JoinRequestInfo, KenAuthError>;
464180}
Modified lib/git/src/service/code_comments.rs View lib/git/src/service/code_comments.rs
@@ -7,7 +7,7 @@
77 QuerySelect,
88};
99use serde::{Deserialize, Serialize};
10use space::services::spaces::UserDto;
10use space::services::dto::UserDto;
1111use time::OffsetDateTime;
1212use utoipa::ToSchema;
1313use uuid::Uuid;
Modified lib/git/src/service/commits.rs View lib/git/src/service/commits.rs
@@ -127,7 +127,7 @@
127127 pub description: Option<String>,
128128 pub target_url: Option<String>,
129129 pub creator_id: i64,
130 pub creator: Option<space::services::spaces::UserDto>,
130 pub creator: Option<space::services::dto::UserDto>,
131131 pub created_at: OffsetDateTime,
132132 pub updated_at: Option<OffsetDateTime>,
133133}
Modified lib/git/src/service/context.rs View lib/git/src/service/context.rs
@@ -19,7 +19,7 @@
1919 return Err(GitError::NotFound);
2020 }
2121 let space = space::authority::space_by_slug(&self.state, 0, &space_lower).await?;
22 if !space.is_active {
22 if !space::authority::space_is_active(&space) {
2323 return Err(GitError::NotFound);
2424 }
2525
@@ -52,7 +52,7 @@
5252 model: repo,
5353 cache,
5454 database: self.state.database.clone(),
55 space_name: space.lower_name,
55 space_name: space.slug,
5656 repo_root: PathBuf::from(&git.repo_root),
5757 ks: self.ks().clone(),
5858 })
Modified lib/git/src/service/releases.rs View lib/git/src/service/releases.rs
@@ -6,7 +6,7 @@
66 QuerySelect,
77};
88use serde::{Deserialize, Serialize};
9use space::services::spaces::UserDto;
9use space::services::dto::UserDto;
1010use time::OffsetDateTime;
1111use utoipa::{IntoParams, ToSchema};
1212use uuid::Uuid;
Modified lib/git/src/service/repos.rs View lib/git/src/service/repos.rs
@@ -295,7 +295,7 @@
295295 return Err(GitError::NotFound);
296296 }
297297 let space = space::authority::space_by_slug(&self.state, 0, &lower).await?;
298 if !space.is_active {
298 if !space::authority::space_is_active(&space) {
299299 return Err(GitError::NotFound);
300300 }
301301 Ok(space)
@@ -308,7 +308,7 @@
308308 cmd: CreateRepoCmd,
309309 ) -> Result<RepoDto, GitError> {
310310 let space = self.lookup_space(&space_name).await?;
311 if space.owner_id != caller {
311 if space.owner_user_id != caller {
312312 return Err(GitError::Forbidden);
313313 }
314314 let name = validate_repo_name(&cmd.name)?;
@@ -420,7 +420,7 @@
420420 model: repo.clone(),
421421 cache,
422422 database: self.state.database.clone(),
423 space_name: space.lower_name.clone(),
423 space_name: space.slug.clone(),
424424 repo_root: std::path::PathBuf::from(&git_cfg.repo_root),
425425 ks: self.ks().clone(),
426426 };
@@ -514,7 +514,7 @@
514514 };
515515 let rows = sel.all(self.db_read()).await?;
516516
517 let owner_view = caller.is_some_and(|uid| space.owner_id == uid);
517 let owner_view = caller.is_some_and(|uid| space.owner_user_id == uid);
518518 let mut accessible: std::collections::HashSet<Uuid> = std::collections::HashSet::new();
519519 if let Some(uid) = caller
520520 && !owner_view
@@ -554,7 +554,7 @@
554554 cmd: UpdateRepoCmd,
555555 ) -> Result<RepoDto, GitError> {
556556 let space = self.repo_space(&ctx).await?;
557 if space.owner_id != caller {
557 if space.owner_user_id != caller {
558558 return Err(GitError::Forbidden);
559559 }
560560 let new_branch = match &cmd.default_branch {
@@ -619,7 +619,7 @@
619619 archived: bool,
620620 ) -> Result<RepoDto, GitError> {
621621 let space = self.repo_space(&ctx).await?;
622 if space.owner_id != caller {
622 if space.owner_user_id != caller {
623623 return Err(GitError::Forbidden);
624624 }
625625 if ctx.model.is_archived == archived {
Modified lib/git/src/service/whitelists.rs View lib/git/src/service/whitelists.rs
@@ -17,7 +17,7 @@
1717 pub policy_id: Uuid,
1818 pub kind: WhitelistKind,
1919 pub user_id: Option<i64>,
20 pub user: Option<space::services::spaces::UserDto>,
20 pub user: Option<space::services::dto::UserDto>,
2121 pub group_id: Option<Uuid>,
2222 pub created_at: OffsetDateTime,
2323}
@@ -47,7 +47,7 @@
4747pub struct TagWhitelistDto {
4848 pub policy_id: Uuid,
4949 pub user_id: i64,
50 pub user: Option<space::services::spaces::UserDto>,
50 pub user: Option<space::services::dto::UserDto>,
5151}
5252
5353impl From<repo_tag_protection_whitelists::Model> for TagWhitelistDto {
@@ -83,14 +83,14 @@
8383 pub(crate) async fn user_dto_map(
8484 &self,
8585 uids: &[i64],
86 ) -> Result<std::collections::HashMap<i64, space::services::spaces::UserDto>, GitError> {
86 ) -> Result<std::collections::HashMap<i64, space::services::dto::UserDto>, GitError> {
8787 let mut map = std::collections::HashMap::new();
8888 if uids.is_empty() {
8989 return Ok(map);
9090 }
9191 let rows = export::users::load_public_users(&self.state, uids).await?;
9292 for (uid, user) in rows {
93 map.insert(uid, space::services::spaces::UserDto::from_public(&user));
93 map.insert(uid, space::services::dto::UserDto::from_public(&user));
9494 }
9595 Ok(map)
9696 }
Modified lib/git/src/share/access.rs View lib/git/src/share/access.rs
@@ -143,7 +143,7 @@
143143 // Walk dir_id up to the owning space row, then load the space to
144144 // compare owner_id. Mirrors `ssh/handler/command.rs::space_id_for`.
145145 let space = find_space_for_repo(&svc.state, repo).await?;
146 if space.owner_id == uid {
146 if space.owner_user_id == uid {
147147 return Ok(());
148148 }
149149
Modified lib/issues/src/handlers/boards/create_board.rs View lib/issues/src/handlers/boards/create_board.rs
@@ -6,14 +6,14 @@
66use crate::services::boards::{BoardDto, CreateBoardCmd};
77use crate::services::service::IssueService;
88use ::session::share::session;
9use space::services::spaces::SpaceNamePath;
9use space::services::dto::SpacePath;
1010
1111#[utoipa::path(
1212 post,
1313 path = "/issues/{space_name}/boards",
1414 tag = "issues",
1515 description = "在空间内创建议题看板,并同步创建其目录树节点。",
16 params(SpaceNamePath),
16 params(SpacePath),
1717 request_body = CreateBoardCmd,
1818 responses(
1919 (status = 200, description = "create board", body = AppResponseData<BoardDto>),
@@ -27,7 +27,7 @@
2727pub async fn create_board(
2828 State(service): State<IssueService>,
2929 session: Session,
30 Path(path): Path<SpaceNamePath>,
30 Path(path): Path<SpacePath>,
3131 Json(cmd): Json<CreateBoardCmd>,
3232) -> Result<AppResponseData<BoardDto>, ApiError> {
3333 let uid = session::require_uid(&session)?;
Modified lib/issues/src/handlers/boards/list_boards.rs View lib/issues/src/handlers/boards/list_boards.rs
@@ -6,14 +6,14 @@
66use crate::services::boards::{BoardPage, ListBoardQuery};
77use crate::services::service::IssueService;
88use ::session::share::session;
9use space::services::spaces::SpaceNamePath;
9use space::services::dto::SpacePath;
1010
1111#[utoipa::path(
1212 get,
1313 path = "/issues/{space_name}/boards",
1414 tag = "issues",
1515 description = "分页列出调用者可见的看板,并统计开放和已关闭议题数量。",
16 params(SpaceNamePath, ListBoardQuery),
16 params(SpacePath, ListBoardQuery),
1717 responses(
1818 (status = 200, description = "list boards", body = AppResponseData<BoardPage>),
1919 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
@@ -24,7 +24,7 @@
2424pub async fn list_boards(
2525 State(service): State<IssueService>,
2626 session: Session,
27 Path(path): Path<SpaceNamePath>,
27 Path(path): Path<SpacePath>,
2828 Query(q): Query<ListBoardQuery>,
2929) -> Result<AppResponseData<BoardPage>, ApiError> {
3030 let uid = session::require_uid(&session)?;
Modified lib/issues/src/services/comments.rs View lib/issues/src/services/comments.rs
@@ -8,7 +8,7 @@
88 QuerySelect, Set, TransactionTrait,
99};
1010use serde::{Deserialize, Serialize};
11use space::services::spaces::UserDto;
11use space::services::dto::UserDto;
1212use time::OffsetDateTime;
1313use utoipa::{IntoParams, ToSchema};
1414use uuid::Uuid;
@@ -124,7 +124,7 @@
124124 let content_str = content.to_string();
125125 let parent_opt = cmd.parent_id;
126126 let this = self.clone();
127 let space_lower_for_txn = space.lower_name.clone();
127 let space_lower_for_txn = space.slug.clone();
128128 let item_title_for_txn = item.title.clone();
129129 let mentioned_for_txn = mentioned_uids.clone();
130130 let pending = export::notify::PendingNotifications::default();
@@ -261,7 +261,7 @@
261261 let link = format!(
262262 "{}/issues/{}/{}/{}",
263263 self.config().web.web_base_url,
264 space.lower_name,
264 space.slug,
265265 board.id,
266266 item.id
267267 );
Modified lib/issues/src/services/content_history.rs View lib/issues/src/services/content_history.rs
@@ -1,7 +1,7 @@
11use models::issue::issue_content_history;
22use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect};
33use serde::{Deserialize, Serialize};
4use space::services::spaces::UserDto;
4use space::services::dto::UserDto;
55use time::OffsetDateTime;
66use utoipa::{IntoParams, ToSchema};
77use uuid::Uuid;
Modified lib/issues/src/services/events.rs View lib/issues/src/services/events.rs
@@ -1,7 +1,7 @@
11use models::issue::issue_event_line::{self, IssueEventType};
22use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect};
33use serde::{Deserialize, Serialize};
4use space::services::spaces::UserDto;
4use space::services::dto::UserDto;
55use time::OffsetDateTime;
66use utoipa::{IntoParams, ToSchema};
77use uuid::Uuid;
Modified lib/issues/src/services/issue_users.rs View lib/issues/src/services/issue_users.rs
@@ -4,7 +4,7 @@
44 QuerySelect, Set,
55};
66use serde::{Deserialize, Serialize};
7use space::services::spaces::UserDto;
7use space::services::dto::UserDto;
88use time::OffsetDateTime;
99use utoipa::{IntoParams, ToSchema};
1010use uuid::Uuid;
Modified lib/issues/src/services/issues.rs View lib/issues/src/services/issues.rs
@@ -6,14 +6,14 @@
66 issue_relation_pull, issue_relation_repo, issue_user, issue_watch, issues, milestone,
77 stopwatch, tracked_time,
88};
9use models::space::space_members::MemberStatus;
9use models::space::MemberStatus;
1010use models::users::user_notifications::NotificationType;
1111use sea_orm::{
1212 ActiveModelTrait, ColumnTrait, Condition, ConnectionTrait, EntityTrait, PaginatorTrait,
1313 QueryFilter, QueryOrder, QuerySelect, Set, TransactionTrait,
1414};
1515use serde::{Deserialize, Serialize};
16use space::services::spaces::UserDto;
16use space::services::dto::UserDto;
1717use time::OffsetDateTime;
1818use utoipa::{IntoParams, ToSchema};
1919use uuid::Uuid;
@@ -475,10 +475,10 @@
475475 }
476476
477477 for a_uid in &cmd.assignee_ids {
478 let is_member = *a_uid == space.owner_id
479 || space::authority::member(&self.state, space.owner_id, space.id, *a_uid)
478 let is_member = *a_uid == space.owner_user_id
479 || space::authority::member(&self.state, space.owner_user_id, space.id, *a_uid)
480480 .await?
481 .is_some_and(|m| m.status == MemberStatus::Active);
481 .is_some_and(|m| m.status() == MemberStatus::Active);
482482 if !is_member {
483483 return Err(IssueError::BadRequest(format!(
484484 "assignee {a_uid} is not a member of this space"
@@ -514,7 +514,7 @@
514514 let assignee_ids = cmd.assignee_ids.clone();
515515 let label_ids = cmd.label_ids.clone();
516516 let attachment_ids = cmd.attachment_ids.clone();
517 let space_lower_for_txn = space.lower_name.clone();
517 let space_lower_for_txn = space.slug.clone();
518518 let mentioned_for_txn = mentioned_uids.clone();
519519 let this = self.clone();
520520 let pending = export::notify::PendingNotifications::default();
@@ -663,7 +663,7 @@
663663 let link = format!(
664664 "{}/issues/{}/{}/{}",
665665 self.config().web.web_base_url,
666 space.lower_name,
666 space.slug,
667667 board.id,
668668 inserted.id
669669 );
@@ -895,10 +895,10 @@
895895 }
896896
897897 for a_uid in cmd.assignee_ids.iter().flatten() {
898 let is_member = *a_uid == space.owner_id
899 || space::authority::member(&self.state, space.owner_id, space.id, *a_uid)
898 let is_member = *a_uid == space.owner_user_id
899 || space::authority::member(&self.state, space.owner_user_id, space.id, *a_uid)
900900 .await?
901 .is_some_and(|m| m.status == MemberStatus::Active);
901 .is_some_and(|m| m.status() == MemberStatus::Active);
902902 if !is_member {
903903 return Err(IssueError::BadRequest(format!(
904904 "assignee {a_uid} is not a member of this space"
Modified lib/issues/src/services/members.rs View lib/issues/src/services/members.rs
@@ -1,13 +1,13 @@
11use models::issue::issue_event_line::IssueEventType;
22use models::issue::issue_member::{self, IssueMemberKind};
3use models::space::space_members::MemberStatus;
3use models::space::MemberStatus;
44use models::users::user_notifications::NotificationType;
55use sea_orm::{
66 ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder,
77 QuerySelect, Set, TransactionTrait,
88};
99use serde::{Deserialize, Serialize};
10use space::services::spaces::UserDto;
10use space::services::dto::UserDto;
1111use time::OffsetDateTime;
1212use utoipa::{IntoParams, ToSchema};
1313use uuid::Uuid;
@@ -65,12 +65,12 @@
6565 let board = lookup_board(self, &space, board_id, caller).await?;
6666 let item = lookup_issue(self, &board, issue_id).await?;
6767
68 let target_is_member = if cmd.user_id == space.owner_id {
68 let target_is_member = if cmd.user_id == space.owner_user_id {
6969 true
7070 } else {
71 space::authority::member(&self.state, space.owner_id, space.id, cmd.user_id)
71 space::authority::member(&self.state, space.owner_user_id, space.id, cmd.user_id)
7272 .await?
73 .is_some_and(|m| m.status == MemberStatus::Active)
73 .is_some_and(|m| m.status() == MemberStatus::Active)
7474 };
7575 if !target_is_member {
7676 return Err(IssueError::Forbidden);
@@ -94,7 +94,7 @@
9494 let target_uid = cmd.user_id;
9595 let this = self.clone();
9696 let item_title_for_txn = item.title.clone();
97 let space_lower_for_txn = space.lower_name.clone();
97 let space_lower_for_txn = space.slug.clone();
9898 let pending = export::notify::PendingNotifications::default();
9999 let pending_txn = pending.clone();
100100
@@ -169,7 +169,7 @@
169169 let link = format!(
170170 "{}/issues/{}/{}/{}",
171171 self.config().web.web_base_url,
172 space.lower_name,
172 space.slug,
173173 board.id,
174174 item.id
175175 );
Modified lib/issues/src/services/reactions.rs View lib/issues/src/services/reactions.rs
@@ -1,11 +1,11 @@
11use std::collections::HashMap;
22
33use models::issue::{comment, issue_reactions};
4use models::space::space_join_rule::SpaceJoinTrigger;
4use models::space::SpaceJoinTrigger;
55use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
66use serde::{Deserialize, Serialize};
7use space::services::dto::UserDto;
78use space::services::join_rules;
8use space::services::spaces::UserDto;
99use time::OffsetDateTime;
1010use utoipa::{IntoParams, ToSchema};
1111use uuid::Uuid;
Modified lib/issues/src/services/service.rs View lib/issues/src/services/service.rs
@@ -10,7 +10,7 @@
1010use models::space::spaces;
1111use models::users::user_notifications::{self, NotificationType};
1212use sea_orm::{ActiveModelTrait, ConnectionTrait, EntityTrait, Set};
13use space::services::spaces::UserDto;
13use space::services::dto::UserDto;
1414use time::OffsetDateTime;
1515use uuid::Uuid;
1616
@@ -200,13 +200,18 @@
200200 text: &str,
201201 exclude: i64,
202202 ) -> Result<Vec<i64>, IssueError> {
203 let members =
204 space::authority::list_members(&self.state, space.owner_id, space.id, false, false)
205 .await?;
203 let members = space::authority::list_members(
204 &self.state,
205 space.owner_user_id,
206 space.id,
207 false,
208 false,
209 )
210 .await?;
206211
207212 let mut member_uids: Vec<i64> = members.into_iter().map(|m| m.user_id).collect();
208 if !member_uids.contains(&space.owner_id) {
209 member_uids.push(space.owner_id);
213 if !member_uids.contains(&space.owner_user_id) {
214 member_uids.push(space.owner_user_id);
210215 }
211216
212217 if member_uids.is_empty() {
Modified lib/issues/src/services/stopwatch.rs View lib/issues/src/services/stopwatch.rs
@@ -1,7 +1,7 @@
11use models::issue::{stopwatch, tracked_time};
22use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set, TransactionTrait};
33use serde::{Deserialize, Serialize};
4use space::services::spaces::UserDto;
4use space::services::dto::UserDto;
55use time::OffsetDateTime;
66use utoipa::{IntoParams, ToSchema};
77use uuid::Uuid;
Modified lib/issues/src/services/tracked_time.rs View lib/issues/src/services/tracked_time.rs
@@ -4,7 +4,7 @@
44 QuerySelect, Set,
55};
66use serde::{Deserialize, Serialize};
7use space::services::spaces::UserDto;
7use space::services::dto::UserDto;
88use time::OffsetDateTime;
99use utoipa::{IntoParams, ToSchema};
1010use uuid::Uuid;
Modified lib/issues/src/services/watches.rs View lib/issues/src/services/watches.rs
@@ -2,7 +2,7 @@
22use models::issue::issue_watch;
33use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
44use serde::{Deserialize, Serialize};
5use space::services::spaces::UserDto;
5use space::services::dto::UserDto;
66use time::OffsetDateTime;
77use utoipa::{IntoParams, ToSchema};
88use uuid::Uuid;
Modified lib/issues/src/share/authz.rs View lib/issues/src/share/authz.rs
@@ -21,7 +21,7 @@
2121 space: &spaces::Model,
2222 caller: i64,
2323) -> Result<bool, IssueError> {
24 if space.owner_id == caller {
24 if space.owner_user_id == caller {
2525 return Ok(true);
2626 }
2727 let perms = effective_permissions(&service.state, space, caller)
Modified lib/issues/src/share/lookup.rs View lib/issues/src/share/lookup.rs
@@ -180,7 +180,7 @@
180180 let space =
181181 space::authority::space_by_id(&service.state, 0, dir.space_id.ok_or(IssueError::NotFound)?)
182182 .await?;
183 if !space.is_active {
183 if !space::authority::space_is_active(&space) {
184184 return Err(IssueError::NotFound);
185185 }
186186 require_member(service, &space, caller).await?;
Modified lib/issues/src/openapi.rs View lib/issues/src/openapi.rs
@@ -106,8 +106,8 @@
106106 AppResponseData<crate::services::repo_relations::RepoRelDto>,
107107 AppResponseData<Vec<crate::services::repo_relations::RepoRelDto>>,
108108 export::rest::NoData,
109 space::services::spaces::UserDto,
110 space::services::spaces::SpaceNamePath,
109 space::services::dto::UserDto,
110 space::services::dto::SpacePath,
111111 crate::services::boards::BoardDto,
112112 crate::services::boards::BoardPage,
113113 crate::services::boards::CreateBoardCmd,
Modified lib/models/src/space/mod.rs View lib/models/src/space/mod.rs
@@ -2,32 +2,23 @@
22use serde::{Deserialize, Serialize};
33use utoipa::ToSchema;
44
5pub mod organization_invite_redemptions;
6pub mod organization_invites;
7pub mod organization_join_requests;
8pub mod organization_join_rules;
9pub mod organization_member_permissions;
10pub mod organization_member_roles;
11pub mod organization_members;
12pub mod organization_role_permissions;
13pub mod organization_roles;
14pub mod organizations;
15pub mod space_bans;
165pub mod space_extras;
176pub mod space_group_permissions;
187pub mod space_groups;
19pub mod space_invite;
20pub mod space_invite_groups;
21pub mod space_join_request;
22pub mod space_join_rule;
8pub mod space_invite_redemptions;
9pub mod space_invites;
10pub mod space_join_requests;
11pub mod space_join_rules;
2312pub mod space_member_extras;
2413pub mod space_member_groups;
14pub mod space_member_permissions;
15pub mod space_member_roles;
2516pub mod space_members;
17pub mod space_role_permissions;
18pub mod space_roles;
2619pub mod space_warn;
2720pub mod spaces;
2821
29pub use spaces::*;
30
3122#[derive(
3223 Clone, Copy, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, ToSchema,
3324)]
@@ -37,7 +28,7 @@
3728 db_type = "String(StringLen::N(16))",
3829 rename_all = "snake_case"
3930)]
40pub enum OrganizationStatus {
31pub enum SpaceStatus {
4132 Active,
4233 Suspended,
4334 Archived,
@@ -52,7 +43,7 @@
5243 db_type = "String(StringLen::N(16))",
5344 rename_all = "snake_case"
5445)]
55pub enum OrgMemberRole {
46pub enum SpaceMemberRole {
5647 Member,
5748 Admin,
5849 Owner,
@@ -67,7 +58,7 @@
6758 db_type = "String(StringLen::N(16))",
6859 rename_all = "snake_case"
6960)]
70pub enum OrgJoinMethod {
61pub enum SpaceJoinMethod {
7162 Direct,
7263 InviteLink,
7364 InviteTargeted,
@@ -105,6 +96,24 @@
10596 Nothing,
10697}
10798
99impl MessageNotifications {
100 pub fn as_str(self) -> &'static str {
101 match self {
102 Self::All => "all",
103 Self::Mentions => "mentions",
104 Self::Nothing => "nothing",
105 }
106 }
107
108 pub fn from_str(value: &str) -> Self {
109 match value.trim() {
110 "mentions" => Self::Mentions,
111 "nothing" => Self::Nothing,
112 _ => Self::All,
113 }
114 }
115}
116
108117#[derive(
109118 Clone, Copy, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, ToSchema,
110119)]
@@ -115,7 +124,7 @@
115124 rename_all = "snake_case"
116125)]
117126#[derive(Default)]
118pub enum OrganizationJoinPolicy {
127pub enum SpaceJoinPolicy {
119128 Open,
120129 Approval,
121130 #[default]
@@ -133,10 +142,90 @@
133142 rename_all = "snake_case"
134143)]
135144#[derive(Default)]
136pub enum OrganizationJoinRequestStatus {
145pub enum SpaceJoinRequestState {
137146 #[default]
138147 Pending,
139148 Approved,
140149 Rejected,
141150 Withdrawn,
142151}
152
153/// 会籍状态,由 `joined_at` / `left_at` / `banned_at` 推导,供 API 层展示。
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, ToSchema)]
155pub enum MemberStatus {
156 #[default]
157 Active,
158 Inactive,
159 Left,
160 Removed,
161 Banned,
162}
163
164impl space_members::Model {
165 pub fn status(&self) -> MemberStatus {
166 if self.banned_at.is_some() {
167 MemberStatus::Banned
168 } else if self.left_at.is_some() {
169 MemberStatus::Left
170 } else {
171 MemberStatus::Active
172 }
173 }
174}
175
176/// 允许加入的历史开关,映射 `spaces.allow_join_level`。
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, ToSchema)]
178pub enum AllowJoinLevel {
179 #[default]
180 None,
181 NeedMailActive,
182 NeedMailActiveAndLastOneMid,
183 NeedMailActiveAndLastOneHours,
184 NeedMailActiveAndPhoneActive,
185}
186
187impl AllowJoinLevel {
188 pub fn as_i16(self) -> i16 {
189 match self {
190 Self::None => 0,
191 Self::NeedMailActive => 1,
192 Self::NeedMailActiveAndLastOneMid => 2,
193 Self::NeedMailActiveAndLastOneHours => 3,
194 Self::NeedMailActiveAndPhoneActive => 4,
195 }
196 }
197
198 pub fn from_i16(value: i16) -> Self {
199 match value {
200 1 => Self::NeedMailActive,
201 2 => Self::NeedMailActiveAndLastOneMid,
202 3 => Self::NeedMailActiveAndLastOneHours,
203 4 => Self::NeedMailActiveAndPhoneActive,
204 _ => Self::None,
205 }
206 }
207}
208
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, ToSchema)]
210#[serde(rename_all = "snake_case")]
211pub enum SpaceJoinTrigger {
212 #[default]
213 IssueReaction,
214 IssueCommentReaction,
215}
216
217impl SpaceJoinTrigger {
218 pub fn as_str(self) -> &'static str {
219 match self {
220 Self::IssueReaction => "issue_reaction",
221 Self::IssueCommentReaction => "issue_comment_reaction",
222 }
223 }
224
225 pub fn from_str(value: &str) -> Self {
226 match value.trim() {
227 "issue_comment_reaction" => Self::IssueCommentReaction,
228 _ => Self::IssueReaction,
229 }
230 }
231}
Deleted lib/models/src/space/organization_members.rs

Diff too large to display.

Deleted lib/models/src/space/organizations.rs

Diff too large to display.

Deleted lib/models/src/space/space_bans.rs

Diff too large to display.

Deleted lib/models/src/space/space_invite.rs

Diff too large to display.

Deleted lib/models/src/space/space_invite_groups.rs

Diff too large to display.

Renamed lib/models/src/space/organization_invite_redemptions.rs → lib/models/src/space/space_invite_redemptions.rs View lib/models/src/space/space_invite_redemptions.rs
@@ -6,7 +6,7 @@
66/// 也留下"谁通过哪个邀请进来的"的审计线索。
77#[sea_orm::model]
88#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
9#[sea_orm(table_name = "organization_invite_redemptions")]
9#[sea_orm(table_name = "space_invite_redemptions")]
1010pub struct Model {
1111 #[sea_orm(primary_key, auto_increment = false)]
1212 pub id: Uuid,
@@ -21,7 +21,7 @@
2121 pub redeemed_at: DateTimeUtc,
2222
2323 #[sea_orm(belongs_to, from = "invite_id", to = "id", on_delete = "Cascade")]
24 pub invite: BelongsTo<super::organization_invites::Entity>,
24 pub invite: BelongsTo<super::space_invites::Entity>,
2525}
2626
2727#[async_trait::async_trait]
Renamed lib/models/src/space/organization_invites.rs → lib/models/src/space/space_invites.rs View lib/models/src/space/space_invites.rs
@@ -7,14 +7,14 @@
77/// `uses < max_uses` 作为条件,避免并发超发。
88#[sea_orm::model]
99#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
10#[sea_orm(table_name = "organization_invites")]
10#[sea_orm(table_name = "space_invites")]
1111pub struct Model {
1212 #[sea_orm(primary_key, auto_increment = false)]
1313 pub id: Uuid,
1414 #[sea_orm(indexed)]
1515 pub organization_id: Uuid,
1616
17 pub method: super::OrgJoinMethod,
17 pub method: super::SpaceJoinMethod,
1818 #[sea_orm(unique, column_type = "String(StringLen::N(64))")]
1919 pub token_hash: String,
2020
@@ -39,7 +39,7 @@
3939 pub created_at: DateTimeUtc,
4040
4141 #[sea_orm(belongs_to, from = "organization_id", to = "id", on_delete = "Cascade")]
42 pub organization: BelongsTo<super::organizations::Entity>,
42 pub organization: BelongsTo<super::spaces::Entity>,
4343}
4444
4545#[async_trait::async_trait]
Deleted lib/models/src/space/space_join_request.rs

Diff too large to display.

Renamed lib/models/src/space/organization_join_requests.rs → lib/models/src/space/space_join_requests.rs View lib/models/src/space/space_join_requests.rs
@@ -4,7 +4,7 @@
44
55#[sea_orm::model]
66#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "organization_join_requests")]
7#[sea_orm(table_name = "space_join_requests")]
88pub struct Model {
99 #[sea_orm(primary_key, auto_increment = false)]
1010 pub id: Uuid,
@@ -12,7 +12,7 @@
1212 pub organization_id: Uuid,
1313 #[sea_orm(indexed)]
1414 pub user_id: i64,
15 pub status: super::OrganizationJoinRequestStatus,
15 pub status: super::SpaceJoinRequestState,
1616 #[sea_orm(column_type = "Text")]
1717 pub note: Option<String>,
1818 pub decided_by: Option<i64>,
@@ -23,7 +23,7 @@
2323 pub updated_at: DateTimeUtc,
2424
2525 #[sea_orm(belongs_to, from = "organization_id", to = "id", on_delete = "Cascade")]
26 pub organization: BelongsTo<super::organizations::Entity>,
26 pub organization: BelongsTo<super::spaces::Entity>,
2727}
2828
2929#[async_trait::async_trait]
Deleted lib/models/src/space/space_join_rule.rs

Diff too large to display.

Renamed lib/models/src/space/organization_join_rules.rs → lib/models/src/space/space_join_rules.rs View lib/models/src/space/space_join_rules.rs
@@ -4,7 +4,7 @@
44
55#[sea_orm::model]
66#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "organization_join_rules")]
7#[sea_orm(table_name = "space_join_rules")]
88pub struct Model {
99 #[sea_orm(primary_key, auto_increment = false)]
1010 pub id: Uuid,
@@ -30,7 +30,7 @@
3030 pub updated_at: DateTimeUtc,
3131
3232 #[sea_orm(belongs_to, from = "organization_id", to = "id", on_delete = "Cascade")]
33 pub organization: BelongsTo<super::organizations::Entity>,
33 pub organization: BelongsTo<super::spaces::Entity>,
3434}
3535
3636#[async_trait::async_trait]
Renamed lib/models/src/space/organization_member_permissions.rs → lib/models/src/space/space_member_permissions.rs View lib/models/src/space/space_member_permissions.rs
@@ -5,7 +5,7 @@
55/// 成员直挂权限。解析时最后叠加,同一权限键上一律 DENY 优先。
66#[sea_orm::model]
77#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
8#[sea_orm(table_name = "organization_member_permissions")]
8#[sea_orm(table_name = "space_member_permissions")]
99pub struct Model {
1010 #[sea_orm(primary_key, auto_increment = false)]
1111 pub id: Uuid,
@@ -24,7 +24,7 @@
2424 pub granted_by: Option<i64>,
2525
2626 #[sea_orm(belongs_to, from = "member_id", to = "id", on_delete = "Cascade")]
27 pub member: BelongsTo<super::organization_members::Entity>,
27 pub member: BelongsTo<super::space_members::Entity>,
2828}
2929
3030#[async_trait::async_trait]
Renamed lib/models/src/space/organization_member_roles.rs → lib/models/src/space/space_member_roles.rs View lib/models/src/space/space_member_roles.rs
@@ -5,7 +5,7 @@
55/// 成员 ↔ 角色绑定。重复绑定靠 (member_id, role_id) 唯一键幂等。
66#[sea_orm::model]
77#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
8#[sea_orm(table_name = "organization_member_roles")]
8#[sea_orm(table_name = "space_member_roles")]
99pub struct Model {
1010 #[sea_orm(primary_key, auto_increment = false)]
1111 pub id: Uuid,
@@ -19,9 +19,9 @@
1919 pub granted_by: Option<i64>,
2020
2121 #[sea_orm(belongs_to, from = "member_id", to = "id", on_delete = "Cascade")]
22 pub member: BelongsTo<super::organization_members::Entity>,
22 pub member: BelongsTo<super::space_members::Entity>,
2323 #[sea_orm(belongs_to, from = "role_id", to = "id", on_delete = "Cascade")]
24 pub role: BelongsTo<super::organization_roles::Entity>,
24 pub role: BelongsTo<super::space_roles::Entity>,
2525}
2626
2727#[async_trait::async_trait]
Modified lib/models/src/space/space_members.rs View lib/models/src/space/space_members.rs
@@ -1,71 +1,84 @@
1use serde::{Deserialize, Serialize};
2use time::OffsetDateTime;
1use sea_orm::entity::Set;
2use sea_orm::entity::prelude::*;
33use uuid::Uuid;
44
5/// A member view assembled from KenOAuth's `OrganizationMemberInfo` plus the
6/// local `space_member_extras` row. Membership, roles, bans and preferences are
7/// owned by KenOAuth; no local table mirrors them.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, utoipa::ToSchema)]
9pub enum MemberStatus {
10 #[default]
11 Active,
12 Inactive,
13 Left,
14 Removed,
15 Banned,
16}
5/// 会籍三段式:`joined_at` 记录最近一次入组,`left_at` 记录退出,`banned_at` 记录封禁。
6/// 同一 (organization_id, user_id) 只有一行,重新加入时复用该行并清空 `left_at`,
7/// 这样审计与"这个人在这个组织的全部历史"始终落在同一处。
8#[sea_orm::model]
9#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
10#[sea_orm(table_name = "space_members")]
11pub struct Model {
12 #[sea_orm(primary_key, auto_increment = false)]
13 pub id: Uuid,
14 #[sea_orm(unique_key = "org_user")]
15 pub organization_id: Uuid,
16 #[sea_orm(indexed, unique_key = "org_user")]
17 pub user_id: i64,
1718
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, utoipa::ToSchema)]
19pub enum MessageNotifications {
20 #[default]
21 All,
22 Mentions,
23 Nothing,
24}
19 pub role: super::SpaceMemberRole,
2520
26impl MessageNotifications {
27 pub fn as_str(self) -> &'static str {
28 match self {
29 Self::All => "all",
30 Self::Mentions => "mentions",
31 Self::Nothing => "nothing",
32 }
33 }
21 #[sea_orm(default_expr = "Expr::current_timestamp()")]
22 pub joined_at: DateTimeUtc,
23 pub left_at: Option<DateTimeUtc>,
24 pub banned_at: Option<DateTimeUtc>,
25 #[sea_orm(column_type = "String(StringLen::N(512))")]
26 pub ban_reason: Option<String>,
3427
35 pub fn from_str(value: &str) -> Self {
36 match value.trim() {
37 "mentions" => Self::Mentions,
38 "nothing" => Self::Nothing,
39 _ => Self::All,
40 }
41 }
42}
28 pub invited_by: Option<i64>,
29 pub join_method: super::SpaceJoinMethod,
30 #[sea_orm(indexed)]
31 pub invite_id: Option<Uuid>,
4332
44#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
45pub struct Model {
46 pub space_id: Uuid,
47 pub user_id: i64,
33 /// 成员展示别名与头像(旧 `space_members.alias_*`)。
34 #[sea_orm(column_type = "String(StringLen::N(64))")]
4835 pub alias_name: Option<String>,
36 #[sea_orm(column_type = "String(StringLen::N(512))")]
4937 pub alias_avatar_url: Option<String>,
50 pub status: MemberStatus,
51 pub last_active_at: Option<OffsetDateTime>,
52 pub left_at: Option<OffsetDateTime>,
53 pub invited_at: Option<OffsetDateTime>,
54 pub invited_by: Option<i64>,
55 pub joined_at: OffsetDateTime,
56 pub created_at: OffsetDateTime,
57 pub updated_at: Option<OffsetDateTime>,
58 pub message_notifications: MessageNotifications,
38
39 /// 通知偏好与免打扰设置(旧 `space_members` 同名列表)。
40 #[sea_orm(default_value = "all")]
41 pub message_notifications: super::MessageNotifications,
42 #[sea_orm(default_value = false)]
5943 pub muted: bool,
60 pub mute_until: Option<OffsetDateTime>,
44 pub mute_until: Option<DateTimeUtc>,
45 #[sea_orm(default_value = false)]
6146 pub mobile_push: bool,
47 #[sea_orm(default_value = false)]
6248 pub suppress_everyone: bool,
49 #[sea_orm(default_value = false)]
6350 pub suppress_roles: bool,
51 #[sea_orm(default_value = false)]
6452 pub hide_muted_channels: bool,
65 pub banned_at: Option<OffsetDateTime>,
66 pub ban_reason: Option<String>,
67 pub ban_expires_at: Option<OffsetDateTime>,
53
54 /// 封禁附加信息(旧 `space_bans` 的 expires_at 与四个 allow_* 位)。
55 pub ban_expires_at: Option<DateTimeUtc>,
56 #[sea_orm(default_value = false)]
57 pub ban_allow_agent_join: bool,
58 #[sea_orm(default_value = false)]
6859 pub ban_allow_speak: bool,
60 #[sea_orm(default_value = false)]
6961 pub ban_allow_push_repo: bool,
62 #[sea_orm(default_value = false)]
7063 pub ban_allow_comment_any: bool,
64
65 pub last_active_at: Option<DateTimeUtc>,
66 #[sea_orm(default_value = 1)]
67 pub version: i32,
68
69 #[sea_orm(belongs_to, from = "organization_id", to = "id", on_delete = "Cascade")]
70 pub organization: BelongsTo<super::spaces::Entity>,
71}
72
73#[async_trait::async_trait]
74impl ActiveModelBehavior for ActiveModel {
75 async fn before_save<C>(mut self, _db: &C, insert: bool) -> Result<Self, DbErr>
76 where
77 C: ConnectionTrait,
78 {
79 if insert && self.id.is_not_set() {
80 self.id = Set(Uuid::now_v7());
81 }
82 Ok(self)
83 }
7184}
Renamed lib/models/src/space/organization_role_permissions.rs → lib/models/src/space/space_role_permissions.rs View lib/models/src/space/space_role_permissions.rs
@@ -5,7 +5,7 @@
55/// 角色 → 权限授予。同一 (role_id, permission) 只有一行,effect 决定授予或拒绝。
66#[sea_orm::model]
77#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
8#[sea_orm(table_name = "organization_role_permissions")]
8#[sea_orm(table_name = "space_role_permissions")]
99pub struct Model {
1010 #[sea_orm(primary_key, auto_increment = false)]
1111 pub id: Uuid,
@@ -23,7 +23,7 @@
2323 pub created_at: DateTimeUtc,
2424
2525 #[sea_orm(belongs_to, from = "role_id", to = "id", on_delete = "Cascade")]
26 pub role: BelongsTo<super::organization_roles::Entity>,
26 pub role: BelongsTo<super::space_roles::Entity>,
2727}
2828
2929#[async_trait::async_trait]
Renamed lib/models/src/space/organization_roles.rs → lib/models/src/space/space_roles.rs View lib/models/src/space/space_roles.rs

Diff too large to display.

Modified lib/models/src/space/spaces.rs View lib/models/src/space/spaces.rs
@@ -1,73 +1,78 @@
1use serde::{Deserialize, Serialize};
2use time::OffsetDateTime;
1use sea_orm::entity::Set;
2use sea_orm::entity::prelude::*;
33use uuid::Uuid;
44
5/// A space view assembled from KenOAuth's `OrganizationInfo` plus the local
6/// `space_extras` row. It deliberately has no SeaORM entity: KenOAuth is the
7/// only authority for the organization itself, membership and permissions.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, utoipa::ToSchema)]
9pub enum AllowJoinLevel {
10 #[default]
11 None,
12 NeedMailActive,
13 NeedMailActiveAndLastOneMid,
14 NeedMailActiveAndLastOneHours,
15 NeedMailActiveAndPhoneActive,
16}
17
18impl AllowJoinLevel {
19 pub fn as_i16(self) -> i16 {
20 match self {
21 Self::None => 0,
22 Self::NeedMailActive => 1,
23 Self::NeedMailActiveAndLastOneMid => 2,
24 Self::NeedMailActiveAndLastOneHours => 3,
25 Self::NeedMailActiveAndPhoneActive => 4,
26 }
27 }
28
29 pub fn from_i16(value: i16) -> Self {
30 match value {
31 1 => Self::NeedMailActive,
32 2 => Self::NeedMailActiveAndLastOneMid,
33 3 => Self::NeedMailActiveAndLastOneHours,
34 4 => Self::NeedMailActiveAndPhoneActive,
35 _ => Self::None,
36 }
37 }
38}
39
40#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "spaces")]
418pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
4210 pub id: Uuid,
43 pub lower_name: String,
11 #[sea_orm(unique, column_type = "String(StringLen::N(32))")]
12 pub slug: String,
13 #[sea_orm(column_type = "String(StringLen::N(128))")]
4414 pub name: String,
15 #[sea_orm(column_type = "String(StringLen::N(1024))")]
4516 pub description: Option<String>,
46 pub owner_id: i64,
17 #[sea_orm(column_type = "String(StringLen::N(512))")]
4718 pub avatar_url: Option<String>,
48 pub is_private: bool,
49 pub is_active: bool,
19
20 pub status: super::SpaceStatus,
21 #[sea_orm(
22 column_type = "String(StringLen::N(16))",
23 default_value = "invite_only"
24 )]
25 pub join_policy: super::SpaceJoinPolicy,
26 #[sea_orm(default_value = false)]
5027 pub allow_join: bool,
51 pub allow_invite: bool,
52 pub invite_sort_url_hash: Option<String>,
53 pub nums_members: i64,
54 pub banner_url: Option<String>,
55 pub banner_width: Option<i32>,
56 pub banner_height: Option<i32>,
57 pub invite_banner_url: Option<String>,
58 pub invite_banner_width: Option<i32>,
59 pub invite_banner_height: Option<i32>,
60 pub embed_splash_url: Option<String>,
61 pub embed_splash_width: Option<i32>,
62 pub embed_splash_height: Option<i32>,
28 #[sea_orm(default_value = true)]
29 pub require_email_verified: bool,
30 #[sea_orm(default_value = false)]
31 pub require_mfa: bool,
32 #[sea_orm(column_type = "Text")]
6333 pub welcome_message: Option<String>,
64 pub allow_join_level: AllowJoinLevel,
34 #[sea_orm(default_value = false)]
6535 pub member_must_enable_2fa: bool,
66 pub welcome_object: Option<Uuid>,
67 pub join_policy: String,
68 pub require_email_verified: bool,
69 pub require_mfa: bool,
36 #[sea_orm(default_value = true)]
37 pub allow_invite: bool,
38 #[sea_orm(default_value = 0)]
39 pub allow_join_level: i16,
40 // 唯一 OWNER。转移所有权时在同一事务内改写两行会籍。
41 #[sea_orm(indexed)]
42 pub owner_user_id: i64,
43 #[sea_orm(column_type = "String(StringLen::N(512))")]
44 pub suspended_reason: Option<String>,
45
46 #[sea_orm(default_expr = "Expr::current_timestamp()")]
47 pub created_at: DateTimeUtc,
48 #[sea_orm(default_expr = "Expr::current_timestamp()")]
49 pub updated_at: DateTimeUtc,
50 pub archived_at: Option<DateTimeUtc>,
51 pub deleted_at: Option<DateTimeUtc>,
52
53 // 成员/角色/权限变更的版本键。缓存键与乐观锁都用它,绝不复用 users.security_version:
54 // 那个字段推进会让用户全部令牌失效,组织层面的改动不该有这个副作用。
55 #[sea_orm(default_value = 1)]
7056 pub version: i32,
71 pub created_at: OffsetDateTime,
72 pub updated_at: OffsetDateTime,
57
58 #[sea_orm(has_many)]
59 pub members: HasMany<super::space_members::Entity>,
60 #[sea_orm(has_many)]
61 pub roles: HasMany<super::space_roles::Entity>,
62 #[sea_orm(has_many)]
63 pub invites: HasMany<super::space_invites::Entity>,
64}
65
66#[async_trait::async_trait]
67impl ActiveModelBehavior for ActiveModel {
68 async fn before_save<C>(mut self, _db: &C, insert: bool) -> Result<Self, DbErr>
69 where
70 C: ConnectionTrait,
71 {
72 if insert && self.id.is_not_set() {
73 self.id = Set(Uuid::now_v7());
74 }
75 self.updated_at = Set(chrono::Utc::now());
76 Ok(self)
77 }
7378}
Modified lib/models/src/lib.rs View lib/models/src/lib.rs
@@ -6,7 +6,6 @@
66pub mod issue;
77pub mod migrate;
88pub mod products;
9mod proto_conv;
109pub mod pull_request;
1110pub mod repo;
1211pub mod room;
Deleted lib/models/src/proto_conv.rs

Diff too large to display.

Deleted lib/proto/proto/auth/internal/v1/organization.proto

Diff too large to display.

Deleted lib/proto/proto/auth/v1/organization.proto

Diff too large to display.

Modified lib/proto/src/client.rs View lib/proto/src/client.rs
@@ -208,7 +208,7 @@
208208}
209209
210210impl KenShellClient {
211 /// Every repository owned by one organization, across admin pages.
211 /// Every repository owned by one space, across admin pages.
212212 pub async fn list_repositories_for_org(
213213 &self,
214214 owner_org_id: &str,
Modified lib/pulls/src/services/assignees.rs View lib/pulls/src/services/assignees.rs
@@ -2,7 +2,7 @@
22use sea_orm::ActiveValue::Set;
33use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder};
44use serde::{Deserialize, Serialize};
5use space::services::spaces::UserDto;
5use space::services::dto::UserDto;
66use time::OffsetDateTime;
77use utoipa::ToSchema;
88use uuid::Uuid;
Modified lib/pulls/src/services/comments.rs View lib/pulls/src/services/comments.rs
@@ -7,7 +7,7 @@
77 QuerySelect,
88};
99use serde::{Deserialize, Serialize};
10use space::services::spaces::UserDto;
10use space::services::dto::UserDto;
1111use std::collections::HashMap;
1212use time::OffsetDateTime;
1313use utoipa::ToSchema;
Modified lib/pulls/src/services/events.rs View lib/pulls/src/services/events.rs
@@ -7,7 +7,7 @@
77 QueryOrder, QuerySelect,
88};
99use serde::{Deserialize, Serialize};
10use space::services::spaces::UserDto;
10use space::services::dto::UserDto;
1111use std::collections::HashMap;
1212use time::OffsetDateTime;
1313use uuid::Uuid;
Modified lib/pulls/src/services/pulls.rs View lib/pulls/src/services/pulls.rs
@@ -12,7 +12,7 @@
1212 QuerySelect, TransactionTrait,
1313};
1414use serde::{Deserialize, Serialize};
15use space::services::spaces::UserDto;
15use space::services::dto::UserDto;
1616use std::collections::HashMap;
1717use time::OffsetDateTime;
1818use utoipa::{IntoParams, ToSchema};
Modified lib/pulls/src/services/reviewers.rs View lib/pulls/src/services/reviewers.rs
@@ -2,7 +2,7 @@
22use sea_orm::ActiveValue::Set;
33use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder};
44use serde::{Deserialize, Serialize};
5use space::services::spaces::UserDto;
5use space::services::dto::UserDto;
66use time::OffsetDateTime;
77use utoipa::ToSchema;
88use uuid::Uuid;
Modified lib/pulls/src/services/reviews.rs View lib/pulls/src/services/reviews.rs
@@ -6,7 +6,7 @@
66 Set as AmSet, TransactionTrait,
77};
88use serde::{Deserialize, Serialize};
9use space::services::spaces::UserDto;
9use space::services::dto::UserDto;
1010use time::OffsetDateTime;
1111use utoipa::ToSchema;
1212use uuid::Uuid;
Modified lib/pulls/src/openapi.rs View lib/pulls/src/openapi.rs
@@ -74,7 +74,7 @@
7474 AppResponseData<crate::services::checks::PullGateDto>,
7575 AppResponseData<crate::services::inbox::InboxPage>,
7676 NoData,
77 space::services::spaces::UserDto,
77 space::services::dto::UserDto,
7878 issues::services::labels::LabelDto,
7979 crate::services::pulls::PullDto,
8080 crate::services::pulls::PullRefDto,
Deleted lib/space/src/handlers/accept_invite.rs

Diff too large to display.

Deleted lib/space/src/handlers/add_member.rs

Diff too large to display.

Modified lib/space/src/handlers/assign_group.rs View lib/space/src/handlers/assign_group.rs
@@ -3,7 +3,7 @@
33use export::rest::{AppResponseData, NoData};
44
55use crate::errors::ApiError;
6use crate::handlers::get_member::MemberPath;
6use crate::services::dto::MemberPath;
77use crate::services::groups::AssignGroupCmd;
88use crate::services::service::SpaceService;
99use ::session::share::session;
Deleted lib/space/src/handlers/ban_user.rs

Diff too large to display.

Modified lib/space/src/handlers/create_dir_node.rs View lib/space/src/handlers/create_dir_node.rs
@@ -4,8 +4,8 @@
44
55use crate::errors::ApiError;
66use crate::services::dir::{CreateDirCmd, DirNodeDto};
7use crate::services::dto::SpacePath;
78use crate::services::service::SpaceService;
8use crate::services::spaces::SpaceNamePath;
99use ::session::share::session;
1010
1111#[utoipa::path(
@@ -13,7 +13,7 @@
1313 path = "/spaces/{space_name}/dir",
1414 tag = "space",
1515 description = "在空间目录树中创建目录节点。",
16 params(SpaceNamePath),
16 params(SpacePath),
1717 request_body = CreateDirCmd,
1818 responses(
1919 (status = 200, description = "dir node created", body = AppResponseData<DirNodeDto>),
@@ -27,7 +27,7 @@
2727pub async fn create_dir_node(
2828 State(service): State<SpaceService>,
2929 session: Session,
30 Path(path): Path<SpaceNamePath>,
30 Path(path): Path<SpacePath>,
3131 Json(cmd): Json<CreateDirCmd>,
3232) -> Result<AppResponseData<DirNodeDto>, ApiError> {
3333 let uid = session::require_uid(&session)?;
Modified lib/space/src/handlers/create_group.rs View lib/space/src/handlers/create_group.rs
@@ -3,9 +3,9 @@
33use export::rest::{AppResponseData, NoData};
44
55use crate::errors::ApiError;
6use crate::services::dto::SpacePath;
67use crate::services::groups::{CreateGroupCmd, GroupDto};
78use crate::services::service::SpaceService;
8use crate::services::spaces::SpaceNamePath;
99use ::session::share::session;
1010
1111#[utoipa::path(
@@ -13,7 +13,7 @@
1313 path = "/spaces/{space_name}/groups",
1414 tag = "space",
1515 description = "创建空间群组并为其授予合法权限集合。",
16 params(SpaceNamePath),
16 params(SpacePath),
1717 request_body = CreateGroupCmd,
1818 responses(
1919 (status = 200, description = "group created", body = AppResponseData<GroupDto>),
@@ -27,7 +27,7 @@
2727pub async fn create_group(
2828 State(service): State<SpaceService>,
2929 session: Session,
30 Path(path): Path<SpaceNamePath>,
30 Path(path): Path<SpacePath>,
3131 Json(cmd): Json<CreateGroupCmd>,
3232) -> Result<AppResponseData<GroupDto>, ApiError> {
3333 let uid = session::require_uid(&session)?;
Deleted lib/space/src/handlers/create_invite.rs

Diff too large to display.

Modified lib/space/src/handlers/create_join_rule.rs View lib/space/src/handlers/create_join_rule.rs
@@ -3,9 +3,9 @@
33use export::rest::{AppResponseData, NoData};
44
55use crate::errors::ApiError;
6use crate::services::dto::SpacePath;
67use crate::services::join_rules::{CreateJoinRuleCmd, JoinRuleDto};
78use crate::services::service::SpaceService;
8use crate::services::spaces::SpaceNamePath;
99use ::session::share::session;
1010
1111#[utoipa::path(
@@ -13,7 +13,7 @@
1313 path = "/spaces/{space_name}/join-rules",
1414 tag = "space",
1515 description = "创建根据议题事件自动授予群组成员身份的规则。",
16 params(SpaceNamePath),
16 params(SpacePath),
1717 request_body = CreateJoinRuleCmd,
1818 responses(
1919 (status = 200, description = "join rule created", body = AppResponseData<JoinRuleDto>),
@@ -27,7 +27,7 @@
2727pub async fn create_join_rule(
2828 State(service): State<SpaceService>,
2929 session: Session,
30 Path(path): Path<SpaceNamePath>,
30 Path(path): Path<SpacePath>,
3131 Json(cmd): Json<CreateJoinRuleCmd>,
3232) -> Result<AppResponseData<JoinRuleDto>, ApiError> {
3333 let uid = session::require_uid(&session)?;
Modified lib/space/src/handlers/create_room.rs View lib/space/src/handlers/create_room.rs
@@ -3,9 +3,9 @@
33use export::rest::{AppResponseData, NoData};
44
55use crate::errors::ApiError;
6use crate::services::dto::SpacePath;
67use crate::services::rooms::{CreateRoomCmd, RoomDto};
78use crate::services::service::SpaceService;
8use crate::services::spaces::SpaceNamePath;
99use ::session::share::session;
1010
1111#[utoipa::path(
@@ -13,7 +13,7 @@
1313 path = "/chat/spaces/{space_name}/rooms",
1414 tag = "chat",
1515 description = "在空间下创建聊天频道;公开频道会自动加入全部活跃成员。",
16 params(SpaceNamePath),
16 params(SpacePath),
1717 request_body = CreateRoomCmd,
1818 responses(
1919 (status = 200, description = "room created", body = AppResponseData<RoomDto>),
@@ -28,7 +28,7 @@
2828pub async fn create_room(
2929 State(service): State<SpaceService>,
3030 session: Session,
31 Path(path): Path<SpaceNamePath>,
31 Path(path): Path<SpacePath>,
3232 Json(cmd): Json<CreateRoomCmd>,
3333) -> Result<AppResponseData<RoomDto>, ApiError> {
3434 let uid = session::require_uid(&session)?;
Deleted lib/space/src/handlers/create_space.rs

Diff too large to display.

Deleted lib/space/src/handlers/delete_space.rs

Diff too large to display.

Modified lib/space/src/handlers/explore_spaces.rs View lib/space/src/handlers/explore_spaces.rs
@@ -3,12 +3,12 @@
33use export::rest::{AppResponseData, NoData};
44
55use crate::errors::ApiError;
6use crate::services::dto::SpacePath;
67use crate::services::join_requests::{
78 ExploreQuery, ExploreSpacePage, JoinRequestDto, JoinRequestPage, JoinRequestPath,
89 ListJoinRequestsQuery,
910};
1011use crate::services::service::SpaceService;
11use crate::services::spaces::SpaceNamePath;
1212use ::session::share::session;
1313
1414#[utoipa::path(
@@ -37,7 +37,7 @@
3737 path = "/spaces/{space_name}/join-requests",
3838 tag = "space",
3939 description = "分页列出空间的加入申请,需要成员管理权限。",
40 params(SpaceNamePath, ListJoinRequestsQuery),
40 params(SpacePath, ListJoinRequestsQuery),
4141 responses(
4242 (status = 200, description = "list join requests", body = AppResponseData<JoinRequestPage>),
4343 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
@@ -48,7 +48,7 @@
4848pub async fn list_join_requests(
4949 State(service): State<SpaceService>,
5050 session: Session,
51 Path(path): Path<SpaceNamePath>,
51 Path(path): Path<SpacePath>,
5252 Query(query): Query<ListJoinRequestsQuery>,
5353) -> Result<AppResponseData<JoinRequestPage>, ApiError> {
5454 let uid = session::require_uid(&session)?;
Deleted lib/space/src/handlers/get_ban.rs

Diff too large to display.

Deleted lib/space/src/handlers/get_member.rs

Diff too large to display.

Deleted lib/space/src/handlers/get_space.rs

Diff too large to display.

Modified lib/space/src/handlers/get_space_dir_tree.rs View lib/space/src/handlers/get_space_dir_tree.rs
@@ -5,8 +5,8 @@
55
66use crate::errors::ApiError;
77use crate::services::dir::DirNodeDto;
8use crate::services::dto::SpacePath;
89use crate::services::service::SpaceService;
9use crate::services::spaces::SpaceNamePath;
1010use ::session::share::session;
1111
1212#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)]
@@ -19,7 +19,7 @@
1919 path = "/spaces/{space_name}/dir",
2020 tag = "space",
2121 description = "按用户与群组覆写权限返回目录树或指定层级的子树。",
22 params(SpaceNamePath, DirTreeQuery),
22 params(SpacePath, DirTreeQuery),
2323 responses(
2424 (status = 200, description = "get space dir tree", body = AppResponseData<Vec<DirNodeDto>>),
2525 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
@@ -30,7 +30,7 @@
3030pub async fn get_space_dir_tree(
3131 State(service): State<SpaceService>,
3232 session: Session,
33 Path(path): Path<SpaceNamePath>,
33 Path(path): Path<SpacePath>,
3434 Query(q): Query<DirTreeQuery>,
3535) -> Result<AppResponseData<Vec<DirNodeDto>>, ApiError> {
3636 let uid = session::require_uid(&session)?;
Modified lib/space/src/handlers/join_requests.rs View lib/space/src/handlers/join_requests.rs
@@ -3,9 +3,9 @@
33use export::rest::{AppResponseData, NoData};
44
55use crate::errors::ApiError;
6use crate::services::dto::SpacePath;
67use crate::services::join_requests::{CreateJoinRequestCmd, JoinRequestDto, JoinRequestPath};
78use crate::services::service::SpaceService;
8use crate::services::spaces::SpaceNamePath;
99use ::session::share::session;
1010
1111#[utoipa::path(
@@ -13,7 +13,7 @@
1313 path = "/spaces/{space_name}/join-requests",
1414 tag = "space",
1515 description = "提交一条加入公开空间的申请,可附一段说明。",
16 params(SpaceNamePath),
16 params(SpacePath),
1717 request_body = CreateJoinRequestCmd,
1818 responses(
1919 (status = 200, description = "join request submitted", body = AppResponseData<JoinRequestDto>),
@@ -28,7 +28,7 @@
2828pub async fn create_join_request(
2929 State(service): State<SpaceService>,
3030 session: Session,
31 Path(path): Path<SpaceNamePath>,
31 Path(path): Path<SpacePath>,
3232 Json(cmd): Json<CreateJoinRequestCmd>,
3333) -> Result<AppResponseData<JoinRequestDto>, ApiError> {
3434 let uid = session::require_uid(&session)?;
Deleted lib/space/src/handlers/list_bans.rs

Diff too large to display.

Modified lib/space/src/handlers/list_groups.rs View lib/space/src/handlers/list_groups.rs
@@ -3,9 +3,9 @@
33use export::rest::{AppResponseData, NoData};
44
55use crate::errors::ApiError;
6use crate::services::dto::{PageQuery, SpacePath};
67use crate::services::groups::GroupPage;
78use crate::services::service::SpaceService;
8use crate::services::spaces::{PageQuery, SpaceNamePath};
99use ::session::share::session;
1010
1111#[utoipa::path(
@@ -13,7 +13,7 @@
1313 path = "/spaces/{space_name}/groups",
1414 tag = "space",
1515 description = "分页列出空间群组及权限;需要角色管理权限。",
16 params(SpaceNamePath, PageQuery),
16 params(SpacePath, PageQuery),
1717 responses(
1818 (status = 200, description = "list space groups", body = AppResponseData<GroupPage>),
1919 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
@@ -24,7 +24,7 @@
2424pub async fn list_groups(
2525 State(service): State<SpaceService>,
2626 session: Session,
27 Path(path): Path<SpaceNamePath>,
27 Path(path): Path<SpacePath>,
2828 Query(query): Query<PageQuery>,
2929) -> Result<AppResponseData<GroupPage>, ApiError> {
3030 let uid = session::require_uid(&session)?;
Deleted lib/space/src/handlers/list_invites.rs

Diff too large to display.

Modified lib/space/src/handlers/list_join_rules.rs View lib/space/src/handlers/list_join_rules.rs
@@ -3,9 +3,9 @@
33use export::rest::{AppResponseData, NoData};
44
55use crate::errors::ApiError;
6use crate::services::dto::SpacePath;
67use crate::services::join_rules::JoinRuleDto;
78use crate::services::service::SpaceService;
8use crate::services::spaces::SpaceNamePath;
99use ::session::share::session;
1010
1111#[utoipa::path(
@@ -13,7 +13,7 @@
1313 path = "/spaces/{space_name}/join-rules",
1414 tag = "space",
1515 description = "列出空间的自动入群规则。",
16 params(SpaceNamePath),
16 params(SpacePath),
1717 responses(
1818 (status = 200, description = "list space join rules", body = AppResponseData<Vec<JoinRuleDto>>),
1919 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
@@ -24,7 +24,7 @@
2424pub async fn list_join_rules(
2525 State(service): State<SpaceService>,
2626 session: Session,
27 Path(path): Path<SpaceNamePath>,
27 Path(path): Path<SpacePath>,
2828) -> Result<AppResponseData<Vec<JoinRuleDto>>, ApiError> {
2929 let uid = session::require_uid(&session)?;
3030 let rules = service.list_join_rules(uid, &path.space_name).await?;
Deleted lib/space/src/handlers/list_members.rs

Diff too large to display.

Deleted lib/space/src/handlers/list_spaces.rs

Diff too large to display.

Modified lib/space/src/handlers/mod.rs View lib/space/src/handlers/mod.rs
@@ -1,53 +1,33 @@
1pub mod accept_invite;
2pub mod add_member;
31pub mod assign_group;
4pub mod ban_user;
52pub mod create_dir_node;
63pub mod create_group;
7pub mod create_invite;
84pub mod create_join_rule;
95pub mod create_room;
10pub mod create_space;
116pub mod delete_group;
127pub mod delete_join_rule;
138pub mod delete_room;
14pub mod delete_space;
159pub mod delete_space_image;
1610pub mod explore_spaces;
17pub mod get_ban;
1811pub mod get_group;
19pub mod get_member;
20pub mod get_space;
2112pub mod get_space_dir_tree;
2213pub mod get_space_image;
2314pub mod get_space_image_file;
2415pub mod join_group_self;
2516pub mod join_requests;
2617pub mod leave_group_self;
27pub mod list_bans;
2818pub mod list_dir_group_overwrites;
2919pub mod list_dir_user_overwrites;
3020pub mod list_groups;
31pub mod list_invites;
3221pub mod list_join_rules;
33pub mod list_members;
34pub mod list_spaces;
3522pub mod move_dir_node;
36pub mod reject_invite;
3723pub mod remove_dir_group_overwrite;
3824pub mod remove_dir_user_overwrite;
39pub mod remove_member;
4025pub mod repair_dir_parents;
41pub mod revoke_invite;
42pub mod self_member;
4326pub mod set_dir_group_overwrite;
4427pub mod set_dir_user_overwrite;
28pub mod space_admin;
4529pub mod unassign_group;
46pub mod unban_user;
47pub mod update_ban;
4830pub mod update_dir_node;
4931pub mod update_group;
5032pub mod update_join_rule;
51pub mod update_member;
52pub mod update_space;
5333pub mod upload_space_image;
Deleted lib/space/src/handlers/reject_invite.rs

Diff too large to display.

Deleted lib/space/src/handlers/remove_member.rs

Diff too large to display.

Modified lib/space/src/handlers/repair_dir_parents.rs View lib/space/src/handlers/repair_dir_parents.rs
@@ -4,8 +4,8 @@
44
55use crate::errors::ApiError;
66use crate::services::dir::RepairDirResp;
7use crate::services::dto::SpacePath;
78use crate::services::service::SpaceService;
8use crate::services::spaces::SpaceNamePath;
99use ::session::share::session;
1010
1111#[utoipa::path(
@@ -13,7 +13,7 @@
1313 path = "/spaces/{space_name}/dir/repair-parents",
1414 tag = "space",
1515 description = "拥有者修复目录树中的悬空父引用和循环结构。",
16 params(SpaceNamePath),
16 params(SpacePath),
1717 responses(
1818 (status = 200, description = "repair dir parents", body = AppResponseData<RepairDirResp>),
1919 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
@@ -25,7 +25,7 @@
2525pub async fn repair_dir_parents(
2626 State(service): State<SpaceService>,
2727 session: Session,
28 Path(path): Path<SpaceNamePath>,
28 Path(path): Path<SpacePath>,
2929) -> Result<AppResponseData<RepairDirResp>, ApiError> {
3030 let uid = session::require_uid(&session)?;
3131 let resp = service.repair_dir_parents(uid, &path.space_name).await?;
Deleted lib/space/src/handlers/revoke_invite.rs

Diff too large to display.

Deleted lib/space/src/handlers/self_member.rs

Diff too large to display.

Renamed lib/auth/src/handlers/identity/organization.rs → lib/space/src/handlers/space_admin.rs View lib/space/src/handlers/space_admin.rs

Diff too large to display.

Deleted lib/space/src/handlers/unban_user.rs

Diff too large to display.

Deleted lib/space/src/handlers/update_ban.rs

Diff too large to display.

Deleted lib/space/src/handlers/update_member.rs

Diff too large to display.

Deleted lib/space/src/handlers/update_space.rs

Diff too large to display.

Deleted lib/space/src/services/bans.rs

Diff too large to display.

Modified lib/space/src/services/dir.rs View lib/space/src/services/dir.rs
@@ -14,8 +14,8 @@
1414
1515use crate::authority;
1616use crate::errors::SpaceError;
17use crate::services::dto::UserDto;
1718use crate::services::service::SpaceService;
18use crate::services::spaces::UserDto;
1919use crate::share::authz::{
2020 MANAGE_CHANNELS, MANAGE_OVERWRITES, effective_permissions, require_perm,
2121};
@@ -513,7 +513,7 @@
513513 space_name: &str,
514514 ) -> Result<RepairDirResp, SpaceError> {
515515 let space = lookup::lookup_space_by_name(self, caller, space_name).await?;
516 if space.owner_id != caller {
516 if space.owner_user_id != caller {
517517 return Err(SpaceError::Forbidden);
518518 }
519519
@@ -749,7 +749,7 @@
749749 require_perm(self, &space, caller, MANAGE_OVERWRITES).await?;
750750 self.dir_in_space(space.id, dir_id).await?;
751751
752 if space.owner_id != user_id {
752 if space.owner_user_id != user_id {
753753 authority::require_member(&self.state, space.id, user_id)
754754 .await
755755 .map_err(|_| {
Added lib/space/src/services/dto.rs View lib/space/src/services/dto.rs

Diff too large to display.

Added lib/space/src/services/error.rs View lib/space/src/services/error.rs

Diff too large to display.

Modified lib/space/src/services/groups.rs View lib/space/src/services/groups.rs
@@ -1,6 +1,8 @@
11use std::collections::HashMap;
22
3use models::space::{space_group_permissions, space_groups, space_member_groups};
3use models::space::{
4 PermissionEffect, space_group_permissions, space_groups, space_member_groups, space_roles,
5};
46use sea_orm::{
57 ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, PaginatorTrait, QueryFilter,
68 QueryOrder, QuerySelect, Set, TransactionTrait,
@@ -12,8 +14,10 @@
1214
1315use crate::authority;
1416use crate::errors::SpaceError;
17use crate::services::dto::{PageQuery, UserDto};
18use crate::services::member::find_active;
1519use crate::services::service::SpaceService;
16use crate::services::spaces::{PageQuery, UserDto};
20use crate::services::types::{CreateRoleBody, PermissionGrant, SetRolePermissionsBody};
1721use crate::share::authz::{
1822 ALL_PERMISSIONS, MANAGE_ROLES, VIEW_SPACE, effective_permissions, require_perm,
1923};
@@ -141,7 +145,30 @@
141145 out
142146}
143147
148fn allow_grants(permissions: Vec<String>) -> Vec<PermissionGrant> {
149 permissions
150 .into_iter()
151 .map(|permission| PermissionGrant {
152 permission,
153 effect: PermissionEffect::Allow,
154 })
155 .collect()
156}
157
144158impl SpaceService {
159 async fn group_role_key(
160 &self,
161 organization_id: Uuid,
162 group_id: Uuid,
163 ) -> Result<String, SpaceError> {
164 space_roles::Entity::find_by_id(group_id)
165 .filter(space_roles::Column::OrganizationId.eq(organization_id))
166 .one(self.db().read())
167 .await?
168 .map(|row| row.key)
169 .ok_or(SpaceError::SpaceRoleNotFound)
170 }
171
145172 #[tracing::instrument(name = "space.create_group", skip_all, fields(caller = %caller, space = %space_name))]
146173 pub async fn create_group(
147174 &self,
@@ -171,25 +198,23 @@
171198 }
172199
173200 let permissions = cmd.permissions.clone();
174 let client = self
175 .state
176 .kenauth
177 .as_ref()
178 .ok_or_else(|| SpaceError::Internal("kenoauth is not configured".to_string()))?;
179 let role = client
201 let role = self
180202 .create_role(
181203 caller,
182 &space.id.to_string(),
183 &format!("g-{}", Uuid::now_v7()),
184 name,
185 "",
186 cmd.position.unwrap_or(0) as i32,
187 self.config().space.allow_self_join_groups,
188 org_permissions(&permissions),
204 space.id,
205 CreateRoleBody {
206 key: format!("g-{}", Uuid::now_v7()),
207 name: name.to_string(),
208 description: None,
209 rank: cmd.position.unwrap_or(0) as i32,
210 default_role: false,
211 self_assignable: self.config().space.allow_self_join_groups,
212 permissions: allow_grants(org_permissions(&permissions)),
213 },
214 None,
189215 )
190216 .await?;
191 let group_id = Uuid::parse_str(&role.id)
192 .map_err(|_| SpaceError::Internal("kenoauth returned a non-uuid role".to_string()))?;
217 let group_id = role.id;
193218
194219 let now = OffsetDateTime::now_utc();
195220 let group_row = space_groups::ActiveModel {
@@ -246,7 +271,7 @@
246271 group_id: Uuid,
247272 ) -> Result<GroupDto, SpaceError> {
248273 let space = lookup::lookup_space_by_name(self, caller.unwrap_or(0), space_name).await?;
249 if !space.is_active {
274 if !authority::space_is_active(&space) {
250275 return Err(SpaceError::NotFound);
251276 }
252277
@@ -259,7 +284,7 @@
259284 let Some(uid) = caller else {
260285 return Err(SpaceError::NotFound);
261286 };
262 if space.is_private {
287 if space.join_policy == models::space::SpaceJoinPolicy::Private {
263288 if require_perm(self, &space, uid, MANAGE_ROLES).await.is_err() {
264289 return Err(SpaceError::NotFound);
265290 }
@@ -358,32 +383,30 @@
358383 }
359384 sea_orm::ActiveValue::NotSet => None,
360385 };
361 let client = self
362 .state
363 .kenauth
364 .as_ref()
365 .ok_or_else(|| SpaceError::Internal("kenoauth is not configured".to_string()))?;
366 client
367 .update_role(
386 self.update_role(
387 caller,
388 space.id,
389 group_id,
390 next_name,
391 None,
392 next_position,
393 None,
394 None,
395 curr_ver,
396 None,
397 )
398 .await?;
399 if let Some(permissions) = cmd.permissions.as_ref() {
400 self.set_role_permissions(
368401 caller,
369 &space.id.to_string(),
370 &group_id.to_string(),
371 next_name,
402 space.id,
403 group_id,
404 SetRolePermissionsBody {
405 permissions: allow_grants(org_permissions(permissions)),
406 },
372407 None,
373 next_position,
374 None,
375 curr_ver,
376408 )
377409 .await?;
378 if let Some(permissions) = cmd.permissions.as_ref() {
379 client
380 .set_role_permissions(
381 caller,
382 &space.id.to_string(),
383 &group_id.to_string(),
384 org_permissions(permissions),
385 )
386 .await?;
387410 }
388411 am.version = Set(curr_ver + 1);
389412 let now = OffsetDateTime::now_utc();
@@ -446,14 +469,7 @@
446469 .await?
447470 .ok_or(SpaceError::NotFound)?;
448471
449 let client = self
450 .state
451 .kenauth
452 .as_ref()
453 .ok_or_else(|| SpaceError::Internal("kenoauth is not configured".to_string()))?;
454 client
455 .delete_role(caller, &space.id.to_string(), &group.id.to_string())
456 .await?;
472 self.delete_role(caller, space.id, group.id, None).await?;
457473
458474 self.db()
459475 .primary()
@@ -570,22 +586,11 @@
570586 .await?
571587 .ok_or(SpaceError::NotFound)?;
572588
573 authority::require_member(&self.state, space.id, uid)
574 .await
575 .map_err(|_| SpaceError::NotFound)?;
576
577 let client = self
578 .state
579 .kenauth
580 .as_ref()
581 .ok_or_else(|| SpaceError::Internal("kenoauth is not configured".to_string()))?;
582 client
583 .assign_role(
584 caller,
585 &space.id.to_string(),
586 uid,
587 &cmd.group_id.to_string(),
588 )
589 let member = find_active(self.db().read(), space.id, uid)
590 .await?
591 .ok_or(SpaceError::NotFound)?;
592 let role_key = self.group_role_key(space.id, cmd.group_id).await?;
593 self.assign_role(caller, space.id, member.id, &role_key, None)
589594 .await?;
590595
591596 let now = OffsetDateTime::now_utc();
@@ -610,13 +615,11 @@
610615 let space = lookup::lookup_space_by_name(self, caller, space_name).await?;
611616 require_perm(self, &space, caller, MANAGE_ROLES).await?;
612617
613 let client = self
614 .state
615 .kenauth
616 .as_ref()
617 .ok_or_else(|| SpaceError::Internal("kenoauth is not configured".to_string()))?;
618 client
619 .unassign_role(caller, &space.id.to_string(), uid, &group_id.to_string())
618 let member = find_active(self.db().read(), space.id, uid)
619 .await?
620 .ok_or(SpaceError::NotFound)?;
621 let role_key = self.group_role_key(space.id, group_id).await?;
622 self.unassign_role(caller, space.id, member.id, &role_key, None)
620623 .await?;
621624
622625 space_member_groups::Entity::delete_many()
@@ -655,13 +658,11 @@
655658 return Err(SpaceError::Conflict("already in group".to_string()));
656659 }
657660
658 let client = self
659 .state
660 .kenauth
661 .as_ref()
662 .ok_or_else(|| SpaceError::Internal("kenoauth is not configured".to_string()))?;
663 client
664 .assign_role(caller, &space.id.to_string(), caller, &group_id.to_string())
661 let member = find_active(self.db().read(), space.id, caller)
662 .await?
663 .ok_or(SpaceError::Forbidden)?;
664 let role_key = self.group_role_key(space.id, group_id).await?;
665 self.assign_role(caller, space.id, member.id, &role_key, None)
665666 .await?;
666667
667668 let now = OffsetDateTime::now_utc();
@@ -685,13 +686,11 @@
685686 let space = lookup::lookup_space_by_name(self, caller, space_name).await?;
686687 effective_permissions(&self.state, &space, caller).await?;
687688
688 let client = self
689 .state
690 .kenauth
691 .as_ref()
692 .ok_or_else(|| SpaceError::Internal("kenoauth is not configured".to_string()))?;
693 client
694 .unassign_role(caller, &space.id.to_string(), caller, &group_id.to_string())
689 let member = find_active(self.db().read(), space.id, caller)
690 .await?
691 .ok_or(SpaceError::Forbidden)?;
692 let role_key = self.group_role_key(space.id, group_id).await?;
693 self.unassign_role(caller, space.id, member.id, &role_key, None)
695694 .await?;
696695
697696 let res = space_member_groups::Entity::delete_many()
Modified lib/space/src/services/images.rs View lib/space/src/services/images.rs
@@ -1,6 +1,6 @@
11use imaging::layout::{ImageVariant, PreviewState};
22use imaging::preset::PreviewPreset;
3use models::space::spaces;
3use models::space::{space_extras, spaces};
44use serde::{Deserialize, Serialize};
55use time::OffsetDateTime;
66use utoipa::{IntoParams, ToSchema};
@@ -122,12 +122,18 @@
122122}
123123
124124/// Stored locator for the image row of `kind`; `None` when it was never uploaded.
125fn stored_space_image_url(space: &spaces::Model, kind: SpaceImageKind) -> Option<String> {
125fn stored_space_image_url(
126 space: &spaces::Model,
127 extras: Option<&space_extras::Model>,
128 kind: SpaceImageKind,
129) -> Option<String> {
126130 match kind {
127 SpaceImageKind::Avatar => space.avatar_url.clone(),
128 SpaceImageKind::Banner => space.banner_url.clone(),
129 SpaceImageKind::InviteBanner => space.invite_banner_url.clone(),
130 SpaceImageKind::EmbedSplash => space.embed_splash_url.clone(),
131 SpaceImageKind::Avatar => extras
132 .and_then(|row| row.avatar_url.clone())
133 .or_else(|| space.avatar_url.clone()),
134 SpaceImageKind::Banner => extras.and_then(|row| row.banner_url.clone()),
135 SpaceImageKind::InviteBanner => extras.and_then(|row| row.invite_banner_url.clone()),
136 SpaceImageKind::EmbedSplash => extras.and_then(|row| row.embed_splash_url.clone()),
131137 }
132138 .filter(|url| !url.is_empty())
133139}
@@ -386,10 +392,10 @@
386392 space_name: &str,
387393 ) -> Result<spaces::Model, SpaceError> {
388394 let space = lookup::lookup_space_by_name(self, caller.unwrap_or(0), space_name).await?;
389 if !space.is_active {
395 if !crate::authority::space_is_active(&space) {
390396 return Err(SpaceError::NotFound);
391397 }
392 if space.is_private {
398 if space.join_policy == models::space::SpaceJoinPolicy::Private {
393399 let Some(uid) = caller else {
394400 return Err(SpaceError::NotFound);
395401 };
@@ -435,7 +441,7 @@
435441 }
436442
437443 let space_id = space.id;
438 let lower_name = space.lower_name.clone();
444 let slug = space.slug.clone();
439445 let object_key = format!(
440446 "{SPACE_IMAGE_PREFIX}/{space_id}/{}.{}",
441447 kind.as_str(),
@@ -506,8 +512,8 @@
506512 Ok(SpaceImageDto {
507513 space_id,
508514 kind,
509 url: crate::services::spaces::resolve_space_image_url(
510 &lower_name,
515 url: crate::services::dto::resolve_space_image_url(
516 &slug,
511517 kind.as_str(),
512518 Some(stored_url),
513519 )
@@ -531,7 +537,8 @@
531537 require_perm(self, &space, caller, MANAGE_CHANNELS).await?;
532538
533539 let space_id = space.id;
534 let old_url = stored_space_image_url(&space, kind);
540 let extras = crate::authority::extras(&self.state, space_id).await?;
541 let old_url = stored_space_image_url(&space, extras.as_ref(), kind);
535542
536543 write_space_image(&self.state, space_id, kind, None, None, None).await?;
537544
@@ -567,15 +574,25 @@
567574 kind: SpaceImageKind,
568575 ) -> Result<Option<SpaceImageDto>, SpaceError> {
569576 let space = self.load_space_for_image_read(caller, space_name).await?;
577 let extras = crate::authority::extras(&self.state, space.id).await?;
570578
571 let Some(url) = stored_space_image_url(&space, kind) else {
579 let Some(url) = stored_space_image_url(&space, extras.as_ref(), kind) else {
572580 return Ok(None);
573581 };
574582 let (width, height) = match kind {
575583 SpaceImageKind::Avatar => (None, None),
576 SpaceImageKind::Banner => (space.banner_width, space.banner_height),
577 SpaceImageKind::InviteBanner => (space.invite_banner_width, space.invite_banner_height),
578 SpaceImageKind::EmbedSplash => (space.embed_splash_width, space.embed_splash_height),
584 SpaceImageKind::Banner => (
585 extras.as_ref().and_then(|row| row.banner_width),
586 extras.as_ref().and_then(|row| row.banner_height),
587 ),
588 SpaceImageKind::InviteBanner => (
589 extras.as_ref().and_then(|row| row.invite_banner_width),
590 extras.as_ref().and_then(|row| row.invite_banner_height),
591 ),
592 SpaceImageKind::EmbedSplash => (
593 extras.as_ref().and_then(|row| row.embed_splash_width),
594 extras.as_ref().and_then(|row| row.embed_splash_height),
595 ),
579596 };
580597
581598 let ext = url.rsplit('.').next().unwrap_or("jpg");
@@ -594,8 +611,8 @@
594611 Ok(Some(SpaceImageDto {
595612 space_id: space.id,
596613 kind,
597 url: crate::services::spaces::resolve_space_image_url(
598 &space.lower_name,
614 url: crate::services::dto::resolve_space_image_url(
615 &space.slug,
599616 kind.as_str(),
600617 Some(url),
601618 )
@@ -604,7 +621,9 @@
604621 width,
605622 height,
606623 bytes,
607 updated_at: space.updated_at,
624 updated_at: extras
625 .map(|row| row.updated_at)
626 .unwrap_or_else(OffsetDateTime::now_utc),
608627 }))
609628 }
610629
@@ -620,7 +639,9 @@
620639 variant: ImageVariant,
621640 ) -> Result<String, SpaceError> {
622641 let space = self.load_space_for_image_read(caller, space_name).await?;
623 let stored_url = stored_space_image_url(&space, kind).ok_or(SpaceError::NotFound)?;
642 let extras = crate::authority::extras(&self.state, space.id).await?;
643 let stored_url =
644 stored_space_image_url(&space, extras.as_ref(), kind).ok_or(SpaceError::NotFound)?;
624645 let locator = match variant {
625646 ImageVariant::Original => stored_url,
626647 ImageVariant::Preview => {
Renamed lib/auth/src/services/organization/invite.rs → lib/space/src/services/invite.rs View lib/space/src/services/invite.rs
@@ -7,24 +7,23 @@
77};
88use uuid::Uuid;
99
10use crate::AuthService;
11use crate::errors::{AuthError, AuthResult};
12use crate::events::label;
13use crate::models::space::{
14 OrgJoinMethod, OrgMemberRole, OrganizationStatus, organization_invite_redemptions,
15 organization_invites, organization_roles,
16};
17use crate::services::organization::member;
18use crate::services::organization::service::{bump_version, normalize_reason};
19use crate::services::organization::types::{
10use crate::errors::{SpaceError, SpaceResult};
11use crate::services::member;
12use crate::services::service::{SpaceService, bump_version, normalize_reason};
13use crate::services::types::{
2014 DEFAULT_INVITE_TTL_SECS, InviteSummary, SYSTEM_ROLE_MEMBER, SYSTEM_ROLE_OWNER,
2115};
2216use crate::share::crypto;
17use events::label;
18use models::space::{
19 SpaceJoinMethod, SpaceMemberRole, SpaceStatus, space_invite_redemptions, space_invites,
20 space_roles,
21};
2322
24impl AuthService {
23impl SpaceService {
2524 #[allow(clippy::too_many_arguments)]
2625 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, request_id = %request_id.unwrap_or_default()))]
27 pub async fn create_invite(
26 pub async fn admin_create_invite(
2827 &self,
2928 actor_user_id: i64,
3029 organization_id: Uuid,
@@ -33,7 +32,7 @@
3332 max_uses: i32,
3433 ttl_secs: Option<u64>,
3534 request_id: Option<&str>,
36 ) -> AuthResult<(InviteSummary, String)> {
35 ) -> SpaceResult<(InviteSummary, String)> {
3736 self.create_invite_with_extras(
3837 actor_user_id,
3938 organization_id,
@@ -62,24 +61,24 @@
6261 email: Option<String>,
6362 extra_role_ids: &[Uuid],
6463 request_id: Option<&str>,
65 ) -> AuthResult<(InviteSummary, String)> {
64 ) -> SpaceResult<(InviteSummary, String)> {
6665 self.require_permission(actor_user_id, organization_id, "invite.create", request_id)
6766 .await?;
6867 let organization = self.load_organization(organization_id).await?;
69 if organization.status != OrganizationStatus::Active {
70 return Err(AuthError::OrganizationInactive);
68 if organization.status != SpaceStatus::Active {
69 return Err(SpaceError::SpaceInactive);
7170 }
7271 if max_uses < 0 {
73 return Err(AuthError::MalformedRequest);
72 return Err(SpaceError::MalformedRequest);
7473 }
7574 if let Some(role_id) = role_id {
76 let role = organization_roles::Entity::find_by_id(role_id)
77 .filter(organization_roles::Column::OrganizationId.eq(organization_id))
78 .one(self.db().read())
75 let role = space_roles::Entity::find_by_id(role_id)
76 .filter(space_roles::Column::OrganizationId.eq(organization_id))
77 .one(self.db_conn().read())
7978 .await?
80 .ok_or(AuthError::OrganizationRoleNotFound)?;
79 .ok_or(SpaceError::SpaceRoleNotFound)?;
8180 if role.key == SYSTEM_ROLE_OWNER {
82 return Err(AuthError::Forbidden);
81 return Err(SpaceError::Forbidden);
8382 }
8483 }
8584 if !extra_role_ids.is_empty() {
@@ -87,13 +86,13 @@
8786 self.require_permission(actor_user_id, organization_id, "role.assign", request_id)
8887 .await?;
8988 for extra_role_id in extra_role_ids {
90 let role = organization_roles::Entity::find_by_id(*extra_role_id)
91 .filter(organization_roles::Column::OrganizationId.eq(organization_id))
92 .one(self.db().read())
89 let role = space_roles::Entity::find_by_id(*extra_role_id)
90 .filter(space_roles::Column::OrganizationId.eq(organization_id))
91 .one(self.db_conn().read())
9392 .await?
94 .ok_or(AuthError::OrganizationRoleNotFound)?;
93 .ok_or(SpaceError::SpaceRoleNotFound)?;
9594 if role.key == SYSTEM_ROLE_OWNER {
96 return Err(AuthError::Forbidden);
95 return Err(SpaceError::Forbidden);
9796 }
9897 }
9998 }
@@ -102,17 +101,17 @@
102101 .transpose()?;
103102 let method = match target_user_id {
104103 Some(user_id) => {
105 crate::models::users::users::Entity::find_by_id(user_id)
106 .filter(crate::models::users::users::Column::DeletedAt.is_null())
107 .one(self.db().read())
104 models::users::users::Entity::find_by_id(user_id)
105 .filter(models::users::users::Column::DeletedAt.is_null())
106 .one(self.db_conn().read())
108107 .await?
109 .ok_or(AuthError::UserNotFound)?;
110 OrgJoinMethod::InviteTargeted
108 .ok_or(SpaceError::UserNotFound)?;
109 SpaceJoinMethod::InviteTargeted
111110 }
112 None => OrgJoinMethod::InviteLink,
111 None => SpaceJoinMethod::InviteLink,
113112 };
114113 let method = if email.is_some() {
115 OrgJoinMethod::InviteEmail
114 SpaceJoinMethod::InviteEmail
116115 } else {
117116 method
118117 };
@@ -136,8 +135,8 @@
136135 .clamp(60, 90 * 24 * 60 * 60);
137136 let expires_at = Some(Utc::now() + chrono::Duration::seconds(ttl as i64));
138137
139 let txn = self.db().write().begin().await?;
140 let row = organization_invites::ActiveModel {
138 let txn = self.db_conn().write().begin().await?;
139 let row = space_invites::ActiveModel {
141140 organization_id: Set(organization_id),
142141 method: Set(method),
143142 token_hash: Set(token_hash),
@@ -161,10 +160,10 @@
161160 serde_json::json!({
162161 "id": row.id,
163162 "method": match row.method {
164 OrgJoinMethod::InviteLink => "invite_link",
165 OrgJoinMethod::InviteTargeted => "invite_targeted",
166 OrgJoinMethod::InviteEmail => "invite_email",
167 OrgJoinMethod::Direct => "direct",
163 SpaceJoinMethod::InviteLink => "invite_link",
164 SpaceJoinMethod::InviteTargeted => "invite_targeted",
165 SpaceJoinMethod::InviteEmail => "invite_email",
166 SpaceJoinMethod::Direct => "direct",
168167 },
169168 "target_user_id": target_user_id,
170169 "max_uses": max_uses,
@@ -181,7 +180,7 @@
181180 }
182181
183182 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, request_id = %request_id.unwrap_or_default()))]
184 pub async fn list_invites(
183 pub async fn admin_list_invites(
185184 &self,
186185 actor_user_id: i64,
187186 organization_id: Uuid,
@@ -189,38 +188,38 @@
189188 limit: u64,
190189 offset: u64,
191190 request_id: Option<&str>,
192 ) -> AuthResult<Vec<InviteSummary>> {
191 ) -> SpaceResult<Vec<InviteSummary>> {
193192 self.require_permission(actor_user_id, organization_id, "invite.view", request_id)
194193 .await?;
195 let mut query = organization_invites::Entity::find()
196 .filter(organization_invites::Column::OrganizationId.eq(organization_id));
194 let mut query = space_invites::Entity::find()
195 .filter(space_invites::Column::OrganizationId.eq(organization_id));
197196 if !include_revoked {
198 query = query.filter(organization_invites::Column::RevokedAt.is_null());
197 query = query.filter(space_invites::Column::RevokedAt.is_null());
199198 }
200199 let rows = query
201 .order_by_desc(organization_invites::Column::CreatedAt)
200 .order_by_desc(space_invites::Column::CreatedAt)
202201 .limit(limit)
203202 .offset(offset)
204 .all(self.db().read())
203 .all(self.db_conn().read())
205204 .await?;
206205 Ok(rows.iter().map(invite_summary).collect())
207206 }
208207
209208 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, invite_id = %invite_id, request_id = %request_id.unwrap_or_default()))]
210 pub async fn revoke_invite(
209 pub async fn admin_revoke_invite(
211210 &self,
212211 actor_user_id: i64,
213212 organization_id: Uuid,
214213 invite_id: Uuid,
215214 reason: Option<&str>,
216215 request_id: Option<&str>,
217 ) -> AuthResult<bool> {
216 ) -> SpaceResult<bool> {
218217 let reason = normalize_reason(reason)?;
219 let invite = organization_invites::Entity::find_by_id(invite_id)
220 .filter(organization_invites::Column::OrganizationId.eq(organization_id))
221 .one(self.db().read())
218 let invite = space_invites::Entity::find_by_id(invite_id)
219 .filter(space_invites::Column::OrganizationId.eq(organization_id))
220 .one(self.db_conn().read())
222221 .await?
223 .ok_or(AuthError::OrganizationInviteNotFound)?;
222 .ok_or(SpaceError::SpaceInviteNotFound)?;
224223 if invite.target_user_id != Some(actor_user_id) {
225224 self.require_permission(actor_user_id, organization_id, "invite.revoke", request_id)
226225 .await?;
@@ -229,13 +228,10 @@
229228 return Ok(false);
230229 }
231230
232 let txn = self.db().write().begin().await?;
233 organization_invites::Entity::update_many()
234 .col_expr(
235 organization_invites::Column::RevokedAt,
236 Expr::value(Utc::now()),
237 )
238 .filter(organization_invites::Column::Id.eq(invite_id))
231 let txn = self.db_conn().write().begin().await?;
232 space_invites::Entity::update_many()
233 .col_expr(space_invites::Column::RevokedAt, Expr::value(Utc::now()))
234 .filter(space_invites::Column::Id.eq(invite_id))
239235 .exec(&txn)
240236 .await?;
241237 let version = bump_version(&txn, organization_id).await?;
@@ -267,39 +263,39 @@
267263 actor_user_id: i64,
268264 token: &str,
269265 request_id: Option<&str>,
270 ) -> AuthResult<(Uuid, Uuid, bool)> {
266 ) -> SpaceResult<(Uuid, Uuid, bool)> {
271267 let trimmed = token.trim();
272268 if trimmed.is_empty() || trimmed.len() > 128 {
273 return Err(AuthError::OrganizationInviteNotFound);
269 return Err(SpaceError::SpaceInviteNotFound);
274270 }
275271 let token_hash = crypto::sha256_hex(trimmed.as_bytes());
276 let invite = organization_invites::Entity::find()
277 .filter(organization_invites::Column::TokenHash.eq(token_hash))
278 .one(self.db().read())
272 let invite = space_invites::Entity::find()
273 .filter(space_invites::Column::TokenHash.eq(token_hash))
274 .one(self.db_conn().read())
279275 .await?
280 .ok_or(AuthError::OrganizationInviteNotFound)?;
276 .ok_or(SpaceError::SpaceInviteNotFound)?;
281277 if invite.revoked_at.is_some() {
282 return Err(AuthError::OrganizationInviteExhausted);
278 return Err(SpaceError::SpaceInviteExhausted);
283279 }
284280 if let Some(expires_at) = invite.expires_at
285281 && expires_at <= Utc::now()
286282 {
287 return Err(AuthError::OrganizationInviteExpired);
283 return Err(SpaceError::SpaceInviteExpired);
288284 }
289285 if let Some(target_user_id) = invite.target_user_id
290286 && target_user_id != actor_user_id
291287 {
292 return Err(AuthError::Forbidden);
288 return Err(SpaceError::Forbidden);
293289 }
294290 let organization = self.load_organization(invite.organization_id).await?;
295 if organization.status != OrganizationStatus::Active {
296 return Err(AuthError::OrganizationInactive);
291 if organization.status != SpaceStatus::Active {
292 return Err(SpaceError::SpaceInactive);
297293 }
298 let actor = crate::models::users::users::Entity::find_by_id(actor_user_id)
299 .filter(crate::models::users::users::Column::DeletedAt.is_null())
300 .one(self.db().read())
294 let actor = models::users::users::Entity::find_by_id(actor_user_id)
295 .filter(models::users::users::Column::DeletedAt.is_null())
296 .one(self.db_conn().read())
301297 .await?
302 .ok_or(AuthError::UserNotFound)?;
298 .ok_or(SpaceError::UserNotFound)?;
303299 if let Some(email) = invite
304300 .email
305301 .as_deref()
@@ -307,28 +303,28 @@
307303 .filter(|value| !value.is_empty())
308304 && !actor.email.eq_ignore_ascii_case(email)
309305 {
310 return Err(AuthError::Forbidden);
306 return Err(SpaceError::Forbidden);
311307 }
312308
313 let txn = self.db().write().begin().await?;
314 let mut consume = organization_invites::Entity::update_many()
309 let txn = self.db_conn().write().begin().await?;
310 let mut consume = space_invites::Entity::update_many()
315311 .col_expr(
316 organization_invites::Column::Uses,
317 Expr::col(organization_invites::Column::Uses).add(1),
312 space_invites::Column::Uses,
313 Expr::col(space_invites::Column::Uses).add(1),
318314 )
319 .filter(organization_invites::Column::Id.eq(invite.id));
315 .filter(space_invites::Column::Id.eq(invite.id));
320316 if invite.max_uses > 0 {
321 consume = consume.filter(organization_invites::Column::Uses.lt(invite.max_uses));
317 consume = consume.filter(space_invites::Column::Uses.lt(invite.max_uses));
322318 }
323319 let consumed = consume.exec(&txn).await?;
324320 if consumed.rows_affected == 0 {
325 return Err(AuthError::OrganizationInviteExhausted);
321 return Err(SpaceError::SpaceInviteExhausted);
326322 }
327323
328324 let role = match invite.role_id {
329325 Some(role_id) => {
330 organization_roles::Entity::find_by_id(role_id)
331 .filter(organization_roles::Column::OrganizationId.eq(invite.organization_id))
326 space_roles::Entity::find_by_id(role_id)
327 .filter(space_roles::Column::OrganizationId.eq(invite.organization_id))
332328 .one(&txn)
333329 .await?
334330 }
@@ -336,16 +332,16 @@
336332 };
337333
338334 let method = match invite.method {
339 OrgJoinMethod::InviteTargeted => OrgJoinMethod::InviteTargeted,
340 OrgJoinMethod::InviteEmail => OrgJoinMethod::InviteEmail,
341 _ => OrgJoinMethod::InviteLink,
335 SpaceJoinMethod::InviteTargeted => SpaceJoinMethod::InviteTargeted,
336 SpaceJoinMethod::InviteEmail => SpaceJoinMethod::InviteEmail,
337 _ => SpaceJoinMethod::InviteLink,
342338 };
343339 let already = member::find_any(&txn, invite.organization_id, actor_user_id).await?;
344340 let (row, joined) = member::upsert_join(
345341 &txn,
346342 invite.organization_id,
347343 actor_user_id,
348 OrgMemberRole::Member,
344 SpaceMemberRole::Member,
349345 method,
350346 Some(invite.created_by),
351347 Some(invite.id),
@@ -354,21 +350,15 @@
354350 if let Some(role) = role
355351 && role.key != SYSTEM_ROLE_OWNER
356352 {
357 crate::services::organization::role::bind_role(
358 &txn,
359 row.id,
360 role.id,
361 invite.created_by,
362 )
363 .await?;
353 crate::services::role::bind_role(&txn, row.id, role.id, invite.created_by).await?;
364354 }
365355 if let Some(extra) = invite.extra_role_ids.as_deref() {
366356 for raw in extra.split(',') {
367357 let Ok(role_id) = Uuid::parse_str(raw.trim()) else {
368358 continue;
369359 };
370 let Some(role) = organization_roles::Entity::find_by_id(role_id)
371 .filter(organization_roles::Column::OrganizationId.eq(invite.organization_id))
360 let Some(role) = space_roles::Entity::find_by_id(role_id)
361 .filter(space_roles::Column::OrganizationId.eq(invite.organization_id))
372362 .one(&txn)
373363 .await?
374364 else {
@@ -377,24 +367,18 @@
377367 if role.key == SYSTEM_ROLE_OWNER || role.key == SYSTEM_ROLE_MEMBER {
378368 continue;
379369 }
380 crate::services::organization::role::bind_role(
381 &txn,
382 row.id,
383 role.id,
384 invite.created_by,
385 )
386 .await?;
370 crate::services::role::bind_role(&txn, row.id, role.id, invite.created_by).await?;
387371 }
388372 }
389373 let redeemed = already.is_none();
390374 if redeemed {
391 let existing = organization_invite_redemptions::Entity::find()
392 .filter(organization_invite_redemptions::Column::InviteId.eq(invite.id))
393 .filter(organization_invite_redemptions::Column::UserId.eq(actor_user_id))
375 let existing = space_invite_redemptions::Entity::find()
376 .filter(space_invite_redemptions::Column::InviteId.eq(invite.id))
377 .filter(space_invite_redemptions::Column::UserId.eq(actor_user_id))
394378 .one(&txn)
395379 .await?;
396380 if existing.is_none() {
397 organization_invite_redemptions::ActiveModel {
381 space_invite_redemptions::ActiveModel {
398382 invite_id: Set(invite.id),
399383 user_id: Set(actor_user_id),
400384 organization_id: Set(invite.organization_id),
@@ -430,11 +414,11 @@
430414 &self,
431415 db: &C,
432416 organization_id: Uuid,
433 ) -> AuthResult<Option<organization_roles::Model>> {
434 Ok(organization_roles::Entity::find()
435 .filter(organization_roles::Column::OrganizationId.eq(organization_id))
436 .filter(organization_roles::Column::DefaultRole.eq(true))
437 .order_by_asc(organization_roles::Column::Rank)
417 ) -> SpaceResult<Option<space_roles::Model>> {
418 Ok(space_roles::Entity::find()
419 .filter(space_roles::Column::OrganizationId.eq(organization_id))
420 .filter(space_roles::Column::DefaultRole.eq(true))
421 .order_by_asc(space_roles::Column::Rank)
438422 .one(db)
439423 .await?)
440424 }
@@ -447,8 +431,8 @@
447431 actor_user_id: i64,
448432 payload: serde_json::Value,
449433 _request_id: Option<&str>,
450 ) -> AuthResult<()> {
451 let labels = crate::events::Labels::new()
434 ) -> SpaceResult<()> {
435 let labels = events::Labels::new()
452436 .with(label::ORGANIZATION_ID, organization_id)
453437 .with(label::ACTOR_USER_ID, actor_user_id);
454438 self.emit_tx(
@@ -462,7 +446,7 @@
462446 }
463447}
464448
465pub(crate) fn invite_summary(row: &organization_invites::Model) -> InviteSummary {
449pub(crate) fn invite_summary(row: &space_invites::Model) -> InviteSummary {
466450 InviteSummary {
467451 id: row.id,
468452 organization_id: row.organization_id,
Deleted lib/space/src/services/invites.rs

Diff too large to display.

Renamed lib/auth/src/services/organization/join.rs → lib/space/src/services/join.rs View lib/space/src/services/join.rs
@@ -6,18 +6,19 @@
66};
77use uuid::Uuid;
88
9use crate::models::space::{
10 OrgJoinMethod, OrgMemberRole, OrganizationJoinRequestStatus, organization_join_requests,
11 organization_join_rules, organization_members, organization_roles,
9use crate::errors::{SpaceError, SpaceResult};
10use crate::services::service::SpaceService;
11use crate::services::types::{
12 AddMemberBody, JoinRequestSummary, JoinRuleSummary, SYSTEM_ROLE_MEMBER, SYSTEM_ROLE_OWNER,
1213};
13use crate::models::users::users;
14use crate::services::organization::types::{
15 AddMemberBody, JoinRequestSummary, JoinRuleSummary, SYSTEM_ROLE_MEMBER, SYSTEM_ROLE_OWNER,
14use crate::services::{member, service};
15use models::space::{
16 SpaceJoinMethod, SpaceJoinRequestState, SpaceMemberRole, space_join_requests, space_join_rules,
17 space_members, space_roles,
1618};
17use crate::services::organization::{member, service};
18use crate::{AuthError, AuthResult, AuthService};
19use models::users::users;
1920
20fn rule_summary(row: &organization_join_rules::Model) -> JoinRuleSummary {
21fn rule_summary(row: &space_join_rules::Model) -> JoinRuleSummary {
2122 JoinRuleSummary {
2223 id: row.id,
2324 organization_id: row.organization_id,
@@ -30,13 +31,13 @@
3031 }
3132}
3233
33impl AuthService {
34impl SpaceService {
3435 async fn join_request_summary(
3536 &self,
36 row: &organization_join_requests::Model,
37 ) -> AuthResult<JoinRequestSummary> {
37 row: &space_join_requests::Model,
38 ) -> SpaceResult<JoinRequestSummary> {
3839 let user = users::Entity::find_by_id(row.user_id)
39 .one(self.db().read())
40 .one(self.db_conn().read())
4041 .await?;
4142 Ok(JoinRequestSummary {
4243 id: row.id,
@@ -59,7 +60,7 @@
5960 // 加入规则的可选字段本身就多,参数表与 HTTP 请求体一一对应,拆结构体只会增加噪音。
6061 #[allow(clippy::too_many_arguments)]
6162 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, request_id = %request_id.unwrap_or_default()))]
62 pub async fn create_join_rule(
63 pub async fn admin_create_join_rule(
6364 &self,
6465 actor_user_id: i64,
6566 organization_id: Uuid,
@@ -68,20 +69,20 @@
6869 matcher: String,
6970 enabled: bool,
7071 request_id: Option<&str>,
71 ) -> AuthResult<JoinRuleSummary> {
72 ) -> SpaceResult<JoinRuleSummary> {
7273 self.require_permission(actor_user_id, organization_id, "role.update", request_id)
7374 .await?;
7475 let trigger = trigger.trim().to_string();
7576 let matcher = matcher.trim().to_string();
7677 if trigger.is_empty() || matcher.is_empty() {
77 return Err(AuthError::MalformedRequest);
78 return Err(SpaceError::MalformedRequest);
7879 }
79 let role = organization_roles::Entity::find_by_id(role_id)
80 .filter(organization_roles::Column::OrganizationId.eq(organization_id))
81 .one(self.db().read())
80 let role = space_roles::Entity::find_by_id(role_id)
81 .filter(space_roles::Column::OrganizationId.eq(organization_id))
82 .one(self.db_conn().read())
8283 .await?
83 .ok_or(AuthError::OrganizationRoleNotFound)?;
84 let row = organization_join_rules::ActiveModel {
84 .ok_or(SpaceError::SpaceRoleNotFound)?;
85 let row = space_join_rules::ActiveModel {
8586 organization_id: Set(organization_id),
8687 role_id: Set(role.id),
8788 trigger: Set(trigger),
@@ -89,16 +90,16 @@
8990 enabled: Set(enabled),
9091 ..Default::default()
9192 }
92 .insert(self.db().write())
93 .insert(self.db_conn().write())
9394 .await?;
94 service::bump_version(self.db().write(), organization_id).await?;
95 service::bump_version(self.db_conn().write(), organization_id).await?;
9596 Ok(rule_summary(&row))
9697 }
9798
9899 // 同上:可选字段多,保持与请求体一致的参数顺序。
99100 #[allow(clippy::too_many_arguments)]
100101 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, rule_id = %rule_id, request_id = %request_id.unwrap_or_default()))]
101 pub async fn update_join_rule(
102 pub async fn admin_update_join_rule(
102103 &self,
103104 actor_user_id: i64,
104105 organization_id: Uuid,
@@ -108,155 +109,152 @@
108109 matcher: Option<String>,
109110 enabled: Option<bool>,
110111 request_id: Option<&str>,
111 ) -> AuthResult<JoinRuleSummary> {
112 ) -> SpaceResult<JoinRuleSummary> {
112113 self.require_permission(actor_user_id, organization_id, "role.update", request_id)
113114 .await?;
114 let row = organization_join_rules::Entity::find_by_id(rule_id)
115 .filter(organization_join_rules::Column::OrganizationId.eq(organization_id))
116 .one(self.db().read())
115 let row = space_join_rules::Entity::find_by_id(rule_id)
116 .filter(space_join_rules::Column::OrganizationId.eq(organization_id))
117 .one(self.db_conn().read())
117118 .await?
118 .ok_or(AuthError::OrganizationNotFound)?;
119 let mut active: organization_join_rules::ActiveModel = row.into();
119 .ok_or(SpaceError::SpaceNotFound)?;
120 let mut active: space_join_rules::ActiveModel = row.into();
120121 if let Some(role_id) = role_id {
121 let role = organization_roles::Entity::find_by_id(role_id)
122 .filter(organization_roles::Column::OrganizationId.eq(organization_id))
123 .one(self.db().read())
122 let role = space_roles::Entity::find_by_id(role_id)
123 .filter(space_roles::Column::OrganizationId.eq(organization_id))
124 .one(self.db_conn().read())
124125 .await?
125 .ok_or(AuthError::OrganizationRoleNotFound)?;
126 .ok_or(SpaceError::SpaceRoleNotFound)?;
126127 active.role_id = Set(role.id);
127128 }
128129 if let Some(trigger) = trigger {
129130 let trigger = trigger.trim().to_string();
130131 if trigger.is_empty() {
131 return Err(AuthError::MalformedRequest);
132 return Err(SpaceError::MalformedRequest);
132133 }
133134 active.trigger = Set(trigger);
134135 }
135136 if let Some(matcher) = matcher {
136137 let matcher = matcher.trim().to_string();
137138 if matcher.is_empty() {
138 return Err(AuthError::MalformedRequest);
139 return Err(SpaceError::MalformedRequest);
139140 }
140141 active.matcher = Set(matcher);
141142 }
142143 if let Some(enabled) = enabled {
143144 active.enabled = Set(enabled);
144145 }
145 let updated = active.update(self.db().write()).await?;
146 service::bump_version(self.db().write(), organization_id).await?;
146 let updated = active.update(self.db_conn().write()).await?;
147 service::bump_version(self.db_conn().write(), organization_id).await?;
147148 Ok(rule_summary(&updated))
148149 }
149150
150151 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, rule_id = %rule_id, request_id = %request_id.unwrap_or_default()))]
151 pub async fn delete_join_rule(
152 pub async fn admin_delete_join_rule(
152153 &self,
153154 actor_user_id: i64,
154155 organization_id: Uuid,
155156 rule_id: Uuid,
156157 request_id: Option<&str>,
157 ) -> AuthResult<bool> {
158 ) -> SpaceResult<bool> {
158159 self.require_permission(actor_user_id, organization_id, "role.update", request_id)
159160 .await?;
160 let deleted = organization_join_rules::Entity::delete_many()
161 .filter(organization_join_rules::Column::Id.eq(rule_id))
162 .filter(organization_join_rules::Column::OrganizationId.eq(organization_id))
163 .exec(self.db().write())
161 let deleted = space_join_rules::Entity::delete_many()
162 .filter(space_join_rules::Column::Id.eq(rule_id))
163 .filter(space_join_rules::Column::OrganizationId.eq(organization_id))
164 .exec(self.db_conn().write())
164165 .await?;
165166 if deleted.rows_affected > 0 {
166 service::bump_version(self.db().write(), organization_id).await?;
167 service::bump_version(self.db_conn().write(), organization_id).await?;
167168 }
168169 Ok(deleted.rows_affected > 0)
169170 }
170171
171172 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, request_id = %request_id.unwrap_or_default()))]
172 pub async fn list_join_rules(
173 pub async fn admin_list_join_rules(
173174 &self,
174175 actor_user_id: i64,
175176 organization_id: Uuid,
176177 request_id: Option<&str>,
177 ) -> AuthResult<Vec<JoinRuleSummary>> {
178 ) -> SpaceResult<Vec<JoinRuleSummary>> {
178179 self.require_permission(actor_user_id, organization_id, "role.view", request_id)
179180 .await?;
180 let rows = organization_join_rules::Entity::find()
181 .filter(organization_join_rules::Column::OrganizationId.eq(organization_id))
182 .order_by_asc(organization_join_rules::Column::CreatedAt)
183 .all(self.db().read())
181 let rows = space_join_rules::Entity::find()
182 .filter(space_join_rules::Column::OrganizationId.eq(organization_id))
183 .order_by_asc(space_join_rules::Column::CreatedAt)
184 .all(self.db_conn().read())
184185 .await?;
185186 Ok(rows.iter().map(rule_summary).collect())
186187 }
187188
188189 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, request_id = %request_id.unwrap_or_default()))]
189 pub async fn create_join_request(
190 pub async fn admin_create_join_request(
190191 &self,
191192 actor_user_id: i64,
192193 organization_id: Uuid,
193194 note: Option<String>,
194195 request_id: Option<&str>,
195 ) -> AuthResult<JoinRequestSummary> {
196 ) -> SpaceResult<JoinRequestSummary> {
196197 let organization = self.load_organization(organization_id).await?;
197 if organization.status != crate::models::space::OrganizationStatus::Active {
198 return Err(AuthError::OrganizationInactive);
198 if organization.status != models::space::SpaceStatus::Active {
199 return Err(SpaceError::SpaceInactive);
199200 }
200201 if let Some(existing) =
201 member::find_any(self.db().read(), organization_id, actor_user_id).await?
202 member::find_any(self.db_conn().read(), organization_id, actor_user_id).await?
202203 {
203204 if existing.banned_at.is_some() {
204 return Err(AuthError::OrganizationMemberBanned);
205 return Err(SpaceError::SpaceMemberBanned);
205206 }
206207 if existing.left_at.is_none() {
207 return Err(AuthError::OrganizationMemberExists);
208 return Err(SpaceError::SpaceMemberExists);
208209 }
209210 }
210 if let Some(pending) = organization_join_requests::Entity::find()
211 .filter(organization_join_requests::Column::OrganizationId.eq(organization_id))
212 .filter(organization_join_requests::Column::UserId.eq(actor_user_id))
213 .filter(
214 organization_join_requests::Column::Status
215 .eq(OrganizationJoinRequestStatus::Pending),
216 )
217 .one(self.db().read())
211 if let Some(pending) = space_join_requests::Entity::find()
212 .filter(space_join_requests::Column::OrganizationId.eq(organization_id))
213 .filter(space_join_requests::Column::UserId.eq(actor_user_id))
214 .filter(space_join_requests::Column::Status.eq(SpaceJoinRequestState::Pending))
215 .one(self.db_conn().read())
218216 .await?
219217 {
220 let mut active: organization_join_requests::ActiveModel = pending.into();
218 let mut active: space_join_requests::ActiveModel = pending.into();
221219 active.note = Set(note);
222 let updated = active.update(self.db().write()).await?;
220 let updated = active.update(self.db_conn().write()).await?;
223221 return self.join_request_summary(&updated).await;
224222 }
225 let row = organization_join_requests::ActiveModel {
223 let row = space_join_requests::ActiveModel {
226224 organization_id: Set(organization_id),
227225 user_id: Set(actor_user_id),
228 status: Set(OrganizationJoinRequestStatus::Pending),
226 status: Set(SpaceJoinRequestState::Pending),
229227 note: Set(note),
230228 ..Default::default()
231229 }
232 .insert(self.db().write())
230 .insert(self.db_conn().write())
233231 .await?;
234232 self.join_request_summary(&row).await
235233 }
236234
237235 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, request_id = %request_id.unwrap_or_default()))]
238 pub async fn list_join_requests(
236 pub async fn admin_list_join_requests(
239237 &self,
240238 actor_user_id: i64,
241239 organization_id: Uuid,
242 status: Option<OrganizationJoinRequestStatus>,
240 status: Option<SpaceJoinRequestState>,
243241 limit: u64,
244242 offset: u64,
245243 request_id: Option<&str>,
246 ) -> AuthResult<(Vec<JoinRequestSummary>, u64)> {
244 ) -> SpaceResult<(Vec<JoinRequestSummary>, u64)> {
247245 self.require_permission(actor_user_id, organization_id, "member.view", request_id)
248246 .await?;
249 let mut select = organization_join_requests::Entity::find()
250 .filter(organization_join_requests::Column::OrganizationId.eq(organization_id));
247 let mut select = space_join_requests::Entity::find()
248 .filter(space_join_requests::Column::OrganizationId.eq(organization_id));
251249 if let Some(status) = status {
252 select = select.filter(organization_join_requests::Column::Status.eq(status));
250 select = select.filter(space_join_requests::Column::Status.eq(status));
253251 }
254 let total = select.clone().count(self.db().read()).await?;
252 let total = select.clone().count(self.db_conn().read()).await?;
255253 let rows = select
256 .order_by_desc(organization_join_requests::Column::CreatedAt)
254 .order_by_desc(space_join_requests::Column::CreatedAt)
257255 .limit(limit)
258256 .offset(offset)
259 .all(self.db().read())
257 .all(self.db_conn().read())
260258 .await?;
261259 let mut out = Vec::with_capacity(rows.len());
262260 for row in rows {
@@ -266,27 +264,27 @@
266264 }
267265
268266 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, join_request_id = %join_request_id, request_id = %request_id.unwrap_or_default()))]
269 pub async fn withdraw_join_request(
267 pub async fn admin_withdraw_join_request(
270268 &self,
271269 actor_user_id: i64,
272270 organization_id: Uuid,
273271 join_request_id: Uuid,
274272 request_id: Option<&str>,
275 ) -> AuthResult<JoinRequestSummary> {
276 let row = organization_join_requests::Entity::find_by_id(join_request_id)
277 .filter(organization_join_requests::Column::OrganizationId.eq(organization_id))
278 .one(self.db().read())
273 ) -> SpaceResult<JoinRequestSummary> {
274 let row = space_join_requests::Entity::find_by_id(join_request_id)
275 .filter(space_join_requests::Column::OrganizationId.eq(organization_id))
276 .one(self.db_conn().read())
279277 .await?
280 .ok_or(AuthError::OrganizationMemberNotFound)?;
278 .ok_or(SpaceError::SpaceMemberNotFound)?;
281279 if row.user_id != actor_user_id {
282 return Err(AuthError::Forbidden);
280 return Err(SpaceError::Forbidden);
283281 }
284 if row.status != OrganizationJoinRequestStatus::Pending {
285 return Err(AuthError::Forbidden);
282 if row.status != SpaceJoinRequestState::Pending {
283 return Err(SpaceError::Forbidden);
286284 }
287 let mut active: organization_join_requests::ActiveModel = row.into();
288 active.status = Set(OrganizationJoinRequestStatus::Withdrawn);
289 let updated = active.update(self.db().write()).await?;
285 let mut active: space_join_requests::ActiveModel = row.into();
286 active.status = Set(SpaceJoinRequestState::Withdrawn);
287 let updated = active.update(self.db_conn().write()).await?;
290288 self.join_request_summary(&updated).await
291289 }
292290
@@ -296,32 +294,32 @@
296294 actor_user_id: i64,
297295 organization_id: Uuid,
298296 join_request_id: Uuid,
299 status: OrganizationJoinRequestStatus,
297 status: SpaceJoinRequestState,
300298 note: Option<String>,
301299 request_id: Option<&str>,
302 ) -> AuthResult<JoinRequestSummary> {
300 ) -> SpaceResult<JoinRequestSummary> {
303301 self.require_permission(actor_user_id, organization_id, "member.add", request_id)
304302 .await?;
305 let row = organization_join_requests::Entity::find_by_id(join_request_id)
306 .filter(organization_join_requests::Column::OrganizationId.eq(organization_id))
307 .one(self.db().read())
303 let row = space_join_requests::Entity::find_by_id(join_request_id)
304 .filter(space_join_requests::Column::OrganizationId.eq(organization_id))
305 .one(self.db_conn().read())
308306 .await?
309 .ok_or(AuthError::OrganizationMemberNotFound)?;
310 if row.status != OrganizationJoinRequestStatus::Pending {
311 return Err(AuthError::Forbidden);
307 .ok_or(SpaceError::SpaceMemberNotFound)?;
308 if row.status != SpaceJoinRequestState::Pending {
309 return Err(SpaceError::Forbidden);
312310 }
313 if status == OrganizationJoinRequestStatus::Approved {
314 let default_role = organization_roles::Entity::find()
315 .filter(organization_roles::Column::OrganizationId.eq(organization_id))
316 .filter(organization_roles::Column::Key.eq(SYSTEM_ROLE_MEMBER))
317 .one(self.db().read())
311 if status == SpaceJoinRequestState::Approved {
312 let default_role = space_roles::Entity::find()
313 .filter(space_roles::Column::OrganizationId.eq(organization_id))
314 .filter(space_roles::Column::Key.eq(SYSTEM_ROLE_MEMBER))
315 .one(self.db_conn().read())
318316 .await?;
319 self.add_member(
317 self.admin_add_member(
320318 actor_user_id,
321319 organization_id,
322320 AddMemberBody {
323321 user_id: row.user_id,
324 role: Some(OrgMemberRole::Member),
322 role: Some(SpaceMemberRole::Member),
325323 role_keys: default_role
326324 .filter(|role| role.default_role)
327325 .map(|role| vec![role.key])
@@ -331,30 +329,30 @@
331329 )
332330 .await?;
333331 }
334 let mut active: organization_join_requests::ActiveModel = row.into();
332 let mut active: space_join_requests::ActiveModel = row.into();
335333 active.status = Set(status);
336334 active.decided_by = Set(Some(actor_user_id));
337335 active.decided_at = Set(Some(Utc::now()));
338336 if let Some(note) = note {
339337 active.note = Set(Some(note));
340338 }
341 let updated = active.update(self.db().write()).await?;
339 let updated = active.update(self.db_conn().write()).await?;
342340 self.join_request_summary(&updated).await
343341 }
344342
345343 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, join_request_id = %join_request_id, request_id = %request_id.unwrap_or_default()))]
346 pub async fn approve_join_request(
344 pub async fn admin_approve_join_request(
347345 &self,
348346 actor_user_id: i64,
349347 organization_id: Uuid,
350348 join_request_id: Uuid,
351349 request_id: Option<&str>,
352 ) -> AuthResult<JoinRequestSummary> {
350 ) -> SpaceResult<JoinRequestSummary> {
353351 self.finish_join_request(
354352 actor_user_id,
355353 organization_id,
356354 join_request_id,
357 OrganizationJoinRequestStatus::Approved,
355 SpaceJoinRequestState::Approved,
358356 None,
359357 request_id,
360358 )
@@ -362,19 +360,19 @@
362360 }
363361
364362 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, join_request_id = %join_request_id, request_id = %request_id.unwrap_or_default()))]
365 pub async fn reject_join_request(
363 pub async fn admin_reject_join_request(
366364 &self,
367365 actor_user_id: i64,
368366 organization_id: Uuid,
369367 join_request_id: Uuid,
370368 note: Option<String>,
371369 request_id: Option<&str>,
372 ) -> AuthResult<JoinRequestSummary> {
370 ) -> SpaceResult<JoinRequestSummary> {
373371 self.finish_join_request(
374372 actor_user_id,
375373 organization_id,
376374 join_request_id,
377 OrganizationJoinRequestStatus::Rejected,
375 SpaceJoinRequestState::Rejected,
378376 note,
379377 request_id,
380378 )
@@ -389,37 +387,37 @@
389387 role_keys: Vec<String>,
390388 role_ids: Vec<String>,
391389 request_id: Option<&str>,
392 ) -> AuthResult<(crate::services::organization::types::MemberSummary, bool)> {
393 let txn = self.db().write().begin().await?;
390 ) -> SpaceResult<(crate::services::types::MemberSummary, bool)> {
391 let txn = self.db_conn().write().begin().await?;
394392 // 机器人成员的角色上限:OWNER 只能由人类走成员面转移,不能通过内部接口直接塞入。
395 let user = crate::models::users::users::Entity::find_by_id(user_id)
396 .filter(crate::models::users::users::Column::DeletedAt.is_null())
393 let user = models::users::users::Entity::find_by_id(user_id)
394 .filter(models::users::users::Column::DeletedAt.is_null())
397395 .one(&txn)
398396 .await?
399 .ok_or(AuthError::UserNotFound)?;
397 .ok_or(SpaceError::UserNotFound)?;
400398 if !user.bot {
401 return Err(AuthError::Forbidden);
399 return Err(SpaceError::Forbidden);
402400 }
403401 let (row, created) = member::upsert_join(
404402 &txn,
405403 organization_id,
406404 user_id,
407 OrgMemberRole::Member,
408 OrgJoinMethod::Direct,
405 SpaceMemberRole::Member,
406 SpaceJoinMethod::Direct,
409407 None,
410408 None,
411409 )
412410 .await?;
413411 let mut roles = Vec::new();
414412 for key in role_keys {
415 if let Some(role) = organization_roles::Entity::find()
416 .filter(organization_roles::Column::OrganizationId.eq(organization_id))
417 .filter(organization_roles::Column::Key.eq(key))
413 if let Some(role) = space_roles::Entity::find()
414 .filter(space_roles::Column::OrganizationId.eq(organization_id))
415 .filter(space_roles::Column::Key.eq(key))
418416 .one(&txn)
419417 .await?
420418 {
421419 if role.key == SYSTEM_ROLE_OWNER {
422 return Err(AuthError::Forbidden);
420 return Err(SpaceError::Forbidden);
423421 }
424422 roles.push(role);
425423 }
@@ -428,27 +426,25 @@
428426 let Ok(id) = Uuid::parse_str(id.trim()) else {
429427 continue;
430428 };
431 if let Some(role) = organization_roles::Entity::find_by_id(id)
432 .filter(organization_roles::Column::OrganizationId.eq(organization_id))
429 if let Some(role) = space_roles::Entity::find_by_id(id)
430 .filter(space_roles::Column::OrganizationId.eq(organization_id))
433431 .one(&txn)
434432 .await?
435433 {
436434 if role.key == SYSTEM_ROLE_OWNER {
437 return Err(AuthError::Forbidden);
435 return Err(SpaceError::Forbidden);
438436 }
439437 roles.push(role);
440438 }
441439 }
442440 for role in roles {
443 let existing = crate::models::space::organization_member_roles::Entity::find()
444 .filter(
445 crate::models::space::organization_member_roles::Column::MemberId.eq(row.id),
446 )
447 .filter(crate::models::space::organization_member_roles::Column::RoleId.eq(role.id))
441 let existing = models::space::space_member_roles::Entity::find()
442 .filter(models::space::space_member_roles::Column::MemberId.eq(row.id))
443 .filter(models::space::space_member_roles::Column::RoleId.eq(role.id))
448444 .one(&txn)
449445 .await?;
450446 if existing.is_none() {
451 crate::models::space::organization_member_roles::ActiveModel {
447 models::space::space_member_roles::ActiveModel {
452448 member_id: Set(row.id),
453449 role_id: Set(role.id),
454450 ..Default::default()
@@ -470,14 +466,11 @@
470466 user_id: i64,
471467 request_id: Option<&str>,
472468 ) {
473 let _ = organization_members::Entity::update_many()
474 .col_expr(
475 organization_members::Column::LastActiveAt,
476 Expr::value(Utc::now()),
477 )
478 .filter(organization_members::Column::OrganizationId.eq(organization_id))
479 .filter(organization_members::Column::UserId.eq(user_id))
480 .exec(self.db().write())
469 let _ = space_members::Entity::update_many()
470 .col_expr(space_members::Column::LastActiveAt, Expr::value(Utc::now()))
471 .filter(space_members::Column::OrganizationId.eq(organization_id))
472 .filter(space_members::Column::UserId.eq(user_id))
473 .exec(self.db_conn().write())
481474 .await;
482475 }
483476}
Modified lib/space/src/services/join_requests.rs View lib/space/src/services/join_requests.rs
@@ -1,23 +1,22 @@
11use std::collections::{HashMap, HashSet};
22
3use models::space::space_join_request::{self, SpaceJoinRequestStatus};
4use models::space::space_members::MemberStatus;
5use models::space::spaces::{self, AllowJoinLevel};
6use models::space::{space_group_permissions, space_member_groups};
3use models::space::space_join_requests::{self, Entity as JoinRequests};
4use models::space::{
5 AllowJoinLevel, MemberStatus, SpaceJoinRequestState, space_group_permissions,
6 space_member_groups, spaces,
7};
78use models::users::user_notifications::{self, NotificationType};
8use sea_orm::sea_query::Expr;
9use sea_orm::{
10 ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder,
11 QuerySelect, Set, TransactionTrait,
12};
9use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set};
1310use serde::{Deserialize, Serialize};
1411use time::{Duration, OffsetDateTime};
1512use utoipa::{IntoParams, ToSchema};
1613use uuid::Uuid;
1714
18use crate::errors::{SpaceError, is_unique_violation};
15use crate::authority;
16use crate::errors::SpaceError;
17use crate::services::dto::{UserDto, resolve_space_image_url};
1918use crate::services::service::SpaceService;
20use crate::services::spaces::{UserDto, resolve_space_image_url};
19use crate::services::types::JoinRequestSummary;
2120use crate::share::authz::{MANAGE_MEMBERS, assert_not_banned, require_perm};
2221use crate::share::lookup;
2322
@@ -32,7 +31,7 @@
3231 pub space_id: Uuid,
3332 pub user: Option<UserDto>,
3433 pub note: Option<String>,
35 pub status: SpaceJoinRequestStatus,
34 pub status: SpaceJoinRequestState,
3635 pub decided_by: Option<UserDto>,
3736 pub decided_at: Option<OffsetDateTime>,
3837 pub created_at: OffsetDateTime,
@@ -53,7 +52,7 @@
5352
5453#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
5554pub struct ListJoinRequestsQuery {
56 pub status: Option<SpaceJoinRequestStatus>,
55 pub status: Option<SpaceJoinRequestState>,
5756 pub limit: Option<u64>,
5857 pub offset: Option<u64>,
5958}
@@ -69,7 +68,7 @@
6968#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
7069pub struct ExploreViewerRequestDto {
7170 pub id: Uuid,
72 pub status: SpaceJoinRequestStatus,
71 pub status: SpaceJoinRequestState,
7372 pub created_at: OffsetDateTime,
7473 pub decided_at: Option<OffsetDateTime>,
7574 #[serde(skip_serializing_if = "Option::is_none")]
@@ -97,20 +96,17 @@
9796 pub viewer: Option<ExploreViewerDto>,
9897}
9998
100fn join_request_dto(
101 row: space_join_request::Model,
102 users: &HashMap<i64, UserDto>,
103) -> JoinRequestDto {
99fn join_request_dto(row: JoinRequestSummary, users: &HashMap<i64, UserDto>) -> JoinRequestDto {
104100 JoinRequestDto {
105101 id: row.id,
106 space_id: row.space_id,
102 space_id: row.organization_id,
107103 user: users.get(&row.user_id).cloned(),
108104 note: row.note,
109105 status: row.status,
110106 decided_by: row.decided_by.and_then(|uid| users.get(&uid).cloned()),
111 decided_at: row.decided_at,
112 created_at: row.created_at,
113 updated_at: row.updated_at,
107 decided_at: authority::to_time_opt(row.decided_at),
108 created_at: authority::to_time(row.created_at),
109 updated_at: authority::to_time(row.updated_at),
114110 }
115111}
116112
@@ -120,7 +116,7 @@
120116 space: &spaces::Model,
121117 uid: i64,
122118 ) -> Result<(), SpaceError> {
123 match space.allow_join_level {
119 match AllowJoinLevel::from_i16(space.allow_join_level) {
124120 AllowJoinLevel::None => {}
125121 AllowJoinLevel::NeedMailActiveAndPhoneActive
126122 | AllowJoinLevel::NeedMailActive
@@ -133,7 +129,7 @@
133129 }
134130 let now = OffsetDateTime::now_utc();
135131 let created_at = crate::authority::account_created_at(&self.state, uid).await?;
136 match space.allow_join_level {
132 match AllowJoinLevel::from_i16(space.allow_join_level) {
137133 AllowJoinLevel::NeedMailActiveAndLastOneMid
138134 if created_at.is_some_and(|created| created > now - Duration::days(1)) =>
139135 {
@@ -162,7 +158,7 @@
162158
163159 async fn approver_uids(&self, space: &spaces::Model) -> Result<Vec<i64>, SpaceError> {
164160 let mut uids: HashSet<i64> = HashSet::new();
165 uids.insert(space.owner_id);
161 uids.insert(space.owner_user_id);
166162 let member_groups = space_member_groups::Entity::find()
167163 .filter(space_member_groups::Column::SpaceId.eq(space.id))
168164 .all(self.db().read())
@@ -189,7 +185,7 @@
189185 let candidate_set: HashSet<i64> = candidates.into_iter().collect();
190186 let active = crate::authority::list_members(
191187 &self.state,
192 space.owner_id,
188 space.owner_user_id,
193189 space.id,
194190 false,
195191 false,
@@ -241,37 +237,41 @@
241237 self.config().space.max_pagination,
242238 );
243239
244 let mut rows = crate::authority::list_public_spaces(
240 let rows = crate::authority::list_public_spaces(
245241 &self.state,
246242 query.q.as_deref().unwrap_or_default(),
247243 )
248244 .await?;
245 let mut ranked = Vec::with_capacity(rows.len());
246 for row in rows {
247 let count = crate::authority::member_count(&self.state, row.id).await?;
248 ranked.push((row, count));
249 }
249250 if query.sort.as_deref() == Some("newest") {
250 rows.sort_by(|a, b| b.created_at.cmp(&a.created_at));
251 ranked.sort_by(|a, b| b.0.created_at.cmp(&a.0.created_at));
251252 } else {
252 rows.sort_by(|a, b| {
253 b.nums_members
254 .cmp(&a.nums_members)
255 .then_with(|| b.created_at.cmp(&a.created_at))
253 ranked.sort_by(|a, b| {
254 b.1.cmp(&a.1)
255 .then_with(|| b.0.created_at.cmp(&a.0.created_at))
256256 });
257257 }
258 let total = rows.len() as i64;
259 let rows: Vec<spaces::Model> = rows
258 let total = ranked.len() as i64;
259 let rows: Vec<(spaces::Model, i64)> = ranked
260260 .into_iter()
261261 .skip(offset as usize)
262262 .take(limit as usize)
263263 .collect();
264264
265265 let mut memberships: HashSet<Uuid> = HashSet::new();
266 let mut latest_requests: HashMap<Uuid, space_join_request::Model> = HashMap::new();
266 let mut latest_requests: HashMap<Uuid, space_join_requests::Model> = HashMap::new();
267267 if let Some(uid) = viewer
268268 && !rows.is_empty()
269269 {
270 let space_ids: Vec<Uuid> = rows.iter().map(|s| s.id).collect();
270 let space_ids: Vec<Uuid> = rows.iter().map(|(space, _)| space.id).collect();
271271 let joined: HashSet<Uuid> = crate::authority::list_user_spaces(&self.state, uid, uid)
272272 .await?
273273 .into_iter()
274 .filter(|space| space.is_active)
274 .filter(crate::authority::space_is_active)
275275 .map(|space| space.id)
276276 .collect();
277277 memberships = space_ids
@@ -279,55 +279,65 @@
279279 .copied()
280280 .filter(|id| joined.contains(id))
281281 .collect();
282 let requests = space_join_request::Entity::find()
283 .filter(space_join_request::Column::UserId.eq(uid))
284 .filter(space_join_request::Column::SpaceId.is_in(space_ids))
285 .order_by_desc(space_join_request::Column::CreatedAt)
282 let requests = JoinRequests::find()
283 .filter(space_join_requests::Column::UserId.eq(uid))
284 .filter(space_join_requests::Column::OrganizationId.is_in(space_ids))
285 .order_by_desc(space_join_requests::Column::CreatedAt)
286286 .all(self.db().read())
287287 .await?;
288288 for row in requests {
289 latest_requests.entry(row.space_id).or_insert(row);
289 latest_requests.entry(row.organization_id).or_insert(row);
290290 }
291291 }
292292
293293 let cooldown_hours = self.config().space.join_request_cooldown_hours.max(0);
294 let items = rows
295 .into_iter()
296 .map(|s| {
297 let viewer = viewer.map(|_| ExploreViewerDto {
298 is_member: memberships.contains(&s.id),
299 request: latest_requests
300 .get(&s.id)
301 .map(|row| ExploreViewerRequestDto {
302 id: row.id,
303 status: row.status,
304 created_at: row.created_at,
305 decided_at: row.decided_at,
306 cooldown_until: if row.status == SpaceJoinRequestStatus::Rejected {
307 Some(
308 row.decided_at.unwrap_or(row.created_at)
309 + Duration::hours(cooldown_hours),
310 )
311 } else {
312 None
313 },
314 }),
315 });
316 ExploreSpaceDto {
317 id: s.id,
318 lower_name: s.lower_name.clone(),
319 name: s.name,
320 description: s.description,
321 avatar_url: resolve_space_image_url(&s.lower_name, "avatar", s.avatar_url),
322 banner_url: resolve_space_image_url(&s.lower_name, "banner", s.banner_url),
323 members_count: s.nums_members,
324 allow_join: s.allow_join,
325 allow_join_level: s.allow_join_level,
326 created_at: s.created_at,
327 viewer,
328 }
329 })
330 .collect();
294 let mut items = Vec::with_capacity(rows.len());
295 for (space, members_count) in rows {
296 let extras = crate::authority::extras(&self.state, space.id).await?;
297 let viewer = viewer.map(|_| ExploreViewerDto {
298 is_member: memberships.contains(&space.id),
299 request: latest_requests
300 .get(&space.id)
301 .map(|row| ExploreViewerRequestDto {
302 id: row.id,
303 status: row.status,
304 created_at: authority::to_time(row.created_at),
305 decided_at: authority::to_time_opt(row.decided_at),
306 cooldown_until: if row.status == SpaceJoinRequestState::Rejected {
307 Some(
308 authority::to_time(row.decided_at.unwrap_or(row.created_at))
309 + Duration::hours(cooldown_hours),
310 )
311 } else {
312 None
313 },
314 }),
315 });
316 items.push(ExploreSpaceDto {
317 id: space.id,
318 lower_name: space.slug.clone(),
319 name: space.name,
320 description: space.description,
321 avatar_url: resolve_space_image_url(
322 &space.slug,
323 "avatar",
324 extras
325 .as_ref()
326 .and_then(|row| row.avatar_url.clone())
327 .or_else(|| space.avatar_url.clone()),
328 ),
329 banner_url: resolve_space_image_url(
330 &space.slug,
331 "banner",
332 extras.and_then(|row| row.banner_url),
333 ),
334 members_count,
335 allow_join: space.allow_join,
336 allow_join_level: AllowJoinLevel::from_i16(space.allow_join_level),
337 created_at: authority::to_time(space.created_at),
338 viewer,
339 });
340 }
331341
332342 Ok(ExploreSpacePage {
333343 items,
@@ -359,7 +369,7 @@
359369 ));
360370 }
361371 let space = lookup::lookup_space_by_name(self, caller, space_name).await?;
362 if space.is_private || !space.allow_join {
372 if space.join_policy == models::space::SpaceJoinPolicy::Private || !space.allow_join {
363373 return Err(SpaceError::JoinNotAllowed(
364374 "this space does not accept join requests".to_string(),
365375 ));
@@ -368,22 +378,22 @@
368378 if let Some(member) =
369379 crate::authority::member(&self.state, caller, space.id, caller).await?
370380 {
371 if member.status == MemberStatus::Active {
381 if member.status() == MemberStatus::Active {
372382 return Err(SpaceError::Conflict(
373383 "user is already a member of this space".to_string(),
374384 ));
375385 }
376 if member.status == MemberStatus::Banned {
386 if member.status() == MemberStatus::Banned {
377387 return Err(SpaceError::JoinNotAllowed(
378388 "you are banned from this space".to_string(),
379389 ));
380390 }
381391 }
382392
383 let pending = space_join_request::Entity::find()
384 .filter(space_join_request::Column::SpaceId.eq(space.id))
385 .filter(space_join_request::Column::UserId.eq(caller))
386 .filter(space_join_request::Column::Status.eq(SpaceJoinRequestStatus::Pending))
393 let pending = JoinRequests::find()
394 .filter(space_join_requests::Column::OrganizationId.eq(space.id))
395 .filter(space_join_requests::Column::UserId.eq(caller))
396 .filter(space_join_requests::Column::Status.eq(SpaceJoinRequestState::Pending))
387397 .one(self.db().read())
388398 .await?;
389399 if pending.is_some() {
@@ -392,16 +402,16 @@
392402 ));
393403 }
394404
395 if let Some(last) = space_join_request::Entity::find()
396 .filter(space_join_request::Column::SpaceId.eq(space.id))
397 .filter(space_join_request::Column::UserId.eq(caller))
398 .filter(space_join_request::Column::Status.eq(SpaceJoinRequestStatus::Rejected))
399 .order_by_desc(space_join_request::Column::CreatedAt)
405 if let Some(last) = JoinRequests::find()
406 .filter(space_join_requests::Column::OrganizationId.eq(space.id))
407 .filter(space_join_requests::Column::UserId.eq(caller))
408 .filter(space_join_requests::Column::Status.eq(SpaceJoinRequestState::Rejected))
409 .order_by_desc(space_join_requests::Column::CreatedAt)
400410 .one(self.db().read())
401411 .await?
402412 {
403413 let cooldown_hours = self.config().space.join_request_cooldown_hours.max(0);
404 let decided_at = last.decided_at.unwrap_or(last.created_at);
414 let decided_at = authority::to_time(last.decided_at.unwrap_or(last.created_at));
405415 if OffsetDateTime::now_utc() < decided_at + Duration::hours(cooldown_hours) {
406416 return Err(SpaceError::JoinRequestCooldown(
407417 "a declined request must cool down before trying again".to_string(),
@@ -423,47 +433,13 @@
423433 )));
424434 }
425435
426 let client = self
427 .state
428 .kenauth
429 .as_ref()
430 .ok_or_else(|| SpaceError::Internal("kenoauth is not configured".to_string()))?;
431 let created = client
432 .create_join_request(
433 caller,
434 &space.id.to_string(),
435 note.as_deref().unwrap_or_default(),
436 )
436 let inserted = self
437 .admin_create_join_request(caller, space.id, note, None)
437438 .await?;
438 let request_id = Uuid::parse_str(&created.id).map_err(|_| {
439 SpaceError::Internal("kenoauth returned a non-uuid request".to_string())
440 })?;
441
442 let now = OffsetDateTime::now_utc();
443 let request_row = space_join_request::ActiveModel {
444 id: Set(request_id),
445 space_id: Set(space.id),
446 user_id: Set(caller),
447 status: Set(SpaceJoinRequestStatus::Pending),
448 note: Set(note),
449 decided_by: Set(None),
450 decided_at: Set(None),
451 created_at: Set(now),
452 updated_at: Set(now),
453 };
454 let inserted = match request_row.insert(self.db().write()).await {
455 Ok(row) => row,
456 Err(error) if is_unique_violation(&error) => {
457 return Err(SpaceError::Conflict(
458 "a join request is already pending".to_string(),
459 ));
460 }
461 Err(error) => return Err(error.into()),
462 };
463439
464440 let data = serde_json::json!({
465441 "space_id": space.id,
466 "space_name": space.lower_name,
442 "space_name": space.slug,
467443 "request_id": inserted.id,
468444 "applicant_id": caller,
469445 "kind": "join_request_submitted",
@@ -492,48 +468,21 @@
492468 request_id: Uuid,
493469 ) -> Result<(), SpaceError> {
494470 let space = lookup::lookup_space_by_name(self, caller, space_name).await?;
495 let request = space_join_request::Entity::find_by_id(request_id)
496 .filter(space_join_request::Column::SpaceId.eq(space.id))
471 let request = JoinRequests::find_by_id(request_id)
472 .filter(space_join_requests::Column::OrganizationId.eq(space.id))
497473 .one(self.db().read())
498474 .await?
499475 .ok_or(SpaceError::NotFound)?;
500476 if request.user_id != caller {
501477 return Err(SpaceError::NotFound);
502478 }
503 if request.status != SpaceJoinRequestStatus::Pending {
479 if request.status != SpaceJoinRequestState::Pending {
504480 return Err(SpaceError::Conflict(
505481 "join request is not pending".to_string(),
506482 ));
507483 }
508 let client = self
509 .state
510 .kenauth
511 .as_ref()
512 .ok_or_else(|| SpaceError::Internal("kenoauth is not configured".to_string()))?;
513 client
514 .withdraw_join_request(caller, &space.id.to_string(), &request_id.to_string())
515 .await?;
516
517 let now = OffsetDateTime::now_utc();
518 let updated = space_join_request::Entity::update_many()
519 .col_expr(
520 space_join_request::Column::Status,
521 Expr::value(SpaceJoinRequestStatus::Withdrawn),
522 )
523 .col_expr(
524 space_join_request::Column::DecidedAt,
525 Expr::value(Some(now)),
526 )
527 .col_expr(space_join_request::Column::UpdatedAt, Expr::value(now))
528 .filter(space_join_request::Column::Id.eq(request_id))
529 .filter(space_join_request::Column::Status.eq(SpaceJoinRequestStatus::Pending))
530 .exec(self.db().write())
484 self.admin_withdraw_join_request(caller, space.id, request_id, None)
531485 .await?;
532 if updated.rows_affected == 0 {
533 return Err(SpaceError::Conflict(
534 "join request is not pending".to_string(),
535 ));
536 }
537486 Ok(())
538487 }
539488
@@ -553,18 +502,8 @@
553502 self.config().space.max_pagination,
554503 );
555504
556 let mut select = space_join_request::Entity::find()
557 .filter(space_join_request::Column::SpaceId.eq(space.id));
558 if let Some(status) = query.status {
559 select = select.filter(space_join_request::Column::Status.eq(status));
560 }
561 select = select.order_by_desc(space_join_request::Column::CreatedAt);
562
563 let total = select.clone().count(self.db().read()).await? as i64;
564 let rows = select
565 .limit(limit)
566 .offset(offset)
567 .all(self.db().read())
505 let (rows, total) = self
506 .admin_list_join_requests(caller, space.id, query.status, limit, offset, None)
568507 .await?;
569508
570509 let mut uids: Vec<i64> = rows.iter().filter_map(|row| row.decided_by).collect();
@@ -576,7 +515,7 @@
576515 .into_iter()
577516 .map(|row| join_request_dto(row, &users))
578517 .collect(),
579 total,
518 total: total as i64,
580519 limit,
581520 offset,
582521 })
@@ -592,12 +531,12 @@
592531 let space = lookup::lookup_space_by_name(self, caller, space_name).await?;
593532 require_perm(self, &space, caller, MANAGE_MEMBERS).await?;
594533
595 let request = space_join_request::Entity::find_by_id(request_id)
596 .filter(space_join_request::Column::SpaceId.eq(space.id))
534 let request = JoinRequests::find_by_id(request_id)
535 .filter(space_join_requests::Column::OrganizationId.eq(space.id))
597536 .one(self.db().read())
598537 .await?
599538 .ok_or(SpaceError::NotFound)?;
600 if request.status != SpaceJoinRequestStatus::Pending {
539 if request.status != SpaceJoinRequestState::Pending {
601540 return Err(SpaceError::Conflict(
602541 "join request is not pending".to_string(),
603542 ));
@@ -612,64 +551,17 @@
612551 }
613552 }
614553
615 let client = self
616 .state
617 .kenauth
618 .as_ref()
619 .ok_or_else(|| SpaceError::Internal("kenoauth is not configured".to_string()))?;
620 let _decided_member = if approve {
621 let (_, member) = client
622 .approve_join_request(caller, &space.id.to_string(), &request_id.to_string())
623 .await?;
624 member
554 let updated = if approve {
555 self.admin_approve_join_request(caller, space.id, request_id, None)
556 .await?
625557 } else {
626 client
627 .reject_join_request(caller, &space.id.to_string(), &request_id.to_string(), "")
628 .await?;
629 None
558 self.admin_reject_join_request(caller, space.id, request_id, None, None)
559 .await?
630560 };
631561
632 let now = OffsetDateTime::now_utc();
633 let status = if approve {
634 SpaceJoinRequestStatus::Approved
635 } else {
636 SpaceJoinRequestStatus::Rejected
637 };
638 let space_id = space.id;
639562 let applicant_id = request.user_id;
640 self.db()
641 .primary()
642 .transaction::<_, (), SpaceError>(|txn| {
643 Box::pin(async move {
644 let updated = space_join_request::Entity::update_many()
645 .col_expr(space_join_request::Column::Status, Expr::value(status))
646 .col_expr(
647 space_join_request::Column::DecidedBy,
648 Expr::value(Some(caller)),
649 )
650 .col_expr(
651 space_join_request::Column::DecidedAt,
652 Expr::value(Some(now)),
653 )
654 .col_expr(space_join_request::Column::UpdatedAt, Expr::value(now))
655 .filter(space_join_request::Column::Id.eq(request_id))
656 .filter(
657 space_join_request::Column::Status.eq(SpaceJoinRequestStatus::Pending),
658 )
659 .exec(txn)
660 .await?;
661 if updated.rows_affected == 0 {
662 return Err(SpaceError::Conflict(
663 "join request is not pending".to_string(),
664 ));
665 }
666 Ok(())
667 })
668 })
669 .await?;
670
671563 if approve {
672 self.grant_public_room_memberships(space_id, applicant_id)
564 self.grant_public_room_memberships(space.id, applicant_id)
673565 .await;
674566 }
675567
@@ -681,7 +573,7 @@
681573 };
682574 let data = serde_json::json!({
683575 "space_id": space.id,
684 "space_name": space.lower_name,
576 "space_name": space.slug,
685577 "request_id": request_id,
686578 "applicant_id": applicant_id,
687579 "kind": if approve { "join_request_approved" } else { "join_request_rejected" },
@@ -690,12 +582,7 @@
690582 tracing::warn!(%error, %applicant_id, %request_id, "join request decision notification failed");
691583 }
692584
693 let updated = space_join_request::Entity::find_by_id(request_id)
694 .one(self.db().read())
695 .await?
696 .ok_or(SpaceError::NotFound)?;
697 let users: HashMap<i64, UserDto> =
698 self.load_user_briefs(&[updated.user_id, caller]).await?;
585 let users: HashMap<i64, UserDto> = self.load_user_briefs(&[applicant_id, caller]).await?;
699586 Ok(join_request_dto(updated, &users))
700587 }
701588
Modified lib/space/src/services/join_rules.rs View lib/space/src/services/join_rules.rs
@@ -1,16 +1,17 @@
1use sea_orm::{ActiveModelTrait, ColumnTrait, Condition, EntityTrait, QueryFilter, Set};
1use sea_orm::{ColumnTrait, Condition, EntityTrait, QueryFilter, TransactionTrait};
22use serde::{Deserialize, Serialize};
33use time::OffsetDateTime;
44use utoipa::{IntoParams, ToSchema};
55use uuid::Uuid;
66
7use models::space::space_join_rule::{self, SpaceJoinTrigger};
8use models::space::space_members::MemberStatus;
9use models::space::{space_bans, space_groups, space_member_groups};
7use models::space::space_join_rules::{self, Entity as JoinRules};
8use models::space::{SpaceJoinTrigger, space_member_roles, space_roles};
109
11use crate::errors::{SpaceError, is_unique_violation};
10use crate::authority;
11use crate::errors::SpaceError;
1212use crate::services::service::SpaceService;
13use crate::share::authz::{MANAGE_ROLES, effective_permissions, require_perm};
13use crate::services::types::JoinRuleSummary;
14use crate::share::authz::{MANAGE_ROLES, require_perm};
1415use crate::share::lookup;
1516
1617pub const MAX_JOIN_RULE_MATCHER_LEN: usize = 64;
@@ -18,31 +19,31 @@
1819#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
1920pub struct JoinRuleDto {
2021 pub id: Uuid,
21 pub group_id: Uuid,
22 pub role_id: Uuid,
2223 pub trigger: SpaceJoinTrigger,
2324 pub matcher: String,
2425 pub enabled: bool,
2526 pub created_at: OffsetDateTime,
26 pub updated_at: Option<OffsetDateTime>,
27 pub updated_at: OffsetDateTime,
2728}
2829
29impl From<space_join_rule::Model> for JoinRuleDto {
30 fn from(m: space_join_rule::Model) -> Self {
30impl From<JoinRuleSummary> for JoinRuleDto {
31 fn from(row: JoinRuleSummary) -> Self {
3132 Self {
32 id: m.id,
33 group_id: m.group_id,
34 trigger: m.trigger,
35 matcher: m.matcher,
36 enabled: m.enabled,
37 created_at: m.created_at,
38 updated_at: m.updated_at,
33 id: row.id,
34 role_id: row.role_id,
35 trigger: SpaceJoinTrigger::from_str(&row.trigger),
36 matcher: row.matcher,
37 enabled: row.enabled,
38 created_at: authority::to_time(row.created_at),
39 updated_at: authority::to_time(row.updated_at),
3940 }
4041 }
4142}
4243
4344#[derive(Debug, Clone, Deserialize, ToSchema)]
4445pub struct CreateJoinRuleCmd {
45 pub group_id: Uuid,
46 pub role_id: Uuid,
4647 #[serde(default)]
4748 pub trigger: SpaceJoinTrigger,
4849 #[serde(default)]
@@ -72,9 +73,8 @@
7273}
7374
7475/// Public entry point consumed by action sources (e.g. lib/issues reactions).
75/// Grants group membership for every enabled, matching join rule of the space.
76/// Idempotent: membership rows are unique per (space, user, group) and races
77/// resolve to a no-op via the unique-violation path.
76/// Grants the configured role for every enabled, matching join rule of the space.
77/// Idempotent: an already bound role is left untouched.
7878pub async fn apply_space_join_rules(
7979 state: &export::AppState,
8080 space_id: Uuid,
@@ -82,17 +82,16 @@
8282 trigger: SpaceJoinTrigger,
8383 matcher: &str,
8484) -> Result<(), SpaceError> {
85 let now = OffsetDateTime::now_utc();
8685 let matcher = matcher.trim();
8786
88 let rules = space_join_rule::Entity::find()
89 .filter(space_join_rule::Column::SpaceId.eq(space_id))
90 .filter(space_join_rule::Column::Trigger.eq(trigger))
91 .filter(space_join_rule::Column::Enabled.eq(true))
87 let rules = JoinRules::find()
88 .filter(space_join_rules::Column::OrganizationId.eq(space_id))
89 .filter(space_join_rules::Column::Trigger.eq(trigger.as_str()))
90 .filter(space_join_rules::Column::Enabled.eq(true))
9291 .filter(
9392 Condition::any()
94 .add(space_join_rule::Column::Matcher.eq(""))
95 .add(space_join_rule::Column::Matcher.eq(matcher)),
93 .add(space_join_rules::Column::Matcher.eq(""))
94 .add(space_join_rules::Column::Matcher.eq(matcher)),
9695 )
9796 .all(state.database.read())
9897 .await?;
@@ -100,78 +99,45 @@
10099 return Ok(());
101100 }
102101
103 if let Some(member) = crate::authority::member(state, user_id, space_id, user_id).await?
104 && member.status == MemberStatus::Active
105 {
106 // continue below
107 } else {
102 let Some(member) = crate::authority::member(state, user_id, space_id, user_id).await? else {
108103 return Ok(());
109 }
110 if let Some(ban) = space_bans::Entity::find_by_id((space_id, user_id))
111 .one(state.database.read())
112 .await?
113 && (ban.expires_at.is_none() || ban.expires_at > Some(now))
114 {
104 };
105 if member.status() != models::space::MemberStatus::Active {
115106 return Ok(());
116107 }
117108
109 let service = SpaceService::new(state.clone());
110 let txn = state.database.write().begin().await?;
111 let mut bound = false;
118112 for rule in &rules {
119 let group_exists = space_groups::Entity::find_by_id(rule.group_id)
120 .filter(space_groups::Column::SpaceId.eq(space_id))
121 .one(state.database.read())
122 .await?
123 .is_some();
124 if !group_exists {
113 let role = space_roles::Entity::find_by_id(rule.role_id)
114 .filter(space_roles::Column::OrganizationId.eq(space_id))
115 .one(&txn)
116 .await?;
117 let Some(role) = role else {
125118 continue;
126 }
127 let existing = space_member_groups::Entity::find_by_id((space_id, user_id, rule.group_id))
128 .one(state.database.read())
119 };
120 let existing = space_member_roles::Entity::find()
121 .filter(space_member_roles::Column::MemberId.eq(member.id))
122 .filter(space_member_roles::Column::RoleId.eq(role.id))
123 .one(&txn)
129124 .await?;
130125 if existing.is_some() {
131126 continue;
132127 }
133 if let Some(client) = export::identity_client::client() {
134 if let Err(error) = client
135 .ensure_bot_member(
136 &space_id.to_string(),
137 user_id,
138 Vec::new(),
139 vec![rule.group_id.to_string()],
140 )
141 .await
142 {
143 tracing::warn!(
144 %error,
145 %space_id,
146 %user_id,
147 group_id = %rule.group_id,
148 "kenoauth join-rule grant failed"
149 );
150 continue;
151 }
152 }
153 let row = space_member_groups::ActiveModel {
154 space_id: Set(space_id),
155 user_id: Set(user_id),
156 group_id: Set(rule.group_id),
157 created_at: Set(now),
158 };
159 match row.insert(state.database.write()).await {
160 Ok(_) => {}
161 Err(err) if is_unique_violation(&err) => {}
162 Err(err) => return Err(SpaceError::from(err)),
163 }
128 crate::services::role::bind_role(&txn, member.id, role.id, user_id).await?;
129 bound = true;
130 }
131 if bound {
132 crate::services::service::bump_version(&txn, space_id).await?;
164133 }
134 txn.commit().await?;
135 if bound {
136 service.invalidate_org(space_id).await?;
137 }
165138 Ok(())
166139}
167140
168fn trigger_name(value: SpaceJoinTrigger) -> &'static str {
169 match value {
170 SpaceJoinTrigger::IssueReaction => "issue_reaction",
171 SpaceJoinTrigger::IssueCommentReaction => "issue_comment_reaction",
172 }
173}
174
175141impl SpaceService {
176142 #[tracing::instrument(name = "space.create_join_rule", skip_all, fields(caller = %caller, space = %space_name))]
177143 pub async fn create_join_rule(
@@ -184,42 +150,18 @@
184150 require_perm(self, &space, caller, MANAGE_ROLES).await?;
185151 let matcher = normalize_matcher(&cmd.matcher)?;
186152
187 space_groups::Entity::find_by_id(cmd.group_id)
188 .filter(space_groups::Column::SpaceId.eq(space.id))
189 .one(self.db().read())
190 .await?
191 .ok_or(SpaceError::NotFound)?;
192
193 let client = self
194 .state
195 .kenauth
196 .as_ref()
197 .ok_or_else(|| SpaceError::Internal("kenoauth is not configured".to_string()))?;
198 let rule = client
199 .create_join_rule(
153 let rule = self
154 .admin_create_join_rule(
200155 caller,
201 &space.id.to_string(),
202 &cmd.group_id.to_string(),
203 trigger_name(cmd.trigger),
204 &matcher,
156 space.id,
157 cmd.role_id,
158 cmd.trigger.as_str().to_string(),
159 matcher,
205160 true,
161 None,
206162 )
207163 .await?;
208 let rule_id = Uuid::parse_str(&rule.id)
209 .map_err(|_| SpaceError::Internal("kenoauth returned a non-uuid rule".to_string()))?;
210 let now = OffsetDateTime::now_utc();
211 let row = space_join_rule::ActiveModel {
212 id: Set(rule_id),
213 space_id: Set(space.id),
214 group_id: Set(cmd.group_id),
215 trigger: Set(cmd.trigger),
216 matcher: Set(matcher),
217 enabled: Set(true),
218 created_at: Set(now),
219 updated_at: Set(None),
220 };
221 let inserted = row.insert(self.db().write()).await?;
222 Ok(JoinRuleDto::from(inserted))
164 Ok(rule.into())
223165 }
224166
225167 #[tracing::instrument(name = "space.update_join_rule", skip_all, fields(caller = %caller, space = %space_name, rule_id = %rule_id))]
@@ -232,46 +174,23 @@
232174 ) -> Result<JoinRuleDto, SpaceError> {
233175 let space = lookup::lookup_space_by_name(self, caller, space_name).await?;
234176 require_perm(self, &space, caller, MANAGE_ROLES).await?;
235 let Some(row) = space_join_rule::Entity::find_by_id(rule_id)
236 .filter(space_join_rule::Column::SpaceId.eq(space.id))
237 .one(self.db().read())
238 .await?
239 else {
240 return Err(SpaceError::NotFound);
241 };
242
243177 let matcher = match cmd.matcher {
244178 Some(raw) => Some(normalize_matcher(&raw)?),
245179 None => None,
246180 };
247 let client = self
248 .state
249 .kenauth
250 .as_ref()
251 .ok_or_else(|| SpaceError::Internal("kenoauth is not configured".to_string()))?;
252 client
253 .update_join_rule(
181 let rule = self
182 .admin_update_join_rule(
254183 caller,
255 &space.id.to_string(),
256 &rule_id.to_string(),
184 space.id,
185 rule_id,
257186 None,
258187 None,
259 matcher.clone(),
188 matcher,
260189 cmd.enabled,
190 None,
261191 )
262192 .await?;
263
264 let now = OffsetDateTime::now_utc();
265 let mut active: space_join_rule::ActiveModel = row.into();
266 if let Some(enabled) = cmd.enabled {
267 active.enabled = Set(enabled);
268 }
269 if let Some(matcher) = matcher {
270 active.matcher = Set(matcher);
271 }
272 active.updated_at = Set(Some(now));
273 let updated = active.update(self.db().write()).await?;
274 Ok(JoinRuleDto::from(updated))
193 Ok(rule.into())
275194 }
276195
277196 #[tracing::instrument(name = "space.delete_join_rule", skip_all, fields(caller = %caller, space = %space_name, rule_id = %rule_id))]
@@ -283,21 +202,10 @@
283202 ) -> Result<(), SpaceError> {
284203 let space = lookup::lookup_space_by_name(self, caller, space_name).await?;
285204 require_perm(self, &space, caller, MANAGE_ROLES).await?;
286 let client = self
287 .state
288 .kenauth
289 .as_ref()
290 .ok_or_else(|| SpaceError::Internal("kenoauth is not configured".to_string()))?;
291 client
292 .delete_join_rule(caller, &space.id.to_string(), &rule_id.to_string())
293 .await?;
294
295 let res = space_join_rule::Entity::delete_many()
296 .filter(space_join_rule::Column::Id.eq(rule_id))
297 .filter(space_join_rule::Column::SpaceId.eq(space.id))
298 .exec(self.db().write())
299 .await?;
300 if res.rows_affected == 0 {
205 if !self
206 .admin_delete_join_rule(caller, space.id, rule_id, None)
207 .await?
208 {
301209 return Err(SpaceError::NotFound);
302210 }
303211 Ok(())
@@ -309,11 +217,8 @@
309217 space_name: &str,
310218 ) -> Result<Vec<JoinRuleDto>, SpaceError> {
311219 let space = lookup::lookup_space_by_name(self, caller, space_name).await?;
312 effective_permissions(&self.state, &space, caller).await?;
313 let rows = space_join_rule::Entity::find()
314 .filter(space_join_rule::Column::SpaceId.eq(space.id))
315 .all(self.db().read())
316 .await?;
220 require_perm(self, &space, caller, MANAGE_ROLES).await?;
221 let rows = self.admin_list_join_rules(caller, space.id, None).await?;
317222 Ok(rows.into_iter().map(JoinRuleDto::from).collect())
318223 }
319224}
Renamed lib/auth/src/services/organization/member.rs → lib/space/src/services/member.rs View lib/space/src/services/member.rs
@@ -7,69 +7,66 @@
77};
88use uuid::Uuid;
99
10use crate::AuthService;
11use crate::errors::{AuthError, AuthResult};
12use crate::events::label;
13use crate::models::space::{
14 OrgJoinMethod, OrgMemberRole, organization_member_permissions, organization_members,
15};
16use crate::models::users::users;
17use crate::services::organization::role;
18use crate::services::organization::service::{bump_version, normalize_reason};
19use crate::services::organization::types::{
10use crate::errors::{SpaceError, SpaceResult};
11use crate::services::role;
12use crate::services::service::{SpaceService, bump_version, normalize_reason};
13use crate::services::types::{
2014 AddMemberBody, MAX_ALIAS_AVATAR_URL_LEN, MAX_ALIAS_NAME_LEN, MAX_REASON_LEN, MemberSummary,
2115 SetMemberRoleBody, UpdateBanBody, UpdateMemberBody, normalize_alias,
2216};
23
24pub(crate) async fn find_any<C: ConnectionTrait>(
17use events::label;
18use models::space::{SpaceJoinMethod, SpaceMemberRole, space_member_permissions, space_members};
19use models::users::users;
20
21pub async fn find_any<C: ConnectionTrait>(
2522 db: &C,
2623 organization_id: Uuid,
2724 user_id: i64,
28) -> AuthResult<Option<organization_members::Model>> {
29 Ok(organization_members::Entity::find()
30 .filter(organization_members::Column::OrganizationId.eq(organization_id))
31 .filter(organization_members::Column::UserId.eq(user_id))
25) -> SpaceResult<Option<space_members::Model>> {
26 Ok(space_members::Entity::find()
27 .filter(space_members::Column::OrganizationId.eq(organization_id))
28 .filter(space_members::Column::UserId.eq(user_id))
3229 .one(db)
3330 .await?)
3431}
3532
36pub(crate) async fn find_active<C: ConnectionTrait>(
33pub async fn find_active<C: ConnectionTrait>(
3734 db: &C,
3835 organization_id: Uuid,
3936 user_id: i64,
40) -> AuthResult<Option<organization_members::Model>> {
41 Ok(organization_members::Entity::find()
42 .filter(organization_members::Column::OrganizationId.eq(organization_id))
43 .filter(organization_members::Column::UserId.eq(user_id))
44 .filter(organization_members::Column::LeftAt.is_null())
45 .filter(organization_members::Column::BannedAt.is_null())
37) -> SpaceResult<Option<space_members::Model>> {
38 Ok(space_members::Entity::find()
39 .filter(space_members::Column::OrganizationId.eq(organization_id))
40 .filter(space_members::Column::UserId.eq(user_id))
41 .filter(space_members::Column::LeftAt.is_null())
42 .filter(space_members::Column::BannedAt.is_null())
4643 .one(db)
4744 .await?)
4845}
4946
50pub(crate) async fn load_by_id<C: ConnectionTrait>(
47pub async fn load_by_id<C: ConnectionTrait>(
5148 db: &C,
5249 organization_id: Uuid,
5350 member_id: Uuid,
54) -> AuthResult<organization_members::Model> {
55 organization_members::Entity::find_by_id(member_id)
56 .filter(organization_members::Column::OrganizationId.eq(organization_id))
51) -> SpaceResult<space_members::Model> {
52 space_members::Entity::find_by_id(member_id)
53 .filter(space_members::Column::OrganizationId.eq(organization_id))
5754 .one(db)
5855 .await?
59 .ok_or(AuthError::OrganizationMemberNotFound)
56 .ok_or(SpaceError::SpaceMemberNotFound)
6057}
6158
6259pub(crate) async fn insert_owner<C: ConnectionTrait>(
6360 db: &C,
6461 organization_id: Uuid,
6562 user_id: i64,
66) -> AuthResult<organization_members::Model> {
67 Ok(organization_members::ActiveModel {
63) -> SpaceResult<space_members::Model> {
64 Ok(space_members::ActiveModel {
6865 organization_id: Set(organization_id),
6966 user_id: Set(user_id),
70 role: Set(OrgMemberRole::Owner),
67 role: Set(SpaceMemberRole::Owner),
7168 joined_at: Set(Utc::now()),
72 join_method: Set(OrgJoinMethod::Direct),
69 join_method: Set(SpaceJoinMethod::Direct),
7370 ..Default::default()
7471 }
7572 .insert(db)
@@ -83,20 +80,20 @@
8380 db: &C,
8481 organization_id: Uuid,
8582 user_id: i64,
86 role: OrgMemberRole,
87 method: OrgJoinMethod,
83 role: SpaceMemberRole,
84 method: SpaceJoinMethod,
8885 invited_by: Option<i64>,
8986 invite_id: Option<Uuid>,
90) -> AuthResult<(organization_members::Model, bool)> {
87) -> SpaceResult<(space_members::Model, bool)> {
9188 match find_any(db, organization_id, user_id).await? {
9289 Some(existing) => {
9390 if existing.banned_at.is_some() {
94 return Err(AuthError::OrganizationMemberBanned);
91 return Err(SpaceError::SpaceMemberBanned);
9592 }
9693 if existing.left_at.is_none() {
9794 return Ok((existing, false));
9895 }
99 let mut active: organization_members::ActiveModel = existing.into();
96 let mut active: space_members::ActiveModel = existing.into();
10097 active.role = Set(role);
10198 active.joined_at = Set(Utc::now());
10299 active.left_at = Set(None);
@@ -108,7 +105,7 @@
108105 Ok((active.update(db).await?, true))
109106 }
110107 None => {
111 let row = organization_members::ActiveModel {
108 let row = space_members::ActiveModel {
112109 organization_id: Set(organization_id),
113110 user_id: Set(user_id),
114111 role: Set(role),
@@ -125,11 +122,11 @@
125122 }
126123}
127124
128impl AuthService {
125impl SpaceService {
129126 // 分页与过滤开关直接对应查询参数,拆结构体没有收益。
130127 #[allow(clippy::too_many_arguments)]
131128 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, request_id = %request_id.unwrap_or_default()))]
132 pub async fn list_members(
129 pub async fn admin_list_members(
133130 &self,
134131 actor_user_id: i64,
135132 organization_id: Uuid,
@@ -138,22 +135,22 @@
138135 limit: u64,
139136 offset: u64,
140137 request_id: Option<&str>,
141 ) -> AuthResult<Vec<MemberSummary>> {
138 ) -> SpaceResult<Vec<MemberSummary>> {
142139 self.require_permission(actor_user_id, organization_id, "member.view", request_id)
143140 .await?;
144 let mut query = organization_members::Entity::find()
145 .filter(organization_members::Column::OrganizationId.eq(organization_id));
141 let mut query = space_members::Entity::find()
142 .filter(space_members::Column::OrganizationId.eq(organization_id));
146143 if !include_inactive {
147 query = query.filter(organization_members::Column::LeftAt.is_null());
144 query = query.filter(space_members::Column::LeftAt.is_null());
148145 }
149146 if !include_banned {
150 query = query.filter(organization_members::Column::BannedAt.is_null());
147 query = query.filter(space_members::Column::BannedAt.is_null());
151148 }
152149 let rows = query
153 .order_by_asc(organization_members::Column::JoinedAt)
150 .order_by_asc(space_members::Column::JoinedAt)
154151 .limit(limit)
155152 .offset(offset)
156 .all(self.db().read())
153 .all(self.db_conn().read())
157154 .await?;
158155 let mut out = Vec::with_capacity(rows.len());
159156 for row in rows {
@@ -163,38 +160,39 @@
163160 }
164161
165162 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, request_id = %request_id.unwrap_or_default()))]
166 pub async fn add_member(
163 pub async fn admin_add_member(
167164 &self,
168165 actor_user_id: i64,
169166 organization_id: Uuid,
170167 body: AddMemberBody,
171168 request_id: Option<&str>,
172 ) -> AuthResult<(MemberSummary, bool)> {
169 ) -> SpaceResult<(MemberSummary, bool)> {
173170 self.require_permission(actor_user_id, organization_id, "member.add", request_id)
174171 .await?;
175 let role = body.role.unwrap_or(OrgMemberRole::Member);
176 if role == OrgMemberRole::Owner {
177 return Err(AuthError::Forbidden);
172 let role = body.role.unwrap_or(SpaceMemberRole::Member);
173 if role == SpaceMemberRole::Owner {
174 return Err(SpaceError::Forbidden);
178175 }
179176 // 角色绑定属于 `role.assign` 范畴:只有 `member.add` 的人不能把新成员直接绑进高权角色。
180177 if !body.role_keys.is_empty() {
181178 self.require_permission(actor_user_id, organization_id, "role.assign", request_id)
182179 .await?;
183180 }
184 let role_keys = role::load_keys(organization_id, &body.role_keys, self.db().read()).await?;
181 let role_keys =
182 role::load_keys(organization_id, &body.role_keys, self.db_conn().read()).await?;
185183 users::Entity::find_by_id(body.user_id)
186184 .filter(users::Column::DeletedAt.is_null())
187 .one(self.db().read())
185 .one(self.db_conn().read())
188186 .await?
189 .ok_or(AuthError::UserNotFound)?;
187 .ok_or(SpaceError::UserNotFound)?;
190188
191 let txn = self.db().write().begin().await?;
189 let txn = self.db_conn().write().begin().await?;
192190 let (row, created) = upsert_join(
193191 &txn,
194192 organization_id,
195193 body.user_id,
196194 role,
197 OrgJoinMethod::Direct,
195 SpaceJoinMethod::Direct,
198196 Some(actor_user_id),
199197 None,
200198 )
@@ -230,7 +228,7 @@
230228 }
231229
232230 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, user_id = user_id, request_id = %request_id.unwrap_or_default()))]
233 pub async fn remove_member(
231 pub async fn admin_remove_member(
234232 &self,
235233 actor_user_id: i64,
236234 organization_id: Uuid,
@@ -238,44 +236,41 @@
238236 user_id: i64,
239237 reason: Option<&str>,
240238 request_id: Option<&str>,
241 ) -> AuthResult<bool> {
239 ) -> SpaceResult<bool> {
242240 self.require_permission(actor_user_id, organization_id, "member.remove", request_id)
243241 .await?;
244242 let reason = normalize_reason(reason)?;
245243 let row = match member_id {
246 Some(id) => load_by_id(self.db().read(), organization_id, id).await?,
247 None => find_any(self.db().read(), organization_id, user_id)
244 Some(id) => load_by_id(self.db_conn().read(), organization_id, id).await?,
245 None => find_any(self.db_conn().read(), organization_id, user_id)
248246 .await?
249 .ok_or(AuthError::OrganizationMemberNotFound)?,
247 .ok_or(SpaceError::SpaceMemberNotFound)?,
250248 };
251 if row.role == OrgMemberRole::Owner {
252 return Err(AuthError::OrganizationOwnerRequired);
249 if row.role == SpaceMemberRole::Owner {
250 return Err(SpaceError::SpaceOwnerRequired);
253251 }
254252 if row.left_at.is_some() {
255253 return Ok(false);
256254 }
257255
258 let txn = self.db().write().begin().await?;
259 organization_members::Entity::update_many()
260 .col_expr(
261 organization_members::Column::LeftAt,
262 Expr::value(Utc::now()),
263 )
256 let txn = self.db_conn().write().begin().await?;
257 space_members::Entity::update_many()
258 .col_expr(space_members::Column::LeftAt, Expr::value(Utc::now()))
264259 .col_expr(
265 organization_members::Column::BanReason,
260 space_members::Column::BanReason,
266261 Expr::value(reason.clone()),
267262 )
268263 .col_expr(
269 organization_members::Column::Version,
270 Expr::col(organization_members::Column::Version).add(1),
264 space_members::Column::Version,
265 Expr::col(space_members::Column::Version).add(1),
271266 )
272 .filter(organization_members::Column::Id.eq(row.id))
267 .filter(space_members::Column::Id.eq(row.id))
273268 .exec(&txn)
274269 .await?;
275270 // 离组即失去全部授予:不清掉绑定,复活时会带着旧角色回来,等于悄悄恢复权限。
276271 role::clear_bindings(&txn, row.id).await?;
277 organization_member_permissions::Entity::delete_many()
278 .filter(organization_member_permissions::Column::MemberId.eq(row.id))
272 space_member_permissions::Entity::delete_many()
273 .filter(space_member_permissions::Column::MemberId.eq(row.id))
279274 .exec(&txn)
280275 .await?;
281276 let version = bump_version(&txn, organization_id).await?;
@@ -305,33 +300,30 @@
305300 actor_user_id: i64,
306301 organization_id: Uuid,
307302 request_id: Option<&str>,
308 ) -> AuthResult<bool> {
309 let row = find_any(self.db().read(), organization_id, actor_user_id)
303 ) -> SpaceResult<bool> {
304 let row = find_any(self.db_conn().read(), organization_id, actor_user_id)
310305 .await?
311 .ok_or(AuthError::OrganizationMemberNotFound)?;
312 if row.role == OrgMemberRole::Owner {
313 return Err(AuthError::OrganizationOwnerRequired);
306 .ok_or(SpaceError::SpaceMemberNotFound)?;
307 if row.role == SpaceMemberRole::Owner {
308 return Err(SpaceError::SpaceOwnerRequired);
314309 }
315310 if row.left_at.is_some() {
316311 return Ok(false);
317312 }
318313
319 let txn = self.db().write().begin().await?;
320 organization_members::Entity::update_many()
321 .col_expr(
322 organization_members::Column::LeftAt,
323 Expr::value(Utc::now()),
324 )
314 let txn = self.db_conn().write().begin().await?;
315 space_members::Entity::update_many()
316 .col_expr(space_members::Column::LeftAt, Expr::value(Utc::now()))
325317 .col_expr(
326 organization_members::Column::Version,
327 Expr::col(organization_members::Column::Version).add(1),
318 space_members::Column::Version,
319 Expr::col(space_members::Column::Version).add(1),
328320 )
329 .filter(organization_members::Column::Id.eq(row.id))
321 .filter(space_members::Column::Id.eq(row.id))
330322 .exec(&txn)
331323 .await?;
332324 role::clear_bindings(&txn, row.id).await?;
333 organization_member_permissions::Entity::delete_many()
334 .filter(organization_member_permissions::Column::MemberId.eq(row.id))
325 space_member_permissions::Entity::delete_many()
326 .filter(space_member_permissions::Column::MemberId.eq(row.id))
335327 .exec(&txn)
336328 .await?;
337329 let version = bump_version(&txn, organization_id).await?;
@@ -363,7 +355,7 @@
363355 member_id: Uuid,
364356 body: SetMemberRoleBody,
365357 request_id: Option<&str>,
366 ) -> AuthResult<MemberSummary> {
358 ) -> SpaceResult<MemberSummary> {
367359 self.require_permission(
368360 actor_user_id,
369361 organization_id,
@@ -371,37 +363,38 @@
371363 request_id,
372364 )
373365 .await?;
374 if body.role == OrgMemberRole::Owner {
375 return Err(AuthError::Forbidden);
366 if body.role == SpaceMemberRole::Owner {
367 return Err(SpaceError::Forbidden);
376368 }
377 let row = load_by_id(self.db().read(), organization_id, member_id).await?;
378 if row.role == OrgMemberRole::Owner {
379 return Err(AuthError::OrganizationOwnerRequired);
369 let row = load_by_id(self.db_conn().read(), organization_id, member_id).await?;
370 if row.role == SpaceMemberRole::Owner {
371 return Err(SpaceError::SpaceOwnerRequired);
380372 }
381373 if body.expected_version > 0 && row.version != body.expected_version {
382 return Err(AuthError::VersionMismatch {
374 return Err(SpaceError::VersionMismatch {
383375 current: row.version,
384376 });
385377 }
386378
387 let owner_role = role::find_by_key(organization_id, "owner", self.db().read()).await?;
388 let admin_role = role::find_by_key(organization_id, "admin", self.db().read()).await?;
389 let member_role = role::find_by_key(organization_id, "member", self.db().read()).await?;
379 let owner_role = role::find_by_key(organization_id, "owner", self.db_conn().read()).await?;
380 let admin_role = role::find_by_key(organization_id, "admin", self.db_conn().read()).await?;
381 let member_role =
382 role::find_by_key(organization_id, "member", self.db_conn().read()).await?;
390383
391 let txn = self.db().write().begin().await?;
392 organization_members::Entity::update_many()
393 .col_expr(organization_members::Column::Role, Expr::value(body.role))
384 let txn = self.db_conn().write().begin().await?;
385 space_members::Entity::update_many()
386 .col_expr(space_members::Column::Role, Expr::value(body.role))
394387 .col_expr(
395 organization_members::Column::Version,
396 Expr::col(organization_members::Column::Version).add(1),
388 space_members::Column::Version,
389 Expr::col(space_members::Column::Version).add(1),
397390 )
398 .filter(organization_members::Column::Id.eq(member_id))
391 .filter(space_members::Column::Id.eq(member_id))
399392 .exec(&txn)
400393 .await?;
401394 // 粗粒度角色与系统角色绑定必须同步,否则解析链上会出现"粗粒度是 ADMIN、
402395 // 绑定里还留着 member"这种自相矛盾的组合。
403396 let target = match body.role {
404 OrgMemberRole::Admin => admin_role.clone(),
397 SpaceMemberRole::Admin => admin_role.clone(),
405398 _ => member_role.clone(),
406399 };
407400 for system in [owner_role, admin_role, member_role].into_iter().flatten() {
@@ -442,43 +435,42 @@
442435 member_id: Uuid,
443436 reason: Option<&str>,
444437 request_id: Option<&str>,
445 ) -> AuthResult<MemberSummary> {
438 ) -> SpaceResult<MemberSummary> {
446439 self.require_permission(actor_user_id, organization_id, "member.ban", request_id)
447440 .await?;
448441 let reason = match reason.map(str::trim) {
449442 None | Some("") => None,
450 Some(value) if value.len() > MAX_REASON_LEN => return Err(AuthError::MalformedRequest),
443 Some(value) if value.len() > MAX_REASON_LEN => {
444 return Err(SpaceError::MalformedRequest);
445 }
451446 Some(value) => Some(value.to_owned()),
452447 };
453 let row = load_by_id(self.db().read(), organization_id, member_id).await?;
454 if row.role == OrgMemberRole::Owner {
455 return Err(AuthError::OrganizationOwnerRequired);
448 let row = load_by_id(self.db_conn().read(), organization_id, member_id).await?;
449 if row.role == SpaceMemberRole::Owner {
450 return Err(SpaceError::SpaceOwnerRequired);
456451 }
457452 if row.banned_at.is_some() {
458453 return self.member_summary(&row).await;
459454 }
460455
461 let txn = self.db().write().begin().await?;
462 organization_members::Entity::update_many()
463 .col_expr(
464 organization_members::Column::BannedAt,
465 Expr::value(Utc::now()),
466 )
456 let txn = self.db_conn().write().begin().await?;
457 space_members::Entity::update_many()
458 .col_expr(space_members::Column::BannedAt, Expr::value(Utc::now()))
467459 .col_expr(
468 organization_members::Column::BanReason,
460 space_members::Column::BanReason,
469461 Expr::value(reason.clone()),
470462 )
471463 .col_expr(
472 organization_members::Column::Version,
473 Expr::col(organization_members::Column::Version).add(1),
464 space_members::Column::Version,
465 Expr::col(space_members::Column::Version).add(1),
474466 )
475 .filter(organization_members::Column::Id.eq(member_id))
467 .filter(space_members::Column::Id.eq(member_id))
476468 .exec(&txn)
477469 .await?;
478470 // 封禁优先于一切授权,清空授予让"解封后权限自己回来"不可能发生。
479471 role::clear_bindings(&txn, member_id).await?;
480 organization_member_permissions::Entity::delete_many()
481 .filter(organization_member_permissions::Column::MemberId.eq(member_id))
472 space_member_permissions::Entity::delete_many()
473 .filter(space_member_permissions::Column::MemberId.eq(member_id))
482474 .exec(&txn)
483475 .await?;
484476 let version = bump_version(&txn, organization_id).await?;
@@ -512,56 +504,47 @@
512504 organization_id: Uuid,
513505 member_id: Uuid,
514506 request_id: Option<&str>,
515 ) -> AuthResult<MemberSummary> {
507 ) -> SpaceResult<MemberSummary> {
516508 self.require_permission(actor_user_id, organization_id, "member.ban", request_id)
517509 .await?;
518 let row = load_by_id(self.db().read(), organization_id, member_id).await?;
510 let row = load_by_id(self.db_conn().read(), organization_id, member_id).await?;
519511 if row.banned_at.is_none() {
520512 return self.member_summary(&row).await;
521513 }
522514
523 let txn = self.db().write().begin().await?;
515 let txn = self.db_conn().write().begin().await?;
524516 // 解封必须把封禁的全部附带状态一起清掉,否则残留的到期时间与四个 allow_* 位会继续生效。
525 let unlocked = organization_members::Entity::update_many()
517 let unlocked = space_members::Entity::update_many()
526518 .col_expr(
527 organization_members::Column::BannedAt,
519 space_members::Column::BannedAt,
528520 Expr::value(None::<chrono::DateTime<Utc>>),
529521 )
530522 .col_expr(
531 organization_members::Column::BanReason,
523 space_members::Column::BanReason,
532524 Expr::value(None::<String>),
533525 )
534526 .col_expr(
535 organization_members::Column::BanExpiresAt,
527 space_members::Column::BanExpiresAt,
536528 Expr::value(None::<chrono::DateTime<Utc>>),
537 )
538 .col_expr(
539 organization_members::Column::BanAllowAgentJoin,
540 Expr::value(false),
541529 )
530 .col_expr(space_members::Column::BanAllowAgentJoin, Expr::value(false))
531 .col_expr(space_members::Column::BanAllowSpeak, Expr::value(false))
532 .col_expr(space_members::Column::BanAllowPushRepo, Expr::value(false))
542533 .col_expr(
543 organization_members::Column::BanAllowSpeak,
534 space_members::Column::BanAllowCommentAny,
544535 Expr::value(false),
545536 )
546537 .col_expr(
547 organization_members::Column::BanAllowPushRepo,
548 Expr::value(false),
549 )
550 .col_expr(
551 organization_members::Column::BanAllowCommentAny,
552 Expr::value(false),
553 )
554 .col_expr(
555 organization_members::Column::Version,
556 Expr::col(organization_members::Column::Version).add(1),
538 space_members::Column::Version,
539 Expr::col(space_members::Column::Version).add(1),
557540 )
558 .filter(organization_members::Column::Id.eq(member_id))
559 .filter(organization_members::Column::BannedAt.is_not_null())
541 .filter(space_members::Column::Id.eq(member_id))
542 .filter(space_members::Column::BannedAt.is_not_null())
560543 .exec(&txn)
561544 .await?;
562545 if unlocked.rows_affected != 1 {
563546 txn.rollback().await?;
564 return Err(AuthError::VersionMismatch {
547 return Err(SpaceError::VersionMismatch {
565548 current: row.version,
566549 });
567550 }
@@ -589,35 +572,35 @@
589572
590573 /// 按会籍 id 或 user_id 读会籍;两者都提供时以 member_id 为准。
591574 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, user_id = user_id, request_id = %request_id.unwrap_or_default()))]
592 pub async fn get_member(
575 pub async fn admin_get_member(
593576 &self,
594577 actor_user_id: i64,
595578 organization_id: Uuid,
596579 member_id: Option<Uuid>,
597580 user_id: i64,
598581 request_id: Option<&str>,
599 ) -> AuthResult<MemberSummary> {
582 ) -> SpaceResult<MemberSummary> {
600583 self.require_permission(actor_user_id, organization_id, "member.view", request_id)
601584 .await?;
602585 let row = match member_id {
603 Some(id) => load_by_id(self.db().read(), organization_id, id).await?,
604 None => find_any(self.db().read(), organization_id, user_id)
586 Some(id) => load_by_id(self.db_conn().read(), organization_id, id).await?,
587 None => find_any(self.db_conn().read(), organization_id, user_id)
605588 .await?
606 .ok_or(AuthError::OrganizationMemberNotFound)?,
589 .ok_or(SpaceError::SpaceMemberNotFound)?,
607590 };
608591 self.member_summary(&row).await
609592 }
610593
611594 /// 管理端更新会籍:别名、头像、通知偏好、免打扰。封禁中的会籍只走 `update_ban`。
612595 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, member_id = %member_id, request_id = %request_id.unwrap_or_default()))]
613 pub async fn update_member(
596 pub async fn admin_update_member(
614597 &self,
615598 actor_user_id: i64,
616599 organization_id: Uuid,
617600 member_id: Uuid,
618601 body: UpdateMemberBody,
619602 request_id: Option<&str>,
620 ) -> AuthResult<MemberSummary> {
603 ) -> SpaceResult<MemberSummary> {
621604 self.require_permission(
622605 actor_user_id,
623606 organization_id,
@@ -644,10 +627,10 @@
644627 organization_id: Uuid,
645628 body: UpdateMemberBody,
646629 request_id: Option<&str>,
647 ) -> AuthResult<MemberSummary> {
648 let row = find_active(self.db().read(), organization_id, actor_user_id)
630 ) -> SpaceResult<MemberSummary> {
631 let row = find_active(self.db_conn().read(), organization_id, actor_user_id)
649632 .await?
650 .ok_or(AuthError::Forbidden)?;
633 .ok_or(SpaceError::Forbidden)?;
651634 self.apply_member_update(
652635 actor_user_id,
653636 organization_id,
@@ -667,28 +650,28 @@
667650 body: UpdateMemberBody,
668651 action: &'static str,
669652 request_id: Option<&str>,
670 ) -> AuthResult<MemberSummary> {
653 ) -> SpaceResult<MemberSummary> {
671654 // 读取、版本校验与写回必须在同一事务内,否则并发封禁/离组/改角色会被旧快照覆盖。
672 let txn = self.db().write().begin().await?;
655 let txn = self.db_conn().write().begin().await?;
673656 let row = load_by_id(&txn, organization_id, member_id).await?;
674657 if body.expected_version > 0 && row.version != body.expected_version {
675658 txn.rollback().await?;
676 return Err(AuthError::VersionMismatch {
659 return Err(SpaceError::VersionMismatch {
677660 current: row.version,
678661 });
679662 }
680663 if action == "update" {
681 if row.role == OrgMemberRole::Owner {
664 if row.role == SpaceMemberRole::Owner {
682665 txn.rollback().await?;
683 return Err(AuthError::OrganizationOwnerRequired);
666 return Err(SpaceError::SpaceOwnerRequired);
684667 }
685668 if row.banned_at.is_some() {
686669 txn.rollback().await?;
687 return Err(AuthError::Forbidden);
670 return Err(SpaceError::Forbidden);
688671 }
689672 } else if row.banned_at.is_some() || row.left_at.is_some() {
690673 txn.rollback().await?;
691 return Err(AuthError::Forbidden);
674 return Err(SpaceError::Forbidden);
692675 }
693676 let alias_name = match body.alias_name {
694677 None => row.alias_name.clone(),
@@ -713,51 +696,42 @@
713696 let suppress_roles = body.suppress_roles.unwrap_or(row.suppress_roles);
714697 let hide_muted_channels = body.hide_muted_channels.unwrap_or(row.hide_muted_channels);
715698 // 条件写回:只有版本未变时才生效,防止旧快照覆盖并发已提交的封禁/离组/角色变更。
716 let written = organization_members::Entity::update_many()
699 let written = space_members::Entity::update_many()
717700 .col_expr(
718 organization_members::Column::AliasName,
701 space_members::Column::AliasName,
719702 Expr::value(alias_name.clone()),
720703 )
721704 .col_expr(
722 organization_members::Column::AliasAvatarUrl,
705 space_members::Column::AliasAvatarUrl,
723706 Expr::value(alias_avatar_url.clone()),
724707 )
725708 .col_expr(
726 organization_members::Column::MessageNotifications,
709 space_members::Column::MessageNotifications,
727710 Expr::value(message_notifications),
728711 )
729 .col_expr(organization_members::Column::Muted, Expr::value(muted))
712 .col_expr(space_members::Column::Muted, Expr::value(muted))
713 .col_expr(space_members::Column::MuteUntil, Expr::value(mute_until))
714 .col_expr(space_members::Column::MobilePush, Expr::value(mobile_push))
730715 .col_expr(
731 organization_members::Column::MuteUntil,
732 Expr::value(mute_until),
733 )
734 .col_expr(
735 organization_members::Column::MobilePush,
736 Expr::value(mobile_push),
737 )
738 .col_expr(
739 organization_members::Column::SuppressEveryone,
716 space_members::Column::SuppressEveryone,
740717 Expr::value(suppress_everyone),
741718 )
742719 .col_expr(
743 organization_members::Column::SuppressRoles,
720 space_members::Column::SuppressRoles,
744721 Expr::value(suppress_roles),
745722 )
746723 .col_expr(
747 organization_members::Column::HideMutedChannels,
724 space_members::Column::HideMutedChannels,
748725 Expr::value(hide_muted_channels),
749726 )
750 .col_expr(
751 organization_members::Column::Version,
752 Expr::value(row.version + 1),
753 )
754 .filter(organization_members::Column::Id.eq(row.id))
755 .filter(organization_members::Column::Version.eq(row.version))
727 .col_expr(space_members::Column::Version, Expr::value(row.version + 1))
728 .filter(space_members::Column::Id.eq(row.id))
729 .filter(space_members::Column::Version.eq(row.version))
756730 .exec(&txn)
757731 .await?;
758732 if written.rows_affected != 1 {
759733 txn.rollback().await?;
760 return Err(AuthError::VersionMismatch {
734 return Err(SpaceError::VersionMismatch {
761735 current: row.version,
762736 });
763737 }
@@ -788,23 +762,23 @@
788762
789763 /// 封禁列表:只看 `banned_at` 非空的会籍。
790764 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, request_id = %request_id.unwrap_or_default()))]
791 pub async fn list_bans(
765 pub async fn admin_list_bans(
792766 &self,
793767 actor_user_id: i64,
794768 organization_id: Uuid,
795769 limit: u64,
796770 offset: u64,
797771 request_id: Option<&str>,
798 ) -> AuthResult<Vec<MemberSummary>> {
772 ) -> SpaceResult<Vec<MemberSummary>> {
799773 self.require_permission(actor_user_id, organization_id, "member.ban", request_id)
800774 .await?;
801 let rows = organization_members::Entity::find()
802 .filter(organization_members::Column::OrganizationId.eq(organization_id))
803 .filter(organization_members::Column::BannedAt.is_not_null())
804 .order_by_desc(organization_members::Column::BannedAt)
775 let rows = space_members::Entity::find()
776 .filter(space_members::Column::OrganizationId.eq(organization_id))
777 .filter(space_members::Column::BannedAt.is_not_null())
778 .order_by_desc(space_members::Column::BannedAt)
805779 .limit(limit)
806780 .offset(offset)
807 .all(self.db().read())
781 .all(self.db_conn().read())
808782 .await?;
809783 let mut out = Vec::with_capacity(rows.len());
810784 for row in rows {
@@ -814,41 +788,41 @@
814788 }
815789
816790 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, user_id = user_id, request_id = %request_id.unwrap_or_default()))]
817 pub async fn get_ban(
791 pub async fn admin_get_ban(
818792 &self,
819793 actor_user_id: i64,
820794 organization_id: Uuid,
821795 user_id: i64,
822796 request_id: Option<&str>,
823 ) -> AuthResult<MemberSummary> {
797 ) -> SpaceResult<MemberSummary> {
824798 self.require_permission(actor_user_id, organization_id, "member.ban", request_id)
825799 .await?;
826 let row = find_any(self.db().read(), organization_id, user_id)
800 let row = find_any(self.db_conn().read(), organization_id, user_id)
827801 .await?
828 .ok_or(AuthError::OrganizationMemberNotFound)?;
802 .ok_or(SpaceError::SpaceMemberNotFound)?;
829803 if row.banned_at.is_none() {
830 return Err(AuthError::OrganizationMemberNotFound);
804 return Err(SpaceError::SpaceMemberNotFound);
831805 }
832806 self.member_summary(&row).await
833807 }
834808
835809 /// 改封禁原因、到期时间与四个放行位;会籍必须处于封禁中。
836810 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, user_id = user_id, request_id = %request_id.unwrap_or_default()))]
837 pub async fn update_ban(
811 pub async fn admin_update_ban(
838812 &self,
839813 actor_user_id: i64,
840814 organization_id: Uuid,
841815 user_id: i64,
842816 body: UpdateBanBody,
843817 request_id: Option<&str>,
844 ) -> AuthResult<MemberSummary> {
818 ) -> SpaceResult<MemberSummary> {
845819 self.require_permission(actor_user_id, organization_id, "member.ban", request_id)
846820 .await?;
847 let row = find_any(self.db().read(), organization_id, user_id)
821 let row = find_any(self.db_conn().read(), organization_id, user_id)
848822 .await?
849 .ok_or(AuthError::OrganizationMemberNotFound)?;
823 .ok_or(SpaceError::SpaceMemberNotFound)?;
850824 if row.banned_at.is_none() {
851 return Err(AuthError::OrganizationMemberNotFound);
825 return Err(SpaceError::SpaceMemberNotFound);
852826 }
853827 let reason = match body.reason {
854828 None => row.ban_reason.clone(),
@@ -857,7 +831,7 @@
857831 Some(value) => {
858832 let trimmed = value.trim();
859833 if trimmed.len() > MAX_REASON_LEN {
860 return Err(AuthError::MalformedRequest);
834 return Err(SpaceError::MalformedRequest);
861835 }
862836 Some(trimmed.to_owned()).filter(|value| !value.is_empty())
863837 }
@@ -868,8 +842,8 @@
868842 Some(value) => value,
869843 };
870844
871 let txn = self.db().write().begin().await?;
872 let mut active: organization_members::ActiveModel = row.clone().into();
845 let txn = self.db_conn().write().begin().await?;
846 let mut active: space_members::ActiveModel = row.clone().into();
873847 active.ban_reason = Set(reason.clone());
874848 active.ban_expires_at = Set(expires_at);
875849 if let Some(value) = body.allow_agent_join {
@@ -918,8 +892,8 @@
918892 actor_user_id: i64,
919893 payload: serde_json::Value,
920894 _request_id: Option<&str>,
921 ) -> AuthResult<()> {
922 let labels = crate::events::Labels::new()
895 ) -> SpaceResult<()> {
896 let labels = events::Labels::new()
923897 .with(label::ORGANIZATION_ID, organization_id)
924898 .with(label::ACTOR_USER_ID, actor_user_id);
925899 self.emit_tx(
@@ -934,6 +908,6 @@
934908}
935909
936910/// 别名/别名头像的规范化:空白视为清空,超长按 400 处理。
937fn normalize_alias_field(value: String, max: usize) -> AuthResult<Option<String>> {
938 normalize_alias(&value, "alias", max).map_err(|_| AuthError::MalformedRequest)
911fn normalize_alias_field(value: String, max: usize) -> SpaceResult<Option<String>> {
912 normalize_alias(&value, "alias", max).map_err(|_| SpaceError::MalformedRequest)
939913}
Deleted lib/space/src/services/members.rs

Diff too large to display.

Modified lib/space/src/services/mod.rs View lib/space/src/services/mod.rs
@@ -1,11 +1,17 @@
1pub mod bans;
21pub mod dir;
2pub mod dto;
3pub mod error;
34pub mod groups;
45pub mod images;
5pub mod invites;
6pub mod invite;
7pub mod join;
68pub mod join_requests;
79pub mod join_rules;
8pub mod members;
10pub mod member;
11pub mod permission;
12pub mod role;
913pub mod rooms;
1014pub mod service;
11pub mod spaces;
15pub mod types;
16
17pub use types::*;
Renamed lib/auth/src/services/organization/permission.rs → lib/space/src/services/permission.rs View lib/space/src/services/permission.rs
@@ -4,20 +4,20 @@
44use serde::{Deserialize, Serialize};
55use uuid::Uuid;
66
7use crate::AuthService;
8use crate::errors::{AuthError, AuthResult};
9use crate::models::space::{
10 OrgMemberRole, PermissionEffect, organization_member_permissions, organization_member_roles,
11 organization_members, organization_role_permissions,
12};
13use crate::services::organization::types::{
7use crate::errors::{SpaceError, SpaceResult};
8use crate::services::service::SpaceService;
9use crate::services::types::{
1410 PERMISSION_CACHE_TTL_SECS, PERMISSIONS, PermissionCheck, Resolution, SYSTEM_ROLE_ADMIN,
1511 SYSTEM_ROLE_OWNER, is_admin_permission, is_known_permission,
1612};
17use crate::services::organization::{member, role, service};
13use crate::services::{member, role, service};
14use models::space::{
15 PermissionEffect, SpaceMemberRole, space_member_permissions, space_member_roles, space_members,
16 space_role_permissions,
17};
1818
1919/// 缓存进 Redis 的判定核心。版本号相同的组织共享同一份核心,
20/// 任何写操作推进 `organizations.version` 都会让整份核心失效。
20/// 任何写操作推进 `spaces.version` 都会让整份核心失效。
2121#[derive(Clone, Debug, Default, Serialize, Deserialize)]
2222pub(crate) struct Kernel {
2323 pub version: i32,
@@ -39,20 +39,20 @@
3939pub(crate) async fn load_kernel<C: ConnectionTrait>(
4040 db: &C,
4141 version: i32,
42 membership: &organization_members::Model,
43) -> AuthResult<Kernel> {
44 let role_ids = organization_member_roles::Entity::find()
42 membership: &space_members::Model,
43) -> SpaceResult<Kernel> {
44 let role_ids = space_member_roles::Entity::find()
4545 .select_only()
46 .column(organization_member_roles::Column::RoleId)
47 .filter(organization_member_roles::Column::MemberId.eq(membership.id))
46 .column(space_member_roles::Column::RoleId)
47 .filter(space_member_roles::Column::MemberId.eq(membership.id))
4848 .into_tuple::<Uuid>()
4949 .all(db)
5050 .await?;
5151 let role_keys = role::keys_of_ids(db, &role_ids).await?;
52 let is_owner = membership.role == OrgMemberRole::Owner
52 let is_owner = membership.role == SpaceMemberRole::Owner
5353 || role_keys.iter().any(|key| key == SYSTEM_ROLE_OWNER);
5454 let is_admin = is_owner
55 || membership.role == OrgMemberRole::Admin
55 || membership.role == SpaceMemberRole::Admin
5656 || role_keys.iter().any(|key| key == SYSTEM_ROLE_ADMIN);
5757
5858 let mut kernel = Kernel {
@@ -64,8 +64,8 @@
6464 };
6565
6666 if !role_ids.is_empty() {
67 let grants = organization_role_permissions::Entity::find()
68 .filter(organization_role_permissions::Column::RoleId.is_in(role_ids))
67 let grants = space_role_permissions::Entity::find()
68 .filter(space_role_permissions::Column::RoleId.is_in(role_ids))
6969 .all(db)
7070 .await?;
7171 for grant in grants {
@@ -79,8 +79,8 @@
7979 }
8080 }
8181
82 let direct = organization_member_permissions::Entity::find()
83 .filter(organization_member_permissions::Column::MemberId.eq(membership.id))
82 let direct = space_member_permissions::Entity::find()
83 .filter(space_member_permissions::Column::MemberId.eq(membership.id))
8484 .all(db)
8585 .await?;
8686 for grant in direct {
@@ -132,12 +132,12 @@
132132}
133133
134134async fn cached_kernel(
135 svc: &AuthService,
135 svc: &SpaceService,
136136 organization_id: Uuid,
137137 user_id: i64,
138138 version: i32,
139 membership: &organization_members::Model,
140) -> AuthResult<Kernel> {
139 membership: &space_members::Model,
140) -> SpaceResult<Kernel> {
141141 let key = service::perm_cache_prefix(organization_id, user_id);
142142 if let Ok(Some(bytes)) = svc.cache().get_bytes(&key).await
143143 && let Ok(kernel) = serde_json::from_slice::<Kernel>(&bytes)
@@ -201,14 +201,14 @@
201201}
202202
203203async fn gate(
204 svc: &AuthService,
204 svc: &SpaceService,
205205 organization_id: Uuid,
206206 user_id: i64,
207207 _request_id: Option<&str>,
208) -> AuthResult<Gate> {
208) -> SpaceResult<Gate> {
209209 let organization = svc.load_organization(organization_id).await?;
210210 let version = organization.version;
211 if organization.status != crate::models::space::OrganizationStatus::Active {
211 if organization.status != models::space::SpaceStatus::Active {
212212 return Ok(Gate::Denied {
213213 version,
214214 reason: "org_inactive",
@@ -248,17 +248,17 @@
248248/// → 角色 Allow/Deny → 成员直挂 Allow/Deny。
249249#[tracing::instrument(skip_all, fields(organization_id = %organization_id, user_id = user_id, permission = %permission, request_id = %request_id.unwrap_or_default()))]
250250pub(crate) async fn resolve(
251 svc: &AuthService,
251 svc: &SpaceService,
252252 organization_id: Uuid,
253253 user_id: i64,
254254 permission: &str,
255255 request_id: Option<&str>,
256) -> AuthResult<Resolution> {
256) -> SpaceResult<Resolution> {
257257 let gate = gate(svc, organization_id, user_id, request_id).await?;
258258 Ok(gate.resolution(permission))
259259}
260260
261impl AuthService {
261impl SpaceService {
262262 /// 平台管理员越过会籍判定,但仍受组织状态约束。
263263 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, user_id = user_id, request_id = %request_id.unwrap_or_default()))]
264264 pub async fn check_permission(
@@ -268,10 +268,10 @@
268268 user_id: i64,
269269 permission: &str,
270270 request_id: Option<&str>,
271 ) -> AuthResult<PermissionCheck> {
271 ) -> SpaceResult<PermissionCheck> {
272272 let target = if user_id == 0 { actor_user_id } else { user_id };
273273 if target != actor_user_id && !self.is_platform_admin(actor_user_id).await? {
274 return Err(AuthError::Forbidden);
274 return Err(SpaceError::Forbidden);
275275 }
276276 let gate = gate(self, organization_id, target, request_id).await?;
277277 let resolution = gate.resolution(permission.trim());
@@ -292,10 +292,10 @@
292292 user_id: i64,
293293 permissions: &[String],
294294 request_id: Option<&str>,
295 ) -> AuthResult<Vec<PermissionCheck>> {
295 ) -> SpaceResult<Vec<PermissionCheck>> {
296296 let target = if user_id == 0 { actor_user_id } else { user_id };
297297 if target != actor_user_id && !self.is_platform_admin(actor_user_id).await? {
298 return Err(AuthError::Forbidden);
298 return Err(SpaceError::Forbidden);
299299 }
300300 let gate = gate(self, organization_id, target, request_id).await?;
301301 let version = gate.version();
@@ -317,8 +317,8 @@
317317/// 列表展示用的有效权限集,复用同一条解析链。
318318pub(crate) async fn effective<C: ConnectionTrait>(
319319 db: &C,
320 membership: &organization_members::Model,
321) -> AuthResult<Vec<String>> {
320 membership: &space_members::Model,
321) -> SpaceResult<Vec<String>> {
322322 let kernel = load_kernel(db, 0, membership).await?;
323323 Ok(allowed_list(&kernel))
324324}
Renamed lib/auth/src/services/organization/role.rs → lib/space/src/services/role.rs View lib/space/src/services/role.rs

Diff too large to display.

Modified lib/space/src/services/rooms.rs View lib/space/src/services/rooms.rs
@@ -106,7 +106,7 @@
106106 }
107107 let space = lookup::lookup_space_by_name(self, caller, space_name).await?;
108108 require_perm(self, &space, caller, MANAGE_CHANNELS).await?;
109 if !space.is_active {
109 if !authority::space_is_active(&space) {
110110 return Err(SpaceError::NotFound);
111111 }
112112 if !matches!(
@@ -238,6 +238,7 @@
238238 "avatar_url": export::resolve_user_avatar_url(user.user_id, &user.avatar_url),
239239 })
240240 });
241 let space_avatar = crate::authority::space_avatar(&self.state, &space).await?;
241242 let request_id = export::middleware::request_id();
242243 events::outbox::enqueue_chat_users(
243244 self.db().write(),
@@ -260,9 +261,9 @@
260261 },
261262 "space": {
262263 "id": space.id,
263 "lower_name": &space.lower_name,
264 "lower_name": &space.slug,
264265 "name": &space.name,
265 "avatar_url": &space.avatar_url,
266 "avatar_url": &space_avatar,
266267 },
267268 "actor": actor,
268269 }),
@@ -289,7 +290,7 @@
289290 };
290291 let space = authority::space_by_id(&self.state, caller, space_id).await?;
291292 require_perm(self, &space, caller, MANAGE_CHANNELS).await?;
292 if !space.is_active {
293 if !authority::space_is_active(&space) {
293294 return Err(SpaceError::NotFound);
294295 }
295296
@@ -318,6 +319,7 @@
318319 am.updated_at = Set(now);
319320 am.update(self.db().write()).await?;
320321
322 let space_avatar = crate::authority::space_avatar(&self.state, &space).await?;
321323 let request_id = export::middleware::request_id();
322324 events::outbox::enqueue_room(
323325 self.db().write(),
@@ -329,9 +331,9 @@
329331 "room": room_detail,
330332 "space": {
331333 "id": space.id,
332 "lower_name": &space.lower_name,
334 "lower_name": &space.slug,
333335 "name": &space.name,
334 "avatar_url": &space.avatar_url,
336 "avatar_url": &space_avatar,
335337 },
336338 "actor": actor,
337339 }),
Modified lib/space/src/services/service.rs View lib/space/src/services/service.rs
@@ -1,25 +1,133 @@
1use std::sync::Arc;
2
1use crate::errors::{SpaceError, SpaceResult};
2use crate::services::{error, member, permission, role};
3use cache::Cache;
4use chrono::Utc;
35use config::AppConfig;
6use config::auth::AuthConfig;
7use db::DbConn;
8use events::{EventHandler, Labels, label};
49use export::{AppState, Database};
10use models::room;
11use models::room::room_members;
12use models::users::users;
13use sea_orm::sea_query::{Expr, ExprTrait};
14use sea_orm::{
15 ActiveModelTrait, ColumnTrait, Condition, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder,
16 QuerySelect, Set, TransactionTrait,
17};
18use serde::Serialize;
19use std::sync::Arc;
520use storage::BlobStore;
21use time::OffsetDateTime;
22use uuid::Uuid;
623
7use crate::errors::SpaceError;
24async fn actor_details(
25 conn: &impl sea_orm::ConnectionTrait,
26 labels: &Labels,
27) -> Option<serde_json::Value> {
28 let uid = labels.get(label::USER_ID)?.parse::<i64>().ok()?;
29 let user = models::users::users::Entity::find_by_id(uid)
30 .one(conn)
31 .await
32 .ok()??;
33 Some(serde_json::json!({
34 "id": user.id,
35 "username": user.username,
36 "display_name": user.displayname,
37 "email": user.email,
38 "is_admin": user.is_admin,
39 "bot": user.bot,
40 }))
41}
842
943#[derive(Clone)]
1044pub struct SpaceService {
1145 pub state: AppState,
46 db_conn: DbConn,
1247}
1348
1449impl SpaceService {
1550 pub fn new(state: AppState) -> Self {
16 Self { state }
51 let db_conn = DbConn::new(
52 state.database.primary().clone(),
53 state.database.replicas().to_vec(),
54 );
55 Self { state, db_conn }
56 }
57
58 pub fn db_conn(&self) -> &DbConn {
59 &self.db_conn
60 }
61
62 pub fn cache(&self) -> &Cache {
63 &self.state.cache
64 }
65
66 pub fn events(&self) -> Option<&EventHandler> {
67 self.state.events.as_ref()
68 }
69
70 pub async fn emit_tx<T>(
71 &self,
72 conn: &impl sea_orm::ConnectionTrait,
73 kind: &str,
74 labels: Labels,
75 payload: T,
76 request_id: Option<&str>,
77 ) -> Result<(), SpaceError>
78 where
79 T: Serialize,
80 {
81 let trace_id = request_id
82 .map(str::to_owned)
83 .or_else(envelope::context::request_id)
84 .unwrap_or_default();
85 let mut payload = serde_json::to_value(&payload).unwrap_or(serde_json::Value::Null);
86 if let Some(actor) = actor_details(conn, &labels).await {
87 payload["actor"] = actor;
88 }
89 events::outbox::enqueue_at(
90 conn,
91 kind,
92 labels,
93 &payload,
94 &trace_id,
95 chrono::Utc::now(),
96 request_id,
97 )
98 .await?;
99 Ok(())
17100 }
18101
19102 pub fn config(&self) -> &AppConfig {
20103 &self.state.config
21104 }
22105
106 pub fn auth(&self) -> &AuthConfig {
107 &self.state.config.auth
108 }
109
110 pub async fn require_session(
111 &self,
112 session: &session::Session,
113 _request_id: Option<&str>,
114 ) -> Result<i64, SpaceError> {
115 let user_id = session.user_id()?.ok_or(SpaceError::Unauthorized)?;
116 let version = session
117 .security_version()?
118 .ok_or(SpaceError::Unauthorized)?;
119 let id = session
120 .get::<String>("auth_session_id")?
121 .ok_or(SpaceError::Unauthorized)?;
122 let kenauth = self
123 .state
124 .kenauth
125 .as_deref()
126 .ok_or_else(|| SpaceError::Internal("kenoauth is not configured".to_owned()))?;
127 let _ = kenauth.get_session(user_id, version, &id).await?;
128 Ok(user_id)
129 }
130
23131 pub fn db(&self) -> &Arc<dyn Database> {
24132 &self.state.database
25133 }
@@ -54,3 +162,1054 @@
54162 .map_err(|error| SpaceError::Internal(format!("email send failed: {error}")))
55163 }
56164}
165
166impl SpaceService {
167 pub(crate) async fn grant_public_room_memberships(&self, space_id: Uuid, user_id: i64) {
168 let public_rooms = models::room::rooms::Entity::find()
169 .filter(models::room::rooms::Column::SpaceId.eq(space_id))
170 .filter(models::room::rooms::Column::IsPrivate.eq(false))
171 .filter(models::room::rooms::Column::IsActive.eq(true))
172 .all(self.db().read())
173 .await;
174 let Ok(public_rooms) = public_rooms else {
175 tracing::warn!(%space_id, %user_id, "public room lookup failed");
176 return;
177 };
178 for room in &public_rooms {
179 let now = OffsetDateTime::now_utc();
180 let rm = models::room::room_members::ActiveModel {
181 role: Set(models::room::room_members::RoomMemberRole::Member),
182 room_id: Set(room.id),
183 user_id: Set(user_id),
184 nick: Set(None),
185 joined_at: Set(now),
186 left_at: Set(None),
187 created_at: Set(now),
188 updated_at: Set(Some(now)),
189 };
190 if let Err(e) = rm.insert(self.db().write()).await {
191 tracing::warn!(error = %e, room_id = %room.id, %user_id, "failed to create room_members record");
192 }
193 }
194 }
195
196 pub async fn load_user_briefs(
197 &self,
198 uids: &[i64],
199 ) -> Result<
200 std::collections::HashMap<i64, crate::services::dto::UserDto>,
201 crate::errors::SpaceError,
202 > {
203 Ok(export::users::load_user_briefs(&self.state, uids).await?)
204 }
205}
206
207use crate::services::types::{
208 CreateOrganizationBody, MAX_ORG_DESCRIPTION_LEN, MAX_ORG_NAME_LEN, MAX_PERMISSION_LEN,
209 MAX_ROLE_KEY_LEN, MAX_ROLE_NAME_LEN, MAX_SLUG_LEN, MemberSummary, OrganizationSummary,
210 PermissionGrant, RoleSummary, SYSTEM_ROLE_ADMIN, SYSTEM_ROLE_OWNER, UpdateOrganizationBody,
211 is_known_permission,
212};
213use models::space::{
214 PermissionEffect, SpaceMemberRole, SpaceStatus, space_invite_redemptions, space_invites,
215 space_member_permissions, space_member_roles, space_members, space_role_permissions,
216 space_roles, spaces,
217};
218
219pub(crate) fn perm_cache_prefix(organization_id: Uuid, user_id: i64) -> String {
220 format!("auth:org:perm:{organization_id}:{user_id}")
221}
222
223impl SpaceService {
224 /// 组织写操作的统一收尾:推进 version,然后清掉本组织全部派生缓存。
225 /// version 同时是权限缓存的版本键与乐观锁基准,必须与写操作同事务推进。
226 pub(crate) async fn invalidate_org(&self, organization_id: Uuid) -> SpaceResult<()> {
227 let members = space_members::Entity::find()
228 .select_only()
229 .column(space_members::Column::UserId)
230 .filter(space_members::Column::OrganizationId.eq(organization_id))
231 .into_tuple::<i64>()
232 .all(self.db_conn().read())
233 .await?;
234 self.drop_org_cache(organization_id, members).await
235 }
236
237 /// 成员行已随事务删除时用这一版:user_id 由调用方在事务内收集后传入。
238 pub(crate) async fn drop_org_cache(
239 &self,
240 organization_id: Uuid,
241 members: Vec<i64>,
242 ) -> SpaceResult<()> {
243 let cache = self.cache();
244 for key in [
245 format!("auth:org:{organization_id}"),
246 format!("auth:org:summary:{organization_id}"),
247 ] {
248 if let Err(error) = cache.delete(&key).await {
249 tracing::warn!(%error, %organization_id, "failed to drop the organization cache");
250 }
251 }
252 for user_id in members {
253 if let Err(error) = cache
254 .delete(&perm_cache_prefix(organization_id, user_id))
255 .await
256 {
257 tracing::warn!(%error, %organization_id, user_id, "failed to drop the permission cache");
258 }
259 }
260 Ok(())
261 }
262
263 pub(crate) async fn peek_org_version(&self, organization_id: Uuid) -> SpaceResult<Option<i32>> {
264 Ok(spaces::Entity::find_by_id(organization_id)
265 .select_only()
266 .column(spaces::Column::Version)
267 .into_tuple::<i32>()
268 .one(self.db_conn().read())
269 .await?)
270 }
271
272 /// `expected_version <= 0` 表示调用方不做乐观锁校验。
273 pub(crate) async fn check_version(
274 &self,
275 organization_id: Uuid,
276 expected: i32,
277 ) -> SpaceResult<()> {
278 if expected <= 0 {
279 return Ok(());
280 }
281 let current = self
282 .peek_org_version(organization_id)
283 .await?
284 .ok_or(SpaceError::SpaceNotFound)?;
285 if current != expected {
286 return Err(SpaceError::VersionMismatch { current });
287 }
288 Ok(())
289 }
290
291 pub(crate) async fn is_platform_admin(&self, user_id: i64) -> SpaceResult<bool> {
292 Ok(users::Entity::find_by_id(user_id)
293 .select_only()
294 .column(users::Column::IsAdmin)
295 .into_tuple::<bool>()
296 .one(self.db_conn().read())
297 .await?
298 .unwrap_or(false))
299 }
300
301 /// 唯一权限闸门。所有组织内写操作先过这里,语义与 CheckPermission 完全一致。
302 pub(crate) async fn require_permission(
303 &self,
304 user_id: i64,
305 organization_id: Uuid,
306 permission: &str,
307 request_id: Option<&str>,
308 ) -> SpaceResult<()> {
309 let outcome =
310 permission::resolve(self, organization_id, user_id, permission, request_id).await?;
311 if outcome.allowed {
312 return Ok(());
313 }
314 Err(error::reason_error(outcome.reason, permission))
315 }
316
317 /// 授权上限:只能授出自己当前确实持有的权限,不能借新建/改角色提权。
318 pub(crate) async fn require_grantable(
319 &self,
320 user_id: i64,
321 organization_id: Uuid,
322 grants: &[PermissionGrant],
323 request_id: Option<&str>,
324 ) -> SpaceResult<()> {
325 for grant in grants {
326 if grant.effect == PermissionEffect::Allow {
327 self.require_permission(user_id, organization_id, &grant.permission, request_id)
328 .await?;
329 }
330 }
331 Ok(())
332 }
333
334 /// 组织事件在业务事务**内部**入队:业务写入与事件行同生共死。
335 ///
336 /// 写不进 outbox 就让整个事务回滚(`SpaceError` 向上传播),这是刻意选择:
337 /// 宁可这次写操作失败重试,也不要出现「组织改成了、消费方永远不知道」。
338 async fn emit_org<T: serde::Serialize>(
339 &self,
340 txn: &sea_orm::DatabaseTransaction,
341 kind: &str,
342 organization_id: Uuid,
343 actor_user_id: i64,
344 payload: T,
345 request_id: Option<&str>,
346 ) -> SpaceResult<()> {
347 let labels = Labels::new()
348 .with(label::ORGANIZATION_ID, organization_id)
349 .with(label::ACTOR_USER_ID, actor_user_id);
350 self.emit_tx(txn, kind, labels, payload, request_id).await
351 }
352
353 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, request_id = %request_id.unwrap_or_default()))]
354 pub async fn create_space(
355 &self,
356 actor_user_id: i64,
357 body: CreateOrganizationBody,
358 request_id: Option<&str>,
359 ) -> SpaceResult<OrganizationSummary> {
360 let slug = normalize_slug(&body.slug)?;
361 let name = normalize_name(&body.name)?;
362 let description = normalize_description(body.description.as_deref())?;
363 let avatar_url = normalize_avatar(body.avatar_url.as_deref())?;
364
365 users::Entity::find_by_id(actor_user_id)
366 .filter(users::Column::DeletedAt.is_null())
367 .one(self.db_conn().read())
368 .await?
369 .ok_or(SpaceError::UserNotFound)?;
370 if spaces::Entity::find()
371 .filter(spaces::Column::Slug.eq(slug.clone()))
372 .one(self.db_conn().read())
373 .await?
374 .is_some()
375 {
376 return Err(SpaceError::SpaceSlugTaken);
377 }
378
379 let welcome_message = match body.welcome_message.as_deref() {
380 Some(value) if value.trim().is_empty() => None,
381 Some(value) => normalize_description(Some(value))?,
382 None => None,
383 };
384 let allow_join_level = valid_join_level(body.allow_join_level.unwrap_or(0))?;
385
386 let txn = self.db_conn().write().begin().await?;
387 let organization = spaces::ActiveModel {
388 slug: Set(slug),
389 name: Set(name),
390 description: Set(description),
391 avatar_url: Set(avatar_url),
392 status: Set(SpaceStatus::Active),
393 owner_user_id: Set(actor_user_id),
394 join_policy: Set(body
395 .join_policy
396 .unwrap_or(models::space::SpaceJoinPolicy::InviteOnly)),
397 allow_join: Set(body.allow_join.unwrap_or(false)),
398 require_email_verified: Set(body.require_email_verified.unwrap_or(true)),
399 require_mfa: Set(body.require_mfa.unwrap_or(false)),
400 welcome_message: Set(welcome_message),
401 member_must_enable_2fa: Set(body.member_must_enable_2fa.unwrap_or(false)),
402 allow_invite: Set(body.allow_invite.unwrap_or(true)),
403 allow_join_level: Set(allow_join_level),
404 ..Default::default()
405 }
406 .insert(&txn)
407 .await
408 .map_err(error::map_insert_conflict)?;
409
410 let owner_role = role::seed_system_roles(&txn, organization.id).await?;
411 let membership = member::insert_owner(&txn, organization.id, actor_user_id).await?;
412 role::bind_role(&txn, membership.id, owner_role, actor_user_id).await?;
413 self.emit_org(
414 &txn,
415 label::ORGANIZATION_LIFE_CYCLE_CREATE,
416 organization.id,
417 actor_user_id,
418 serde_json::json!({
419 "id": organization.id,
420 "slug": &organization.slug,
421 "name": &organization.name,
422 "request_id": request_id,
423 }),
424 request_id,
425 )
426 .await?;
427 txn.commit().await?;
428
429 let summary = self
430 .organization_summary(&organization, Some(actor_user_id))
431 .await?;
432 Ok(summary)
433 }
434
435 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = ?organization_id, request_id = %request_id.unwrap_or_default()))]
436 pub async fn get_space(
437 &self,
438 actor_user_id: i64,
439 organization_id: Option<Uuid>,
440 slug: Option<&str>,
441 request_id: Option<&str>,
442 ) -> SpaceResult<(OrganizationSummary, Option<MemberSummary>)> {
443 let organization = match (
444 organization_id,
445 slug.map(str::trim).filter(|value| !value.is_empty()),
446 ) {
447 (Some(id), _) => {
448 spaces::Entity::find_by_id(id)
449 .filter(spaces::Column::DeletedAt.is_null())
450 .one(self.db_conn().read())
451 .await?
452 }
453 (None, Some(slug)) => {
454 spaces::Entity::find()
455 .filter(spaces::Column::Slug.eq(slug.to_owned()))
456 .filter(spaces::Column::DeletedAt.is_null())
457 .one(self.db_conn().read())
458 .await?
459 }
460 (None, None) => None,
461 }
462 .ok_or(SpaceError::SpaceNotFound)?;
463
464 let summary = self
465 .organization_summary(&organization, Some(actor_user_id))
466 .await?;
467 // 非成员只看公开字段:my_role 为空即可,不额外补一层 DTO。
468 let detail =
469 match member::find_any(self.db_conn().read(), organization.id, actor_user_id).await? {
470 Some(row) => Some(self.member_summary(&row).await?),
471 None => None,
472 };
473 Ok((summary, detail))
474 }
475
476 pub async fn space_id_by_name(
477 &self,
478 actor_user_id: i64,
479 space_name: &str,
480 ) -> SpaceResult<Uuid> {
481 let slug = space_name.trim().to_lowercase();
482 let (summary, _) = self
483 .get_space(actor_user_id, None, Some(&slug), None)
484 .await?;
485 Ok(summary.id)
486 }
487
488 pub(crate) async fn organization_summary(
489 &self,
490 organization: &spaces::Model,
491 actor_user_id: Option<i64>,
492 ) -> SpaceResult<OrganizationSummary> {
493 let member_count = space_members::Entity::find()
494 .filter(space_members::Column::OrganizationId.eq(organization.id))
495 .filter(space_members::Column::LeftAt.is_null())
496 .filter(space_members::Column::BannedAt.is_null())
497 .count(self.db_conn().read())
498 .await? as i64;
499 let my_role = match actor_user_id {
500 Some(user_id) => member::find_active(self.db_conn().read(), organization.id, user_id)
501 .await?
502 .map(|row| row.role),
503 None => None,
504 };
505 Ok(OrganizationSummary {
506 id: organization.id,
507 slug: organization.slug.clone(),
508 name: organization.name.clone(),
509 description: organization.description.clone(),
510 avatar_url: organization.avatar_url.clone(),
511 status: organization.status,
512 owner_user_id: organization.owner_user_id,
513 member_count,
514 version: organization.version,
515 created_at: organization.created_at,
516 updated_at: organization.updated_at,
517 archived_at: organization.archived_at,
518 my_role,
519 join_policy: organization.join_policy,
520 allow_join: organization.allow_join,
521 require_email_verified: organization.require_email_verified,
522 require_mfa: organization.require_mfa,
523 welcome_message: organization.welcome_message.clone(),
524 member_must_enable_2fa: organization.member_must_enable_2fa,
525 allow_invite: organization.allow_invite,
526 allow_join_level: organization.allow_join_level,
527 })
528 }
529
530 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, user_id = user_id, request_id = %request_id.unwrap_or_default()))]
531 pub async fn list_spaces(
532 &self,
533 actor_user_id: i64,
534 user_id: i64,
535 include_archived: bool,
536 limit: u64,
537 offset: u64,
538 request_id: Option<&str>,
539 ) -> SpaceResult<Vec<OrganizationSummary>> {
540 let target = if user_id == 0 { actor_user_id } else { user_id };
541 if target != actor_user_id && !self.is_platform_admin(actor_user_id).await? {
542 return Err(SpaceError::Forbidden);
543 }
544
545 let ids = space_members::Entity::find()
546 .select_only()
547 .column(space_members::Column::OrganizationId)
548 .filter(space_members::Column::UserId.eq(target))
549 .filter(space_members::Column::LeftAt.is_null())
550 .filter(space_members::Column::BannedAt.is_null())
551 .into_tuple::<Uuid>()
552 .all(self.db_conn().read())
553 .await?;
554 if ids.is_empty() {
555 return Ok(Vec::new());
556 }
557
558 let mut query = spaces::Entity::find()
559 .filter(spaces::Column::DeletedAt.is_null())
560 .filter(spaces::Column::Id.is_in(ids));
561 if !include_archived {
562 query = query.filter(spaces::Column::Status.ne(SpaceStatus::Archived));
563 }
564 let rows = query
565 .order_by_desc(spaces::Column::UpdatedAt)
566 .limit(limit)
567 .offset(offset)
568 .all(self.db_conn().read())
569 .await?;
570
571 let mut out = Vec::with_capacity(rows.len());
572 for row in rows {
573 out.push(self.organization_summary(&row, Some(target)).await?);
574 }
575 Ok(out)
576 }
577
578 /// 公开目录:ACTIVE 且非 PRIVATE 的组织。不要求成员身份。
579 pub async fn list_public_spaces(
580 &self,
581 query: &str,
582 limit: u64,
583 offset: u64,
584 ) -> SpaceResult<Vec<OrganizationSummary>> {
585 let mut select = spaces::Entity::find()
586 .filter(spaces::Column::DeletedAt.is_null())
587 .filter(spaces::Column::Status.eq(SpaceStatus::Active))
588 .filter(spaces::Column::JoinPolicy.ne(models::space::SpaceJoinPolicy::Private));
589 let needle = query.trim();
590 if !needle.is_empty() {
591 select = select.filter(
592 Condition::any()
593 .add(spaces::Column::Slug.contains(needle))
594 .add(spaces::Column::Name.contains(needle)),
595 );
596 }
597 let rows = select
598 .order_by_desc(spaces::Column::CreatedAt)
599 .limit(limit)
600 .offset(offset)
601 .all(self.db_conn().read())
602 .await?;
603 let mut out = Vec::with_capacity(rows.len());
604 for row in rows {
605 out.push(self.organization_summary(&row, None).await?);
606 }
607 Ok(out)
608 }
609
610 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, request_id = %request_id.unwrap_or_default()))]
611 pub async fn update_space(
612 &self,
613 actor_user_id: i64,
614 organization_id: Uuid,
615 body: UpdateOrganizationBody,
616 request_id: Option<&str>,
617 ) -> SpaceResult<OrganizationSummary> {
618 self.require_permission(actor_user_id, organization_id, "org.update", request_id)
619 .await?;
620 self.check_version(organization_id, body.expected_version)
621 .await?;
622
623 let name = body.name.as_deref().map(normalize_name).transpose()?;
624 let description = match body.description.as_deref() {
625 Some(value) => Some(normalize_description(Some(value))?),
626 None => None,
627 };
628 let avatar_url = match body.avatar_url.as_deref() {
629 Some(value) => Some(normalize_avatar(Some(value))?),
630 None => None,
631 };
632
633 let txn = self.db_conn().write().begin().await?;
634 let current = spaces::Entity::find_by_id(organization_id)
635 .filter(spaces::Column::DeletedAt.is_null())
636 .one(&txn)
637 .await?
638 .ok_or(SpaceError::SpaceNotFound)?;
639 if body.expected_version > 0 && current.version != body.expected_version {
640 return Err(SpaceError::VersionMismatch {
641 current: current.version,
642 });
643 }
644 let mut active: spaces::ActiveModel = current.into();
645 if let Some(value) = name {
646 active.name = Set(value);
647 }
648 if let Some(value) = description {
649 active.description = Set(value);
650 }
651 if let Some(value) = avatar_url {
652 active.avatar_url = Set(value);
653 }
654 if let Some(value) = body.join_policy {
655 active.join_policy = Set(value);
656 }
657 if let Some(value) = body.allow_join {
658 active.allow_join = Set(value);
659 }
660 if let Some(value) = body.require_email_verified {
661 active.require_email_verified = Set(value);
662 }
663 if let Some(value) = body.require_mfa {
664 active.require_mfa = Set(value);
665 }
666 if let Some(value) = body.welcome_message.clone() {
667 let trimmed = value.trim();
668 active.welcome_message = Set(if trimmed.is_empty() {
669 None
670 } else {
671 Some(trimmed.to_string())
672 });
673 }
674 if let Some(value) = body.member_must_enable_2fa {
675 active.member_must_enable_2fa = Set(value);
676 }
677 if let Some(value) = body.allow_invite {
678 active.allow_invite = Set(value);
679 }
680 if let Some(value) = body.allow_join_level {
681 active.allow_join_level = Set(valid_join_level(value)?);
682 }
683 let updated = active.update(&txn).await?;
684 let version = bump_version(&txn, organization_id).await?;
685 self.emit_org(
686 &txn,
687 label::ORGANIZATION_LIFE_CYCLE_UPDATE,
688 organization_id,
689 actor_user_id,
690 serde_json::json!({
691 "id": organization_id,
692 "name": &updated.name,
693 "request_id": request_id,
694 }),
695 request_id,
696 )
697 .await?;
698 txn.commit().await?;
699
700 self.invalidate_org(organization_id).await?;
701 let refreshed = self.load_organization(organization_id).await?;
702 let mut summary = self
703 .organization_summary(&refreshed, Some(actor_user_id))
704 .await?;
705 summary.version = version;
706 Ok(summary)
707 }
708
709 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, request_id = %request_id.unwrap_or_default()))]
710 pub async fn archive_space(
711 &self,
712 actor_user_id: i64,
713 organization_id: Uuid,
714 reason: Option<&str>,
715 request_id: Option<&str>,
716 ) -> SpaceResult<OrganizationSummary> {
717 self.require_permission(actor_user_id, organization_id, "org.archive", request_id)
718 .await?;
719 let reason = normalize_reason(reason)?;
720
721 let txn = self.db_conn().write().begin().await?;
722 spaces::Entity::update_many()
723 .col_expr(spaces::Column::Status, Expr::value(SpaceStatus::Archived))
724 .col_expr(spaces::Column::ArchivedAt, Expr::value(Utc::now()))
725 .col_expr(spaces::Column::SuspendedReason, Expr::value(reason))
726 .filter(spaces::Column::Id.eq(organization_id))
727 .filter(spaces::Column::DeletedAt.is_null())
728 .exec(&txn)
729 .await?;
730 let version = bump_version(&txn, organization_id).await?;
731 self.emit_org(
732 &txn,
733 label::ORGANIZATION_LIFE_CYCLE_UPDATE,
734 organization_id,
735 actor_user_id,
736 serde_json::json!({
737 "id": organization_id,
738 "action": "archive",
739 "request_id": request_id,
740 }),
741 request_id,
742 )
743 .await?;
744 txn.commit().await?;
745
746 self.invalidate_org(organization_id).await?;
747 let refreshed = self.load_organization(organization_id).await?;
748 let mut summary = self
749 .organization_summary(&refreshed, Some(actor_user_id))
750 .await?;
751 summary.version = version;
752 Ok(summary)
753 }
754
755 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, request_id = %request_id.unwrap_or_default()))]
756 pub async fn restore_space(
757 &self,
758 actor_user_id: i64,
759 organization_id: Uuid,
760 request_id: Option<&str>,
761 ) -> SpaceResult<OrganizationSummary> {
762 // 归档组织的权限判定一律不通过,所以这里不能用 require_permission:
763 // 恢复动作由 OWNER 或平台管理员执行。
764 let organization = self.load_organization(organization_id).await?;
765 if organization.owner_user_id != actor_user_id
766 && !self.is_platform_admin(actor_user_id).await?
767 {
768 return Err(SpaceError::Forbidden);
769 }
770
771 let txn = self.db_conn().write().begin().await?;
772 spaces::Entity::update_many()
773 .col_expr(spaces::Column::Status, Expr::value(SpaceStatus::Active))
774 .col_expr(
775 spaces::Column::ArchivedAt,
776 Expr::value(None::<chrono::DateTime<Utc>>),
777 )
778 .col_expr(spaces::Column::SuspendedReason, Expr::value(None::<String>))
779 .filter(spaces::Column::Id.eq(organization_id))
780 .exec(&txn)
781 .await?;
782 let version = bump_version(&txn, organization_id).await?;
783 self.emit_org(
784 &txn,
785 label::ORGANIZATION_LIFE_CYCLE_UPDATE,
786 organization_id,
787 actor_user_id,
788 serde_json::json!({
789 "id": organization_id,
790 "action": "restore",
791 "request_id": request_id,
792 }),
793 request_id,
794 )
795 .await?;
796 txn.commit().await?;
797
798 self.invalidate_org(organization_id).await?;
799 let refreshed = self.load_organization(organization_id).await?;
800 let mut summary = self
801 .organization_summary(&refreshed, Some(actor_user_id))
802 .await?;
803 summary.version = version;
804 Ok(summary)
805 }
806
807 /// 删除分两种语义:`force = false` 时仍有活跃成员会被拦下,
808 /// `force = true` 时才连成员与角色一并清理。软删标记写在 `deleted_at` 上,
809 /// 但权限缓存与派生数据必须真的清掉,否则残留的授予会在重建同名组织时复活。
810 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, force = force, request_id = %request_id.unwrap_or_default()))]
811 pub async fn delete_space(
812 &self,
813 actor_user_id: i64,
814 organization_id: Uuid,
815 force: bool,
816 reason: Option<&str>,
817 request_id: Option<&str>,
818 ) -> SpaceResult<bool> {
819 let organization = self.load_organization(organization_id).await?;
820 if organization.owner_user_id != actor_user_id
821 && !self.is_platform_admin(actor_user_id).await?
822 {
823 return Err(SpaceError::Forbidden);
824 }
825 let reason = normalize_reason(reason)?;
826
827 if !force {
828 let active = space_members::Entity::find()
829 .filter(space_members::Column::OrganizationId.eq(organization_id))
830 .filter(space_members::Column::LeftAt.is_null())
831 .filter(space_members::Column::BannedAt.is_null())
832 .count(self.db_conn().read())
833 .await?;
834 if active > 0 {
835 return Err(SpaceError::SpaceHasMembers);
836 }
837 }
838
839 let txn = self.db_conn().write().begin().await?;
840 let member_ids = space_members::Entity::find()
841 .select_only()
842 .column(space_members::Column::Id)
843 .filter(space_members::Column::OrganizationId.eq(organization_id))
844 .into_tuple::<Uuid>()
845 .all(&txn)
846 .await?;
847 // 权限缓存键依赖 user_id,而成员行在本事务里会被物理删除,所以先把 user_id 取出来。
848 let member_user_ids = space_members::Entity::find()
849 .select_only()
850 .column(space_members::Column::UserId)
851 .filter(space_members::Column::OrganizationId.eq(organization_id))
852 .into_tuple::<i64>()
853 .all(&txn)
854 .await?;
855 if !member_ids.is_empty() {
856 space_member_permissions::Entity::delete_many()
857 .filter(space_member_permissions::Column::MemberId.is_in(member_ids.clone()))
858 .exec(&txn)
859 .await?;
860 space_member_roles::Entity::delete_many()
861 .filter(space_member_roles::Column::MemberId.is_in(member_ids))
862 .exec(&txn)
863 .await?;
864 space_members::Entity::delete_many()
865 .filter(space_members::Column::OrganizationId.eq(organization_id))
866 .exec(&txn)
867 .await?;
868 }
869 space_invite_redemptions::Entity::delete_many()
870 .filter(space_invite_redemptions::Column::OrganizationId.eq(organization_id))
871 .exec(&txn)
872 .await?;
873 space_invites::Entity::delete_many()
874 .filter(space_invites::Column::OrganizationId.eq(organization_id))
875 .exec(&txn)
876 .await?;
877 let role_ids = space_roles::Entity::find()
878 .select_only()
879 .column(space_roles::Column::Id)
880 .filter(space_roles::Column::OrganizationId.eq(organization_id))
881 .into_tuple::<Uuid>()
882 .all(&txn)
883 .await?;
884 if !role_ids.is_empty() {
885 space_role_permissions::Entity::delete_many()
886 .filter(space_role_permissions::Column::RoleId.is_in(role_ids))
887 .exec(&txn)
888 .await?;
889 }
890 space_roles::Entity::delete_many()
891 .filter(space_roles::Column::OrganizationId.eq(organization_id))
892 .exec(&txn)
893 .await?;
894 spaces::Entity::update_many()
895 .col_expr(spaces::Column::DeletedAt, Expr::value(Utc::now()))
896 .col_expr(spaces::Column::SuspendedReason, Expr::value(reason.clone()))
897 .filter(spaces::Column::Id.eq(organization_id))
898 .exec(&txn)
899 .await?;
900 bump_version(&txn, organization_id).await?;
901 self.emit_org(
902 &txn,
903 label::ORGANIZATION_LIFE_CYCLE_DELETE,
904 organization_id,
905 actor_user_id,
906 serde_json::json!({
907 "id": organization_id,
908 "force": force,
909 "reason": &reason,
910 "request_id": request_id,
911 }),
912 request_id,
913 )
914 .await?;
915 txn.commit().await?;
916
917 self.drop_org_cache(organization_id, member_user_ids)
918 .await?;
919 Ok(true)
920 }
921
922 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, target_user_id = target_user_id, request_id = %request_id.unwrap_or_default()))]
923 pub async fn transfer_ownership(
924 &self,
925 actor_user_id: i64,
926 organization_id: Uuid,
927 target_user_id: i64,
928 request_id: Option<&str>,
929 ) -> SpaceResult<OrganizationSummary> {
930 let organization = self.load_organization(organization_id).await?;
931 if organization.owner_user_id != actor_user_id
932 && !self.is_platform_admin(actor_user_id).await?
933 {
934 return Err(SpaceError::Forbidden);
935 }
936 if organization.status != SpaceStatus::Active {
937 return Err(SpaceError::SpaceInactive);
938 }
939 if target_user_id == organization.owner_user_id {
940 return Err(SpaceError::SpaceMemberExists);
941 }
942 let target = member::find_active(self.db_conn().read(), organization_id, target_user_id)
943 .await?
944 .ok_or(SpaceError::SpaceMemberNotFound)?;
945 let owner_role =
946 role::find_by_key(organization_id, SYSTEM_ROLE_OWNER, self.db_conn().read())
947 .await?
948 .ok_or_else(|| SpaceError::internal("owner role is missing"))?;
949 let admin_role =
950 role::find_by_key(organization_id, SYSTEM_ROLE_ADMIN, self.db_conn().read())
951 .await?
952 .ok_or_else(|| SpaceError::internal("admin role is missing"))?;
953
954 let txn = self.db_conn().write().begin().await?;
955 // 唯一 OWNER 的不变式落在两处:spaces.owner_user_id 与成员行的粗粒度角色。
956 // 两个改写放在同一事务里,读到中间态的可能性为零。
957 if let Some(previous) =
958 member::find_any(&txn, organization_id, organization.owner_user_id).await?
959 {
960 let mut active: space_members::ActiveModel = previous.clone().into();
961 active.role = Set(SpaceMemberRole::Admin);
962 active.update(&txn).await?;
963 role::unbind_role(&txn, previous.id, owner_role.id).await?;
964 role::bind_role(&txn, previous.id, admin_role.id, actor_user_id).await?;
965 }
966 let mut active: space_members::ActiveModel = target.clone().into();
967 active.role = Set(SpaceMemberRole::Owner);
968 active.update(&txn).await?;
969 role::unbind_role(&txn, target.id, admin_role.id).await?;
970 role::bind_role(&txn, target.id, owner_role.id, actor_user_id).await?;
971 spaces::Entity::update_many()
972 .col_expr(spaces::Column::OwnerUserId, Expr::value(target_user_id))
973 .filter(spaces::Column::Id.eq(organization_id))
974 .exec(&txn)
975 .await?;
976 let version = bump_version(&txn, organization_id).await?;
977 self.emit_org(
978 &txn,
979 label::ORGANIZATION_MEMBER_UPDATE,
980 organization_id,
981 actor_user_id,
982 serde_json::json!({
983 "id": organization_id,
984 "action": "transfer_ownership",
985 "previous_owner": organization.owner_user_id,
986 "new_owner": target_user_id,
987 "request_id": request_id,
988 }),
989 request_id,
990 )
991 .await?;
992 txn.commit().await?;
993
994 self.invalidate_org(organization_id).await?;
995 let refreshed = self.load_organization(organization_id).await?;
996 let mut summary = self
997 .organization_summary(&refreshed, Some(actor_user_id))
998 .await?;
999 summary.version = version;
1000 Ok(summary)
1001 }
1002
1003 pub async fn load_organization(&self, organization_id: Uuid) -> SpaceResult<spaces::Model> {
1004 spaces::Entity::find_by_id(organization_id)
1005 .filter(spaces::Column::DeletedAt.is_null())
1006 .one(self.db_conn().read())
1007 .await?
1008 .ok_or(SpaceError::SpaceNotFound)
1009 }
1010
1011 pub async fn member_summary(&self, row: &space_members::Model) -> SpaceResult<MemberSummary> {
1012 let user = users::Entity::find_by_id(row.user_id)
1013 .one(self.db_conn().read())
1014 .await?;
1015 Ok(MemberSummary {
1016 id: row.id,
1017 organization_id: row.organization_id,
1018 user_id: row.user_id,
1019 username: user
1020 .as_ref()
1021 .map(|u| u.username.clone())
1022 .unwrap_or_default(),
1023 displayname: user.and_then(|u| u.displayname),
1024 role: row.role,
1025 role_keys: role::role_keys_of(self.db_conn().read(), row.id).await?,
1026 permissions: permission::effective(self.db_conn().read(), row).await?,
1027 joined_at: row.joined_at,
1028 left_at: row.left_at,
1029 banned_at: row.banned_at,
1030 invited_by: row.invited_by,
1031 join_method: row.join_method,
1032 alias_name: row.alias_name.clone(),
1033 alias_avatar_url: row.alias_avatar_url.clone(),
1034 message_notifications: row.message_notifications,
1035 muted: row.muted,
1036 mute_until: row.mute_until,
1037 mobile_push: row.mobile_push,
1038 suppress_everyone: row.suppress_everyone,
1039 suppress_roles: row.suppress_roles,
1040 hide_muted_channels: row.hide_muted_channels,
1041 ban_reason: row.ban_reason.clone(),
1042 ban_expires_at: row.ban_expires_at,
1043 ban_allow_agent_join: row.ban_allow_agent_join,
1044 ban_allow_speak: row.ban_allow_speak,
1045 ban_allow_push_repo: row.ban_allow_push_repo,
1046 ban_allow_comment_any: row.ban_allow_comment_any,
1047 version: row.version,
1048 last_active_at: row.last_active_at,
1049 })
1050 }
1051
1052 pub(crate) async fn role_summary(&self, row: &space_roles::Model) -> SpaceResult<RoleSummary> {
1053 let permissions = role::grants_of(self.db_conn().read(), row.id).await?;
1054 let member_count = space_member_roles::Entity::find()
1055 .filter(space_member_roles::Column::RoleId.eq(row.id))
1056 .count(self.db_conn().read())
1057 .await? as i64;
1058 Ok(RoleSummary {
1059 id: row.id,
1060 organization_id: row.organization_id,
1061 key: row.key.clone(),
1062 name: row.name.clone(),
1063 description: row.description.clone(),
1064 rank: row.rank,
1065 system: row.system,
1066 default_role: row.default_role,
1067 self_assignable: row.self_assignable,
1068 permissions,
1069 member_count,
1070 created_at: row.created_at,
1071 updated_at: row.updated_at,
1072 version: row.version,
1073 color: row.color.clone(),
1074 emoji: row.emoji.clone(),
1075 icon_hash: row.icon_hash.clone(),
1076 position: row.position,
1077 })
1078 }
1079
1080 pub(crate) async fn default_role_id(&self, organization_id: Uuid) -> SpaceResult<Option<Uuid>> {
1081 Ok(space_roles::Entity::find()
1082 .filter(space_roles::Column::OrganizationId.eq(organization_id))
1083 .filter(space_roles::Column::DefaultRole.eq(true))
1084 .order_by_asc(space_roles::Column::Rank)
1085 .one(self.db_conn().read())
1086 .await?
1087 .map(|row| row.id))
1088 }
1089
1090 pub(crate) fn validate_grants(grants: &[PermissionGrant]) -> SpaceResult<Vec<PermissionGrant>> {
1091 let mut out = Vec::with_capacity(grants.len());
1092 for grant in grants {
1093 let permission = grant.permission.trim();
1094 if permission.is_empty()
1095 || permission.len() > MAX_PERMISSION_LEN
1096 || !is_known_permission(permission)
1097 {
1098 return Err(SpaceError::InvalidPermission {
1099 permission: grant.permission.clone(),
1100 });
1101 }
1102 out.push(PermissionGrant {
1103 permission: permission.to_owned(),
1104 effect: match grant.effect {
1105 PermissionEffect::Deny => PermissionEffect::Deny,
1106 _ => PermissionEffect::Allow,
1107 },
1108 });
1109 }
1110 Ok(out)
1111 }
1112}
1113
1114/// 在写事务内推进 version 并回读新值。
1115pub async fn bump_version<C>(db: &C, organization_id: Uuid) -> SpaceResult<i32>
1116where
1117 C: sea_orm::ConnectionTrait,
1118{
1119 let bumped = spaces::Entity::update_many()
1120 .col_expr(
1121 spaces::Column::Version,
1122 Expr::col(spaces::Column::Version).add(1),
1123 )
1124 .col_expr(spaces::Column::UpdatedAt, Expr::value(Utc::now()))
1125 .filter(spaces::Column::Id.eq(organization_id))
1126 .exec(db)
1127 .await?;
1128 if bumped.rows_affected == 0 {
1129 return Err(SpaceError::SpaceNotFound);
1130 }
1131 spaces::Entity::find_by_id(organization_id)
1132 .select_only()
1133 .column(spaces::Column::Version)
1134 .into_tuple::<i32>()
1135 .one(db)
1136 .await?
1137 .ok_or(SpaceError::SpaceNotFound)
1138}
1139
1140pub(crate) fn normalize_slug(raw: &str) -> SpaceResult<String> {
1141 let slug = raw.trim().to_lowercase();
1142 if slug.is_empty()
1143 || slug.len() > MAX_SLUG_LEN
1144 || !slug
1145 .chars()
1146 .all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-')
1147 || slug.starts_with('-')
1148 || slug.ends_with('-')
1149 {
1150 return Err(SpaceError::MalformedRequest);
1151 }
1152 Ok(slug)
1153}
1154
1155pub(crate) fn normalize_name(raw: &str) -> SpaceResult<String> {
1156 let name = raw.trim();
1157 if name.is_empty() || name.len() > MAX_ORG_NAME_LEN {
1158 return Err(SpaceError::MalformedRequest);
1159 }
1160 Ok(name.to_owned())
1161}
1162
1163pub(crate) fn normalize_description(raw: Option<&str>) -> SpaceResult<Option<String>> {
1164 match raw.map(str::trim) {
1165 None | Some("") => Ok(None),
1166 Some(value) if value.len() > MAX_ORG_DESCRIPTION_LEN => Err(SpaceError::MalformedRequest),
1167 Some(value) => Ok(Some(value.to_owned())),
1168 }
1169}
1170
1171pub(crate) fn normalize_avatar(raw: Option<&str>) -> SpaceResult<Option<String>> {
1172 match raw.map(str::trim) {
1173 None | Some("") => Ok(None),
1174 Some(value) if value.len() > 512 || !value.starts_with("https://") => {
1175 Err(SpaceError::MalformedRequest)
1176 }
1177 Some(value) => Ok(Some(value.to_owned())),
1178 }
1179}
1180
1181pub(crate) fn normalize_reason(raw: Option<&str>) -> SpaceResult<Option<String>> {
1182 match raw.map(str::trim) {
1183 None | Some("") => Ok(None),
1184 Some(value) if value.len() > 512 => Err(SpaceError::MalformedRequest),
1185 Some(value) => Ok(Some(value.to_owned())),
1186 }
1187}
1188
1189pub(crate) fn normalize_role_key(raw: &str) -> SpaceResult<String> {
1190 let key = raw.trim().to_lowercase();
1191 if key.is_empty()
1192 || key.len() > MAX_ROLE_KEY_LEN
1193 || !key
1194 .chars()
1195 .all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '_' || ch == '-')
1196 {
1197 return Err(SpaceError::MalformedRequest);
1198 }
1199 Ok(key)
1200}
1201
1202pub(crate) fn normalize_role_name(raw: &str) -> SpaceResult<String> {
1203 let name = raw.trim();
1204 if name.is_empty() || name.len() > MAX_ROLE_NAME_LEN {
1205 return Err(SpaceError::MalformedRequest);
1206 }
1207 Ok(name.to_owned())
1208}
1209
1210pub(crate) fn valid_join_level(value: i16) -> SpaceResult<i16> {
1211 (0..=4)
1212 .contains(&value)
1213 .then_some(value)
1214 .ok_or(SpaceError::MalformedRequest)
1215}
Deleted lib/space/src/services/spaces.rs

Diff too large to display.

Renamed lib/auth/src/services/organization/types.rs → lib/space/src/services/types.rs View lib/space/src/services/types.rs
@@ -3,9 +3,9 @@
33use utoipa::ToSchema;
44use uuid::Uuid;
55
6use crate::models::space::{
7 MessageNotifications, OrgJoinMethod, OrgMemberRole, OrganizationJoinPolicy,
8 OrganizationJoinRequestStatus, OrganizationStatus, PermissionEffect,
6use models::space::{
7 MessageNotifications, PermissionEffect, SpaceJoinMethod, SpaceJoinPolicy,
8 SpaceJoinRequestState, SpaceMemberRole, SpaceStatus,
99};
1010
1111/// 组织内权限键白名单。写入口一律先过这里,未知键直接拒绝;
@@ -114,15 +114,15 @@
114114 pub name: String,
115115 pub description: Option<String>,
116116 pub avatar_url: Option<String>,
117 pub status: OrganizationStatus,
117 pub status: SpaceStatus,
118118 pub owner_user_id: i64,
119119 pub member_count: i64,
120120 pub version: i32,
121121 pub created_at: DateTime<Utc>,
122122 pub updated_at: DateTime<Utc>,
123123 pub archived_at: Option<DateTime<Utc>>,
124 pub my_role: Option<OrgMemberRole>,
125 pub join_policy: OrganizationJoinPolicy,
124 pub my_role: Option<SpaceMemberRole>,
125 pub join_policy: SpaceJoinPolicy,
126126 pub allow_join: bool,
127127 pub require_email_verified: bool,
128128 pub require_mfa: bool,
@@ -139,14 +139,14 @@
139139 pub user_id: i64,
140140 pub username: String,
141141 pub displayname: Option<String>,
142 pub role: OrgMemberRole,
142 pub role: SpaceMemberRole,
143143 pub role_keys: Vec<String>,
144144 pub permissions: Vec<String>,
145145 pub joined_at: DateTime<Utc>,
146146 pub left_at: Option<DateTime<Utc>>,
147147 pub banned_at: Option<DateTime<Utc>>,
148148 pub invited_by: Option<i64>,
149 pub join_method: OrgJoinMethod,
149 pub join_method: SpaceJoinMethod,
150150 pub alias_name: Option<String>,
151151 pub alias_avatar_url: Option<String>,
152152 pub message_notifications: MessageNotifications,
@@ -198,7 +198,7 @@
198198pub struct InviteSummary {
199199 pub id: Uuid,
200200 pub organization_id: Uuid,
201 pub method: OrgJoinMethod,
201 pub method: SpaceJoinMethod,
202202 pub role_id: Option<Uuid>,
203203 pub target_user_id: Option<i64>,
204204 pub created_by: i64,
@@ -225,7 +225,7 @@
225225 pub description: Option<String>,
226226 pub avatar_url: Option<String>,
227227 #[serde(default)]
228 pub join_policy: Option<OrganizationJoinPolicy>,
228 pub join_policy: Option<SpaceJoinPolicy>,
229229 #[serde(default)]
230230 pub allow_join: Option<bool>,
231231 #[serde(default)]
@@ -250,7 +250,7 @@
250250 #[serde(default)]
251251 pub expected_version: i32,
252252 #[serde(default)]
253 pub join_policy: Option<OrganizationJoinPolicy>,
253 pub join_policy: Option<SpaceJoinPolicy>,
254254 #[serde(default)]
255255 pub allow_join: Option<bool>,
256256 #[serde(default)]
@@ -271,14 +271,14 @@
271271pub struct AddMemberBody {
272272 pub user_id: i64,
273273 #[serde(default)]
274 pub role: Option<OrgMemberRole>,
274 pub role: Option<SpaceMemberRole>,
275275 #[serde(default)]
276276 pub role_keys: Vec<String>,
277277}
278278
279279#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
280280pub struct SetMemberRoleBody {
281 pub role: OrgMemberRole,
281 pub role: SpaceMemberRole,
282282 #[serde(default)]
283283 pub expected_version: i32,
284284}
@@ -413,7 +413,7 @@
413413 pub user_id: i64,
414414 pub username: String,
415415 pub displayname: Option<String>,
416 pub status: OrganizationJoinRequestStatus,
416 pub status: SpaceJoinRequestState,
417417 pub note: Option<String>,
418418 pub decided_by: Option<i64>,
419419 pub decided_at: Option<DateTime<Utc>>,
Modified lib/space/src/share/authz.rs View lib/space/src/share/authz.rs
@@ -65,7 +65,7 @@
6565 user_id: i64,
6666) -> Result<(), SpaceError> {
6767 if let Ok(Some(member)) = authority::member(state, user_id, space_id, user_id).await
68 && member.status == models::space::space_members::MemberStatus::Banned
68 && member.status() == models::space::MemberStatus::Banned
6969 {
7070 return Err(SpaceError::Forbidden);
7171 }
Added lib/space/src/share/crypto.rs View lib/space/src/share/crypto.rs

Diff too large to display.

Modified lib/space/src/share/dir_visibility.rs View lib/space/src/share/dir_visibility.rs
@@ -47,7 +47,7 @@
4747 dir_id: Uuid,
4848 caller: i64,
4949) -> Result<bool, SpaceError> {
50 if space.owner_id == caller {
50 if space.owner_user_id == caller {
5151 return Ok(true);
5252 }
5353 let target = dirs::Entity::find_by_id(dir_id).one(db).await?;
Modified lib/space/src/share/lookup.rs View lib/space/src/share/lookup.rs
@@ -15,7 +15,7 @@
1515 return Err(SpaceError::NotFound);
1616 }
1717 let space = authority::space_by_slug(state, caller, &lower_name).await?;
18 if !space.is_active {
18 if !authority::space_is_active(&space) {
1919 return Err(SpaceError::NotFound);
2020 }
2121 Ok(space)
@@ -35,7 +35,7 @@
3535 space_id: Uuid,
3636) -> Result<spaces::Model, SpaceError> {
3737 let space = authority::space_by_id(&service.state, caller, space_id).await?;
38 if !space.is_active {
38 if !authority::space_is_active(&space) {
3939 return Err(SpaceError::NotFound);
4040 }
4141 Ok(space)
Modified lib/space/src/share/mod.rs View lib/space/src/share/mod.rs
@@ -1,4 +1,6 @@
11pub mod authz;
2pub mod crypto;
23pub mod dir_visibility;
34pub mod guard;
45pub mod lookup;
6pub mod validate;
Added lib/space/src/share/validate.rs View lib/space/src/share/validate.rs

Diff too large to display.

Modified lib/space/src/authority.rs View lib/space/src/authority.rs
@@ -1,14 +1,18 @@
1use std::collections::HashMap;
2
13use export::AppState;
24use models::space::space_extras::{self, Entity as SpaceExtras};
35use models::space::space_member_extras::{self, Entity as MemberExtras};
4use models::space::space_members::{self, MemberStatus, MessageNotifications};
5use models::space::spaces::{self, AllowJoinLevel};
6use proto::auth as proto;
7use sea_orm::{ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, QueryFilter, Set};
6use models::space::space_members::{self, Entity as Members};
7use models::space::spaces::{self, Entity as Spaces};
8use models::space::{AllowJoinLevel, SpaceJoinPolicy, SpaceStatus};
9use sea_orm::{ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, QueryFilter, QuerySelect, Set};
810use time::OffsetDateTime;
911use uuid::Uuid;
1012
1113use crate::errors::SpaceError;
14use crate::services::dto::{MemberDto, SpaceDto, UserDto, resolve_space_image_url};
15use crate::services::service::SpaceService;
1216
1317pub const RPC_KEY: &str = "kenoauth is not configured";
1418
@@ -19,118 +23,83 @@
1923 .ok_or_else(|| SpaceError::Internal(RPC_KEY.to_string()))
2024}
2125
22fn timestamp(value: &Option<prost_types::Timestamp>) -> Option<OffsetDateTime> {
23 let value = value.as_ref()?;
24 OffsetDateTime::from_unix_timestamp(value.seconds)
25 .ok()?
26 .replace_nanosecond(value.nanos.clamp(0, 999_999_999) as u32)
27 .ok()
26pub fn to_time(value: chrono::DateTime<chrono::Utc>) -> OffsetDateTime {
27 OffsetDateTime::from_unix_timestamp(value.timestamp())
28 .map(|row| {
29 row.replace_nanosecond(value.timestamp_subsec_nanos())
30 .unwrap_or(row)
31 })
32 .unwrap_or_else(|_| OffsetDateTime::now_utc())
2833}
2934
30fn join_policy(value: i32) -> &'static str {
31 match proto::v1::OrganizationJoinPolicy::try_from(value)
32 .unwrap_or(proto::v1::OrganizationJoinPolicy::InviteOnly)
33 {
34 proto::v1::OrganizationJoinPolicy::Open => "open",
35 proto::v1::OrganizationJoinPolicy::Approval => "approval",
36 proto::v1::OrganizationJoinPolicy::Private => "private",
37 _ => "invite_only",
38 }
35pub fn to_time_opt(value: Option<chrono::DateTime<chrono::Utc>>) -> Option<OffsetDateTime> {
36 value.map(to_time)
3937}
4038
41fn status_of_member(member: &proto::v1::OrganizationMemberInfo) -> MemberStatus {
42 if member.banned_at.is_some() {
43 MemberStatus::Banned
44 } else if member.left_at.is_some() {
45 MemberStatus::Left
46 } else {
47 MemberStatus::Active
39pub fn space_dto(
40 record: &spaces::Model,
41 extras: Option<&space_extras::Model>,
42 nums_members: i64,
43 owners: &HashMap<i64, UserDto>,
44) -> SpaceDto {
45 SpaceDto {
46 id: record.id,
47 lower_name: record.slug.clone(),
48 name: record.name.clone(),
49 description: record.description.clone(),
50 owner: owners.get(&record.owner_user_id).cloned(),
51 avatar_url: resolve_space_image_url(
52 &record.slug,
53 "avatar",
54 extras
55 .and_then(|row| row.avatar_url.clone())
56 .or_else(|| record.avatar_url.clone()),
57 ),
58 is_private: record.join_policy == SpaceJoinPolicy::Private,
59 is_active: record.status == SpaceStatus::Active && record.archived_at.is_none(),
60 allow_join: record.allow_join,
61 allow_invite: record.allow_invite,
62 members_count: nums_members,
63 welcome_message: record.welcome_message.clone(),
64 allow_join_level: AllowJoinLevel::from_i16(record.allow_join_level),
65 member_must_enable_2fa: record.member_must_enable_2fa,
66 welcome_object: extras.and_then(|row| row.welcome_object),
67 created_at: to_time(record.created_at),
68 updated_at: to_time(record.updated_at),
4869 }
4970}
5071
51fn non_empty(value: &str) -> Option<String> {
52 let value = value.trim();
53 (!value.is_empty()).then(|| value.to_string())
72pub fn space_is_active(space: &spaces::Model) -> bool {
73 space.status == SpaceStatus::Active && space.archived_at.is_none()
5474}
5575
56pub fn space_from_info(
57 info: &proto::v1::OrganizationInfo,
58 extras: Option<&space_extras::Model>,
59) -> spaces::Model {
60 let id = Uuid::parse_str(info.id.trim()).unwrap_or_default();
61 let now = OffsetDateTime::now_utc();
62 let policy = join_policy(info.join_policy);
63 let status = proto::v1::OrganizationStatus::try_from(info.status)
64 .unwrap_or(proto::v1::OrganizationStatus::Active);
65 spaces::Model {
66 id,
67 lower_name: info.slug.clone(),
68 name: info.name.clone(),
69 description: non_empty(&info.description),
70 owner_id: info.owner_user_id,
71 avatar_url: extras
72 .and_then(|row| row.avatar_url.clone())
73 .or_else(|| non_empty(&info.avatar_url)),
74 is_private: policy == "private",
75 is_active: status == proto::v1::OrganizationStatus::Active,
76 allow_join: info.allow_join,
77 allow_invite: info.allow_invite,
78 invite_sort_url_hash: extras.and_then(|row| row.invite_sort_url_hash.clone()),
79 nums_members: info.member_count as i64,
80 banner_url: extras.and_then(|row| row.banner_url.clone()),
81 banner_width: extras.and_then(|row| row.banner_width),
82 banner_height: extras.and_then(|row| row.banner_height),
83 invite_banner_url: extras.and_then(|row| row.invite_banner_url.clone()),
84 invite_banner_width: extras.and_then(|row| row.invite_banner_width),
85 invite_banner_height: extras.and_then(|row| row.invite_banner_height),
86 embed_splash_url: extras.and_then(|row| row.embed_splash_url.clone()),
87 embed_splash_width: extras.and_then(|row| row.embed_splash_width),
88 embed_splash_height: extras.and_then(|row| row.embed_splash_height),
89 welcome_message: non_empty(&info.welcome_message),
90 allow_join_level: AllowJoinLevel::from_i16(info.allow_join_level as i16),
91 member_must_enable_2fa: info.member_must_enable_2fa,
92 welcome_object: extras.and_then(|row| row.welcome_object),
93 join_policy: policy.to_string(),
94 require_email_verified: info.require_email_verified,
95 require_mfa: info.require_mfa,
96 version: info.version,
97 created_at: timestamp(&info.created_at).unwrap_or(now),
98 updated_at: timestamp(&info.updated_at).unwrap_or(now),
99 }
100}
101
102pub fn member_from_info(
103 info: &proto::v1::OrganizationMemberInfo,
76pub fn member_dto(
77 record: &space_members::Model,
10478 extras: Option<&space_member_extras::Model>,
105) -> space_members::Model {
106 let now = OffsetDateTime::now_utc();
107 let joined_at = timestamp(&info.joined_at).unwrap_or(now);
108 space_members::Model {
109 space_id: Uuid::parse_str(info.organization_id.trim()).unwrap_or_default(),
110 user_id: info.user_id,
111 alias_name: non_empty(&info.alias_name),
112 alias_avatar_url: non_empty(&info.alias_avatar_url),
113 status: status_of_member(info),
114 last_active_at: timestamp(&info.last_active_at),
115 left_at: timestamp(&info.left_at),
79 users: &HashMap<i64, UserDto>,
80) -> MemberDto {
81 MemberDto {
82 space_id: record.organization_id,
83 user: users.get(&record.user_id).cloned(),
84 alias_name: record.alias_name.clone(),
85 alias_avatar_url: record.alias_avatar_url.clone(),
86 status: record.status(),
87 last_active_at: to_time_opt(record.last_active_at),
88 left_at: to_time_opt(record.left_at),
11689 invited_at: extras.and_then(|row| row.invited_at),
117 invited_by: (info.invited_by > 0).then_some(info.invited_by),
118 joined_at,
119 created_at: extras.map(|row| row.created_at).unwrap_or(joined_at),
90 invited_by: record.invited_by.and_then(|uid| users.get(&uid).cloned()),
91 joined_at: to_time(record.joined_at),
92 created_at: extras
93 .map(|row| row.created_at)
94 .unwrap_or_else(|| to_time(record.joined_at)),
12095 updated_at: extras.map(|row| row.updated_at),
121 message_notifications: MessageNotifications::from_str(&info.message_notifications),
122 muted: info.muted,
123 mute_until: timestamp(&info.mute_until),
124 mobile_push: info.mobile_push,
125 suppress_everyone: info.suppress_everyone,
126 suppress_roles: info.suppress_roles,
127 hide_muted_channels: info.hide_muted_channels,
128 banned_at: timestamp(&info.banned_at),
129 ban_reason: non_empty(&info.ban_reason),
130 ban_expires_at: timestamp(&info.ban_expires_at),
131 ban_allow_speak: info.ban_allow_speak,
132 ban_allow_push_repo: info.ban_allow_push_repo,
133 ban_allow_comment_any: info.ban_allow_comment_any,
96 message_notifications: record.message_notifications,
97 muted: record.muted,
98 mute_until: to_time_opt(record.mute_until),
99 mobile_push: record.mobile_push,
100 suppress_everyone: record.suppress_everyone,
101 suppress_roles: record.suppress_roles,
102 hide_muted_channels: record.hide_muted_channels,
134103 }
135104}
136105
@@ -153,54 +122,64 @@
153122 .await?)
154123}
155124
125pub async fn space_avatar(
126 state: &AppState,
127 space: &spaces::Model,
128) -> Result<Option<String>, SpaceError> {
129 Ok(extras(state, space.id)
130 .await?
131 .and_then(|row| row.avatar_url)
132 .or_else(|| space.avatar_url.clone()))
133}
134
135pub async fn member_count(state: &AppState, space_id: Uuid) -> Result<i64, SpaceError> {
136 use sea_orm::PaginatorTrait;
137
138 let count = Members::find()
139 .filter(space_members::Column::OrganizationId.eq(space_id))
140 .filter(space_members::Column::LeftAt.is_null())
141 .filter(space_members::Column::BannedAt.is_null())
142 .count(state.database.read())
143 .await?;
144 Ok(count as i64)
145}
146
156147pub async fn space_by_id(
157148 state: &AppState,
158 actor: i64,
149 _actor: i64,
159150 space_id: Uuid,
160151) -> Result<spaces::Model, SpaceError> {
161 let response = client(state)?
162 .get_organization(actor, &space_id.to_string(), "")
163 .await?;
164 let info = response.organization.ok_or(SpaceError::NotFound)?;
165 let extras = extras(state, space_id).await?;
166 Ok(space_from_info(&info, extras.as_ref()))
152 Spaces::find_by_id(space_id)
153 .filter(spaces::Column::DeletedAt.is_null())
154 .one(state.database.read())
155 .await?
156 .ok_or(SpaceError::NotFound)
167157}
168158
169159pub async fn space_by_slug(
170160 state: &AppState,
171 actor: i64,
161 _actor: i64,
172162 slug: &str,
173163) -> Result<spaces::Model, SpaceError> {
174 let response = client(state)?.get_organization(actor, "", slug).await?;
175 let info = response.organization.ok_or(SpaceError::NotFound)?;
176 let id = Uuid::parse_str(info.id.trim()).unwrap_or_default();
177 let extras = extras(state, id).await?;
178 Ok(space_from_info(&info, extras.as_ref()))
164 Spaces::find()
165 .filter(spaces::Column::Slug.eq(slug.trim().to_lowercase()))
166 .filter(spaces::Column::DeletedAt.is_null())
167 .one(state.database.read())
168 .await?
169 .ok_or(SpaceError::NotFound)
179170}
180171
181172pub async fn member(
182173 state: &AppState,
183 actor: i64,
174 _actor: i64,
184175 space_id: Uuid,
185176 user_id: i64,
186177) -> Result<Option<space_members::Model>, SpaceError> {
187 let client = client(state)?;
188 let info = match client
189 .get_member(actor, &space_id.to_string(), user_id)
190 .await
191 {
192 Ok(Some(member)) => member,
193 Ok(None) => return Ok(None),
194 Err(export::KenAuthError::Rejected(message))
195 if message.to_lowercase().contains("forbidden")
196 || message.to_lowercase().contains("permission") =>
197 {
198 return Ok(None);
199 }
200 Err(error) => return Err(error.into()),
201 };
202 let extras = member_extras(state, space_id, user_id).await?;
203 Ok(Some(member_from_info(&info, extras.as_ref())))
178 Ok(Members::find()
179 .filter(space_members::Column::OrganizationId.eq(space_id))
180 .filter(space_members::Column::UserId.eq(user_id))
181 .one(state.database.read())
182 .await?)
204183}
205184
206185pub async fn require_member(
@@ -209,168 +188,78 @@
209188 user_id: i64,
210189) -> Result<space_members::Model, SpaceError> {
211190 match member(state, user_id, space_id, user_id).await? {
212 Some(member) if member.status == MemberStatus::Active => Ok(member),
191 Some(member) if member.status() == models::space::MemberStatus::Active => Ok(member),
213192 _ => Err(SpaceError::Forbidden),
214193 }
215194}
216195
217196pub async fn list_members(
218197 state: &AppState,
219 actor: i64,
198 _actor: i64,
220199 space_id: Uuid,
221200 include_inactive: bool,
222201 include_banned: bool,
223202) -> Result<Vec<space_members::Model>, SpaceError> {
224 let client = client(state)?;
225 let mut members = Vec::new();
226 let mut token = String::new();
227 loop {
228 let response = client
229 .list_members(
230 actor,
231 &space_id.to_string(),
232 include_inactive,
233 include_banned,
234 proto::common::v1::PageRequest {
235 page_size: 100,
236 page_token: token.clone(),
237 },
238 )
239 .await?;
240 members.extend(response.members);
241 let next = response
242 .page
243 .map(|page| page.next_page_token)
244 .unwrap_or_default();
245 if next.is_empty() {
246 break;
247 }
248 token = next;
203 let mut query = Members::find().filter(space_members::Column::OrganizationId.eq(space_id));
204 if !include_inactive {
205 query = query.filter(space_members::Column::LeftAt.is_null());
249206 }
250
251 let user_ids: Vec<i64> = members.iter().map(|member| member.user_id).collect();
252 let extras = if user_ids.is_empty() {
253 Vec::new()
254 } else {
255 MemberExtras::find()
256 .filter(space_member_extras::Column::SpaceId.eq(space_id))
257 .filter(space_member_extras::Column::UserId.is_in(user_ids))
258 .all(state.database.read())
259 .await?
260 };
261 let mut extras: std::collections::HashMap<i64, space_member_extras::Model> =
262 extras.into_iter().map(|row| (row.user_id, row)).collect();
263 Ok(members
264 .iter()
265 .map(|info| member_from_info(info, extras.remove(&info.user_id).as_ref()))
266 .collect())
207 if !include_banned {
208 query = query.filter(space_members::Column::BannedAt.is_null());
209 }
210 Ok(query.all(state.database.read()).await?)
267211}
268212
269213pub async fn list_user_spaces(
270214 state: &AppState,
271 actor: i64,
215 _actor: i64,
272216 user_id: i64,
273217) -> Result<Vec<spaces::Model>, SpaceError> {
274 let client = client(state)?;
275 let mut organizations = Vec::new();
276 let mut token = String::new();
277 loop {
278 let response = client
279 .list_organizations(
280 actor,
281 user_id,
282 false,
283 proto::common::v1::PageRequest {
284 page_size: 100,
285 page_token: token.clone(),
286 },
287 )
288 .await?;
289 organizations.extend(response.organizations);
290 let next = response
291 .page
292 .map(|page| page.next_page_token)
293 .unwrap_or_default();
294 if next.is_empty() {
295 break;
296 }
297 token = next;
298 }
299
300 let ids: Vec<Uuid> = organizations
301 .iter()
302 .filter_map(|info| Uuid::parse_str(info.id.trim()).ok())
303 .collect();
304 let extras = if ids.is_empty() {
305 Vec::new()
306 } else {
307 SpaceExtras::find()
308 .filter(space_extras::Column::SpaceId.is_in(ids))
309 .all(state.database.read())
310 .await?
311 };
312 let mut extras: std::collections::HashMap<Uuid, space_extras::Model> =
313 extras.into_iter().map(|row| (row.space_id, row)).collect();
314 Ok(organizations
315 .iter()
316 .map(|info| {
317 let id = Uuid::parse_str(info.id.trim()).unwrap_or_default();
318 space_from_info(info, extras.remove(&id).as_ref())
319 })
320 .collect())
218 let space_ids: Vec<Uuid> = Members::find()
219 .filter(space_members::Column::UserId.eq(user_id))
220 .filter(space_members::Column::LeftAt.is_null())
221 .filter(space_members::Column::BannedAt.is_null())
222 .select_only()
223 .column(space_members::Column::OrganizationId)
224 .into_tuple()
225 .all(state.database.read())
226 .await?;
227 spaces_by_ids(state, space_ids).await
321228}
322229
323/// Public directory (ACTIVE, non-PRIVATE). Authority is KenOAuth; the local
324/// extras row only contributes presentation fields.
230/// Public directory (ACTIVE, joinable). Presentation fields come from the extras row.
325231pub async fn list_public_spaces(
326232 state: &AppState,
327233 query: &str,
328234) -> Result<Vec<spaces::Model>, SpaceError> {
329 let client = client(state)?;
330 let mut organizations = Vec::new();
331 let mut token = String::new();
332 loop {
333 let response = client
334 .list_public_organizations(
335 query,
336 proto::common::v1::PageRequest {
337 page_size: 100,
338 page_token: token.clone(),
339 },
340 )
341 .await?;
342 organizations.extend(response.organizations);
343 let next = response
344 .page
345 .map(|page| page.next_page_token)
346 .unwrap_or_default();
347 if next.is_empty() || organizations.len() >= 1_000 {
348 break;
349 }
350 token = next;
235 use sea_orm::Condition;
236
237 let mut select = Spaces::find()
238 .filter(spaces::Column::Status.eq(SpaceStatus::Active))
239 .filter(spaces::Column::DeletedAt.is_null())
240 .filter(
241 spaces::Column::JoinPolicy.is_in([SpaceJoinPolicy::Open, SpaceJoinPolicy::Approval]),
242 );
243 let needle = query.trim();
244 if !needle.is_empty() {
245 select = select.filter(
246 Condition::any()
247 .add(spaces::Column::Name.contains(needle))
248 .add(spaces::Column::Slug.contains(needle)),
249 );
351250 }
251 Ok(select.limit(1_000).all(state.database.read()).await?)
252}
352253
353 let ids: Vec<Uuid> = organizations
354 .iter()
355 .filter_map(|info| Uuid::parse_str(info.id.trim()).ok())
356 .collect();
357 let extras = if ids.is_empty() {
358 Vec::new()
359 } else {
360 SpaceExtras::find()
361 .filter(space_extras::Column::SpaceId.is_in(ids))
362 .all(state.database.read())
363 .await?
364 };
365 let mut extras: std::collections::HashMap<Uuid, space_extras::Model> =
366 extras.into_iter().map(|row| (row.space_id, row)).collect();
367 Ok(organizations
368 .iter()
369 .map(|info| {
370 let id = Uuid::parse_str(info.id.trim()).unwrap_or_default();
371 space_from_info(info, extras.remove(&id).as_ref())
372 })
373 .collect())
254async fn spaces_by_ids(state: &AppState, ids: Vec<Uuid>) -> Result<Vec<spaces::Model>, SpaceError> {
255 if ids.is_empty() {
256 return Ok(Vec::new());
257 }
258 Ok(Spaces::find()
259 .filter(spaces::Column::Id.is_in(ids))
260 .filter(spaces::Column::DeletedAt.is_null())
261 .all(state.database.read())
262 .await?)
374263}
375264
376265pub async fn account_created_at(
@@ -378,7 +267,15 @@
378267 user_id: i64,
379268) -> Result<Option<OffsetDateTime>, SpaceError> {
380269 let user = client(state)?.get_authority_user(user_id).await?;
381 Ok(timestamp(&user.created_at))
270 let Some(created) = user.created_at else {
271 return Ok(None);
272 };
273 Ok(OffsetDateTime::from_unix_timestamp(created.seconds)
274 .ok()
275 .map(|row| {
276 row.replace_nanosecond(created.nanos.clamp(0, 999_999_999) as u32)
277 .unwrap_or(row)
278 }))
382279}
383280
384281pub async fn mfa_enabled(state: &AppState, user_id: i64) -> Result<bool, SpaceError> {
@@ -397,11 +294,11 @@
397294 user_id: i64,
398295 actions: &[String],
399296) -> Result<Vec<bool>, SpaceError> {
400 let client = client(state)?;
401 let response = client
402 .check_permissions(user_id, &space_id.to_string(), user_id, actions.to_vec())
297 let service = SpaceService::new(state.clone());
298 let checks = service
299 .check_permissions(user_id, space_id, user_id, actions, None)
403300 .await?;
404 Ok(response.allowed)
301 Ok(checks.into_iter().map(|check| check.allowed).collect())
405302}
406303
407304pub async fn upsert_extras(
Modified lib/space/src/errors.rs View lib/space/src/errors.rs
@@ -1,3 +1,4 @@
1use actix_web::http::StatusCode;
12use std::fmt;
23
34pub use export::is_unique_violation;
@@ -14,8 +15,31 @@
1415 Conflict(String),
1516 RateLimited(String),
1617 Internal(String),
18 MalformedRequest,
19 InvalidEmail,
20 InvalidPermission { permission: String },
21 SpaceNotFound,
22 SpaceMemberNotFound,
23 SpaceRoleNotFound,
24 SpaceInviteNotFound,
25 SpaceSlugTaken,
26 SpaceMemberExists,
27 SpaceRoleKeyTaken,
28 SpaceRoleInUse,
29 SpaceRoleSystem,
30 SpaceInviteExhausted,
31 SpaceInviteExpired,
32 SpaceOwnerRequired,
33 SpaceHasMembers,
34 SpaceInactive,
35 SpaceMemberBanned,
36 VersionMismatch { current: i32 },
37 UserNotFound,
38 ScopeInsufficient { required: String },
1739}
1840
41pub type SpaceResult<T> = Result<T, SpaceError>;
42
1943impl fmt::Display for SpaceError {
2044 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2145 match self {
@@ -29,6 +53,33 @@
2953 Self::Conflict(message) => write!(f, "{message}"),
3054 Self::RateLimited(message) => write!(f, "{message}"),
3155 Self::Internal(message) => write!(f, "{message}"),
56 Self::MalformedRequest => write!(f, "request payload is malformed"),
57 Self::InvalidEmail => write!(f, "email is invalid"),
58 Self::InvalidPermission { permission } => {
59 write!(f, "permission `{permission}` is not recognized")
60 }
61 Self::SpaceNotFound => write!(f, "space does not exist"),
62 Self::SpaceMemberNotFound => write!(f, "space member does not exist"),
63 Self::SpaceRoleNotFound => write!(f, "space role does not exist"),
64 Self::SpaceInviteNotFound => write!(f, "space invite does not exist"),
65 Self::SpaceSlugTaken => write!(f, "space slug is already taken"),
66 Self::SpaceMemberExists => write!(f, "user is already a member of this space"),
67 Self::SpaceRoleKeyTaken => write!(f, "space role key is already used"),
68 Self::SpaceRoleInUse => write!(f, "space role is still assigned to members"),
69 Self::SpaceRoleSystem => write!(f, "space role is built in and cannot be modified"),
70 Self::SpaceInviteExhausted => write!(f, "space invite is exhausted or revoked"),
71 Self::SpaceInviteExpired => write!(f, "space invite has expired"),
72 Self::SpaceOwnerRequired => {
73 write!(f, "space must have an owner; transfer ownership first")
74 }
75 Self::SpaceHasMembers => write!(f, "space still has active members"),
76 Self::SpaceInactive => write!(f, "space is not active"),
77 Self::SpaceMemberBanned => write!(f, "space membership is banned"),
78 Self::VersionMismatch { current } => {
79 write!(f, "resource version mismatch, current version is {current}")
80 }
81 Self::UserNotFound => write!(f, "user does not exist"),
82 Self::ScopeInsufficient { required } => write!(f, "scope `{required}` is missing"),
3283 }
3384 }
3485}
@@ -52,6 +103,12 @@
52103 }
53104}
54105
106impl From<session::SessionError> for SpaceError {
107 fn from(_: session::SessionError) -> Self {
108 Self::Unauthorized
109 }
110}
111
55112impl From<export::KenAuthError> for SpaceError {
56113 fn from(error: export::KenAuthError) -> Self {
57114 match error {
@@ -112,6 +169,175 @@
112169 SpaceError::Conflict(msg) => Self::conflict(msg),
113170 SpaceError::RateLimited(msg) => Self::rate_limited(msg),
114171 SpaceError::Internal(message) => Self::internal(message),
172 SpaceError::MalformedRequest => Self::bad_request("request payload is malformed"),
173 SpaceError::InvalidEmail => Self::bad_request("email is invalid"),
174 SpaceError::InvalidPermission { permission } => {
175 Self::bad_request(format!("permission `{permission}` is not recognized"))
176 }
177 SpaceError::SpaceNotFound => Self::not_found("space does not exist"),
178 SpaceError::SpaceMemberNotFound => Self::not_found("space member does not exist"),
179 SpaceError::SpaceRoleNotFound => Self::not_found("space role does not exist"),
180 SpaceError::SpaceInviteNotFound => Self::not_found("space invite does not exist"),
181 SpaceError::UserNotFound => Self::not_found("user does not exist"),
182 SpaceError::SpaceSlugTaken => Self::conflict("space slug is already taken"),
183 SpaceError::SpaceMemberExists => {
184 Self::conflict("user is already a member of this space")
185 }
186 SpaceError::SpaceRoleKeyTaken => Self::conflict("space role key is already used"),
187 SpaceError::SpaceHasMembers => Self::conflict("space still has active members"),
188 SpaceError::VersionMismatch { .. } => Self::build(
189 409,
190 Self::payload(409, "resource version mismatch").with_error_code("version_mismatch"),
191 ),
192 SpaceError::SpaceInactive => {
193 Self::forbidden_with_code("space_inactive", "space is not active".to_string())
194 }
195 SpaceError::SpaceMemberBanned => Self::forbidden_with_code(
196 "space_member_banned",
197 "space membership is banned".to_string(),
198 ),
199 SpaceError::ScopeInsufficient { required } => Self::forbidden_with_code(
200 "scope_insufficient",
201 format!("scope `{required}` is missing"),
202 ),
203 SpaceError::SpaceRoleInUse => Self::build(
204 412,
205 Self::payload(412, "space role is still assigned to members")
206 .with_error_code("space_role_in_use"),
207 ),
208 SpaceError::SpaceRoleSystem => Self::build(
209 412,
210 Self::payload(412, "space role is built in and cannot be modified")
211 .with_error_code("space_role_system"),
212 ),
213 SpaceError::SpaceInviteExhausted => Self::build(
214 412,
215 Self::payload(412, "space invite is exhausted or revoked")
216 .with_error_code("space_invite_exhausted"),
217 ),
218 SpaceError::SpaceInviteExpired => Self::build(
219 412,
220 Self::payload(412, "space invite has expired")
221 .with_error_code("space_invite_expired"),
222 ),
223 SpaceError::SpaceOwnerRequired => Self::build(
224 412,
225 Self::payload(412, "space must have an owner; transfer ownership first")
226 .with_error_code("space_owner_required"),
227 ),
115228 }
116229 }
117230}
231
232impl SpaceError {
233 pub fn internal(message: impl Into<String>) -> Self {
234 Self::Internal(message.into())
235 }
236
237 pub fn status_code(&self) -> StatusCode {
238 match self {
239 Self::Unauthorized => StatusCode::UNAUTHORIZED,
240 Self::Forbidden
241 | Self::JoinNotAllowed(_)
242 | Self::JoinRequirementNotMet(_)
243 | Self::JoinRequestCooldown(_)
244 | Self::SpaceInactive
245 | Self::SpaceMemberBanned
246 | Self::ScopeInsufficient { .. } => StatusCode::FORBIDDEN,
247 Self::NotFound
248 | Self::SpaceNotFound
249 | Self::SpaceMemberNotFound
250 | Self::SpaceRoleNotFound
251 | Self::SpaceInviteNotFound
252 | Self::UserNotFound => StatusCode::NOT_FOUND,
253 Self::Conflict(_)
254 | Self::SpaceSlugTaken
255 | Self::SpaceMemberExists
256 | Self::SpaceRoleKeyTaken
257 | Self::SpaceHasMembers
258 | Self::VersionMismatch { .. } => StatusCode::CONFLICT,
259 Self::SpaceRoleInUse
260 | Self::SpaceRoleSystem
261 | Self::SpaceInviteExhausted
262 | Self::SpaceInviteExpired
263 | Self::SpaceOwnerRequired => StatusCode::PRECONDITION_FAILED,
264 Self::RateLimited(_) => StatusCode::TOO_MANY_REQUESTS,
265 Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
266 Self::BadRequest(_)
267 | Self::MalformedRequest
268 | Self::InvalidEmail
269 | Self::InvalidPermission { .. } => StatusCode::BAD_REQUEST,
270 }
271 }
272
273 pub fn code(&self) -> &'static str {
274 match self {
275 Self::BadRequest(_) => "bad_request",
276 Self::Unauthorized => "unauthorized",
277 Self::Forbidden => "forbidden",
278 Self::JoinNotAllowed(_) => "join_not_allowed",
279 Self::JoinRequirementNotMet(_) => "join_requirement_not_met",
280 Self::JoinRequestCooldown(_) => "join_request_cooldown",
281 Self::NotFound => "not_found",
282 Self::Conflict(_) => "conflict",
283 Self::RateLimited(_) => "rate_limited",
284 Self::Internal(_) => "internal",
285 Self::MalformedRequest => "malformed_request",
286 Self::InvalidEmail => "invalid_email",
287 Self::InvalidPermission { .. } => "invalid_permission",
288 Self::SpaceNotFound => "space_not_found",
289 Self::SpaceMemberNotFound => "space_member_not_found",
290 Self::SpaceRoleNotFound => "space_role_not_found",
291 Self::SpaceInviteNotFound => "space_invite_not_found",
292 Self::SpaceSlugTaken => "space_slug_taken",
293 Self::SpaceMemberExists => "space_member_exists",
294 Self::SpaceRoleKeyTaken => "space_role_key_taken",
295 Self::SpaceRoleInUse => "space_role_in_use",
296 Self::SpaceRoleSystem => "space_role_system",
297 Self::SpaceInviteExhausted => "space_invite_exhausted",
298 Self::SpaceInviteExpired => "space_invite_expired",
299 Self::SpaceOwnerRequired => "space_owner_required",
300 Self::SpaceHasMembers => "space_has_members",
301 Self::SpaceInactive => "space_inactive",
302 Self::SpaceMemberBanned => "space_member_banned",
303 Self::VersionMismatch { .. } => "version_mismatch",
304 Self::UserNotFound => "user_not_found",
305 Self::ScopeInsufficient { .. } => "scope_insufficient",
306 }
307 }
308
309 pub fn is_internal(&self) -> bool {
310 self.status_code().is_server_error()
311 }
312
313 pub fn message(&self) -> String {
314 if self.is_internal() {
315 "internal server error".to_string()
316 } else {
317 self.to_string()
318 }
319 }
320
321 pub fn body(&self) -> ErrorBody {
322 ErrorBody {
323 code: self.code().to_string(),
324 message: self.message(),
325 }
326 }
327}
328
329#[derive(Debug, serde::Serialize)]
330pub struct ErrorBody {
331 pub code: String,
332 pub message: String,
333}
334
335impl actix_web::ResponseError for SpaceError {
336 fn status_code(&self) -> StatusCode {
337 self.status_code()
338 }
339
340 fn error_response(&self) -> actix_web::HttpResponse {
341 actix_web::HttpResponse::build(self.status_code()).json(self.body())
342 }
343}
Modified lib/space/src/openapi.rs View lib/space/src/openapi.rs
@@ -4,15 +4,15 @@
44#[derive(OpenApi)]
55#[openapi(
66 paths(
7 crate::handlers::get_space::get_space,
7 crate::handlers::space_admin::get_space,
88 crate::handlers::get_group::get_group,
99 crate::handlers::get_space_image::get_space_image,
1010 crate::handlers::get_space_image_file::get_space_image_file,
11 crate::handlers::create_space::create_space,
12 crate::handlers::list_spaces::list_spaces,
13 crate::handlers::update_space::update_space,
14 crate::handlers::delete_space::delete_space,
15 crate::handlers::delete_space::restore_space,
11 crate::handlers::space_admin::create_space,
12 crate::handlers::space_admin::list_spaces,
13 crate::handlers::space_admin::update_space,
14 crate::handlers::space_admin::delete_space,
15 crate::handlers::space_admin::restore_space,
1616 crate::handlers::upload_space_image::upload_space_image,
1717 crate::handlers::delete_space_image::delete_space_image,
1818 crate::handlers::explore_spaces::explore_spaces,
@@ -32,13 +32,11 @@
3232 crate::handlers::list_dir_user_overwrites::list_dir_user_overwrites,
3333 crate::handlers::set_dir_user_overwrite::set_dir_user_overwrite,
3434 crate::handlers::remove_dir_user_overwrite::remove_dir_user_overwrite,
35 crate::handlers::add_member::add_member,
36 crate::handlers::list_members::list_space_members,
37 crate::handlers::get_member::get_member,
38 crate::handlers::update_member::update_member,
39 crate::handlers::self_member::update_self_member,
40 crate::handlers::self_member::leave_space,
41 crate::handlers::remove_member::remove_member,
35 crate::handlers::space_admin::add_member,
36 crate::handlers::space_admin::get_member,
37 crate::handlers::space_admin::update_member,
38 crate::handlers::space_admin::update_self_member,
39 crate::handlers::space_admin::remove_member,
4240 crate::handlers::create_group::create_group,
4341 crate::handlers::list_groups::list_groups,
4442 crate::handlers::update_group::update_group,
@@ -51,32 +49,43 @@
5149 crate::handlers::list_join_rules::list_join_rules,
5250 crate::handlers::update_join_rule::update_join_rule,
5351 crate::handlers::delete_join_rule::delete_join_rule,
54 crate::handlers::create_invite::create_invite,
55 crate::handlers::list_invites::list_invites,
56 crate::handlers::revoke_invite::revoke_invite,
57 crate::handlers::accept_invite::accept_invite,
58 crate::handlers::reject_invite::reject_invite,
59 crate::handlers::ban_user::ban_user,
60 crate::handlers::list_bans::list_bans,
61 crate::handlers::get_ban::get_ban,
62 crate::handlers::update_ban::update_ban,
63 crate::handlers::unban_user::unban_user,
52 crate::handlers::space_admin::create_invite,
53 crate::handlers::space_admin::list_invites,
54 crate::handlers::space_admin::revoke_invite,
55 crate::handlers::space_admin::list_bans,
56 crate::handlers::space_admin::get_ban,
57 crate::handlers::space_admin::update_ban,
58 crate::handlers::space_admin::archive_space,
59 crate::handlers::space_admin::transfer_ownership,
60 crate::handlers::space_admin::list_members,
61 crate::handlers::space_admin::leave_organization,
62 crate::handlers::space_admin::set_member_role,
63 crate::handlers::space_admin::ban_member,
64 crate::handlers::space_admin::unban_member,
65 crate::handlers::space_admin::unban_by_user,
66 crate::handlers::space_admin::set_member_permissions,
67 crate::handlers::space_admin::list_roles,
68 crate::handlers::space_admin::get_role,
69 crate::handlers::space_admin::create_role,
70 crate::handlers::space_admin::update_role,
71 crate::handlers::space_admin::delete_role,
72 crate::handlers::space_admin::set_role_permissions,
73 crate::handlers::space_admin::assign_role,
74 crate::handlers::space_admin::unassign_role,
75 crate::handlers::space_admin::join_role,
76 crate::handlers::space_admin::leave_role,
77 crate::handlers::space_admin::check_permission,
78 crate::handlers::space_admin::redeem_invite,
6479 ),
6580 components(
6681 schemas(
67 AppResponseData<crate::services::spaces::SpaceDto>,
68 AppResponseData<crate::services::spaces::SpacePage>,
82 AppResponseData<crate::services::dto::SpaceDto>,
6983 AppResponseData<crate::services::join_requests::ExploreSpacePage>,
7084 AppResponseData<crate::services::join_requests::JoinRequestDto>,
7185 AppResponseData<crate::services::join_requests::JoinRequestPage>,
7286 AppResponseData<crate::services::groups::GroupDto>,
7387 AppResponseData<crate::services::groups::GroupPage>,
74 AppResponseData<crate::services::members::MemberDto>,
75 AppResponseData<crate::services::members::MemberPage>,
76 AppResponseData<crate::services::invites::InviteDto>,
77 AppResponseData<crate::services::invites::InvitePage>,
78 AppResponseData<crate::services::bans::BanDto>,
79 AppResponseData<crate::services::bans::BanPage>,
88 AppResponseData<crate::services::dto::MemberDto>,
8089 AppResponseData<crate::services::images::SpaceImageDto>,
8190 AppResponseData<Option<crate::services::images::SpaceImageDto>>,
8291 AppResponseData<crate::services::dir::DirNodeDto>,
@@ -86,7 +95,7 @@
8695 AppResponseData<Vec<crate::services::dir::GroupOverwriteDto>>,
8796 AppResponseData<crate::services::dir::UserOverwriteDto>,
8897 AppResponseData<Vec<crate::services::dir::UserOverwriteDto>>,
89 crate::services::spaces::SpaceDto,
98 crate::services::dto::SpaceDto,
9099 crate::services::join_requests::ExploreSpaceDto,
91100 crate::services::join_requests::ExploreSpacePage,
92101 crate::services::join_requests::ExploreViewerDto,
@@ -94,28 +103,15 @@
94103 crate::services::join_requests::JoinRequestDto,
95104 crate::services::join_requests::JoinRequestPage,
96105 crate::services::join_requests::CreateJoinRequestCmd,
97 models::space::space_join_request::SpaceJoinRequestStatus,
98 export::pagination::Page<crate::services::spaces::SpaceDto>,
99 crate::services::spaces::CreateSpaceCmd,
100 crate::services::spaces::UpdateSpaceCmd,
101 crate::services::spaces::UserDto,
106 models::space::SpaceJoinRequestState,
107 export::pagination::Page<crate::services::dto::SpaceDto>,
108 crate::services::dto::UserDto,
102109 crate::services::groups::GroupDto,
103110 crate::services::groups::GroupPage,
104111 crate::services::groups::CreateGroupCmd,
105112 crate::services::groups::UpdateGroupCmd,
106113 crate::services::groups::AssignGroupCmd,
107 crate::services::members::MemberDto,
108 crate::services::members::MemberPage,
109 crate::services::members::AddMemberCmd,
110 crate::services::members::UpdateMemberCmd,
111 crate::services::invites::InviteDto,
112 crate::services::invites::InvitePage,
113 crate::services::invites::CreateInviteCmd,
114 crate::services::invites::AcceptInviteCmd,
115 crate::services::bans::BanDto,
116 crate::services::bans::BanPage,
117 crate::services::bans::BanUserCmd,
118 crate::services::bans::UpdateBanCmd,
114 crate::services::dto::MemberDto,
119115 crate::services::images::AvatarFormat,
120116 crate::services::images::SpaceImageDto,
121117 crate::services::images::SpaceImageUploadQuery,
@@ -129,10 +125,38 @@
129125 crate::services::dir::GroupOverwriteDto,
130126 crate::services::dir::UserOverwriteDto,
131127 crate::services::dir::SetOverwriteCmd,
132 models::space::spaces::AllowJoinLevel,
133 models::space::space_members::MemberStatus,
134 models::space::space_members::MessageNotifications,
128 models::space::AllowJoinLevel,
129 models::space::MemberStatus,
130 models::space::MessageNotifications,
135131 models::dir::dirs::DirKind,
132 crate::services::types::OrganizationSummary,
133 crate::services::types::MemberSummary,
134 crate::services::types::RoleSummary,
135 crate::services::types::InviteSummary,
136 crate::services::types::PermissionGrant,
137 crate::services::types::PermissionCheck,
138 crate::services::types::CreateOrganizationBody,
139 crate::services::types::UpdateOrganizationBody,
140 crate::services::types::AddMemberBody,
141 crate::services::types::SetMemberRoleBody,
142 crate::services::types::BanMemberBody,
143 crate::services::types::CheckPermissionBody,
144 crate::services::types::CreateRoleBody,
145 crate::services::types::SetRolePermissionsBody,
146 crate::handlers::space_admin::SpaceMemberPath,
147 crate::handlers::space_admin::SpaceRolePath,
148 crate::handlers::space_admin::SpaceInvitePath,
149 crate::handlers::space_admin::OrgListQuery,
150 crate::handlers::space_admin::MemberListQuery,
151 crate::handlers::space_admin::UpdateRoleBody,
152 crate::handlers::space_admin::TransferBody,
153 crate::handlers::space_admin::ReasonBody,
154 crate::handlers::space_admin::DeleteOrgQuery,
155 crate::handlers::space_admin::CreateInviteBody,
156 crate::handlers::space_admin::RedeemInviteBody,
157 crate::handlers::space_admin::CreateInviteResponse,
158 crate::handlers::space_admin::RedeemInviteResponse,
159 crate::handlers::space_admin::AssignRoleBody,
136160 )
137161 ),
138162 tags(
Modified lib/space/src/routes.rs View lib/space/src/routes.rs
@@ -1,64 +1,43 @@
11use export::http::Router;
22use export::http::routing::{delete, get, patch, post, put};
33
4use crate::handlers::accept_invite::accept_invite;
5use crate::handlers::add_member::add_member;
64use crate::handlers::assign_group::assign_group;
7use crate::handlers::ban_user::ban_user;
85use crate::handlers::create_dir_node::create_dir_node;
96use crate::handlers::create_group::create_group;
10use crate::handlers::create_invite::create_invite;
117use crate::handlers::create_join_rule::create_join_rule;
12use crate::handlers::create_space::create_space;
138use crate::handlers::delete_group::delete_group;
149use crate::handlers::delete_join_rule::delete_join_rule;
15use crate::handlers::delete_space::{delete_space, restore_space};
1610use crate::handlers::delete_space_image::delete_space_image;
1711use crate::handlers::explore_spaces::{
1812 approve_join_request, explore_spaces, list_join_requests, reject_join_request,
1913};
20use crate::handlers::get_ban::get_ban;
2114use crate::handlers::get_group::get_group;
22use crate::handlers::get_member::get_member;
23use crate::handlers::get_space::get_space;
2415use crate::handlers::get_space_dir_tree::get_space_dir_tree;
2516use crate::handlers::get_space_image::get_space_image;
2617use crate::handlers::get_space_image_file::get_space_image_file;
2718use crate::handlers::join_group_self::join_group_self;
2819use crate::handlers::join_requests::{create_join_request, withdraw_join_request};
2920use crate::handlers::leave_group_self::leave_group_self;
30use crate::handlers::list_bans::list_bans;
3121use crate::handlers::list_dir_group_overwrites::list_dir_group_overwrites;
3222use crate::handlers::list_dir_user_overwrites::list_dir_user_overwrites;
3323use crate::handlers::list_groups::list_groups;
34use crate::handlers::list_invites::list_invites;
3524use crate::handlers::list_join_rules::list_join_rules;
36use crate::handlers::list_members::list_space_members;
37use crate::handlers::list_spaces::list_spaces;
3825use crate::handlers::move_dir_node::move_dir_node;
39use crate::handlers::reject_invite::reject_invite;
4026use crate::handlers::remove_dir_group_overwrite::remove_dir_group_overwrite;
4127use crate::handlers::remove_dir_user_overwrite::remove_dir_user_overwrite;
42use crate::handlers::remove_member::remove_member;
4328use crate::handlers::repair_dir_parents::repair_dir_parents;
44use crate::handlers::revoke_invite::revoke_invite;
4529use crate::handlers::set_dir_group_overwrite::set_dir_group_overwrite;
4630use crate::handlers::set_dir_user_overwrite::set_dir_user_overwrite;
4731use crate::handlers::unassign_group::unassign_group;
48use crate::handlers::unban_user::unban_user;
49use crate::handlers::update_ban::update_ban;
5032use crate::handlers::update_dir_node::update_dir_node;
5133use crate::handlers::update_group::update_group;
5234use crate::handlers::update_join_rule::update_join_rule;
53use crate::handlers::update_member::update_member;
54use crate::handlers::update_space::update_space;
5535use crate::handlers::upload_space_image::upload_space_image;
5636use crate::services::service::SpaceService;
5737
5838pub fn router(_service: SpaceService) -> Router<SpaceService> {
5939 let public: Router<SpaceService> = Router::new()
6040 .route("/spaces/explore", get(explore_spaces))
61 .route("/spaces/{space_name}", get(get_space))
6241 .route("/spaces/{space_name}/groups/{group_id}", get(get_group))
6342 .route("/spaces/{space_name}/images/{kind}", get(get_space_image))
6443 .route(
@@ -67,14 +46,6 @@
6746 );
6847
6948 let protected = Router::<SpaceService>::new()
70 .route("/spaces", post(create_space).get(list_spaces))
71 .route("/spaces/invites/accept", post(accept_invite))
72 .route("/spaces/invites/reject", post(reject_invite))
73 .route(
74 "/spaces/{space_name}",
75 patch(update_space).delete(delete_space),
76 )
77 .route("/spaces/{space_name}/restore", post(restore_space))
7849 .route(
7950 "/spaces/{space_name}/images/{kind}",
8051 put(upload_space_image)
@@ -111,19 +82,6 @@
11182 put(set_dir_user_overwrite).delete(remove_dir_user_overwrite),
11283 )
11384 .route(
114 "/spaces/{space_name}/members",
115 post(add_member).get(list_space_members),
116 )
117 .route(
118 "/spaces/{space_name}/members/{user_id}",
119 get(get_member).patch(update_member).delete(remove_member),
120 )
121 .route(
122 "/spaces/{space_name}/members/@me",
123 export::http::routing::patch(crate::handlers::self_member::update_self_member)
124 .delete(crate::handlers::self_member::leave_space),
125 )
126 .route(
12785 "/spaces/{space_name}/groups",
12886 post(create_group).get(list_groups),
12987 )
@@ -167,18 +125,111 @@
167125 "/spaces/{space_name}/join-requests/{request_id}/reject",
168126 post(reject_join_request),
169127 )
128 .route("/spaces", post(crate::handlers::space_admin::create_space))
129 .route("/spaces", get(crate::handlers::space_admin::list_spaces))
170130 .route(
171 "/spaces/{space_name}/invites",
172 post(create_invite).get(list_invites),
131 "/spaces/invites/redeem",
132 post(crate::handlers::space_admin::redeem_invite),
173133 )
174134 .route(
175 "/spaces/{space_name}/invites/{invite_id}",
176 delete(revoke_invite),
135 "/spaces/{space_name}",
136 get(crate::handlers::space_admin::get_space),
177137 )
178 .route("/spaces/{space_name}/bans", post(ban_user).get(list_bans))
179138 .route(
139 "/spaces/{space_name}",
140 patch(crate::handlers::space_admin::update_space),
141 )
142 .route(
143 "/spaces/{space_name}",
144 delete(crate::handlers::space_admin::delete_space),
145 )
146 .route(
147 "/spaces/{space_name}/archive",
148 post(crate::handlers::space_admin::archive_space),
149 )
150 .route(
151 "/spaces/{space_name}/restore",
152 post(crate::handlers::space_admin::restore_space),
153 )
154 .route(
155 "/spaces/{space_name}/transfer",
156 post(crate::handlers::space_admin::transfer_ownership),
157 )
158 .route(
159 "/spaces/{space_name}/leave",
160 post(crate::handlers::space_admin::leave_organization),
161 )
162 .route(
163 "/spaces/{space_name}/members",
164 get(crate::handlers::space_admin::list_members),
165 )
166 .route(
167 "/spaces/{space_name}/members/@me",
168 patch(crate::handlers::space_admin::update_self_member),
169 )
170 .route(
171 "/spaces/{space_name}/members/{member_id}/role",
172 patch(crate::handlers::space_admin::set_member_role),
173 )
174 .route(
175 "/spaces/{space_name}/members/{member_id}/ban",
176 post(crate::handlers::space_admin::ban_member),
177 )
178 .route(
179 "/spaces/{space_name}/members/{member_id}/unban",
180 post(crate::handlers::space_admin::unban_member),
181 )
182 .route(
183 "/spaces/{space_name}/members/{member_id}/permissions",
184 put(crate::handlers::space_admin::set_member_permissions),
185 )
186 .route(
180187 "/spaces/{space_name}/bans/{user_id}",
181 get(get_ban).patch(update_ban).delete(unban_user),
188 delete(crate::handlers::space_admin::unban_by_user),
189 )
190 .route(
191 "/spaces/{space_name}/roles",
192 get(crate::handlers::space_admin::list_roles),
193 )
194 .route(
195 "/spaces/{space_name}/roles",
196 post(crate::handlers::space_admin::create_role),
197 )
198 .route(
199 "/spaces/{space_name}/roles/assign",
200 post(crate::handlers::space_admin::assign_role),
201 )
202 .route(
203 "/spaces/{space_name}/roles/unassign",
204 post(crate::handlers::space_admin::unassign_role),
205 )
206 .route(
207 "/spaces/{space_name}/roles/{role_id}",
208 get(crate::handlers::space_admin::get_role),
209 )
210 .route(
211 "/spaces/{space_name}/roles/{role_id}",
212 patch(crate::handlers::space_admin::update_role),
213 )
214 .route(
215 "/spaces/{space_name}/roles/{role_id}/join",
216 post(crate::handlers::space_admin::join_role),
217 )
218 .route(
219 "/spaces/{space_name}/roles/{role_id}/join",
220 delete(crate::handlers::space_admin::leave_role),
221 )
222 .route(
223 "/spaces/{space_name}/roles/{role_id}",
224 delete(crate::handlers::space_admin::delete_role),
225 )
226 .route(
227 "/spaces/{space_name}/roles/{role_id}/permissions",
228 put(crate::handlers::space_admin::set_role_permissions),
229 )
230 .route(
231 "/spaces/{space_name}/permissions/check",
232 post(crate::handlers::space_admin::check_permission),
182233 )
183234 .wrap(export::http::Guard::Session);
184235
Modified lib/space/Cargo.toml View lib/space/Cargo.toml
@@ -5,22 +5,29 @@
55
66[dependencies]
77async-trait = { workspace = true }
8base64 = { workspace = true }
9cache = { workspace = true }
10chrono = "0.4.45"
811config = { workspace = true }
12db = { workspace = true }
913deletion = { workspace = true }
14envelope = { workspace = true }
1015export = { workspace = true }
1116events = { workspace = true }
1217proto = { workspace = true }
1318i18n = { workspace = true }
1419imaging = { workspace = true }
1520models = { workspace = true }
21rand = { workspace = true }
1622session = { workspace = true }
23sha2 = { workspace = true }
1724prost-types = { workspace = true }
1825storage = { workspace = true }
1926actix-web = { workspace = true }
2027sea-orm = { workspace = true }
2128serde = { workspace = true, features = ["derive"] }
2229serde_json = { workspace = true }
23utoipa = { workspace = true }
30utoipa = { workspace = true, features = ["chrono"] }
2431uuid = { workspace = true }
2532time = { workspace = true }
2633tracing = { workspace = true }
Modified lib/user/src/services/profile.rs View lib/user/src/services/profile.rs
@@ -120,8 +120,8 @@
120120 let limit = q.limit();
121121 let offset = q.offset();
122122 let mut space_rows = space::authority::list_user_spaces(&self.state, uid, uid).await?;
123 space_rows.retain(|space| space.is_active);
124 space_rows.sort_by(|a, b| a.lower_name.cmp(&b.lower_name));
123 space_rows.retain(space::authority::space_is_active);
124 space_rows.sort_by(|a, b| a.slug.cmp(&b.slug));
125125
126126 let mut out = Vec::new();
127127 for space in space_rows
@@ -133,19 +133,20 @@
133133 .await?
134134 .into_vec();
135135
136 let avatar_url = space::services::spaces::resolve_space_image_url(
137 &space.lower_name,
136 let avatar_url = space::services::dto::resolve_space_image_url(
137 &space.slug,
138138 "avatar",
139 space.avatar_url,
139 space::authority::space_avatar(&self.state, &space).await?,
140140 );
141 let members_count = space::authority::member_count(&self.state, space.id).await?;
141142 let space_dto = SpaceDto {
142143 id: space.id,
143144 name: space.name,
144 owner_uid: space.owner_id,
145 owner_uid: space.owner_user_id,
145146 avatar_url,
146 is_private: space.is_private,
147 members_count: space.nums_members,
148 created_at: space.created_at,
147 is_private: space.join_policy == models::space::SpaceJoinPolicy::Private,
148 members_count,
149 created_at: space::authority::to_time(space.created_at),
149150 };
150151 out.push(MySpaceDto {
151152 space: space_dto,
@@ -366,7 +367,7 @@
366367 user_id: target_uid,
367368 username: public.username.clone(),
368369 display_name,
369 avatar_url: space::services::spaces::resolve_user_avatar_url(target_uid, &avatar_raw),
370 avatar_url: space::services::dto::resolve_user_avatar_url(target_uid, &avatar_raw),
370371 visibility: if is_owner { Some(visibility) } else { None },
371372 real_name: None,
372373 bio: None,
@@ -432,10 +433,10 @@
432433 let target_spaces = space::authority::list_user_spaces(&service.state, target, target).await?;
433434 let target_ids: std::collections::HashSet<Uuid> = target_spaces
434435 .into_iter()
435 .filter(|space| space.is_active)
436 .filter(|space| space::authority::space_is_active(space))
436437 .map(|space| space.id)
437438 .collect();
438439 Ok(viewer_spaces
439440 .iter()
440 .any(|space| space.is_active && target_ids.contains(&space.id)))
441 .any(|space| space::authority::space_is_active(space) && target_ids.contains(&space.id)))
441442}
Modified lib/webapi/src/cleanup.rs View lib/webapi/src/cleanup.rs
@@ -149,7 +149,7 @@
149149 let space = lookup::lookup_space_by_name(&state.space, uid, space_name)
150150 .await
151151 .map_err(map_space)?;
152 if space.owner_id != uid {
152 if space.owner_user_id != uid {
153153 return Err(ApiError::forbidden());
154154 }
155155 Ok(space)
Modified view/auth/src/shared/api/auth/auth.schemas.ts View view/auth/src/shared/api/auth/auth.schemas.ts
@@ -5,23 +5,6 @@
55 * 认证与账号服务:工作量证明、口令与验证码登录、二次验证、注册、口令重置与 SSH 公钥。
66 * OpenAPI spec version: 1.0.0
77 */
8export type OrgMemberRole = "member" | "admin" | "owner";
9export interface AddMemberBody {
10 role?: null | OrgMemberRole;
11 role_keys?: string[];
12 user_id: number;
13}
14
15export interface AssignRoleBody {
16 member_id: string;
17 role_key: string;
18}
19
20export interface BanMemberBody {
21 /** @nullable */
22 reason?: string | null;
23}
24
258export interface ChallengeResponse {
269 context: string;
2710 /** @minimum 0 */
@@ -32,101 +15,6 @@
3215 issued_at: number;
3316}
3417
35export interface CheckPermissionBody {
36 permission: string;
37 /** @nullable */
38 user_id?: number | null;
39}
40
41export interface CreateInviteBody {
42 max_uses?: number;
43 /** @nullable */
44 role_id?: string | null;
45 /** @nullable */
46 target_user_id?: number | null;
47 /**
48 * @minimum 0
49 * @nullable
50 */
51 ttl_secs?: number | null;
52}
53
54export type OrgJoinMethod =
55 "direct" | "invite_link" | "invite_targeted" | "invite_email";
56export interface InviteSummary {
57 created_at: string;
58 created_by: number;
59 /** @nullable */
60 email?: string | null;
61 /** @nullable */
62 expires_at?: string | null;
63 id: string;
64 max_uses: number;
65 method: OrgJoinMethod;
66 organization_id: string;
67 /** @nullable */
68 revoked_at?: string | null;
69 /** @nullable */
70 role_id?: string | null;
71 /** @nullable */
72 target_user_id?: number | null;
73 uses: number;
74}
75
76export interface CreateInviteResponse {
77 invite: InviteSummary;
78 token: string;
79}
80
81export type OrganizationJoinPolicy =
82 "open" | "approval" | "invite_only" | "private";
83export interface CreateOrganizationBody {
84 /** @nullable */
85 allow_invite?: boolean | null;
86 /** @nullable */
87 allow_join?: boolean | null;
88 /** @nullable */
89 allow_join_level?: number | null;
90 /** @nullable */
91 avatar_url?: string | null;
92 /** @nullable */
93 description?: string | null;
94 join_policy?: null | OrganizationJoinPolicy;
95 /** @nullable */
96 member_must_enable_2fa?: boolean | null;
97 name: string;
98 /** @nullable */
99 require_email_verified?: boolean | null;
100 /** @nullable */
101 require_mfa?: boolean | null;
102 slug: string;
103 /** @nullable */
104 welcome_message?: string | null;
105}
106
107export type PermissionEffect = "allow" | "deny";
108export interface PermissionGrant {
109 effect: PermissionEffect;
110 permission: string;
111}
112
113export interface CreateRoleBody {
114 default_role?: boolean;
115 /** @nullable */
116 description?: string | null;
117 key: string;
118 name: string;
119 permissions?: PermissionGrant[];
120 rank?: number;
121 self_assignable?: boolean;
122}
123
124export interface DeleteOrgQuery {
125 force?: boolean;
126 /** @nullable */
127 reason?: string | null;
128}
129
13018export type EnvelopeChallengeResponseData = {
13119 context: string;
13220 /** @minimum 0 */
@@ -148,22 +36,6 @@
14836 request_id?: string;
14937}
15038
151export type EnvelopeCreateInviteResponseData = {
152 invite: InviteSummary;
153 token: string;
154};
155
156export interface EnvelopeCreateInviteResponse {
157 code: number;
158 data?: EnvelopeCreateInviteResponseData;
159 /** @nullable */
160 error?: string | null;
161 /** @nullable */
162 error_code?: string | null;
163 message: string;
164 request_id?: string;
165}
166
16739export type EnvelopeLoginCodeResponseData = {
16840 /** @minimum 0 */
16941 expires_in_secs: number;
@@ -222,108 +94,9 @@
22294 request_id?: string;
22395}
22496
225/**
226 * 会籍的通知偏好,对应旧 `space_members.message_notifications`。
227 * 旧表用 0/1/2 存,这里用字符串存,便于跨服务读写时直接透传。
228 */
229export type MessageNotifications = "all" | "mentions" | "nothing";
230export type EnvelopeMemberSummaryData = {
231 /** @nullable */
232 alias_avatar_url?: string | null;
233 /** @nullable */
234 alias_name?: string | null;
235 ban_allow_agent_join: boolean;
236 ban_allow_comment_any: boolean;
237 ban_allow_push_repo: boolean;
238 ban_allow_speak: boolean;
239 /** @nullable */
240 ban_expires_at?: string | null;
241 /** @nullable */
242 ban_reason?: string | null;
243 /** @nullable */
244 banned_at?: string | null;
245 /** @nullable */
246 displayname?: string | null;
247 hide_muted_channels: boolean;
248 id: string;
249 /** @nullable */
250 invited_by?: number | null;
251 join_method: OrgJoinMethod;
252 joined_at: string;
253 /** @nullable */
254 last_active_at?: string | null;
255 /** @nullable */
256 left_at?: string | null;
257 message_notifications: MessageNotifications;
258 mobile_push: boolean;
259 /** @nullable */
260 mute_until?: string | null;
261 muted: boolean;
262 organization_id: string;
263 permissions: string[];
264 role: OrgMemberRole;
265 role_keys: string[];
266 suppress_everyone: boolean;
267 suppress_roles: boolean;
268 user_id: number;
269 username: string;
270 version: number;
271};
272
273export interface EnvelopeMemberSummary {
274 code: number;
275 data?: EnvelopeMemberSummaryData;
276 /** @nullable */
277 error?: string | null;
278 /** @nullable */
279 error_code?: string | null;
280 message: string;
281 request_id?: string;
282}
283
28497export interface EnvelopeNoData {
28598 code: number;
28699 data?: unknown;
287 /** @nullable */
288 error?: string | null;
289 /** @nullable */
290 error_code?: string | null;
291 message: string;
292 request_id?: string;
293}
294
295export type OrganizationStatus = "active" | "suspended" | "archived";
296export type EnvelopeOrganizationSummaryData = {
297 allow_invite: boolean;
298 allow_join: boolean;
299 allow_join_level: number;
300 /** @nullable */
301 archived_at?: string | null;
302 /** @nullable */
303 avatar_url?: string | null;
304 created_at: string;
305 /** @nullable */
306 description?: string | null;
307 id: string;
308 join_policy: OrganizationJoinPolicy;
309 member_count: number;
310 member_must_enable_2fa: boolean;
311 my_role?: null | OrgMemberRole;
312 name: string;
313 owner_user_id: number;
314 require_email_verified: boolean;
315 require_mfa: boolean;
316 slug: string;
317 status: OrganizationStatus;
318 updated_at: string;
319 version: number;
320 /** @nullable */
321 welcome_message?: string | null;
322};
323
324export interface EnvelopeOrganizationSummary {
325 code: number;
326 data?: EnvelopeOrganizationSummaryData;
327100 /** @nullable */
328101 error?: string | null;
329102 /** @nullable */
@@ -363,16 +136,14 @@
363136 request_id?: string;
364137}
365138
366export type EnvelopePermissionCheckData = {
367 allowed: boolean;
368 matched_by: string;
369 organization_version: number;
370 reason: string;
139export type EnvelopeRegisterCodeResponseData = {
140 /** @minimum 0 */
141 expires_in_secs: number;
371142};
372143
373export interface EnvelopePermissionCheck {
144export interface EnvelopeRegisterCodeResponse {
374145 code: number;
375 data?: EnvelopePermissionCheckData;
146 data?: EnvelopeRegisterCodeResponseData;
376147 /** @nullable */
377148 error?: string | null;
378149 /** @nullable */
@@ -381,82 +152,13 @@
381152 request_id?: string;
382153}
383154
384export type EnvelopeRedeemInviteResponseData = {
385 joined: boolean;
386 member_id: string;
387 organization_id: string;
388};
389
390export interface EnvelopeRedeemInviteResponse {
391 code: number;
392 data?: EnvelopeRedeemInviteResponseData;
155export type SshKeyAlgorithm = "rsa" | "ed25519" | "ecdsa";
156export type EnvelopeSshKeySummaryData = {
157 algorithm: SshKeyAlgorithm;
158 created_at: string;
393159 /** @nullable */
394 error?: string | null;
160 description?: string | null;
395161 /** @nullable */
396 error_code?: string | null;
397 message: string;
398 request_id?: string;
399}
400
401export type EnvelopeRegisterCodeResponseData = {
402 /** @minimum 0 */
403 expires_in_secs: number;
404};
405
406export interface EnvelopeRegisterCodeResponse {
407 code: number;
408 data?: EnvelopeRegisterCodeResponseData;
409 /** @nullable */
410 error?: string | null;
411 /** @nullable */
412 error_code?: string | null;
413 message: string;
414 request_id?: string;
415}
416
417export type EnvelopeRoleSummaryData = {
418 /** @nullable */
419 color?: string | null;
420 created_at: string;
421 default_role: boolean;
422 /** @nullable */
423 description?: string | null;
424 /** @nullable */
425 emoji?: string | null;
426 /** @nullable */
427 icon_hash?: string | null;
428 id: string;
429 key: string;
430 member_count: number;
431 name: string;
432 organization_id: string;
433 permissions: PermissionGrant[];
434 position: number;
435 rank: number;
436 self_assignable: boolean;
437 system: boolean;
438 updated_at: string;
439 version: number;
440};
441
442export interface EnvelopeRoleSummary {
443 code: number;
444 data?: EnvelopeRoleSummaryData;
445 /** @nullable */
446 error?: string | null;
447 /** @nullable */
448 error_code?: string | null;
449 message: string;
450 request_id?: string;
451}
452
453export type SshKeyAlgorithm = "rsa" | "ed25519" | "ecdsa";
454export type EnvelopeSshKeySummaryData = {
455 algorithm: SshKeyAlgorithm;
456 created_at: string;
457 /** @nullable */
458 description?: string | null;
459 /** @nullable */
460162 expires_at?: string | null;
461163 fingerprint: string;
462164 id: string;
@@ -518,166 +220,6 @@
518220 request_id?: string;
519221}
520222
521export type EnvelopeVecInviteSummaryDataItem = {
522 created_at: string;
523 created_by: number;
524 /** @nullable */
525 email?: string | null;
526 /** @nullable */
527 expires_at?: string | null;
528 id: string;
529 max_uses: number;
530 method: OrgJoinMethod;
531 organization_id: string;
532 /** @nullable */
533 revoked_at?: string | null;
534 /** @nullable */
535 role_id?: string | null;
536 /** @nullable */
537 target_user_id?: number | null;
538 uses: number;
539};
540
541export interface EnvelopeVecInviteSummary {
542 code: number;
543 data?: EnvelopeVecInviteSummaryDataItem[];
544 /** @nullable */
545 error?: string | null;
546 /** @nullable */
547 error_code?: string | null;
548 message: string;
549 request_id?: string;
550}
551
552export type EnvelopeVecMemberSummaryDataItem = {
553 /** @nullable */
554 alias_avatar_url?: string | null;
555 /** @nullable */
556 alias_name?: string | null;
557 ban_allow_agent_join: boolean;
558 ban_allow_comment_any: boolean;
559 ban_allow_push_repo: boolean;
560 ban_allow_speak: boolean;
561 /** @nullable */
562 ban_expires_at?: string | null;
563 /** @nullable */
564 ban_reason?: string | null;
565 /** @nullable */
566 banned_at?: string | null;
567 /** @nullable */
568 displayname?: string | null;
569 hide_muted_channels: boolean;
570 id: string;
571 /** @nullable */
572 invited_by?: number | null;
573 join_method: OrgJoinMethod;
574 joined_at: string;
575 /** @nullable */
576 last_active_at?: string | null;
577 /** @nullable */
578 left_at?: string | null;
579 message_notifications: MessageNotifications;
580 mobile_push: boolean;
581 /** @nullable */
582 mute_until?: string | null;
583 muted: boolean;
584 organization_id: string;
585 permissions: string[];
586 role: OrgMemberRole;
587 role_keys: string[];
588 suppress_everyone: boolean;
589 suppress_roles: boolean;
590 user_id: number;
591 username: string;
592 version: number;
593};
594
595export interface EnvelopeVecMemberSummary {
596 code: number;
597 data?: EnvelopeVecMemberSummaryDataItem[];
598 /** @nullable */
599 error?: string | null;
600 /** @nullable */
601 error_code?: string | null;
602 message: string;
603 request_id?: string;
604}
605
606export type EnvelopeVecOrganizationSummaryDataItem = {
607 allow_invite: boolean;
608 allow_join: boolean;
609 allow_join_level: number;
610 /** @nullable */
611 archived_at?: string | null;
612 /** @nullable */
613 avatar_url?: string | null;
614 created_at: string;
615 /** @nullable */
616 description?: string | null;
617 id: string;
618 join_policy: OrganizationJoinPolicy;
619 member_count: number;
620 member_must_enable_2fa: boolean;
621 my_role?: null | OrgMemberRole;
622 name: string;
623 owner_user_id: number;
624 require_email_verified: boolean;
625 require_mfa: boolean;
626 slug: string;
627 status: OrganizationStatus;
628 updated_at: string;
629 version: number;
630 /** @nullable */
631 welcome_message?: string | null;
632};
633
634export interface EnvelopeVecOrganizationSummary {
635 code: number;
636 data?: EnvelopeVecOrganizationSummaryDataItem[];
637 /** @nullable */
638 error?: string | null;
639 /** @nullable */
640 error_code?: string | null;
641 message: string;
642 request_id?: string;
643}
644
645export type EnvelopeVecRoleSummaryDataItem = {
646 /** @nullable */
647 color?: string | null;
648 created_at: string;
649 default_role: boolean;
650 /** @nullable */
651 description?: string | null;
652 /** @nullable */
653 emoji?: string | null;
654 /** @nullable */
655 icon_hash?: string | null;
656 id: string;
657 key: string;
658 member_count: number;
659 name: string;
660 organization_id: string;
661 permissions: PermissionGrant[];
662 position: number;
663 rank: number;
664 self_assignable: boolean;
665 system: boolean;
666 updated_at: string;
667 version: number;
668};
669
670export interface EnvelopeVecRoleSummary {
671 code: number;
672 data?: EnvelopeVecRoleSummaryDataItem[];
673 /** @nullable */
674 error?: string | null;
675 /** @nullable */
676 error_code?: string | null;
677 message: string;
678 request_id?: string;
679}
680
681223export type EnvelopeVecSshKeySummaryDataItem = {
682224 algorithm: SshKeyAlgorithm;
683225 created_at: string;
@@ -706,28 +248,6 @@
706248 request_id?: string;
707249}
708250
709export interface EnvelopeVecString {
710 code: number;
711 data?: string[];
712 /** @nullable */
713 error?: string | null;
714 /** @nullable */
715 error_code?: string | null;
716 message: string;
717 request_id?: string;
718}
719
720export interface EnvelopeBool {
721 code: number;
722 data?: boolean;
723 /** @nullable */
724 error?: string | null;
725 /** @nullable */
726 error_code?: string | null;
727 message: string;
728 request_id?: string;
729}
730
731251export interface PowProofRequest {
732252 /** @minimum 0 */
733253 nonce: number;
@@ -776,130 +296,8 @@
776296 version: number;
777297}
778298
779export interface MemberListQuery {
780 /** @nullable */
781 include_banned?: boolean | null;
782 /** @nullable */
783 include_inactive?: boolean | null;
784 /**
785 * @minimum 0
786 * @nullable
787 */
788 page?: number | null;
789 /**
790 * @minimum 0
791 * @nullable
792 */
793 page_size?: number | null;
794}
795
796export interface MemberSummary {
797 /** @nullable */
798 alias_avatar_url?: string | null;
799 /** @nullable */
800 alias_name?: string | null;
801 ban_allow_agent_join: boolean;
802 ban_allow_comment_any: boolean;
803 ban_allow_push_repo: boolean;
804 ban_allow_speak: boolean;
805 /** @nullable */
806 ban_expires_at?: string | null;
807 /** @nullable */
808 ban_reason?: string | null;
809 /** @nullable */
810 banned_at?: string | null;
811 /** @nullable */
812 displayname?: string | null;
813 hide_muted_channels: boolean;
814 id: string;
815 /** @nullable */
816 invited_by?: number | null;
817 join_method: OrgJoinMethod;
818 joined_at: string;
819 /** @nullable */
820 last_active_at?: string | null;
821 /** @nullable */
822 left_at?: string | null;
823 message_notifications: MessageNotifications;
824 mobile_push: boolean;
825 /** @nullable */
826 mute_until?: string | null;
827 muted: boolean;
828 organization_id: string;
829 permissions: string[];
830 role: OrgMemberRole;
831 role_keys: string[];
832 suppress_everyone: boolean;
833 suppress_roles: boolean;
834 user_id: number;
835 username: string;
836 version: number;
837}
838
839299export interface NoData {}
840300
841export interface OrgInvitePath {
842 invite_id: string;
843 organization_id: string;
844}
845
846export interface OrgListQuery {
847 /** @nullable */
848 include_archived?: boolean | null;
849 /**
850 * @minimum 0
851 * @nullable
852 */
853 page?: number | null;
854 /**
855 * @minimum 0
856 * @nullable
857 */
858 page_size?: number | null;
859}
860
861export interface OrgMemberPath {
862 member_id: string;
863 organization_id: string;
864}
865
866export interface OrgPath {
867 organization_id: string;
868}
869
870export interface OrgRolePath {
871 organization_id: string;
872 role_id: string;
873}
874
875export interface OrganizationSummary {
876 allow_invite: boolean;
877 allow_join: boolean;
878 allow_join_level: number;
879 /** @nullable */
880 archived_at?: string | null;
881 /** @nullable */
882 avatar_url?: string | null;
883 created_at: string;
884 /** @nullable */
885 description?: string | null;
886 id: string;
887 join_policy: OrganizationJoinPolicy;
888 member_count: number;
889 member_must_enable_2fa: boolean;
890 my_role?: null | OrgMemberRole;
891 name: string;
892 owner_user_id: number;
893 require_email_verified: boolean;
894 require_mfa: boolean;
895 slug: string;
896 status: OrganizationStatus;
897 updated_at: string;
898 version: number;
899 /** @nullable */
900 welcome_message?: string | null;
901}
902
903301export interface PasswordResetConfirmRequest {
904302 password_hash: string;
905303 pow_payload: PowProofRequest;
@@ -920,28 +318,6 @@
920318 expires_in_secs: number;
921319}
922320
923export interface PermissionCheck {
924 allowed: boolean;
925 matched_by: string;
926 organization_version: number;
927 reason: string;
928}
929
930export interface ReasonBody {
931 /** @nullable */
932 reason?: string | null;
933}
934
935export interface RedeemInviteBody {
936 token: string;
937}
938
939export interface RedeemInviteResponse {
940 joined: boolean;
941 member_id: string;
942 organization_id: string;
943}
944
945321export interface RegisterCodeRequest {
946322 email: string;
947323 pow_payload: PowProofRequest;
@@ -952,40 +328,6 @@
952328 expires_in_secs: number;
953329}
954330
955export interface RoleSummary {
956 /** @nullable */
957 color?: string | null;
958 created_at: string;
959 default_role: boolean;
960 /** @nullable */
961 description?: string | null;
962 /** @nullable */
963 emoji?: string | null;
964 /** @nullable */
965 icon_hash?: string | null;
966 id: string;
967 key: string;
968 member_count: number;
969 name: string;
970 organization_id: string;
971 permissions: PermissionGrant[];
972 position: number;
973 rank: number;
974 self_assignable: boolean;
975 system: boolean;
976 updated_at: string;
977 version: number;
978}
979
980export interface SetMemberRoleBody {
981 expected_version?: number;
982 role: OrgMemberRole;
983}
984
985export interface SetRolePermissionsBody {
986 permissions?: PermissionGrant[];
987}
988
989331export interface SshKeyAddRequest {
990332 /** @nullable */
991333 description?: string | null;
@@ -1012,92 +354,6 @@
1012354 revoked_at?: string | null;
1013355}
1014356
1015export interface TransferBody {
1016 target_user_id: number;
1017}
1018
1019/**
1020 * 封禁更新:原因、到期时间与四个放行位。缺失表示不变,null 表示清空。
1021 */
1022export interface UpdateBanBody {
1023 /** @nullable */
1024 allow_agent_join?: boolean | null;
1025 /** @nullable */
1026 allow_comment_any?: boolean | null;
1027 /** @nullable */
1028 allow_push_repo?: boolean | null;
1029 /** @nullable */
1030 allow_speak?: boolean | null;
1031 /** @nullable */
1032 expires_at?: string | null;
1033 /** @nullable */
1034 reason?: string | null;
1035}
1036
1037/**
1038 * 会籍更新:管理端可改别名、通知偏好、免打扰与封禁附加信息。
1039 * 所有字段缺失表示不变,显式 null 表示清空(仅限 Option 字段)。
1040 */
1041export interface UpdateMemberBody {
1042 /** @nullable */
1043 alias_avatar_url?: string | null;
1044 /** @nullable */
1045 alias_name?: string | null;
1046 expected_version?: number;
1047 /** @nullable */
1048 hide_muted_channels?: boolean | null;
1049 message_notifications?: null | MessageNotifications;
1050 /** @nullable */
1051 mobile_push?: boolean | null;
1052 /** @nullable */
1053 mute_until?: string | null;
1054 /** @nullable */
1055 muted?: boolean | null;
1056 /** @nullable */
1057 suppress_everyone?: boolean | null;
1058 /** @nullable */
1059 suppress_roles?: boolean | null;
1060}
1061
1062export interface UpdateOrganizationBody {
1063 /** @nullable */
1064 allow_invite?: boolean | null;
1065 /** @nullable */
1066 allow_join?: boolean | null;
1067 /** @nullable */
1068 allow_join_level?: number | null;
1069 /** @nullable */
1070 avatar_url?: string | null;
1071 /** @nullable */
1072 description?: string | null;
1073 expected_version?: number;
1074 join_policy?: null | OrganizationJoinPolicy;
1075 /** @nullable */
1076 member_must_enable_2fa?: boolean | null;
1077 /** @nullable */
1078 name?: string | null;
1079 /** @nullable */
1080 require_email_verified?: boolean | null;
1081 /** @nullable */
1082 require_mfa?: boolean | null;
1083 /** @nullable */
1084 welcome_message?: string | null;
1085}
1086
1087export interface UpdateRoleBody {
1088 /** @nullable */
1089 default_role?: boolean | null;
1090 /** @nullable */
1091 description?: string | null;
1092 expected_version?: number;
1093 /** @nullable */
1094 name?: string | null;
1095 /** @nullable */
1096 rank?: number | null;
1097 /** @nullable */
1098 self_assignable?: boolean | null;
1099}
1100
1101357export interface UserLoginRequest {
1102358 identity: string;
1103359 password_hash: string;
@@ -1143,53 +399,4 @@
1143399 * @minimum 0
1144400 */
1145401 page_size?: number;
1146};
1147
1148export type AuthOrganizationDeleteParams = {
1149 /**
1150 * 是否连成员一并清理
1151 */
1152 force?: boolean;
1153 /**
1154 * 删除原因,最长 512 字符
1155 */
1156 reason?: string;
1157};
1158
1159export type AuthOrganizationBanListParams = {
1160 /**
1161 * 页码,从 1 开始
1162 * @minimum 0
1163 */
1164 page?: number;
1165 /**
1166 * 每页条数
1167 * @minimum 0
1168 */
1169 page_size?: number;
1170};
1171
1172export type AuthOrganizationInviteListParams = {
1173 /**
1174 * 包含已撤销邀请
1175 */
1176 include_revoked?: boolean;
1177};
1178
1179export type AuthOrganizationMemberListParams = {
1180 /**
1181 * 包含已退出成员
1182 */
1183 include_inactive?: boolean;
1184 /**
1185 * 包含已封禁成员
1186 */
1187 include_banned?: boolean;
1188};
1189
1190export type AuthOrganizationMemberRemoveParams = {
1191 /**
1192 * 移除原因
1193 */
1194 reason?: string;
1195402};
Modified view/auth/src/shared/api/auth/auth.ts View view/auth/src/shared/api/auth/auth.ts
@@ -6,59 +6,26 @@
66 * OpenAPI spec version: 1.0.0
77 */
88import type {
9 AddMemberBody,
10 AssignRoleBody,
11 AuthOrganizationBanListParams,
12 AuthOrganizationDeleteParams,
13 AuthOrganizationInviteListParams,
14 AuthOrganizationMemberListParams,
15 AuthOrganizationMemberRemoveParams,
169 AuthSshKeyListParams,
17 BanMemberBody,
18 CheckPermissionBody,
19 CreateInviteBody,
20 CreateOrganizationBody,
21 CreateRoleBody,
22 EnvelopeBool,
2310 EnvelopeChallengeResponse,
24 EnvelopeCreateInviteResponse,
2511 EnvelopeLoginCodeResponse,
2612 EnvelopeLogoutResponse,
2713 EnvelopeMeResponse,
28 EnvelopeMemberSummary,
2914 EnvelopeNoData,
30 EnvelopeOrganizationSummary,
3115 EnvelopePasswordResetConfirmResponse,
3216 EnvelopePasswordResetResponse,
33 EnvelopePermissionCheck,
34 EnvelopeRedeemInviteResponse,
3517 EnvelopeRegisterCodeResponse,
36 EnvelopeRoleSummary,
3718 EnvelopeSshKeySummary,
3819 EnvelopeUserLoginResponse,
3920 EnvelopeUserRegisterResponse,
40 EnvelopeVecInviteSummary,
41 EnvelopeVecMemberSummary,
42 EnvelopeVecOrganizationSummary,
43 EnvelopeVecRoleSummary,
4421 EnvelopeVecSshKeySummary,
45 EnvelopeVecString,
4622 LoginCodeRequest,
4723 LoginCodeVerifyRequest,
4824 LoginVerifyRequest,
4925 PasswordResetConfirmRequest,
5026 PasswordResetRequest,
51 ReasonBody,
52 RedeemInviteBody,
5327 RegisterCodeRequest,
54 SetMemberRoleBody,
55 SetRolePermissionsBody,
5628 SshKeyAddRequest,
57 TransferBody,
58 UpdateBanBody,
59 UpdateMemberBody,
60 UpdateOrganizationBody,
61 UpdateRoleBody,
6229 UserLoginRequest,
6330 UserRegisterRequest,
6431} from "./auth.schemas";
@@ -1067,2374 +1034,4 @@
10671034 ...options,
10681035 method: "DELETE",
10691036 });
1070};
1071
1072export type authOrganizationListResponse200 = {
1073 data: EnvelopeVecOrganizationSummary;
1074 status: 200;
1075};
1076
1077export type authOrganizationListResponse401 = {
1078 data: EnvelopeNoData;
1079 status: 401;
1080};
1081
1082export type authOrganizationListResponseSuccess =
1083 authOrganizationListResponse200 & {
1084 headers: Headers;
1085 };
1086export type authOrganizationListResponseError =
1087 authOrganizationListResponse401 & {
1088 headers: Headers;
1089 };
1090
1091export type authOrganizationListResponse =
1092 authOrganizationListResponseSuccess | authOrganizationListResponseError;
1093
1094export const getAuthOrganizationListUrl = () => {
1095 return `/api/v1/organizations`;
1096};
1097
1098/**
1099 * 默认只返回 ACTIVE 组织;`include_archived=true` 时一并返回已归档组织。
1100 * @summary 列示我加入的组织
1101 */
1102export const authOrganizationList = async (
1103 options?: Parameters<typeof authClient>[1],
1104): Promise<authOrganizationListResponse> => {
1105 return authClient<authOrganizationListResponse>(
1106 getAuthOrganizationListUrl(),
1107 {
1108 ...options,
1109 method: "GET",
1110 },
1111 );
1112};
1113
1114export type authOrganizationCreateResponse200 = {
1115 data: EnvelopeOrganizationSummary;
1116 status: 200;
1117};
1118
1119export type authOrganizationCreateResponse401 = {
1120 data: EnvelopeNoData;
1121 status: 401;
1122};
1123
1124export type authOrganizationCreateResponse409 = {
1125 data: EnvelopeNoData;
1126 status: 409;
1127};
1128
1129export type authOrganizationCreateResponseSuccess =
1130 authOrganizationCreateResponse200 & {
1131 headers: Headers;
1132 };
1133export type authOrganizationCreateResponseError = (
1134 authOrganizationCreateResponse401 | authOrganizationCreateResponse409
1135) & {
1136 headers: Headers;
1137};
1138
1139export type authOrganizationCreateResponse =
1140 authOrganizationCreateResponseSuccess | authOrganizationCreateResponseError;
1141
1142export const getAuthOrganizationCreateUrl = () => {
1143 return `/api/v1/organizations`;
1144};
1145
1146/**
1147 * 创建者自动成为 OWNER 并写入 owner/admin/member 三个内置角色。slug 全局唯一且只允许小写字母、数字与连字符。
1148 * @summary 新建组织
1149 */
1150export const authOrganizationCreate = async (
1151 createOrganizationBody: CreateOrganizationBody,
1152 options?: Parameters<typeof authClient>[1],
1153): Promise<authOrganizationCreateResponse> => {
1154 const getHeaders = (
1155 h?: NonNullable<RequestInit["headers"]>,
1156 ): Record<string, string | readonly string[]> => {
1157 if (!h) return {};
1158 if (h instanceof Headers) return Object.fromEntries(h.entries());
1159 if (Symbol.iterator in h) {
1160 return Object.fromEntries(
1161 Array.from(
1162 h as Iterable<Iterable<string>>,
1163 (entry) => Array.from(entry) as [string, string],
1164 ),
1165 );
1166 }
1167 const headers: Record<string, string | readonly string[]> = {};
1168 for (const [name, value] of Object.entries<
1169 string | readonly string[] | undefined
1170 >(h)) {
1171 if (value !== undefined) headers[name] = value;
1172 }
1173 return headers;
1174 };
1175 return authClient<authOrganizationCreateResponse>(
1176 getAuthOrganizationCreateUrl(),
1177 {
1178 ...options,
1179 method: "POST",
1180 headers: {
1181 "Content-Type": "application/json",
1182 ...getHeaders(options?.headers),
1183 },
1184 body: JSON.stringify(createOrganizationBody),
1185 },
1186 );
1187};
1188
1189export type authOrganizationInviteRedeemResponse200 = {
1190 data: EnvelopeRedeemInviteResponse;
1191 status: 200;
1192};
1193
1194export type authOrganizationInviteRedeemResponse403 = {
1195 data: EnvelopeNoData;
1196 status: 403;
1197};
1198
1199export type authOrganizationInviteRedeemResponse409 = {
1200 data: EnvelopeNoData;
1201 status: 409;
1202};
1203
1204export type authOrganizationInviteRedeemResponseSuccess =
1205 authOrganizationInviteRedeemResponse200 & {
1206 headers: Headers;
1207 };
1208export type authOrganizationInviteRedeemResponseError = (
1209 | authOrganizationInviteRedeemResponse403
1210 | authOrganizationInviteRedeemResponse409
1211) & {
1212 headers: Headers;
1213};
1214
1215export type authOrganizationInviteRedeemResponse =
1216 | authOrganizationInviteRedeemResponseSuccess
1217 | authOrganizationInviteRedeemResponseError;
1218
1219export const getAuthOrganizationInviteRedeemUrl = () => {
1220 return `/api/v1/organizations/invites/redeem`;
1221};
1222
1223/**
1224 * uses 递增与兑换记录写入同事务。定向邀请只能由目标用户兑换。
1225 * @summary 兑换邀请
1226 */
1227export const authOrganizationInviteRedeem = async (
1228 redeemInviteBody: RedeemInviteBody,
1229 options?: Parameters<typeof authClient>[1],
1230): Promise<authOrganizationInviteRedeemResponse> => {
1231 const getHeaders = (
1232 h?: NonNullable<RequestInit["headers"]>,
1233 ): Record<string, string | readonly string[]> => {
1234 if (!h) return {};
1235 if (h instanceof Headers) return Object.fromEntries(h.entries());
1236 if (Symbol.iterator in h) {
1237 return Object.fromEntries(
1238 Array.from(
1239 h as Iterable<Iterable<string>>,
1240 (entry) => Array.from(entry) as [string, string],
1241 ),
1242 );
1243 }
1244 const headers: Record<string, string | readonly string[]> = {};
1245 for (const [name, value] of Object.entries<
1246 string | readonly string[] | undefined
1247 >(h)) {
1248 if (value !== undefined) headers[name] = value;
1249 }
1250 return headers;
1251 };
1252 return authClient<authOrganizationInviteRedeemResponse>(
1253 getAuthOrganizationInviteRedeemUrl(),
1254 {
1255 ...options,
1256 method: "POST",
1257 headers: {
1258 "Content-Type": "application/json",
1259 ...getHeaders(options?.headers),
1260 },
1261 body: JSON.stringify(redeemInviteBody),
1262 },
1263 );
1264};
1265
1266export type authOrganizationGetResponse200 = {
1267 data: EnvelopeOrganizationSummary;
1268 status: 200;
1269};
1270
1271export type authOrganizationGetResponse404 = {
1272 data: EnvelopeNoData;
1273 status: 404;
1274};
1275
1276export type authOrganizationGetResponseSuccess =
1277 authOrganizationGetResponse200 & {
1278 headers: Headers;
1279 };
1280export type authOrganizationGetResponseError =
1281 authOrganizationGetResponse404 & {
1282 headers: Headers;
1283 };
1284
1285export type authOrganizationGetResponse =
1286 authOrganizationGetResponseSuccess | authOrganizationGetResponseError;
1287
1288export const getAuthOrganizationGetUrl = (organizationId: string) => {
1289 return `/api/v1/organizations/${organizationId}`;
1290};
1291
1292/**
1293 * @summary 组织详情
1294 */
1295export const authOrganizationGet = async (
1296 organizationId: string,
1297 options?: Parameters<typeof authClient>[1],
1298): Promise<authOrganizationGetResponse> => {
1299 return authClient<authOrganizationGetResponse>(
1300 getAuthOrganizationGetUrl(organizationId),
1301 {
1302 ...options,
1303 method: "GET",
1304 },
1305 );
1306};
1307
1308export type authOrganizationDeleteResponse200 = {
1309 data: EnvelopeBool;
1310 status: 200;
1311};
1312
1313export type authOrganizationDeleteResponse403 = {
1314 data: EnvelopeNoData;
1315 status: 403;
1316};
1317
1318export type authOrganizationDeleteResponse409 = {
1319 data: EnvelopeNoData;
1320 status: 409;
1321};
1322
1323export type authOrganizationDeleteResponseSuccess =
1324 authOrganizationDeleteResponse200 & {
1325 headers: Headers;
1326 };
1327export type authOrganizationDeleteResponseError = (
1328 authOrganizationDeleteResponse403 | authOrganizationDeleteResponse409
1329) & {
1330 headers: Headers;
1331};
1332
1333export type authOrganizationDeleteResponse =
1334 authOrganizationDeleteResponseSuccess | authOrganizationDeleteResponseError;
1335
1336export const getAuthOrganizationDeleteUrl = (
1337 organizationId: string,
1338 params?: AuthOrganizationDeleteParams,
1339) => {
1340 const normalizedParams = new URLSearchParams();
1341
1342 Object.entries(params || {}).forEach(([key, value]) => {
1343 if (value !== undefined) {
1344 normalizedParams.append(key, value === null ? "null" : String(value));
1345 }
1346 });
1347
1348 const stringifiedParams = normalizedParams.toString();
1349
1350 return stringifiedParams.length > 0
1351 ? `/api/v1/organizations/${organizationId}?${stringifiedParams}`
1352 : `/api/v1/organizations/${organizationId}`;
1353};
1354
1355/**
1356 * 仍存在活跃成员且未指定 `force=true` 时返回 409。删除会清理成员、角色与邀请。
1357 * @summary 删除组织
1358 */
1359export const authOrganizationDelete = async (
1360 organizationId: string,
1361 params?: AuthOrganizationDeleteParams,
1362 options?: Parameters<typeof authClient>[1],
1363): Promise<authOrganizationDeleteResponse> => {
1364 return authClient<authOrganizationDeleteResponse>(
1365 getAuthOrganizationDeleteUrl(organizationId, params),
1366 {
1367 ...options,
1368 method: "DELETE",
1369 },
1370 );
1371};
1372
1373export type authOrganizationUpdateResponse200 = {
1374 data: EnvelopeOrganizationSummary;
1375 status: 200;
1376};
1377
1378export type authOrganizationUpdateResponse403 = {
1379 data: EnvelopeNoData;
1380 status: 403;
1381};
1382
1383export type authOrganizationUpdateResponse409 = {
1384 data: EnvelopeNoData;
1385 status: 409;
1386};
1387
1388export type authOrganizationUpdateResponseSuccess =
1389 authOrganizationUpdateResponse200 & {
1390 headers: Headers;
1391 };
1392export type authOrganizationUpdateResponseError = (
1393 authOrganizationUpdateResponse403 | authOrganizationUpdateResponse409
1394) & {
1395 headers: Headers;
1396};
1397
1398export type authOrganizationUpdateResponse =
1399 authOrganizationUpdateResponseSuccess | authOrganizationUpdateResponseError;
1400
1401export const getAuthOrganizationUpdateUrl = (organizationId: string) => {
1402 return `/api/v1/organizations/${organizationId}`;
1403};
1404
1405/**
1406 * 需要 `org.update`。`expected_version` 与当前 version 不一致时返回 409。
1407 * @summary 修改组织
1408 */
1409export const authOrganizationUpdate = async (
1410 organizationId: string,
1411 updateOrganizationBody: UpdateOrganizationBody,
1412 options?: Parameters<typeof authClient>[1],
1413): Promise<authOrganizationUpdateResponse> => {
1414 const getHeaders = (
1415 h?: NonNullable<RequestInit["headers"]>,
1416 ): Record<string, string | readonly string[]> => {
1417 if (!h) return {};
1418 if (h instanceof Headers) return Object.fromEntries(h.entries());
1419 if (Symbol.iterator in h) {
1420 return Object.fromEntries(
1421 Array.from(
1422 h as Iterable<Iterable<string>>,
1423 (entry) => Array.from(entry) as [string, string],
1424 ),
1425 );
1426 }
1427 const headers: Record<string, string | readonly string[]> = {};
1428 for (const [name, value] of Object.entries<
1429 string | readonly string[] | undefined
1430 >(h)) {
1431 if (value !== undefined) headers[name] = value;
1432 }
1433 return headers;
1434 };
1435 return authClient<authOrganizationUpdateResponse>(
1436 getAuthOrganizationUpdateUrl(organizationId),
1437 {
1438 ...options,
1439 method: "PATCH",
1440 headers: {
1441 "Content-Type": "application/json",
1442 ...getHeaders(options?.headers),
1443 },
1444 body: JSON.stringify(updateOrganizationBody),
1445 },
1446 );
1447};
1448
1449export type authOrganizationArchiveResponse200 = {
1450 data: EnvelopeOrganizationSummary;
1451 status: 200;
1452};
1453
1454export type authOrganizationArchiveResponse403 = {
1455 data: EnvelopeNoData;
1456 status: 403;
1457};
1458
1459export type authOrganizationArchiveResponseSuccess =
1460 authOrganizationArchiveResponse200 & {
1461 headers: Headers;
1462 };
1463export type authOrganizationArchiveResponseError =
1464 authOrganizationArchiveResponse403 & {
1465 headers: Headers;
1466 };
1467
1468export type authOrganizationArchiveResponse =
1469 authOrganizationArchiveResponseSuccess | authOrganizationArchiveResponseError;
1470
1471export const getAuthOrganizationArchiveUrl = (organizationId: string) => {
1472 return `/api/v1/organizations/${organizationId}/archive`;
1473};
1474
1475/**
1476 * 需要 `org.archive`。归档后全部权限判定返回 false,可恢复。
1477 * @summary 归档组织
1478 */
1479export const authOrganizationArchive = async (
1480 organizationId: string,
1481 reasonBody: ReasonBody,
1482 options?: Parameters<typeof authClient>[1],
1483): Promise<authOrganizationArchiveResponse> => {
1484 const getHeaders = (
1485 h?: NonNullable<RequestInit["headers"]>,
1486 ): Record<string, string | readonly string[]> => {
1487 if (!h) return {};
1488 if (h instanceof Headers) return Object.fromEntries(h.entries());
1489 if (Symbol.iterator in h) {
1490 return Object.fromEntries(
1491 Array.from(
1492 h as Iterable<Iterable<string>>,
1493 (entry) => Array.from(entry) as [string, string],
1494 ),
1495 );
1496 }
1497 const headers: Record<string, string | readonly string[]> = {};
1498 for (const [name, value] of Object.entries<
1499 string | readonly string[] | undefined
1500 >(h)) {
1501 if (value !== undefined) headers[name] = value;
1502 }
1503 return headers;
1504 };
1505 return authClient<authOrganizationArchiveResponse>(
1506 getAuthOrganizationArchiveUrl(organizationId),
1507 {
1508 ...options,
1509 method: "POST",
1510 headers: {
1511 "Content-Type": "application/json",
1512 ...getHeaders(options?.headers),
1513 },
1514 body: JSON.stringify(reasonBody),
1515 },
1516 );
1517};
1518
1519export type authOrganizationBanListResponse200 = {
1520 data: EnvelopeVecMemberSummary;
1521 status: 200;
1522};
1523
1524export type authOrganizationBanListResponse403 = {
1525 data: EnvelopeNoData;
1526 status: 403;
1527};
1528
1529export type authOrganizationBanListResponseSuccess =
1530 authOrganizationBanListResponse200 & {
1531 headers: Headers;
1532 };
1533export type authOrganizationBanListResponseError =
1534 authOrganizationBanListResponse403 & {
1535 headers: Headers;
1536 };
1537
1538export type authOrganizationBanListResponse =
1539 authOrganizationBanListResponseSuccess | authOrganizationBanListResponseError;
1540
1541export const getAuthOrganizationBanListUrl = (
1542 organizationId: string,
1543 params?: AuthOrganizationBanListParams,
1544) => {
1545 const normalizedParams = new URLSearchParams();
1546
1547 Object.entries(params || {}).forEach(([key, value]) => {
1548 if (value !== undefined) {
1549 normalizedParams.append(key, value === null ? "null" : String(value));
1550 }
1551 });
1552
1553 const stringifiedParams = normalizedParams.toString();
1554
1555 return stringifiedParams.length > 0
1556 ? `/api/v1/organizations/${organizationId}/bans?${stringifiedParams}`
1557 : `/api/v1/organizations/${organizationId}/bans`;
1558};
1559
1560/**
1561 * 需要 `member.ban`。按封禁时间倒序。
1562 * @summary 封禁列表
1563 */
1564export const authOrganizationBanList = async (
1565 organizationId: string,
1566 params?: AuthOrganizationBanListParams,
1567 options?: Parameters<typeof authClient>[1],
1568): Promise<authOrganizationBanListResponse> => {
1569 return authClient<authOrganizationBanListResponse>(
1570 getAuthOrganizationBanListUrl(organizationId, params),
1571 {
1572 ...options,
1573 method: "GET",
1574 },
1575 );
1576};
1577
1578export type authOrganizationBanGetResponse200 = {
1579 data: EnvelopeMemberSummary;
1580 status: 200;
1581};
1582
1583export type authOrganizationBanGetResponse404 = {
1584 data: EnvelopeNoData;
1585 status: 404;
1586};
1587
1588export type authOrganizationBanGetResponseSuccess =
1589 authOrganizationBanGetResponse200 & {
1590 headers: Headers;
1591 };
1592export type authOrganizationBanGetResponseError =
1593 authOrganizationBanGetResponse404 & {
1594 headers: Headers;
1595 };
1596
1597export type authOrganizationBanGetResponse =
1598 authOrganizationBanGetResponseSuccess | authOrganizationBanGetResponseError;
1599
1600export const getAuthOrganizationBanGetUrl = (
1601 organizationId: string,
1602 userId: number,
1603) => {
1604 return `/api/v1/organizations/${organizationId}/bans/${userId}`;
1605};
1606
1607/**
1608 * 需要 `member.ban`。未封禁返回 404。
1609 * @summary 封禁详情
1610 */
1611export const authOrganizationBanGet = async (
1612 organizationId: string,
1613 userId: number,
1614 options?: Parameters<typeof authClient>[1],
1615): Promise<authOrganizationBanGetResponse> => {
1616 return authClient<authOrganizationBanGetResponse>(
1617 getAuthOrganizationBanGetUrl(organizationId, userId),
1618 {
1619 ...options,
1620 method: "GET",
1621 },
1622 );
1623};
1624
1625export type authOrganizationBanDeleteResponse200 = {
1626 data: EnvelopeMemberSummary;
1627 status: 200;
1628};
1629
1630export type authOrganizationBanDeleteResponse404 = {
1631 data: EnvelopeNoData;
1632 status: 404;
1633};
1634
1635export type authOrganizationBanDeleteResponseSuccess =
1636 authOrganizationBanDeleteResponse200 & {
1637 headers: Headers;
1638 };
1639export type authOrganizationBanDeleteResponseError =
1640 authOrganizationBanDeleteResponse404 & {
1641 headers: Headers;
1642 };
1643
1644export type authOrganizationBanDeleteResponse =
1645 | authOrganizationBanDeleteResponseSuccess
1646 | authOrganizationBanDeleteResponseError;
1647
1648export const getAuthOrganizationBanDeleteUrl = (
1649 organizationId: string,
1650 userId: number,
1651) => {
1652 return `/api/v1/organizations/${organizationId}/bans/${userId}`;
1653};
1654
1655/**
1656 * 需要 `member.ban`。等价于按会籍 id 解封,便于按用户维度操作。
1657 * @summary 按用户解封
1658 */
1659export const authOrganizationBanDelete = async (
1660 organizationId: string,
1661 userId: number,
1662 options?: Parameters<typeof authClient>[1],
1663): Promise<authOrganizationBanDeleteResponse> => {
1664 return authClient<authOrganizationBanDeleteResponse>(
1665 getAuthOrganizationBanDeleteUrl(organizationId, userId),
1666 {
1667 ...options,
1668 method: "DELETE",
1669 },
1670 );
1671};
1672
1673export type authOrganizationBanUpdateResponse200 = {
1674 data: EnvelopeMemberSummary;
1675 status: 200;
1676};
1677
1678export type authOrganizationBanUpdateResponse404 = {
1679 data: EnvelopeNoData;
1680 status: 404;
1681};
1682
1683export type authOrganizationBanUpdateResponseSuccess =
1684 authOrganizationBanUpdateResponse200 & {
1685 headers: Headers;
1686 };
1687export type authOrganizationBanUpdateResponseError =
1688 authOrganizationBanUpdateResponse404 & {
1689 headers: Headers;
1690 };
1691
1692export type authOrganizationBanUpdateResponse =
1693 | authOrganizationBanUpdateResponseSuccess
1694 | authOrganizationBanUpdateResponseError;
1695
1696export const getAuthOrganizationBanUpdateUrl = (
1697 organizationId: string,
1698 userId: number,
1699) => {
1700 return `/api/v1/organizations/${organizationId}/bans/${userId}`;
1701};
1702
1703/**
1704 * 需要 `member.ban`。改原因、到期时间与四个放行位;未封禁返回 404。
1705 * @summary 更新封禁
1706 */
1707export const authOrganizationBanUpdate = async (
1708 organizationId: string,
1709 userId: number,
1710 updateBanBody: UpdateBanBody,
1711 options?: Parameters<typeof authClient>[1],
1712): Promise<authOrganizationBanUpdateResponse> => {
1713 const getHeaders = (
1714 h?: NonNullable<RequestInit["headers"]>,
1715 ): Record<string, string | readonly string[]> => {
1716 if (!h) return {};
1717 if (h instanceof Headers) return Object.fromEntries(h.entries());
1718 if (Symbol.iterator in h) {
1719 return Object.fromEntries(
1720 Array.from(
1721 h as Iterable<Iterable<string>>,
1722 (entry) => Array.from(entry) as [string, string],
1723 ),
1724 );
1725 }
1726 const headers: Record<string, string | readonly string[]> = {};
1727 for (const [name, value] of Object.entries<
1728 string | readonly string[] | undefined
1729 >(h)) {
1730 if (value !== undefined) headers[name] = value;
1731 }
1732 return headers;
1733 };
1734 return authClient<authOrganizationBanUpdateResponse>(
1735 getAuthOrganizationBanUpdateUrl(organizationId, userId),
1736 {
1737 ...options,
1738 method: "PATCH",
1739 headers: {
1740 "Content-Type": "application/json",
1741 ...getHeaders(options?.headers),
1742 },
1743 body: JSON.stringify(updateBanBody),
1744 },
1745 );
1746};
1747
1748export type authOrganizationInviteListResponse200 = {
1749 data: EnvelopeVecInviteSummary;
1750 status: 200;
1751};
1752
1753export type authOrganizationInviteListResponse403 = {
1754 data: EnvelopeNoData;
1755 status: 403;
1756};
1757
1758export type authOrganizationInviteListResponseSuccess =
1759 authOrganizationInviteListResponse200 & {
1760 headers: Headers;
1761 };
1762export type authOrganizationInviteListResponseError =
1763 authOrganizationInviteListResponse403 & {
1764 headers: Headers;
1765 };
1766
1767export type authOrganizationInviteListResponse =
1768 | authOrganizationInviteListResponseSuccess
1769 | authOrganizationInviteListResponseError;
1770
1771export const getAuthOrganizationInviteListUrl = (
1772 organizationId: string,
1773 params?: AuthOrganizationInviteListParams,
1774) => {
1775 const normalizedParams = new URLSearchParams();
1776
1777 Object.entries(params || {}).forEach(([key, value]) => {
1778 if (value !== undefined) {
1779 normalizedParams.append(key, value === null ? "null" : String(value));
1780 }
1781 });
1782
1783 const stringifiedParams = normalizedParams.toString();
1784
1785 return stringifiedParams.length > 0
1786 ? `/api/v1/organizations/${organizationId}/invites?${stringifiedParams}`
1787 : `/api/v1/organizations/${organizationId}/invites`;
1788};
1789
1790/**
1791 * 需要 `invite.view`。不含 token 明文。
1792 * @summary 列示邀请
1793 */
1794export const authOrganizationInviteList = async (
1795 organizationId: string,
1796 params?: AuthOrganizationInviteListParams,
1797 options?: Parameters<typeof authClient>[1],
1798): Promise<authOrganizationInviteListResponse> => {
1799 return authClient<authOrganizationInviteListResponse>(
1800 getAuthOrganizationInviteListUrl(organizationId, params),
1801 {
1802 ...options,
1803 method: "GET",
1804 },
1805 );
1806};
1807
1808export type authOrganizationInviteCreateResponse200 = {
1809 data: EnvelopeCreateInviteResponse;
1810 status: 200;
1811};
1812
1813export type authOrganizationInviteCreateResponse403 = {
1814 data: EnvelopeNoData;
1815 status: 403;
1816};
1817
1818export type authOrganizationInviteCreateResponseSuccess =
1819 authOrganizationInviteCreateResponse200 & {
1820 headers: Headers;
1821 };
1822export type authOrganizationInviteCreateResponseError =
1823 authOrganizationInviteCreateResponse403 & {
1824 headers: Headers;
1825 };
1826
1827export type authOrganizationInviteCreateResponse =
1828 | authOrganizationInviteCreateResponseSuccess
1829 | authOrganizationInviteCreateResponseError;
1830
1831export const getAuthOrganizationInviteCreateUrl = (organizationId: string) => {
1832 return `/api/v1/organizations/${organizationId}/invites`;
1833};
1834
1835/**
1836 * 需要 `invite.create`。明文 token 只在创建响应里返回一次,之后只存 sha256。
1837 * @summary 创建邀请
1838 */
1839export const authOrganizationInviteCreate = async (
1840 organizationId: string,
1841 createInviteBody: CreateInviteBody,
1842 options?: Parameters<typeof authClient>[1],
1843): Promise<authOrganizationInviteCreateResponse> => {
1844 const getHeaders = (
1845 h?: NonNullable<RequestInit["headers"]>,
1846 ): Record<string, string | readonly string[]> => {
1847 if (!h) return {};
1848 if (h instanceof Headers) return Object.fromEntries(h.entries());
1849 if (Symbol.iterator in h) {
1850 return Object.fromEntries(
1851 Array.from(
1852 h as Iterable<Iterable<string>>,
1853 (entry) => Array.from(entry) as [string, string],
1854 ),
1855 );
1856 }
1857 const headers: Record<string, string | readonly string[]> = {};
1858 for (const [name, value] of Object.entries<
1859 string | readonly string[] | undefined
1860 >(h)) {
1861 if (value !== undefined) headers[name] = value;
1862 }
1863 return headers;
1864 };
1865 return authClient<authOrganizationInviteCreateResponse>(
1866 getAuthOrganizationInviteCreateUrl(organizationId),
1867 {
1868 ...options,
1869 method: "POST",
1870 headers: {
1871 "Content-Type": "application/json",
1872 ...getHeaders(options?.headers),
1873 },
1874 body: JSON.stringify(createInviteBody),
1875 },
1876 );
1877};
1878
1879export type authOrganizationInviteRevokeResponse200 = {
1880 data: EnvelopeBool;
1881 status: 200;
1882};
1883
1884export type authOrganizationInviteRevokeResponse404 = {
1885 data: EnvelopeNoData;
1886 status: 404;
1887};
1888
1889export type authOrganizationInviteRevokeResponseSuccess =
1890 authOrganizationInviteRevokeResponse200 & {
1891 headers: Headers;
1892 };
1893export type authOrganizationInviteRevokeResponseError =
1894 authOrganizationInviteRevokeResponse404 & {
1895 headers: Headers;
1896 };
1897
1898export type authOrganizationInviteRevokeResponse =
1899 | authOrganizationInviteRevokeResponseSuccess
1900 | authOrganizationInviteRevokeResponseError;
1901
1902export const getAuthOrganizationInviteRevokeUrl = (
1903 organizationId: string,
1904 inviteId: string,
1905) => {
1906 return `/api/v1/organizations/${organizationId}/invites/${inviteId}`;
1907};
1908
1909/**
1910 * 需要 `invite.revoke`。已撤销的邀请再次撤销返回 false。
1911 * @summary 撤销邀请
1912 */
1913export const authOrganizationInviteRevoke = async (
1914 organizationId: string,
1915 inviteId: string,
1916 options?: Parameters<typeof authClient>[1],
1917): Promise<authOrganizationInviteRevokeResponse> => {
1918 return authClient<authOrganizationInviteRevokeResponse>(
1919 getAuthOrganizationInviteRevokeUrl(organizationId, inviteId),
1920 {
1921 ...options,
1922 method: "DELETE",
1923 },
1924 );
1925};
1926
1927export type authOrganizationLeaveResponse200 = {
1928 data: EnvelopeBool;
1929 status: 200;
1930};
1931
1932export type authOrganizationLeaveResponse409 = {
1933 data: EnvelopeNoData;
1934 status: 409;
1935};
1936
1937export type authOrganizationLeaveResponseSuccess =
1938 authOrganizationLeaveResponse200 & {
1939 headers: Headers;
1940 };
1941export type authOrganizationLeaveResponseError =
1942 authOrganizationLeaveResponse409 & {
1943 headers: Headers;
1944 };
1945
1946export type authOrganizationLeaveResponse =
1947 authOrganizationLeaveResponseSuccess | authOrganizationLeaveResponseError;
1948
1949export const getAuthOrganizationLeaveUrl = (organizationId: string) => {
1950 return `/api/v1/organizations/${organizationId}/leave`;
1951};
1952
1953/**
1954 * OWNER 必须先转移所有权,否则返回 409。
1955 * @summary 退出组织
1956 */
1957export const authOrganizationLeave = async (
1958 organizationId: string,
1959 options?: Parameters<typeof authClient>[1],
1960): Promise<authOrganizationLeaveResponse> => {
1961 return authClient<authOrganizationLeaveResponse>(
1962 getAuthOrganizationLeaveUrl(organizationId),
1963 {
1964 ...options,
1965 method: "POST",
1966 },
1967 );
1968};
1969
1970export type authOrganizationMemberListResponse200 = {
1971 data: EnvelopeVecMemberSummary;
1972 status: 200;
1973};
1974
1975export type authOrganizationMemberListResponse403 = {
1976 data: EnvelopeNoData;
1977 status: 403;
1978};
1979
1980export type authOrganizationMemberListResponseSuccess =
1981 authOrganizationMemberListResponse200 & {
1982 headers: Headers;
1983 };
1984export type authOrganizationMemberListResponseError =
1985 authOrganizationMemberListResponse403 & {
1986 headers: Headers;
1987 };
1988
1989export type authOrganizationMemberListResponse =
1990 | authOrganizationMemberListResponseSuccess
1991 | authOrganizationMemberListResponseError;
1992
1993export const getAuthOrganizationMemberListUrl = (
1994 organizationId: string,
1995 params?: AuthOrganizationMemberListParams,
1996) => {
1997 const normalizedParams = new URLSearchParams();
1998
1999 Object.entries(params || {}).forEach(([key, value]) => {
2000 if (value !== undefined) {
2001 normalizedParams.append(key, value === null ? "null" : String(value));
2002 }
2003 });
2004
2005 const stringifiedParams = normalizedParams.toString();
2006
2007 return stringifiedParams.length > 0
2008 ? `/api/v1/organizations/${organizationId}/members?${stringifiedParams}`
2009 : `/api/v1/organizations/${organizationId}/members`;
2010};
2011
2012/**
2013 * 需要 `member.view`。默认只返回活跃成员。
2014 * @summary 列示组织成员
2015 */
2016export const authOrganizationMemberList = async (
2017 organizationId: string,
2018 params?: AuthOrganizationMemberListParams,
2019 options?: Parameters<typeof authClient>[1],
2020): Promise<authOrganizationMemberListResponse> => {
2021 return authClient<authOrganizationMemberListResponse>(
2022 getAuthOrganizationMemberListUrl(organizationId, params),
2023 {
2024 ...options,
2025 method: "GET",
2026 },
2027 );
2028};
2029
2030export type authOrganizationMemberAddResponse200 = {
2031 data: EnvelopeMemberSummary;
2032 status: 200;
2033};
2034
2035export type authOrganizationMemberAddResponse403 = {
2036 data: EnvelopeNoData;
2037 status: 403;
2038};
2039
2040export type authOrganizationMemberAddResponse404 = {
2041 data: EnvelopeNoData;
2042 status: 404;
2043};
2044
2045export type authOrganizationMemberAddResponseSuccess =
2046 authOrganizationMemberAddResponse200 & {
2047 headers: Headers;
2048 };
2049export type authOrganizationMemberAddResponseError = (
2050 authOrganizationMemberAddResponse403 | authOrganizationMemberAddResponse404
2051) & {
2052 headers: Headers;
2053};
2054
2055export type authOrganizationMemberAddResponse =
2056 | authOrganizationMemberAddResponseSuccess
2057 | authOrganizationMemberAddResponseError;
2058
2059export const getAuthOrganizationMemberAddUrl = (organizationId: string) => {
2060 return `/api/v1/organizations/${organizationId}/members`;
2061};
2062
2063/**
2064 * 需要 `member.add`。已退出的会籍按复活处理,已封禁的返回 403。
2065 * @summary 直接添加成员
2066 */
2067export const authOrganizationMemberAdd = async (
2068 organizationId: string,
2069 addMemberBody: AddMemberBody,
2070 options?: Parameters<typeof authClient>[1],
2071): Promise<authOrganizationMemberAddResponse> => {
2072 const getHeaders = (
2073 h?: NonNullable<RequestInit["headers"]>,
2074 ): Record<string, string | readonly string[]> => {
2075 if (!h) return {};
2076 if (h instanceof Headers) return Object.fromEntries(h.entries());
2077 if (Symbol.iterator in h) {
2078 return Object.fromEntries(
2079 Array.from(
2080 h as Iterable<Iterable<string>>,
2081 (entry) => Array.from(entry) as [string, string],
2082 ),
2083 );
2084 }
2085 const headers: Record<string, string | readonly string[]> = {};
2086 for (const [name, value] of Object.entries<
2087 string | readonly string[] | undefined
2088 >(h)) {
2089 if (value !== undefined) headers[name] = value;
2090 }
2091 return headers;
2092 };
2093 return authClient<authOrganizationMemberAddResponse>(
2094 getAuthOrganizationMemberAddUrl(organizationId),
2095 {
2096 ...options,
2097 method: "POST",
2098 headers: {
2099 "Content-Type": "application/json",
2100 ...getHeaders(options?.headers),
2101 },
2102 body: JSON.stringify(addMemberBody),
2103 },
2104 );
2105};
2106
2107export type authOrganizationMemberUpdateSelfResponse200 = {
2108 data: EnvelopeMemberSummary;
2109 status: 200;
2110};
2111
2112export type authOrganizationMemberUpdateSelfResponse403 = {
2113 data: EnvelopeNoData;
2114 status: 403;
2115};
2116
2117export type authOrganizationMemberUpdateSelfResponseSuccess =
2118 authOrganizationMemberUpdateSelfResponse200 & {
2119 headers: Headers;
2120 };
2121export type authOrganizationMemberUpdateSelfResponseError =
2122 authOrganizationMemberUpdateSelfResponse403 & {
2123 headers: Headers;
2124 };
2125
2126export type authOrganizationMemberUpdateSelfResponse =
2127 | authOrganizationMemberUpdateSelfResponseSuccess
2128 | authOrganizationMemberUpdateSelfResponseError;
2129
2130export const getAuthOrganizationMemberUpdateSelfUrl = (
2131 organizationId: string,
2132) => {
2133 return `/api/v1/organizations/${organizationId}/members/@me`;
2134};
2135
2136/**
2137 * 只允许修改自己的别名、头像与通知偏好;无需 `member.role.update`。
2138 * @summary 更新自己的会籍资料
2139 */
2140export const authOrganizationMemberUpdateSelf = async (
2141 organizationId: string,
2142 updateMemberBody: UpdateMemberBody,
2143 options?: Parameters<typeof authClient>[1],
2144): Promise<authOrganizationMemberUpdateSelfResponse> => {
2145 const getHeaders = (
2146 h?: NonNullable<RequestInit["headers"]>,
2147 ): Record<string, string | readonly string[]> => {
2148 if (!h) return {};
2149 if (h instanceof Headers) return Object.fromEntries(h.entries());
2150 if (Symbol.iterator in h) {
2151 return Object.fromEntries(
2152 Array.from(
2153 h as Iterable<Iterable<string>>,
2154 (entry) => Array.from(entry) as [string, string],
2155 ),
2156 );
2157 }
2158 const headers: Record<string, string | readonly string[]> = {};
2159 for (const [name, value] of Object.entries<
2160 string | readonly string[] | undefined
2161 >(h)) {
2162 if (value !== undefined) headers[name] = value;
2163 }
2164 return headers;
2165 };
2166 return authClient<authOrganizationMemberUpdateSelfResponse>(
2167 getAuthOrganizationMemberUpdateSelfUrl(organizationId),
2168 {
2169 ...options,
2170 method: "PATCH",
2171 headers: {
2172 "Content-Type": "application/json",
2173 ...getHeaders(options?.headers),
2174 },
2175 body: JSON.stringify(updateMemberBody),
2176 },
2177 );
2178};
2179
2180export type authOrganizationMemberGetResponse200 = {
2181 data: EnvelopeMemberSummary;
2182 status: 200;
2183};
2184
2185export type authOrganizationMemberGetResponse404 = {
2186 data: EnvelopeNoData;
2187 status: 404;
2188};
2189
2190export type authOrganizationMemberGetResponseSuccess =
2191 authOrganizationMemberGetResponse200 & {
2192 headers: Headers;
2193 };
2194export type authOrganizationMemberGetResponseError =
2195 authOrganizationMemberGetResponse404 & {
2196 headers: Headers;
2197 };
2198
2199export type authOrganizationMemberGetResponse =
2200 | authOrganizationMemberGetResponseSuccess
2201 | authOrganizationMemberGetResponseError;
2202
2203export const getAuthOrganizationMemberGetUrl = (
2204 organizationId: string,
2205 memberId: string,
2206) => {
2207 return `/api/v1/organizations/${organizationId}/members/${memberId}`;
2208};
2209
2210/**
2211 * 需要 `member.view`。包含别名、通知偏好与封禁附加信息。
2212 * @summary 成员详情
2213 */
2214export const authOrganizationMemberGet = async (
2215 organizationId: string,
2216 memberId: string,
2217 options?: Parameters<typeof authClient>[1],
2218): Promise<authOrganizationMemberGetResponse> => {
2219 return authClient<authOrganizationMemberGetResponse>(
2220 getAuthOrganizationMemberGetUrl(organizationId, memberId),
2221 {
2222 ...options,
2223 method: "GET",
2224 },
2225 );
2226};
2227
2228export type authOrganizationMemberRemoveResponse200 = {
2229 data: EnvelopeBool;
2230 status: 200;
2231};
2232
2233export type authOrganizationMemberRemoveResponse403 = {
2234 data: EnvelopeNoData;
2235 status: 403;
2236};
2237
2238export type authOrganizationMemberRemoveResponseSuccess =
2239 authOrganizationMemberRemoveResponse200 & {
2240 headers: Headers;
2241 };
2242export type authOrganizationMemberRemoveResponseError =
2243 authOrganizationMemberRemoveResponse403 & {
2244 headers: Headers;
2245 };
2246
2247export type authOrganizationMemberRemoveResponse =
2248 | authOrganizationMemberRemoveResponseSuccess
2249 | authOrganizationMemberRemoveResponseError;
2250
2251export const getAuthOrganizationMemberRemoveUrl = (
2252 organizationId: string,
2253 memberId: string,
2254 params?: AuthOrganizationMemberRemoveParams,
2255) => {
2256 const normalizedParams = new URLSearchParams();
2257
2258 Object.entries(params || {}).forEach(([key, value]) => {
2259 if (value !== undefined) {
2260 normalizedParams.append(key, value === null ? "null" : String(value));
2261 }
2262 });
2263
2264 const stringifiedParams = normalizedParams.toString();
2265
2266 return stringifiedParams.length > 0
2267 ? `/api/v1/organizations/${organizationId}/members/${memberId}?${stringifiedParams}`
2268 : `/api/v1/organizations/${organizationId}/members/${memberId}`;
2269};
2270
2271/**
2272 * 需要 `member.remove`。OWNER 不能移除,需先转移所有权。会籍行保留用于复活。
2273 * @summary 移除成员
2274 */
2275export const authOrganizationMemberRemove = async (
2276 organizationId: string,
2277 memberId: string,
2278 params?: AuthOrganizationMemberRemoveParams,
2279 options?: Parameters<typeof authClient>[1],
2280): Promise<authOrganizationMemberRemoveResponse> => {
2281 return authClient<authOrganizationMemberRemoveResponse>(
2282 getAuthOrganizationMemberRemoveUrl(organizationId, memberId, params),
2283 {
2284 ...options,
2285 method: "DELETE",
2286 },
2287 );
2288};
2289
2290export type authOrganizationMemberUpdateResponse200 = {
2291 data: EnvelopeMemberSummary;
2292 status: 200;
2293};
2294
2295export type authOrganizationMemberUpdateResponse403 = {
2296 data: EnvelopeNoData;
2297 status: 403;
2298};
2299
2300export type authOrganizationMemberUpdateResponse409 = {
2301 data: EnvelopeNoData;
2302 status: 409;
2303};
2304
2305export type authOrganizationMemberUpdateResponseSuccess =
2306 authOrganizationMemberUpdateResponse200 & {
2307 headers: Headers;
2308 };
2309export type authOrganizationMemberUpdateResponseError = (
2310 | authOrganizationMemberUpdateResponse403
2311 | authOrganizationMemberUpdateResponse409
2312) & {
2313 headers: Headers;
2314};
2315
2316export type authOrganizationMemberUpdateResponse =
2317 | authOrganizationMemberUpdateResponseSuccess
2318 | authOrganizationMemberUpdateResponseError;
2319
2320export const getAuthOrganizationMemberUpdateUrl = (
2321 organizationId: string,
2322 memberId: string,
2323) => {
2324 return `/api/v1/organizations/${organizationId}/members/${memberId}`;
2325};
2326
2327/**
2328 * 需要 `member.role.update`。修改别名、头像、通知偏好与免打扰;封禁中的会籍走封禁接口。
2329 * @summary 更新成员资料
2330 */
2331export const authOrganizationMemberUpdate = async (
2332 organizationId: string,
2333 memberId: string,
2334 updateMemberBody: UpdateMemberBody,
2335 options?: Parameters<typeof authClient>[1],
2336): Promise<authOrganizationMemberUpdateResponse> => {
2337 const getHeaders = (
2338 h?: NonNullable<RequestInit["headers"]>,
2339 ): Record<string, string | readonly string[]> => {
2340 if (!h) return {};
2341 if (h instanceof Headers) return Object.fromEntries(h.entries());
2342 if (Symbol.iterator in h) {
2343 return Object.fromEntries(
2344 Array.from(
2345 h as Iterable<Iterable<string>>,
2346 (entry) => Array.from(entry) as [string, string],
2347 ),
2348 );
2349 }
2350 const headers: Record<string, string | readonly string[]> = {};
2351 for (const [name, value] of Object.entries<
2352 string | readonly string[] | undefined
2353 >(h)) {
2354 if (value !== undefined) headers[name] = value;
2355 }
2356 return headers;
2357 };
2358 return authClient<authOrganizationMemberUpdateResponse>(
2359 getAuthOrganizationMemberUpdateUrl(organizationId, memberId),
2360 {
2361 ...options,
2362 method: "PATCH",
2363 headers: {
2364 "Content-Type": "application/json",
2365 ...getHeaders(options?.headers),
2366 },
2367 body: JSON.stringify(updateMemberBody),
2368 },
2369 );
2370};
2371
2372export type authOrganizationMemberBanResponse200 = {
2373 data: EnvelopeMemberSummary;
2374 status: 200;
2375};
2376
2377export type authOrganizationMemberBanResponse403 = {
2378 data: EnvelopeNoData;
2379 status: 403;
2380};
2381
2382export type authOrganizationMemberBanResponseSuccess =
2383 authOrganizationMemberBanResponse200 & {
2384 headers: Headers;
2385 };
2386export type authOrganizationMemberBanResponseError =
2387 authOrganizationMemberBanResponse403 & {
2388 headers: Headers;
2389 };
2390
2391export type authOrganizationMemberBanResponse =
2392 | authOrganizationMemberBanResponseSuccess
2393 | authOrganizationMemberBanResponseError;
2394
2395export const getAuthOrganizationMemberBanUrl = (
2396 organizationId: string,
2397 memberId: string,
2398) => {
2399 return `/api/v1/organizations/${organizationId}/members/${memberId}/ban`;
2400};
2401
2402/**
2403 * 写 banned_at 并清空角色绑定与直挂权限。封禁优先于一切授权。
2404 * @summary 封禁成员
2405 */
2406export const authOrganizationMemberBan = async (
2407 organizationId: string,
2408 memberId: string,
2409 banMemberBody: BanMemberBody,
2410 options?: Parameters<typeof authClient>[1],
2411): Promise<authOrganizationMemberBanResponse> => {
2412 const getHeaders = (
2413 h?: NonNullable<RequestInit["headers"]>,
2414 ): Record<string, string | readonly string[]> => {
2415 if (!h) return {};
2416 if (h instanceof Headers) return Object.fromEntries(h.entries());
2417 if (Symbol.iterator in h) {
2418 return Object.fromEntries(
2419 Array.from(
2420 h as Iterable<Iterable<string>>,
2421 (entry) => Array.from(entry) as [string, string],
2422 ),
2423 );
2424 }
2425 const headers: Record<string, string | readonly string[]> = {};
2426 for (const [name, value] of Object.entries<
2427 string | readonly string[] | undefined
2428 >(h)) {
2429 if (value !== undefined) headers[name] = value;
2430 }
2431 return headers;
2432 };
2433 return authClient<authOrganizationMemberBanResponse>(
2434 getAuthOrganizationMemberBanUrl(organizationId, memberId),
2435 {
2436 ...options,
2437 method: "POST",
2438 headers: {
2439 "Content-Type": "application/json",
2440 ...getHeaders(options?.headers),
2441 },
2442 body: JSON.stringify(banMemberBody),
2443 },
2444 );
2445};
2446
2447export type authOrganizationMemberPermissionsResponse200 = {
2448 data: EnvelopeVecString;
2449 status: 200;
2450};
2451
2452export type authOrganizationMemberPermissionsResponse403 = {
2453 data: EnvelopeNoData;
2454 status: 403;
2455};
2456
2457export type authOrganizationMemberPermissionsResponseSuccess =
2458 authOrganizationMemberPermissionsResponse200 & {
2459 headers: Headers;
2460 };
2461export type authOrganizationMemberPermissionsResponseError =
2462 authOrganizationMemberPermissionsResponse403 & {
2463 headers: Headers;
2464 };
2465
2466export type authOrganizationMemberPermissionsResponse =
2467 | authOrganizationMemberPermissionsResponseSuccess
2468 | authOrganizationMemberPermissionsResponseError;
2469
2470export const getAuthOrganizationMemberPermissionsUrl = (
2471 organizationId: string,
2472 memberId: string,
2473) => {
2474 return `/api/v1/organizations/${organizationId}/members/${memberId}/permissions`;
2475};
2476
2477/**
2478 * 整表替换语义。需要 `role.permission.update`,OWNER 不接受直挂覆盖。
2479 * @summary 设置成员直挂权限
2480 */
2481export const authOrganizationMemberPermissions = async (
2482 organizationId: string,
2483 memberId: string,
2484 setRolePermissionsBody: SetRolePermissionsBody,
2485 options?: Parameters<typeof authClient>[1],
2486): Promise<authOrganizationMemberPermissionsResponse> => {
2487 const getHeaders = (
2488 h?: NonNullable<RequestInit["headers"]>,
2489 ): Record<string, string | readonly string[]> => {
2490 if (!h) return {};
2491 if (h instanceof Headers) return Object.fromEntries(h.entries());
2492 if (Symbol.iterator in h) {
2493 return Object.fromEntries(
2494 Array.from(
2495 h as Iterable<Iterable<string>>,
2496 (entry) => Array.from(entry) as [string, string],
2497 ),
2498 );
2499 }
2500 const headers: Record<string, string | readonly string[]> = {};
2501 for (const [name, value] of Object.entries<
2502 string | readonly string[] | undefined
2503 >(h)) {
2504 if (value !== undefined) headers[name] = value;
2505 }
2506 return headers;
2507 };
2508 return authClient<authOrganizationMemberPermissionsResponse>(
2509 getAuthOrganizationMemberPermissionsUrl(organizationId, memberId),
2510 {
2511 ...options,
2512 method: "PUT",
2513 headers: {
2514 "Content-Type": "application/json",
2515 ...getHeaders(options?.headers),
2516 },
2517 body: JSON.stringify(setRolePermissionsBody),
2518 },
2519 );
2520};
2521
2522export type authOrganizationMemberRoleResponse200 = {
2523 data: EnvelopeMemberSummary;
2524 status: 200;
2525};
2526
2527export type authOrganizationMemberRoleResponse403 = {
2528 data: EnvelopeNoData;
2529 status: 403;
2530};
2531
2532export type authOrganizationMemberRoleResponse409 = {
2533 data: EnvelopeNoData;
2534 status: 409;
2535};
2536
2537export type authOrganizationMemberRoleResponseSuccess =
2538 authOrganizationMemberRoleResponse200 & {
2539 headers: Headers;
2540 };
2541export type authOrganizationMemberRoleResponseError = (
2542 authOrganizationMemberRoleResponse403 | authOrganizationMemberRoleResponse409
2543) & {
2544 headers: Headers;
2545};
2546
2547export type authOrganizationMemberRoleResponse =
2548 | authOrganizationMemberRoleResponseSuccess
2549 | authOrganizationMemberRoleResponseError;
2550
2551export const getAuthOrganizationMemberRoleUrl = (
2552 organizationId: string,
2553 memberId: string,
2554) => {
2555 return `/api/v1/organizations/${organizationId}/members/${memberId}/role`;
2556};
2557
2558/**
2559 * 只接受 MEMBER 与 ADMIN。OWNER 只能通过转移所有权变更。
2560 * @summary 改粗粒度角色
2561 */
2562export const authOrganizationMemberRole = async (
2563 organizationId: string,
2564 memberId: string,
2565 setMemberRoleBody: SetMemberRoleBody,
2566 options?: Parameters<typeof authClient>[1],
2567): Promise<authOrganizationMemberRoleResponse> => {
2568 const getHeaders = (
2569 h?: NonNullable<RequestInit["headers"]>,
2570 ): Record<string, string | readonly string[]> => {
2571 if (!h) return {};
2572 if (h instanceof Headers) return Object.fromEntries(h.entries());
2573 if (Symbol.iterator in h) {
2574 return Object.fromEntries(
2575 Array.from(
2576 h as Iterable<Iterable<string>>,
2577 (entry) => Array.from(entry) as [string, string],
2578 ),
2579 );
2580 }
2581 const headers: Record<string, string | readonly string[]> = {};
2582 for (const [name, value] of Object.entries<
2583 string | readonly string[] | undefined
2584 >(h)) {
2585 if (value !== undefined) headers[name] = value;
2586 }
2587 return headers;
2588 };
2589 return authClient<authOrganizationMemberRoleResponse>(
2590 getAuthOrganizationMemberRoleUrl(organizationId, memberId),
2591 {
2592 ...options,
2593 method: "PATCH",
2594 headers: {
2595 "Content-Type": "application/json",
2596 ...getHeaders(options?.headers),
2597 },
2598 body: JSON.stringify(setMemberRoleBody),
2599 },
2600 );
2601};
2602
2603export type authOrganizationMemberUnbanResponse200 = {
2604 data: EnvelopeMemberSummary;
2605 status: 200;
2606};
2607
2608export type authOrganizationMemberUnbanResponse403 = {
2609 data: EnvelopeNoData;
2610 status: 403;
2611};
2612
2613export type authOrganizationMemberUnbanResponseSuccess =
2614 authOrganizationMemberUnbanResponse200 & {
2615 headers: Headers;
2616 };
2617export type authOrganizationMemberUnbanResponseError =
2618 authOrganizationMemberUnbanResponse403 & {
2619 headers: Headers;
2620 };
2621
2622export type authOrganizationMemberUnbanResponse =
2623 | authOrganizationMemberUnbanResponseSuccess
2624 | authOrganizationMemberUnbanResponseError;
2625
2626export const getAuthOrganizationMemberUnbanUrl = (
2627 organizationId: string,
2628 memberId: string,
2629) => {
2630 return `/api/v1/organizations/${organizationId}/members/${memberId}/unban`;
2631};
2632
2633/**
2634 * 清空 banned_at。已清空的角色不会自动恢复。
2635 * @summary 解封成员
2636 */
2637export const authOrganizationMemberUnban = async (
2638 organizationId: string,
2639 memberId: string,
2640 options?: Parameters<typeof authClient>[1],
2641): Promise<authOrganizationMemberUnbanResponse> => {
2642 return authClient<authOrganizationMemberUnbanResponse>(
2643 getAuthOrganizationMemberUnbanUrl(organizationId, memberId),
2644 {
2645 ...options,
2646 method: "POST",
2647 },
2648 );
2649};
2650
2651export type authOrganizationPermissionCheckResponse200 = {
2652 data: EnvelopePermissionCheck;
2653 status: 200;
2654};
2655
2656export type authOrganizationPermissionCheckResponse403 = {
2657 data: EnvelopeNoData;
2658 status: 403;
2659};
2660
2661export type authOrganizationPermissionCheckResponseSuccess =
2662 authOrganizationPermissionCheckResponse200 & {
2663 headers: Headers;
2664 };
2665export type authOrganizationPermissionCheckResponseError =
2666 authOrganizationPermissionCheckResponse403 & {
2667 headers: Headers;
2668 };
2669
2670export type authOrganizationPermissionCheckResponse =
2671 | authOrganizationPermissionCheckResponseSuccess
2672 | authOrganizationPermissionCheckResponseError;
2673
2674export const getAuthOrganizationPermissionCheckUrl = (
2675 organizationId: string,
2676) => {
2677 return `/api/v1/organizations/${organizationId}/permissions/check`;
2678};
2679
2680/**
2681 * 拒绝不作为错误返回:`allowed=false` 附 `reason` 与 `matched_by`,便于调用方聚合。
2682 * @summary 单权限判定
2683 */
2684export const authOrganizationPermissionCheck = async (
2685 organizationId: string,
2686 checkPermissionBody: CheckPermissionBody,
2687 options?: Parameters<typeof authClient>[1],
2688): Promise<authOrganizationPermissionCheckResponse> => {
2689 const getHeaders = (
2690 h?: NonNullable<RequestInit["headers"]>,
2691 ): Record<string, string | readonly string[]> => {
2692 if (!h) return {};
2693 if (h instanceof Headers) return Object.fromEntries(h.entries());
2694 if (Symbol.iterator in h) {
2695 return Object.fromEntries(
2696 Array.from(
2697 h as Iterable<Iterable<string>>,
2698 (entry) => Array.from(entry) as [string, string],
2699 ),
2700 );
2701 }
2702 const headers: Record<string, string | readonly string[]> = {};
2703 for (const [name, value] of Object.entries<
2704 string | readonly string[] | undefined
2705 >(h)) {
2706 if (value !== undefined) headers[name] = value;
2707 }
2708 return headers;
2709 };
2710 return authClient<authOrganizationPermissionCheckResponse>(
2711 getAuthOrganizationPermissionCheckUrl(organizationId),
2712 {
2713 ...options,
2714 method: "POST",
2715 headers: {
2716 "Content-Type": "application/json",
2717 ...getHeaders(options?.headers),
2718 },
2719 body: JSON.stringify(checkPermissionBody),
2720 },
2721 );
2722};
2723
2724export type authOrganizationRestoreResponse200 = {
2725 data: EnvelopeOrganizationSummary;
2726 status: 200;
2727};
2728
2729export type authOrganizationRestoreResponse403 = {
2730 data: EnvelopeNoData;
2731 status: 403;
2732};
2733
2734export type authOrganizationRestoreResponseSuccess =
2735 authOrganizationRestoreResponse200 & {
2736 headers: Headers;
2737 };
2738export type authOrganizationRestoreResponseError =
2739 authOrganizationRestoreResponse403 & {
2740 headers: Headers;
2741 };
2742
2743export type authOrganizationRestoreResponse =
2744 authOrganizationRestoreResponseSuccess | authOrganizationRestoreResponseError;
2745
2746export const getAuthOrganizationRestoreUrl = (organizationId: string) => {
2747 return `/api/v1/organizations/${organizationId}/restore`;
2748};
2749
2750/**
2751 * 仅 OWNER 或平台管理员可执行。
2752 * @summary 恢复归档组织
2753 */
2754export const authOrganizationRestore = async (
2755 organizationId: string,
2756 options?: Parameters<typeof authClient>[1],
2757): Promise<authOrganizationRestoreResponse> => {
2758 return authClient<authOrganizationRestoreResponse>(
2759 getAuthOrganizationRestoreUrl(organizationId),
2760 {
2761 ...options,
2762 method: "POST",
2763 },
2764 );
2765};
2766
2767export type authOrganizationRoleListResponse200 = {
2768 data: EnvelopeVecRoleSummary;
2769 status: 200;
2770};
2771
2772export type authOrganizationRoleListResponse403 = {
2773 data: EnvelopeNoData;
2774 status: 403;
2775};
2776
2777export type authOrganizationRoleListResponseSuccess =
2778 authOrganizationRoleListResponse200 & {
2779 headers: Headers;
2780 };
2781export type authOrganizationRoleListResponseError =
2782 authOrganizationRoleListResponse403 & {
2783 headers: Headers;
2784 };
2785
2786export type authOrganizationRoleListResponse =
2787 | authOrganizationRoleListResponseSuccess
2788 | authOrganizationRoleListResponseError;
2789
2790export const getAuthOrganizationRoleListUrl = (organizationId: string) => {
2791 return `/api/v1/organizations/${organizationId}/roles`;
2792};
2793
2794/**
2795 * 需要 `role.view`。含内置 owner/admin/member 三个系统角色。
2796 * @summary 列示组织角色
2797 */
2798export const authOrganizationRoleList = async (
2799 organizationId: string,
2800 options?: Parameters<typeof authClient>[1],
2801): Promise<authOrganizationRoleListResponse> => {
2802 return authClient<authOrganizationRoleListResponse>(
2803 getAuthOrganizationRoleListUrl(organizationId),
2804 {
2805 ...options,
2806 method: "GET",
2807 },
2808 );
2809};
2810
2811export type authOrganizationRoleCreateResponse200 = {
2812 data: EnvelopeRoleSummary;
2813 status: 200;
2814};
2815
2816export type authOrganizationRoleCreateResponse409 = {
2817 data: EnvelopeNoData;
2818 status: 409;
2819};
2820
2821export type authOrganizationRoleCreateResponseSuccess =
2822 authOrganizationRoleCreateResponse200 & {
2823 headers: Headers;
2824 };
2825export type authOrganizationRoleCreateResponseError =
2826 authOrganizationRoleCreateResponse409 & {
2827 headers: Headers;
2828 };
2829
2830export type authOrganizationRoleCreateResponse =
2831 | authOrganizationRoleCreateResponseSuccess
2832 | authOrganizationRoleCreateResponseError;
2833
2834export const getAuthOrganizationRoleCreateUrl = (organizationId: string) => {
2835 return `/api/v1/organizations/${organizationId}/roles`;
2836};
2837
2838/**
2839 * 需要 `role.create`。key 在组织内唯一,权限键必须在白名单内。
2840 * @summary 新建角色
2841 */
2842export const authOrganizationRoleCreate = async (
2843 organizationId: string,
2844 createRoleBody: CreateRoleBody,
2845 options?: Parameters<typeof authClient>[1],
2846): Promise<authOrganizationRoleCreateResponse> => {
2847 const getHeaders = (
2848 h?: NonNullable<RequestInit["headers"]>,
2849 ): Record<string, string | readonly string[]> => {
2850 if (!h) return {};
2851 if (h instanceof Headers) return Object.fromEntries(h.entries());
2852 if (Symbol.iterator in h) {
2853 return Object.fromEntries(
2854 Array.from(
2855 h as Iterable<Iterable<string>>,
2856 (entry) => Array.from(entry) as [string, string],
2857 ),
2858 );
2859 }
2860 const headers: Record<string, string | readonly string[]> = {};
2861 for (const [name, value] of Object.entries<
2862 string | readonly string[] | undefined
2863 >(h)) {
2864 if (value !== undefined) headers[name] = value;
2865 }
2866 return headers;
2867 };
2868 return authClient<authOrganizationRoleCreateResponse>(
2869 getAuthOrganizationRoleCreateUrl(organizationId),
2870 {
2871 ...options,
2872 method: "POST",
2873 headers: {
2874 "Content-Type": "application/json",
2875 ...getHeaders(options?.headers),
2876 },
2877 body: JSON.stringify(createRoleBody),
2878 },
2879 );
2880};
2881
2882export type authOrganizationRoleAssignResponse200 = {
2883 data: EnvelopeVecString;
2884 status: 200;
2885};
2886
2887export type authOrganizationRoleAssignResponse404 = {
2888 data: EnvelopeNoData;
2889 status: 404;
2890};
2891
2892export type authOrganizationRoleAssignResponseSuccess =
2893 authOrganizationRoleAssignResponse200 & {
2894 headers: Headers;
2895 };
2896export type authOrganizationRoleAssignResponseError =
2897 authOrganizationRoleAssignResponse404 & {
2898 headers: Headers;
2899 };
2900
2901export type authOrganizationRoleAssignResponse =
2902 | authOrganizationRoleAssignResponseSuccess
2903 | authOrganizationRoleAssignResponseError;
2904
2905export const getAuthOrganizationRoleAssignUrl = (organizationId: string) => {
2906 return `/api/v1/organizations/${organizationId}/roles/assign`;
2907};
2908
2909/**
2910 * 需要 `role.assign`。重复绑定幂等。
2911 * @summary 绑定角色到成员
2912 */
2913export const authOrganizationRoleAssign = async (
2914 organizationId: string,
2915 assignRoleBody: AssignRoleBody,
2916 options?: Parameters<typeof authClient>[1],
2917): Promise<authOrganizationRoleAssignResponse> => {
2918 const getHeaders = (
2919 h?: NonNullable<RequestInit["headers"]>,
2920 ): Record<string, string | readonly string[]> => {
2921 if (!h) return {};
2922 if (h instanceof Headers) return Object.fromEntries(h.entries());
2923 if (Symbol.iterator in h) {
2924 return Object.fromEntries(
2925 Array.from(
2926 h as Iterable<Iterable<string>>,
2927 (entry) => Array.from(entry) as [string, string],
2928 ),
2929 );
2930 }
2931 const headers: Record<string, string | readonly string[]> = {};
2932 for (const [name, value] of Object.entries<
2933 string | readonly string[] | undefined
2934 >(h)) {
2935 if (value !== undefined) headers[name] = value;
2936 }
2937 return headers;
2938 };
2939 return authClient<authOrganizationRoleAssignResponse>(
2940 getAuthOrganizationRoleAssignUrl(organizationId),
2941 {
2942 ...options,
2943 method: "POST",
2944 headers: {
2945 "Content-Type": "application/json",
2946 ...getHeaders(options?.headers),
2947 },
2948 body: JSON.stringify(assignRoleBody),
2949 },
2950 );
2951};
2952
2953export type authOrganizationRoleUnassignResponse200 = {
2954 data: EnvelopeVecString;
2955 status: 200;
2956};
2957
2958export type authOrganizationRoleUnassignResponse403 = {
2959 data: EnvelopeNoData;
2960 status: 403;
2961};
2962
2963export type authOrganizationRoleUnassignResponseSuccess =
2964 authOrganizationRoleUnassignResponse200 & {
2965 headers: Headers;
2966 };
2967export type authOrganizationRoleUnassignResponseError =
2968 authOrganizationRoleUnassignResponse403 & {
2969 headers: Headers;
2970 };
2971
2972export type authOrganizationRoleUnassignResponse =
2973 | authOrganizationRoleUnassignResponseSuccess
2974 | authOrganizationRoleUnassignResponseError;
2975
2976export const getAuthOrganizationRoleUnassignUrl = (organizationId: string) => {
2977 return `/api/v1/organizations/${organizationId}/roles/unassign`;
2978};
2979
2980/**
2981 * 需要 `role.assign`。系统角色不可解绑。
2982 * @summary 解绑成员角色
2983 */
2984export const authOrganizationRoleUnassign = async (
2985 organizationId: string,
2986 assignRoleBody: AssignRoleBody,
2987 options?: Parameters<typeof authClient>[1],
2988): Promise<authOrganizationRoleUnassignResponse> => {
2989 const getHeaders = (
2990 h?: NonNullable<RequestInit["headers"]>,
2991 ): Record<string, string | readonly string[]> => {
2992 if (!h) return {};
2993 if (h instanceof Headers) return Object.fromEntries(h.entries());
2994 if (Symbol.iterator in h) {
2995 return Object.fromEntries(
2996 Array.from(
2997 h as Iterable<Iterable<string>>,
2998 (entry) => Array.from(entry) as [string, string],
2999 ),
3000 );
3001 }
3002 const headers: Record<string, string | readonly string[]> = {};
3003 for (const [name, value] of Object.entries<
3004 string | readonly string[] | undefined
3005 >(h)) {
3006 if (value !== undefined) headers[name] = value;
3007 }
3008 return headers;
3009 };
3010 return authClient<authOrganizationRoleUnassignResponse>(
3011 getAuthOrganizationRoleUnassignUrl(organizationId),
3012 {
3013 ...options,
3014 method: "POST",
3015 headers: {
3016 "Content-Type": "application/json",
3017 ...getHeaders(options?.headers),
3018 },
3019 body: JSON.stringify(assignRoleBody),
3020 },
3021 );
3022};
3023
3024export type authOrganizationRoleGetResponse200 = {
3025 data: EnvelopeRoleSummary;
3026 status: 200;
3027};
3028
3029export type authOrganizationRoleGetResponse404 = {
3030 data: EnvelopeNoData;
3031 status: 404;
3032};
3033
3034export type authOrganizationRoleGetResponseSuccess =
3035 authOrganizationRoleGetResponse200 & {
3036 headers: Headers;
3037 };
3038export type authOrganizationRoleGetResponseError =
3039 authOrganizationRoleGetResponse404 & {
3040 headers: Headers;
3041 };
3042
3043export type authOrganizationRoleGetResponse =
3044 authOrganizationRoleGetResponseSuccess | authOrganizationRoleGetResponseError;
3045
3046export const getAuthOrganizationRoleGetUrl = (
3047 organizationId: string,
3048 roleId: string,
3049) => {
3050 return `/api/v1/organizations/${organizationId}/roles/${roleId}`;
3051};
3052
3053/**
3054 * 需要 `role.view`。
3055 * @summary 角色详情
3056 */
3057export const authOrganizationRoleGet = async (
3058 organizationId: string,
3059 roleId: string,
3060 options?: Parameters<typeof authClient>[1],
3061): Promise<authOrganizationRoleGetResponse> => {
3062 return authClient<authOrganizationRoleGetResponse>(
3063 getAuthOrganizationRoleGetUrl(organizationId, roleId),
3064 {
3065 ...options,
3066 method: "GET",
3067 },
3068 );
3069};
3070
3071export type authOrganizationRoleDeleteResponse200 = {
3072 data: EnvelopeBool;
3073 status: 200;
3074};
3075
3076export type authOrganizationRoleDeleteResponse409 = {
3077 data: EnvelopeNoData;
3078 status: 409;
3079};
3080
3081export type authOrganizationRoleDeleteResponseSuccess =
3082 authOrganizationRoleDeleteResponse200 & {
3083 headers: Headers;
3084 };
3085export type authOrganizationRoleDeleteResponseError =
3086 authOrganizationRoleDeleteResponse409 & {
3087 headers: Headers;
3088 };
3089
3090export type authOrganizationRoleDeleteResponse =
3091 | authOrganizationRoleDeleteResponseSuccess
3092 | authOrganizationRoleDeleteResponseError;
3093
3094export const getAuthOrganizationRoleDeleteUrl = (
3095 organizationId: string,
3096 roleId: string,
3097) => {
3098 return `/api/v1/organizations/${organizationId}/roles/${roleId}`;
3099};
3100
3101/**
3102 * 需要 `role.delete`。系统角色不可删;仍有成员绑定时返回 409。
3103 * @summary 删除角色
3104 */
3105export const authOrganizationRoleDelete = async (
3106 organizationId: string,
3107 roleId: string,
3108 options?: Parameters<typeof authClient>[1],
3109): Promise<authOrganizationRoleDeleteResponse> => {
3110 return authClient<authOrganizationRoleDeleteResponse>(
3111 getAuthOrganizationRoleDeleteUrl(organizationId, roleId),
3112 {
3113 ...options,
3114 method: "DELETE",
3115 },
3116 );
3117};
3118
3119export type authOrganizationRoleUpdateResponse200 = {
3120 data: EnvelopeRoleSummary;
3121 status: 200;
3122};
3123
3124export type authOrganizationRoleUpdateResponse409 = {
3125 data: EnvelopeNoData;
3126 status: 409;
3127};
3128
3129export type authOrganizationRoleUpdateResponseSuccess =
3130 authOrganizationRoleUpdateResponse200 & {
3131 headers: Headers;
3132 };
3133export type authOrganizationRoleUpdateResponseError =
3134 authOrganizationRoleUpdateResponse409 & {
3135 headers: Headers;
3136 };
3137
3138export type authOrganizationRoleUpdateResponse =
3139 | authOrganizationRoleUpdateResponseSuccess
3140 | authOrganizationRoleUpdateResponseError;
3141
3142export const getAuthOrganizationRoleUpdateUrl = (
3143 organizationId: string,
3144 roleId: string,
3145) => {
3146 return `/api/v1/organizations/${organizationId}/roles/${roleId}`;
3147};
3148
3149/**
3150 * 需要 `role.update`。OWNER 系统角色不允许改 rank 与 default_role。
3151 * @summary 修改角色
3152 */
3153export const authOrganizationRoleUpdate = async (
3154 organizationId: string,
3155 roleId: string,
3156 updateRoleBody: UpdateRoleBody,
3157 options?: Parameters<typeof authClient>[1],
3158): Promise<authOrganizationRoleUpdateResponse> => {
3159 const getHeaders = (
3160 h?: NonNullable<RequestInit["headers"]>,
3161 ): Record<string, string | readonly string[]> => {
3162 if (!h) return {};
3163 if (h instanceof Headers) return Object.fromEntries(h.entries());
3164 if (Symbol.iterator in h) {
3165 return Object.fromEntries(
3166 Array.from(
3167 h as Iterable<Iterable<string>>,
3168 (entry) => Array.from(entry) as [string, string],
3169 ),
3170 );
3171 }
3172 const headers: Record<string, string | readonly string[]> = {};
3173 for (const [name, value] of Object.entries<
3174 string | readonly string[] | undefined
3175 >(h)) {
3176 if (value !== undefined) headers[name] = value;
3177 }
3178 return headers;
3179 };
3180 return authClient<authOrganizationRoleUpdateResponse>(
3181 getAuthOrganizationRoleUpdateUrl(organizationId, roleId),
3182 {
3183 ...options,
3184 method: "PATCH",
3185 headers: {
3186 "Content-Type": "application/json",
3187 ...getHeaders(options?.headers),
3188 },
3189 body: JSON.stringify(updateRoleBody),
3190 },
3191 );
3192};
3193
3194export type authOrganizationRoleJoinResponse200 = {
3195 data: EnvelopeMemberSummary;
3196 status: 200;
3197};
3198
3199export type authOrganizationRoleJoinResponse403 = {
3200 data: EnvelopeNoData;
3201 status: 403;
3202};
3203
3204export type authOrganizationRoleJoinResponseSuccess =
3205 authOrganizationRoleJoinResponse200 & {
3206 headers: Headers;
3207 };
3208export type authOrganizationRoleJoinResponseError =
3209 authOrganizationRoleJoinResponse403 & {
3210 headers: Headers;
3211 };
3212
3213export type authOrganizationRoleJoinResponse =
3214 | authOrganizationRoleJoinResponseSuccess
3215 | authOrganizationRoleJoinResponseError;
3216
3217export const getAuthOrganizationRoleJoinUrl = (
3218 organizationId: string,
3219 roleId: string,
3220) => {
3221 return `/api/v1/organizations/${organizationId}/roles/${roleId}/join`;
3222};
3223
3224/**
3225 * 只允许 `self_assignable = true` 的非内置角色。
3226 * @summary 自助加入角色
3227 */
3228export const authOrganizationRoleJoin = async (
3229 organizationId: string,
3230 roleId: string,
3231 options?: Parameters<typeof authClient>[1],
3232): Promise<authOrganizationRoleJoinResponse> => {
3233 return authClient<authOrganizationRoleJoinResponse>(
3234 getAuthOrganizationRoleJoinUrl(organizationId, roleId),
3235 {
3236 ...options,
3237 method: "POST",
3238 },
3239 );
3240};
3241
3242export type authOrganizationRoleLeaveResponse200 = {
3243 data: EnvelopeMemberSummary;
3244 status: 200;
3245};
3246
3247export type authOrganizationRoleLeaveResponse403 = {
3248 data: EnvelopeNoData;
3249 status: 403;
3250};
3251
3252export type authOrganizationRoleLeaveResponseSuccess =
3253 authOrganizationRoleLeaveResponse200 & {
3254 headers: Headers;
3255 };
3256export type authOrganizationRoleLeaveResponseError =
3257 authOrganizationRoleLeaveResponse403 & {
3258 headers: Headers;
3259 };
3260
3261export type authOrganizationRoleLeaveResponse =
3262 | authOrganizationRoleLeaveResponseSuccess
3263 | authOrganizationRoleLeaveResponseError;
3264
3265export const getAuthOrganizationRoleLeaveUrl = (
3266 organizationId: string,
3267 roleId: string,
3268) => {
3269 return `/api/v1/organizations/${organizationId}/roles/${roleId}/join`;
3270};
3271
3272/**
3273 * 只允许 `self_assignable = true` 的非内置角色;重复退出视为成功。
3274 * @summary 自助退出角色
3275 */
3276export const authOrganizationRoleLeave = async (
3277 organizationId: string,
3278 roleId: string,
3279 options?: Parameters<typeof authClient>[1],
3280): Promise<authOrganizationRoleLeaveResponse> => {
3281 return authClient<authOrganizationRoleLeaveResponse>(
3282 getAuthOrganizationRoleLeaveUrl(organizationId, roleId),
3283 {
3284 ...options,
3285 method: "DELETE",
3286 },
3287 );
3288};
3289
3290export type authOrganizationRolePermissionsResponse200 = {
3291 data: EnvelopeRoleSummary;
3292 status: 200;
3293};
3294
3295export type authOrganizationRolePermissionsResponse403 = {
3296 data: EnvelopeNoData;
3297 status: 403;
3298};
3299
3300export type authOrganizationRolePermissionsResponseSuccess =
3301 authOrganizationRolePermissionsResponse200 & {
3302 headers: Headers;
3303 };
3304export type authOrganizationRolePermissionsResponseError =
3305 authOrganizationRolePermissionsResponse403 & {
3306 headers: Headers;
3307 };
3308
3309export type authOrganizationRolePermissionsResponse =
3310 | authOrganizationRolePermissionsResponseSuccess
3311 | authOrganizationRolePermissionsResponseError;
3312
3313export const getAuthOrganizationRolePermissionsUrl = (
3314 organizationId: string,
3315 roleId: string,
3316) => {
3317 return `/api/v1/organizations/${organizationId}/roles/${roleId}/permissions`;
3318};
3319
3320/**
3321 * 整体替换语义,是角色权限的唯一写入口。需要 `role.permission.update`。
3322 * @summary 设置角色权限
3323 */
3324export const authOrganizationRolePermissions = async (
3325 organizationId: string,
3326 roleId: string,
3327 setRolePermissionsBody: SetRolePermissionsBody,
3328 options?: Parameters<typeof authClient>[1],
3329): Promise<authOrganizationRolePermissionsResponse> => {
3330 const getHeaders = (
3331 h?: NonNullable<RequestInit["headers"]>,
3332 ): Record<string, string | readonly string[]> => {
3333 if (!h) return {};
3334 if (h instanceof Headers) return Object.fromEntries(h.entries());
3335 if (Symbol.iterator in h) {
3336 return Object.fromEntries(
3337 Array.from(
3338 h as Iterable<Iterable<string>>,
3339 (entry) => Array.from(entry) as [string, string],
3340 ),
3341 );
3342 }
3343 const headers: Record<string, string | readonly string[]> = {};
3344 for (const [name, value] of Object.entries<
3345 string | readonly string[] | undefined
3346 >(h)) {
3347 if (value !== undefined) headers[name] = value;
3348 }
3349 return headers;
3350 };
3351 return authClient<authOrganizationRolePermissionsResponse>(
3352 getAuthOrganizationRolePermissionsUrl(organizationId, roleId),
3353 {
3354 ...options,
3355 method: "PUT",
3356 headers: {
3357 "Content-Type": "application/json",
3358 ...getHeaders(options?.headers),
3359 },
3360 body: JSON.stringify(setRolePermissionsBody),
3361 },
3362 );
3363};
3364
3365export type authOrganizationTransferResponse200 = {
3366 data: EnvelopeOrganizationSummary;
3367 status: 200;
3368};
3369
3370export type authOrganizationTransferResponse403 = {
3371 data: EnvelopeNoData;
3372 status: 403;
3373};
3374
3375export type authOrganizationTransferResponse404 = {
3376 data: EnvelopeNoData;
3377 status: 404;
3378};
3379
3380export type authOrganizationTransferResponseSuccess =
3381 authOrganizationTransferResponse200 & {
3382 headers: Headers;
3383 };
3384export type authOrganizationTransferResponseError = (
3385 authOrganizationTransferResponse403 | authOrganizationTransferResponse404
3386) & {
3387 headers: Headers;
3388};
3389
3390export type authOrganizationTransferResponse =
3391 | authOrganizationTransferResponseSuccess
3392 | authOrganizationTransferResponseError;
3393
3394export const getAuthOrganizationTransferUrl = (organizationId: string) => {
3395 return `/api/v1/organizations/${organizationId}/transfer`;
3396};
3397
3398/**
3399 * 目标必须是活跃成员。原 Owner 降为 ADMIN。
3400 * @summary 转移 OWNER
3401 */
3402export const authOrganizationTransfer = async (
3403 organizationId: string,
3404 transferBody: TransferBody,
3405 options?: Parameters<typeof authClient>[1],
3406): Promise<authOrganizationTransferResponse> => {
3407 const getHeaders = (
3408 h?: NonNullable<RequestInit["headers"]>,
3409 ): Record<string, string | readonly string[]> => {
3410 if (!h) return {};
3411 if (h instanceof Headers) return Object.fromEntries(h.entries());
3412 if (Symbol.iterator in h) {
3413 return Object.fromEntries(
3414 Array.from(
3415 h as Iterable<Iterable<string>>,
3416 (entry) => Array.from(entry) as [string, string],
3417 ),
3418 );
3419 }
3420 const headers: Record<string, string | readonly string[]> = {};
3421 for (const [name, value] of Object.entries<
3422 string | readonly string[] | undefined
3423 >(h)) {
3424 if (value !== undefined) headers[name] = value;
3425 }
3426 return headers;
3427 };
3428 return authClient<authOrganizationTransferResponse>(
3429 getAuthOrganizationTransferUrl(organizationId),
3430 {
3431 ...options,
3432 method: "POST",
3433 headers: {
3434 "Content-Type": "application/json",
3435 ...getHeaders(options?.headers),
3436 },
3437 body: JSON.stringify(transferBody),
3438 },
3439 );
34401037};
Modified Cargo.lock View Cargo.lock
@@ -810,6 +810,7 @@
810810 "session",
811811 "sha1 0.10.7",
812812 "sha2 0.10.9",
813 "space",
813814 "sqlparser",
814815 "thiserror 2.0.21",
815816 "time 0.3.55",
@@ -4906,8 +4907,6 @@
49064907 "portable-atomic",
49074908 "rand 0.9.5",
49084909 "thiserror 2.0.21",
4909 "tokio",
4910 "tokio-stream",
49114910]
49124911
49134912[[package]]
@@ -7181,8 +7180,13 @@
71817180dependencies = [
71827181 "actix-web",
71837182 "async-trait",
7183 "base64 0.23.1",
7184 "cache",
7185 "chrono",
71847186 "config 1.0.0",
7187 "db",
71857188 "deletion",
7189 "envelope",
71867190 "events",
71877191 "export",
71887192 "i18n",
@@ -7190,10 +7194,12 @@
71907194 "models",
71917195 "prost-types 0.14.4",
71927196 "proto",
7197 "rand 0.8.8",
71937198 "sea-orm",
71947199 "serde",
71957200 "serde_json",
71967201 "session",
7202 "sha2 0.11.1",
71977203 "storage",
71987204 "time 0.3.55",
71997205 "tracing",
Modified openapi.json View openapi.json

Diff too large to display.