fractal/src/login/mod.rs

651 lines
21 KiB
Rust
Raw Normal View History

use adw::{prelude::*, subclass::prelude::BinImpl};
use gettextrs::gettext;
use gtk::{self, gio, glib, glib::clone, subclass::prelude::*, CompositeTemplate};
use log::{error, warn};
use matrix_sdk::Client;
use ruma::{
api::client::session::{get_login_types::v3::LoginType, login},
OwnedServerName,
};
use strum::{AsRefStr, EnumString};
2022-10-05 19:13:57 +00:00
use url::Url;
2021-02-12 23:16:16 +00:00
2022-10-05 19:13:57 +00:00
mod advanced_dialog;
mod homeserver_page;
2022-03-26 17:32:57 +00:00
mod idp_button;
2022-10-05 19:13:57 +00:00
mod method_page;
mod sso_page;
2022-03-26 17:32:57 +00:00
2022-10-05 19:13:57 +00:00
use self::{
advanced_dialog::LoginAdvancedDialog, homeserver_page::LoginHomeserverPage,
method_page::LoginMethodPage, sso_page::LoginSsoPage,
};
use crate::{
prelude::*,
session::{model::Session, view::SessionVerification},
spawn, spawn_tokio, toast, Application, Window, RUNTIME,
};
2022-01-20 08:24:22 +00:00
#[derive(Clone, Debug, glib::Boxed)]
#[boxed_type(name = "BoxedLoginTypes")]
pub struct BoxedLoginTypes(Vec<LoginType>);
/// A page of the login stack.
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, AsRefStr)]
#[strum(serialize_all = "kebab-case")]
enum LoginPage {
/// The homeserver page.
Homeserver,
/// The page to select a login method.
Method,
/// The page to wait for SSO to be finished.
Sso,
/// The loading page.
Loading,
/// The session verification stack.
SessionVerification,
/// The login is completed.
Completed,
}
2021-02-12 23:16:16 +00:00
mod imp {
use std::cell::{Cell, RefCell};
2021-02-12 23:16:16 +00:00
use glib::{subclass::InitializingObject, SignalHandlerId};
2022-01-20 08:24:22 +00:00
use once_cell::sync::Lazy;
use super::*;
#[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/org/gnome/Fractal/ui/login/mod.ui")]
2021-04-13 18:21:45 +00:00
pub struct Login {
#[template_child]
pub back_button: TemplateChild<gtk::Button>,
#[template_child]
pub main_stack: TemplateChild<gtk::Stack>,
#[template_child]
2022-10-05 19:13:57 +00:00
pub homeserver_page: TemplateChild<LoginHomeserverPage>,
2022-02-03 16:20:49 +00:00
#[template_child]
2022-10-05 19:13:57 +00:00
pub method_page: TemplateChild<LoginMethodPage>,
2022-02-03 16:20:49 +00:00
#[template_child]
2022-10-05 19:13:57 +00:00
pub sso_page: TemplateChild<LoginSsoPage>,
#[template_child]
pub offline_banner: TemplateChild<adw::Banner>,
#[template_child]
pub done_button: TemplateChild<gtk::Button>,
pub prepared_source_id: RefCell<Option<SignalHandlerId>>,
pub logged_out_source_id: RefCell<Option<SignalHandlerId>>,
pub ready_source_id: RefCell<Option<SignalHandlerId>>,
/// Whether auto-discovery is enabled.
pub autodiscovery: Cell<bool>,
/// The login types supported by the homeserver.
pub login_types: RefCell<Vec<LoginType>>,
/// The domain of the homeserver to log into.
pub domain: RefCell<Option<OwnedServerName>>,
/// The URL of the homeserver to log into.
pub homeserver: RefCell<Option<Url>>,
/// The Matrix client used to log in.
pub client: RefCell<Option<Client>>,
/// The session that was just logged in.
pub session: RefCell<Option<Session>>,
2021-02-12 23:16:16 +00:00
}
2021-03-23 11:30:47 +00:00
#[glib::object_subclass]
2021-04-13 18:21:45 +00:00
impl ObjectSubclass for Login {
const NAME: &'static str = "Login";
type Type = super::Login;
2021-02-12 23:16:16 +00:00
type ParentType = adw::Bin;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
Self::Type::bind_template_callbacks(klass);
klass.set_css_name("login");
klass.set_accessible_role(gtk::AccessibleRole::Group);
2022-10-05 19:13:57 +00:00
klass.install_action("login.sso", Some("ms"), move |widget, _, variant| {
let idp_id = variant.and_then(|v| v.get::<Option<String>>()).flatten();
spawn!(clone!(@weak widget => async move {
widget.login_with_sso(idp_id).await;
}));
2022-10-05 19:13:57 +00:00
});
klass.install_action("login.open-advanced", None, move |widget, _, _| {
spawn!(clone!(@weak widget => async move {
widget.open_advanced_dialog().await;
}));
});
2021-02-12 23:16:16 +00:00
}
2021-03-23 11:30:47 +00:00
fn instance_init(obj: &InitializingObject<Self>) {
2021-02-12 23:16:16 +00:00
obj.init_template();
}
}
2021-04-13 18:21:45 +00:00
impl ObjectImpl for Login {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecString::builder("domain").read_only().build(),
glib::ParamSpecString::builder("homeserver")
.read_only()
.build(),
glib::ParamSpecBoolean::builder("autodiscovery")
.default_value(true)
.construct()
.explicit_notify()
.build(),
glib::ParamSpecBoxed::builder::<BoxedLoginTypes>("login-types")
.read_only()
.build(),
]
});
PROPERTIES.as_ref()
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let obj = self.obj();
match pspec.name() {
"domain" => obj.domain().to_value(),
"homeserver" => obj.homeserver_pretty().to_value(),
"autodiscovery" => obj.autodiscovery().to_value(),
"login-types" => BoxedLoginTypes(obj.login_types()).to_value(),
_ => unimplemented!(),
}
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"autodiscovery" => self.obj().set_autodiscovery(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn constructed(&self) {
let obj = self.obj();
obj.action_set_enabled("login.next", false);
self.parent_constructed();
let monitor = gio::NetworkMonitor::default();
monitor.connect_network_changed(clone!(@weak obj => move |_, _| {
obj.update_network_state();
}));
self.main_stack
.connect_visible_child_notify(clone!(@weak obj => move |_|
obj.focus_default();
));
2022-10-05 19:13:57 +00:00
obj.update_network_state();
}
fn dispose(&self) {
let obj = self.obj();
obj.drop_client();
obj.drop_session();
}
}
2021-04-13 18:21:45 +00:00
impl WidgetImpl for Login {}
2021-04-13 18:21:45 +00:00
impl BinImpl for Login {}
2021-02-12 23:16:16 +00:00
}
glib::wrapper! {
/// A widget handling the login flows.
2021-04-13 18:21:45 +00:00
pub struct Login(ObjectSubclass<imp::Login>)
2021-02-12 23:16:16 +00:00
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
}
#[gtk::template_callbacks]
2021-04-13 18:21:45 +00:00
impl Login {
2021-02-12 23:16:16 +00:00
pub fn new() -> Self {
glib::Object::new()
2021-02-12 23:16:16 +00:00
}
fn parent_window(&self) -> Window {
self.root()
.and_then(|root| root.downcast().ok())
.expect("Login needs to have a parent window")
}
/// The Matrix client.
pub async fn client(&self) -> Option<Client> {
if let Some(client) = self.imp().client.borrow().clone() {
return Some(client);
}
// If the client was dropped, try to recreate it.
self.imp().homeserver_page.check_homeserver().await;
if let Some(client) = self.imp().client.borrow().clone() {
return Some(client);
}
None
}
/// Set the Matrix client.
async fn set_client(&self, client: Option<Client>) {
let homeserver = if let Some(client) = client.clone() {
Some(
spawn_tokio!(async move { client.homeserver().await })
.await
.unwrap(),
)
} else {
None
};
self.set_homeserver(homeserver);
self.imp().client.replace(client);
}
/// Drop the Matrix client.
pub fn drop_client(&self) {
if let Some(client) = self.imp().client.take() {
// The `Client` needs to access a tokio runtime when it is dropped.
let guard = RUNTIME.enter();
RUNTIME.block_on(async move {
drop(client);
drop(guard);
});
}
}
/// Drop the session and clean up its data from the system.
fn drop_session(&self) {
if let Some(session) = self.imp().session.take() {
glib::MainContext::default().block_on(async move {
let _ = session.logout().await;
});
}
}
/// The domain of the homeserver to log into.
///
/// If autodiscovery is enabled, this is the server name, otherwise, this is
/// the prettified homeserver URL.
pub fn domain(&self) -> Option<String> {
if self.autodiscovery() {
self.imp().domain.borrow().clone().map(Into::into)
} else {
self.homeserver_pretty()
}
}
fn set_domain(&self, domain: Option<OwnedServerName>) {
let imp = self.imp();
if imp.domain.borrow().as_ref() == domain.as_ref() {
return;
}
imp.domain.replace(domain);
self.notify("domain");
}
/// The URL of the homeserver to log into.
pub fn homeserver(&self) -> Option<Url> {
self.imp().homeserver.borrow().clone()
}
/// The pretty-formatted URL of the homeserver to log into.
pub fn homeserver_pretty(&self) -> Option<String> {
let homeserver = self.homeserver();
homeserver
.as_ref()
.and_then(|url| url.as_ref().strip_suffix('/').map(ToOwned::to_owned))
.or_else(|| homeserver.as_ref().map(ToString::to_string))
}
/// Set the homeserver to log into.
pub fn set_homeserver(&self, homeserver: Option<Url>) {
let imp = self.imp();
if self.homeserver() == homeserver {
return;
}
imp.homeserver.replace(homeserver);
self.notify("homeserver");
if !self.autodiscovery() {
self.notify("domain");
}
}
/// Whether auto-discovery is enabled.
2022-10-05 19:13:57 +00:00
pub fn autodiscovery(&self) -> bool {
self.imp().autodiscovery.get()
}
/// Set whether auto-discovery is enabled
2022-10-05 19:13:57 +00:00
pub fn set_autodiscovery(&self, autodiscovery: bool) {
if self.autodiscovery() == autodiscovery {
return;
}
self.imp().autodiscovery.set(autodiscovery);
self.notify("autodiscovery");
}
/// The login types supported by the homeserver.
pub fn login_types(&self) -> Vec<LoginType> {
self.imp().login_types.borrow().clone()
}
/// Set the login types supported by the homeserver.
fn set_login_types(&self, types: Vec<LoginType>) {
self.imp().login_types.replace(types);
self.notify("login-types");
}
/// Whether the password login type is supported.
pub fn supports_password(&self) -> bool {
self.imp()
.login_types
.borrow()
.iter()
.any(|t| matches!(t, LoginType::Password(_)))
2022-10-05 19:13:57 +00:00
}
/// The visible page of the login stack.
fn visible_child(&self) -> LoginPage {
self.imp()
.main_stack
.visible_child_name()
.and_then(|s| s.as_str().try_into().ok())
.unwrap()
}
/// Set the visible page of the login stack.
fn set_visible_child(&self, visible_child: LoginPage) {
self.imp()
.main_stack
.set_visible_child_name(visible_child.as_ref());
}
/// The page to go back to for the current login stack page.
fn previous_page(&self) -> Option<LoginPage> {
match self.visible_child() {
LoginPage::Homeserver => None,
LoginPage::Method => Some(LoginPage::Homeserver),
LoginPage::Sso | LoginPage::Loading | LoginPage::SessionVerification => {
if self.supports_password() {
Some(LoginPage::Method)
2022-02-03 16:20:49 +00:00
} else {
Some(LoginPage::Homeserver)
}
2022-02-03 16:20:49 +00:00
}
// The go-back button should be deactivated.
LoginPage::Completed => None,
}
}
/// Go back to the previous step.
#[template_callback]
fn go_previous(&self) {
let session_verification = self.session_verification();
if let Some(session_verification) = &session_verification {
if session_verification.go_previous() {
// The session verification handled the action.
return;
}
}
let Some(previous_page) = self.previous_page() else {
self.parent_window().switch_to_greeter_page();
self.clean();
return;
};
self.set_visible_child(previous_page);
match previous_page {
LoginPage::Homeserver => {
// Drop the client because it is bound to the homeserver.
self.drop_client();
// Drop the session because it is bound to the homeserver and account.
self.drop_session();
self.imp().method_page.clean();
}
LoginPage::Method => {
// Drop the session because it is bound to the account.
self.drop_session();
}
_ => {}
}
}
async fn open_advanced_dialog(&self) {
let dialog = LoginAdvancedDialog::new(self.parent_window().upcast_ref());
self.bind_property("autodiscovery", &dialog, "autodiscovery")
.flags(glib::BindingFlags::SYNC_CREATE | glib::BindingFlags::BIDIRECTIONAL)
.build();
dialog.run_future().await;
}
/// Show the appropriate login screen given the current login types.
fn show_login_screen(&self) {
if self.supports_password() {
self.set_visible_child(LoginPage::Method);
} else {
spawn!(clone!(@weak self as obj => async move {
obj.login_with_sso(None).await;
}));
}
}
/// Log in with the SSO login type.
async fn login_with_sso(&self, idp_id: Option<String>) {
self.set_visible_child(LoginPage::Sso);
let client = self.client().await.unwrap();
2022-02-03 16:20:49 +00:00
let handle = spawn_tokio!(async move {
let mut login = client
.login_sso(|sso_url| async move {
let ctx = glib::MainContext::default();
ctx.spawn(async move {
spawn!(async move {
if let Err(error) = gtk::UriLauncher::new(&sso_url)
.launch_future(gtk::Window::NONE)
.await
{
// FIXME: We should forward the error.
error!("Could not launch URI: {error}");
}
});
});
Ok(())
})
.initial_device_display_name("Fractal");
if let Some(idp_id) = idp_id.as_deref() {
login = login.identity_provider_id(idp_id);
}
login.send().await
});
match handle.await.unwrap() {
Ok(response) => {
self.handle_login_response(response).await;
}
Err(error) => {
warn!("Failed to log in: {error}");
toast!(self, error.to_user_facing());
self.go_previous();
}
}
}
/// Handle the given response after successfully logging in.
async fn handle_login_response(&self, response: login::v3::Response) {
let client = self.client().await.unwrap();
// The homeserver could have changed with the login response so get it from the
// Client.
let homeserver = spawn_tokio!(async move { client.homeserver().await })
.await
.unwrap();
match Session::new(homeserver, response.into()).await {
Ok(session) => {
self.init_session(session).await;
}
Err(error) => {
warn!("Failed to create session: {error}");
toast!(self, error.to_user_facing());
self.go_previous();
}
}
}
pub async fn init_session(&self, session: Session) {
self.set_visible_child(LoginPage::Loading);
self.drop_client();
self.imp().session.replace(Some(session.clone()));
// Save ID of logging in session to GSettings
let settings = Application::default().settings();
if let Err(err) = settings.set_string("current-session", session.session_id()) {
warn!("Failed to save current session: {err}");
}
2022-10-22 08:48:23 +00:00
let session_info = session.info().clone();
let handle = spawn_tokio!(async move { session_info.store().await });
if let Err(error) = handle.await.unwrap() {
error!("Couldn't store session: {error}");
let (message, item) = error.into_parts();
self.parent_window().switch_to_error_page(
&format!("{}\n\n{}", gettext("Unable to store session"), message),
item,
);
return;
}
session.connect_ready(clone!(@weak self as obj => move |_| {
spawn!(clone!(@weak obj => async move {
obj.check_verification().await;
}));
}));
session.prepare().await;
}
/// Check whether the logged in session needs to be verified.
async fn check_verification(&self) {
let imp = self.imp();
let session = imp.session.borrow().clone().unwrap();
if session.is_verified().await {
self.finish_login();
return;
}
let stack = &imp.main_stack;
let widget = SessionVerification::new(self, &session);
stack.add_named(&widget, Some(LoginPage::SessionVerification.as_ref()));
stack.set_visible_child(&widget);
}
/// Get the session verification, if any.
fn session_verification(&self) -> Option<SessionVerification> {
self.imp()
.main_stack
.child_by_name(LoginPage::SessionVerification.as_ref())
.and_downcast()
}
/// Show the completed page.
#[template_callback]
pub fn show_completed(&self) {
let imp = self.imp();
imp.back_button.set_visible(false);
self.set_visible_child(LoginPage::Completed);
imp.done_button.grab_focus();
}
/// Finish the login process and show the session.
#[template_callback]
fn finish_login(&self) {
let session = self.imp().session.take().unwrap();
self.parent_window().add_session(&session);
self.clean();
2022-02-03 16:20:49 +00:00
}
/// Reset the login stack.
pub fn clean(&self) {
let imp = self.imp();
// Clean pages.
imp.homeserver_page.clean();
imp.method_page.clean();
if let Some(session_verification) = self.session_verification() {
imp.main_stack.remove(&session_verification);
}
// Clean data.
2022-10-05 19:13:57 +00:00
self.set_autodiscovery(true);
self.set_login_types(vec![]);
self.set_domain(None);
self.set_homeserver(None);
self.drop_client();
self.drop_session();
// Reinitialize UI.
self.set_visible_child(LoginPage::Homeserver);
imp.back_button.set_visible(true);
self.unfreeze();
}
/// Freeze the login screen.
fn freeze(&self) {
self.imp().main_stack.set_sensitive(false);
}
/// Unfreeze the login screen.
fn unfreeze(&self) {
self.imp().main_stack.set_sensitive(true);
}
/// Set focus to the proper widget of the current page.
pub fn focus_default(&self) {
let imp = self.imp();
match self.visible_child() {
LoginPage::Homeserver => {
imp.homeserver_page.focus_default();
}
LoginPage::Method => {
imp.method_page.focus_default();
}
_ => {}
}
}
fn update_network_state(&self) {
let imp = self.imp();
let monitor = gio::NetworkMonitor::default();
if !monitor.is_network_available() {
imp.offline_banner
.set_title(&gettext("No network connection"));
imp.offline_banner.set_revealed(true);
2022-10-05 19:13:57 +00:00
self.action_set_enabled("login.sso", false);
} else if monitor.connectivity() < gio::NetworkConnectivity::Full {
imp.offline_banner
.set_title(&gettext("No Internet connection"));
imp.offline_banner.set_revealed(true);
2022-10-05 19:13:57 +00:00
self.action_set_enabled("login.sso", true);
} else {
imp.offline_banner.set_revealed(false);
2022-10-05 19:13:57 +00:00
self.action_set_enabled("login.sso", true);
}
}
2021-02-12 23:16:16 +00:00
}