rustauth-plugins 0.2.0

Official RustAuth plugin modules.
Documentation
use ::http::{Method, StatusCode};
use rustauth_core::api::{create_auth_endpoint, AsyncAuthEndpoint, AuthEndpointOptions};
use rustauth_core::db::{DbValue, SortDirection, WhereOperator};

use crate::organization::additional_fields;
use crate::organization::http;
use crate::organization::models::Member;
use crate::organization::options::OrganizationOptions;
use crate::organization::store::{MemberListQuery, OrganizationStore};

use super::validation::{query_param, require_session};

pub(super) fn endpoints(options: OrganizationOptions) -> Vec<AsyncAuthEndpoint> {
    vec![
        get_active_member(options.clone()),
        list_members(options),
        get_active_member_role(),
    ]
}

fn get_active_member(options: OrganizationOptions) -> AsyncAuthEndpoint {
    let options = std::sync::Arc::new(options);
    create_auth_endpoint(
        "/organization/get-active-member",
        Method::GET,
        AuthEndpointOptions::new().operation_id("organizationGetActiveMember"),
        move |context, request| {
            let options = std::sync::Arc::clone(&options);
            async move {
                let adapter = context.require_adapter()?;
                let store = OrganizationStore::new(adapter.as_ref());
                let session = require_session(&context, &request, &store).await?;
                let Some(organization_id) = session.active_organization_id else {
                    return http::organization_error(
                        StatusCode::BAD_REQUEST,
                        "NO_ACTIVE_ORGANIZATION",
                    );
                };
                match store
                    .member_by_org_user(&organization_id, &session.user.id)
                    .await?
                {
                    Some(mut member) => {
                        retain_returned_member_fields(&mut member, &options);
                        http::json(StatusCode::OK, &member)
                    }
                    None => http::organization_error(StatusCode::BAD_REQUEST, "MEMBER_NOT_FOUND"),
                }
            }
        },
    )
}

fn list_members(options: OrganizationOptions) -> AsyncAuthEndpoint {
    let options = std::sync::Arc::new(options);
    create_auth_endpoint(
        "/organization/list-members",
        Method::GET,
        AuthEndpointOptions::new().operation_id("organizationListMembers"),
        move |context, request| {
            let options = std::sync::Arc::clone(&options);
            async move {
                let adapter = context.require_adapter()?;
                let store = OrganizationStore::new(adapter.as_ref());
                let session = require_session(&context, &request, &store).await?;
                let Some(organization_id) = resolve_organization_id(
                    &store,
                    &request,
                    session.active_organization_id.as_deref(),
                )
                .await?
                else {
                    return http::organization_error(
                        StatusCode::BAD_REQUEST,
                        "NO_ACTIVE_ORGANIZATION",
                    );
                };
                if store
                    .member_by_org_user(&organization_id, &session.user.id)
                    .await?
                    .is_none()
                {
                    return http::organization_error(
                        StatusCode::FORBIDDEN,
                        "YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION",
                    );
                }
                let member_query = member_list_query(&request, organization_id.clone());
                let total = store
                    .count_members_matching(&organization_id, member_query.filters.clone())
                    .await?;
                let mut members = store.list_members(member_query).await?;
                for member in &mut members {
                    retain_returned_member_fields(member, &options);
                }
                http::json(
                    StatusCode::OK,
                    &serde_json::json!({ "members": members, "total": total }),
                )
            }
        },
    )
}

fn get_active_member_role() -> AsyncAuthEndpoint {
    create_auth_endpoint(
        "/organization/get-active-member-role",
        Method::GET,
        AuthEndpointOptions::new().operation_id("organizationGetActiveMemberRole"),
        |context, request| async move {
            let adapter = context.require_adapter()?;
            let store = OrganizationStore::new(adapter.as_ref());
            let session = require_session(&context, &request, &store).await?;
            let Some(organization_id) = resolve_organization_id(
                &store,
                &request,
                session.active_organization_id.as_deref(),
            )
            .await?
            else {
                return http::organization_error(StatusCode::BAD_REQUEST, "NO_ACTIVE_ORGANIZATION");
            };
            match store
                .member_by_org_user(&organization_id, &session.user.id)
                .await?
            {
                Some(_) => {
                    let target_user_id =
                        query_param(&request, "userId").unwrap_or_else(|| session.user.id.clone());
                    let Some(member) = store
                        .member_by_org_user(&organization_id, &target_user_id)
                        .await?
                    else {
                        return http::organization_error(
                            StatusCode::FORBIDDEN,
                            "YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION",
                        );
                    };
                    http::json(StatusCode::OK, &serde_json::json!({ "role": member.role }))
                }
                None => http::organization_error(
                    StatusCode::FORBIDDEN,
                    "YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION",
                ),
            }
        },
    )
}

fn retain_returned_member_fields(member: &mut Member, options: &OrganizationOptions) {
    additional_fields::retain_returned(
        &mut member.additional_fields,
        &options.schema.member.additional_fields,
    );
}

async fn resolve_organization_id(
    store: &OrganizationStore<'_>,
    request: &rustauth_core::api::ApiRequest,
    active_organization_id: Option<&str>,
) -> Result<Option<String>, rustauth_core::error::RustAuthError> {
    if let Some(slug) = query_param(request, "organizationSlug") {
        return match store.organization_by_slug(&slug).await? {
            Some(organization) => Ok(Some(organization.id)),
            None => Err(rustauth_core::error::RustAuthError::Api(
                "ORGANIZATION_NOT_FOUND".to_owned(),
            )),
        };
    }
    if let Some(id) = query_param(request, "organizationId") {
        return Ok(Some(id));
    }
    Ok(active_organization_id.map(str::to_owned))
}

fn member_list_query(
    request: &rustauth_core::api::ApiRequest,
    organization_id: String,
) -> MemberListQuery {
    let mut query = MemberListQuery::new(organization_id);
    if let Some(field) = query_param(request, "filterField") {
        if let Some(value) = query_param(request, "filterValue") {
            let operator = filter_operator(
                query_param(request, "filterOperator")
                    .as_deref()
                    .unwrap_or("eq"),
            );
            query = query.filter(
                member_field_name(&field),
                filter_value(&value, operator),
                operator,
            );
        }
    }
    if let Some(limit) = query_param(request, "limit").and_then(|value| value.parse().ok()) {
        query = query.limit(limit);
    }
    if let Some(offset) = query_param(request, "offset").and_then(|value| value.parse().ok()) {
        query = query.offset(offset);
    }
    if let Some(sort_by) = query_param(request, "sortBy") {
        let direction = match query_param(request, "sortDirection").as_deref() {
            Some("desc") => SortDirection::Desc,
            _ => SortDirection::Asc,
        };
        query = query.sort(member_field_name(&sort_by), direction);
    }
    query
}

fn filter_operator(value: &str) -> WhereOperator {
    match value {
        "ne" => WhereOperator::Ne,
        "lt" => WhereOperator::Lt,
        "lte" => WhereOperator::Lte,
        "gt" => WhereOperator::Gt,
        "gte" => WhereOperator::Gte,
        "in" => WhereOperator::In,
        "not_in" | "notIn" => WhereOperator::NotIn,
        "contains" => WhereOperator::Contains,
        "starts_with" | "startsWith" => WhereOperator::StartsWith,
        "ends_with" | "endsWith" => WhereOperator::EndsWith,
        _ => WhereOperator::Eq,
    }
}

fn filter_value(value: &str, operator: WhereOperator) -> DbValue {
    match operator {
        WhereOperator::In | WhereOperator::NotIn => {
            DbValue::StringArray(value.split(',').map(str::to_owned).collect())
        }
        _ => value
            .parse::<i64>()
            .map(DbValue::Number)
            .unwrap_or_else(|_| DbValue::String(value.to_owned())),
    }
}

fn member_field_name(field: &str) -> String {
    match field {
        "organizationId" => "organization_id".to_owned(),
        "userId" => "user_id".to_owned(),
        "createdAt" => "created_at".to_owned(),
        value => value.to_owned(),
    }
}