use crate::resolver::session_token_hash;
use crate::session_policy::{AllowSessionPolicy, AuthSessionPolicy, SessionCreateInput};
use chrono::{DateTime, Utc};
use platform_core::{AppError, AppResult, DbPool, ErrorCode};
use sqlx::{Postgres, Transaction};
pub use crate::models::{AuthSession, AuthUserId};
pub use crate::session_policy::SessionCreateOptions;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthIdentity {
pub id: String,
pub user_id: AuthUserId,
}
pub async fn create_user_identity_in_tx(
tx: &mut Transaction<'_, Postgres>,
user_id: AuthUserId,
identity_id: String,
provider: &str,
provider_subject: &str,
created_at: DateTime<Utc>,
) -> AppResult<AuthIdentity> {
sqlx::query(
r#"
insert into auth.users (id, created_at, disabled_at, disabled_reason, disabled_until)
values ($1, $2, null, null, null)
"#,
)
.bind(&user_id.0)
.bind(created_at)
.execute(&mut **tx)
.await
.map_err(map_sql_error)?;
sqlx::query(
r#"
insert into auth.identities (id, user_id, provider, provider_subject, created_at, updated_at)
values ($1, $2, $3, $4, $5, $5)
"#,
)
.bind(&identity_id)
.bind(&user_id.0)
.bind(provider)
.bind(provider_subject)
.bind(created_at)
.execute(&mut **tx)
.await
.map_err(map_sql_error)?;
Ok(AuthIdentity {
id: identity_id,
user_id,
})
}
pub async fn find_active_identity(
pool: &DbPool,
provider: &str,
provider_subject: &str,
) -> AppResult<Option<AuthIdentity>> {
sqlx::query_as::<_, IdentityRow>(
r#"
select identities.id, identities.user_id
from auth.identities identities
join auth.users users on users.id = identities.user_id
where identities.provider = $1
and identities.provider_subject = $2
and (users.disabled_at is null or users.disabled_until <= now())
limit 1
"#,
)
.bind(provider)
.bind(provider_subject)
.fetch_optional(pool)
.await
.map(|row| row.map(identity_from_row))
.map_err(map_sql_error)
}
pub async fn create_session(
pool: &DbPool,
user_id: &AuthUserId,
session_id: String,
token: String,
created_at: DateTime<Utc>,
expires_at: DateTime<Utc>,
) -> AppResult<AuthSession> {
create_session_with_policy(
pool,
user_id,
session_id,
token,
created_at,
expires_at,
SessionCreateOptions::default(),
&AllowSessionPolicy,
)
.await
}
pub async fn create_session_with_policy(
pool: &DbPool,
user_id: &AuthUserId,
session_id: String,
token: String,
created_at: DateTime<Utc>,
expires_at: DateTime<Utc>,
options: SessionCreateOptions,
policy: &dyn AuthSessionPolicy,
) -> AppResult<AuthSession> {
let mut tx = pool.begin().await.map_err(map_sql_error)?;
let session = create_session_in_tx_with_policy(
&mut tx, user_id, session_id, token, created_at, expires_at, options, policy,
)
.await?;
tx.commit().await.map_err(map_sql_error)?;
Ok(session)
}
pub async fn create_session_in_tx(
tx: &mut Transaction<'_, Postgres>,
user_id: &AuthUserId,
session_id: String,
token: String,
created_at: DateTime<Utc>,
expires_at: DateTime<Utc>,
) -> AppResult<AuthSession> {
create_session_in_tx_with_policy(
tx,
user_id,
session_id,
token,
created_at,
expires_at,
SessionCreateOptions::default(),
&AllowSessionPolicy,
)
.await
}
pub async fn create_session_in_tx_with_policy(
tx: &mut Transaction<'_, Postgres>,
user_id: &AuthUserId,
session_id: String,
token: String,
created_at: DateTime<Utc>,
expires_at: DateTime<Utc>,
options: SessionCreateOptions,
policy: &dyn AuthSessionPolicy,
) -> AppResult<AuthSession> {
let active_user_exists = sqlx::query_scalar::<_, bool>(
r#"
select exists(
select 1
from auth.users
where id = $1
and (disabled_at is null or disabled_until <= now())
)
"#,
)
.bind(&user_id.0)
.fetch_one(&mut **tx)
.await
.map_err(map_sql_error)?;
if !active_user_exists {
return Err(AppError::new(ErrorCode::Forbidden, "Auth user is disabled"));
}
let decision = policy
.before_session_create(&SessionCreateInput {
user_id: user_id.clone(),
session_id: session_id.clone(),
proposed_device_id: options.device_id,
created_at,
expires_at,
client: options.client.clone(),
})
.await?;
sqlx::query(
r#"
insert into auth.sessions (
id,
user_id,
token_hash,
device_id,
client_ip,
user_agent,
created_at,
expires_at,
revoked_at
)
values ($1, $2, $3, $4, $5, $6, $7, $8, null)
"#,
)
.bind(&session_id)
.bind(&user_id.0)
.bind(session_token_hash(&token))
.bind(decision.device_id.as_deref())
.bind(options.client.ip.as_deref())
.bind(options.client.user_agent.as_deref())
.bind(created_at)
.bind(expires_at)
.execute(&mut **tx)
.await
.map_err(map_sql_error)?;
Ok(AuthSession {
id: session_id,
user_id: user_id.clone(),
token,
device_id: decision.device_id,
expires_at,
})
}
type IdentityRow = (String, String);
fn identity_from_row(row: IdentityRow) -> AuthIdentity {
let (id, user_id) = row;
AuthIdentity {
id,
user_id: AuthUserId(user_id),
}
}
fn map_sql_error(source: sqlx::Error) -> AppError {
if let sqlx::Error::Database(database_error) = &source {
if database_error.constraint() == Some("identities_provider_subject_key") {
return AppError::new(ErrorCode::Conflict, "An auth identity already exists")
.with_source(source);
}
}
AppError::new(ErrorCode::Internal, "Internal server error").with_source(source)
}