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