libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

token.rs (9058B)


      1 /*
      2 * This file is part of LibEuFin.
      3 * Copyright (C) 2026 Taler Systems S.A.
      4 
      5 * LibEuFin is free software; you can redistribute it and/or modify
      6 * it under the terms of the GNU Affero General Public License as
      7 * published by the Free Software Foundation; either version 3, or
      8 * (at your option) any later version.
      9 
     10 * LibEuFin is distributed in the hope that it will be useful, but
     11 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
     12 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
     13 * Public License for more details.
     14 
     15 * You should have received a copy of the GNU Affero General Public
     16 * License along with LibEuFin; see the file COPYING.  If not, see
     17 * <http://www.gnu.org/licenses/>
     18 */
     19 
     20 use jiff::Timestamp;
     21 use sqlx::{
     22     Arguments, PgPool, QueryBuilder, Row as _,
     23     postgres::{PgArguments, PgRow},
     24 };
     25 use taler_api::{
     26     db::{BindHelper, TypeHelper},
     27     serialized,
     28 };
     29 use taler_common::{api::params::Page, types::time::TalerTimestamp};
     30 
     31 use crate::{
     32     api::token::TokenInfo,
     33     auth::TokenScope,
     34     db::account::BankInfo,
     35     payto::{PaytoCtx, sql_bank_payto},
     36 };
     37 
     38 pub struct BearerToken {
     39     pub scope: TokenScope,
     40     pub is_refreshable: bool,
     41     pub creation: Timestamp,
     42     pub expiration: TalerTimestamp,
     43 }
     44 
     45 /** Result status of token creation */
     46 pub enum TokenCreationResult {
     47     Success(u64),
     48     TanRequired,
     49 }
     50 
     51 /** Create new token for [username] */
     52 pub async fn create(
     53     db: &PgPool,
     54     username: &str,
     55     content: &[u8],
     56     creation: &Timestamp,
     57     expiration: &TalerTimestamp,
     58     scope: &TokenScope,
     59     is_refreshable: bool,
     60     description: Option<&str>,
     61     is2fa: bool,
     62 ) -> sqlx::Result<TokenCreationResult> {
     63     serialized!(
     64         sqlx::query(
     65             "SELECT out_tan_required, out_token_id FROM create_token($1,$2,$3,$4,$5,$6,$7,$8)"
     66         )
     67         .bind(username)
     68         .bind(content)
     69         .bind_timestamp(creation)
     70         .bind(expiration)
     71         .bind(scope)
     72         .bind(is_refreshable)
     73         .bind(description)
     74         .bind(is2fa)
     75         .try_map(|r: PgRow| {
     76             Ok(if r.try_get_flag("out_tan_required")? {
     77                 TokenCreationResult::TanRequired
     78             } else {
     79                 TokenCreationResult::Success(r.try_get_u64("out_token_id")?)
     80             })
     81         })
     82         .fetch_one(db)
     83     )
     84 }
     85 
     86 /** Create new token for [username] */
     87 pub async fn refresh(
     88     db: &PgPool,
     89     username: &str,
     90     source: &[u8],
     91     content: &[u8],
     92     creation: &Timestamp,
     93     expiration: &TalerTimestamp,
     94     source_expiration_cap: &Timestamp,
     95     scope: &TokenScope,
     96     is_refreshable: bool,
     97     description: Option<&str>,
     98 ) -> sqlx::Result<TokenCreationResult> {
     99     serialized!(
    100         sqlx::query(
    101             "
    102             WITH source_token AS (
    103               UPDATE bearer_tokens
    104                  SET expiration_time=LEAST(expiration_time, $1)
    105                WHERE content=$2
    106                  AND bank_customer=(
    107                    SELECT customer_id FROM customers
    108                     WHERE username=$3 AND deleted_at IS NULL
    109                  )
    110               RETURNING bank_customer
    111             )
    112             INSERT INTO bearer_tokens (
    113             content, creation_time, expiration_time, scope, bank_customer,
    114             is_refreshable, description, last_access
    115             )
    116             SELECT $4, $5, $6, $7, bank_customer, $8, $9, $5
    117             FROM source_token
    118             RETURNING bearer_token_id
    119             "
    120         )
    121         .bind_timestamp(source_expiration_cap)
    122         .bind(source)
    123         .bind(username)
    124         .bind(content)
    125         .bind_timestamp(creation)
    126         .bind(expiration)
    127         .bind(scope)
    128         .bind(is_refreshable)
    129         .bind(description)
    130         .try_map(|r: PgRow| {
    131             Ok(TokenCreationResult::Success(
    132                 r.try_get_u64("bearer_token_id")?,
    133             ))
    134         })
    135         .fetch_one(db)
    136     )
    137 }
    138 
    139 /** Get info for [token] */
    140 pub async fn access(
    141     db: &PgPool,
    142     token: &[u8],
    143     access_time: &Timestamp,
    144 ) -> sqlx::Result<Option<BearerToken>> {
    145     serialized!(
    146         sqlx::query(
    147             "
    148         UPDATE bearer_tokens
    149             SET last_access=$1
    150         FROM customers
    151         WHERE bank_customer=customer_id AND content=$2 AND deleted_at IS NULL
    152         RETURNING
    153             creation_time,
    154             expiration_time,
    155             scope,
    156             is_refreshable
    157     ",
    158         )
    159         .bind_timestamp(access_time)
    160         .bind(token)
    161         .try_map(|r: PgRow| {
    162             Ok(BearerToken {
    163                 scope: r.try_get("scope")?,
    164                 is_refreshable: r.try_get("is_refreshable")?,
    165                 creation: r.try_get_timestamp("creation_time")?,
    166                 expiration: r.try_get("expiration_time")?,
    167             })
    168         })
    169         .fetch_optional(db)
    170     )
    171 }
    172 
    173 /** Get info for [token] and its associated bank account*/
    174 pub async fn access_info(
    175     db: &PgPool,
    176     ctx: &PaytoCtx,
    177     token: &[u8],
    178     access_time: &Timestamp,
    179 ) -> sqlx::Result<Option<(BearerToken, BankInfo)>> {
    180     serialized!(
    181         sqlx::query(
    182             "
    183         UPDATE bearer_tokens
    184             SET last_access=$1
    185         FROM customers
    186             JOIN bank_accounts ON customer_id=owning_customer_id
    187         WHERE bank_customer=customer_id AND content=$2 AND deleted_at IS NULL
    188         RETURNING
    189             creation_time,
    190             expiration_time,
    191             scope,
    192             is_refreshable,
    193             username,
    194             is_taler_exchange,
    195             is_public,
    196             bank_account_id,
    197             internal_payto,
    198             name,
    199             tan_channels,
    200             email,
    201             phone
    202     ",
    203         )
    204         .bind_timestamp(access_time)
    205         .bind(token)
    206         .try_map(|r: PgRow| {
    207             Ok((
    208                 BearerToken {
    209                     scope: r.try_get("scope")?,
    210                     is_refreshable: r.try_get("is_refreshable")?,
    211                     creation: r.try_get_timestamp("creation_time")?,
    212                     expiration: r.try_get("expiration_time")?,
    213                 },
    214                 BankInfo {
    215                     username: r.try_get("username")?,
    216                     payto: sql_bank_payto(&r, ctx, "internal_payto", "name")?,
    217                     bank_account_id: r.try_get_u64("bank_account_id")?,
    218                     is_exchange: r.try_get("is_taler_exchange")?,
    219                     is_public: r.try_get("is_public")?,
    220                     phone: r.try_get("phone")?,
    221                     email: r.try_get("email")?,
    222                     channels: r.try_get("tan_channels")?,
    223                 },
    224             ))
    225         })
    226         .fetch_optional(db)
    227     )
    228 }
    229 
    230 pub async fn delete(db: &PgPool, token: &[u8]) -> sqlx::Result<bool> {
    231     let res = serialized!(
    232         sqlx::query("DELETE FROM bearer_tokens WHERE content=$1")
    233             .bind(token)
    234             .execute(db)
    235     )?;
    236     Ok(res.rows_affected() > 0)
    237 }
    238 
    239 pub async fn delete_by_id(db: &PgPool, username: &str, id: u64) -> sqlx::Result<bool> {
    240     let res = serialized!(
    241         sqlx::query(
    242             "
    243         DELETE FROM bearer_tokens
    244         USING customers
    245         WHERE bearer_tokens.bank_customer = customers.customer_id
    246         AND bearer_token_id = $1
    247         AND username = $2
    248         "
    249         )
    250         .bind(id as i64)
    251         .bind(username)
    252         .execute(db)
    253     )?;
    254     Ok(res.rows_affected() > 0)
    255 }
    256 
    257 /** Get info for [token] and its associated bank account*/
    258 pub async fn page(
    259     db: &PgPool,
    260     params: &Page,
    261     username: &str,
    262     now: &Timestamp,
    263 ) -> sqlx::Result<Vec<TokenInfo>> {
    264     taler_api::db::page(
    265         db,
    266         params,
    267         "bearer_token_id",
    268         || {
    269             let mut args = PgArguments::default();
    270             args.add(now.as_microsecond()).unwrap();
    271             args.add(username).unwrap();
    272             QueryBuilder::with_arguments(
    273                 "
    274                     SELECT
    275                         creation_time,
    276                         expiration_time,
    277                         scope,
    278                         is_refreshable,
    279                         description,
    280                         last_access,
    281                         bearer_token_id
    282                         FROM bearer_tokens
    283                     WHERE
    284                         (expiration_time > $1 OR expiration_time IS NULL) AND
    285                         bank_customer=(SELECT customer_id FROM customers WHERE deleted_at IS NULL AND username = $2)
    286                     AND
    287                 ",
    288                 args
    289             )
    290         },
    291         |r: PgRow| {
    292             Ok(TokenInfo {
    293                 creation_time: r.try_get("creation_time")?,
    294                 expiration: r.try_get("expiration_time")?,
    295                 scope: r.try_get("scope")?,
    296                 refreshable: r.try_get("is_refreshable")?,
    297                 description: r.try_get("description")?,
    298                 last_access: r.try_get("last_access")?,
    299                 row_id: r.try_get_u64("bearer_token_id")?,
    300                 token_id: r.try_get_u64("bearer_token_id")?,
    301             })
    302         },
    303     ).await
    304 }