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