Files
Ferrous-Solitaire/solitaire_data/src/auth_tokens.rs
T
funman300 4b9d008be2 refactor(workspace): sweep low-risk clippy::pedantic findings
Conservative cleanup pass — applied only the high-signal pedantic
lints whose fixes either remove genuine waste or read more naturally,
skipping anything stylistic that would bloat the diff.

- map_unwrap_or: 29 .map(...).unwrap_or(...) sites collapsed to
  .map_or / .is_some_and / .map_or_else equivalents
- uninlined_format_args: 7 production format!/write!/println! sites
  rewritten to the inline-argument style; assert! sites in test code
  intentionally untouched
- match_same_arms: 2 redundant arms collapsed where the bodies were
  identical and the merger didn't obscure intent

Public API is unchanged. No dependencies added or removed. The
pedantic warning count dropped from 840 to 807 (-33). Out-of-scope
findings — needless_pass_by_value on Bevy Res params, false-positive
explicit_iter_loop on Bevy Query iterators, items_after_statements
inside test mods, and the "ask before changing" merge logic in
solitaire_sync — were intentionally deferred.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 02:46:32 +00:00

115 lines
4.3 KiB
Rust

//! Secure storage for JWT access and refresh tokens using the OS keychain.
//!
//! Tokens are stored under service name `"solitaire_quest_server"` with entry
//! keys `"{username}_access"` and `"{username}_refresh"`.
//!
//! On Linux this requires a running secret service (GNOME Keyring / KWallet).
//! If the keychain is unavailable, operations return
//! [`TokenError::KeychainUnavailable`] — callers should fall back to prompting
//! the user to log in again.
//!
//! Before calling any function in this module the application must initialise
//! the default keyring store exactly once at startup by calling
//! `keyring::use_native_store` (e.g. in `solitaire_app::main` before building
//! the Bevy `App`). If no default store is set, all operations in this module
//! will return [`TokenError::KeychainUnavailable`].
//!
//! # Note: no unit tests — requires live OS keychain.
use keyring_core::Entry;
use thiserror::Error;
/// Errors that can occur when reading or writing tokens in the OS keychain.
#[derive(Debug, Error)]
pub enum TokenError {
/// The OS keychain (secret service / keychain daemon) is not available.
#[error("keychain unavailable: {0}")]
KeychainUnavailable(String),
/// No token was found in the keychain for the given username.
#[error("token not found for user {0}")]
NotFound(String),
/// An unexpected keychain error occurred.
#[error("keychain error: {0}")]
Keyring(String),
}
/// Service name used to namespace all keychain entries for this application.
const SERVICE: &str = "solitaire_quest_server";
/// Map a `keyring_core::Error` to the appropriate `TokenError`.
fn map_keyring_err(err: keyring_core::Error, username: &str) -> TokenError {
let msg = err.to_string();
match err {
keyring_core::Error::NoStorageAccess(_) | keyring_core::Error::NoDefaultStore => {
TokenError::KeychainUnavailable(msg)
}
keyring_core::Error::NoEntry => TokenError::NotFound(username.to_string()),
_ => TokenError::Keyring(msg),
}
}
/// Store the access and refresh tokens for `username` in the OS keychain.
///
/// Any previously stored tokens for that username are overwritten.
pub fn store_tokens(
username: &str,
access_token: &str,
refresh_token: &str,
) -> Result<(), TokenError> {
Entry::new(SERVICE, &format!("{username}_access"))
.map_err(|e| map_keyring_err(e, username))?
.set_password(access_token)
.map_err(|e| map_keyring_err(e, username))?;
Entry::new(SERVICE, &format!("{username}_refresh"))
.map_err(|e| map_keyring_err(e, username))?
.set_password(refresh_token)
.map_err(|e| map_keyring_err(e, username))?;
Ok(())
}
/// Load the stored access token for `username` from the OS keychain.
///
/// Returns [`TokenError::NotFound`] if no token has been stored yet.
pub fn load_access_token(username: &str) -> Result<String, TokenError> {
Entry::new(SERVICE, &format!("{username}_access"))
.map_err(|e| map_keyring_err(e, username))?
.get_password()
.map_err(|e| map_keyring_err(e, username))
}
/// Load the stored refresh token for `username` from the OS keychain.
///
/// Returns [`TokenError::NotFound`] if no token has been stored yet.
pub fn load_refresh_token(username: &str) -> Result<String, TokenError> {
Entry::new(SERVICE, &format!("{username}_refresh"))
.map_err(|e| map_keyring_err(e, username))?
.get_password()
.map_err(|e| map_keyring_err(e, username))
}
/// Delete the stored access and refresh tokens for `username`.
///
/// Intended to be called on logout or account deletion. Missing entries are
/// silently ignored (the tokens are already gone, which is the desired state).
pub fn delete_tokens(username: &str) -> Result<(), TokenError> {
match Entry::new(SERVICE, &format!("{username}_access"))
.map_err(|e| map_keyring_err(e, username))?
.delete_credential()
{
Ok(()) | Err(keyring_core::Error::NoEntry) => {}
Err(e) => return Err(map_keyring_err(e, username)),
}
match Entry::new(SERVICE, &format!("{username}_refresh"))
.map_err(|e| map_keyring_err(e, username))?
.delete_credential()
{
Ok(()) | Err(keyring_core::Error::NoEntry) => {}
Err(e) => return Err(map_keyring_err(e, username)),
}
Ok(())
}