api.rs (4682B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2026 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it under the 6 terms of the GNU Affero General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 TALER is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 12 13 You should have received a copy of the GNU Affero General Public License along with 14 TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 use std::sync::Arc; 18 19 use axum::{ 20 Json, Router, 21 extract::State, 22 response::{IntoResponse as _, NoContent}, 23 routing::{delete, get, post}, 24 }; 25 use jiff::Timestamp; 26 use serde::{Deserialize, Serialize}; 27 use sqlx::PgPool; 28 use taler_api::{ 29 error::{ApiResult, failure}, 30 extract::{Path, Req}, 31 }; 32 use taler_common::error_code::ErrorCode; 33 34 use crate::db; 35 36 pub struct RelayApi { 37 db: PgPool, 38 } 39 40 impl RelayApi { 41 pub fn new(db: PgPool) -> Self { 42 Self { db } 43 } 44 } 45 46 #[derive(Debug, Deserialize, Serialize)] 47 pub struct DeviceRegistrationRequest { 48 pub token: String, 49 } 50 51 #[derive(Debug, Deserialize, Serialize)] 52 pub struct DeviceUnregistrationRequest { 53 pub token: String, 54 } 55 56 #[derive(Debug, Deserialize, Serialize)] 57 pub struct ApnsRelayConfig<'a> { 58 pub name: &'a str, 59 pub version: &'a str, 60 pub implementation: Option<&'a str>, 61 } 62 63 pub fn router(state: Arc<RelayApi>) -> Router { 64 Router::new() 65 .route( 66 "/devices", 67 post( 68 async |State(state): State<Arc<RelayApi>>, 69 Req(DeviceRegistrationRequest { token}): Req<DeviceRegistrationRequest>| { 70 if token.is_empty() 71 || !token.len().is_multiple_of(2) 72 || !token.bytes().all(|b| b.is_ascii_hexdigit()) { 73 return Err(failure( 74 ErrorCode::GENERIC_PARAMETER_MALFORMED, 75 "token must be a nonempty, even-length ASCII hexadecimal string", 76 ) 77 .with_path("token")); 78 } 79 db::register(&state.db, &token, &Timestamp::now()).await?; 80 ApiResult::Ok(NoContent) 81 }, 82 ), 83 ) 84 .route( 85 "/devices/{token}", 86 delete( 87 async |State(state): State<Arc<RelayApi>>, Path(token): Path<String>| { 88 db::unregister(&state.db, &token, &Timestamp::now()).await?; 89 ApiResult::Ok(NoContent) 90 }, 91 ), 92 ) 93 .route( 94 "/config", 95 get(async || { 96 Json(ApnsRelayConfig { 97 name: "taler-apns-relay", 98 version: "0:0:0", 99 implementation: Some("urn:net:taler:specs:taler-apns-relay:taler-rust"), 100 }) 101 .into_response() 102 }), 103 ) 104 .with_state(state) 105 } 106 107 #[cfg(test)] 108 mod test { 109 use std::sync::Arc; 110 111 use sqlx::PgPool; 112 use taler_api::api::TalerRouter as _; 113 use taler_macros::db_test; 114 use taler_test_utils::{json, server::TestServer as _}; 115 116 use crate::{ 117 api::{ApnsRelayConfig, RelayApi, router}, 118 db::all_registrations, 119 }; 120 121 #[db_test] 122 async fn api(pool: PgPool) { 123 let api = Arc::new(RelayApi::new(pool.clone())); 124 let server = router(api).finalize(); 125 126 server 127 .get("/config") 128 .await 129 .assert_ok_json::<ApnsRelayConfig>(); 130 131 assert_eq!(all_registrations(&pool).await.unwrap(), &[""; 0]); 132 133 server 134 .post("/devices") 135 .json(json!({ 136 "token": "aa01" 137 })) 138 .await 139 .assert_no_content(); 140 server 141 .post("/devices") 142 .json(json!({ 143 "token": "aa01" 144 })) 145 .await 146 .assert_no_content(); 147 server 148 .post("/devices") 149 .json(json!({ 150 "token": "aa02" 151 })) 152 .await 153 .assert_no_content(); 154 155 assert_eq!(all_registrations(&pool).await.unwrap(), &["aa01", "aa02"]); 156 157 server.delete("/devices/aa01").await.assert_no_content(); 158 server.delete("/devices/aa03").await.assert_no_content(); 159 assert_eq!(all_registrations(&pool).await.unwrap(), &["aa02"]); 160 } 161 }