Skip to main content

systemprompt_cli/commands/admin/users/
mod.rs

1//! User administration command tree.
2//!
3//! [`UsersCommands`] groups the user CRUD, search, export, stats, merge, and
4//! the `bulk`, `role`, `session`, `ban`, and `webauthn` subcommand trees. On a
5//! `--database-url` invocation only the read-only commands are served; write
6//! operations require a full profile context.
7//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11mod apikey;
12mod ban;
13mod bulk;
14mod count;
15mod create;
16pub(crate) mod delete;
17mod export;
18mod list;
19mod merge;
20mod role;
21mod search;
22mod session;
23mod show;
24mod stats;
25mod types;
26mod update;
27mod webauthn;
28
29use crate::context::CommandContext;
30use crate::descriptor::DataImpact;
31use crate::shared::{CommandOutput, render_result};
32use anyhow::{Result, bail};
33use clap::Subcommand;
34
35pub use apikey::{ApiKeyCommands, IssueArgs as ApiKeyIssueArgs};
36pub use types::*;
37
38#[derive(Debug, Subcommand)]
39pub enum UsersCommands {
40    #[command(about = "List users with pagination and filtering")]
41    List(list::ListArgs),
42
43    #[command(about = "Show detailed user information")]
44    Show(show::ShowArgs),
45
46    #[command(about = "Search users by name, email, or full name")]
47    Search(search::SearchArgs),
48
49    #[command(about = "Create a new user")]
50    Create(create::CreateArgs),
51
52    #[command(about = "Update user fields")]
53    Update(update::UpdateArgs),
54
55    #[command(about = "Delete a user")]
56    Delete(delete::DeleteArgs),
57
58    #[command(about = "Get total user count")]
59    Count(count::CountArgs),
60
61    #[command(about = "Export users to JSON")]
62    Export(export::ExportArgs),
63
64    #[command(about = "Show user statistics dashboard")]
65    Stats,
66
67    #[command(about = "Merge source user into target user")]
68    Merge(merge::MergeArgs),
69
70    #[command(subcommand, about = "Bulk operations on users")]
71    Bulk(bulk::BulkCommands),
72
73    #[command(subcommand, about = "Role management commands")]
74    Role(role::RoleCommands),
75
76    #[command(subcommand, about = "Session management commands")]
77    Session(session::SessionCommands),
78
79    #[command(subcommand, about = "IP ban management commands")]
80    Ban(ban::BanCommands),
81
82    #[command(subcommand, about = "WebAuthn credential management commands")]
83    Webauthn(webauthn::WebauthnCommands),
84
85    #[command(
86        subcommand,
87        name = "api-key",
88        about = "Personal access token (sp-live-) management"
89    )]
90    ApiKey(ApiKeyCommands),
91}
92
93pub async fn execute(cmd: UsersCommands, ctx: &CommandContext) -> Result<()> {
94    if ctx.is_database_scoped()
95        && matches!(
96            cmd,
97            UsersCommands::Create(_)
98                | UsersCommands::Update(_)
99                | UsersCommands::Delete(_)
100                | UsersCommands::Merge(_)
101                | UsersCommands::Bulk(_)
102                | UsersCommands::Webauthn(_)
103                | UsersCommands::ApiKey(_)
104        )
105    {
106        bail!("Write operations require full profile context");
107    }
108
109    match cmd {
110        UsersCommands::Bulk(cmd) => Box::pin(bulk::execute(cmd, ctx)).await,
111        UsersCommands::Role(cmd) => Box::pin(role::execute(cmd, ctx)).await,
112        UsersCommands::Session(cmd) => Box::pin(session::execute(cmd, ctx)).await,
113        UsersCommands::Ban(cmd) => Box::pin(ban::execute(cmd, ctx)).await,
114        UsersCommands::Webauthn(cmd) => Box::pin(webauthn::execute(cmd, ctx)).await,
115        other => {
116            let output = Box::pin(render_output(other, ctx)).await?;
117            render_result(&output, &ctx.cli);
118            Ok(())
119        },
120    }
121}
122
123async fn render_output(cmd: UsersCommands, ctx: &CommandContext) -> Result<CommandOutput> {
124    match cmd {
125        UsersCommands::List(args) => list::execute(args, ctx).await,
126        UsersCommands::Show(args) => show::execute(args, ctx).await,
127        UsersCommands::Search(args) => search::execute(args, ctx).await,
128        UsersCommands::Create(args) => create::execute(args, ctx).await,
129        UsersCommands::Update(args) => update::execute(args, ctx).await,
130        UsersCommands::Delete(args) => delete::execute(args, ctx).await,
131        UsersCommands::Count(args) => count::execute(args, ctx).await,
132        UsersCommands::Export(args) => export::execute(args, ctx).await,
133        UsersCommands::Stats => stats::execute(ctx).await,
134        UsersCommands::Merge(args) => merge::execute(args, ctx).await,
135        UsersCommands::ApiKey(cmd) => apikey::execute(cmd, ctx).await,
136        UsersCommands::Bulk(_)
137        | UsersCommands::Role(_)
138        | UsersCommands::Session(_)
139        | UsersCommands::Ban(_)
140        | UsersCommands::Webauthn(_) => bail!(
141            "internal: a users subgroup reached the rendering dispatch, which only serves \
142             commands that produce a single output"
143        ),
144    }
145}
146
147impl UsersCommands {
148    pub const fn data_impact(&self) -> DataImpact {
149        match self {
150            Self::Delete(_)
151            | Self::Merge(_)
152            | Self::Bulk(bulk::BulkCommands::Delete(_) | bulk::BulkCommands::Update(_))
153            | Self::Role(
154                role::RoleCommands::Assign(_)
155                | role::RoleCommands::Promote(_)
156                | role::RoleCommands::Demote(_),
157            )
158            | Self::Session(session::SessionCommands::Cleanup(_))
159            | Self::Ban(ban::BanCommands::Cleanup(_)) => DataImpact::Destructive,
160            Self::List(_)
161            | Self::Show(_)
162            | Self::Search(_)
163            | Self::Create(_)
164            | Self::Update(_)
165            | Self::Count(_)
166            | Self::Export(_)
167            | Self::Stats
168            | Self::Session(session::SessionCommands::List(_) | session::SessionCommands::End(_))
169            | Self::Ban(
170                ban::BanCommands::List(_)
171                | ban::BanCommands::Add(_)
172                | ban::BanCommands::Remove(_)
173                | ban::BanCommands::Check(_),
174            )
175            | Self::Webauthn(webauthn::WebauthnCommands::GenerateSetupToken(_))
176            | Self::ApiKey(
177                ApiKeyCommands::Issue(_) | ApiKeyCommands::List(_) | ApiKeyCommands::Revoke(_),
178            ) => DataImpact::Preserving,
179        }
180    }
181}