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 let saved_api_base = if flatland_client_lib::host_override_active() {
37 SavedLogin::load().and_then(|s| s.api_base)
38 } else {
39 Some(api_base)
40 };
41 SavedLogin {
42 email: email.trim().to_string(),
43 password: password.to_string(),
44 api_base: saved_api_base,
45 }
46 .save()?;
47 Ok(())
48}
49
50pub async fn register(
52 email: &str,
53 password: &str,
54 display_name: &str,
55 api: &str,
56) -> anyhow::Result<()> {
57 if email.trim().is_empty() || password.is_empty() || display_name.trim().is_empty() {
58 anyhow::bail!("email, password, and display name are required");
59 }
60 let client = ControlPlaneClient::new(api);
61 let _: serde_json::Value = client
62 .post(
63 "/v1/auth/register",
64 &serde_json::json!({
65 "email": email.trim(),
66 "password": password,
67 "display_name": display_name.trim(),
68 }),
69 None,
70 )
71 .await?;
72 login(email, password, api).await
73}
74
75pub async fn login_default(email: &str, password: &str) -> anyhow::Result<()> {
77 let api = SavedLogin::load()
78 .and_then(|s| s.api_base)
79 .unwrap_or_else(default_api_base);
80 login(email, password, &api).await
81}
82
83pub async fn register_default(
85 email: &str,
86 password: &str,
87 display_name: &str,
88) -> anyhow::Result<()> {
89 let api = SavedLogin::load()
90 .and_then(|s| s.api_base)
91 .unwrap_or_else(default_api_base);
92 register(email, password, display_name, &api).await
93}
94
95pub fn has_api_token() -> bool {
97 std::env::var("FLATLAND_API_TOKEN")
98 .map(|t| !t.is_empty())
99 .unwrap_or(false)
100}
101
102pub async fn session_is_valid() -> bool {
104 if has_api_token() {
105 return true;
106 }
107 if !credentials_available() {
108 return false;
109 }
110 let Ok(auth) = load_play_auth() else {
111 return false;
112 };
113 if auth.kind == PlayAuthKind::ApiToken {
114 return true;
115 }
116 let client = ControlPlaneClient::new(&auth.api_base);
117 client
118 .get::<serde_json::Value>("/v1/me", &auth.bearer)
119 .await
120 .is_ok()
121}
122
123pub async fn try_restore_expired_session() -> bool {
126 if session_is_valid().await {
127 return true;
128 }
129 if !SessionConfig::exists() {
130 return false;
131 }
132 let Some(saved) = SavedLogin::load() else {
133 return false;
134 };
135 if saved.email.trim().is_empty() || saved.password.is_empty() {
136 return false;
137 }
138 let api = saved
139 .api_base
140 .clone()
141 .unwrap_or_else(default_api_base);
142 login(&saved.email, &saved.password, &api).await.is_ok()
143}
144
145pub async fn needs_interactive_login() -> bool {
147 if has_api_token() {
148 return false;
149 }
150 if session_is_valid().await {
151 return false;
152 }
153 if try_restore_expired_session().await {
155 return false;
156 }
157 true
158}
159
160pub async fn logout() -> anyhow::Result<()> {
163 if let Ok(session) = SessionConfig::load() {
164 let client = ControlPlaneClient::new(&session.api_base);
165 let _ = client
166 .post::<serde_json::Value, _>(
167 "/v1/auth/logout",
168 &serde_json::json!({}),
169 Some(&session.session_token),
170 )
171 .await;
172 }
173 SessionConfig::clear()?;
174 Ok(())
175}