openauth-core 0.0.6

Core types and primitives for OpenAuth.
Documentation
use http::{header, StatusCode};
use serde::{Deserialize, Serialize};

use super::super::shared::{error_response, json_response};
use crate::api::{ApiRequest, ApiResponse, BodyField, BodySchema, JsonSchemaType, PathParams};
use crate::db::User;
use crate::error::OpenAuthError;

const PASSWORD_RESET_MESSAGE: &str =
    "If this email exists in our system, check your email for the reset link";

#[derive(Debug, Deserialize)]
pub(super) struct ChangePasswordBody {
    #[serde(alias = "currentPassword")]
    pub(super) current_password: String,
    #[serde(alias = "newPassword")]
    pub(super) new_password: String,
    #[serde(default, alias = "revokeOtherSessions")]
    pub(super) revoke_other_sessions: Option<bool>,
}

#[derive(Debug, Deserialize)]
pub(super) struct SetPasswordBody {
    #[serde(alias = "newPassword")]
    pub(super) new_password: String,
}

#[derive(Debug, Deserialize)]
pub(super) struct VerifyPasswordBody {
    pub(super) password: String,
}

#[derive(Debug, Deserialize)]
pub(super) struct RequestPasswordResetBody {
    pub(super) email: String,
    #[serde(default, alias = "redirectTo")]
    pub(super) redirect_to: Option<String>,
}

#[derive(Debug, Deserialize)]
pub(super) struct ResetPasswordBody {
    #[serde(alias = "newPassword")]
    pub(super) new_password: String,
    #[serde(default)]
    pub(super) token: Option<String>,
}

#[derive(Debug, Serialize)]
pub(super) struct StatusBody {
    pub(super) status: bool,
}

#[derive(Debug, Serialize)]
struct RequestPasswordResetResponse {
    status: bool,
    message: &'static str,
}

#[derive(Debug, Serialize)]
pub(super) struct TokenUserResponse {
    pub(super) token: Option<String>,
    pub(super) user: User,
}

pub(super) fn change_password_body_schema() -> BodySchema {
    BodySchema::object([
        BodyField::new("newPassword", JsonSchemaType::String)
            .description("The new password to set"),
        BodyField::new("currentPassword", JsonSchemaType::String)
            .description("The current password is required"),
        BodyField::optional("revokeOtherSessions", JsonSchemaType::Boolean)
            .description("Must be a boolean value"),
    ])
}

pub(super) fn set_password_body_schema() -> BodySchema {
    BodySchema::object([BodyField::new("newPassword", JsonSchemaType::String)
        .description("The new password to set is required")])
}

pub(super) fn verify_password_body_schema() -> BodySchema {
    BodySchema::object([
        BodyField::new("password", JsonSchemaType::String).description("The password to verify")
    ])
}

pub(super) fn request_password_reset_body_schema() -> BodySchema {
    BodySchema::object([
        BodyField::new("email", JsonSchemaType::String)
            .format("email")
            .description("The email address of the user to send a password reset email to"),
        BodyField::optional("redirectTo", JsonSchemaType::String)
            .description("The URL to redirect the user to reset their password"),
    ])
}

pub(super) fn reset_password_body_schema() -> BodySchema {
    BodySchema::object([
        BodyField::new("newPassword", JsonSchemaType::String)
            .description("The new password to set"),
        BodyField::optional("token", JsonSchemaType::String)
            .description("The token to reset the password"),
    ])
}

pub(super) fn invalid_password() -> Result<ApiResponse, OpenAuthError> {
    error_response(
        StatusCode::BAD_REQUEST,
        "INVALID_PASSWORD",
        "Invalid password",
    )
}

pub(super) fn invalid_token() -> Result<ApiResponse, OpenAuthError> {
    error_response(StatusCode::BAD_REQUEST, "INVALID_TOKEN", "Invalid token")
}

pub(super) fn password_reset_response() -> Result<ApiResponse, OpenAuthError> {
    json_response(
        StatusCode::OK,
        &RequestPasswordResetResponse {
            status: true,
            message: PASSWORD_RESET_MESSAGE,
        },
        Vec::new(),
    )
}

pub(super) fn query_param(query: Option<&str>, key: &str) -> Option<String> {
    query?.split('&').find_map(|pair| {
        let (name, value) = pair.split_once('=')?;
        (name == key).then(|| value.replace('+', " "))
    })
}

pub(super) fn path_param<'a>(request: &'a ApiRequest, name: &str) -> Option<&'a str> {
    request
        .extensions()
        .get::<PathParams>()
        .and_then(|params| params.get(name))
}

pub(super) fn redirect_with_query(
    location: &str,
    key: &str,
    value: &str,
) -> Result<ApiResponse, OpenAuthError> {
    let separator = if location.contains('?') { '&' } else { '?' };
    redirect(&format!(
        "{location}{separator}{key}={}",
        percent_encode(value)
    ))
}

fn redirect(location: &str) -> Result<ApiResponse, OpenAuthError> {
    http::Response::builder()
        .status(StatusCode::FOUND)
        .header(header::LOCATION, location)
        .body(Vec::new())
        .map_err(|error| OpenAuthError::Serialization {
            context: "building password redirect response",
            message: error.to_string(),
        })
}

fn percent_encode(value: &str) -> String {
    url::form_urlencoded::byte_serialize(value.as_bytes()).collect()
}