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