scufflecloud_core/operations/
organization_invitations.rs

1use diesel::query_dsl::methods::{FilterDsl, FindDsl, SelectDsl};
2use diesel::{ExpressionMethods, OptionalExtension};
3use diesel_async::RunQueryDsl;
4use tonic_types::{ErrorDetails, StatusExt};
5
6use crate::cedar::Action;
7use crate::http_ext::RequestExt;
8use crate::models::{
9    Organization, OrganizationId, OrganizationInvitation, OrganizationInvitationId, OrganizationMember, User, UserId,
10};
11use crate::operations::Operation;
12use crate::schema::{organization_invitations, organization_members, user_emails};
13use crate::std_ext::{OptionExt, ResultExt};
14use crate::{CoreConfig, common};
15
16impl<G: CoreConfig> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::CreateOrganizationInvitationRequest> {
17    type Principal = User;
18    type Resource = OrganizationInvitation;
19    type Response = pb::scufflecloud::core::v1::OrganizationInvitation;
20
21    const ACTION: Action = Action::CreateOrganizationInvitation;
22
23    async fn load_principal(
24        &mut self,
25        _conn: &mut diesel_async::AsyncPgConnection,
26    ) -> Result<Self::Principal, tonic::Status> {
27        let global = &self.global::<G>()?;
28        let session = self.session_or_err()?;
29        common::get_user_by_id(global, session.user_id).await
30    }
31
32    async fn load_resource(&mut self, conn: &mut diesel_async::AsyncPgConnection) -> Result<Self::Resource, tonic::Status> {
33        let session = self.session_or_err()?;
34
35        let organization_id: OrganizationId = self
36            .get_ref()
37            .organization_id
38            .parse()
39            .into_tonic_internal_err("failed to parse id")?;
40
41        let invited_user = user_emails::dsl::user_emails
42            .find(&self.get_ref().email)
43            .select(user_emails::dsl::user_id)
44            .get_result::<UserId>(conn)
45            .await
46            .optional()
47            .into_tonic_internal_err("failed to query user email")?;
48
49        Ok(OrganizationInvitation {
50            id: OrganizationInvitationId::new(),
51            user_id: invited_user,
52            organization_id,
53            email: self.get_ref().email.clone(),
54            invited_by_id: session.user_id,
55            expires_at: self
56                .get_ref()
57                .expires_in_s
58                .map(|s| chrono::Utc::now() + chrono::Duration::seconds(s as i64)),
59        })
60    }
61
62    async fn execute(
63        self,
64        conn: &mut diesel_async::AsyncPgConnection,
65        _principal: Self::Principal,
66        resource: Self::Resource,
67    ) -> Result<Self::Response, tonic::Status> {
68        diesel::insert_into(organization_invitations::dsl::organization_invitations)
69            .values(&resource)
70            .execute(conn)
71            .await
72            .into_tonic_internal_err("failed to insert organization invitation")?;
73
74        Ok(resource.into())
75    }
76}
77
78impl<G: CoreConfig> Operation<G>
79    for tonic::Request<pb::scufflecloud::core::v1::ListOrganizationInvitationsByOrganizationRequest>
80{
81    type Principal = User;
82    type Resource = Organization;
83    type Response = pb::scufflecloud::core::v1::OrganizationInvitationList;
84
85    const ACTION: Action = Action::ListOrganizationInvitationsByOrganization;
86    const TRANSACTION: bool = false;
87
88    async fn load_principal(
89        &mut self,
90        _conn: &mut diesel_async::AsyncPgConnection,
91    ) -> Result<Self::Principal, tonic::Status> {
92        let global = &self.global::<G>()?;
93        let session = self.session_or_err()?;
94        common::get_user_by_id(global, session.user_id).await
95    }
96
97    async fn load_resource(&mut self, conn: &mut diesel_async::AsyncPgConnection) -> Result<Self::Resource, tonic::Status> {
98        let organization_id: OrganizationId = self
99            .get_ref()
100            .id
101            .parse()
102            .into_tonic_err_with_field_violation("id", "invalid ID")?;
103        common::get_organization_by_id(conn, organization_id).await
104    }
105
106    async fn execute(
107        self,
108        conn: &mut diesel_async::AsyncPgConnection,
109        _principal: Self::Principal,
110        resource: Self::Resource,
111    ) -> Result<Self::Response, tonic::Status> {
112        let invitations = organization_invitations::dsl::organization_invitations
113            .filter(organization_invitations::dsl::organization_id.eq(resource.id))
114            .load::<OrganizationInvitation>(conn)
115            .await
116            .into_tonic_internal_err("failed to query organization invitations")?;
117
118        Ok(pb::scufflecloud::core::v1::OrganizationInvitationList {
119            invitations: invitations.into_iter().map(Into::into).collect(),
120        })
121    }
122}
123
124impl<G: CoreConfig> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::ListOrgnizationInvitesByUserRequest> {
125    type Principal = User;
126    type Resource = User;
127    type Response = pb::scufflecloud::core::v1::OrganizationInvitationList;
128
129    const ACTION: Action = Action::ListOrganizationInvitationsByUser;
130    const TRANSACTION: bool = false;
131
132    async fn load_principal(
133        &mut self,
134        _conn: &mut diesel_async::AsyncPgConnection,
135    ) -> Result<Self::Principal, tonic::Status> {
136        let global = &self.global::<G>()?;
137        let session = self.session_or_err()?;
138        common::get_user_by_id(global, session.user_id).await
139    }
140
141    async fn load_resource(&mut self, _conn: &mut diesel_async::AsyncPgConnection) -> Result<Self::Resource, tonic::Status> {
142        let global = &self.global::<G>()?;
143        let user_id: UserId = self
144            .get_ref()
145            .id
146            .parse()
147            .into_tonic_err_with_field_violation("id", "invalid ID")?;
148        common::get_user_by_id(global, user_id).await
149    }
150
151    async fn execute(
152        self,
153        conn: &mut diesel_async::AsyncPgConnection,
154        _principal: Self::Principal,
155        resource: Self::Resource,
156    ) -> Result<Self::Response, tonic::Status> {
157        let invitations = organization_invitations::dsl::organization_invitations
158            .filter(organization_invitations::dsl::user_id.eq(resource.id))
159            .load::<OrganizationInvitation>(conn)
160            .await
161            .into_tonic_internal_err("failed to query organization invitations")?;
162
163        Ok(pb::scufflecloud::core::v1::OrganizationInvitationList {
164            invitations: invitations.into_iter().map(Into::into).collect(),
165        })
166    }
167}
168
169impl<G: CoreConfig> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::GetOrganizationInvitationRequest> {
170    type Principal = User;
171    type Resource = OrganizationInvitation;
172    type Response = pb::scufflecloud::core::v1::OrganizationInvitation;
173
174    const ACTION: Action = Action::GetOrganizationInvitation;
175    const TRANSACTION: bool = false;
176
177    async fn load_principal(
178        &mut self,
179        _conn: &mut diesel_async::AsyncPgConnection,
180    ) -> Result<Self::Principal, tonic::Status> {
181        let global = &self.global::<G>()?;
182        let session = self.session_or_err()?;
183        common::get_user_by_id(global, session.user_id).await
184    }
185
186    async fn load_resource(&mut self, conn: &mut diesel_async::AsyncPgConnection) -> Result<Self::Resource, tonic::Status> {
187        let id: OrganizationInvitationId = self
188            .get_ref()
189            .id
190            .parse()
191            .into_tonic_err_with_field_violation("id", "invalid ID")?;
192        organization_invitations::dsl::organization_invitations
193            .find(id)
194            .first::<OrganizationInvitation>(conn)
195            .await
196            .optional()
197            .into_tonic_internal_err("failed to query organization invitation")?
198            .into_tonic_not_found("organization invitation not found")
199    }
200
201    async fn execute(
202        self,
203        _conn: &mut diesel_async::AsyncPgConnection,
204        _principal: Self::Principal,
205        resource: Self::Resource,
206    ) -> Result<Self::Response, tonic::Status> {
207        Ok(resource.into())
208    }
209}
210
211impl<G: CoreConfig> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::AcceptOrganizationInvitationRequest> {
212    type Principal = User;
213    type Resource = OrganizationInvitation;
214    type Response = pb::scufflecloud::core::v1::OrganizationMember;
215
216    const ACTION: Action = Action::AcceptOrganizationInvitation;
217
218    async fn load_principal(
219        &mut self,
220        _conn: &mut diesel_async::AsyncPgConnection,
221    ) -> Result<Self::Principal, tonic::Status> {
222        let global = &self.global::<G>()?;
223        let session = self.session_or_err()?;
224        common::get_user_by_id(global, session.user_id).await
225    }
226
227    async fn load_resource(&mut self, conn: &mut diesel_async::AsyncPgConnection) -> Result<Self::Resource, tonic::Status> {
228        let id: OrganizationInvitationId = self
229            .get_ref()
230            .id
231            .parse()
232            .into_tonic_err_with_field_violation("id", "invalid ID")?;
233        organization_invitations::dsl::organization_invitations
234            .find(id)
235            .first::<OrganizationInvitation>(conn)
236            .await
237            .optional()
238            .into_tonic_internal_err("failed to query organization invitation")?
239            .into_tonic_not_found("organization invitation not found")
240    }
241
242    async fn execute(
243        self,
244        conn: &mut diesel_async::AsyncPgConnection,
245        _principal: Self::Principal,
246        resource: Self::Resource,
247    ) -> Result<Self::Response, tonic::Status> {
248        let Some(user_id) = resource.user_id else {
249            return Err(tonic::Status::with_error_details(
250                tonic::Code::FailedPrecondition,
251                "register first to accept this organization invitation",
252                ErrorDetails::new(),
253            ));
254        };
255
256        let organization_member = OrganizationMember {
257            organization_id: resource.organization_id,
258            user_id,
259            invited_by_id: Some(resource.invited_by_id),
260            inline_policy: None,
261            created_at: chrono::Utc::now(),
262        };
263
264        diesel::insert_into(organization_members::dsl::organization_members)
265            .values(&organization_member)
266            .execute(conn)
267            .await
268            .into_tonic_internal_err("failed to insert organization member")?;
269
270        Ok(organization_member.into())
271    }
272}
273
274impl<G: CoreConfig> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::DeclineOrganizationInvitationRequest> {
275    type Principal = User;
276    type Resource = OrganizationInvitation;
277    type Response = ();
278
279    const ACTION: Action = Action::DeclineOrganizationInvitation;
280
281    async fn load_principal(
282        &mut self,
283        _conn: &mut diesel_async::AsyncPgConnection,
284    ) -> Result<Self::Principal, tonic::Status> {
285        let global = &self.global::<G>()?;
286        let session = self.session_or_err()?;
287        common::get_user_by_id(global, session.user_id).await
288    }
289
290    async fn load_resource(&mut self, conn: &mut diesel_async::AsyncPgConnection) -> Result<Self::Resource, tonic::Status> {
291        let id: OrganizationInvitationId = self
292            .get_ref()
293            .id
294            .parse()
295            .into_tonic_err_with_field_violation("id", "invalid ID")?;
296        organization_invitations::dsl::organization_invitations
297            .find(id)
298            .first::<OrganizationInvitation>(conn)
299            .await
300            .optional()
301            .into_tonic_internal_err("failed to query organization invitation")?
302            .into_tonic_not_found("organization invitation not found")
303    }
304
305    async fn execute(
306        self,
307        conn: &mut diesel_async::AsyncPgConnection,
308        _principal: Self::Principal,
309        resource: Self::Resource,
310    ) -> Result<Self::Response, tonic::Status> {
311        diesel::delete(organization_invitations::dsl::organization_invitations)
312            .filter(organization_invitations::dsl::id.eq(resource.id))
313            .execute(conn)
314            .await
315            .into_tonic_internal_err("failed to delete organization invitation")?;
316
317        Ok(())
318    }
319}