openauth-plugins 0.0.3

Official OpenAuth plugin modules.
Documentation
use http::{header, HeaderValue, StatusCode};
use openauth_core::api::{ApiResponse, BodyField, BodySchema, JsonSchemaType};
use openauth_core::cookies::Cookie;
use openauth_core::error::OpenAuthError;
use serde::Serialize;

pub(super) fn json_response<T: Serialize>(
    status: StatusCode,
    value: &T,
) -> Result<ApiResponse, OpenAuthError> {
    http::Response::builder()
        .status(status)
        .header(header::CONTENT_TYPE, "application/json")
        .body(serde_json::to_vec(value).map_err(|error| OpenAuthError::Api(error.to_string()))?)
        .map_err(|error| OpenAuthError::Api(error.to_string()))
}

pub(super) fn api_error(
    status: StatusCode,
    code: &str,
    message: &str,
) -> Result<ApiResponse, OpenAuthError> {
    json_response(
        status,
        &serde_json::json!({ "code": code, "message": message }),
    )
}

pub(super) fn redirect(location: &str, cookies: Vec<Cookie>) -> Result<ApiResponse, OpenAuthError> {
    let mut response = http::Response::builder()
        .status(StatusCode::FOUND)
        .header(header::LOCATION, location)
        .body(Vec::new())
        .map_err(|error| OpenAuthError::Api(error.to_string()))?;
    for cookie in cookies {
        response.headers_mut().append(
            header::SET_COOKIE,
            HeaderValue::from_str(&serialize_cookie(&cookie))
                .map_err(|error| OpenAuthError::Cookie(error.to_string()))?,
        );
    }
    Ok(response)
}

pub(super) fn redirect_with_error(
    location: &str,
    error: &str,
) -> Result<ApiResponse, OpenAuthError> {
    redirect_with_error_description(location, error, None)
}

pub(super) fn redirect_with_error_description(
    location: &str,
    error: &str,
    description: Option<&str>,
) -> Result<ApiResponse, OpenAuthError> {
    let separator = if location.contains('?') { '&' } else { '?' };
    let mut target = format!("{location}{separator}error={}", percent_encode(error));
    if let Some(description) = description {
        target.push_str("&error_description=");
        target.push_str(&percent_encode(description));
    }
    redirect(&target, Vec::new())
}

pub(super) fn sign_in_schema() -> BodySchema {
    BodySchema::object([
        BodyField::new("providerId", JsonSchemaType::String),
        BodyField::optional("callbackURL", JsonSchemaType::String),
        BodyField::optional("errorCallbackURL", JsonSchemaType::String),
        BodyField::optional("newUserCallbackURL", JsonSchemaType::String),
        BodyField::optional("disableRedirect", JsonSchemaType::Boolean),
        BodyField::optional("scopes", JsonSchemaType::Array),
        BodyField::optional("requestSignUp", JsonSchemaType::Boolean),
        BodyField::optional("additionalData", JsonSchemaType::Object),
    ])
}

pub(super) fn link_schema() -> BodySchema {
    BodySchema::object([
        BodyField::new("providerId", JsonSchemaType::String),
        BodyField::new("callbackURL", JsonSchemaType::String),
        BodyField::optional("errorCallbackURL", JsonSchemaType::String),
        BodyField::optional("scopes", JsonSchemaType::Array),
    ])
}

fn serialize_cookie(cookie: &Cookie) -> String {
    let mut value = format!("{}={}", cookie.name, cookie.value);
    if let Some(path) = &cookie.attributes.path {
        value.push_str(&format!("; Path={path}"));
    }
    if let Some(max_age) = cookie.attributes.max_age {
        value.push_str(&format!("; Max-Age={max_age}"));
    }
    if cookie.attributes.http_only.unwrap_or(false) {
        value.push_str("; HttpOnly");
    }
    if cookie.attributes.secure.unwrap_or(false) {
        value.push_str("; Secure");
    }
    if let Some(same_site) = &cookie.attributes.same_site {
        value.push_str(&format!("; SameSite={same_site}"));
    }
    value
}

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