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, 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_reset(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_reset(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}