openauth-plugins 0.0.4

Official OpenAuth plugin modules.
Documentation
use std::sync::Arc;

use http::StatusCode;
use openauth_core::api::additional_fields;
use openauth_core::api::{parse_request_body, ApiRequest};
use openauth_core::context::AuthContext;
use openauth_core::cookies::{set_session_cookie, CookieOptions, SessionCookieOptions};
use openauth_core::db::DbAdapter;
use openauth_core::options::EmailVerificationCallbackPayload;
use openauth_core::user::{CreateUserInput, DbUserStore};
use openauth_core::verification::DbVerificationStore;
use serde::{Deserialize, Serialize};
use serde_json::Value;

use super::helpers::{
    create_session, parse_type, resolve_otp, send_email, validated_email, verify_otp,
};
use super::otp;
use super::response;
use super::types::{EmailOtpOptions, EmailOtpType};

pub(super) const SEND_PATH: &str = "/email-otp/send-verification-otp";
pub(super) const CREATE_PATH: &str = "/email-otp/create-verification-otp";
pub(super) const GET_PATH: &str = "/email-otp/get-verification-otp";
pub(super) const CHECK_PATH: &str = "/email-otp/check-verification-otp";
pub(super) const VERIFY_EMAIL_PATH: &str = "/email-otp/verify-email";
pub(super) const SIGN_IN_PATH: &str = "/sign-in/email-otp";
pub(super) const RESET_PASSWORD_PATH: &str = "/email-otp/reset-password";
pub(super) const REQUEST_CHANGE_EMAIL_PATH: &str = "/email-otp/request-email-change";
pub(super) const CHANGE_EMAIL_PATH: &str = "/email-otp/change-email";

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct SendOtpBody {
    email: String,
    #[serde(rename = "type")]
    otp_type: String,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CheckOtpBody {
    email: String,
    #[serde(rename = "type")]
    otp_type: String,
    otp: String,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct VerifyEmailBody {
    email: String,
    otp: String,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct SignInBody {
    email: String,
    otp: String,
    name: Option<String>,
    image: Option<String>,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct TokenUserResponse {
    token: String,
    user: Value,
}

pub(super) fn send_otp<'a>(
    context: &'a AuthContext,
    request: ApiRequest,
    adapter: Arc<dyn DbAdapter>,
    options: Arc<EmailOtpOptions>,
) -> openauth_core::api::EndpointFuture<'a> {
    Box::pin(async move {
        let body: SendOtpBody = parse_request_body(&request)?;
        let email = match validated_email(&body.email)? {
            Ok(email) => email,
            Err(response) => return Ok(response),
        };
        let otp_type = match parse_type(&body.otp_type)? {
            Ok(otp_type) => otp_type,
            Err(response) => return Ok(response),
        };
        if otp_type == EmailOtpType::ChangeEmail {
            return response::error(
                StatusCode::BAD_REQUEST,
                "INVALID_OTP_TYPE",
                "Invalid OTP type",
            );
        }
        let user_exists = DbUserStore::new(adapter.as_ref())
            .find_user_by_email(&email)
            .await?
            .is_some();
        let should_send = otp_type == EmailOtpType::SignIn && !options.disable_sign_up;
        let identifier = otp::identifier(otp_type, &email);
        let otp = resolve_otp(
            adapter.as_ref(),
            &options,
            &context.secret_config,
            &email,
            otp_type,
            &identifier,
        )
        .await?;

        if !user_exists && !should_send {
            DbVerificationStore::new(adapter.as_ref())
                .delete_verification(&identifier)
                .await?;
            return response::success();
        }
        if let Some(response) = send_email(&options, &email, otp, otp_type, Some(&request))? {
            return Ok(response);
        }
        response::success()
    })
}

pub(super) fn check_otp<'a>(
    context: &'a AuthContext,
    request: ApiRequest,
    adapter: Arc<dyn DbAdapter>,
    options: Arc<EmailOtpOptions>,
) -> openauth_core::api::EndpointFuture<'a> {
    Box::pin(async move {
        let body: CheckOtpBody = parse_request_body(&request)?;
        let email = match validated_email(&body.email)? {
            Ok(email) => email,
            Err(response) => return Ok(response),
        };
        let otp_type = match parse_type(&body.otp_type)? {
            Ok(otp_type) => otp_type,
            Err(response) => return Ok(response),
        };
        if otp_type == EmailOtpType::ChangeEmail {
            return response::error(
                StatusCode::BAD_REQUEST,
                "INVALID_OTP_TYPE",
                "Invalid OTP type",
            );
        }
        if DbUserStore::new(adapter.as_ref())
            .find_user_by_email(&email)
            .await?
            .is_none()
        {
            return response::error(StatusCode::BAD_REQUEST, "USER_NOT_FOUND", "User not found");
        }
        if let Some(response) = verify_otp(
            adapter.as_ref(),
            &options,
            &context.secret_config,
            &otp::identifier(otp_type, &email),
            &body.otp,
            false,
        )
        .await?
        {
            return Ok(response);
        }
        response::success()
    })
}

pub(super) fn verify_email<'a>(
    context: &'a AuthContext,
    request: ApiRequest,
    adapter: Arc<dyn DbAdapter>,
    options: Arc<EmailOtpOptions>,
) -> openauth_core::api::EndpointFuture<'a> {
    Box::pin(async move {
        let body: VerifyEmailBody = parse_request_body(&request)?;
        let email = match validated_email(&body.email)? {
            Ok(email) => email,
            Err(response) => return Ok(response),
        };
        if let Some(response) = verify_otp(
            adapter.as_ref(),
            &options,
            &context.secret_config,
            &otp::identifier(EmailOtpType::EmailVerification, &email),
            &body.otp,
            true,
        )
        .await?
        {
            return Ok(response);
        }
        let users = DbUserStore::new(adapter.as_ref());
        let Some(user) = users.find_user_by_email(&email).await? else {
            return response::error(StatusCode::BAD_REQUEST, "USER_NOT_FOUND", "User not found");
        };
        if let Some(callback) = &context.options.email_verification.before_email_verification {
            callback.before_email_verification(
                EmailVerificationCallbackPayload { user: user.clone() },
                Some(&request),
            )?;
        }
        let user = users
            .update_user_email_verified(&user.id, true)
            .await?
            .unwrap_or(user);
        if let Some(callback) = &context.options.email_verification.after_email_verification {
            callback.after_email_verification(
                EmailVerificationCallbackPayload { user: user.clone() },
                Some(&request),
            )?;
        }
        let response_user = additional_fields::user_response_value(
            adapter.as_ref(),
            &context.options.user.additional_fields,
            &user,
        )
        .await?;
        if context
            .options
            .email_verification
            .auto_sign_in_after_verification
        {
            let session = create_session(adapter.as_ref(), context, &user.id, &request).await?;
            let cookies = set_session_cookie(
                &context.auth_cookies,
                &context.secret,
                &session.token,
                SessionCookieOptions {
                    dont_remember: false,
                    overrides: CookieOptions::default(),
                },
            )?;
            return response::json(
                StatusCode::OK,
                &serde_json::json!({ "status": true, "token": session.token, "user": response_user }),
                cookies,
            );
        }
        response::json(
            StatusCode::OK,
            &serde_json::json!({ "status": true, "token": null, "user": response_user }),
            Vec::new(),
        )
    })
}

pub(super) fn sign_in<'a>(
    context: &'a AuthContext,
    request: ApiRequest,
    adapter: Arc<dyn DbAdapter>,
    options: Arc<EmailOtpOptions>,
) -> openauth_core::api::EndpointFuture<'a> {
    Box::pin(async move {
        let raw_body: Value = parse_request_body(&request)?;
        let body_object = match raw_body.as_object() {
            Some(object) => object,
            None => {
                return response::error(
                    StatusCode::BAD_REQUEST,
                    "INVALID_REQUEST_BODY",
                    "request body must be an object",
                );
            }
        };
        let body: SignInBody = serde_json::from_value(raw_body.clone())
            .map_err(|error| openauth_core::error::OpenAuthError::Api(error.to_string()))?;
        let email = match validated_email(&body.email)? {
            Ok(email) => email,
            Err(response) => return Ok(response),
        };
        if let Some(response) = verify_otp(
            adapter.as_ref(),
            &options,
            &context.secret_config,
            &otp::identifier(EmailOtpType::SignIn, &email),
            &body.otp,
            true,
        )
        .await?
        {
            return Ok(response);
        }
        let users = DbUserStore::new(adapter.as_ref());
        let user = if let Some(user) = users.find_user_by_email(&email).await? {
            if !user.email_verified {
                users
                    .update_user_email_verified(&user.id, true)
                    .await?
                    .unwrap_or(user)
            } else {
                user
            }
        } else {
            if options.disable_sign_up {
                return response::error(StatusCode::BAD_REQUEST, "INVALID_OTP", "Invalid OTP");
            }
            let mut input =
                CreateUserInput::new(body.name.unwrap_or_default(), &email).email_verified(true);
            if let Some(image) = body.image {
                input = input.image(image);
            }
            match additional_fields::create_values(
                &context.options.user.additional_fields,
                body_object,
            ) {
                Ok(fields) => {
                    input = input.additional_fields(fields);
                }
                Err(message) => {
                    return response::error(
                        StatusCode::BAD_REQUEST,
                        "INVALID_REQUEST_BODY",
                        message.message(),
                    );
                }
            }
            users.create_user(input).await?
        };
        let session = create_session(adapter.as_ref(), context, &user.id, &request).await?;
        let cookies = set_session_cookie(
            &context.auth_cookies,
            &context.secret,
            &session.token,
            SessionCookieOptions::default(),
        )?;
        response::json(
            StatusCode::OK,
            &TokenUserResponse {
                token: session.token,
                user: additional_fields::user_response_value(
                    adapter.as_ref(),
                    &context.options.user.additional_fields,
                    &user,
                )
                .await?,
            },
            cookies,
        )
    })
}