Skip to main content

alibi_plugins/organization/
endpoint.rs

1use super::{OrganizationPlugin, handlers, hooks, types};
2use crate::authentication_helpers::JsonField;
3use crate::endpoint::{definition, error_response, validate_fields, validation};
4use alibi_core::endpoint::{
5    EndpointCall, EndpointDefinition, EndpointInput, EndpointResponse, ServerEndpoint,
6};
7use alibi_core::session::SessionRequest;
8use alibi_core::utils::json::JsValue;
9use alibi_core::{AuthContext, AuthError, AuthResult, AuthSchema, HttpMethod};
10
11pub(super) fn definitions() -> Vec<EndpointDefinition> {
12    let mut endpoints = vec![
13        definition("addMember", "addMember", None, HttpMethod::Post),
14        definition(
15            "removeMember",
16            "removeMember",
17            Some("/organization/remove-member"),
18            HttpMethod::Post,
19        ),
20        definition(
21            "createOrganization",
22            "createOrganization",
23            Some("/organization/create"),
24            HttpMethod::Post,
25        ),
26        definition(
27            "deleteOrganization",
28            "deleteOrganization",
29            Some("/organization/delete"),
30            HttpMethod::Post,
31        ),
32    ];
33    endpoints.extend(super::native::definitions());
34    endpoints
35}
36
37pub(super) fn validate(
38    call: &EndpointCall,
39    organization_fields: &alibi_core::field_policy::SessionFields,
40) -> AuthResult<EndpointInput> {
41    let mut body = call.body().cloned();
42    match call.operation_id() {
43        "createOrganization" => {
44            let _validated =
45                handlers::org_input::create_value(body.as_ref()).map_err(error_response)?;
46            if let Some(JsValue::Object(body)) = &mut body {
47                // Configured additional fields belong to the body schema, as upstream.
48                body.retain(|key, _| {
49                    [
50                        "name",
51                        "slug",
52                        "userId",
53                        "logo",
54                        "metadata",
55                        "keepCurrentActiveOrganization",
56                    ]
57                    .contains(&key.as_str())
58                        || organization_fields.0.contains_key(key)
59                });
60                if let Some(value) = body.get("userId") {
61                    let value = value.coerce_string().map_err(validation)?;
62                    _ = body.insert("userId".into(), JsValue::String(value));
63                }
64            }
65        }
66        "addMember" => {
67            let Some(JsValue::Object(input)) = &mut body else {
68                return Err(validation(format!(
69                    "[body] Invalid input: expected object, received {}",
70                    crate::authentication_helpers::json_type(body.as_ref())
71                )));
72            };
73            let user_id = input
74                .get("userId")
75                .map_or_else(|| Ok("undefined".into()), JsValue::coerce_string)
76                .map_err(validation)?;
77            _ = input.insert("userId".into(), JsValue::String(user_id));
78            _ = validate_fields(
79                body.as_ref(),
80                "body",
81                &[
82                    JsonField::string("userId", true),
83                    JsonField::string("organizationId", false),
84                    JsonField::string("teamId", false),
85                ],
86            )?;
87            let role = body.as_ref().and_then(|body| body.get("role"));
88            if !role.is_some_and(|role| {
89                role.is_string()
90                    || role
91                        .as_array()
92                        .is_some_and(|values| values.iter().all(JsValue::is_string))
93            }) {
94                return Err(validation("[body.role] Invalid input"));
95            }
96            if let Some(JsValue::Object(body)) = &mut body {
97                body.retain(|key, _| {
98                    ["userId", "role", "organizationId", "teamId"].contains(&key.as_str())
99                });
100            }
101        }
102        "removeMember" => {
103            body = Some(validate_fields(
104                body.as_ref(),
105                "body",
106                &[
107                    JsonField::string("memberIdOrEmail", true),
108                    JsonField::string("organizationId", false),
109                ],
110            )?);
111        }
112        "deleteOrganization" => {
113            body = Some(validate_fields(
114                body.as_ref(),
115                "body",
116                &[JsonField::string("organizationId", true)],
117            )?);
118        }
119        _ => return super::native::validate(call),
120    }
121    Ok(EndpointInput {
122        body,
123        query: call.query().cloned(),
124    })
125}
126
127impl OrganizationPlugin {
128    /// Privileged server-only admission through actual installed hooks.
129    /// # Errors
130    /// Returns an error if serialization fails.
131    pub fn add_member_endpoint(
132        body: &types::AddOrganizationMemberRequest,
133    ) -> AuthResult<ServerEndpoint<types::BasicMemberResponse>> {
134        ServerEndpoint::new("organization", "addMember").with_body(body)
135    }
136
137    /// Remove a member using genuine verified credentials and installed hooks.
138    /// # Errors
139    /// Returns an error if serialization fails.
140    pub fn remove_member_endpoint(
141        body: &types::RemoveMemberRequest,
142    ) -> AuthResult<
143        ServerEndpoint<types::RemovedMemberResponse<types::OrganizationMemberRemovalSnapshot>>,
144    > {
145        ServerEndpoint::new("organization", "removeMember").with_body(body)
146    }
147
148    /// Create through logical dispatch; an explicit user ID is a trusted server option.
149    /// # Errors
150    /// Returns an error if serialization fails.
151    pub fn create_endpoint(
152        body: &types::CreateOrganizationRequest,
153        user_id: Option<&str>,
154    ) -> AuthResult<
155        ServerEndpoint<
156            types::CreateOrganizationResponse<
157                types::CreatedOrganizationResponse,
158                types::BasicMemberResponse,
159            >,
160        >,
161    > {
162        let mut value =
163            alibi_core::utils::json::parse_value(&alibi_core::utils::json::to_string(body)?)?;
164        if let Some(user_id) = user_id
165            && let JsValue::Object(body) = &mut value
166        {
167            _ = body.insert("userId".into(), JsValue::String(user_id.into()));
168        }
169        Ok(ServerEndpoint::new("organization", "createOrganization").with_body_value(value))
170    }
171
172    /// Delete through installed hooks and scoped organization authority.
173    /// # Errors
174    /// Returns an error if serialization fails.
175    pub fn delete_endpoint(
176        body: &types::DeleteOrganizationRequest,
177    ) -> AuthResult<ServerEndpoint<Option<types::OrganizationResponse>>> {
178        ServerEndpoint::new("organization", "deleteOrganization").with_body(body)
179    }
180
181    pub(super) async fn call_endpoint<S: AuthSchema>(
182        &self,
183        call: &EndpointCall,
184        ctx: &AuthContext<S>,
185    ) -> AuthResult<EndpointResponse> {
186        match call.operation_id() {
187            "addMember" => {
188                let body: types::AddOrganizationMemberRequest = call.body_as()?;
189                let session = if body.user_id.is_empty() {
190                    None
191                } else {
192                    ctx.require_cached_session(call).await.ok()
193                };
194                EndpointResponse::json(
195                    &handlers::member_addition::add_member_with_session(
196                        &body,
197                        session,
198                        call.request(),
199                        &self.config,
200                        ctx,
201                    )
202                    .await?,
203                )
204            }
205            "removeMember" => {
206                let body: types::RemoveMemberRequest = call.body_as()?;
207                let (user, session) = ctx.require_cached_session(call).await.map_err(|error| {
208                    if matches!(error, AuthError::Unauthenticated) {
209                        handlers::extension_common::org_error(401, "UNAUTHORIZED")
210                    } else {
211                        error
212                    }
213                })?;
214                EndpointResponse::json(
215                    &handlers::member::remove_member_core(
216                        &body,
217                        &user,
218                        &session,
219                        &self.config,
220                        ctx,
221                    )
222                    .await?,
223                )
224            }
225            "deleteOrganization" => {
226                let body: types::DeleteOrganizationRequest = call.body_as()?;
227                if self.config.disable_organization_deletion {
228                    return Err(handlers::extension_common::org_error(
229                        404,
230                        "ORGANIZATION_DELETION_DISABLED",
231                    ));
232                }
233                let (user, session) = ctx.require_cached_session(call).await.map_err(|error| {
234                    if matches!(error, AuthError::Unauthenticated) {
235                        handlers::extension_common::org_error(401, "UNAUTHORIZED")
236                    } else {
237                        error
238                    }
239                })?;
240                EndpointResponse::json(
241                    &handlers::org::delete_organization_core(
242                        &body,
243                        &user,
244                        &session,
245                        hooks::organization::DeleteInvocation {
246                            headers: call.session_headers(),
247                            request: call.request(),
248                        },
249                        &self.config,
250                        ctx,
251                    )
252                    .await?,
253                )
254            }
255            "createOrganization" => {
256                let body: types::CreateOrganizationRequest = call.body_as()?;
257                let session = ctx.require_cached_session(call).await.ok();
258                if session.is_none() && (call.request().is_some() || call.headers().is_some()) {
259                    return Err(AuthError::Api {
260                        status: 401,
261                        code: None,
262                        message: String::new(),
263                    });
264                }
265                if let Some((user, session)) = session {
266                    let response = handlers::org::create_organization_core(
267                        &body,
268                        &user,
269                        call.request(),
270                        Some(&session),
271                        &self.config,
272                        ctx,
273                    )
274                    .await?;
275                    if !body.keep_current_active_organization.unwrap_or(false) {
276                        handlers::org::activate_created_organization(
277                            &response,
278                            &session.token,
279                            ctx,
280                        )
281                        .await?;
282                    }
283                    EndpointResponse::json(&response)
284                } else {
285                    let user_id = call
286                        .body()
287                        .and_then(|body| body.get("userId"))
288                        .and_then(JsValue::as_str)
289                        .filter(|id| !id.is_empty())
290                        .ok_or(AuthError::Api {
291                            status: 401,
292                            code: None,
293                            message: String::new(),
294                        })?;
295                    let user =
296                        ctx.database
297                            .get_user_by_id(user_id)
298                            .await?
299                            .ok_or(AuthError::Api {
300                                status: 401,
301                                code: None,
302                                message: String::new(),
303                            })?;
304                    EndpointResponse::json(
305                        &handlers::org::create_organization_core(
306                            &body,
307                            &user,
308                            None,
309                            None,
310                            &self.config,
311                            ctx,
312                        )
313                        .await?,
314                    )
315                }
316            }
317            // Keep the extended operation state out of every dispatcher caller's future.
318            _ => Box::pin(super::native::execute(self, call, ctx)).await,
319        }
320    }
321}