systemprompt_cli/commands/admin/session/
login_helpers.rs1use std::path::Path;
7use std::sync::Arc;
8
9use anyhow::{Context, Result};
10
11use systemprompt_cloud::{
12 CliSession, CredentialsBootstrap, SessionBinding, SessionIdentity, SessionKey, SessionStore,
13};
14use systemprompt_database::DbPool;
15use systemprompt_identifiers::{ContextId, SessionId, UserId};
16use systemprompt_logging::CliService;
17use systemprompt_users::{User, UserRepository, UserRole, UserService};
18
19use super::login::{LoginArgs, LoginOutput};
20use crate::shared::CommandOutput;
21
22pub(super) async fn try_use_existing_session(
23 sessions_dir: &Path,
24 session_key: &SessionKey,
25 issuer: &str,
26 args: &LoginArgs,
27 db_pool: &DbPool,
28) -> Result<Option<CommandOutput>> {
29 let mut store = SessionStore::load_or_create(sessions_dir)?;
30
31 let Some(session) = store.get_valid_session(session_key, issuer) else {
32 if !args.token_only {
33 CliService::info("No valid session found, creating new session...");
34 }
35 return Ok(None);
36 };
37
38 let session_id = session.session_id.clone();
39 let user_id = session.user_id.clone();
40 let user_email = session.user_email.to_string();
41 let session_token = session.session_token.clone();
42
43 let user_service = UserService::new(Arc::new(UserRepository::new(db_pool)?));
44 let exists = user_service
45 .session_exists(&session_id)
46 .await
47 .unwrap_or(false);
48
49 if !exists {
50 if !args.token_only {
51 CliService::info(
52 "Cached session is stale (not found in database), creating new session...",
53 );
54 }
55 store.remove_session(session_key);
56 store.save(sessions_dir)?;
57 return Ok(None);
58 }
59
60 let output = LoginOutput {
61 status: "existing".to_owned(),
62 user_id,
63 email: user_email,
64 session_id,
65 expires_in_hours: 24,
66 };
67
68 if args.token_only {
69 CliService::output(session_token.as_str());
70 return Ok(Some(
71 CommandOutput::card_value("Admin Session", &output).with_skip_render(),
72 ));
73 }
74
75 CliService::success("Using existing valid session");
76 Ok(Some(CommandOutput::card_value("Admin Session", &output)))
77}
78
79pub async fn fetch_admin_user(
80 db_pool: &DbPool,
81 admin_name: &str,
82 is_cloud_profile: bool,
83 email_override: Option<&str>,
84) -> Result<User> {
85 let user_service = UserService::new(Arc::new(UserRepository::new(db_pool)?));
86
87 if let Some(user) = user_service
88 .find_by_name(admin_name)
89 .await
90 .context("Failed to fetch admin user")?
91 {
92 if !user.is_admin() {
93 anyhow::bail!(
94 "User '{}' exists but is not an admin. Contact your administrator.",
95 admin_name
96 );
97 }
98 return Ok(user);
99 }
100
101 if !is_cloud_profile {
102 anyhow::bail!(
103 "Local admin user '{}' not found. Run 'systemprompt admin bootstrap --email \
104 <email>' to create it.",
105 admin_name
106 );
107 }
108
109 let email = match email_override {
110 Some(e) => e.to_owned(),
111 None => cloud_credentials_email().await?,
112 };
113
114 CliService::info(&format!(
115 "Admin user '{}' not found, creating it for cloud profile...",
116 admin_name
117 ));
118
119 let user = user_service
120 .create(admin_name, &email, None, None)
121 .await
122 .context("Failed to create user")?;
123
124 let user = user_service
125 .assign_roles(&user.id, &[UserRole::Admin.as_str().to_owned()])
126 .await
127 .context("Failed to assign admin role")?;
128
129 CliService::success(&format!("Created admin user: {admin_name} <{email}>"));
130 Ok(user)
131}
132
133async fn cloud_credentials_email() -> Result<String> {
134 CredentialsBootstrap::try_init()
135 .await
136 .context("Failed to initialize credentials")?;
137 let creds = CredentialsBootstrap::require().map_err(|_e| {
138 anyhow::anyhow!(
139 "No credentials found. Run 'systemprompt cloud auth login' first to authenticate."
140 )
141 })?;
142 Ok(creds.user_email.as_str().to_owned())
143}
144
145pub(super) struct SessionStoreParams<'a> {
146 pub sessions_dir: &'a Path,
147 pub session_key: &'a SessionKey,
148 pub profile_path: &'a str,
149 pub session_token: systemprompt_identifiers::SessionToken,
150 pub issuer: &'a str,
151 pub ttl: chrono::Duration,
152 pub session_id: SessionId,
153 pub context_id: ContextId,
154 pub user_id: UserId,
155 pub user_email: &'a str,
156 pub user_type: systemprompt_models::auth::UserType,
157}
158
159pub(super) fn save_session_to_store(params: SessionStoreParams<'_>) -> Result<()> {
160 let SessionStoreParams {
161 sessions_dir,
162 session_key,
163 profile_path,
164 session_token,
165 issuer,
166 ttl,
167 session_id,
168 context_id,
169 user_id,
170 user_email,
171 user_type,
172 } = params;
173 let mut store = SessionStore::load_or_create(sessions_dir)?;
174
175 let profile_dir = Path::new(profile_path).parent();
176 let profile_name_str = profile_dir
177 .and_then(|d| d.file_name())
178 .and_then(|n| n.to_str())
179 .context("Invalid profile path")?;
180
181 let profile_name = systemprompt_identifiers::ProfileName::try_new(profile_name_str)
182 .map_err(|e| anyhow::anyhow!("Invalid profile name: {}", e))?;
183
184 let email = systemprompt_identifiers::Email::try_new(user_email)
185 .map_err(|e| anyhow::anyhow!("Invalid email: {}", e))?;
186
187 let cli_session = CliSession::builder(
188 SessionBinding::new(profile_name, issuer.to_owned()),
189 session_token,
190 session_id,
191 context_id,
192 SessionIdentity::new(user_id, email, user_type),
193 )
194 .with_ttl(ttl)
195 .with_session_key(session_key)
196 .with_profile_path(profile_path)
197 .build();
198
199 store.upsert_session(session_key, cli_session);
200 store.set_active_with_profile(session_key, profile_name_str);
201 store.save(sessions_dir)?;
202
203 tracing::debug!(sessions_dir = %sessions_dir.display(), "session saved to index.json");
204 Ok(())
205}