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.

lock it in with some saves

+45 -2
+1
.gitignore
··· 1 1 .worktrees/ 2 + target/
+44 -2
api/src/api/saves.rs
··· 1 1 use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; 2 + use poem_openapi::auth::Bearer; 2 3 use poem_openapi::param::Path; 3 4 use poem_openapi::payload::Json; 4 5 use poem_openapi::{ApiResponse, Object, OpenApi}; ··· 6 7 use sha2::Digest; 7 8 use uuid::Uuid; 8 9 10 + use crate::auth::AuthService; 9 11 use crate::db::DbPool; 10 12 use crate::db::{check_quota, get_quota, increment_quota}; 11 13 use crate::storage::StorageProvider; ··· 72 74 Created(Json<SaveUploadResponse>), 73 75 #[oai(status = 400)] 74 76 BadRequest(Json<Error>), 77 + #[oai(status = 401)] 78 + Unauthorized(Json<Error>), 79 + #[oai(status = 403)] 80 + Forbidden(Json<Error>), 75 81 #[oai(status = 404)] 76 82 NotFound(Json<Error>), 77 83 #[oai(status = 413)] ··· 83 89 pub enum DownloadSaveResponse { 84 90 #[oai(status = 200)] 85 91 Ok(Json<SaveDownloadResponse>), 92 + #[oai(status = 401)] 93 + Unauthorized(Json<Error>), 86 94 #[oai(status = 404)] 87 95 NotFound(Json<Error>), 88 96 } ··· 92 100 pub enum GetQuotaResponse { 93 101 #[oai(status = 200)] 94 102 Ok(Json<QuotaResponse>), 103 + #[oai(status = 401)] 104 + Unauthorized(Json<Error>), 95 105 } 96 106 97 107 /// Save management endpoint 98 108 pub struct SavesEndpoint { 99 109 pool: DbPool, 100 110 storage: StorageProvider, 111 + auth_service: AuthService, 101 112 } 102 113 103 114 impl SavesEndpoint { 104 - pub fn new(pool: DbPool, storage: StorageProvider) -> Self { 105 - Self { pool, storage } 115 + pub fn new(pool: DbPool, storage: StorageProvider, auth_service: AuthService) -> Self { 116 + Self { 117 + pool, 118 + storage, 119 + auth_service, 120 + } 106 121 } 107 122 108 123 /// Generate a new CID (content identifier) ··· 129 144 #[oai(path = "/saves", method = "put")] 130 145 async fn upload_save( 131 146 &self, 147 + auth: poem_openapi::param::Header<Option<Bearer>>, 132 148 body: Json<UploadSaveRequest>, 133 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 + } 158 + }; 159 + 160 + let authenticated_gamer = match self.auth_service.verify_token(&bearer.token).await { 161 + Ok(Some(gamer)) => gamer, 162 + Ok(None) | Err(_) => { 163 + return UploadSaveResponse::Unauthorized(Json(Error { 164 + message: "Invalid or expired token".to_string(), 165 + })); 166 + } 167 + }; 168 + 134 169 let req = body; 170 + 171 + // Validate that body.gamer_id matches authenticated user 172 + if req.gamer_id != authenticated_gamer.id { 173 + return UploadSaveResponse::Forbidden(Json(Error { 174 + message: "Cannot modify another user's save".to_string(), 175 + })); 176 + } 135 177 136 178 let save_data = match Self::decode_data(&req.data) { 137 179 Ok(d) => d,