Something went wrong. Try again.
Game sync and live services for independent game developers (targeting itch.io)
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180use anyhow::{anyhow, Result};use reqwest::Client;use serde::{Deserialize, Serialize};use sqlx::SqlitePool;
use crate::config::AuthConfig;
mod state;
use crate::auth::state::OAuthStateStore;
/// Itch.io user info response#[derive(Debug, Deserialize, Serialize)]pub struct ItchUser { pub id: i64, pub username: String, pub display_name: Option<String>, pub url: String,}
/// OOB authorization result#[derive(Debug, Serialize)]pub struct OobAuthorizationResult { pub authorization_url: String, pub state: String, pub expires_at: chrono::DateTime<chrono::Utc>,}
/// Token verification result#[derive(Debug, Serialize)]pub struct TokenVerificationResult { pub gamer: GamerRecord, pub scope: String,}
/// Record of a gamer in the system#[derive(Debug, Clone, Serialize, poem_openapi::Object)]pub struct GamerRecord { pub id: String, pub itchio_user_id: i64, pub username: String, pub access_token: String,}
/// Auth service for itch.io OAuth (OOB flow)pub struct AuthService { client: Client, config: AuthConfig, pool: SqlitePool, state_store: OAuthStateStore,}
impl AuthService { pub fn new(config: AuthConfig, pool: SqlitePool) -> Self { let state_store = OAuthStateStore::new(pool.clone()); Self { client: Client::new(), config, pool, state_store, } }
/// Generate OOB authorization URL for itch.io OAuth pub async fn get_authorization_url(&self) -> Result<OobAuthorizationResult> { if self.config.itchio_client_id.is_empty() { return Err(anyhow!("itch.io OAuth not configured")); }
let state = uuid::Uuid::new_v4().to_string(); let expires_at = chrono::Utc::now() + chrono::Duration::minutes(30);
self.state_store.create(&state, expires_at).await?;
let scope = if self.config.itchio_oauth_scope.is_empty() { "profile:me".to_string() } else { self.config.itchio_oauth_scope.clone() };
let params = [ ("client_id", self.config.itchio_client_id.as_str()), ("scope", &scope), ("redirect_uri", "urn:ietf:wg:oauth:2.0:oob"), ("response_type", "token"), ("state", &state), ];
let query_string = serde_urlencoded::to_string(¶ms) .map_err(|e| anyhow!("Failed to encode params: {}", e))?;
let authorization_url = format!("{}?{}", self.config.itchio_authorization_url, query_string);
Ok(OobAuthorizationResult { authorization_url, state, expires_at, }) }
/// Verify token from itch.io and create/link gamer pub async fn verify_and_link_token( &self, access_token: &str, state: &str, ) -> Result<TokenVerificationResult> { // Validate and consume the state (ensures single use, not expired) self.state_store.validate_and_consume(state).await?;
// Validate token by fetching user info let itch_user = self.get_itch_user(access_token).await?;
// Upsert gamer in database let gamer_id = self.upsert_gamer(&itch_user).await?;
Ok(TokenVerificationResult { gamer: GamerRecord { id: gamer_id, itchio_user_id: itch_user.id, username: itch_user.username.clone(), access_token: access_token.to_string(), }, scope: "profile:me".to_string(), }) }
/// Get user info from itch.io pub async fn get_itch_user(&self, access_token: &str) -> Result<ItchUser> { let response = self .client .get("https://api.itch.io/profile") .header("Authorization", format!("Bearer {}", access_token)) .send() .await? .json::<ItchUser>() .await?;
Ok(response) }
/// Register or update a gamer in the database async fn upsert_gamer(&self, itch_user: &ItchUser) -> Result<String> { let id = uuid::Uuid::new_v4().to_string();
sqlx::query( r#" INSERT INTO gamers (id, itchio_user_id, username) VALUES (?, ?, ?) ON CONFLICT (itchio_user_id) DO UPDATE SET username = excluded.username, updated_at = CURRENT_TIMESTAMP "#, ) .bind(&id) .bind(itch_user.id) .bind(&itch_user.username) .execute(&self.pool) .await?;
Ok(id) }
/// Verify an access token and return the gamer record (if valid) pub async fn verify_token(&self, access_token: &str) -> Result<Option<GamerRecord>> { let itch_user = match self.get_itch_user(access_token).await { Ok(user) => user, Err(_) => return Ok(None), };
let gamer_id = self.upsert_gamer(&itch_user).await?;
Ok(Some(GamerRecord { id: gamer_id, itchio_user_id: itch_user.id, username: itch_user.username, access_token: access_token.to_string(), })) }}