From 1f90f6108e496dc22310eb0588a7bd8d535ab204 Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Sat, 30 Jan 2021 23:16:44 +0100 Subject: [PATCH] Add registration dialog --- res/musicus.gresource.xml | 1 + res/ui/login_dialog.ui | 90 +++++++++---- res/ui/register_dialog.ui | 233 +++++++++++++++++++++++++++++++++ src/backend/client/mod.rs | 3 + src/backend/client/register.rs | 49 +++++++ src/dialogs/login_dialog.rs | 22 ++++ src/dialogs/mod.rs | 3 + src/dialogs/register.rs | 149 +++++++++++++++++++++ src/meson.build | 2 + 9 files changed, 530 insertions(+), 22 deletions(-) create mode 100644 res/ui/register_dialog.ui create mode 100644 src/backend/client/register.rs create mode 100644 src/dialogs/register.rs diff --git a/res/musicus.gresource.xml b/res/musicus.gresource.xml index bea74fa..9784187 100644 --- a/res/musicus.gresource.xml +++ b/res/musicus.gresource.xml @@ -15,6 +15,7 @@ ui/preferences.ui ui/recording_editor.ui ui/recording_screen.ui + ui/register_dialog.ui ui/selector.ui ui/server_dialog.ui ui/source_selector.ui diff --git a/res/ui/login_dialog.ui b/res/ui/login_dialog.ui index 2576b5c..56508c4 100644 --- a/res/ui/login_dialog.ui +++ b/res/ui/login_dialog.ui @@ -47,38 +47,84 @@ 12 18 12 - 500 - 300 + 800 - - start + + vertical + 12 - - none + + start + Login to existing account + + + + + + + + start - - True - Username - username_entry + + none - - center - True + + True + Username + username_entry + + + center + True + + + + + + + True + Password + password_entry + + + center + True + False + password + + + + + + + start + Create a new account + + + + + + + + start - - True - Password - password_entry + + none - - center - True - False - password + + True + Register a new account + register_button + + + Start + center + + diff --git a/res/ui/register_dialog.ui b/res/ui/register_dialog.ui new file mode 100644 index 0000000..65a2ade --- /dev/null +++ b/res/ui/register_dialog.ui @@ -0,0 +1,233 @@ + + + + + + crossfade + + + loading + + + vertical + + + false + false + + + Register + + + + + + + + true + true + true + center + center + + + + + + + + + content + + + vertical + + + false + false + + + + + + + go-previous-symbolic + + + + + Create account + + + + + + + + False + + + + + true + + + 12 + 12 + 18 + 12 + 800 + + + vertical + 12 + + + start + Personal data + + + + + + + + start + + + none + + + True + Username + username_entry + + + center + True + + + + + + + True + E-mail (optional) + email_entry + + + center + True + + + + + + + + + + + start + Password + + + + + + + + start + + + none + + + True + Password + password_entry + + + center + True + False + password + + + + + + + True + Repeat password + repeat_password_entry + + + center + True + False + password + + + + + + + + + + + start + Captcha + + + + + + + + start + + + none + + + 0 + Feel free to look for the answer online! + + + + + True + Your answer + captcha_entry + + + center + True + False + True + + + + + + + + + + + + + + + + + + + + diff --git a/src/backend/client/mod.rs b/src/backend/client/mod.rs index 34cfbc2..f4a45f5 100644 --- a/src/backend/client/mod.rs +++ b/src/backend/client/mod.rs @@ -22,6 +22,9 @@ pub use persons::*; pub mod recordings; pub use recordings::*; +pub mod register; +pub use register::*; + pub mod works; pub use works::*; diff --git a/src/backend/client/register.rs b/src/backend/client/register.rs new file mode 100644 index 0000000..bfe639e --- /dev/null +++ b/src/backend/client/register.rs @@ -0,0 +1,49 @@ +use super::Backend; +use anyhow::{anyhow, Result}; +use isahc::http::StatusCode; +use isahc::prelude::*; +use serde::{Deserialize, Serialize}; +use std::time::Duration; + +/// Response body data for captcha requests. +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Captcha { + pub id: String, + pub question: String, +} + +/// Request body data for user registration. +#[derive(Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct UserRegistration { + pub username: String, + pub password: String, + pub email: Option, + pub captcha_id: String, + pub answer: String, +} + +impl Backend { + /// Request a new captcha for registration. + pub async fn get_captcha(&self) -> Result { + let body = self.get("captcha").await?; + let captcha = serde_json::from_str(&body)?; + Ok(captcha) + } + + /// Register a new user and return whether the process suceeded. This will + /// not store the new login credentials. + pub async fn register(&self, data: UserRegistration) -> Result { + let server_url = self.get_server_url().ok_or(anyhow!("No server URL set!"))?; + + let mut response = Request::post(format!("{}/users", server_url)) + .timeout(Duration::from_secs(10)) + .header("Content-Type", "application/json") + .body(serde_json::to_string(&data)?)? + .send_async() + .await?; + + Ok(response.status() == StatusCode::OK) + } +} diff --git a/src/dialogs/login_dialog.rs b/src/dialogs/login_dialog.rs index 6111d1a..f7763f0 100644 --- a/src/dialogs/login_dialog.rs +++ b/src/dialogs/login_dialog.rs @@ -1,3 +1,4 @@ +use super::RegisterDialog; use crate::backend::{Backend, LoginData}; use crate::widgets::{Navigator, NavigatorScreen}; use glib::clone; @@ -29,6 +30,7 @@ impl LoginDialog { get_widget!(builder, gtk::Button, login_button); get_widget!(builder, gtk::Entry, username_entry); get_widget!(builder, gtk::Entry, password_entry); + get_widget!(builder, gtk::Button, register_button); let this = Rc::new(Self { backend, @@ -77,6 +79,26 @@ impl LoginDialog { }); })); + register_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let dialog = RegisterDialog::new(this.backend.clone()); + + dialog.set_selected_cb(clone!(@strong this => move |data| { + if let Some(cb) = &*this.selected_cb.borrow() { + cb(data); + } + + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } + })); + + navigator.push(dialog); + } + })); + this } diff --git a/src/dialogs/mod.rs b/src/dialogs/mod.rs index b244bda..c04e7bc 100644 --- a/src/dialogs/mod.rs +++ b/src/dialogs/mod.rs @@ -7,5 +7,8 @@ pub use login_dialog::*; pub mod preferences; pub use preferences::*; +pub mod register; +pub use register::*; + pub mod server_dialog; pub use server_dialog::*; diff --git a/src/dialogs/register.rs b/src/dialogs/register.rs new file mode 100644 index 0000000..727713d --- /dev/null +++ b/src/dialogs/register.rs @@ -0,0 +1,149 @@ +use crate::backend::{Backend, LoginData, UserRegistration}; +use crate::widgets::{Navigator, NavigatorScreen}; +use glib::clone; +use gtk::prelude::*; +use gtk_macros::get_widget; +use libadwaita::prelude::*; +use std::cell::RefCell; +use std::rc::Rc; + +/// A dialog for creating a new user account. +pub struct RegisterDialog { + backend: Rc, + widget: gtk::Stack, + username_entry: gtk::Entry, + email_entry: gtk::Entry, + password_entry: gtk::Entry, + repeat_password_entry: gtk::Entry, + captcha_row: libadwaita::ActionRow, + captcha_entry: gtk::Entry, + captcha_id: RefCell>, + selected_cb: RefCell>>, + navigator: RefCell>>, +} + +impl RegisterDialog { + /// Create a new register dialog. + pub fn new(backend: Rc) -> Rc { + // Create UI + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/register_dialog.ui"); + + get_widget!(builder, gtk::Stack, widget); + get_widget!(builder, gtk::Button, cancel_button); + get_widget!(builder, gtk::Button, register_button); + get_widget!(builder, gtk::Entry, username_entry); + get_widget!(builder, gtk::Entry, email_entry); + get_widget!(builder, gtk::Entry, password_entry); + get_widget!(builder, gtk::Entry, repeat_password_entry); + get_widget!(builder, libadwaita::ActionRow, captcha_row); + get_widget!(builder, gtk::Entry, captcha_entry); + + let this = Rc::new(Self { + backend, + widget, + username_entry, + email_entry, + password_entry, + repeat_password_entry, + captcha_row, + captcha_entry, + captcha_id: RefCell::new(None), + selected_cb: RefCell::new(None), + navigator: RefCell::new(None), + }); + + // Connect signals and callbacks + + cancel_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } + })); + + register_button.connect_clicked(clone!(@strong this => move |_| { + let password = this.password_entry.get_text().unwrap().to_string(); + let repeat = this.repeat_password_entry.get_text().unwrap().to_string(); + + if (password != repeat) { + // TODO: Show error and validate other input. + } else { + this.widget.set_visible_child_name("loading"); + + let context = glib::MainContext::default(); + let clone = this.clone(); + context.spawn_local(async move { + let username = clone.username_entry.get_text().unwrap().to_string(); + let email = clone.email_entry.get_text().unwrap().to_string(); + let captcha_id = clone.captcha_id.borrow().clone().unwrap(); + let answer = clone.captcha_entry.get_text().unwrap().to_string(); + + let email = if email.len() == 0 { + None + } else { + Some(email) + }; + + let registration = UserRegistration { + username: username.clone(), + password: password.clone(), + email, + captcha_id, + answer, + }; + + // TODO: Handle errors. + if clone.backend.register(registration).await.unwrap() { + if let Some(cb) = &*clone.selected_cb.borrow() { + let data = LoginData { + username, + password, + }; + + cb(data); + } + + let navigator = clone.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } + } else { + clone.widget.set_visible_child_name("content"); + } + }); + } + })); + + // Initialize + + let context = glib::MainContext::default(); + let clone = this.clone(); + context.spawn_local(async move { + let captcha = clone.backend.get_captcha().await.unwrap(); + clone.captcha_row.set_title(Some(&captcha.question)); + clone.captcha_id.replace(Some(captcha.id)); + clone.widget.set_visible_child_name("content"); + }); + + this + } + + /// The closure to call when the login succeded. + pub fn set_selected_cb(&self, cb: F) { + self.selected_cb.replace(Some(Box::new(cb))); + } +} + +impl NavigatorScreen for RegisterDialog { + fn attach_navigator(&self, navigator: Rc) { + self.navigator.replace(Some(navigator)); + } + + fn get_widget(&self) -> gtk::Widget { + self.widget.clone().upcast() + } + + fn detach_navigator(&self) { + self.navigator.replace(None); + } +} diff --git a/src/meson.build b/src/meson.build index f708860..85c77f1 100644 --- a/src/meson.build +++ b/src/meson.build @@ -39,6 +39,7 @@ sources = files( 'backend/client/mod.rs', 'backend/client/persons.rs', 'backend/client/recordings.rs', + 'backend/client/register.rs', 'backend/client/works.rs', 'backend/library.rs', 'backend/mod.rs', @@ -56,6 +57,7 @@ sources = files( 'dialogs/login_dialog.rs', 'dialogs/mod.rs', 'dialogs/preferences.rs', + 'dialogs/register.rs', 'dialogs/server_dialog.rs', 'editors/ensemble.rs', 'editors/instrument.rs',