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