systemprompt_cli/commands/admin/session/
login.rs1use std::path::Path;
7use std::sync::Arc;
8
9use anyhow::{Context, Result};
10use chrono::Duration as ChronoDuration;
11use clap::Args;
12use serde::{Deserialize, Serialize};
13
14use crate::cli_settings::CliConfig;
15use crate::paths::ResolvedPaths;
16use crate::shared::CommandOutput;
17use systemprompt_agent::repository::context::ContextRepository;
18use systemprompt_cloud::SessionKey;
19use systemprompt_config::{ProfileBootstrap, SecretsBootstrap};
20use systemprompt_database::{Database, DbPool};
21use systemprompt_identifiers::{ContextId, SessionId, SessionToken, UserId};
22use systemprompt_logging::CliService;
23use systemprompt_manifest::{Profile, Secrets};
24use systemprompt_models::auth::{Permission, RateLimitTier, UserType};
25use systemprompt_security::{SessionGenerator, SessionParams};
26use systemprompt_users::{User, UserRole};
27
28use super::login_helpers::{
29 SessionStoreParams, fetch_admin_user, save_session_to_store, try_use_existing_session,
30};
31use crate::session::api::create_local_session_row;
32
33#[derive(Debug, Args)]
34pub struct LoginArgs {
35 #[arg(long, hide = true, help = "Override email from credentials")]
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 && let Some(output) = try_use_existing_session(
86 &sessions_dir,
87 &session_key,
88 &profile.security.issuer,
89 args,
90 &db_pool,
91 )
92 .await?
93 {
94 return Ok(output);
95 }
96
97 let admin_name = &profile.system_admin.username;
98 progress(args, &format!("Fetching admin user: {admin_name}"));
99 let admin_user = fetch_admin_user(
100 &db_pool,
101 admin_name,
102 profile.target.is_cloud(),
103 args.email.as_deref(),
104 )
105 .await?;
106
107 progress(args, "Creating session...");
108 let session_id = create_local_session_row(
109 &db_pool,
110 &admin_user.id,
111 chrono::Duration::hours(args.duration_hours),
112 )
113 .await?;
114
115 progress(args, "Creating context...");
116 let context_id =
117 create_cli_context(&db_pool, &admin_user.id, &session_id, profile_path).await?;
118
119 progress(args, "Generating token...");
120 let session_token = generate_session_token(profile, args, &admin_user, &session_id)?;
121
122 save_session_to_store(SessionStoreParams {
123 sessions_dir: &sessions_dir,
124 session_key: &session_key,
125 profile_path,
126 session_token: session_token.clone(),
127 issuer: &profile.security.issuer,
128 ttl: chrono::Duration::hours(args.duration_hours),
129 session_id: session_id.clone(),
130 context_id,
131 user_id: admin_user.id.clone(),
132 user_email: &admin_user.email,
133 user_type: UserType::Admin,
134 })?;
135
136 let output = LoginOutput {
137 status: "created".to_owned(),
138 user_id: admin_user.id.clone(),
139 email: admin_user.email.clone(),
140 session_id,
141 expires_in_hours: args.duration_hours,
142 };
143
144 if args.token_only {
145 return Ok(CommandOutput::text(session_token.as_str()));
146 }
147
148 CliService::success(&format!(
149 "Session saved to {}/index.json",
150 sessions_dir.display()
151 ));
152 Ok(CommandOutput::card_value("Admin Session", &output))
153}
154
155fn progress(args: &LoginArgs, message: &str) {
156 if !args.token_only {
157 CliService::info(message);
158 }
159}
160
161async fn create_cli_context(
162 db_pool: &DbPool,
163 user_id: &UserId,
164 session_id: &SessionId,
165 profile_path: &str,
166) -> Result<ContextId> {
167 let profile_name = Path::new(profile_path)
168 .parent()
169 .and_then(|d| d.file_name())
170 .and_then(|n| n.to_str())
171 .unwrap_or("unknown");
172
173 let context_repo = ContextRepository::new(db_pool);
174 context_repo
175 .get_or_create_cli_context(
176 user_id,
177 session_id,
178 &format!("CLI Session - {}", profile_name),
179 )
180 .await
181 .context("Failed to create CLI context")
182}
183
184fn generate_session_token(
185 profile: &Profile,
186 args: &LoginArgs,
187 admin_user: &User,
188 session_id: &SessionId,
189) -> Result<SessionToken> {
190 let session_generator = SessionGenerator::new(&profile.security.issuer);
191 session_generator
192 .generate(&SessionParams {
193 user_id: &admin_user.id,
194 session_id,
195 email: &admin_user.email,
196 duration: ChronoDuration::hours(args.duration_hours),
197 user_type: UserType::Admin,
198 permissions: vec![Permission::Admin],
199 roles: vec![UserRole::Admin.as_str().to_owned()],
200 attributes: std::collections::BTreeMap::new(),
201 rate_limit_tier: RateLimitTier::Admin,
202 })
203 .context("Failed to generate session token")
204}
205
206fn session_key_for_profile(profile: &Profile) -> SessionKey {
207 if profile.target.is_local() {
208 SessionKey::Local
209 } else {
210 let tenant_id = profile.cloud.as_ref().and_then(|c| c.tenant_id.as_ref());
211 SessionKey::from_tenant_id(tenant_id)
212 }
213}