systemprompt_cli/commands/admin/session/
login.rs1use std::path::Path;
2use std::sync::Arc;
3
4use anyhow::{Context, Result};
5use chrono::Duration as ChronoDuration;
6use clap::Args;
7use serde::{Deserialize, Serialize};
8
9use crate::cli_settings::CliConfig;
10use crate::paths::ResolvedPaths;
11use crate::shared::CommandOutput;
12use systemprompt_agent::repository::context::ContextRepository;
13use systemprompt_cloud::SessionKey;
14use systemprompt_config::{ProfileBootstrap, SecretsBootstrap};
15use systemprompt_database::{Database, DbPool};
16use systemprompt_identifiers::{SessionId, UserId};
17use systemprompt_logging::CliService;
18use systemprompt_models::auth::{Permission, RateLimitTier, UserType};
19use systemprompt_models::{Profile, Secrets};
20use systemprompt_security::{SessionGenerator, SessionParams};
21use systemprompt_users::UserRole;
22
23use super::login_helpers::{
24 SessionStoreParams, fetch_admin_user, save_session_to_store, try_use_existing_session,
25};
26use crate::session::api::create_local_session_row;
27
28#[derive(Debug, Args)]
29pub struct LoginArgs {
30 #[arg(
31 long,
32 env = "SYSTEMPROMPT_ADMIN_EMAIL",
33 hide = true,
34 help = "Override email from credentials"
35 )]
36 pub email: Option<String>,
37
38 #[arg(long, default_value = "24", help = "Session duration in hours")]
39 pub duration_hours: i64,
40
41 #[arg(long, help = "Only output the token (for scripting)")]
42 pub token_only: bool,
43
44 #[arg(
45 long,
46 help = "Force creation of a new session even if a valid one exists"
47 )]
48 pub force_new: bool,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct LoginOutput {
53 pub status: String,
54 pub user_id: UserId,
55 pub email: String,
56 pub session_id: SessionId,
57 pub expires_in_hours: i64,
58}
59
60pub async fn execute(args: LoginArgs, _config: &CliConfig) -> Result<CommandOutput> {
61 let profile = ProfileBootstrap::get().context("No profile loaded")?;
62 let profile_path = ProfileBootstrap::get_path().context("Profile path not set")?;
63 let secrets = SecretsBootstrap::get().context("Secrets not initialized")?;
64
65 login_for_profile(profile, profile_path, secrets, &args).await
66}
67
68pub async fn login_for_profile(
69 profile: &Profile,
70 profile_path: &str,
71 secrets: &Secrets,
72 args: &LoginArgs,
73) -> Result<CommandOutput> {
74 let sessions_dir = ResolvedPaths::discover().sessions_dir();
75 let session_key = session_key_for_profile(profile);
76
77 let database_url = secrets.effective_database_url(profile.database.external_db_access);
78
79 let db = Database::new_postgres(database_url)
80 .await
81 .context("Failed to connect to database")?;
82 let db_pool = DbPool::from(Arc::new(db));
83
84 if !args.force_new {
85 if let Some(output) =
86 try_use_existing_session(&sessions_dir, &session_key, args, &db_pool).await?
87 {
88 return Ok(output);
89 }
90 }
91
92 let admin_name = &profile.system_admin.username;
93 if !args.token_only {
94 CliService::info(&format!("Fetching admin user: {admin_name}"));
95 }
96 let admin_user = fetch_admin_user(
97 &db_pool,
98 admin_name,
99 profile.target.is_cloud(),
100 args.email.as_deref(),
101 )
102 .await?;
103
104 if !args.token_only {
105 CliService::info("Creating session...");
106 }
107 let session_id = create_local_session_row(&db_pool, &admin_user.id).await?;
108
109 if !args.token_only {
110 CliService::info("Creating context...");
111 }
112 let profile_name = Path::new(profile_path)
113 .parent()
114 .and_then(|d| d.file_name())
115 .and_then(|n| n.to_str())
116 .unwrap_or("unknown");
117 let context_repo = ContextRepository::new(&db_pool)?;
118 let context_id = context_repo
119 .create_context(
120 &admin_user.id,
121 Some(&session_id),
122 &format!("CLI Session - {}", profile_name),
123 )
124 .await
125 .context("Failed to create CLI context")?;
126
127 if !args.token_only {
128 CliService::info("Generating token...");
129 }
130 let session_generator = SessionGenerator::new(&profile.security.issuer);
131 let duration = ChronoDuration::hours(args.duration_hours);
132 let session_token = session_generator
133 .generate(&SessionParams {
134 user_id: &admin_user.id,
135 session_id: &session_id,
136 email: &admin_user.email,
137 duration,
138 user_type: UserType::Admin,
139 permissions: vec![Permission::Admin],
140 roles: vec![UserRole::Admin.as_str().to_owned()],
141 attributes: std::collections::BTreeMap::new(),
142 rate_limit_tier: RateLimitTier::Admin,
143 })
144 .context("Failed to generate session token")?;
145
146 save_session_to_store(SessionStoreParams {
147 sessions_dir: &sessions_dir,
148 session_key: &session_key,
149 profile_path,
150 session_token: session_token.clone(),
151 session_id: session_id.clone(),
152 context_id,
153 user_id: admin_user.id.clone(),
154 user_email: &admin_user.email,
155 user_type: UserType::Admin,
156 })?;
157
158 let output = LoginOutput {
159 status: "created".to_owned(),
160 user_id: admin_user.id.clone(),
161 email: admin_user.email.clone(),
162 session_id,
163 expires_in_hours: args.duration_hours,
164 };
165
166 if args.token_only {
167 CliService::output(session_token.as_str());
168 return Ok(CommandOutput::card_value("Admin Session", &output).with_skip_render());
169 }
170
171 CliService::success(&format!(
172 "Session saved to {}/index.json",
173 sessions_dir.display()
174 ));
175 Ok(CommandOutput::card_value("Admin Session", &output))
176}
177
178fn session_key_for_profile(profile: &Profile) -> SessionKey {
179 if profile.target.is_local() {
180 SessionKey::Local
181 } else {
182 let tenant_id = profile.cloud.as_ref().and_then(|c| c.tenant_id.as_ref());
183 SessionKey::from_tenant_id(tenant_id)
184 }
185}