use std::sync::Arc;
use async_graphql::dynamic::indexmap::IndexMap;
use async_graphql::dynamic::{Field, FieldFuture, FieldValue, InputValue, Object, TypeRef};
use async_graphql::{Name, Value as GraphqlValue};
use super::error::{GraphqlError, auth_error, resolver_error};
use super::utils::GraphqlValueUtils;
use crate::catalog::{AccessDefinition, AccessType};
use crate::dbs::Session;
use crate::iam::token::Token;
use crate::iam::{signin, signup};
use crate::kvs::Datastore;
use crate::types::PublicVariables;
pub fn add_auth_mutations(
mutation: Object,
accesses: &[AccessDefinition],
ns: &str,
db: &str,
datastore: &Arc<Datastore>,
) -> Object {
let has_signin = accesses.iter().any(|ac| match &ac.access_type {
AccessType::Record(rec) => rec.signin.is_some(),
_ => false,
});
let has_signup = accesses.iter().any(|ac| match &ac.access_type {
AccessType::Record(rec) => rec.signup.is_some(),
_ => false,
});
let mut mutation = mutation;
if has_signin {
let kvs = Arc::clone(datastore);
let ns_name = ns.to_string();
let db_name = db.to_string();
mutation = mutation.field(
Field::new("signIn", TypeRef::named_nn(TypeRef::STRING), move |ctx| {
let kvs = Arc::clone(&kvs);
let ns_name = ns_name.clone();
let db_name = db_name.clone();
FieldFuture::new(async move {
let sess = ctx.data::<Arc<Session>>()?;
let args = ctx.args.as_index_map();
let access = args
.get("access")
.and_then(GraphqlValueUtils::as_string)
.ok_or_else(|| resolver_error("Missing required 'access' argument"))?;
let variables = args
.get("variables")
.and_then(GraphqlValueUtils::as_object)
.ok_or_else(|| {
resolver_error(
"Missing required 'variables' argument (must be an object)",
)
})?;
let vars = build_public_variables(&ns_name, &db_name, &access, variables)?;
let mut auth_sess = Session {
ns: Some(ns_name.clone()),
db: Some(db_name.clone()),
..Default::default()
};
auth_sess.ip.clone_from(&sess.ip);
auth_sess.or.clone_from(&sess.or);
let token = signin::signin(&kvs, &mut auth_sess, vars).await.map_err(|e| {
warn!("GraphQL signIn failed: {e}");
auth_error("There was a problem with authentication")
})?;
let access_token = match token {
Token::Access(t) => t,
Token::WithRefresh {
access,
..
} => access,
};
Ok(Some(FieldValue::value(GraphqlValue::String(access_token))))
})
})
.description("Sign in using a database access method and return a JWT token")
.argument(InputValue::new("access", TypeRef::named_nn(TypeRef::STRING)))
.argument(InputValue::new("variables", TypeRef::named_nn("JSON"))),
);
}
if has_signup {
let kvs = Arc::clone(datastore);
let ns_name = ns.to_string();
let db_name = db.to_string();
mutation = mutation.field(
Field::new("signUp", TypeRef::named_nn(TypeRef::STRING), move |ctx| {
let kvs = Arc::clone(&kvs);
let ns_name = ns_name.clone();
let db_name = db_name.clone();
FieldFuture::new(async move {
let sess = ctx.data::<Arc<Session>>()?;
let args = ctx.args.as_index_map();
let access = args
.get("access")
.and_then(GraphqlValueUtils::as_string)
.ok_or_else(|| resolver_error("Missing required 'access' argument"))?;
let variables = args
.get("variables")
.and_then(GraphqlValueUtils::as_object)
.ok_or_else(|| {
resolver_error(
"Missing required 'variables' argument (must be an object)",
)
})?;
let vars = build_public_variables(&ns_name, &db_name, &access, variables)?;
let mut auth_sess = Session {
ns: Some(ns_name.clone()),
db: Some(db_name.clone()),
..Default::default()
};
auth_sess.ip.clone_from(&sess.ip);
auth_sess.or.clone_from(&sess.or);
let token = signup::signup(&kvs, &mut auth_sess, vars).await.map_err(|e| {
warn!("GraphQL signUp failed: {e}");
auth_error("There was a problem with authentication")
})?;
let access_token = match token {
Token::Access(t) => t,
Token::WithRefresh {
access,
..
} => access,
};
Ok(Some(FieldValue::value(GraphqlValue::String(access_token))))
})
})
.description("Sign up using a database access method and return a JWT token")
.argument(InputValue::new("access", TypeRef::named_nn(TypeRef::STRING)))
.argument(InputValue::new("variables", TypeRef::named_nn("JSON"))),
);
}
mutation
}
fn build_public_variables(
ns: &str,
db: &str,
access: &str,
variables: &IndexMap<Name, GraphqlValue>,
) -> Result<PublicVariables, GraphqlError> {
let mut vars = PublicVariables::new();
vars.insert("NS", ns.to_string());
vars.insert("DB", db.to_string());
vars.insert("AC", access.to_string());
for (key, val) in variables {
let key_str = key.as_str();
if matches!(key_str, "NS" | "ns" | "DB" | "db" | "AC" | "ac") {
continue;
}
match val {
GraphqlValue::Null => continue,
GraphqlValue::String(s) => vars.insert(key_str.to_string(), s.clone()),
GraphqlValue::Number(n) => {
if let Some(i) = n.as_i64() {
vars.insert(key_str.to_string(), i);
} else if let Some(f) = n.as_f64() {
vars.insert(key_str.to_string(), f);
} else {
vars.insert(key_str.to_string(), n.to_string());
}
}
GraphqlValue::Boolean(b) => vars.insert(key_str.to_string(), *b),
GraphqlValue::Enum(s) => vars.insert(key_str.to_string(), s.as_str().to_string()),
other => vars.insert(key_str.to_string(), other.to_string()),
}
}
Ok(vars)
}