Add password authentication dialog

This commit is contained in:
Joscha 2022-08-21 01:19:07 +02:00
parent 19d75a1d15
commit 7b52add24e
3 changed files with 148 additions and 43 deletions

View file

@ -15,6 +15,7 @@ Procedure when bumping the version number:
## Unreleased ## Unreleased
### Added ### Added
- Authentication dialog for password-protected rooms
- Error popups in rooms when something goes wrong - Error popups in rooms when something goes wrong
### Changed ### Changed

View file

@ -6,7 +6,7 @@ use std::time::Duration;
use anyhow::bail; use anyhow::bail;
use cookie::{Cookie, CookieJar}; use cookie::{Cookie, CookieJar};
use euphoxide::api::packet::ParsedPacket; use euphoxide::api::packet::ParsedPacket;
use euphoxide::api::{Data, Log, Nick, Send, Snowflake, Time, UserId}; use euphoxide::api::{Auth, AuthOption, Data, Log, Nick, Send, Snowflake, Time, UserId};
use euphoxide::conn::{ConnRx, ConnTx, Joining, Status}; use euphoxide::conn::{ConnRx, ConnTx, Joining, Status};
use log::{error, info, warn}; use log::{error, info, warn};
use parking_lot::Mutex; use parking_lot::Mutex;
@ -46,6 +46,7 @@ enum Event {
// Commands // Commands
Status(oneshot::Sender<Option<Status>>), Status(oneshot::Sender<Option<Status>>),
RequestLogs, RequestLogs,
Auth(String),
Nick(String), Nick(String),
Send(Option<Snowflake>, String, oneshot::Sender<Snowflake>), Send(Option<Snowflake>, String, oneshot::Sender<Snowflake>),
} }
@ -188,6 +189,7 @@ impl State {
} }
Event::Status(reply_tx) => self.on_status(reply_tx).await, Event::Status(reply_tx) => self.on_status(reply_tx).await,
Event::RequestLogs => self.on_request_logs(), Event::RequestLogs => self.on_request_logs(),
Event::Auth(password) => self.on_auth(password),
Event::Nick(name) => self.on_nick(name), Event::Nick(name) => self.on_nick(name),
Event::Send(parent, content, id_tx) => self.on_send(parent, content, id_tx), Event::Send(parent, content, id_tx) => self.on_send(parent, content, id_tx),
} }
@ -321,6 +323,20 @@ impl State {
Ok(()) Ok(())
} }
fn on_auth(&self, password: String) {
if let Some(conn_tx) = &self.conn_tx {
let conn_tx = conn_tx.clone();
task::spawn(async move {
let _ = conn_tx
.send(Auth {
r#type: AuthOption::Passcode,
passcode: Some(password),
})
.await;
});
}
}
fn on_nick(&self, name: String) { fn on_nick(&self, name: String) {
if let Some(conn_tx) = &self.conn_tx { if let Some(conn_tx) = &self.conn_tx {
let conn_tx = conn_tx.clone(); let conn_tx = conn_tx.clone();
@ -389,6 +405,12 @@ impl Room {
rx.await.map_err(|_| Error::Stopped) rx.await.map_err(|_| Error::Stopped)
} }
pub fn auth(&self, password: String) -> Result<(), Error> {
self.event_tx
.send(Event::Auth(password))
.map_err(|_| Error::Stopped)
}
pub fn nick(&self, name: String) -> Result<(), Error> { pub fn nick(&self, name: String) -> Result<(), Error> {
self.event_tx self.event_tx
.send(Event::Nick(name)) .send(Event::Nick(name))

View file

@ -5,7 +5,7 @@ use std::sync::Arc;
use crossterm::event::KeyCode; use crossterm::event::KeyCode;
use crossterm::style::{Color, ContentStyle, Stylize}; use crossterm::style::{Color, ContentStyle, Stylize};
use euphoxide::api::{Data, PacketType, SessionType, SessionView, Snowflake}; use euphoxide::api::{Data, PacketType, SessionType, SessionView, Snowflake};
use euphoxide::conn::{Joined, Status}; use euphoxide::conn::{Joined, Joining, Status};
use parking_lot::FairMutex; use parking_lot::FairMutex;
use tokio::sync::oneshot::error::TryRecvError; use tokio::sync::oneshot::error::TryRecvError;
use tokio::sync::{mpsc, oneshot}; use tokio::sync::{mpsc, oneshot};
@ -35,7 +35,8 @@ use super::popup::RoomPopup;
enum State { enum State {
Normal, Normal,
ChooseNick(EditorState), Auth(EditorState),
Nick(EditorState),
} }
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
@ -153,10 +154,35 @@ impl EuphRoom {
} }
} }
pub async fn widget(&mut self) -> BoxedWidget { fn stabilize_state(&mut self, status: &RoomStatus) {
self.stabilize_pseudo_msg().await; match &self.state {
State::Auth(_)
if !matches!(
status,
RoomStatus::Connected(Status::Joining(Joining {
bounce: Some(_),
..
}))
) =>
{
self.state = State::Normal
}
State::Nick(_) if !matches!(status, RoomStatus::Connected(Status::Joined(_))) => {
self.state = State::Normal
}
_ => {}
}
}
async fn stabilize(&mut self, status: &RoomStatus) {
self.stabilize_pseudo_msg().await;
self.stabilize_state(status);
}
pub async fn widget(&mut self) -> BoxedWidget {
let status = self.status().await; let status = self.status().await;
self.stabilize(&status).await;
let chat = if let RoomStatus::Connected(Status::Joined(joined)) = &status { let chat = if let RoomStatus::Connected(Status::Joined(joined)) = &status {
self.widget_with_nick_list(&status, joined).await self.widget_with_nick_list(&status, joined).await
} else { } else {
@ -167,7 +193,8 @@ impl EuphRoom {
match &self.state { match &self.state {
State::Normal => {} State::Normal => {}
State::ChooseNick(editor) => layers.push(Self::choose_nick_widget(editor)), State::Auth(editor) => layers.push(Self::auth_widget(editor)),
State::Nick(editor) => layers.push(Self::nick_widget(editor)),
} }
for popup in &self.popups { for popup in &self.popups {
@ -177,7 +204,14 @@ impl EuphRoom {
Layer::new(layers).into() Layer::new(layers).into()
} }
fn choose_nick_widget(editor: &EditorState) -> BoxedWidget { fn auth_widget(editor: &EditorState) -> BoxedWidget {
Popup::new(Padding::new(editor.widget()).left(1))
.title("Enter password")
.inner_padding(false)
.build()
}
fn nick_widget(editor: &EditorState) -> BoxedWidget {
let editor = editor let editor = editor
.widget() .widget()
.highlight(|s| Styled::new(s, euph::nick_style(s))); .highlight(|s| Styled::new(s, euph::nick_style(s)));
@ -373,14 +407,21 @@ impl EuphRoom {
match &self.state { match &self.state {
State::Normal => { State::Normal => {
// TODO Use if-let chain
bindings.binding("esc", "leave room"); bindings.binding("esc", "leave room");
let can_compose = if let Some(room) = &self.room { let can_compose = if let Some(room) = &self.room {
if let Ok(Some(Status::Joined(_))) = room.status().await { match room.status().await {
Ok(Some(Status::Joining(Joining {
bounce: Some(_), ..
}))) => {
bindings.binding("a", "authenticate");
false
}
Ok(Some(Status::Joined(_))) => {
bindings.binding("n", "change nick"); bindings.binding("n", "change nick");
true true
} else { }
false _ => false,
} }
} else { } else {
false false
@ -389,7 +430,12 @@ impl EuphRoom {
bindings.empty(); bindings.empty();
self.chat.list_key_bindings(bindings, can_compose).await; self.chat.list_key_bindings(bindings, can_compose).await;
} }
State::ChooseNick(_) => { State::Auth(_) => {
bindings.binding("esc", "abort");
bindings.binding("enter", "authenticate");
util::list_editor_key_bindings(bindings, Self::nick_char, false);
}
State::Nick(_) => {
bindings.binding("esc", "abort"); bindings.binding("esc", "abort");
bindings.binding("enter", "set nick"); bindings.binding("enter", "set nick");
util::list_editor_key_bindings(bindings, Self::nick_char, false); util::list_editor_key_bindings(bindings, Self::nick_char, false);
@ -413,12 +459,15 @@ impl EuphRoom {
match &self.state { match &self.state {
State::Normal => { State::Normal => {
// TODO Use if-let chain
if let Some(room) = &self.room { if let Some(room) = &self.room {
if let Ok(Some(Status::Joined(joined))) = room.status().await { let status = room.status().await;
let can_compose = matches!(status, Ok(Some(Status::Joined(_))));
// We need to handle chat input first, otherwise the other
// key bindings will shadow characters in the editor.
match self match self
.chat .chat
.handle_input_event(terminal, crossterm_lock, event, true) .handle_input_event(terminal, crossterm_lock, event, can_compose)
.await .await
{ {
Reaction::NotHandled => {} Reaction::NotHandled => {}
@ -432,23 +481,56 @@ impl EuphRoom {
} }
} }
match status {
Ok(Some(Status::Joining(Joining {
bounce: Some(_), ..
}))) => {
if let key!('a') | key!('A') = event {
self.state = State::Auth(EditorState::new());
return true;
}
false
}
Ok(Some(Status::Joined(joined))) => {
if let key!('n') | key!('N') = event { if let key!('n') | key!('N') = event {
self.state = State::ChooseNick(EditorState::with_initial_text( self.state = State::Nick(EditorState::with_initial_text(
joined.session.name.clone(), joined.session.name,
)); ));
return true; return true;
} }
true
return false;
} }
_ => false,
} }
} else {
self.chat self.chat
.handle_input_event(terminal, crossterm_lock, event, false) .handle_input_event(terminal, crossterm_lock, event, false)
.await .await
.handled() .handled()
} }
State::ChooseNick(ed) => match event { }
State::Auth(ed) => match event {
key!(Esc) => {
self.state = State::Normal;
true
}
key!(Enter) => {
if let Some(room) = &self.room {
let _ = room.auth(ed.text());
}
self.state = State::Normal;
true
}
_ => util::handle_editor_input_event(
ed,
terminal,
crossterm_lock,
event,
Self::nick_char,
false,
),
},
State::Nick(ed) => match event {
key!(Esc) => { key!(Esc) => {
self.state = State::Normal; self.state = State::Normal;
true true