1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
use anyhow::Result;
use clap::Subcommand;
use crate::cli::api_client::ApiClient;
#[derive(Subcommand)]
pub enum TenantCommands {
/// List all tenants
List,
/// Create a new tenant
Create {
/// Display name
display_name: String,
/// Email address
#[arg(long)]
email: Option<String>,
},
/// Enable a tenant
Enable {
/// Tenant ID
id: String,
},
/// Disable a tenant
Disable {
/// Tenant ID
id: String,
},
/// Map an IP address to a tenant
MapIp {
/// Tenant ID
tenant_id: String,
/// IP address
ip: String,
},
/// Enable a plugin for a tenant
EnablePlugin {
/// Tenant ID
tenant_id: String,
/// Plugin (namespace/name format)
plugin: String,
},
/// Disable a plugin for a tenant
DisablePlugin {
/// Tenant ID
tenant_id: String,
/// Plugin (namespace/name format)
plugin: String,
},
/// Set plugin configuration for a tenant
SetPluginConfig {
/// Tenant ID
tenant_id: String,
/// Plugin (namespace/name format)
plugin: String,
/// Configuration as JSON object
json: String,
},
}
pub struct TenantHandler;
impl TenantHandler {
pub async fn handle(&self, command: &TenantCommands) -> Result<()> {
let client = ApiClient::from_auth_store()?
.ok_or_else(|| anyhow::anyhow!("Not authenticated. Run 'witm auth login' first."))?;
match command {
TenantCommands::List => {
let resp = client.get("/api/manage/tenants").await?;
let body = resp.text().await?;
println!("{}", body);
}
TenantCommands::Create {
display_name,
email,
} => {
let mut body = serde_json::json!({
"display_name": display_name,
});
if let Some(email) = email {
body["email"] = serde_json::json!(email);
}
let resp = client
.post_json(
"/api/auth/register",
&serde_json::json!({
"display_name": display_name,
"email": email.as_deref().unwrap_or(""),
"password": "", // Placeholder for CLI tenant creation
}),
)
.await?;
let text = resp.text().await?;
println!("{}", text);
}
TenantCommands::Enable { id } => {
let resp = client
.put_json(
&format!("/api/manage/tenants/{}", id),
&serde_json::json!({"enabled": true}),
)
.await?;
println!("{}", resp.text().await?);
}
TenantCommands::Disable { id } => {
let resp = client
.put_json(
&format!("/api/manage/tenants/{}", id),
&serde_json::json!({"enabled": false}),
)
.await?;
println!("{}", resp.text().await?);
}
TenantCommands::MapIp { tenant_id, ip } => {
let resp = client
.post_json(
&format!("/api/manage/tenants/{}/ip-mappings", tenant_id),
&serde_json::json!({"ip_address": ip}),
)
.await?;
println!("{}", resp.text().await?);
}
TenantCommands::EnablePlugin { tenant_id, plugin } => {
let (ns, name) = parse_plugin_id(plugin)?;
let resp = client
.put_json(
&format!(
"/api/manage/tenants/{}/plugins/{}/{}/enabled",
tenant_id, ns, name
),
&serde_json::json!({"enabled": true}),
)
.await?;
println!("{}", resp.text().await?);
}
TenantCommands::DisablePlugin { tenant_id, plugin } => {
let (ns, name) = parse_plugin_id(plugin)?;
let resp = client
.put_json(
&format!(
"/api/manage/tenants/{}/plugins/{}/{}/enabled",
tenant_id, ns, name
),
&serde_json::json!({"enabled": false}),
)
.await?;
println!("{}", resp.text().await?);
}
TenantCommands::SetPluginConfig {
tenant_id,
plugin,
json,
} => {
let (ns, name) = parse_plugin_id(plugin)?;
let config: serde_json::Value = serde_json::from_str(json)?;
let resp = client
.put_json(
&format!(
"/api/manage/tenants/{}/plugins/{}/{}/config",
tenant_id, ns, name
),
&serde_json::json!({"config": config}),
)
.await?;
println!("{}", resp.text().await?);
}
}
Ok(())
}
}
fn parse_plugin_id(plugin: &str) -> Result<(&str, &str)> {
let parts: Vec<&str> = plugin.splitn(2, '/').collect();
if parts.len() != 2 {
anyhow::bail!("Plugin must be in 'namespace/name' format, got: {}", plugin);
}
Ok((parts[0], parts[1]))
}