Skip to main content

systemprompt_cli/commands/admin/
bootstrap.rs

1use 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
96// Why: bootstrap must run before AppContext::build, because AppContext
97// resolution requires the admin row to already exist. Open a database
98// pool directly so SystemAdmin does not need to be installed yet.
99async 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}