Game sync and live services for independent game developers (targeting itch.io)
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add Bearer token authentication to save endpoints

- All save endpoints now require Authorization: Bearer <itchio_token>
- AuthService::verify_token() validates tokens against itch.io API
- upload_save validates gamer_id in body matches authenticated user
- download_save/get_save_metadata/get_quota_status derive gamer_id from token
- Returns 401 Unauthorized for missing/invalid tokens
- Returns 403 Forbidden for gamer_id mismatch

+88 -28
+4 -3
api/src/api/mod.rs
··· 1 1 use poem_openapi::payload::Json; 2 2 use poem_openapi::{ApiResponse, Object}; 3 + use std::sync::Arc; 3 4 4 5 pub mod games; 5 6 pub mod saves; 6 7 7 - use crate::auth::GamerRecord; 8 + use crate::auth::{AuthService, GamerRecord}; 8 9 9 10 /// Tuple type alias combining all API endpoints 10 11 pub type Api = ( ··· 49 50 50 51 /// Auth endpoint 51 52 pub struct AuthEndpoint { 52 - service: crate::auth::AuthService, 53 + service: Arc<AuthService>, 53 54 } 54 55 55 56 impl AuthEndpoint { 56 - pub fn new(service: crate::auth::AuthService) -> Self { 57 + pub fn new(service: Arc<AuthService>) -> Self { 57 58 Self { service } 58 59 } 59 60 }
+79 -22
api/src/api/saves.rs
··· 1 1 use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; 2 - use poem_openapi::auth::Bearer; 3 2 use poem_openapi::param::Path; 4 3 use poem_openapi::payload::Json; 5 4 use poem_openapi::{ApiResponse, Object, OpenApi}; 6 5 use qbsdiff::{Bsdiff, Bspatch}; 7 6 use sha2::Digest; 7 + use std::sync::Arc; 8 8 use uuid::Uuid; 9 9 10 10 use crate::auth::AuthService; ··· 108 108 pub struct SavesEndpoint { 109 109 pool: DbPool, 110 110 storage: StorageProvider, 111 - auth_service: AuthService, 111 + auth_service: Arc<AuthService>, 112 112 } 113 113 114 114 impl SavesEndpoint { 115 - pub fn new(pool: DbPool, storage: StorageProvider, auth_service: AuthService) -> Self { 115 + pub fn new(pool: DbPool, storage: StorageProvider, auth_service: Arc<AuthService>) -> Self { 116 116 Self { 117 117 pool, 118 118 storage, ··· 144 144 #[oai(path = "/saves", method = "put")] 145 145 async fn upload_save( 146 146 &self, 147 - auth: poem_openapi::param::Header<Option<Bearer>>, 147 + auth: poem_openapi::param::Header<String>, 148 148 body: Json<UploadSaveRequest>, 149 149 ) -> UploadSaveResponse { 150 - // Extract and verify token 151 - let bearer = match auth.0 { 152 - Some(b) => b, 153 - None => { 154 - return UploadSaveResponse::Unauthorized(Json(Error { 155 - message: "Missing authorization header".to_string(), 156 - })); 157 - } 150 + // Extract Bearer token from Authorization header 151 + let token = if auth.0.starts_with("Bearer ") { 152 + &auth.0[7..] 153 + } else { 154 + return UploadSaveResponse::Unauthorized(Json(Error { 155 + message: "Missing authorization header".to_string(), 156 + })); 158 157 }; 159 158 160 - let authenticated_gamer = match self.auth_service.verify_token(&bearer.token).await { 159 + // Verify token 160 + let authenticated_gamer = match self.auth_service.verify_token(token).await { 161 161 Ok(Some(gamer)) => gamer, 162 162 Ok(None) | Err(_) => { 163 163 return UploadSaveResponse::Unauthorized(Json(Error { ··· 328 328 #[oai(path = "/saves", method = "get")] 329 329 async fn download_save( 330 330 &self, 331 - gamer_id: poem_openapi::param::Query<String>, 331 + auth: poem_openapi::param::Header<String>, 332 332 game_id: poem_openapi::param::Query<String>, 333 333 slot_id: poem_openapi::param::Query<String>, 334 334 version: poem_openapi::param::Query<Option<i32>>, 335 335 ) -> DownloadSaveResponse { 336 - let gamer_id = gamer_id.0; 336 + // Extract Bearer token from Authorization header 337 + let token = if auth.0.starts_with("Bearer ") { 338 + &auth.0[7..] 339 + } else { 340 + return DownloadSaveResponse::Unauthorized(Json(Error { 341 + message: "Missing authorization header".to_string(), 342 + })); 343 + }; 344 + 345 + // Verify token 346 + let authenticated_gamer = match self.auth_service.verify_token(token).await { 347 + Ok(Some(gamer)) => gamer, 348 + Ok(None) | Err(_) => { 349 + return DownloadSaveResponse::Unauthorized(Json(Error { 350 + message: "Invalid or expired token".to_string(), 351 + })); 352 + } 353 + }; 354 + 355 + let gamer_id = authenticated_gamer.id.clone(); 337 356 let game_id = game_id.0; 338 357 let slot_id = slot_id.0; 339 358 let target_version = version.0.unwrap_or(1); ··· 466 485 } 467 486 468 487 /// Get save metadata by path params 469 - #[oai(path = "/saves/:gamer_id/:game_id/:slot_id", method = "get")] 488 + #[oai(path = "/saves/:game_id/:slot_id", method = "get")] 470 489 async fn get_save_metadata( 471 490 &self, 472 - gamer_id: Path<String>, 491 + auth: poem_openapi::param::Header<String>, 473 492 game_id: Path<String>, 474 493 slot_id: Path<String>, 475 494 ) -> DownloadSaveResponse { 476 - let gamer_id = gamer_id.0.clone(); 495 + // Extract Bearer token from Authorization header 496 + let token = if auth.0.starts_with("Bearer ") { 497 + &auth.0[7..] 498 + } else { 499 + return DownloadSaveResponse::Unauthorized(Json(Error { 500 + message: "Missing authorization header".to_string(), 501 + })); 502 + }; 503 + 504 + // Verify token 505 + let authenticated_gamer = match self.auth_service.verify_token(token).await { 506 + Ok(Some(gamer)) => gamer, 507 + Ok(None) | Err(_) => { 508 + return DownloadSaveResponse::Unauthorized(Json(Error { 509 + message: "Invalid or expired token".to_string(), 510 + })); 511 + } 512 + }; 513 + 514 + let gamer_id = authenticated_gamer.id.clone(); 477 515 let game_id = game_id.0.clone(); 478 516 let slot_id = slot_id.0.clone(); 479 517 ··· 613 651 } 614 652 } 615 653 616 - /// Get quota status for a gamer 654 + /// Get quota status for the authenticated user 617 655 #[oai(path = "/saves/quota", method = "get")] 618 656 async fn get_quota_status( 619 657 &self, 620 - gamer_id: poem_openapi::param::Query<String>, 658 + auth: poem_openapi::param::Header<String>, 621 659 ) -> GetQuotaResponse { 622 - let gamer_id = gamer_id.0; 623 - 660 + // Extract Bearer token from Authorization header 661 + let token = if auth.0.starts_with("Bearer ") { 662 + &auth.0[7..] 663 + } else { 664 + return GetQuotaResponse::Unauthorized(Json(Error { 665 + message: "Missing authorization header".to_string(), 666 + })); 667 + }; 668 + 669 + // Verify token 670 + let authenticated_gamer = match self.auth_service.verify_token(token).await { 671 + Ok(Some(gamer)) => gamer, 672 + Ok(None) | Err(_) => { 673 + return GetQuotaResponse::Unauthorized(Json(Error { 674 + message: "Invalid or expired token".to_string(), 675 + })); 676 + } 677 + }; 678 + 679 + let gamer_id = authenticated_gamer.id; 680 + 624 681 // Get quota from database, or use default if not exists 625 682 let quota = get_quota(&self.pool, &gamer_id).await.ok().flatten().unwrap_or(crate::db::GamerQuota { 626 683 gamer_id: gamer_id.clone(),
+5 -3
api/src/main.rs
··· 1 + use std::sync::Arc; 2 + 1 3 use anyhow::Result; 2 4 use poem::middleware::Cors; 3 5 use poem::{EndpointExt, Route}; ··· 32 34 // Initialize storage 33 35 let storage = StorageProvider::new(&app_config.storage.provider, &app_config.storage).await?; 34 36 35 - // Initialize auth service 36 - let auth_service = AuthService::new(app_config.auth.clone(), db_pool.clone()); 37 + // Initialize auth service wrapped in Arc for sharing across endpoints 38 + let auth_service = Arc::new(AuthService::new(app_config.auth.clone(), db_pool.clone())); 37 39 38 40 // Create API - tuple of endpoints 39 41 let api = ( 40 42 api::games::GamesEndpoint::new(db_pool.clone()), 41 - api::saves::SavesEndpoint::new(db_pool.clone(), storage), 43 + api::saves::SavesEndpoint::new(db_pool.clone(), storage, auth_service.clone()), 42 44 api::AuthEndpoint::new(auth_service), 43 45 ); 44 46