openauth-core 0.0.6

Core types and primitives for OpenAuth.
Documentation
use std::sync::Arc;

use http::{Method, StatusCode};
use serde::{Deserialize, Serialize};
use serde_json::Value;

use super::shared::{
    additional_session_create_values, auth_flow_error_response, auth_session_cookies,
    error_response, invalid_additional_field_response, json_response, message_openapi_response,
    record_new_session, sign_up_email_openapi_response, user_response_value,
};
use crate::api::additional_fields::{create_values, AdditionalFieldError};
use crate::api::services::email_password as email_password_service;
use crate::api::services::email_password::{
    EmailAuthResult, EmailPasswordServiceError, SignUpEmailInput,
};
use crate::api::{
    create_auth_endpoint, parse_request_body, AsyncAuthEndpoint, AuthEndpointOptions, BodyField,
    BodySchema, JsonSchemaType, OpenApiOperation,
};
use crate::db::DbAdapter;
use crate::error::OpenAuthError;

#[derive(Debug, Deserialize)]
struct SignUpEmailBody {
    name: String,
    email: String,
    password: String,
    #[serde(default)]
    image: Option<String>,
    #[serde(default)]
    username: Option<String>,
    #[serde(default, alias = "displayUsername")]
    display_username: Option<String>,
    #[serde(default, alias = "rememberMe")]
    remember_me: Option<bool>,
    #[serde(default, alias = "callbackURL")]
    callback_url: Option<String>,
}

#[derive(Debug, Serialize)]
struct AuthTokenUserBody {
    token: Option<String>,
    user: Value,
}

pub(super) fn sign_up_email_endpoint(adapter: Arc<dyn DbAdapter>) -> AsyncAuthEndpoint {
    create_auth_endpoint(
        "/sign-up/email",
        Method::POST,
        AuthEndpointOptions::new()
            .operation_id("signUpWithEmailAndPassword")
            .allowed_media_types(["application/x-www-form-urlencoded", "application/json"])
            .body_schema(sign_up_email_body_schema())
            .openapi(
                OpenApiOperation::new("signUpWithEmailAndPassword")
                    .description("Sign up a user using email and password")
                    .response("200", sign_up_email_openapi_response())
                    .response(
                        "422",
                        message_openapi_response(
                            "Unprocessable Entity. User already exists or failed to create user.",
                        ),
                    ),
            ),
        move |context, request| {
            let adapter = Arc::clone(&adapter);
            Box::pin(async move {
                if !context.options.email_password.enabled
                    || context.options.email_password.disable_sign_up
                {
                    return error_response(
                        StatusCode::BAD_REQUEST,
                        "EMAIL_PASSWORD_SIGN_UP_DISABLED",
                        "Email and password sign up is not enabled",
                    );
                }

                let raw_body: Value = parse_request_body(&request)?;
                let Some(body_object) = raw_body.as_object() else {
                    return invalid_additional_field_response(AdditionalFieldError::InvalidType(
                        "request body must be an object".to_owned(),
                    ));
                };
                let body: SignUpEmailBody =
                    serde_json::from_value(raw_body.clone()).map_err(|error| {
                        OpenAuthError::InvalidRequestBody {
                            encoding: "JSON",
                            message: error.to_string(),
                        }
                    })?;
                let additional_user_fields =
                    match create_values(&context.options.user.additional_fields, body_object) {
                        Ok(fields) => fields,
                        Err(error) => return invalid_additional_field_response(error),
                    };
                let additional_session_fields = additional_session_create_values(context);
                let result = match email_password_service::sign_up_email(
                    adapter.as_ref(),
                    context,
                    &request,
                    SignUpEmailInput {
                        name: body.name,
                        email: body.email,
                        password: body.password,
                        image: body.image,
                        username: body.username,
                        display_username: body.display_username,
                        remember_me: body.remember_me.unwrap_or(true),
                        callback_url: body.callback_url,
                        additional_user_fields,
                        additional_session_fields,
                    },
                )
                .await
                {
                    Ok(result) => result,
                    Err(error) => return email_password_service_error_response(error),
                };
                email_sign_up_response(adapter.as_ref(), context, result).await
            })
        },
    )
}

async fn email_sign_up_response(
    adapter: &dyn DbAdapter,
    context: &crate::context::AuthContext,
    result: EmailAuthResult,
) -> Result<crate::api::ApiResponse, OpenAuthError> {
    let mut cookies = Vec::new();
    let token = if let Some(session) = result.session {
        record_new_session(&session, &result.user)?;
        cookies = auth_session_cookies(context, &session, &result.user, !result.remember_me)?;
        Some(session.token)
    } else {
        None
    };
    json_response(
        StatusCode::OK,
        &AuthTokenUserBody {
            token,
            user: user_response_value(adapter, context, &result.user).await?,
        },
        cookies,
    )
}

fn email_password_service_error_response(
    error: EmailPasswordServiceError,
) -> Result<crate::api::ApiResponse, OpenAuthError> {
    match error {
        EmailPasswordServiceError::Disabled | EmailPasswordServiceError::SignUpDisabled => {
            error_response(
                StatusCode::BAD_REQUEST,
                "EMAIL_PASSWORD_SIGN_UP_DISABLED",
                "Email and password sign up is not enabled",
            )
        }
        EmailPasswordServiceError::UsernameTaken => error_response(
            StatusCode::BAD_REQUEST,
            "USERNAME_IS_ALREADY_TAKEN",
            "Username is already taken. Please try another.",
        ),
        EmailPasswordServiceError::AuthFlow(error) => auth_flow_error_response(error),
        EmailPasswordServiceError::PasswordValidation(rejection) => {
            super::shared::password_validation_rejection_response(rejection)
        }
        EmailPasswordServiceError::OpenAuth(error) => Err(error),
    }
}

fn sign_up_email_body_schema() -> BodySchema {
    BodySchema::object([
        BodyField::new("name", JsonSchemaType::String).description("The name of the user"),
        BodyField::new("email", JsonSchemaType::String)
            .format("email")
            .description("The email of the user"),
        BodyField::new("password", JsonSchemaType::String).description("The password of the user"),
        BodyField::optional("image", JsonSchemaType::String)
            .description("The profile image URL of the user"),
        BodyField::optional("username", JsonSchemaType::String)
            .description("The username of the user"),
        BodyField::optional("displayUsername", JsonSchemaType::String)
            .description("The display username of the user"),
        BodyField::optional("callbackURL", JsonSchemaType::String)
            .description("The URL to use for email verification callback"),
        BodyField::optional("rememberMe", JsonSchemaType::Boolean)
            .description("If false, the session will not be remembered"),
    ])
}