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