commit 8398693a23f3774478221940f2ad683da4512b90
parent 5c66545f2ce01c7eb25dbb091871334b406f2687
Author: Antoine A <>
Date: Sat, 2 May 2026 17:07:27 +0200
bank: more token API work
Diffstat:
10 files changed, 457 insertions(+), 147 deletions(-)
diff --git a/crates/libeufin-bank/src/api.rs b/crates/libeufin-bank/src/api.rs
@@ -35,13 +35,14 @@ pub mod test {
use axum::{
Router,
- http::{Method, header::AUTHORIZATION},
+ http::{Method, StatusCode, header::AUTHORIZATION},
};
use compact_str::CompactString;
use sqlx::{PgPool, Postgres, pool::PoolConnection};
use taler_api::api::TalerRouter;
use taler_common::{
config::Config,
+ error_code::ErrorCode,
types::{
amount::{Amount, Decimal},
payto::{IbanPayto, Payto},
@@ -116,7 +117,7 @@ pub mod test {
self.tokens.borrow()[username].clone()
}
- async fn token_auth_request(
+ async fn requesta(
&self,
method: Method,
path: &str,
@@ -125,48 +126,44 @@ pub mod test {
let username = username.unwrap_or_else(|| Self::extract_username(path));
let token = self.cached_token(username).await;
self.server
- .method(method, path)
+ .request(method, path)
.header(AUTHORIZATION, token)
}
pub async fn postpw(&self, path: &str) -> TestRequest {
- Self::pw_auth(self.server.method(Method::POST, path), None)
+ Self::pw_auth(self.server.request(Method::POST, path), None)
}
pub async fn geta(&self, path: &str) -> TestRequest {
- self.token_auth_request(Method::GET, path, None).await
+ self.requesta(Method::GET, path, None).await
}
pub async fn posta(&self, path: &str) -> TestRequest {
- self.token_auth_request(Method::POST, path, None).await
+ self.requesta(Method::POST, path, None).await
}
pub async fn patcha(&self, path: &str) -> TestRequest {
- self.token_auth_request(Method::PATCH, path, None).await
+ self.requesta(Method::PATCH, path, None).await
}
pub async fn deletea(&self, path: &str) -> TestRequest {
- self.token_auth_request(Method::DELETE, path, None).await
+ self.requesta(Method::DELETE, path, None).await
}
pub async fn get_admin(&self, path: &str) -> TestRequest {
- self.token_auth_request(Method::GET, path, Some("admin"))
- .await
+ self.requesta(Method::GET, path, Some("admin")).await
}
pub async fn post_admin(&self, path: &str) -> TestRequest {
- self.token_auth_request(Method::POST, path, Some("admin"))
- .await
+ self.requesta(Method::POST, path, Some("admin")).await
}
pub async fn patch_admin(&self, path: &str) -> TestRequest {
- self.token_auth_request(Method::PATCH, path, Some("admin"))
- .await
+ self.requesta(Method::PATCH, path, Some("admin")).await
}
pub async fn delete_admin(&self, path: &str) -> TestRequest {
- self.token_auth_request(Method::DELETE, path, Some("admin"))
- .await
+ self.requesta(Method::DELETE, path, Some("admin")).await
}
}
@@ -291,4 +288,55 @@ pub mod test {
tokens: RefCell::new(BTreeMap::new()),
}
}
+
+ pub enum Auth {
+ Admin,
+ Exchange,
+ User,
+ UserOnly,
+ }
+
+ pub async fn auth_routine(
+ ctx: &BankTestCtx,
+ method: Method,
+ path: &str,
+ auth: Auth,
+ optional: bool,
+ ) {
+ // Bad header
+ ctx.request(method.clone(), path)
+ .header(AUTHORIZATION, "WTF")
+ .await
+ .assert_error(ErrorCode::GENERIC_UNAUTHORIZED);
+
+ if !optional {
+ // No header
+ ctx.request(method.clone(), path).await.assert_error_status(
+ ErrorCode::GENERIC_PARAMETER_MISSING,
+ StatusCode::UNAUTHORIZED,
+ );
+
+ // Other account
+ ctx.requesta(method.clone(), path, Some("merchant"))
+ .await
+ .await
+ .assert_error(ErrorCode::GENERIC_FORBIDDEN);
+ }
+
+ match auth {
+ Auth::Admin | Auth::Exchange => {
+ ctx.requesta(method.clone(), path, Some("merchant"))
+ .await
+ .await
+ .assert_error(ErrorCode::GENERIC_FORBIDDEN);
+ }
+ Auth::User => {}
+ Auth::UserOnly => {
+ ctx.requesta(method.clone(), path, Some("admin"))
+ .await
+ .await
+ .assert_error(ErrorCode::GENERIC_FORBIDDEN);
+ }
+ }
+ }
}
diff --git a/crates/libeufin-bank/src/api/account.rs b/crates/libeufin-bank/src/api/account.rs
@@ -306,7 +306,7 @@ pub async fn create_admin_account(
// TODO is this secure enough ?
let pw = pw
.map(|it| it.to_owned())
- .unwrap_or_else(|| Base32::<32>::rand().to_string());
+ .unwrap_or_else(|| Base32::<32>::secure_rand().to_string());
let payto = match cfg.wire_method {
WireMethod::iban => LibeufinId::IBAN(rand_iban_payto().into_inner()),
diff --git a/crates/libeufin-bank/src/api/token.rs b/crates/libeufin-bank/src/api/token.rs
@@ -19,11 +19,21 @@
use std::{sync::Arc, time::Duration};
-use axum::{Json, Router, extract::State, routing::post};
+use axum::{
+ Json, Router,
+ extract::State,
+ response::{IntoResponse, NoContent},
+ routing::{delete, get, post},
+};
+use compact_str::CompactString;
use jiff::Timestamp;
use serde::{Deserialize, Serialize};
-use taler_api::{error::failure, extract::Req};
+use taler_api::{
+ error::{ApiResult, failure},
+ extract::{Path, Query, Req},
+};
use taler_common::{
+ api_params::PageParams,
error_code::ErrorCode::{self},
types::{
base32::Base32,
@@ -33,10 +43,14 @@ use taler_common::{
use crate::{
api::BankState,
- auth::{RefreshAuth, TOKEN_PREFIX, TokenScope},
- db::token::{TokenCreationResult, access, create},
+ auth::{Auth, TOKEN_PREFIX, Token, TokenScope, UserR, UserRW},
+ db::{
+ self,
+ token::{TokenCreationResult, access, create},
+ },
};
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TokenInfo {
pub creation_time: TalerTimestamp,
pub expiration: TalerTimestamp,
@@ -49,6 +63,11 @@ pub struct TokenInfo {
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct TokenInfos {
+ pub tokens: Vec<TokenInfo>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TokenRequest {
pub scope: TokenScope,
pub duration: Option<RelativeTime>,
@@ -61,66 +80,117 @@ pub struct TokenRequest {
pub struct TokenSuccessResponse {
pub access_token: String,
pub expiration: TalerTimestamp,
+ pub token_id: u64,
}
pub fn token_api() -> Router<Arc<BankState>> {
- Router::new().route(
- "/accounts/{username}/token",
- post(
- async |RefreshAuth { username, token }: RefreshAuth,
- State(state): State<Arc<BankState>>,
- Req(req): Req<TokenRequest>| {
- if let Some(token) = token {
- // This block checks permissions ONLY IF the call was authenticated with a token
- let token = access(&state.db, &token, &Timestamp::now()).await?;
- let Some(token) = token else {
- return Err(failure(
- ErrorCode::BANK_UNMANAGED_EXCEPTION,
- "Token used to auth not found in the database",
- ));
- };
- if !req.scope.logical().is_valid_scope(token.scope, true) {
- return Err(failure(
- ErrorCode::GENERIC_TOKEN_PERMISSION_INSUFFICIENT,
- "Impossible to refresh a token with a larger scope",
- ));
+ Router::new()
+ .route(
+ "/accounts/{username}/token",
+ post(
+ async |Auth {
+ username, token, ..
+ }: Auth<Token>,
+ State(state): State<Arc<BankState>>,
+ Req(req): Req<TokenRequest>| {
+ if let Some(token) = token {
+ // This block checks permissions ONLY IF the call was authenticated with a token
+ let token = access(&state.db, &token, &Timestamp::now()).await?;
+ let Some(token) = token else {
+ return Err(failure(
+ ErrorCode::BANK_UNMANAGED_EXCEPTION,
+ "Token used to auth not found in the database",
+ ));
+ };
+ if !req.scope.logical().is_valid_scope(token.scope, true) {
+ return Err(failure(
+ ErrorCode::GENERIC_TOKEN_PERMISSION_INSUFFICIENT,
+ "Impossible to refresh a token with a larger scope",
+ ));
+ }
}
- }
- // TODO secure random
- let new = Base32::<32>::rand();
- let creation = Timestamp::now();
- let expiration = match req
- .duration
- .unwrap_or(RelativeTime::Duration(Duration::from_hours(24)))
- {
- RelativeTime::Forever => TalerTimestamp::Never,
- RelativeTime::Duration(duration) => {
- TalerTimestamp::Timestamp(creation + duration)
+ let new = Base32::<32>::secure_rand();
+ let creation = Timestamp::now();
+ let expiration = match req
+ .duration
+ .unwrap_or(RelativeTime::Duration(Duration::from_hours(24)))
+ {
+ RelativeTime::Forever => TalerTimestamp::Never,
+ RelativeTime::Duration(duration) => TalerTimestamp::Timestamp(
+ creation.checked_add(duration).map_err(|e| {
+ failure(
+ ErrorCode::GENERIC_JSON_INVALID,
+ format_args!("Bad token duration: {}", e),
+ )
+ })?,
+ ),
+ };
+ match create(
+ &state.db,
+ &username,
+ new.as_ref(),
+ &creation,
+ &expiration,
+ &req.scope,
+ req.refreshable,
+ req.description.as_deref(),
+ false,
+ )
+ .await?
+ {
+ TokenCreationResult::Success(token_id) => Ok(Json(TokenSuccessResponse {
+ access_token: format!("{TOKEN_PREFIX}{new}"),
+ expiration,
+ token_id,
+ })),
+ TokenCreationResult::TanRequired => todo!(),
}
- };
- match create(
- &state.db,
- &username,
- new.as_ref(),
- &creation,
- &expiration,
- &req.scope,
- req.refreshable,
- req.description.as_deref(),
- false,
- )
- .await?
- {
- TokenCreationResult::Success => Ok(Json(TokenSuccessResponse {
- access_token: format!("{TOKEN_PREFIX}{new}"),
- expiration,
- })),
- TokenCreationResult::TanRequired => todo!(),
- }
- },
- ),
- )
+ },
+ )
+ .delete(
+ async |Auth { token, .. }: Auth<UserR>, State(state): State<Arc<BankState>>| {
+ if let Some(token) = token {
+ db::token::delete(&state.db, &token).await?;
+ }
+ ApiResult::Ok(NoContent)
+ },
+ ),
+ )
+ .route(
+ "/accounts/{username}/tokens/{id}",
+ delete(
+ async |Auth { username, .. }: Auth<UserRW>,
+ Path((_, id)): Path<(CompactString, u64)>,
+ State(state): State<Arc<BankState>>| {
+ if db::token::delete_by_id(&state.db, &username, id).await? {
+ ApiResult::Ok(NoContent)
+ } else {
+ ApiResult::Err(failure(
+ ErrorCode::BANK_TRANSACTION_NOT_FOUND,
+ format_args!("Token '{id}' not found"),
+ ))
+ }
+ },
+ ),
+ )
+ .route(
+ "/accounts/{username}/tokens",
+ get(
+ async |Auth { username, .. }: Auth<UserR>,
+ Query(params): Query<PageParams>,
+ State(state): State<Arc<BankState>>| {
+ let params = params.check()?;
+ let tokens =
+ db::token::page(&state.db, ¶ms, &username, &Timestamp::now()).await?;
+ if tokens.is_empty() {
+ ApiResult::Ok(NoContent.into_response())
+ } else {
+ ApiResult::Ok(Json(TokenInfos { tokens }).into_response())
+ }
+ },
+ ),
+ )
}
#[cfg(test)]
@@ -128,7 +198,7 @@ pub mod test {
use std::str::FromStr;
- use axum::http::header::AUTHORIZATION;
+ use axum::http::{Method, header::AUTHORIZATION};
use jiff::{SignedDuration, Timestamp};
use taler_common::{
error_code::ErrorCode,
@@ -137,16 +207,34 @@ pub mod test {
use taler_test_utils::{json, server::TestServer};
use crate::{
- api::{test::bank_setup_conf, token::TokenSuccessResponse},
+ api::{
+ test::{Auth, auth_routine, bank_setup_conf},
+ token::{TokenInfos, TokenSuccessResponse},
+ },
auth::TOKEN_PREFIX,
db::token::access,
};
#[tokio::test]
- async fn create() {
+ async fn create_and_delete() {
let ctx = bank_setup_conf("test.conf").await;
- // TODO auth routine
+ auth_routine(
+ &ctx,
+ Method::POST,
+ "/accounts/customer/token",
+ Auth::User,
+ true,
+ )
+ .await;
+ auth_routine(
+ &ctx,
+ Method::DELETE,
+ "/accounts/customer/tokens/1",
+ Auth::User,
+ false,
+ )
+ .await;
// Unknown account
ctx.post("/accounts/merchant/token")
@@ -184,8 +272,13 @@ pub mod test {
.await
.unwrap()
.unwrap();
- let lifetime = token.creation.duration_until(token.expiration);
- assert_eq!(lifetime, SignedDuration::from_hours(24));
+ match token.expiration {
+ TalerTimestamp::Never => unreachable!(),
+ TalerTimestamp::Timestamp(expiration) => {
+ let lifetime = token.creation.duration_until(expiration);
+ assert_eq!(lifetime, SignedDuration::from_hours(24));
+ }
+ }
// Check valid refresh scope
for (from_scope, to_scope) in [
@@ -287,5 +380,105 @@ pub mod test {
}))
.await
.assert_error(ErrorCode::GENERIC_JSON_INVALID);
+
+ // Delete current token
+ let res: TokenSuccessResponse = ctx
+ .postpw("/accounts/merchant/token")
+ .await
+ .json(json!({ "scope": "readonly" }))
+ .await
+ .assert_ok_json();
+ // Check OK
+ ctx.delete("/accounts/merchant/token")
+ .header(AUTHORIZATION, format!("Bearer {}", res.access_token))
+ .json(json!({ "scope": "readonly" }))
+ .await
+ .assert_no_content();
+ // Check token no longer work
+ ctx.delete("/accounts/merchant/token")
+ .header(AUTHORIZATION, format!("Bearer {}", res.access_token))
+ .json(json!({ "scope": "readonly" }))
+ .await
+ .assert_error(ErrorCode::GENERIC_TOKEN_UNKNOWN);
+
+ // Delete by id
+ let res: TokenSuccessResponse = ctx
+ .postpw("/accounts/merchant/token")
+ .await
+ .json(json!({ "scope": "readonly" }))
+ .await
+ .assert_ok_json();
+ // Wrong account
+ ctx.deletea(&format!("/accounts/customer/tokens/{}", res.token_id))
+ .await
+ .await
+ .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND);
+ // Check OK
+ ctx.deletea(&format!("/accounts/merchant/tokens/{}", res.token_id))
+ .await
+ .await
+ .assert_no_content();
+ ctx.deletea(&format!("/accounts/merchant/tokens/{}", res.token_id))
+ .await
+ .await
+ .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND);
+ // Check token no longer work
+ ctx.post("/accounts/merchant/token")
+ .header(AUTHORIZATION, format!("Bearer {}", res.access_token))
+ .json(json!({ "scope": "readonly" }))
+ .await
+ .assert_error(ErrorCode::GENERIC_TOKEN_UNKNOWN);
+ }
+
+ #[tokio::test]
+ async fn get() {
+ let ctx = bank_setup_conf("test.conf").await;
+ auth_routine(
+ &ctx,
+ Method::GET,
+ "/accounts/customer/tokens",
+ Auth::User,
+ false,
+ )
+ .await;
+
+ // Check OK
+ for account in ["merchant", "customer"] {
+ ctx.geta(&format!("/accounts/{account}/tokens"))
+ .await
+ .await
+ .assert_no_content();
+ }
+ for scope in ["readonly", "readwrite"] {
+ ctx.postpw("/accounts/merchant/token")
+ .await
+ .json(json!({ "scope": scope }))
+ .await
+ .assert_ok();
+ }
+ ctx.postpw("/accounts/customer/token")
+ .await
+ .json(json!({
+ "scope": "revenue",
+ "description": "description"
+ }))
+ .await
+ .assert_ok();
+ let res: TokenInfos = ctx
+ .geta("/accounts/merchant/tokens")
+ .await
+ .await
+ .assert_ok_json();
+ assert_eq!(res.tokens.len(), 2);
+ for token in res.tokens {
+ assert_eq!(token.description, None);
+ }
+ let res: TokenInfos = ctx
+ .geta("/accounts/customer/tokens")
+ .await
+ .await
+ .assert_ok_json();
+ assert_eq!(res.tokens.len(), 1);
+ assert_eq!(res.tokens[0].description.as_deref(), Some("description"));
}
}
diff --git a/crates/libeufin-bank/src/auth.rs b/crates/libeufin-bank/src/auth.rs
@@ -17,13 +17,12 @@
* <http://www.gnu.org/licenses/>
*/
-use std::{fmt::Display, sync::Arc};
+use std::{fmt::Display, marker::PhantomData, sync::Arc};
use axum::{
- RequestPartsExt as _,
- extract::{FromRequestParts, Path},
+ extract::FromRequestParts,
http::{
- HeaderValue,
+ HeaderMap, HeaderValue, StatusCode, Uri,
header::{AUTHORIZATION, WWW_AUTHENTICATE},
request::Parts,
},
@@ -32,10 +31,11 @@ use compact_str::CompactString;
use jiff::Timestamp;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
-use taler_api::error::{ApiError, ApiResult, failure, failure_code, unauthorized};
+use taler_api::error::{ApiError, ApiResult, failure, failure_code, failure_status, unauthorized};
use taler_common::{
encoding::{base32, base64},
error_code::ErrorCode,
+ types::timestamp::TalerTimestamp,
};
use tracing::warn;
@@ -132,10 +132,10 @@ impl FromRequestParts<Arc<BankState>> for RegistrationAuth {
TokenLogicalScope::readwrite,
false,
state.cfg.basic_auth_compat,
- parts,
+ &parts.headers,
)
.await?;
- if info.is_admin() {
+ if !info.is_admin() {
return Err(failure(
ErrorCode::GENERIC_FORBIDDEN,
"Only administrator allowed",
@@ -145,60 +145,112 @@ impl FromRequestParts<Arc<BankState>> for RegistrationAuth {
}
}
-pub struct RefreshAuth {
+pub enum AuthKind {
+ AdminOnly,
+ UserOnly,
+ UserOrAdmin,
+}
+
+pub trait AuthScope {
+ const SCOPE: TokenLogicalScope;
+ const KIND: AuthKind;
+ const ALLOW_BASIC_AUTH: bool = false;
+}
+
+pub struct Auth<S: AuthScope> {
pub username: CompactString,
+ pub info: BankInfo,
pub token: Option<Vec<u8>>,
+ scope: PhantomData<S>,
+}
+
+fn extract_username(url: &Uri) -> &str {
+ let mut iter = url.path().strip_prefix('/').unwrap().split('/');
+ assert_eq!(iter.next(), Some("accounts"));
+ iter.next().unwrap()
}
-impl FromRequestParts<Arc<BankState>> for RefreshAuth {
+impl<S: AuthScope> FromRequestParts<Arc<BankState>> for Auth<S> {
type Rejection = ApiError;
async fn from_request_parts(
parts: &mut Parts,
state: &Arc<BankState>,
) -> Result<Self, Self::Rejection> {
- let Path(username): Path<CompactString> = parts.extract().await?;
+ let username = extract_username(&parts.uri);
let (info, token) = auth_request(
&state.db,
&state.cfg.ctx,
&state.cfg.pw_crypto,
- TokenLogicalScope::refreshable,
- true,
+ S::SCOPE,
+ S::ALLOW_BASIC_AUTH || state.cfg.basic_auth_compat,
state.cfg.basic_auth_compat,
- parts,
+ &parts.headers,
)
.await?;
- if info.is_admin() {
- return Err(failure(
- ErrorCode::GENERIC_FORBIDDEN,
- "Only administrator allowed",
- ));
+
+ match S::KIND {
+ AuthKind::AdminOnly => {
+ if !info.is_admin() {
+ return Err(failure(
+ ErrorCode::GENERIC_FORBIDDEN,
+ "Only administrator allowed",
+ ));
+ }
+ }
+ AuthKind::UserOnly => {
+ if info.username != username {
+ return Err(failure(
+ ErrorCode::GENERIC_FORBIDDEN,
+ format_args!(
+ "Customer {} have no right on {username} account",
+ info.username
+ ),
+ ));
+ }
+ }
+ AuthKind::UserOrAdmin => {
+ if info.username != username && !info.is_admin() {
+ return Err(failure(
+ ErrorCode::GENERIC_FORBIDDEN,
+ format_args!(
+ "Customer {} have no right on {username} account",
+ info.username
+ ),
+ ));
+ }
+ }
}
- Ok(RefreshAuth { username, token })
+
+ Ok(Self {
+ username: username.into(),
+ info,
+ token,
+ scope: PhantomData,
+ })
}
}
-pub struct AdminRWAuth(BankInfo);
+pub struct UserRW;
-impl FromRequestParts<Arc<BankState>> for AdminRWAuth {
- type Rejection = ApiError;
+impl AuthScope for UserRW {
+ const SCOPE: TokenLogicalScope = TokenLogicalScope::readwrite;
+ const KIND: AuthKind = AuthKind::UserOrAdmin;
+}
- async fn from_request_parts(
- parts: &mut Parts,
- state: &Arc<BankState>,
- ) -> Result<Self, Self::Rejection> {
- let (info, _) = auth_request(
- &state.db,
- &state.cfg.ctx,
- &state.cfg.pw_crypto,
- TokenLogicalScope::readwrite,
- false,
- false,
- parts,
- )
- .await?;
- Ok(Self(info))
- }
+pub struct UserR;
+
+impl AuthScope for UserR {
+ const SCOPE: TokenLogicalScope = TokenLogicalScope::readonly;
+ const KIND: AuthKind = AuthKind::UserOrAdmin;
+}
+
+pub struct Token;
+
+impl AuthScope for Token {
+ const SCOPE: TokenLogicalScope = TokenLogicalScope::refreshable;
+ const KIND: AuthKind = AuthKind::UserOrAdmin;
+ const ALLOW_BASIC_AUTH: bool = true;
}
/**
@@ -215,17 +267,22 @@ async fn auth_request(
scope: TokenLogicalScope,
allow_pw: bool,
compat_pw: bool,
- parts: &Parts,
+ headers: &HeaderMap,
) -> ApiResult<(BankInfo, Option<Vec<u8>>)> {
fn headers_malformed(hint: impl Display) -> ApiError {
- ApiError::new(ErrorCode::GENERIC_HTTP_HEADERS_MALFORMED).with_hint(hint)
+ failure_status(
+ ErrorCode::GENERIC_HTTP_HEADERS_MALFORMED,
+ hint,
+ StatusCode::UNAUTHORIZED,
+ )
}
- let header = parts.headers.get(AUTHORIZATION);
+ let header = headers.get(AUTHORIZATION);
let Some(authorisation) = header else {
- let err = failure(
- ErrorCode::GENERIC_UNAUTHORIZED,
+ let err = failure_status(
+ ErrorCode::GENERIC_PARAMETER_MISSING,
"Authorization header not found",
+ StatusCode::UNAUTHORIZED,
);
if allow_pw || compat_pw {
return Err(err.with_header(
@@ -288,7 +345,9 @@ async fn auth_request(
return Err(failure_code(ErrorCode::GENERIC_TOKEN_UNKNOWN));
};
- if token.expiration < now {
+ if let TalerTimestamp::Timestamp(expiration) = token.expiration
+ && expiration < now
+ {
return Err(failure_code(ErrorCode::GENERIC_TOKEN_EXPIRED));
} else if !scope.is_valid_scope(token.scope, token.is_refreshable) {
return Err(failure_code(
diff --git a/crates/libeufin-bank/src/config.rs b/crates/libeufin-bank/src/config.rs
@@ -27,6 +27,7 @@ use taler_common::{
map_config,
types::amount::{Amount, Currency},
};
+use taler_macros::EnumMeta;
use tracing::warn;
use url::Url;
@@ -44,8 +45,9 @@ pub struct CurrencySpecification {
pub alt_unit_names: BTreeMap<CompactString, CompactString>,
}
-#[derive(Debug, Clone, Copy, PartialEq, Eq, taler_macros::EnumMeta)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumMeta)]
#[enum_meta(Str)]
+#[allow(non_camel_case_types)]
pub enum WireMethod {
iban,
x_taler_bank,
diff --git a/crates/libeufin-bank/src/db/token.rs b/crates/libeufin-bank/src/db/token.rs
@@ -26,7 +26,7 @@ use taler_api::{
db::{BindHelper, TypeHelper},
serialized,
};
-use taler_common::{api_common::ShortHashCode, api_params::Page, types::timestamp::TalerTimestamp};
+use taler_common::{api_params::Page, types::timestamp::TalerTimestamp};
use crate::{
api::token::TokenInfo,
@@ -39,12 +39,12 @@ pub struct BearerToken {
pub scope: TokenScope,
pub is_refreshable: bool,
pub creation: Timestamp,
- pub expiration: Timestamp,
+ pub expiration: TalerTimestamp,
}
/** Result status of token creation */
pub enum TokenCreationResult {
- Success,
+ Success(u64),
TanRequired,
}
@@ -62,11 +62,7 @@ pub async fn create(
) -> sqlx::Result<TokenCreationResult> {
serialized!(
sqlx::query(
- "
- SELECT out_tan_required FROM create_token(
- $1,$2,$3,$4,$5,$6,$7,$8
- )
- ",
+ "SELECT out_tan_required, out_token_id FROM create_token($1,$2,$3,$4,$5,$6,$7,$8)"
)
.bind(username)
.bind(content)
@@ -80,7 +76,7 @@ pub async fn create(
Ok(if r.try_get_flag("out_tan_required")? {
TokenCreationResult::TanRequired
} else {
- TokenCreationResult::Success
+ TokenCreationResult::Success(r.try_get_u64("out_token_id")?)
})
})
.fetch_one(db)
@@ -114,7 +110,7 @@ pub async fn access(
scope: r.try_get("scope")?,
is_refreshable: r.try_get("is_refreshable")?,
creation: r.try_get_timestamp("creation_time")?,
- expiration: r.try_get_timestamp("expiration_time")?,
+ expiration: r.try_get_taler_timestamp("expiration_time")?,
})
})
.fetch_optional(db)
@@ -159,7 +155,7 @@ pub async fn access_info(
scope: r.try_get("scope")?,
is_refreshable: r.try_get("is_refreshable")?,
creation: r.try_get_timestamp("creation_time")?,
- expiration: r.try_get_timestamp("expiration_time")?,
+ expiration: r.try_get_taler_timestamp("expiration_time")?,
},
BankInfo {
username: r.try_get("username")?,
@@ -185,11 +181,20 @@ pub async fn delete(db: &PgPool, token: &[u8]) -> sqlx::Result<bool> {
Ok(res.rows_affected() > 0)
}
-pub async fn delete_by_id(db: &PgPool, id: u64) -> sqlx::Result<bool> {
+pub async fn delete_by_id(db: &PgPool, username: &str, id: u64) -> sqlx::Result<bool> {
let res = serialized!(
- sqlx::query("DELETE FROM bearer_tokens WHERE bearer_token_id=$1")
- .bind(id as i64)
- .execute(db)
+ sqlx::query(
+ "
+ DELETE FROM bearer_tokens
+ USING customers
+ WHERE bearer_tokens.bank_customer = customers.customer_id
+ AND bearer_token_id = $1
+ AND username = $2
+ "
+ )
+ .bind(id as i64)
+ .bind(username)
+ .execute(db)
)?;
Ok(res.rows_affected() > 0)
}
diff --git a/crates/libeufin-bank/src/lib.rs b/crates/libeufin-bank/src/lib.rs
@@ -17,6 +17,7 @@
* <http://www.gnu.org/licenses/>
*/
+use serde::{Deserialize, Serialize};
use taler_common::config::parser::ConfigSource;
use taler_macros::EnumMeta;
@@ -43,11 +44,12 @@ pub const CONFIG_SOURCE: ConfigSource =
PartialOrd,
Ord,
EnumMeta,
- serde::Serialize,
- serde::Deserialize,
+ Serialize,
+ Deserialize,
)]
#[sqlx(type_name = "tan_enum")]
#[enum_meta(Str)]
+#[allow(non_camel_case_types)]
pub enum TanChannel {
sms,
email,
diff --git a/crates/libeufin-nexus/src/config.rs b/crates/libeufin-nexus/src/config.rs
@@ -31,7 +31,6 @@ use regex::Regex;
use taler_api::config::DbCfg;
use taler_common::{
config::{Config, ValueErr},
- map_config,
types::{
amount::{Amount, Currency},
payto::{BankID, FullIbanPayto},
@@ -100,6 +99,7 @@ impl NexusHostCfg {
#[derive(Debug, Clone, Copy, EnumMeta)]
#[enum_meta(Str)]
+#[allow(non_camel_case_types)]
pub enum AccountType {
exchange,
normal,
diff --git a/crates/libeufin-nexus/src/db.rs b/crates/libeufin-nexus/src/db.rs
@@ -15,7 +15,7 @@
*/
use jiff::Timestamp;
-use sqlx::{PgPool, Row, types::Json};
+use sqlx::{PgPool, types::Json};
use taler_api::db::BindHelper;
use taler_common::config::Config;
use tokio::sync::watch::Sender;
diff --git a/database-versioning/libeufin-bank-procedures.sql b/database-versioning/libeufin-bank-procedures.sql
@@ -236,7 +236,8 @@ CREATE FUNCTION create_token(
IN in_refreshable BOOLEAN,
IN in_description TEXT,
IN in_is_tan BOOLEAN,
- OUT out_tan_required BOOLEAN
+ OUT out_tan_required BOOLEAN,
+ OUT out_token_id INT8
)
LANGUAGE plpgsql AS $$
DECLARE
@@ -268,7 +269,7 @@ INSERT INTO bearer_tokens (
in_refreshable,
in_description,
in_creation_time
-);
+) RETURNING bearer_token_id INTO out_token_id;
END $$;
CREATE FUNCTION bank_wire_transfer(