systemprompt_cli/commands/admin/
bootstrap.rs1use std::sync::Arc;
7
8use anyhow::{Context, Result, anyhow};
9use clap::Args;
10use schemars::JsonSchema;
11use serde::{Deserialize, Serialize};
12use systemprompt_database::{Database, DbPool};
13use systemprompt_identifiers::UserId;
14use systemprompt_models::Config;
15use systemprompt_users::{User, UserRole, UserService, UserStatus};
16
17use crate::CliConfig;
18use crate::shared::CommandOutput;
19
20#[derive(Debug, Args)]
21pub struct BootstrapArgs {
22 #[arg(long)]
23 pub name: Option<String>,
24
25 #[arg(long)]
26 pub email: Option<String>,
27
28 #[arg(long, default_value = "Platform Admin")]
29 pub full_name: String,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
33pub struct BootstrapOutput {
34 pub id: UserId,
35 pub name: String,
36 pub email: String,
37 pub created: bool,
38 pub roles: Vec<String>,
39 pub message: String,
40}
41
42pub async fn execute(args: BootstrapArgs, _config: &CliConfig) -> Result<CommandOutput> {
43 let name = resolve_admin_name(args.name.as_deref())?;
44
45 let email = args
46 .email
47 .clone()
48 .filter(|e| !e.trim().is_empty())
49 .unwrap_or_else(|| format!("{name}@localhost"));
50
51 let user_service = connect_user_service().await?;
52
53 let (user, created) = if let Some(existing) = user_service.find_by_name(&name).await? {
54 (existing, false)
55 } else {
56 let created = user_service
57 .create(&name, &email, Some(&args.full_name), None)
58 .await?;
59 (created, true)
60 };
61
62 if !user.is_active() {
63 return Err(anyhow!(
64 "Bootstrap user '{}' exists but has status '{}'; expected '{}'. Re-activate it before \
65 running the platform.",
66 user.name,
67 user.status.as_deref().unwrap_or("(none)"),
68 UserStatus::Active.as_str(),
69 ));
70 }
71
72 let user = ensure_admin_role(&user_service, user).await?;
73
74 Ok(build_output(user, created))
75}
76
77fn resolve_admin_name(requested: Option<&str>) -> Result<String> {
78 let configured = Config::get()?.system_admin_username.clone();
79 if configured.trim().is_empty() {
80 return Err(anyhow!(
81 "Profile is missing `system_admin.username`; cannot run bootstrap"
82 ));
83 }
84
85 match requested {
86 Some(n) if !n.trim().is_empty() => {
87 if n != configured {
88 return Err(anyhow!(
89 "--name '{}' does not match profile system_admin.username '{}'; refusing to \
90 bootstrap the wrong user",
91 n,
92 configured,
93 ));
94 }
95 Ok(n.to_owned())
96 },
97 _ => Ok(configured),
98 }
99}
100
101async fn connect_user_service() -> Result<UserService> {
105 let database: DbPool = Arc::new(
106 Database::from_config_with_write(
107 &Config::get()?.database_type,
108 &Config::get()?.database_url,
109 Config::get()?.database_write_url.as_deref(),
110 &systemprompt_database::PoolConfig::default(),
111 )
112 .await
113 .context("Failed to connect to database")?,
114 );
115 Ok(UserService::new(&database)?)
116}
117
118async fn ensure_admin_role(user_service: &UserService, user: User) -> Result<User> {
119 let admin_role = UserRole::Admin.as_str().to_owned();
120
121 let user = if user.roles.contains(&admin_role) {
122 user
123 } else {
124 let mut next_roles = user.roles.clone();
125 next_roles.push(admin_role.clone());
126 user_service.assign_roles(&user.id, &next_roles).await?
127 };
128
129 if !user.roles.contains(&admin_role) {
130 return Err(anyhow!(
131 "Failed to assign 'admin' role to bootstrap user '{}'",
132 user.name
133 ));
134 }
135
136 Ok(user)
137}
138
139fn build_output(user: User, created: bool) -> CommandOutput {
140 let message = if created {
141 format!(
142 "Bootstrap user '{}' created and granted admin role",
143 user.name
144 )
145 } else {
146 format!(
147 "Bootstrap user '{}' already exists; admin role verified",
148 user.name
149 )
150 };
151
152 let output = BootstrapOutput {
153 id: user.id,
154 name: user.name,
155 email: user.email,
156 created,
157 roles: user.roles,
158 message,
159 };
160
161 let title = if created {
162 "Admin Bootstrapped"
163 } else {
164 "Admin Verified"
165 };
166 CommandOutput::card_value(title, &output)
167}