flatland3 0.2.16

Flatland3 terminal play client and account CLI
Documentation
use std::path::PathBuf;

use serde::{Deserialize, Serialize};
use uuid::Uuid;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionConfig {
    pub api_base: String,
    pub session_token: String,
    /// Last character used for play — preferred over name matching on reconnect.
    #[serde(default)]
    pub last_character_id: Option<Uuid>,
}

impl SessionConfig {
    pub fn path() -> anyhow::Result<PathBuf> {
        let base = dirs::config_dir()
            .ok_or_else(|| anyhow::anyhow!("could not resolve config directory"))?;
        Ok(base.join("flatland").join("session.json"))
    }

    pub fn load() -> anyhow::Result<Self> {
        let path = Self::path()?;
        let bytes = std::fs::read(&path)
            .map_err(|_| anyhow::anyhow!("not logged in; run `flatland3 auth login`"))?;
        Ok(serde_json::from_slice(&bytes)?)
    }

    pub fn save(&self) -> anyhow::Result<()> {
        let path = Self::path()?;
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent)?;
        }
        std::fs::write(path, serde_json::to_vec_pretty(self)?)?;
        Ok(())
    }

    pub fn remember_character(&mut self, character_id: Uuid) -> anyhow::Result<()> {
        if self.last_character_id == Some(character_id) {
            return Ok(());
        }
        self.last_character_id = Some(character_id);
        self.save()
    }

    /// Remove the local session file (after logout or revoke).
    #[allow(dead_code)] // used by flatland3-gfx via the lib crate
    pub fn clear() -> anyhow::Result<()> {
        let path = Self::path()?;
        match std::fs::remove_file(&path) {
            Ok(()) => Ok(()),
            Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
            Err(err) => Err(err.into()),
        }
    }

    #[allow(dead_code)] // used via flatland_cli lib; bin also compiles this module
    pub fn exists() -> bool {
        Self::path().ok().map(|p| p.is_file()).unwrap_or(false)
    }
}

/// Last successful email/password login (survives Redis/session expiry).
///
/// Stored separately from [`SessionConfig`] so explicit logout can clear the
/// session token without wiping the form autofill. Passwords are local-only
/// convenience for single-player / LAN clients — not a substitute for a vault.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SavedLogin {
    pub email: String,
    pub password: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub api_base: Option<String>,
}

impl SavedLogin {
    pub fn path() -> anyhow::Result<PathBuf> {
        let base = dirs::config_dir()
            .ok_or_else(|| anyhow::anyhow!("could not resolve config directory"))?;
        Ok(base.join("flatland").join("login.json"))
    }

    #[allow(dead_code)] // used via flatland_cli lib; bin also compiles this module
    pub fn load() -> Option<Self> {
        let path = Self::path().ok()?;
        let bytes = std::fs::read(path).ok()?;
        serde_json::from_slice(&bytes).ok()
    }

    #[allow(dead_code)] // used via flatland_cli lib; bin also compiles this module
    pub fn save(&self) -> anyhow::Result<()> {
        let path = Self::path()?;
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent)?;
        }
        std::fs::write(&path, serde_json::to_vec_pretty(self)?)?;
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600));
        }
        Ok(())
    }

    #[allow(dead_code)]
    pub fn clear() -> anyhow::Result<()> {
        let path = Self::path()?;
        match std::fs::remove_file(&path) {
            Ok(()) => Ok(()),
            Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
            Err(err) => Err(err.into()),
        }
    }
}

pub fn default_api_base() -> String {
    flatland_client_lib::default_api_base_url()
}