Skip to main content

game_manager_lib/sources/
providers.rs

1//! Este módulo define a estrutura `SourceGame` e a trait `GameSource`.
2//!
3//! - Representa jogos detectados de várias fontes (como Steam, Epic Games, GOG, etc.)
4//! - Define a interface que cada fonte deve implementar para buscar os jogos instalados.
5
6use crate::errors::AppError;
7use crate::utils::oauth::config::{
8    build_authorize_url, exchange_code_for_token, OAuthProviderConfig,
9};
10use crate::utils::oauth::core::{generate_state, wait_for_auth_code, PkceChallenge};
11use crate::utils::oauth::token_store::{
12    delete_oauth_token, get_valid_access_token, load_oauth_token, save_oauth_token,
13};
14use async_trait::async_trait;
15use tauri::AppHandle;
16use tauri_plugin_opener::OpenerExt;
17
18#[derive(Debug, Clone)]
19pub struct SourceGame {
20    pub platform: String,
21    pub platform_game_id: String,
22    pub name: Option<String>,
23    pub installed: bool,
24    pub executable_path: Option<String>,
25    pub install_path: Option<String>,
26    pub playtime_minutes: Option<u32>,
27    pub last_played: Option<i64>, // Unix timestamp
28}
29
30#[async_trait]
31pub trait GameSource {
32    async fn fetch_games(&self) -> Result<Vec<SourceGame>, AppError>;
33}
34
35/// Extensão de `GameSource` para fontes que dependem de login OAuth2 (biblioteca remota da conta do usuário).
36#[async_trait]
37pub trait OAuthGameSource: GameSource {
38    /// Configuração do provedor (endpoints, client_id, escopos, etc.).
39    fn oauth_config(&self) -> &OAuthProviderConfig;
40
41    /// Handle da aplicação, necessário para acessar o secrets.db.
42    fn app_handle(&self) -> &AppHandle;
43
44    /// Verifica se existe um token salvo para este provedor.
45    fn is_authenticated(&self) -> Result<bool, AppError> {
46        let token = load_oauth_token(self.app_handle(), self.oauth_config().provider_id)?;
47        Ok(token.is_some())
48    }
49
50    /// Retorna um access_token válido, renovando via refresh_token se necessário.
51    async fn ensure_valid_token(&self) -> Result<String, AppError> {
52        get_valid_access_token(self.app_handle(), self.oauth_config()).await
53    }
54
55    /// Executa o fluxo completo de login: abre o navegador, aguarda o callback local, valida o `state` (CSRF) e troca o `code` por um token, que é salvo automaticamente.
56    async fn login(&self) -> Result<(), AppError> {
57        let config = self.oauth_config();
58
59        let pkce = config.uses_pkce.then(PkceChallenge::generate);
60        let expected_state = generate_state();
61
62        let auth_url = build_authorize_url(config, pkce.as_ref(), &expected_state)?;
63
64        self.app_handle()
65            .opener()
66            .open_url(auth_url.as_str(), None::<String>)
67            .map_err(|e| AppError::OAuthConfigError(format!("Falha ao abrir navegador: {e}")))?;
68
69        let port = extract_port(&config.redirect_uri)?;
70
71        // wait_for_auth_code é bloqueante (usa thread + recv_timeout), então roda numa thread separada do runtime async.
72        let callback = tokio::task::spawn_blocking(move || wait_for_auth_code(port))
73            .await
74            .map_err(|e| AppError::OAuthConfigError(format!("Task de callback falhou: {e}")))?
75            .map_err(AppError::OAuthConfigError)?;
76
77        match &callback.state {
78            Some(state) if state == &expected_state => {}
79            _ => {
80                return Err(AppError::OAuthConfigError(
81                    "Parâmetro 'state' ausente ou não confere (possível CSRF)".into(),
82                ))
83            }
84        }
85
86        let verifier = pkce.as_ref().map(|p| p.verifier.as_str());
87        let token_response = exchange_code_for_token(config, &callback.code, verifier).await?;
88
89        save_oauth_token(
90            self.app_handle(),
91            config.provider_id,
92            &token_response.into(),
93        )?;
94
95        Ok(())
96    }
97
98    /// Remove o token salvo (logout).
99    fn logout(&self) -> Result<(), AppError> {
100        delete_oauth_token(self.app_handle(), self.oauth_config().provider_id)
101    }
102}
103
104/// Extrai a porta de um redirect_uri local (ex: "http://127.0.0.1:8901/callback" -> 8901).
105fn extract_port(redirect_uri: &str) -> Result<u16, AppError> {
106    url::Url::parse(redirect_uri)
107        .ok()
108        .and_then(|u| u.port())
109        .ok_or_else(|| {
110            AppError::OAuthConfigError(format!("redirect_uri sem porta válida: {redirect_uri}"))
111        })
112}