Tokens
This commit is contained in:
parent
e0fae4ede8
commit
a1e755061e
@ -1077,7 +1077,7 @@ impl ShoppingCartItem {
|
|||||||
pub struct TokenId(RecordId);
|
pub struct TokenId(RecordId);
|
||||||
|
|
||||||
#[cfg_attr(feature = "db", derive(sqlx::FromRow))]
|
#[cfg_attr(feature = "db", derive(sqlx::FromRow))]
|
||||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct Token {
|
pub struct Token {
|
||||||
pub id: TokenId,
|
pub id: TokenId,
|
||||||
pub customer_id: uuid::Uuid,
|
pub customer_id: uuid::Uuid,
|
||||||
|
@ -32,6 +32,7 @@ thiserror = { version = "1" }
|
|||||||
tokio = { version = "1", features = ['full'] }
|
tokio = { version = "1", features = ['full'] }
|
||||||
tracing = { version = "0" }
|
tracing = { version = "0" }
|
||||||
uuid = { version = "1", features = ['v4'] }
|
uuid = { version = "1", features = ['v4'] }
|
||||||
|
gumdrop = { version = "0" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
fake = { version = "2" }
|
fake = { version = "2" }
|
||||||
|
519
crates/token_manager/src/actions.rs
Normal file
519
crates/token_manager/src/actions.rs
Normal file
@ -0,0 +1,519 @@
|
|||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use chrono::{NaiveDateTime, TimeZone, Utc};
|
||||||
|
use config::SharedAppConfig;
|
||||||
|
use db_utils::PgT;
|
||||||
|
use hmac::digest::KeyInit;
|
||||||
|
use hmac::Hmac;
|
||||||
|
use model::{AccessTokenString, AccountId, Audience, Role, Token};
|
||||||
|
use sha2::Sha256;
|
||||||
|
|
||||||
|
use crate::db::Database;
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, serde::Serialize, thiserror::Error)]
|
||||||
|
#[serde(rename_all = "kebab-case", tag = "token")]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("Database connection failure")]
|
||||||
|
DatabaseError,
|
||||||
|
#[error("Unable to save new token")]
|
||||||
|
Save,
|
||||||
|
#[error("Unable to save new token. Can't connect to database")]
|
||||||
|
SaveInternal,
|
||||||
|
#[error("Unable to validate token")]
|
||||||
|
Validate,
|
||||||
|
#[error("Unable to validate token. Can't connect to database")]
|
||||||
|
ValidateInternal,
|
||||||
|
#[error("Token does not exists or some fields are incorrect")]
|
||||||
|
Invalid,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
/// Creates single token, it's mostly used by [CreatePair]
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use actix::Addr;
|
||||||
|
/// use model::{AccountId, Role};
|
||||||
|
/// use token_manager::*;
|
||||||
|
/// async fn create_pair(token_manager: Addr<token_manager::TokenManager>) {
|
||||||
|
/// match token_manager.send(CreateToken { customer_id: uuid::Uuid::new_v4(), role: Role::Admin, subject: AccountId::from(1), audience: None, exp: None }).await {
|
||||||
|
/// Ok(Ok(pair)) => {}
|
||||||
|
/// Ok(Err(manager_error)) => {}
|
||||||
|
/// Err(actor_error) => {}
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CreateToken {
|
||||||
|
pub customer_id: uuid::Uuid,
|
||||||
|
pub role: Role,
|
||||||
|
pub subject: AccountId,
|
||||||
|
pub audience: Option<Audience>,
|
||||||
|
pub exp: Option<NaiveDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CreateToken {
|
||||||
|
pub async fn run(
|
||||||
|
self,
|
||||||
|
db: Database,
|
||||||
|
config: SharedAppConfig,
|
||||||
|
) -> Result<(Token, AccessTokenString)> {
|
||||||
|
let mut t = db.pool.begin().await.map_err(|e| {
|
||||||
|
tracing::error!("{e}");
|
||||||
|
Error::DatabaseError
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let auth = match self.run_t(&mut t, config).await {
|
||||||
|
Ok(auth) => auth,
|
||||||
|
Err(e) => {
|
||||||
|
t.rollback().await.ok();
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if t.commit().await.is_err() {
|
||||||
|
return Err(Error::DatabaseError);
|
||||||
|
}
|
||||||
|
Ok(auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_t(
|
||||||
|
self,
|
||||||
|
t: &mut PgT<'_>,
|
||||||
|
config: SharedAppConfig,
|
||||||
|
) -> Result<(Token, AccessTokenString)> {
|
||||||
|
let CreateToken {
|
||||||
|
customer_id,
|
||||||
|
role,
|
||||||
|
subject,
|
||||||
|
audience,
|
||||||
|
exp,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
let audience = audience.unwrap_or_default();
|
||||||
|
|
||||||
|
let token: Token = match exp {
|
||||||
|
None => crate::db::CreateToken {
|
||||||
|
customer_id,
|
||||||
|
role,
|
||||||
|
subject,
|
||||||
|
audience,
|
||||||
|
}
|
||||||
|
.run(&mut *t)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::warn!("{e}");
|
||||||
|
Error::Save
|
||||||
|
})?,
|
||||||
|
Some(exp) => crate::db::CreateExtendedToken {
|
||||||
|
customer_id,
|
||||||
|
role,
|
||||||
|
subject,
|
||||||
|
audience,
|
||||||
|
expiration_time: exp,
|
||||||
|
}
|
||||||
|
.run(&mut *t)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::warn!("{e}");
|
||||||
|
Error::Save
|
||||||
|
})?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let token_string = {
|
||||||
|
use jwt::SignWithKey;
|
||||||
|
|
||||||
|
let secret = config.lock().web().jwt_secret();
|
||||||
|
let key: Hmac<Sha256> = build_key(secret)?;
|
||||||
|
let mut claims = BTreeMap::new();
|
||||||
|
|
||||||
|
// cti (customer id): Customer uuid identifier used by payment service
|
||||||
|
claims.insert("cti", format!("{}", token.customer_id));
|
||||||
|
// arl (account role): account role
|
||||||
|
claims.insert("arl", String::from(token.role.as_str()));
|
||||||
|
// iss (issuer): Issuer of the JWT
|
||||||
|
claims.insert("iss", token.issuer.to_string());
|
||||||
|
// sub (subject): Subject of the JWT (the user)
|
||||||
|
claims.insert("sub", format!("{}", token.subject));
|
||||||
|
// aud (audience): Recipient for which the JWT is intended
|
||||||
|
claims.insert("aud", String::from(token.audience.as_str()));
|
||||||
|
// exp (expiration time): Time after which the JWT expires
|
||||||
|
claims.insert(
|
||||||
|
"exp",
|
||||||
|
format!(
|
||||||
|
"{}",
|
||||||
|
Utc.from_utc_datetime(&token.expiration_time).format("%+")
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// nbt (not before time): Time before which the JWT must not be accepted
|
||||||
|
// for processing
|
||||||
|
claims.insert(
|
||||||
|
"nbt",
|
||||||
|
format!(
|
||||||
|
"{}",
|
||||||
|
Utc.from_utc_datetime(&token.not_before_time).format("%+")
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// iat (issued at time): Time at which the JWT was issued; can be used
|
||||||
|
// to determine age of the JWT,
|
||||||
|
claims.insert(
|
||||||
|
"iat",
|
||||||
|
format!(
|
||||||
|
"{}",
|
||||||
|
Utc.from_utc_datetime(&token.issued_at_time).format("%+")
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// jti (JWT ID): Unique identifier; can be used to prevent the JWT from
|
||||||
|
// being replayed (allows a token to be used only once)
|
||||||
|
claims.insert("jti", format!("{}", token.jwt_id));
|
||||||
|
|
||||||
|
let s = match claims.sign_with_key(&key) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("{e:?}");
|
||||||
|
return Err(Error::SaveInternal);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
AccessTokenString::new(s)
|
||||||
|
};
|
||||||
|
Ok((token, token_string))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct AuthPair {
|
||||||
|
pub access_token: Token,
|
||||||
|
pub access_token_string: AccessTokenString,
|
||||||
|
pub _refresh_token: Token,
|
||||||
|
pub refresh_token_string: model::RefreshTokenString,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates access token and refresh token
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use actix::Addr;
|
||||||
|
/// use model::{AccountId, Role};
|
||||||
|
/// use token_manager::CreatePair;
|
||||||
|
/// async fn create_pair(token_manager: Addr<token_manager::TokenManager>) {
|
||||||
|
/// match token_manager.send(CreatePair { customer_id: uuid::Uuid::new_v4(), account_id: AccountId::from(0), role: Role::Admin }).await {
|
||||||
|
/// Ok(Ok(pair)) => {}
|
||||||
|
/// Ok(Err(manager_error)) => {}
|
||||||
|
/// Err(actor_error) => {}
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CreatePair {
|
||||||
|
pub customer_id: uuid::Uuid,
|
||||||
|
pub role: Role,
|
||||||
|
pub account_id: AccountId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CreatePair {
|
||||||
|
pub async fn run(self, db: Database, config: SharedAppConfig) -> Result<AuthPair> {
|
||||||
|
let mut t = db.pool.begin().await.map_err(|e| {
|
||||||
|
tracing::error!("{e}");
|
||||||
|
Error::DatabaseError
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let auth = match self.run_t(&mut t, config).await {
|
||||||
|
Ok(auth) => auth,
|
||||||
|
Err(e) => {
|
||||||
|
t.rollback().await.ok();
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if t.commit().await.is_err() {
|
||||||
|
return Err(Error::DatabaseError);
|
||||||
|
}
|
||||||
|
Ok(auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_t(self, t: &mut PgT<'_>, config: SharedAppConfig) -> Result<AuthPair> {
|
||||||
|
let (access_token, refresh_token) = (
|
||||||
|
CreateToken {
|
||||||
|
customer_id: self.customer_id,
|
||||||
|
role: self.role,
|
||||||
|
subject: self.account_id,
|
||||||
|
audience: Some(model::Audience::Web),
|
||||||
|
exp: None,
|
||||||
|
}
|
||||||
|
.run_t(&mut *t, config.clone())
|
||||||
|
.await,
|
||||||
|
CreateToken {
|
||||||
|
customer_id: self.customer_id,
|
||||||
|
role: self.role,
|
||||||
|
subject: self.account_id,
|
||||||
|
audience: Some(model::Audience::Web),
|
||||||
|
exp: Some((chrono::Utc::now() + chrono::Duration::days(31)).naive_utc()),
|
||||||
|
}
|
||||||
|
.run_t(&mut *t, config.clone())
|
||||||
|
.await,
|
||||||
|
);
|
||||||
|
|
||||||
|
let (access_token, access_token_string): (Token, AccessTokenString) = access_token?;
|
||||||
|
let (refresh_token, refresh_token_string): (Token, AccessTokenString) = refresh_token?;
|
||||||
|
|
||||||
|
Ok(AuthPair {
|
||||||
|
access_token,
|
||||||
|
access_token_string,
|
||||||
|
_refresh_token: refresh_token,
|
||||||
|
refresh_token_string: refresh_token_string.into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if token is still valid
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use actix::Addr;
|
||||||
|
/// use model::{AccessTokenString, AccountId, Role};
|
||||||
|
/// use token_manager::{CreatePair, Validate};
|
||||||
|
/// async fn create_pair(token_manager: Addr<token_manager::TokenManager>, token: AccessTokenString) {
|
||||||
|
/// match token_manager.send(Validate { token }).await {
|
||||||
|
/// Ok(Ok(pair)) => {}
|
||||||
|
/// Ok(Err(manager_error)) => {}
|
||||||
|
/// Err(actor_error) => {}
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Validate {
|
||||||
|
pub token: AccessTokenString,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Validate {
|
||||||
|
pub async fn run(self, db: Database, config: SharedAppConfig) -> Result<Token> {
|
||||||
|
let mut t = db.pool.begin().await.map_err(|e| {
|
||||||
|
tracing::error!("{e}");
|
||||||
|
Error::DatabaseError
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let token = match self.run_t(&mut t, config).await {
|
||||||
|
Ok(token) => token,
|
||||||
|
Err(e) => {
|
||||||
|
t.rollback().await.ok();
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if t.commit().await.is_err() {
|
||||||
|
return Err(Error::DatabaseError);
|
||||||
|
}
|
||||||
|
Ok(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_t(self, t: &mut PgT<'_>, config: SharedAppConfig) -> Result<Token> {
|
||||||
|
use jwt::VerifyWithKey;
|
||||||
|
|
||||||
|
tracing::info!("Validating token {:?}", self.token);
|
||||||
|
|
||||||
|
let secret = config.lock().web().jwt_secret();
|
||||||
|
let key: Hmac<Sha256> = build_key(secret)?;
|
||||||
|
let claims: BTreeMap<String, String> = self.token.verify_with_key(&key).map_err(|e| {
|
||||||
|
tracing::warn!("{e}");
|
||||||
|
Error::Validate
|
||||||
|
})?;
|
||||||
|
let jti = claims.get("jti").ok_or_else(|| Error::Validate)?;
|
||||||
|
|
||||||
|
let token: Token = crate::db::TokenByJti {
|
||||||
|
jti: uuid::Uuid::from_str(jti).map_err(|e| {
|
||||||
|
tracing::warn!("{e}");
|
||||||
|
Error::Validate
|
||||||
|
})?,
|
||||||
|
}
|
||||||
|
.run(&mut *t)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("{e}");
|
||||||
|
Error::Validate
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if token.expiration_time < Utc::now().naive_utc() {
|
||||||
|
return Err(Error::Validate);
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_pair(&claims, "cti", token.customer_id, validate_uuid)?;
|
||||||
|
validate_pair(&claims, "arl", token.role, eq)?;
|
||||||
|
validate_pair(&claims, "iss", &token.issuer, eq)?;
|
||||||
|
validate_pair(&claims, "sub", token.subject, validate_num)?;
|
||||||
|
validate_pair(&claims, "aud", token.audience, eq)?;
|
||||||
|
validate_pair(&claims, "exp", &token.expiration_time, validate_time)?;
|
||||||
|
validate_pair(&claims, "nbt", &token.not_before_time, validate_time)?;
|
||||||
|
validate_pair(&claims, "iat", &token.issued_at_time, validate_time)?;
|
||||||
|
|
||||||
|
tracing::info!("JWT token valid");
|
||||||
|
Ok(token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Refresh {}
|
||||||
|
|
||||||
|
impl Refresh {
|
||||||
|
pub async fn run(self, _db: Database, _config: SharedAppConfig) -> Result<AuthPair> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_key(secret: String) -> Result<Hmac<Sha256>> {
|
||||||
|
match Hmac::new_from_slice(secret.as_bytes()) {
|
||||||
|
Ok(key) => Ok(key),
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("{e:?}");
|
||||||
|
dbg!(e);
|
||||||
|
Err(Error::ValidateInternal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn validate_pair<F, V>(
|
||||||
|
claims: &BTreeMap<String, String>,
|
||||||
|
key: &str,
|
||||||
|
v: V,
|
||||||
|
cmp: F,
|
||||||
|
) -> std::result::Result<(), Error>
|
||||||
|
where
|
||||||
|
F: for<'s> FnOnce(V, &'s str) -> bool,
|
||||||
|
V: PartialEq,
|
||||||
|
{
|
||||||
|
claims
|
||||||
|
.get(key)
|
||||||
|
.map(|s| cmp(v, s.as_str()))
|
||||||
|
.unwrap_or_default()
|
||||||
|
.then_some(())
|
||||||
|
.ok_or(Error::Invalid)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn eq<V>(value: V, text: &str) -> bool
|
||||||
|
where
|
||||||
|
V: for<'s> PartialEq<&'s str>,
|
||||||
|
{
|
||||||
|
value == text
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn validate_time(left: &NaiveDateTime, right: &str) -> bool {
|
||||||
|
chrono::DateTime::parse_from_str(right, "%+")
|
||||||
|
.map(|t| t.naive_utc() == *left)
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn validate_num(left: i32, right: &str) -> bool {
|
||||||
|
right.parse::<i32>().map(|n| left == n).unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn validate_uuid(left: uuid::Uuid, right: &str) -> bool {
|
||||||
|
uuid::Uuid::from_str(right)
|
||||||
|
.map(|u| u == left)
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[cfg(test)]
|
||||||
|
// mod tests {
|
||||||
|
// use actix::Actor;
|
||||||
|
// use config::UpdateConfig;
|
||||||
|
// use database_manager::Database;
|
||||||
|
// use model::*;
|
||||||
|
//
|
||||||
|
// use super::*;
|
||||||
|
//
|
||||||
|
// pub struct NoOpts;
|
||||||
|
//
|
||||||
|
// impl UpdateConfig for NoOpts {}
|
||||||
|
//
|
||||||
|
// #[actix::test]
|
||||||
|
// async fn create_token() {
|
||||||
|
// testx::db!(config, db);
|
||||||
|
// let db = db.start();
|
||||||
|
//
|
||||||
|
// let (token, _text) = super::create_token(
|
||||||
|
// CreateToken {
|
||||||
|
// customer_id: Default::default(),
|
||||||
|
// role: Role::Admin,
|
||||||
|
// subject: AccountId::from(1),
|
||||||
|
// audience: None,
|
||||||
|
// exp: None,
|
||||||
|
// },
|
||||||
|
// db.clone(),
|
||||||
|
// config,
|
||||||
|
// )
|
||||||
|
// .await
|
||||||
|
// .unwrap();
|
||||||
|
//
|
||||||
|
// db.send(database_manager::DeleteToken { token_id: token.id })
|
||||||
|
// .await
|
||||||
|
// .ok();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// #[actix::test]
|
||||||
|
// async fn create_pair() {
|
||||||
|
// testx::db!(config, db);
|
||||||
|
// let db = db.start();
|
||||||
|
//
|
||||||
|
// let AuthPair {
|
||||||
|
// access_token,
|
||||||
|
// access_token_string: _,
|
||||||
|
// refresh_token_string: _,
|
||||||
|
// _refresh_token,
|
||||||
|
// } = super::create_pair(
|
||||||
|
// CreatePair {
|
||||||
|
// customer_id: Default::default(),
|
||||||
|
// role: Role::Admin,
|
||||||
|
// account_id: AccountId::from(0),
|
||||||
|
// },
|
||||||
|
// db.clone(),
|
||||||
|
// config,
|
||||||
|
// )
|
||||||
|
// .await
|
||||||
|
// .unwrap();
|
||||||
|
//
|
||||||
|
// db.send(database_manager::DeleteToken {
|
||||||
|
// token_id: access_token.id,
|
||||||
|
// })
|
||||||
|
// .await
|
||||||
|
// .ok();
|
||||||
|
//
|
||||||
|
// db.send(database_manager::DeleteToken {
|
||||||
|
// token_id: _refresh_token.id,
|
||||||
|
// })
|
||||||
|
// .await
|
||||||
|
// .ok();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// #[actix::test]
|
||||||
|
// async fn validate() {
|
||||||
|
// testx::db!(config, db);
|
||||||
|
// let db = db.start();
|
||||||
|
//
|
||||||
|
// let (token, text) = super::create_token(
|
||||||
|
// CreateToken {
|
||||||
|
// customer_id: Default::default(),
|
||||||
|
// role: Role::Admin,
|
||||||
|
// subject: AccountId::from(1),
|
||||||
|
// audience: None,
|
||||||
|
// exp: None,
|
||||||
|
// },
|
||||||
|
// db.clone(),
|
||||||
|
// config.clone(),
|
||||||
|
// )
|
||||||
|
// .await
|
||||||
|
// .unwrap();
|
||||||
|
//
|
||||||
|
// super::validate(Validate { token: text }, db.clone(), config.clone())
|
||||||
|
// .await
|
||||||
|
// .unwrap();
|
||||||
|
//
|
||||||
|
// db.send(database_manager::DeleteToken { token_id: token.id })
|
||||||
|
// .await
|
||||||
|
// .ok();
|
||||||
|
// }
|
||||||
|
// }
|
@ -1,8 +1,6 @@
|
|||||||
use actix::Message;
|
use db_utils::PgT;
|
||||||
use model::{AccountId, Audience, Token, TokenId};
|
use model::{AccountId, Audience, Token, TokenId};
|
||||||
|
|
||||||
use crate::{db_async_handler, Result};
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, thiserror::Error)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("Failed to save new token")]
|
#[error("Failed to save new token")]
|
||||||
@ -37,7 +35,7 @@ pub struct TokenByJti {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TokenByJti {
|
impl TokenByJti {
|
||||||
pub async fn run(self, t: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Result<Token> {
|
pub async fn run(self, t: &mut PgT<'_>) -> Result<Token> {
|
||||||
sqlx::query_as(r#"
|
sqlx::query_as(r#"
|
||||||
SELECT id, customer_id, role, issuer, subject, audience, expiration_time, not_before_time, issued_at_time, jwt_id
|
SELECT id, customer_id, role, issuer, subject, audience, expiration_time, not_before_time, issued_at_time, jwt_id
|
||||||
FROM tokens
|
FROM tokens
|
||||||
@ -62,7 +60,7 @@ pub struct CreateToken {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CreateToken {
|
impl CreateToken {
|
||||||
pub async fn run(self, t: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Result<Token> {
|
pub async fn run(self, t: &mut PgT<'_>) -> Result<Token> {
|
||||||
let CreateToken {
|
let CreateToken {
|
||||||
customer_id,
|
customer_id,
|
||||||
role,
|
role,
|
||||||
@ -97,7 +95,7 @@ pub struct CreateExtendedToken {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CreateExtendedToken {
|
impl CreateExtendedToken {
|
||||||
pub async fn run(self, t: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Result<Token> {
|
pub async fn run(self, t: &mut PgT<'_>) -> Result<Token> {
|
||||||
let CreateExtendedToken {
|
let CreateExtendedToken {
|
||||||
customer_id,
|
customer_id,
|
||||||
role,
|
role,
|
||||||
@ -130,7 +128,7 @@ pub struct DeleteToken {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DeleteToken {
|
impl DeleteToken {
|
||||||
pub async fn run(self, t: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Result<Option<Token>> {
|
pub async fn run(self, t: &mut PgT<'_>) -> Result<Option<Token>> {
|
||||||
sqlx::query_as(r#"
|
sqlx::query_as(r#"
|
||||||
DELETE FROM tokens
|
DELETE FROM tokens
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
|
@ -70,21 +70,9 @@
|
|||||||
//! }
|
//! }
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
|
mod actions;
|
||||||
mod db;
|
mod db;
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use channels::payments::CreatePayment;
|
|
||||||
use chrono::prelude::*;
|
|
||||||
use config::SharedAppConfig;
|
|
||||||
use hmac::digest::KeyInit;
|
|
||||||
use hmac::Hmac;
|
|
||||||
use model::{AccessTokenString, AccountId, Audience, Role, Token};
|
|
||||||
use sha2::Sha256;
|
|
||||||
|
|
||||||
use crate::db::Database;
|
|
||||||
|
|
||||||
/*struct Jwt {
|
/*struct Jwt {
|
||||||
/// cti (customer id): Customer uuid identifier used by payment service
|
/// cti (customer id): Customer uuid identifier used by payment service
|
||||||
pub cti: uuid::Uuid,
|
pub cti: uuid::Uuid,
|
||||||
@ -109,462 +97,16 @@ use crate::db::Database;
|
|||||||
pub jti: uuid::Uuid,
|
pub jti: uuid::Uuid,
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, serde::Serialize, thiserror::Error)]
|
#[derive(gumdrop::Options)]
|
||||||
#[serde(rename_all = "kebab-case", tag = "token")]
|
pub struct Opts {}
|
||||||
pub enum Error {
|
|
||||||
#[error("Unable to save new token")]
|
|
||||||
Save,
|
|
||||||
#[error("Unable to save new token. Can't connect to database")]
|
|
||||||
SaveInternal,
|
|
||||||
#[error("Unable to validate token")]
|
|
||||||
Validate,
|
|
||||||
#[error("Unable to validate token. Can't connect to database")]
|
|
||||||
ValidateInternal,
|
|
||||||
#[error("Token does not exists or some fields are incorrect")]
|
|
||||||
Invalid,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
|
||||||
|
|
||||||
pub struct TokenManager {
|
|
||||||
db: Database,
|
|
||||||
config: SharedAppConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TokenManager {
|
|
||||||
pub fn new(config: SharedAppConfig, db: Database) -> Self {
|
|
||||||
Self { db, config }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates single token, it's mostly used by [CreatePair]
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use actix::Addr;
|
|
||||||
/// use model::{AccountId, Role};
|
|
||||||
/// use token_manager::*;
|
|
||||||
/// async fn create_pair(token_manager: Addr<token_manager::TokenManager>) {
|
|
||||||
/// match token_manager.send(CreateToken { customer_id: uuid::Uuid::new_v4(), role: Role::Admin, subject: AccountId::from(1), audience: None, exp: None }).await {
|
|
||||||
/// Ok(Ok(pair)) => {}
|
|
||||||
/// Ok(Err(manager_error)) => {}
|
|
||||||
/// Err(actor_error) => {}
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct CreateToken {
|
|
||||||
pub customer_id: uuid::Uuid,
|
|
||||||
pub role: Role,
|
|
||||||
pub subject: AccountId,
|
|
||||||
pub audience: Option<Audience>,
|
|
||||||
pub exp: Option<NaiveDateTime>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CreateToken {
|
|
||||||
pub async fn run(
|
|
||||||
self,
|
|
||||||
db: Addr<Database>,
|
|
||||||
config: SharedAppConfig,
|
|
||||||
) -> Result<(Token, AccessTokenString)> {
|
|
||||||
let CreateToken {
|
|
||||||
customer_id,
|
|
||||||
role,
|
|
||||||
subject,
|
|
||||||
audience,
|
|
||||||
exp,
|
|
||||||
} = self;
|
|
||||||
|
|
||||||
let audience = audience.unwrap_or_default();
|
|
||||||
|
|
||||||
let token: Token = match exp {
|
|
||||||
None => query_db!(
|
|
||||||
db,
|
|
||||||
database_manager::CreateToken {
|
|
||||||
customer_id,
|
|
||||||
role,
|
|
||||||
subject,
|
|
||||||
audience,
|
|
||||||
},
|
|
||||||
Error::Save,
|
|
||||||
Error::SaveInternal
|
|
||||||
),
|
|
||||||
Some(exp) => query_db!(
|
|
||||||
db,
|
|
||||||
database_manager::CreateExtendedToken {
|
|
||||||
customer_id,
|
|
||||||
role,
|
|
||||||
subject,
|
|
||||||
audience,
|
|
||||||
expiration_time: exp
|
|
||||||
},
|
|
||||||
Error::Save,
|
|
||||||
Error::SaveInternal
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
let token_string = {
|
|
||||||
use jwt::SignWithKey;
|
|
||||||
|
|
||||||
let secret = config.lock().web().jwt_secret();
|
|
||||||
let key: Hmac<Sha256> = build_key(secret)?;
|
|
||||||
let mut claims = BTreeMap::new();
|
|
||||||
|
|
||||||
// cti (customer id): Customer uuid identifier used by payment service
|
|
||||||
claims.insert("cti", format!("{}", token.customer_id));
|
|
||||||
// arl (account role): account role
|
|
||||||
claims.insert("arl", String::from(token.role.as_str()));
|
|
||||||
// iss (issuer): Issuer of the JWT
|
|
||||||
claims.insert("iss", token.issuer.to_string());
|
|
||||||
// sub (subject): Subject of the JWT (the user)
|
|
||||||
claims.insert("sub", format!("{}", token.subject));
|
|
||||||
// aud (audience): Recipient for which the JWT is intended
|
|
||||||
claims.insert("aud", String::from(token.audience.as_str()));
|
|
||||||
// exp (expiration time): Time after which the JWT expires
|
|
||||||
claims.insert(
|
|
||||||
"exp",
|
|
||||||
format!(
|
|
||||||
"{}",
|
|
||||||
Utc.from_utc_datetime(&token.expiration_time).format("%+")
|
|
||||||
),
|
|
||||||
);
|
|
||||||
// nbt (not before time): Time before which the JWT must not be accepted
|
|
||||||
// for processing
|
|
||||||
claims.insert(
|
|
||||||
"nbt",
|
|
||||||
format!(
|
|
||||||
"{}",
|
|
||||||
Utc.from_utc_datetime(&token.not_before_time).format("%+")
|
|
||||||
),
|
|
||||||
);
|
|
||||||
// iat (issued at time): Time at which the JWT was issued; can be used
|
|
||||||
// to determine age of the JWT,
|
|
||||||
claims.insert(
|
|
||||||
"iat",
|
|
||||||
format!(
|
|
||||||
"{}",
|
|
||||||
Utc.from_utc_datetime(&token.issued_at_time).format("%+")
|
|
||||||
),
|
|
||||||
);
|
|
||||||
// jti (JWT ID): Unique identifier; can be used to prevent the JWT from
|
|
||||||
// being replayed (allows a token to be used only once)
|
|
||||||
claims.insert("jti", format!("{}", token.jwt_id));
|
|
||||||
|
|
||||||
let s = match claims.sign_with_key(&key) {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("{e:?}");
|
|
||||||
return Err(Error::SaveInternal);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
AccessTokenString::new(s)
|
|
||||||
};
|
|
||||||
Ok((token, token_string))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct AuthPair {
|
|
||||||
pub access_token: Token,
|
|
||||||
pub access_token_string: AccessTokenString,
|
|
||||||
pub _refresh_token: Token,
|
|
||||||
pub refresh_token_string: model::RefreshTokenString,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates access token and refresh token
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use actix::Addr;
|
|
||||||
/// use model::{AccountId, Role};
|
|
||||||
/// use token_manager::CreatePair;
|
|
||||||
/// async fn create_pair(token_manager: Addr<token_manager::TokenManager>) {
|
|
||||||
/// match token_manager.send(CreatePair { customer_id: uuid::Uuid::new_v4(), account_id: AccountId::from(0), role: Role::Admin }).await {
|
|
||||||
/// Ok(Ok(pair)) => {}
|
|
||||||
/// Ok(Err(manager_error)) => {}
|
|
||||||
/// Err(actor_error) => {}
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct CreatePair {
|
|
||||||
pub customer_id: uuid::Uuid,
|
|
||||||
pub role: Role,
|
|
||||||
pub account_id: AccountId,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CreatePair {
|
|
||||||
pub async fn run(self, db: Database, config: SharedAppConfig) -> Result<AuthPair> {
|
|
||||||
let (access_token, refresh_token) = tokio::join!(
|
|
||||||
create_token(
|
|
||||||
CreateToken {
|
|
||||||
customer_id: self.customer_id,
|
|
||||||
role: self.role,
|
|
||||||
subject: self.account_id,
|
|
||||||
audience: Some(model::Audience::Web),
|
|
||||||
exp: None
|
|
||||||
},
|
|
||||||
db.clone(),
|
|
||||||
config.clone()
|
|
||||||
),
|
|
||||||
create_token(
|
|
||||||
CreateToken {
|
|
||||||
customer_id: self.customer_id,
|
|
||||||
role: self.role,
|
|
||||||
subject: self.account_id,
|
|
||||||
audience: Some(model::Audience::Web),
|
|
||||||
exp: Some((chrono::Utc::now() + chrono::Duration::days(31)).naive_utc())
|
|
||||||
},
|
|
||||||
db.clone(),
|
|
||||||
config.clone()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
let (access_token, access_token_string): (Token, AccessTokenString) = access_token?;
|
|
||||||
let (refresh_token, refresh_token_string): (Token, AccessTokenString) = refresh_token?;
|
|
||||||
Ok(AuthPair {
|
|
||||||
access_token,
|
|
||||||
access_token_string,
|
|
||||||
_refresh_token: refresh_token,
|
|
||||||
refresh_token_string: refresh_token_string.into(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks if token is still valid
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use actix::Addr;
|
|
||||||
/// use model::{AccessTokenString, AccountId, Role};
|
|
||||||
/// use token_manager::{CreatePair, Validate};
|
|
||||||
/// async fn create_pair(token_manager: Addr<token_manager::TokenManager>, token: AccessTokenString) {
|
|
||||||
/// match token_manager.send(Validate { token }).await {
|
|
||||||
/// Ok(Ok(pair)) => {}
|
|
||||||
/// Ok(Err(manager_error)) => {}
|
|
||||||
/// Err(actor_error) => {}
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Validate {
|
|
||||||
pub token: AccessTokenString,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Validate {
|
|
||||||
pub async fn run(self, db: Database, config: SharedAppConfig) -> Result<Token> {
|
|
||||||
use jwt::VerifyWithKey;
|
|
||||||
|
|
||||||
tracing::info!("Validating token {:?}", self.token);
|
|
||||||
|
|
||||||
let secret = config.lock().web().jwt_secret();
|
|
||||||
let key: Hmac<Sha256> = build_key(secret)?;
|
|
||||||
let claims: BTreeMap<String, String> = match self.token.verify_with_key(&key) {
|
|
||||||
Ok(claims) => claims,
|
|
||||||
_ => return Err(Error::Validate),
|
|
||||||
};
|
|
||||||
let jti = match claims.get("jti") {
|
|
||||||
Some(jti) => jti,
|
|
||||||
_ => return Err(Error::Validate),
|
|
||||||
};
|
|
||||||
|
|
||||||
let token: Token = query_db!(
|
|
||||||
db,
|
|
||||||
database_manager::TokenByJti {
|
|
||||||
jti: match uuid::Uuid::from_str(jti) {
|
|
||||||
Ok(uid) => uid,
|
|
||||||
_ => return Err(Error::Validate),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Error::Validate,
|
|
||||||
Error::ValidateInternal
|
|
||||||
);
|
|
||||||
|
|
||||||
if token.expiration_time < Utc::now().naive_utc() {
|
|
||||||
return Err(Error::Validate);
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_pair(&claims, "cti", token.customer_id, validate_uuid)?;
|
|
||||||
validate_pair(&claims, "arl", token.role, eq)?;
|
|
||||||
validate_pair(&claims, "iss", &token.issuer, eq)?;
|
|
||||||
validate_pair(&claims, "sub", token.subject, validate_num)?;
|
|
||||||
validate_pair(&claims, "aud", token.audience, eq)?;
|
|
||||||
validate_pair(&claims, "exp", &token.expiration_time, validate_time)?;
|
|
||||||
validate_pair(&claims, "nbt", &token.not_before_time, validate_time)?;
|
|
||||||
validate_pair(&claims, "iat", &token.issued_at_time, validate_time)?;
|
|
||||||
|
|
||||||
tracing::info!("JWT token valid");
|
|
||||||
Ok(token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Refresh {}
|
|
||||||
|
|
||||||
impl Refresh {
|
|
||||||
pub async fn run(self, db: Database, config: SharedAppConfig) -> Result<AuthPair> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_key(secret: String) -> Result<Hmac<Sha256>> {
|
|
||||||
match Hmac::new_from_slice(secret.as_bytes()) {
|
|
||||||
Ok(key) => Ok(key),
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("{e:?}");
|
|
||||||
dbg!(e);
|
|
||||||
Err(Error::ValidateInternal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn validate_pair<F, V>(
|
|
||||||
claims: &BTreeMap<String, String>,
|
|
||||||
key: &str,
|
|
||||||
v: V,
|
|
||||||
cmp: F,
|
|
||||||
) -> std::result::Result<(), Error>
|
|
||||||
where
|
|
||||||
F: for<'s> FnOnce(V, &'s str) -> bool,
|
|
||||||
V: PartialEq,
|
|
||||||
{
|
|
||||||
claims
|
|
||||||
.get(key)
|
|
||||||
.map(|s| cmp(v, s.as_str()))
|
|
||||||
.unwrap_or_default()
|
|
||||||
.then_some(())
|
|
||||||
.ok_or(Error::Invalid)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn eq<V>(value: V, text: &str) -> bool
|
|
||||||
where
|
|
||||||
V: for<'s> PartialEq<&'s str>,
|
|
||||||
{
|
|
||||||
value == text
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn validate_time(left: &NaiveDateTime, right: &str) -> bool {
|
|
||||||
chrono::DateTime::parse_from_str(right, "%+")
|
|
||||||
.map(|t| t.naive_utc() == *left)
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn validate_num(left: i32, right: &str) -> bool {
|
|
||||||
right.parse::<i32>().map(|n| left == n).unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn validate_uuid(left: uuid::Uuid, right: &str) -> bool {
|
|
||||||
uuid::Uuid::from_str(right)
|
|
||||||
.map(|u| u == left)
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
// #[cfg(test)]
|
|
||||||
// mod tests {
|
|
||||||
// use actix::Actor;
|
|
||||||
// use config::UpdateConfig;
|
|
||||||
// use database_manager::Database;
|
|
||||||
// use model::*;
|
|
||||||
//
|
|
||||||
// use super::*;
|
|
||||||
//
|
|
||||||
// pub struct NoOpts;
|
|
||||||
//
|
|
||||||
// impl UpdateConfig for NoOpts {}
|
|
||||||
//
|
|
||||||
// #[actix::test]
|
|
||||||
// async fn create_token() {
|
|
||||||
// testx::db!(config, db);
|
|
||||||
// let db = db.start();
|
|
||||||
//
|
|
||||||
// let (token, _text) = super::create_token(
|
|
||||||
// CreateToken {
|
|
||||||
// customer_id: Default::default(),
|
|
||||||
// role: Role::Admin,
|
|
||||||
// subject: AccountId::from(1),
|
|
||||||
// audience: None,
|
|
||||||
// exp: None,
|
|
||||||
// },
|
|
||||||
// db.clone(),
|
|
||||||
// config,
|
|
||||||
// )
|
|
||||||
// .await
|
|
||||||
// .unwrap();
|
|
||||||
//
|
|
||||||
// db.send(database_manager::DeleteToken { token_id: token.id })
|
|
||||||
// .await
|
|
||||||
// .ok();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// #[actix::test]
|
|
||||||
// async fn create_pair() {
|
|
||||||
// testx::db!(config, db);
|
|
||||||
// let db = db.start();
|
|
||||||
//
|
|
||||||
// let AuthPair {
|
|
||||||
// access_token,
|
|
||||||
// access_token_string: _,
|
|
||||||
// refresh_token_string: _,
|
|
||||||
// _refresh_token,
|
|
||||||
// } = super::create_pair(
|
|
||||||
// CreatePair {
|
|
||||||
// customer_id: Default::default(),
|
|
||||||
// role: Role::Admin,
|
|
||||||
// account_id: AccountId::from(0),
|
|
||||||
// },
|
|
||||||
// db.clone(),
|
|
||||||
// config,
|
|
||||||
// )
|
|
||||||
// .await
|
|
||||||
// .unwrap();
|
|
||||||
//
|
|
||||||
// db.send(database_manager::DeleteToken {
|
|
||||||
// token_id: access_token.id,
|
|
||||||
// })
|
|
||||||
// .await
|
|
||||||
// .ok();
|
|
||||||
//
|
|
||||||
// db.send(database_manager::DeleteToken {
|
|
||||||
// token_id: _refresh_token.id,
|
|
||||||
// })
|
|
||||||
// .await
|
|
||||||
// .ok();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// #[actix::test]
|
|
||||||
// async fn validate() {
|
|
||||||
// testx::db!(config, db);
|
|
||||||
// let db = db.start();
|
|
||||||
//
|
|
||||||
// let (token, text) = super::create_token(
|
|
||||||
// CreateToken {
|
|
||||||
// customer_id: Default::default(),
|
|
||||||
// role: Role::Admin,
|
|
||||||
// subject: AccountId::from(1),
|
|
||||||
// audience: None,
|
|
||||||
// exp: None,
|
|
||||||
// },
|
|
||||||
// db.clone(),
|
|
||||||
// config.clone(),
|
|
||||||
// )
|
|
||||||
// .await
|
|
||||||
// .unwrap();
|
|
||||||
//
|
|
||||||
// super::validate(Validate { token: text }, db.clone(), config.clone())
|
|
||||||
// .await
|
|
||||||
// .unwrap();
|
|
||||||
//
|
|
||||||
// db.send(database_manager::DeleteToken { token_id: token.id })
|
|
||||||
// .await
|
|
||||||
// .ok();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {}
|
async fn main() {
|
||||||
|
config::init_tracing("payments");
|
||||||
|
|
||||||
|
let opts: Opts = gumdrop::parse_args_default_or_exit();
|
||||||
|
|
||||||
|
let config = config::default_load(&opts);
|
||||||
|
|
||||||
|
let _db = db::Database::build(config).await;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user