[WIP] User Invites
This commit is contained in:
parent
0827882bb3
commit
7164f4671d
24 changed files with 2829 additions and 170 deletions
|
|
@ -484,6 +484,38 @@ impl IpcClient {
|
|||
.join("\n")
|
||||
}
|
||||
}
|
||||
ResponsePayload::InvitationCreated(invitation) => format!(
|
||||
"Created {:?} invitation {}\nToken: {}\nExpires: {}{}",
|
||||
invitation.authority,
|
||||
invitation.invitation_id,
|
||||
invitation.raw_token.0,
|
||||
invitation.expires_at,
|
||||
invitation
|
||||
.short_url
|
||||
.as_ref()
|
||||
.map(|url| format!("\nLink: {url}"))
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
ResponsePayload::Invitations(invitations) => {
|
||||
if invitations.is_empty() {
|
||||
"No invitations.".into()
|
||||
} else {
|
||||
invitations
|
||||
.iter()
|
||||
.map(|invitation| {
|
||||
format!(
|
||||
"{} ({:?}, {:?})",
|
||||
invitation.invitation_id, invitation.authority, invitation.state
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
}
|
||||
ResponsePayload::InvitationUpdated(invitation) => format!(
|
||||
"Invitation {} is {:?}",
|
||||
invitation.invitation_id, invitation.state
|
||||
),
|
||||
ResponsePayload::UserCreated { user_id, username } => {
|
||||
format!("Created user {} ({})", username, user_id)
|
||||
}
|
||||
|
|
@ -588,6 +620,7 @@ impl IpcClient {
|
|||
iota_ipc::IpcErrorCode::OmikronUnavailable => {
|
||||
"Omikron is unavailable; try reconnecting."
|
||||
}
|
||||
iota_ipc::IpcErrorCode::Unsupported => "The daemon does not implement this operation.",
|
||||
iota_ipc::IpcErrorCode::UnsupportedVersion => {
|
||||
"CLI and daemon versions are incompatible."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ pub enum AddUserPhase {
|
|||
pub struct AddUserFlow {
|
||||
pub phase: AddUserPhase,
|
||||
pub methods: MenuState<AddUserMethod>,
|
||||
pub invitation_authorities: MenuState<iota_ipc::InvitationAuthority>,
|
||||
pub username: TextInput,
|
||||
pub import_path: TextInput,
|
||||
pub credential: Option<iota_ipc::SecretString>,
|
||||
|
|
@ -39,7 +40,7 @@ impl AddUserFlow {
|
|||
methods: MenuState::new(vec![
|
||||
MenuItem {
|
||||
label: "Share an invitation".into(),
|
||||
description: Some("Not supported by the connected daemon".into()),
|
||||
description: Some("Create a single-use onboarding credential".into()),
|
||||
value: AddUserMethod::Invitation,
|
||||
enabled: invitation_supported,
|
||||
disabled_reason: Some("Not supported by the connected daemon".into()),
|
||||
|
|
@ -60,6 +61,22 @@ impl AddUserFlow {
|
|||
.then(|| "TU inspection is not supported by the connected daemon".into()),
|
||||
},
|
||||
]),
|
||||
invitation_authorities: MenuState::new(vec![
|
||||
MenuItem {
|
||||
label: "Omega".into(),
|
||||
description: Some("Central invitation authority".into()),
|
||||
value: iota_ipc::InvitationAuthority::Omega,
|
||||
enabled: true,
|
||||
disabled_reason: None,
|
||||
},
|
||||
MenuItem {
|
||||
label: "This Iota".into(),
|
||||
description: Some("Local invitation authority".into()),
|
||||
value: iota_ipc::InvitationAuthority::Iota,
|
||||
enabled: false,
|
||||
disabled_reason: Some("Local invitations are not implemented yet".into()),
|
||||
},
|
||||
]),
|
||||
username: TextInput::new("Username"),
|
||||
import_path: TextInput::new("TU path"),
|
||||
credential: None,
|
||||
|
|
|
|||
|
|
@ -399,6 +399,42 @@ impl UsersScreen {
|
|||
}),
|
||||
}
|
||||
}
|
||||
fn start_invitation(&mut self, authority: iota_ipc::InvitationAuthority) -> InteractionResult {
|
||||
self.pending = true;
|
||||
self.message = Some("Creating invitation…".into());
|
||||
self.overlay = None;
|
||||
let ipc = self.ipc.clone();
|
||||
InteractionResult::AppTask {
|
||||
task: Box::pin(async move {
|
||||
let result = match ipc
|
||||
.send_request(iota_ipc::LocalRequest::CreateInvitation {
|
||||
authority,
|
||||
lifetime_seconds: 7 * 24 * 60 * 60,
|
||||
password: None,
|
||||
label: None,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(iota_ipc::ResponseResult::Ok(
|
||||
iota_ipc::ResponsePayload::InvitationCreated(invitation),
|
||||
)) => Ok(format!(
|
||||
"Invitation {} created. Token: {} Link: {}",
|
||||
invitation.invitation_id,
|
||||
invitation.raw_token.0,
|
||||
invitation.short_url.as_deref().unwrap_or("unavailable")
|
||||
)),
|
||||
Ok(iota_ipc::ResponseResult::Error(error)) => {
|
||||
Err(format!("Create invitation failed: {error}"))
|
||||
}
|
||||
Ok(_) => Err(
|
||||
"Create invitation failed: daemon returned an unexpected response.".into(),
|
||||
),
|
||||
Err(error) => Err(format!("Create invitation failed: {error}")),
|
||||
};
|
||||
UiEvent::App(AppEvent::UserOperationFinished(result))
|
||||
}),
|
||||
}
|
||||
}
|
||||
fn refresh(&self) -> InteractionResult {
|
||||
let ipc = self.ipc.clone();
|
||||
InteractionResult::AppTask {
|
||||
|
|
@ -649,6 +685,33 @@ impl UsersScreen {
|
|||
inner,
|
||||
);
|
||||
}
|
||||
AddUserPhase::ConfigureInvitation => {
|
||||
let lines = flow
|
||||
.invitation_authorities
|
||||
.items()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, item)| {
|
||||
Line::from(format!(
|
||||
"{} {}{}",
|
||||
if flow.invitation_authorities.selected_index() == Some(index) {
|
||||
">"
|
||||
} else {
|
||||
" "
|
||||
},
|
||||
item.label,
|
||||
item.disabled_reason
|
||||
.as_ref()
|
||||
.map(|reason| format!(" ({reason})"))
|
||||
.unwrap_or_default()
|
||||
))
|
||||
})
|
||||
.chain(std::iter::once(Line::from(
|
||||
"\nEnter creates a single-use invitation valid for one week.",
|
||||
)))
|
||||
.collect::<Vec<_>>();
|
||||
frame.render_widget(Paragraph::new(lines), inner);
|
||||
}
|
||||
AddUserPhase::InspectingTu => {
|
||||
frame.render_widget(Paragraph::new("Inspecting credential…"), inner);
|
||||
}
|
||||
|
|
@ -991,6 +1054,29 @@ impl Screen for UsersScreen {
|
|||
InteractionResult::Handled
|
||||
}
|
||||
},
|
||||
AddUserPhase::ConfigureInvitation => {
|
||||
if flow.invitation_authorities.handle_key(key.code) {
|
||||
return InteractionResult::Handled;
|
||||
}
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
flow.phase = AddUserPhase::ChooseMethod;
|
||||
InteractionResult::Handled
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
let authority = flow
|
||||
.invitation_authorities
|
||||
.selected_item()
|
||||
.map(|item| item.value);
|
||||
if let Some(authority) = authority {
|
||||
self.start_invitation(authority)
|
||||
} else {
|
||||
InteractionResult::Handled
|
||||
}
|
||||
}
|
||||
_ => InteractionResult::Handled,
|
||||
}
|
||||
}
|
||||
AddUserPhase::ReviewImport => match key.code {
|
||||
KeyCode::Esc => {
|
||||
flow.phase = AddUserPhase::ConfigureImport;
|
||||
|
|
|
|||
Loading…
Reference in a new issue