SSO auth (1)
This commit is contained in:
51
src-tauri/src/assets/auth-cancelled.html
Normal file
51
src-tauri/src/assets/auth-cancelled.html
Normal file
@@ -0,0 +1,51 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Friendolls Sign-in Cancelled</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background:
|
||||
radial-gradient(circle at top, #fff0cf, transparent 45%),
|
||||
linear-gradient(180deg, #fffdf7 0%, #f8f2e6 100%);
|
||||
font-family: "Avenir Next", "Segoe UI", sans-serif;
|
||||
color: #55411d;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: min(420px, calc(100vw - 32px));
|
||||
padding: 32px 28px;
|
||||
border-radius: 24px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
box-shadow: 0 24px 80px rgba(120, 95, 35, 0.14);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
color: #7a6237;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>Sign-in cancelled</h1>
|
||||
<p>You can close this tab and return to Friendolls to try again.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
51
src-tauri/src/assets/auth-failed.html
Normal file
51
src-tauri/src/assets/auth-failed.html
Normal file
@@ -0,0 +1,51 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Friendolls Sign-in Failed</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background:
|
||||
radial-gradient(circle at top, #ffd8d8, transparent 45%),
|
||||
linear-gradient(180deg, #fff8f8 0%, #fceeee 100%);
|
||||
font-family: "Avenir Next", "Segoe UI", sans-serif;
|
||||
color: #4d2323;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: min(420px, calc(100vw - 32px));
|
||||
padding: 32px 28px;
|
||||
border-radius: 24px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
box-shadow: 0 24px 80px rgba(135, 57, 57, 0.14);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
color: #7c4a4a;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>Sign-in failed</h1>
|
||||
<p>Friendolls could not complete the browser handshake. Return to the app and try again.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
51
src-tauri/src/assets/auth-success.html
Normal file
51
src-tauri/src/assets/auth-success.html
Normal file
@@ -0,0 +1,51 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Friendolls Sign-in Complete</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background:
|
||||
radial-gradient(circle at top, #d8f8ff, transparent 45%),
|
||||
linear-gradient(180deg, #f8feff 0%, #eef8fb 100%);
|
||||
font-family: "Avenir Next", "Segoe UI", sans-serif;
|
||||
color: #24414b;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: min(420px, calc(100vw - 32px));
|
||||
padding: 32px 28px;
|
||||
border-radius: 24px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 24px 80px rgba(55, 113, 130, 0.18);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
color: #4b6973;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>Signed in</h1>
|
||||
<p>You can close this browser tab and return to Friendolls.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -8,45 +8,16 @@ pub async fn logout_and_restart() -> Result<(), String> {
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn login(email: String, password: String) -> Result<(), String> {
|
||||
auth::login_and_init_session(&email, &password)
|
||||
pub async fn start_google_auth() -> Result<(), String> {
|
||||
auth::start_browser_login("google")
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn register(
|
||||
email: String,
|
||||
password: String,
|
||||
name: Option<String>,
|
||||
username: Option<String>,
|
||||
) -> Result<String, String> {
|
||||
auth::register(
|
||||
&email,
|
||||
&password,
|
||||
name.as_deref(),
|
||||
username.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn change_password(
|
||||
current_password: String,
|
||||
new_password: String,
|
||||
) -> Result<(), String> {
|
||||
auth::change_password(¤t_password, &new_password)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn reset_password(old_password: String, new_password: String) -> Result<(), String> {
|
||||
auth::reset_password(&old_password, &new_password)
|
||||
pub async fn start_discord_auth() -> Result<(), String> {
|
||||
auth::start_browser_login("discord")
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use commands::app_state::{
|
||||
get_active_doll_sprite_base64, get_app_data, get_friend_active_doll_sprites_base64,
|
||||
refresh_app_data,
|
||||
};
|
||||
use commands::auth::{change_password, login, logout_and_restart, register, reset_password};
|
||||
use commands::auth::{logout_and_restart, start_discord_auth, start_google_auth};
|
||||
use commands::config::{get_client_config, open_client_config, save_client_config};
|
||||
use commands::dolls::{
|
||||
create_doll, delete_doll, get_doll, get_dolls, remove_active_doll, set_active_doll, update_doll,
|
||||
@@ -27,9 +27,9 @@ use tauri::async_runtime;
|
||||
use tauri_specta::{collect_commands, collect_events, Builder as SpectaBuilder, ErrorHandlingMode};
|
||||
|
||||
use crate::services::app_events::{
|
||||
ActiveDollSpriteChanged, AppDataRefreshed, CreateDoll, CursorMoved, EditDoll,
|
||||
FriendActiveDollChanged, FriendActiveDollSpritesUpdated, FriendCursorPositionsUpdated,
|
||||
FriendDisconnected,
|
||||
ActiveDollSpriteChanged, AppDataRefreshed, AuthFlowUpdated, CreateDoll, CursorMoved,
|
||||
EditDoll, FriendActiveDollChanged, FriendActiveDollSpritesUpdated,
|
||||
FriendCursorPositionsUpdated, FriendDisconnected,
|
||||
FriendRequestAccepted, FriendRequestDenied, FriendRequestReceived, FriendUserStatusChanged,
|
||||
InteractionDeliveryFailed, InteractionReceived, SceneInteractiveChanged, SetInteractionOverlay,
|
||||
Unfriended, UserStatusChanged,
|
||||
@@ -99,10 +99,8 @@ pub fn run() {
|
||||
get_scene_interactive,
|
||||
set_scene_interactive,
|
||||
set_pet_menu_state,
|
||||
login,
|
||||
register,
|
||||
change_password,
|
||||
reset_password,
|
||||
start_google_auth,
|
||||
start_discord_auth,
|
||||
logout_and_restart,
|
||||
send_interaction_cmd,
|
||||
get_modules
|
||||
@@ -126,7 +124,8 @@ pub fn run() {
|
||||
FriendRequestReceived,
|
||||
FriendRequestAccepted,
|
||||
FriendRequestDenied,
|
||||
Unfriended
|
||||
Unfriended,
|
||||
AuthFlowUpdated
|
||||
]);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
|
||||
@@ -18,6 +18,23 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AuthFlowStatus {
|
||||
Started,
|
||||
Succeeded,
|
||||
Failed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AuthFlowUpdatedPayload {
|
||||
pub provider: String,
|
||||
pub status: AuthFlowStatus,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||
#[tauri_specta(event_name = "cursor-position")]
|
||||
pub struct CursorMoved(pub CursorPositions);
|
||||
@@ -93,3 +110,7 @@ pub struct FriendRequestDenied(pub FriendRequestDeniedPayload);
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||
#[tauri_specta(event_name = "unfriended")]
|
||||
pub struct Unfriended(pub UnfriendedPayload);
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||
#[tauri_specta(event_name = "auth-flow-updated")]
|
||||
pub struct AuthFlowUpdated(pub AuthFlowUpdatedPayload);
|
||||
|
||||
@@ -6,48 +6,46 @@ use crate::{lock_r, lock_w, state::FDOLL};
|
||||
use super::storage::{build_auth_pass, save_auth_pass, AuthError, AuthPass};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct LoginResponse {
|
||||
pub struct StartSsoResponse {
|
||||
pub state: String,
|
||||
#[serde(rename = "authorizeUrl")]
|
||||
pub authorize_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct TokenResponse {
|
||||
#[serde(rename = "accessToken")]
|
||||
access_token: String,
|
||||
#[serde(rename = "expiresIn")]
|
||||
expires_in: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RegisterResponse {
|
||||
id: String,
|
||||
#[serde(rename = "refreshToken")]
|
||||
refresh_token: String,
|
||||
#[serde(rename = "refreshExpiresIn")]
|
||||
refresh_expires_in: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct LoginRequest<'a> {
|
||||
email: &'a str,
|
||||
password: &'a str,
|
||||
struct StartSsoRequest<'a> {
|
||||
provider: &'a str,
|
||||
#[serde(rename = "redirectUri")]
|
||||
redirect_uri: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct RegisterRequest<'a> {
|
||||
email: &'a str,
|
||||
password: &'a str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
name: Option<&'a str>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
username: Option<&'a str>,
|
||||
struct ExchangeSsoCodeRequest<'a> {
|
||||
code: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ChangePasswordRequest<'a> {
|
||||
#[serde(rename = "currentPassword")]
|
||||
current_password: &'a str,
|
||||
#[serde(rename = "newPassword")]
|
||||
new_password: &'a str,
|
||||
struct RefreshTokenRequest<'a> {
|
||||
#[serde(rename = "refreshToken")]
|
||||
refresh_token: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ResetPasswordRequest<'a> {
|
||||
#[serde(rename = "oldPassword")]
|
||||
old_password: &'a str,
|
||||
#[serde(rename = "newPassword")]
|
||||
new_password: &'a str,
|
||||
struct LogoutRequest<'a> {
|
||||
#[serde(rename = "refreshToken")]
|
||||
refresh_token: &'a str,
|
||||
}
|
||||
|
||||
fn auth_http_context() -> Result<(String, reqwest::Client), AuthError> {
|
||||
@@ -87,96 +85,72 @@ pub async fn with_auth(request: reqwest::RequestBuilder) -> reqwest::RequestBuil
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn login(email: &str, password: &str) -> Result<AuthPass, AuthError> {
|
||||
pub async fn start_sso(provider: &str, redirect_uri: &str) -> Result<StartSsoResponse, AuthError> {
|
||||
let (base_url, http_client) = auth_http_context()?;
|
||||
let response = http_client
|
||||
.post(format!("{}/auth/login", base_url))
|
||||
.json(&LoginRequest { email, password })
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let login_response: LoginResponse = ensure_success(response).await?.json().await?;
|
||||
let auth_pass = build_auth_pass(login_response.access_token, login_response.expires_in)?;
|
||||
lock_w!(FDOLL).auth.auth_pass = Some(auth_pass.clone());
|
||||
save_auth_pass(&auth_pass)?;
|
||||
Ok(auth_pass)
|
||||
}
|
||||
|
||||
pub async fn register(
|
||||
email: &str,
|
||||
password: &str,
|
||||
name: Option<&str>,
|
||||
username: Option<&str>,
|
||||
) -> Result<String, AuthError> {
|
||||
let (base_url, http_client) = auth_http_context()?;
|
||||
let response = http_client
|
||||
.post(format!("{}/auth/register", base_url))
|
||||
.json(&RegisterRequest {
|
||||
email,
|
||||
password,
|
||||
name,
|
||||
username,
|
||||
.post(format!("{}/auth/sso/start", base_url))
|
||||
.json(&StartSsoRequest {
|
||||
provider,
|
||||
redirect_uri,
|
||||
})
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let register_response: RegisterResponse = ensure_success(response).await?.json().await?;
|
||||
Ok(register_response.id)
|
||||
ensure_success(response).await?.json().await.map_err(AuthError::from)
|
||||
}
|
||||
|
||||
pub async fn change_password(
|
||||
current_password: &str,
|
||||
new_password: &str,
|
||||
) -> Result<(), AuthError> {
|
||||
let (base_url, http_client) = auth_http_context()?;
|
||||
let response = with_auth(http_client.post(format!("{}/auth/change-password", base_url)).json(
|
||||
&ChangePasswordRequest {
|
||||
current_password,
|
||||
new_password,
|
||||
},
|
||||
))
|
||||
.await
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
ensure_success(response).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reset_password(old_password: &str, new_password: &str) -> Result<(), AuthError> {
|
||||
let (base_url, http_client) = auth_http_context()?;
|
||||
let response = with_auth(http_client.post(format!("{}/auth/reset-password", base_url)).json(
|
||||
&ResetPasswordRequest {
|
||||
old_password,
|
||||
new_password,
|
||||
},
|
||||
))
|
||||
.await
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
ensure_success(response).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn refresh_token(access_token: &str) -> Result<AuthPass, AuthError> {
|
||||
pub async fn exchange_sso_code(code: &str) -> Result<AuthPass, AuthError> {
|
||||
let (base_url, http_client) = auth_http_context()?;
|
||||
let response = http_client
|
||||
.post(format!("{}/auth/refresh", base_url))
|
||||
.header("Authorization", format!("Bearer {}", access_token))
|
||||
.post(format!("{}/auth/sso/exchange", base_url))
|
||||
.json(&ExchangeSsoCodeRequest { code })
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let error_text = response.text().await.unwrap_or_default();
|
||||
error!("Token refresh failed with status {}: {}", status, error_text);
|
||||
return Err(AuthError::RefreshFailed);
|
||||
}
|
||||
let token_response: TokenResponse = ensure_success(response).await?.json().await?;
|
||||
build_auth_pass(
|
||||
token_response.access_token,
|
||||
token_response.expires_in,
|
||||
token_response.refresh_token,
|
||||
token_response.refresh_expires_in,
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn refresh_token(refresh_token: &str) -> Result<AuthPass, AuthError> {
|
||||
let (base_url, http_client) = auth_http_context()?;
|
||||
let response = http_client
|
||||
.post(format!("{}/auth/refresh", base_url))
|
||||
.json(&RefreshTokenRequest { refresh_token })
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let token_response: TokenResponse = ensure_success(response).await?.json().await?;
|
||||
let auth_pass = build_auth_pass(
|
||||
token_response.access_token,
|
||||
token_response.expires_in,
|
||||
token_response.refresh_token,
|
||||
token_response.refresh_expires_in,
|
||||
)?;
|
||||
|
||||
let refresh_response: LoginResponse = response.json().await?;
|
||||
let auth_pass = build_auth_pass(refresh_response.access_token, refresh_response.expires_in)?;
|
||||
lock_w!(FDOLL).auth.auth_pass = Some(auth_pass.clone());
|
||||
save_auth_pass(&auth_pass)?;
|
||||
Ok(auth_pass)
|
||||
}
|
||||
|
||||
pub async fn logout_remote(refresh_token: &str) -> Result<(), AuthError> {
|
||||
let (base_url, http_client) = auth_http_context()?;
|
||||
let response = http_client
|
||||
.post(format!("{}/auth/logout", base_url))
|
||||
.json(&LogoutRequest { refresh_token })
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
ensure_success(response).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn persist_auth_pass(auth_pass: &AuthPass) -> Result<(), AuthError> {
|
||||
lock_w!(FDOLL).auth.auth_pass = Some(auth_pass.clone());
|
||||
save_auth_pass(auth_pass)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
387
src-tauri/src/services/auth/flow.rs
Normal file
387
src-tauri/src/services/auth/flow.rs
Normal file
@@ -0,0 +1,387 @@
|
||||
use tauri_specta::Event as _;
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::get_app_handle;
|
||||
use crate::services::app_events::{AuthFlowStatus, AuthFlowUpdated, AuthFlowUpdatedPayload};
|
||||
use crate::state::{begin_auth_flow, clear_auth_flow_state, is_auth_flow_active};
|
||||
use crate::{lock_r, state::FDOLL};
|
||||
|
||||
use super::api::{exchange_sso_code, persist_auth_pass, start_sso};
|
||||
use super::storage::AuthError;
|
||||
|
||||
static AUTH_SUCCESS_HTML: &str = include_str!("../../assets/auth-success.html");
|
||||
static AUTH_CANCELLED_HTML: &str = include_str!("../../assets/auth-cancelled.html");
|
||||
static AUTH_FAILED_HTML: &str = include_str!("../../assets/auth-failed.html");
|
||||
|
||||
pub struct OAuthCallbackParams {
|
||||
pub state: String,
|
||||
pub result: OAuthCallbackResult,
|
||||
}
|
||||
|
||||
pub enum OAuthCallbackResult {
|
||||
Code(String),
|
||||
Error { message: String, cancelled: bool },
|
||||
}
|
||||
|
||||
struct PendingOAuthCallback {
|
||||
stream: TcpStream,
|
||||
params: OAuthCallbackParams,
|
||||
}
|
||||
|
||||
pub async fn start_browser_auth_flow(provider: &str) -> Result<(), AuthError> {
|
||||
let (flow_id, cancel_token) = begin_auth_flow();
|
||||
|
||||
let bind_addr = "127.0.0.1:0";
|
||||
let std_listener = std::net::TcpListener::bind(bind_addr)
|
||||
.map_err(|e| AuthError::ServerBindError(e.to_string()))?;
|
||||
std_listener
|
||||
.set_nonblocking(true)
|
||||
.map_err(|e| AuthError::ServerBindError(e.to_string()))?;
|
||||
let local_addr = std_listener
|
||||
.local_addr()
|
||||
.map_err(|e| AuthError::ServerBindError(e.to_string()))?;
|
||||
|
||||
let redirect_uri = format!("http://127.0.0.1:{}/callback", local_addr.port());
|
||||
let start_response = match start_sso(provider, &redirect_uri).await {
|
||||
Ok(response) => response,
|
||||
Err(err) => {
|
||||
clear_auth_flow_state(flow_id);
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
|
||||
let listener = TcpListener::from_std(std_listener)
|
||||
.map_err(|e| AuthError::ServerBindError(e.to_string()))?;
|
||||
let expected_state = start_response.state.clone();
|
||||
let auth_url = match start_response.authorize_url.clone() {
|
||||
Some(authorize_url) => authorize_url,
|
||||
None => match build_authorize_url(provider, &start_response.state) {
|
||||
Ok(url) => url,
|
||||
Err(err) => {
|
||||
clear_auth_flow_state(flow_id);
|
||||
return Err(err);
|
||||
}
|
||||
},
|
||||
};
|
||||
let provider_name = provider.to_string();
|
||||
|
||||
if let Err(err) = get_app_handle().opener().open_url(auth_url, None::<&str>) {
|
||||
clear_auth_flow_state(flow_id);
|
||||
emit_auth_flow_event(
|
||||
provider,
|
||||
AuthFlowStatus::Failed,
|
||||
Some("Friendolls could not open your browser for sign-in.".to_string()),
|
||||
);
|
||||
return Err(err.into());
|
||||
}
|
||||
|
||||
emit_auth_flow_event(provider, AuthFlowStatus::Started, None);
|
||||
tauri::async_runtime::spawn(async move {
|
||||
match listen_for_callback(listener, cancel_token.clone()).await {
|
||||
Ok(mut callback) => {
|
||||
if !is_auth_flow_active(flow_id) {
|
||||
let _ = write_html_response(&mut callback.stream, AUTH_CANCELLED_HTML).await;
|
||||
return;
|
||||
}
|
||||
|
||||
if callback.params.state != expected_state {
|
||||
error!("SSO state mismatch");
|
||||
if let Err(err) = write_html_response(&mut callback.stream, AUTH_FAILED_HTML).await {
|
||||
warn!("Failed to write auth failure response: {}", err);
|
||||
}
|
||||
emit_auth_flow_event(
|
||||
&provider_name,
|
||||
AuthFlowStatus::Failed,
|
||||
Some("Sign-in verification failed. Please try again.".to_string()),
|
||||
);
|
||||
clear_auth_flow_state(flow_id);
|
||||
return;
|
||||
}
|
||||
|
||||
match callback.params.result {
|
||||
OAuthCallbackResult::Code(code) => {
|
||||
let auth_pass = match exchange_sso_code(&code).await {
|
||||
Ok(auth_pass) => auth_pass,
|
||||
Err(err) => {
|
||||
error!("Failed to exchange SSO code: {}", err);
|
||||
if let Err(write_err) =
|
||||
write_html_response(&mut callback.stream, AUTH_FAILED_HTML).await
|
||||
{
|
||||
warn!("Failed to write auth failure response: {}", write_err);
|
||||
}
|
||||
emit_auth_flow_event(
|
||||
&provider_name,
|
||||
AuthFlowStatus::Failed,
|
||||
Some(
|
||||
"Friendolls could not complete sign-in. Please try again."
|
||||
.to_string(),
|
||||
),
|
||||
);
|
||||
clear_auth_flow_state(flow_id);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if !is_auth_flow_active(flow_id) {
|
||||
let _ = write_html_response(&mut callback.stream, AUTH_CANCELLED_HTML).await;
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(err) = persist_auth_pass(&auth_pass) {
|
||||
error!("Failed to persist SSO auth pass: {}", err);
|
||||
if let Err(write_err) =
|
||||
write_html_response(&mut callback.stream, AUTH_FAILED_HTML).await
|
||||
{
|
||||
warn!("Failed to write auth failure response: {}", write_err);
|
||||
}
|
||||
emit_auth_flow_event(
|
||||
&provider_name,
|
||||
AuthFlowStatus::Failed,
|
||||
Some(
|
||||
"Friendolls could not complete sign-in. Please try again."
|
||||
.to_string(),
|
||||
),
|
||||
);
|
||||
clear_auth_flow_state(flow_id);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(err) = super::session::finish_login_session().await {
|
||||
error!("Failed to finalize desktop login session: {}", err);
|
||||
if let Err(write_err) =
|
||||
write_html_response(&mut callback.stream, AUTH_FAILED_HTML).await
|
||||
{
|
||||
warn!("Failed to write auth failure response: {}", write_err);
|
||||
}
|
||||
emit_auth_flow_event(
|
||||
&provider_name,
|
||||
AuthFlowStatus::Failed,
|
||||
Some(
|
||||
"Signed in, but Friendolls could not open your session."
|
||||
.to_string(),
|
||||
),
|
||||
);
|
||||
clear_auth_flow_state(flow_id);
|
||||
} else {
|
||||
if let Err(err) =
|
||||
write_html_response(&mut callback.stream, AUTH_SUCCESS_HTML).await
|
||||
{
|
||||
warn!("Failed to write auth success response: {}", err);
|
||||
}
|
||||
emit_auth_flow_event(&provider_name, AuthFlowStatus::Succeeded, None);
|
||||
clear_auth_flow_state(flow_id);
|
||||
}
|
||||
}
|
||||
OAuthCallbackResult::Error { message, cancelled } => {
|
||||
let response_html = if cancelled {
|
||||
AUTH_CANCELLED_HTML
|
||||
} else {
|
||||
AUTH_FAILED_HTML
|
||||
};
|
||||
if let Err(err) = write_html_response(&mut callback.stream, response_html).await {
|
||||
warn!("Failed to write auth callback response: {}", err);
|
||||
}
|
||||
emit_auth_flow_event(
|
||||
&provider_name,
|
||||
if cancelled {
|
||||
AuthFlowStatus::Cancelled
|
||||
} else {
|
||||
AuthFlowStatus::Failed
|
||||
},
|
||||
Some(message),
|
||||
);
|
||||
clear_auth_flow_state(flow_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(AuthError::Cancelled) => {
|
||||
info!("Auth flow cancelled");
|
||||
if is_auth_flow_active(flow_id) {
|
||||
emit_auth_flow_event(
|
||||
&provider_name,
|
||||
AuthFlowStatus::Cancelled,
|
||||
Some("Sign-in was cancelled.".to_string()),
|
||||
);
|
||||
clear_auth_flow_state(flow_id);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Auth callback listener failed: {}", err);
|
||||
if is_auth_flow_active(flow_id) {
|
||||
emit_auth_flow_event(
|
||||
&provider_name,
|
||||
AuthFlowStatus::Failed,
|
||||
Some(auth_flow_error_message(&err)),
|
||||
);
|
||||
clear_auth_flow_state(flow_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_authorize_url(provider: &str, state: &str) -> Result<String, AuthError> {
|
||||
let base_url = lock_r!(FDOLL)
|
||||
.app_config
|
||||
.api_base_url
|
||||
.clone()
|
||||
.ok_or(AuthError::InvalidConfig)?;
|
||||
|
||||
let mut parsed = url::Url::parse(&base_url)
|
||||
.map_err(|e| AuthError::RequestFailed(format!("Invalid API base URL: {}", e)))?;
|
||||
let existing_path = parsed.path().trim_end_matches('/');
|
||||
parsed.set_path(&format!("{}/auth/sso/{}", existing_path, provider));
|
||||
let query = url::form_urlencoded::Serializer::new(String::new())
|
||||
.append_pair("state", state)
|
||||
.finish();
|
||||
parsed.set_query(Some(&query));
|
||||
|
||||
Ok(parsed.to_string())
|
||||
}
|
||||
|
||||
async fn listen_for_callback(
|
||||
listener: TcpListener,
|
||||
cancel_token: CancellationToken,
|
||||
) -> Result<PendingOAuthCallback, AuthError> {
|
||||
let timeout = tokio::time::Duration::from_secs(300);
|
||||
let start = tokio::time::Instant::now();
|
||||
|
||||
loop {
|
||||
let elapsed = start.elapsed();
|
||||
if elapsed >= timeout {
|
||||
return Err(AuthError::CallbackTimeout);
|
||||
}
|
||||
|
||||
let remaining = timeout - elapsed;
|
||||
let accepted = tokio::select! {
|
||||
_ = cancel_token.cancelled() => return Err(AuthError::Cancelled),
|
||||
result = tokio::time::timeout(remaining, listener.accept()) => result,
|
||||
};
|
||||
|
||||
let (mut stream, _) = match accepted {
|
||||
Ok(Ok(value)) => value,
|
||||
Ok(Err(err)) => {
|
||||
warn!("Accept error in auth callback listener: {}", err);
|
||||
continue;
|
||||
}
|
||||
Err(_) => return Err(AuthError::CallbackTimeout),
|
||||
};
|
||||
|
||||
if let Some(params) = parse_callback(&mut stream).await? {
|
||||
return Ok(PendingOAuthCallback { stream, params });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn parse_callback(stream: &mut TcpStream) -> Result<Option<OAuthCallbackParams>, AuthError> {
|
||||
let mut buffer = [0; 4096];
|
||||
let bytes_read = match stream.read(&mut buffer).await {
|
||||
Ok(value) if value > 0 => value,
|
||||
_ => return Ok(None),
|
||||
};
|
||||
|
||||
let request = String::from_utf8_lossy(&buffer[..bytes_read]);
|
||||
let first_line = request.lines().next().unwrap_or_default();
|
||||
let mut parts = first_line.split_whitespace();
|
||||
|
||||
match (parts.next(), parts.next()) {
|
||||
(Some("GET"), Some(path)) if path.starts_with("/callback") => {
|
||||
let parsed = url::Url::parse(&format!("http://localhost{}", path))
|
||||
.map_err(|e| AuthError::RequestFailed(e.to_string()))?;
|
||||
let params: std::collections::HashMap<_, _> = parsed.query_pairs().into_owned().collect();
|
||||
|
||||
let state = params
|
||||
.get("state")
|
||||
.cloned()
|
||||
.ok_or_else(|| AuthError::MissingParameter("state".to_string()))?;
|
||||
if let Some(error_code) = params.get("error") {
|
||||
let message = oauth_error_message(error_code, params.get("error_description"));
|
||||
return Ok(Some(OAuthCallbackParams {
|
||||
state,
|
||||
result: OAuthCallbackResult::Error {
|
||||
message,
|
||||
cancelled: is_oauth_cancellation(error_code),
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
let code = params
|
||||
.get("code")
|
||||
.cloned()
|
||||
.ok_or_else(|| AuthError::MissingParameter("code".to_string()))?;
|
||||
|
||||
Ok(Some(OAuthCallbackParams {
|
||||
state,
|
||||
result: OAuthCallbackResult::Code(code),
|
||||
}))
|
||||
}
|
||||
(Some("GET"), Some("/health")) => {
|
||||
stream
|
||||
.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK")
|
||||
.await?;
|
||||
Ok(None)
|
||||
}
|
||||
_ => {
|
||||
stream
|
||||
.write_all(b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n")
|
||||
.await?;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_auth_flow_event(provider: &str, status: AuthFlowStatus, message: Option<String>) {
|
||||
if let Err(err) = AuthFlowUpdated(AuthFlowUpdatedPayload {
|
||||
provider: provider.to_string(),
|
||||
status,
|
||||
message,
|
||||
})
|
||||
.emit(get_app_handle())
|
||||
{
|
||||
warn!("Failed to emit auth flow event: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
fn auth_flow_error_message(err: &AuthError) -> String {
|
||||
match err {
|
||||
AuthError::Cancelled => "Sign-in was cancelled.".to_string(),
|
||||
AuthError::CallbackTimeout => "Sign-in timed out. Please try again.".to_string(),
|
||||
AuthError::MissingParameter(_) => {
|
||||
"Friendolls did not receive a complete sign-in response. Please try again.".to_string()
|
||||
}
|
||||
_ => "Friendolls could not complete sign-in. Please try again.".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn oauth_error_message(error_code: &str, description: Option<&String>) -> String {
|
||||
if let Some(description) = description.filter(|description| !description.is_empty()) {
|
||||
return description.clone();
|
||||
}
|
||||
|
||||
if is_oauth_cancellation(error_code) {
|
||||
"Sign-in was cancelled.".to_string()
|
||||
} else {
|
||||
"The sign-in provider reported an error. Please try again.".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn is_oauth_cancellation(error_code: &str) -> bool {
|
||||
matches!(error_code, "access_denied" | "user_cancelled" | "authorization_cancelled")
|
||||
}
|
||||
|
||||
async fn write_html_response(stream: &mut TcpStream, html: &str) -> Result<(), AuthError> {
|
||||
let response = format!(
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {}\r\n\r\n{}",
|
||||
html.len(),
|
||||
html
|
||||
);
|
||||
stream.write_all(response.as_bytes()).await?;
|
||||
stream.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
mod api;
|
||||
mod flow;
|
||||
mod session;
|
||||
mod storage;
|
||||
|
||||
pub use api::{change_password, refresh_token, register, reset_password, with_auth};
|
||||
pub use session::{get_access_token, get_session_token, login_and_init_session, logout_and_restart};
|
||||
pub use api::{refresh_token, with_auth};
|
||||
pub use session::{get_access_token, get_session_token, logout_and_restart, start_browser_login};
|
||||
pub use storage::{clear_auth_pass, load_auth_pass, AuthPass};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use tracing::info;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
use crate::get_app_handle;
|
||||
use crate::services::{scene::close_splash_window, session::construct_user_session, welcome::close_welcome_window};
|
||||
@@ -15,21 +16,33 @@ pub async fn get_access_token() -> Option<String> {
|
||||
get_session_token().await.map(|pass| pass.access_token)
|
||||
}
|
||||
|
||||
pub fn logout() -> Result<(), AuthError> {
|
||||
pub async fn logout() -> Result<(), AuthError> {
|
||||
info!("Logging out user");
|
||||
lock_w!(FDOLL).auth.auth_pass = None;
|
||||
let refresh_token = lock_w!(FDOLL)
|
||||
.auth
|
||||
.auth_pass
|
||||
.take()
|
||||
.and_then(|pass| pass.refresh_token);
|
||||
clear_auth_pass()?;
|
||||
|
||||
if let Some(refresh_token) = refresh_token {
|
||||
match timeout(Duration::from_secs(5), super::api::logout_remote(&refresh_token)).await {
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(err)) => info!("Failed to revoke refresh token on server: {}", err),
|
||||
Err(_) => info!("Timed out while revoking refresh token on server"),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn logout_and_restart() -> Result<(), AuthError> {
|
||||
logout()?;
|
||||
logout().await?;
|
||||
let app_handle = get_app_handle();
|
||||
app_handle.restart();
|
||||
}
|
||||
|
||||
pub async fn login_and_init_session(email: &str, password: &str) -> Result<(), AuthError> {
|
||||
super::api::login(email, password).await?;
|
||||
pub async fn finish_login_session() -> Result<(), AuthError> {
|
||||
close_welcome_window();
|
||||
tauri::async_runtime::spawn(async {
|
||||
construct_user_session().await;
|
||||
@@ -37,3 +50,7 @@ pub async fn login_and_init_session(email: &str, password: &str) -> Result<(), A
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn start_browser_login(provider: &str) -> Result<(), AuthError> {
|
||||
super::flow::start_browser_auth_flow(provider).await
|
||||
}
|
||||
|
||||
@@ -29,6 +29,21 @@ pub enum AuthError {
|
||||
#[error("Request failed: {0}")]
|
||||
RequestFailed(String),
|
||||
|
||||
#[error("Missing callback parameter: {0}")]
|
||||
MissingParameter(String),
|
||||
|
||||
#[error("Authentication flow cancelled")]
|
||||
Cancelled,
|
||||
|
||||
#[error("Callback timeout - no response received")]
|
||||
CallbackTimeout,
|
||||
|
||||
#[error("Server binding failed: {0}")]
|
||||
ServerBindError(String),
|
||||
|
||||
#[error("Failed to open auth portal: {0}")]
|
||||
OpenPortalFailed(#[from] tauri_plugin_opener::Error),
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
}
|
||||
@@ -37,12 +52,18 @@ pub enum AuthError {
|
||||
pub struct AuthPass {
|
||||
pub access_token: String,
|
||||
pub expires_in: u64,
|
||||
#[serde(default)]
|
||||
pub refresh_token: Option<String>,
|
||||
#[serde(default)]
|
||||
pub refresh_expires_in: Option<u64>,
|
||||
pub issued_at: Option<u64>,
|
||||
}
|
||||
|
||||
pub(crate) fn build_auth_pass(
|
||||
access_token: String,
|
||||
expires_in: u64,
|
||||
refresh_token: String,
|
||||
refresh_expires_in: u64,
|
||||
) -> Result<AuthPass, AuthError> {
|
||||
let issued_at = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
@@ -51,6 +72,8 @@ pub(crate) fn build_auth_pass(
|
||||
Ok(AuthPass {
|
||||
access_token,
|
||||
expires_in,
|
||||
refresh_token: Some(refresh_token),
|
||||
refresh_expires_in: Some(refresh_expires_in),
|
||||
issued_at: Some(issued_at),
|
||||
})
|
||||
}
|
||||
@@ -168,6 +191,10 @@ pub fn load_auth_pass() -> Result<Option<AuthPass>, AuthError> {
|
||||
}
|
||||
};
|
||||
|
||||
if auth_pass.refresh_token.is_none() || auth_pass.refresh_expires_in.is_none() {
|
||||
info!("Loaded legacy auth pass without refresh token support");
|
||||
}
|
||||
|
||||
Ok(Some(auth_pass))
|
||||
}
|
||||
|
||||
|
||||
@@ -11,15 +11,30 @@ use tracing::{error, info, warn};
|
||||
static REFRESH_LOCK: once_cell::sync::Lazy<Mutex<()>> =
|
||||
once_cell::sync::Lazy::new(|| Mutex::new(()));
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct OAuthFlowTracker {
|
||||
pub active_flow_id: u64,
|
||||
pub background_auth_token: Option<CancellationToken>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AuthState {
|
||||
pub auth_pass: Option<AuthPass>,
|
||||
pub background_refresh_token: Option<tokio_util::sync::CancellationToken>,
|
||||
pub oauth_flow: OAuthFlowTracker,
|
||||
pub background_refresh_token: Option<CancellationToken>,
|
||||
}
|
||||
|
||||
pub fn init_auth_state() -> AuthState {
|
||||
let auth_pass = match load_auth_pass() {
|
||||
Ok(pass) => pass,
|
||||
Ok(Some(pass)) if has_supported_auth_pass(&pass) => Some(pass),
|
||||
Ok(Some(_)) => {
|
||||
warn!("Discarding stored auth pass from unsupported auth format");
|
||||
if let Err(err) = clear_auth_pass() {
|
||||
error!("Failed to clear unsupported auth pass: {}", err);
|
||||
}
|
||||
None
|
||||
}
|
||||
Ok(None) => None,
|
||||
Err(e) => {
|
||||
warn!("Failed to load auth pass from keyring: {e}");
|
||||
None
|
||||
@@ -29,10 +44,50 @@ pub fn init_auth_state() -> AuthState {
|
||||
|
||||
AuthState {
|
||||
auth_pass,
|
||||
oauth_flow: OAuthFlowTracker::default(),
|
||||
background_refresh_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn has_supported_auth_pass(auth_pass: &AuthPass) -> bool {
|
||||
auth_pass.issued_at.is_some()
|
||||
&& auth_pass.refresh_token.is_some()
|
||||
&& auth_pass.refresh_expires_in.is_some()
|
||||
}
|
||||
|
||||
pub fn begin_auth_flow() -> (u64, CancellationToken) {
|
||||
let mut guard = lock_w!(FDOLL);
|
||||
if let Some(cancel_token) = guard.auth.oauth_flow.background_auth_token.take() {
|
||||
cancel_token.cancel();
|
||||
}
|
||||
|
||||
guard.auth.oauth_flow.active_flow_id = guard.auth.oauth_flow.active_flow_id.saturating_add(1);
|
||||
let flow_id = guard.auth.oauth_flow.active_flow_id;
|
||||
let cancel_token = CancellationToken::new();
|
||||
guard.auth.oauth_flow.background_auth_token = Some(cancel_token.clone());
|
||||
|
||||
(flow_id, cancel_token)
|
||||
}
|
||||
|
||||
pub fn clear_auth_flow_state(flow_id: u64) -> bool {
|
||||
let mut guard = lock_w!(FDOLL);
|
||||
if guard.auth.oauth_flow.active_flow_id != flow_id {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(cancel_token) = guard.auth.oauth_flow.background_auth_token.take() {
|
||||
cancel_token.cancel();
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn is_auth_flow_active(flow_id: u64) -> bool {
|
||||
let guard = lock_r!(FDOLL);
|
||||
guard.auth.oauth_flow.active_flow_id == flow_id
|
||||
&& guard.auth.oauth_flow.background_auth_token.is_some()
|
||||
}
|
||||
|
||||
/// Returns the auth pass object, including access token and metadata.
|
||||
/// Automatically refreshes if expired and clears session on refresh failure.
|
||||
pub async fn get_auth_pass_with_refresh() -> Option<AuthPass> {
|
||||
@@ -41,14 +96,22 @@ pub async fn get_auth_pass_with_refresh() -> Option<AuthPass> {
|
||||
|
||||
let Some(issued_at) = auth_pass.issued_at else {
|
||||
warn!("Auth pass missing issued_at timestamp, clearing");
|
||||
lock_w!(FDOLL).auth.auth_pass = None;
|
||||
clear_invalid_auth().await;
|
||||
return None;
|
||||
};
|
||||
|
||||
let current_time = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs();
|
||||
let expires_at = issued_at.saturating_add(auth_pass.expires_in);
|
||||
let expired = current_time >= expires_at;
|
||||
if !expired {
|
||||
let access_expires_at = issued_at.saturating_add(auth_pass.expires_in);
|
||||
let refresh_expires_at = auth_pass
|
||||
.refresh_expires_in
|
||||
.map(|refresh_expires_in| issued_at.saturating_add(refresh_expires_in));
|
||||
|
||||
if refresh_expires_at.is_some_and(|refresh_expires_at| current_time >= refresh_expires_at) {
|
||||
clear_expired_auth().await;
|
||||
return None;
|
||||
}
|
||||
|
||||
if current_time < access_expires_at {
|
||||
return Some(auth_pass);
|
||||
}
|
||||
|
||||
@@ -58,31 +121,54 @@ pub async fn get_auth_pass_with_refresh() -> Option<AuthPass> {
|
||||
let current_time = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs();
|
||||
let Some(issued_at) = auth_pass.issued_at else {
|
||||
warn!("Auth pass missing issued_at timestamp after refresh lock, clearing");
|
||||
lock_w!(FDOLL).auth.auth_pass = None;
|
||||
clear_invalid_auth().await;
|
||||
return None;
|
||||
};
|
||||
let expires_at = issued_at.saturating_add(auth_pass.expires_in);
|
||||
let expired = current_time >= expires_at;
|
||||
if !expired {
|
||||
|
||||
let access_expires_at = issued_at.saturating_add(auth_pass.expires_in);
|
||||
let refresh_expires_at = auth_pass
|
||||
.refresh_expires_in
|
||||
.map(|refresh_expires_in| issued_at.saturating_add(refresh_expires_in));
|
||||
|
||||
if refresh_expires_at.is_some_and(|refresh_expires_at| current_time >= refresh_expires_at) {
|
||||
clear_expired_auth().await;
|
||||
return None;
|
||||
}
|
||||
|
||||
if current_time < access_expires_at {
|
||||
return Some(auth_pass);
|
||||
}
|
||||
|
||||
info!("Access token expired, attempting refresh");
|
||||
match refresh_token(&auth_pass.access_token).await {
|
||||
let Some(refresh_token_value) = auth_pass.refresh_token.as_deref() else {
|
||||
warn!("Auth pass missing refresh token, clearing session");
|
||||
clear_invalid_auth().await;
|
||||
return None;
|
||||
};
|
||||
|
||||
match refresh_token(refresh_token_value).await {
|
||||
Ok(new_pass) => Some(new_pass),
|
||||
Err(e) => {
|
||||
error!("Failed to refresh token: {}", e);
|
||||
lock_w!(FDOLL).auth.auth_pass = None;
|
||||
if let Err(e) = clear_auth_pass() {
|
||||
error!("Failed to clear expired auth pass: {}", e);
|
||||
}
|
||||
destruct_user_session().await;
|
||||
open_welcome_window();
|
||||
clear_expired_auth().await;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn clear_expired_auth() {
|
||||
lock_w!(FDOLL).auth.auth_pass = None;
|
||||
if let Err(e) = clear_auth_pass() {
|
||||
error!("Failed to clear expired auth pass: {}", e);
|
||||
}
|
||||
destruct_user_session().await;
|
||||
open_welcome_window();
|
||||
}
|
||||
|
||||
async fn clear_invalid_auth() {
|
||||
clear_expired_auth().await;
|
||||
}
|
||||
|
||||
async fn refresh_if_expiring_soon() {
|
||||
let Some(auth_pass) = ({ lock_r!(FDOLL).auth.auth_pass.clone() }) else {
|
||||
return;
|
||||
@@ -98,6 +184,19 @@ async fn refresh_if_expiring_soon() {
|
||||
};
|
||||
|
||||
let access_expires_at = issued_at.saturating_add(auth_pass.expires_in);
|
||||
if current_time >= access_expires_at && auth_pass.refresh_token.is_none() {
|
||||
clear_expired_auth().await;
|
||||
return;
|
||||
}
|
||||
|
||||
let refresh_expires_at = auth_pass
|
||||
.refresh_expires_in
|
||||
.map(|refresh_expires_in| issued_at.saturating_add(refresh_expires_in));
|
||||
if refresh_expires_at.is_some_and(|refresh_expires_at| current_time >= refresh_expires_at) {
|
||||
clear_expired_auth().await;
|
||||
return;
|
||||
}
|
||||
|
||||
if access_expires_at.saturating_sub(current_time) >= 60 {
|
||||
return;
|
||||
}
|
||||
@@ -118,18 +217,30 @@ async fn refresh_if_expiring_soon() {
|
||||
};
|
||||
|
||||
let access_expires_at = latest_issued_at.saturating_add(latest_pass.expires_in);
|
||||
if current_time >= access_expires_at && latest_pass.refresh_token.is_none() {
|
||||
clear_expired_auth().await;
|
||||
return;
|
||||
}
|
||||
|
||||
let refresh_expires_at = latest_pass
|
||||
.refresh_expires_in
|
||||
.map(|refresh_expires_in| latest_issued_at.saturating_add(refresh_expires_in));
|
||||
if refresh_expires_at.is_some_and(|refresh_expires_at| current_time >= refresh_expires_at) {
|
||||
clear_expired_auth().await;
|
||||
return;
|
||||
}
|
||||
|
||||
if access_expires_at.saturating_sub(current_time) >= 60 {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(e) = refresh_token(&latest_pass.access_token).await {
|
||||
let Some(refresh_token_value) = latest_pass.refresh_token.as_deref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Err(e) = refresh_token(refresh_token_value).await {
|
||||
warn!("Background refresh failed: {}", e);
|
||||
lock_w!(FDOLL).auth.auth_pass = None;
|
||||
if let Err(e) = clear_auth_pass() {
|
||||
error!("Failed to clear auth pass after refresh failure: {}", e);
|
||||
}
|
||||
destruct_user_session().await;
|
||||
open_welcome_window();
|
||||
clear_expired_auth().await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user