game_manager_lib/sources/
providers.rs1use 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>, }
29
30#[async_trait]
31pub trait GameSource {
32 async fn fetch_games(&self) -> Result<Vec<SourceGame>, AppError>;
33}
34
35#[async_trait]
37pub trait OAuthGameSource: GameSource {
38 fn oauth_config(&self) -> &OAuthProviderConfig;
40
41 fn app_handle(&self) -> &AppHandle;
43
44 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 async fn ensure_valid_token(&self) -> Result<String, AppError> {
52 get_valid_access_token(self.app_handle(), self.oauth_config()).await
53 }
54
55 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 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 fn logout(&self) -> Result<(), AppError> {
100 delete_oauth_token(self.app_handle(), self.oauth_config().provider_id)
101 }
102}
103
104fn 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}