1use std::net::{Ipv4Addr, Ipv6Addr};
2
3use adw::{prelude::*, subclass::prelude::*};
4use gettextrs::gettext;
5use gtk::{gio, glib, glib::clone};
6use matrix_sdk::{
7 Client,
8 authentication::oauth::{
9 ClientRegistrationData,
10 registration::{ApplicationType, ClientMetadata, Localized, OAuthGrantType},
11 },
12 sanitize_server_name,
13 utils::local_server::LocalServerRedirectHandle,
14};
15use ruma::{OwnedServerName, api::client::session::get_login_types::v3::LoginType, serde::Raw};
16use tracing::warn;
17use url::Url;
18
19mod advanced_dialog;
20mod greeter;
21mod homeserver_page;
22mod in_browser_page;
23mod local_server;
24mod method_page;
25mod session_setup_view;
26
27use self::{
28 advanced_dialog::LoginAdvancedDialog,
29 greeter::Greeter,
30 homeserver_page::LoginHomeserverPage,
31 in_browser_page::{LoginInBrowserData, LoginInBrowserPage},
32 local_server::spawn_local_server,
33 method_page::LoginMethodPage,
34 session_setup_view::SessionSetupView,
35};
36use crate::{
37 APP_HOMEPAGE_URL, APP_NAME, Application, RUNTIME, SETTINGS_KEY_CURRENT_SESSION, Window,
38 components::OfflineBanner, prelude::*, secret::Secret, session::Session, spawn, spawn_tokio,
39 toast,
40};
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44enum LoginPage {
45 Greeter,
47 Homeserver,
49 Method,
51 InBrowser,
53 SessionSetup,
55 Completed,
57}
58
59impl LoginPage {
60 const fn tag(self) -> &'static str {
62 match self {
63 Self::Greeter => Greeter::TAG,
64 Self::Homeserver => LoginHomeserverPage::TAG,
65 Self::Method => LoginMethodPage::TAG,
66 Self::InBrowser => LoginInBrowserPage::TAG,
67 Self::SessionSetup => SessionSetupView::TAG,
68 Self::Completed => "completed",
69 }
70 }
71
72 fn from_tag(tag: &str) -> Self {
76 match tag {
77 Greeter::TAG => Self::Greeter,
78 LoginHomeserverPage::TAG => Self::Homeserver,
79 LoginMethodPage::TAG => Self::Method,
80 LoginInBrowserPage::TAG => Self::InBrowser,
81 SessionSetupView::TAG => Self::SessionSetup,
82 "completed" => Self::Completed,
83 _ => panic!("Unknown LoginPage: {tag}"),
84 }
85 }
86}
87
88mod imp {
89 use std::cell::{Cell, RefCell};
90
91 use glib::subclass::InitializingObject;
92
93 use super::*;
94
95 #[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
96 #[template(resource = "/org/gnome/Fractal/ui/login/mod.ui")]
97 #[properties(wrapper_type = super::Login)]
98 pub struct Login {
99 #[template_child]
100 navigation: TemplateChild<adw::NavigationView>,
101 #[template_child]
102 greeter: TemplateChild<Greeter>,
103 #[template_child]
104 homeserver_page: TemplateChild<LoginHomeserverPage>,
105 #[template_child]
106 method_page: TemplateChild<LoginMethodPage>,
107 #[template_child]
108 in_browser_page: TemplateChild<LoginInBrowserPage>,
109 #[template_child]
110 done_button: TemplateChild<gtk::Button>,
111 #[property(get, set = Self::set_autodiscovery, construct, explicit_notify, default = true)]
113 autodiscovery: Cell<bool>,
114 client: RefCell<Option<Client>>,
116 session: RefCell<Option<Session>>,
118 }
119
120 #[glib::object_subclass]
121 impl ObjectSubclass for Login {
122 const NAME: &'static str = "Login";
123 type Type = super::Login;
124 type ParentType = adw::Bin;
125
126 fn class_init(klass: &mut Self::Class) {
127 OfflineBanner::ensure_type();
128
129 Self::bind_template(klass);
130 Self::bind_template_callbacks(klass);
131
132 klass.set_css_name("login");
133 klass.set_accessible_role(gtk::AccessibleRole::Group);
134
135 klass.install_action_async("login.sso", None, |obj, _, _| async move {
136 obj.imp().init_matrix_sso_login().await;
137 });
138
139 klass.install_action_async("login.open-advanced", None, |obj, _, _| async move {
140 obj.imp().open_advanced_dialog().await;
141 });
142 }
143
144 fn instance_init(obj: &InitializingObject<Self>) {
145 obj.init_template();
146 }
147 }
148
149 #[glib::derived_properties]
150 impl ObjectImpl for Login {
151 fn constructed(&self) {
152 self.parent_constructed();
153 let obj = self.obj();
154
155 let monitor = gio::NetworkMonitor::default();
156 monitor.connect_network_changed(clone!(
157 #[weak]
158 obj,
159 move |_, available| {
160 obj.action_set_enabled("login.sso", available);
161 }
162 ));
163 obj.action_set_enabled("login.sso", monitor.is_network_available());
164
165 self.navigation.connect_visible_page_notify(clone!(
166 #[weak(rename_to = imp)]
167 self,
168 move |_| {
169 imp.visible_page_changed();
170 }
171 ));
172 }
173
174 fn dispose(&self) {
175 self.drop_client();
176 self.drop_session();
177 }
178 }
179
180 impl WidgetImpl for Login {
181 fn grab_focus(&self) -> bool {
182 match self.visible_page() {
183 LoginPage::Greeter => self.greeter.grab_focus(),
184 LoginPage::Homeserver => self.homeserver_page.grab_focus(),
185 LoginPage::Method => self.method_page.grab_focus(),
186 LoginPage::InBrowser => self.in_browser_page.grab_focus(),
187 LoginPage::SessionSetup => {
188 if let Some(session_setup) = self.session_setup() {
189 session_setup.grab_focus()
190 } else {
191 false
192 }
193 }
194 LoginPage::Completed => self.done_button.grab_focus(),
195 }
196 }
197 }
198
199 impl BinImpl for Login {}
200 impl AccessibleImpl for Login {}
201
202 #[gtk::template_callbacks]
203 impl Login {
204 pub(super) fn visible_page(&self) -> LoginPage {
206 LoginPage::from_tag(
207 &self
208 .navigation
209 .visible_page()
210 .expect("Login navigation view should always have a visible page")
211 .tag()
212 .expect("Login navigation page should always have a tag"),
213 )
214 }
215
216 pub fn set_autodiscovery(&self, autodiscovery: bool) {
218 if self.autodiscovery.get() == autodiscovery {
219 return;
220 }
221
222 self.autodiscovery.set(autodiscovery);
223 self.obj().notify_autodiscovery();
224 }
225
226 pub(super) fn session_setup(&self) -> Option<SessionSetupView> {
228 self.navigation
229 .find_page(LoginPage::SessionSetup.tag())
230 .and_downcast()
231 }
232
233 fn visible_page_changed(&self) {
235 match self.visible_page() {
236 LoginPage::Greeter => {
237 self.clean();
238 }
239 LoginPage::Homeserver => {
240 self.drop_client();
242 self.drop_session();
244 self.method_page.clean();
245 }
246 LoginPage::Method => {
247 self.drop_session();
249 }
250 _ => {}
251 }
252 }
253
254 pub(super) async fn client(&self) -> Option<Client> {
256 if let Some(client) = self.client.borrow().clone() {
257 return Some(client);
258 }
259
260 let autodiscovery = self.autodiscovery.get();
262 let client = self.homeserver_page.build_client(autodiscovery).await.ok();
263 self.set_client(client.clone());
264
265 client
266 }
267
268 pub(super) fn set_client(&self, client: Option<Client>) {
270 self.client.replace(client);
271 }
272
273 pub(super) fn drop_client(&self) {
275 if let Some(client) = self.client.take() {
276 let _guard = RUNTIME.enter();
278 drop(client);
279 }
280 }
281
282 fn drop_session(&self) {
284 if let Some(session) = self.session.take() {
285 spawn!(async move {
286 let _ = session.log_out().await;
287 });
288 }
289 }
290
291 async fn open_advanced_dialog(&self) {
293 let obj = self.obj();
294 let dialog = LoginAdvancedDialog::new();
295 obj.bind_property("autodiscovery", &dialog, "autodiscovery")
296 .sync_create()
297 .bidirectional()
298 .build();
299 dialog.run_future(&*obj).await;
300 }
301
302 pub(super) async fn init_oauth_login(&self) {
304 let Some(client) = self.client.borrow().clone() else {
305 return;
306 };
307
308 let Ok((redirect_uri, local_server_handle)) = spawn_local_server().await else {
309 return;
310 };
311
312 let oauth = client.oauth();
313 let handle = spawn_tokio!(async move {
314 oauth
315 .login(redirect_uri, None, Some(client_registration_data()), None)
316 .build()
317 .await
318 });
319
320 let authorization_data = match handle.await.expect("task was not aborted") {
321 Ok(authorization_data) => authorization_data,
322 Err(error) => {
323 warn!("Could not construct OAuth 2.0 authorization URL: {error}");
324 toast!(self.obj(), gettext("Could not set up login"));
325 return;
326 }
327 };
328
329 self.show_in_browser_page(
330 local_server_handle,
331 LoginInBrowserData::Oauth(authorization_data),
332 );
333 }
334
335 pub(super) async fn init_matrix_login(&self) {
337 let Some(client) = self.client.borrow().clone() else {
338 return;
339 };
340
341 let matrix_auth = client.matrix_auth();
342 let handle = spawn_tokio!(async move { matrix_auth.get_login_types().await });
343
344 let login_types = match handle.await.expect("task was not aborted") {
345 Ok(response) => response.flows,
346 Err(error) => {
347 warn!("Could not get available Matrix login types: {error}");
348 toast!(self.obj(), gettext("Could not set up login"));
349 return;
350 }
351 };
352
353 let supports_password = login_types
354 .iter()
355 .any(|login_type| matches!(login_type, LoginType::Password(_)));
356 let supports_sso = login_types
357 .iter()
358 .any(|login_type| matches!(login_type, LoginType::Sso(_)));
359
360 if supports_password {
361 let server_name = self
362 .autodiscovery
363 .get()
364 .then(|| self.homeserver_page.homeserver())
365 .and_then(|s| sanitize_server_name(&s).ok());
366
367 self.show_method_page(&client.homeserver(), server_name.as_ref(), supports_sso);
368 } else {
369 self.init_matrix_sso_login().await;
370 }
371 }
372
373 pub(super) async fn init_matrix_sso_login(&self) {
375 let Some(client) = self.client.borrow().clone() else {
376 return;
377 };
378
379 let Ok((redirect_uri, local_server_handle)) = spawn_local_server().await else {
380 return;
381 };
382
383 let matrix_auth = client.matrix_auth();
384 let handle = spawn_tokio!(async move {
385 matrix_auth
386 .get_sso_login_url(redirect_uri.as_str(), None)
387 .await
388 });
389
390 match handle.await.expect("task was not aborted") {
391 Ok(url) => {
392 let url = Url::parse(&url).expect("Matrix SSO URL should be a valid URL");
393 self.show_in_browser_page(local_server_handle, LoginInBrowserData::Matrix(url));
394 }
395 Err(error) => {
396 warn!("Could not build Matrix SSO URL: {error}");
397 toast!(self.obj(), gettext("Could not set up login"));
398 }
399 }
400 }
401
402 fn show_method_page(
404 &self,
405 homeserver: &Url,
406 server_name: Option<&OwnedServerName>,
407 supports_sso: bool,
408 ) {
409 self.method_page
410 .update(homeserver, server_name, supports_sso);
411 self.navigation.push_by_tag(LoginPage::Method.tag());
412 }
413
414 fn show_in_browser_page(
416 &self,
417 local_server_handle: LocalServerRedirectHandle,
418 data: LoginInBrowserData,
419 ) {
420 self.in_browser_page.set_up(local_server_handle, data);
421 self.navigation.push_by_tag(LoginPage::InBrowser.tag());
422 }
423
424 pub(super) async fn create_session(&self) {
426 let client = self.client().await.expect("client should be constructed");
427
428 match Session::create(&client).await {
429 Ok(session) => {
430 self.init_session(session).await;
431 }
432 Err(error) => {
433 warn!("Could not create session: {error}");
434 toast!(self.obj(), error.to_user_facing());
435
436 self.navigation.pop();
437 }
438 }
439 }
440
441 async fn init_session(&self, session: Session) {
443 let setup_view = SessionSetupView::new(&session);
444 setup_view.connect_completed(clone!(
445 #[weak(rename_to = imp)]
446 self,
447 move |_| {
448 imp.navigation.push_by_tag(LoginPage::Completed.tag());
449 }
450 ));
451 self.navigation.push(&setup_view);
452
453 self.drop_client();
454 self.session.replace(Some(session.clone()));
455
456 let settings = Application::default().settings();
458 if let Err(err) =
459 settings.set_string(SETTINGS_KEY_CURRENT_SESSION, session.session_id())
460 {
461 warn!("Could not save current session: {err}");
462 }
463
464 let session_info = session.info().clone();
465
466 if Secret::store_session(session_info).await.is_err() {
467 toast!(self.obj(), gettext("Could not store session"));
468 }
469
470 session.prepare().await;
471 }
472
473 #[template_callback]
475 fn finish_login(&self) {
476 let Some(window) = self.obj().root().and_downcast::<Window>() else {
477 return;
478 };
479
480 if let Some(session) = self.session.take() {
481 window.add_session(session);
482 }
483
484 self.clean();
485 }
486
487 pub(super) fn clean(&self) {
489 self.homeserver_page.clean();
491 self.method_page.clean();
492
493 self.set_autodiscovery(true);
495 self.drop_client();
496 self.drop_session();
497
498 self.navigation.pop_to_tag(LoginPage::Greeter.tag());
500 self.unfreeze();
501 }
502
503 pub(super) fn freeze(&self) {
505 self.navigation.set_sensitive(false);
506 }
507
508 pub(super) fn unfreeze(&self) {
510 self.navigation.set_sensitive(true);
511 }
512 }
513}
514
515glib::wrapper! {
516 pub struct Login(ObjectSubclass<imp::Login>)
518 @extends gtk::Widget, adw::Bin,
519 @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
520}
521
522impl Login {
523 pub fn new() -> Self {
524 glib::Object::new()
525 }
526
527 fn set_client(&self, client: Option<Client>) {
529 self.imp().set_client(client);
530 }
531
532 async fn client(&self) -> Option<Client> {
534 self.imp().client().await
535 }
536
537 fn drop_client(&self) {
539 self.imp().drop_client();
540 }
541
542 fn freeze(&self) {
544 self.imp().freeze();
545 }
546
547 fn unfreeze(&self) {
549 self.imp().unfreeze();
550 }
551
552 async fn init_oauth_login(&self) {
554 self.imp().init_oauth_login().await;
555 }
556
557 async fn init_matrix_login(&self) {
559 self.imp().init_matrix_login().await;
560 }
561
562 async fn create_session(&self) {
564 self.imp().create_session().await;
565 }
566}
567
568fn client_registration_data() -> ClientRegistrationData {
570 let ipv4_localhost_uri = Url::parse(&format!("http://{}/", Ipv4Addr::LOCALHOST))
573 .expect("IPv4 localhost address should be a valid URL");
574 let ipv6_localhost_uri = Url::parse(&format!("http://[{}]/", Ipv6Addr::LOCALHOST))
575 .expect("IPv6 localhost address should be a valid URL");
576
577 let client_uri =
578 Url::parse(APP_HOMEPAGE_URL).expect("application homepage URL should be a valid URL");
579
580 let mut client_metadata = ClientMetadata::new(
581 ApplicationType::Native,
582 vec![OAuthGrantType::AuthorizationCode {
583 redirect_uris: vec![ipv4_localhost_uri, ipv6_localhost_uri],
584 }],
585 Localized::new(client_uri, None),
586 );
587 client_metadata.client_name = Some(Localized::new(APP_NAME.to_owned(), None));
588
589 Raw::new(&client_metadata)
590 .expect("client metadata should serialize to JSON successfully")
591 .into()
592}