A CLI-only walkthrough (E2E_TODO_REPORT.md) found two control-plane
operations with no `pic` command — every CLI-created app 404'd until it
claimed a domain, and external SSE feeds needed a raw topic-registration
call — plus several friction points. All admin APIs already existed; this
adds thin wrappers mirroring `routes`/`triggers`:
- B1: `pic apps domains {ls,add,rm}` over apps/{id}/domains.
- B2: `pic topics {ls,create,update,rm}` over apps/{id}/topics, and a
triggers help note distinguishing a pubsub trigger from topic
registration.
- F2: `pic users {ls,show,reset-password,revoke-sessions}` over the app
end-user admin surface (read + the two admin actions; create/invitations
deferred).
- F1: `apps create` and `deploy` now honor `--output json`, emitting the
created object so scripts can capture the id.
- F3: non-interactive login via `--username` + `--password-stdin` (inline
passwords still rejected, mirroring the `--token` rule).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
96 lines
3.2 KiB
Rust
96 lines
3.2 KiB
Rust
//! `pic users {ls,show,reset-password,revoke-sessions}` — admin view of
|
|
//! an app's *end-users* (people who registered via `users::create` in a
|
|
//! script), distinct from `pic admins` (instance control-plane accounts)
|
|
//! and app membership/collaborators.
|
|
//!
|
|
//! Scoped to read + the two admin actions the E2E report needed. Create,
|
|
//! delete, and invitations are deferred to a follow-up.
|
|
|
|
use anyhow::Result;
|
|
|
|
use crate::client::Client;
|
|
use crate::config;
|
|
use crate::output::{KvBlock, OutputMode, Table};
|
|
|
|
pub async fn ls(app: &str, limit: u32, mode: OutputMode) -> Result<()> {
|
|
let creds = config::resolve()?;
|
|
let client = Client::from_creds(&creds)?;
|
|
let users = client.app_users_list(app, limit).await?;
|
|
let mut table = Table::new(["id", "email", "display_name", "last_login_at", "created_at"]);
|
|
for u in users {
|
|
table.row([
|
|
u.id.to_string(),
|
|
u.email.clone(),
|
|
u.display_name.clone().unwrap_or_else(|| "-".into()),
|
|
u.last_login_at
|
|
.map(|t| t.to_rfc3339())
|
|
.unwrap_or_else(|| "-".into()),
|
|
u.created_at.to_rfc3339(),
|
|
]);
|
|
}
|
|
table.print(mode);
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn show(app: &str, user_id: &str, mode: OutputMode) -> Result<()> {
|
|
let creds = config::resolve()?;
|
|
let client = Client::from_creds(&creds)?;
|
|
let u = client.app_user_get(app, user_id).await?;
|
|
let mut block = KvBlock::new();
|
|
block
|
|
.field("id", u.id.to_string())
|
|
.field("email", u.email.clone())
|
|
.field(
|
|
"display_name",
|
|
u.display_name.clone().unwrap_or_else(|| "-".into()),
|
|
)
|
|
.field(
|
|
"email_verified_at",
|
|
u.email_verified_at
|
|
.map(|t| t.to_rfc3339())
|
|
.unwrap_or_else(|| "-".into()),
|
|
)
|
|
.field(
|
|
"last_login_at",
|
|
u.last_login_at
|
|
.map(|t| t.to_rfc3339())
|
|
.unwrap_or_else(|| "-".into()),
|
|
)
|
|
.field(
|
|
"roles",
|
|
if u.roles.is_empty() {
|
|
"-".into()
|
|
} else {
|
|
u.roles.join(",")
|
|
},
|
|
)
|
|
.field("created_at", u.created_at.to_rfc3339())
|
|
.field("updated_at", u.updated_at.to_rfc3339());
|
|
block.print(mode);
|
|
Ok(())
|
|
}
|
|
|
|
/// Mint a one-shot password-reset token for an end-user. The token is
|
|
/// returned exactly once — the admin pastes it into a manual reset link.
|
|
pub async fn reset_password(app: &str, user_id: &str, mode: OutputMode) -> Result<()> {
|
|
let creds = config::resolve()?;
|
|
let client = Client::from_creds(&creds)?;
|
|
let resp = client.app_user_reset_password(app, user_id).await?;
|
|
let mut block = KvBlock::new();
|
|
block
|
|
.field("token", resp.token.clone())
|
|
.field("expires_in_seconds", resp.expires_in_seconds.to_string());
|
|
block.print(mode);
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn revoke_sessions(app: &str, user_id: &str, mode: OutputMode) -> Result<()> {
|
|
let creds = config::resolve()?;
|
|
let client = Client::from_creds(&creds)?;
|
|
let resp = client.app_user_revoke_sessions(app, user_id).await?;
|
|
let mut block = KvBlock::new();
|
|
block.field("revoked", resp.revoked.to_string());
|
|
block.print(mode);
|
|
Ok(())
|
|
}
|