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