Skip to main content

systemprompt_cli/commands/admin/
bootstrap.rs

1//! `admin bootstrap` command: ensures the system-admin user row exists.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use 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, UserRepository, 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> {
102    let database: DbPool = Arc::new(
103        Database::from_config_with_write(
104            &Config::get()?.database_type,
105            &Config::get()?.database_url,
106            Config::get()?.database_write_url.as_deref(),
107            &systemprompt_database::PoolConfig::default(),
108        )
109        .await
110        .context("Failed to connect to database")?,
111    );
112    Ok(UserService::new(Arc::new(UserRepository::new(&database)?)))
113}
114
115async fn ensure_admin_role(user_service: &UserService, user: User) -> Result<User> {
116    let admin_role = UserRole::Admin.as_str().to_owned();
117
118    let user = if user.roles.contains(&admin_role) {
119        user
120    } else {
121        let mut next_roles = user.roles.clone();
122        next_roles.push(admin_role.clone());
123        user_service.assign_roles(&user.id, &next_roles).await?
124    };
125
126    if !user.roles.contains(&admin_role) {
127        return Err(anyhow!(
128            "Failed to assign 'admin' role to bootstrap user '{}'",
129            user.name
130        ));
131    }
132
133    Ok(user)
134}
135
136fn build_output(user: User, created: bool) -> CommandOutput {
137    let message = if created {
138        format!(
139            "Bootstrap user '{}' created and granted admin role",
140            user.name
141        )
142    } else {
143        format!(
144            "Bootstrap user '{}' already exists; admin role verified",
145            user.name
146        )
147    };
148
149    let output = BootstrapOutput {
150        id: user.id,
151        name: user.name,
152        email: user.email,
153        created,
154        roles: user.roles,
155        message,
156    };
157
158    let title = if created {
159        "Admin Bootstrapped"
160    } else {
161        "Admin Verified"
162    };
163    CommandOutput::card_value(title, &output)
164}