pub struct Scope(pub String);
#[derive(Debug, thiserror::Error)]
pub enum AuthError {
#[error("diesel error: {0}")]
Diesel(#[from] diesel::result::Error),
#[error("ft_sdk::auth::UserData::Name is required")]
NameNotProvided,
#[error("identity already exists")]
IdentityExists,
}
pub fn user_data_by_verified_email(
conn: &mut ft_sdk::Connection,
provider_id: &str,
email: &str,
) -> Result<(ft_sdk::auth::UserId, ft_sdk::auth::ProviderData), ft_sdk::auth::UserDataError> {
assert_valid_provider_id(provider_id);
let (id, _, data) = ft_sdk::auth::user_data_by_query(
conn,
format!(
r#"
SELECT
id, identity, data -> '{provider_id}' as data
FROM
fastn_user
WHERE
EXISTS (
SELECT
1
FROM
json_each(data -> '{provider_id}' -> 'verified_emails')
WHERE value = $1
)
"#
)
.as_str(),
email,
)?;
Ok((id, data))
}
pub fn user_data_by_email(
conn: &mut ft_sdk::Connection,
provider_id: &str,
email: &str,
) -> Result<(ft_sdk::auth::UserId, ft_sdk::auth::ProviderData), ft_sdk::auth::UserDataError> {
assert_valid_provider_id(provider_id);
let (id, _, data) = ft_sdk::auth::user_data_by_query(
conn,
format!(
r#"
SELECT
id, identity, data -> '{provider_id}' as data
FROM
fastn_user
WHERE
EXISTS (
SELECT
1
FROM
json_each(data -> '{provider_id}' -> 'emails')
WHERE value = $1
)
"#
)
.as_str(),
email,
)?;
Ok((id, data))
}
pub fn user_data_by_custom_attribute(
conn: &mut ft_sdk::Connection,
provider_id: &str,
key: &str,
value: &str,
) -> Result<(ft_sdk::auth::UserId, ft_sdk::auth::ProviderData), ft_sdk::auth::UserDataError> {
assert_valid_provider_id(provider_id);
let (id, _, data) = ft_sdk::auth::user_data_by_query(
conn,
format!(
r#"
SELECT
id, identity, data -> '{provider_id}' as data
FROM
fastn_user
WHERE
EXISTS (
SELECT
1
FROM
json_each(data -> '{provider_id}' -> 'custom' -> '{key}')
WHERE value = $1
)
"#
)
.as_str(),
value,
)?;
Ok((id, data))
}
pub fn assert_valid_provider_id(provider_id: &str) {
provider_id.chars().for_each(|c| {
if !c.is_ascii_alphanumeric() {
panic!("invalid provider id: {}", provider_id);
}
});
}
pub fn user_data_by_identity(
conn: &mut ft_sdk::Connection,
provider_id: &str,
identity: &str,
) -> Result<(ft_sdk::auth::UserId, ft_sdk::auth::ProviderData), ft_sdk::auth::UserDataError> {
assert_valid_provider_id(provider_id);
let (id, _, data) = ft_sdk::auth::user_data_by_query(
conn,
format!(
r#"
SELECT
id, identity, data -> '{provider_id}' as data
FROM fastn_user
WHERE
data -> '{provider_id}' -> 'identity' = json_quote($1)
"#
)
.as_str(),
identity,
)?;
Ok((id, data))
}
pub fn user_data_by_id(
conn: &mut ft_sdk::Connection,
provider_id: &str,
user_id: &ft_sdk::UserId,
) -> Result<ft_sdk::auth::ProviderData, ft_sdk::auth::UserDataError> {
use diesel::prelude::*;
use ft_sdk::auth::fastn_user;
let data: String = fastn_user::table
.select(fastn_user::data)
.filter(fastn_user::id.eq(user_id.0))
.first(conn)?;
let data: serde_json::Value = serde_json::from_str(&data)?;
let data = data
.as_object()
.and_then(|m| m.get(provider_id))
.map(|v| serde_json::from_value::<ft_sdk::auth::ProviderData>(v.clone()));
if data.is_none() {
return Err(ft_sdk::auth::UserDataError::NoDataFound);
}
let data = data.unwrap()?;
Ok(data)
}
#[derive(Debug, thiserror::Error)]
pub enum CreateUserError {
#[error("diesel error {0}")]
Diesel(#[from] diesel::result::Error),
#[error("login error {0}")]
Login(#[from] LoginError),
}
#[derive(Debug, thiserror::Error)]
pub enum UpdateUserDataError {
#[error("provider input data is not valid json {0}")]
ProviderDataNotJson(serde_json::Error),
#[error("cant read user data from db {0}")]
CantReadUserData(diesel::result::Error),
#[error("db data is not valid json {0}")]
DbDataNotJson(serde_json::Error),
#[error("data in db is not a map")]
DbDataIsNotMap,
#[error("cant serialise merged data {0}")]
CantSerialiseMergedData(serde_json::Error),
#[error("cant store user data {0}")]
CantStoreUserData(diesel::result::Error),
#[error("failed to commit transaction {0}")]
FailedToCommitTransaction(#[from] diesel::result::Error),
}
pub fn update_user(
conn: &mut ft_sdk::Connection,
provider_id: &str,
user_id: &ft_sdk::auth::UserId,
data: ft_sdk::auth::ProviderData,
update_identity: bool,
) -> Result<(), UpdateUserDataError> {
use diesel::prelude::*;
use ft_sdk::auth::fastn_user;
let data_value =
serde_json::to_value(&data).map_err(UpdateUserDataError::ProviderDataNotJson)?;
conn.transaction::<_, UpdateUserDataError, _>(|conn| {
let existing_data = fastn_user::table
.select(fastn_user::data)
.filter(fastn_user::id.eq(user_id.0))
.first::<String>(conn)
.map_err(UpdateUserDataError::CantReadUserData)?;
let mut existing_data: serde_json::Value =
serde_json::from_str(&existing_data).map_err(UpdateUserDataError::DbDataNotJson)?;
match existing_data {
serde_json::Value::Object(ref mut m) => {
m.insert(provider_id.to_string(), data_value);
}
_ => {
return Err(UpdateUserDataError::DbDataIsNotMap);
}
}
let merged_data = serde_json::to_string(&existing_data)
.map_err(UpdateUserDataError::CantSerialiseMergedData)?;
if update_identity {
diesel::update(fastn_user::table.filter(fastn_user::id.eq(user_id.0)))
.set((
fastn_user::data.eq(merged_data),
fastn_user::identity.eq(data.identity),
fastn_user::name.eq(data.name),
fastn_user::updated_at.eq(ft_sdk::env::now()),
))
.execute(conn)
.map_err(UpdateUserDataError::CantStoreUserData)
} else {
diesel::update(fastn_user::table.filter(fastn_user::id.eq(user_id.0)))
.set((
fastn_user::data.eq(merged_data),
fastn_user::updated_at.eq(ft_sdk::env::now()),
))
.execute(conn)
.map_err(UpdateUserDataError::CantStoreUserData)
}
})?;
Ok(())
}
pub fn create_user(
conn: &mut ft_sdk::Connection,
provider_id: &str,
data: ft_sdk::auth::ProviderData,
) -> Result<ft_sdk::auth::UserId, CreateUserError> {
use diesel::prelude::*;
use ft_sdk::auth::fastn_user;
let provider_data =
serde_json::to_string(&serde_json::json!({provider_id: data.clone()})).unwrap();
let user_id: i64 = diesel::insert_into(fastn_user::table)
.values((
fastn_user::name.eq(data.name),
fastn_user::data.eq(provider_data),
fastn_user::identity.eq(data.identity),
fastn_user::created_at.eq(ft_sdk::env::now()),
fastn_user::updated_at.eq(ft_sdk::env::now()),
))
.returning(fastn_user::id)
.get_result(conn)?;
Ok(ft_sdk::auth::UserId(user_id))
}
pub fn login(
conn: &mut ft_sdk::Connection,
user_id: &ft_sdk::UserId,
session_id: Option<ft_sdk::session::SessionID>,
) -> Result<ft_sdk::session::SessionID, LoginError> {
match session_id {
Some(session_id) => Ok(session_id.set_user_id(conn, user_id.clone())?),
None => Ok(ft_sdk::session::SessionID::create(
conn,
Some(user_id.clone()),
None,
)?),
}
}
#[derive(thiserror::Error, Debug)]
pub enum LoginError {
#[error("db error: {0}")]
DatabaseError(#[from] diesel::result::Error),
#[error("json error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("session error: {0}")]
SessionError(#[from] ft_sdk::Error),
}
pub fn identity_exists(
conn: &mut ft_sdk::Connection,
identity: &str,
provider_id: &str,
) -> Result<bool, diesel::result::Error> {
use diesel::prelude::*;
match diesel::sql_query(format!(
r#"
SELECT count(*) AS count
FROM fastn_user
WHERE
data -> '{provider_id}' -> 'identity' = $1
"#
))
.bind::<diesel::sql_types::Text, _>(identity)
.get_result::<ft_sdk::auth::utils::Counter>(conn)
{
Ok(r) if r.count == 0 => Ok(false),
Ok(_) => Ok(true),
Err(e) => Err(e),
}
}