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 }