Skip to main content

systemprompt_cli/commands/admin/session/
login_helpers.rs

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