scufflecloud_core/operations/
organization_invitations.rs1use 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}