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::{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 configured = Config::get()?.system_admin_username.clone();
39 if configured.trim().is_empty() {
40 return Err(anyhow!(
41 "Profile is missing `system_admin.username`; cannot run bootstrap"
42 ));
43 }
44
45 let name = match args.name.as_deref() {
46 Some(n) if !n.trim().is_empty() => {
47 if n != configured {
48 return Err(anyhow!(
49 "--name '{}' does not match profile system_admin.username '{}'; refusing to \
50 bootstrap the wrong user",
51 n,
52 configured,
53 ));
54 }
55 n.to_owned()
56 },
57 _ => configured.clone(),
58 };
59
60 let email = args
61 .email
62 .clone()
63 .filter(|e| !e.trim().is_empty())
64 .unwrap_or_else(|| format!("{name}@localhost"));
65
66 let database: DbPool = Arc::new(
70 Database::from_config_with_write(
71 &Config::get()?.database_type,
72 &Config::get()?.database_url,
73 Config::get()?.database_write_url.as_deref(),
74 )
75 .await
76 .context("Failed to connect to database")?,
77 );
78 let user_service = UserService::new(&database)?;
79
80 let admin_role = UserRole::Admin.as_str().to_owned();
81
82 let (user, created) = if let Some(existing) = user_service.find_by_name(&name).await? {
83 (existing, false)
84 } else {
85 let created = user_service
86 .create(&name, &email, Some(&args.full_name), None)
87 .await?;
88 (created, true)
89 };
90
91 if !user.is_active() {
92 return Err(anyhow!(
93 "Bootstrap user '{}' exists but has status '{}'; expected '{}'. Re-activate it before \
94 running the platform.",
95 user.name,
96 user.status.as_deref().unwrap_or("(none)"),
97 UserStatus::Active.as_str(),
98 ));
99 }
100
101 let user = if user.roles.contains(&admin_role) {
102 user
103 } else {
104 let mut next_roles = user.roles.clone();
105 next_roles.push(admin_role.clone());
106 user_service.assign_roles(&user.id, &next_roles).await?
107 };
108
109 if !user.roles.contains(&admin_role) {
110 return Err(anyhow!(
111 "Failed to assign 'admin' role to bootstrap user '{}'",
112 user.name
113 ));
114 }
115
116 let message = if created {
117 format!(
118 "Bootstrap user '{}' created and granted admin role",
119 user.name
120 )
121 } else {
122 format!(
123 "Bootstrap user '{}' already exists; admin role verified",
124 user.name
125 )
126 };
127
128 let output = BootstrapOutput {
129 id: user.id,
130 name: user.name,
131 email: user.email,
132 created,
133 roles: user.roles,
134 message,
135 };
136
137 let title = if created {
138 "Admin Bootstrapped"
139 } else {
140 "Admin Verified"
141 };
142 Ok(CommandOutput::card_value(title, &output))
143}