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