scufflecloud_core/operations/
organization_invitations.rs

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