flatland_cli/
session_auth.rs1use crate::api::ControlPlaneClient;
4use crate::auth_context::{credentials_available, load_play_auth, PlayAuthKind};
5use crate::config::{default_api_base, SavedLogin, SessionConfig};
6
7#[derive(serde::Deserialize)]
8struct LoginData {
9 session_token: String,
10}
11
12pub async fn login(email: &str, password: &str, api: &str) -> anyhow::Result<()> {
14 if email.trim().is_empty() || password.is_empty() {
15 anyhow::bail!("email and password are required");
16 }
17 let client = ControlPlaneClient::new(api);
18 let data: LoginData = client
19 .post(
20 "/v1/auth/login",
21 &serde_json::json!({ "email": email.trim(), "password": password }),
22 None,
23 )
24 .await?;
25 let last_character_id = SessionConfig::load().ok().and_then(|s| s.last_character_id);
27 let api_base = api.trim_end_matches('/').to_string();
28 let config = SessionConfig {
29 api_base: api_base.clone(),
30 session_token: data.session_token,
31 last_character_id,
32 };
33 config.save()?;
34 SavedLogin {
35 email: email.trim().to_string(),
36 password: password.to_string(),
37 api_base: Some(api_base),
38 }
39 .save()?;
40 Ok(())
41}
42
43pub async fn register(
45 email: &str,
46 password: &str,
47 display_name: &str,
48 api: &str,
49) -> anyhow::Result<()> {
50 if email.trim().is_empty() || password.is_empty() || display_name.trim().is_empty() {
51 anyhow::bail!("email, password, and display name are required");
52 }
53 let client = ControlPlaneClient::new(api);
54 let _: serde_json::Value = client
55 .post(
56 "/v1/auth/register",
57 &serde_json::json!({
58 "email": email.trim(),
59 "password": password,
60 "display_name": display_name.trim(),
61 }),
62 None,
63 )
64 .await?;
65 login(email, password, api).await
66}
67
68pub async fn login_default(email: &str, password: &str) -> anyhow::Result<()> {
70 let api = SavedLogin::load()
71 .and_then(|s| s.api_base)
72 .unwrap_or_else(default_api_base);
73 login(email, password, &api).await
74}
75
76pub async fn register_default(
78 email: &str,
79 password: &str,
80 display_name: &str,
81) -> anyhow::Result<()> {
82 let api = SavedLogin::load()
83 .and_then(|s| s.api_base)
84 .unwrap_or_else(default_api_base);
85 register(email, password, display_name, &api).await
86}
87
88pub fn has_api_token() -> bool {
90 std::env::var("FLATLAND_API_TOKEN")
91 .map(|t| !t.is_empty())
92 .unwrap_or(false)
93}
94
95pub async fn session_is_valid() -> bool {
97 if has_api_token() {
98 return true;
99 }
100 if !credentials_available() {
101 return false;
102 }
103 let Ok(auth) = load_play_auth() else {
104 return false;
105 };
106 if auth.kind == PlayAuthKind::ApiToken {
107 return true;
108 }
109 let client = ControlPlaneClient::new(&auth.api_base);
110 client
111 .get::<serde_json::Value>("/v1/me", &auth.bearer)
112 .await
113 .is_ok()
114}
115
116pub async fn try_restore_expired_session() -> bool {
119 if session_is_valid().await {
120 return true;
121 }
122 if !SessionConfig::exists() {
123 return false;
124 }
125 let Some(saved) = SavedLogin::load() else {
126 return false;
127 };
128 if saved.email.trim().is_empty() || saved.password.is_empty() {
129 return false;
130 }
131 let api = saved
132 .api_base
133 .clone()
134 .unwrap_or_else(default_api_base);
135 login(&saved.email, &saved.password, &api).await.is_ok()
136}
137
138pub async fn needs_interactive_login() -> bool {
140 if has_api_token() {
141 return false;
142 }
143 if session_is_valid().await {
144 return false;
145 }
146 if try_restore_expired_session().await {
148 return false;
149 }
150 true
151}
152
153pub async fn logout() -> anyhow::Result<()> {
156 if let Ok(session) = SessionConfig::load() {
157 let client = ControlPlaneClient::new(&session.api_base);
158 let _ = client
159 .post::<serde_json::Value, _>(
160 "/v1/auth/logout",
161 &serde_json::json!({}),
162 Some(&session.session_token),
163 )
164 .await;
165 }
166 SessionConfig::clear()?;
167 Ok(())
168}