openauth-core 0.0.6

Core types and primitives for OpenAuth.
Documentation
use serde_json::{Map, Value};
use time::OffsetDateTime;

use crate::api::additional_fields::json_to_db_value;
use crate::auth::session::{GetSessionInput, GetSessionResult, SessionAuth};
use crate::context::AuthContext;
use crate::db::{DbAdapter, DbRecord, DbValue, Session, Update, User, Where};
use crate::error::OpenAuthError;
use crate::session::SessionStore;

#[derive(Debug, Clone, PartialEq, Eq)]
pub(in crate::api) struct CurrentSessionInput {
    pub(in crate::api) cookie_header: String,
    pub(in crate::api) disable_cookie_cache: bool,
    pub(in crate::api) disable_refresh: bool,
    pub(in crate::api) defer_refresh: bool,
}

#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
pub(in crate::api) enum UpdateSessionError {
    #[error("body must be an object")]
    BodyMustBeObject,
    #[error("field is not accepted as input")]
    FieldNotInput,
    #[error("invalid session field value")]
    InvalidFieldValue,
    #[error("no fields to update")]
    NoFieldsToUpdate,
    #[error("session was not found")]
    SessionNotFound,
}

#[derive(Debug, thiserror::Error)]
pub(in crate::api) enum UpdateSessionErrorOrOpenAuth {
    #[error(transparent)]
    Service(#[from] UpdateSessionError),
    #[error(transparent)]
    OpenAuth(#[from] OpenAuthError),
}

pub(in crate::api) async fn current_session(
    adapter: &dyn DbAdapter,
    context: &AuthContext,
    input: CurrentSessionInput,
) -> Result<Option<GetSessionResult>, OpenAuthError> {
    let mut session_input = GetSessionInput::new(input.cookie_header);
    if input.disable_cookie_cache {
        session_input = session_input.disable_cookie_cache();
    }
    if input.disable_refresh {
        session_input = session_input.disable_refresh();
    }
    if input.defer_refresh {
        session_input = session_input.defer_refresh();
    }
    SessionAuth::new(adapter, context)
        .get_session(session_input)
        .await
}

pub(in crate::api) async fn list_sessions(
    adapter: &dyn DbAdapter,
    context: &AuthContext,
    user: &User,
) -> Result<Vec<Session>, OpenAuthError> {
    SessionStore::new(adapter, context)
        .list_user_sessions(&user.id)
        .await
}

pub(in crate::api) async fn revoke_session(
    adapter: &dyn DbAdapter,
    context: &AuthContext,
    user: &User,
    token: &str,
) -> Result<(), OpenAuthError> {
    let store = SessionStore::new(adapter, context);
    if let Some(session) = store.find_session(token).await? {
        if session.user_id == user.id {
            store.delete_session(token).await?;
        }
    }
    Ok(())
}

pub(in crate::api) async fn revoke_sessions(
    adapter: &dyn DbAdapter,
    context: &AuthContext,
    user: &User,
) -> Result<(), OpenAuthError> {
    SessionStore::new(adapter, context)
        .delete_user_sessions(&user.id)
        .await?;
    Ok(())
}

pub(in crate::api) async fn revoke_other_sessions(
    adapter: &dyn DbAdapter,
    context: &AuthContext,
    current_session: &Session,
    user: &User,
) -> Result<(), OpenAuthError> {
    let store = SessionStore::new(adapter, context);
    let sessions = store.list_user_sessions(&user.id).await?;
    for session in sessions {
        if session.token != current_session.token {
            store.delete_session(&session.token).await?;
        }
    }
    Ok(())
}

pub(in crate::api) async fn update_session(
    adapter: &dyn DbAdapter,
    context: &AuthContext,
    current: &Session,
    body: Value,
) -> Result<DbRecord, UpdateSessionErrorOrOpenAuth> {
    let Some(fields) = body.as_object() else {
        return Err(UpdateSessionError::BodyMustBeObject.into());
    };
    update_session_fields(adapter, context, current, fields).await
}

async fn update_session_fields(
    adapter: &dyn DbAdapter,
    context: &AuthContext,
    current: &Session,
    fields: &Map<String, Value>,
) -> Result<DbRecord, UpdateSessionErrorOrOpenAuth> {
    let mut update = Update::new("session")
        .where_clause(Where::new("token", DbValue::String(current.token.clone())));
    for (name, value) in fields {
        if is_core_session_field(name) {
            continue;
        }
        let Some(field) = context.options.session.additional_fields.get(name) else {
            continue;
        };
        if !field.input {
            return Err(UpdateSessionError::FieldNotInput.into());
        }
        let Ok(value) = json_to_db_value(name, &field.field_type, value) else {
            return Err(UpdateSessionError::InvalidFieldValue.into());
        };
        update = update.data(name, value);
    }

    if update.data.is_empty() {
        return Err(UpdateSessionError::NoFieldsToUpdate.into());
    }

    update = update.data("updated_at", DbValue::Timestamp(OffsetDateTime::now_utc()));
    adapter
        .update(update)
        .await?
        .ok_or_else(|| UpdateSessionError::SessionNotFound.into())
}

fn is_core_session_field(name: &str) -> bool {
    matches!(
        name,
        "id" | "user_id"
            | "userId"
            | "expires_at"
            | "expiresAt"
            | "token"
            | "ip_address"
            | "ipAddress"
            | "user_agent"
            | "userAgent"
            | "created_at"
            | "createdAt"
            | "updated_at"
            | "updatedAt"
    )
}