Various changes (mostly finish logins)
This commit is contained in:
parent
94801968fa
commit
67440ccca1
@ -7,5 +7,6 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gtk4 = { version = "0.7.3", features = ["blueprint", "v4_12"] }
|
gtk4 = { version = "0.7.3", features = ["blueprint", "v4_12"] }
|
||||||
|
once_cell = "1.18.0"
|
||||||
simplelogin = { version = "0.1.0", path = "../simplelogin" }
|
simplelogin = { version = "0.1.0", path = "../simplelogin" }
|
||||||
tokio = { version = "1.33.0", features = ["full"] }
|
tokio = { version = "1.33.0", features = ["full"] }
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use std::{env, io, fs, path::Path, process::Command};
|
use std::{env, io, fs, path::Path, process::Command};
|
||||||
|
|
||||||
const SCHEMA_NAME: &'static str = "im.zander.SimpleLogin.gschema.xml";
|
const SCHEMA_NAME: &str = "im.zander.SimpleLogin.gschema.xml";
|
||||||
|
|
||||||
fn main() -> io::Result<()> {
|
fn main() -> io::Result<()> {
|
||||||
println!("cargo:rerun-if-changed=data/{}", SCHEMA_NAME);
|
println!("cargo:rerun-if-changed=data/{}", SCHEMA_NAME);
|
||||||
|
@ -1,28 +1,16 @@
|
|||||||
using Gtk 4.0;
|
using Gtk 4.0;
|
||||||
|
|
||||||
Popover device_entry_popover {
|
|
||||||
child: Label {
|
|
||||||
accessible-role: tooltip;
|
|
||||||
label: "This is the human readable name that will show up in the API key section of the SimpleLogin website.";
|
|
||||||
max-width-chars: 30;
|
|
||||||
wrap: true;
|
|
||||||
}
|
|
||||||
|
|
||||||
;
|
|
||||||
}
|
|
||||||
|
|
||||||
template $LoginWindow : ApplicationWindow {
|
template $LoginWindow : ApplicationWindow {
|
||||||
title: "SimpleLogin";
|
title: "SimpleLogin";
|
||||||
|
|
||||||
Box {
|
Overlay forms_overlay {
|
||||||
orientation: vertical;
|
valign: center;
|
||||||
|
vexpand: true;
|
||||||
|
|
||||||
Overlay forms_overlay {
|
child: Box forms_wrapper {
|
||||||
child: forms_wrapper;
|
orientation: vertical;
|
||||||
valign: end;
|
|
||||||
vexpand: true;
|
|
||||||
|
|
||||||
Box forms_wrapper {
|
Box {
|
||||||
valign: end;
|
valign: end;
|
||||||
vexpand: true;
|
vexpand: true;
|
||||||
|
|
||||||
@ -55,6 +43,7 @@ template $LoginWindow : ApplicationWindow {
|
|||||||
Label error_label {
|
Label error_label {
|
||||||
label: "ERROR";
|
label: "ERROR";
|
||||||
margin-top: 5;
|
margin-top: 5;
|
||||||
|
visible: false;
|
||||||
|
|
||||||
styles [
|
styles [
|
||||||
"error-message-label",
|
"error-message-label",
|
||||||
@ -73,11 +62,23 @@ template $LoginWindow : ApplicationWindow {
|
|||||||
secondary-icon-name: "dialog-question-symbolic";
|
secondary-icon-name: "dialog-question-symbolic";
|
||||||
truncate-multiline: true;
|
truncate-multiline: true;
|
||||||
width-chars: 18;
|
width-chars: 18;
|
||||||
|
icon-release => $device_help_clicked() swapped;
|
||||||
|
changed => $login_form_changed() swapped;
|
||||||
|
activate => $login_form_submit() swapped;
|
||||||
|
|
||||||
layout {
|
layout {
|
||||||
column: "1";
|
column: "1";
|
||||||
row: "3";
|
row: "3";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Popover device_entry_popover {
|
||||||
|
child: Label {
|
||||||
|
accessible-role: tooltip;
|
||||||
|
label: "This is the human readable name that will show up in the API key section of the SimpleLogin website.";
|
||||||
|
max-width-chars: 30;
|
||||||
|
wrap: true;
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
@ -116,6 +117,8 @@ template $LoginWindow : ApplicationWindow {
|
|||||||
margin-start: 6;
|
margin-start: 6;
|
||||||
receives-default: true;
|
receives-default: true;
|
||||||
truncate-multiline: true;
|
truncate-multiline: true;
|
||||||
|
changed => $login_form_changed() swapped;
|
||||||
|
activate => $login_form_submit() swapped;
|
||||||
|
|
||||||
layout {
|
layout {
|
||||||
column: "1";
|
column: "1";
|
||||||
@ -127,6 +130,8 @@ template $LoginWindow : ApplicationWindow {
|
|||||||
hexpand: true;
|
hexpand: true;
|
||||||
margin-start: 6;
|
margin-start: 6;
|
||||||
show-peek-icon: true;
|
show-peek-icon: true;
|
||||||
|
changed => $login_form_changed() swapped;
|
||||||
|
activate => $login_form_submit() swapped;
|
||||||
|
|
||||||
layout {
|
layout {
|
||||||
column: "1";
|
column: "1";
|
||||||
@ -140,6 +145,7 @@ template $LoginWindow : ApplicationWindow {
|
|||||||
margin-top: 5;
|
margin-top: 5;
|
||||||
sensitive: false;
|
sensitive: false;
|
||||||
valign: center;
|
valign: center;
|
||||||
|
clicked => $login_form_submit() swapped;
|
||||||
|
|
||||||
layout {
|
layout {
|
||||||
column: "0";
|
column: "0";
|
||||||
@ -185,6 +191,8 @@ template $LoginWindow : ApplicationWindow {
|
|||||||
margin-start: 8;
|
margin-start: 8;
|
||||||
show-peek-icon: true;
|
show-peek-icon: true;
|
||||||
width-chars: 27;
|
width-chars: 27;
|
||||||
|
changed => $api_key_form_changed() swapped;
|
||||||
|
activate => $api_key_form_submit() swapped;
|
||||||
}
|
}
|
||||||
|
|
||||||
Button api_key_button {
|
Button api_key_button {
|
||||||
@ -192,46 +200,50 @@ template $LoginWindow : ApplicationWindow {
|
|||||||
label: "Accept";
|
label: "Accept";
|
||||||
margin-top: 5;
|
margin-top: 5;
|
||||||
sensitive: false;
|
sensitive: false;
|
||||||
|
clicked => $api_key_form_submit() swapped;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
label: "Don\'t have an account? You can register <a href=\"https://app.simplelogin.io/auth/register\">here</a>.";
|
Label {
|
||||||
|
label: "Don\'t have an account? You can register <a href=\"https://app.simplelogin.io/auth/register\">here</a>.";
|
||||||
margin-bottom: 10;
|
margin-bottom: 10;
|
||||||
use-markup: true;
|
use-markup: true;
|
||||||
valign: start;
|
valign: start;
|
||||||
vexpand: true;
|
vexpand: true;
|
||||||
yalign: 0.0;
|
yalign: 0.0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
[overlay]
|
||||||
|
Box loading_overlay {
|
||||||
|
orientation: vertical;
|
||||||
|
visible: false;
|
||||||
|
|
||||||
|
Spinner {
|
||||||
|
spinning: true;
|
||||||
|
valign: end;
|
||||||
|
vexpand: true;
|
||||||
|
|
||||||
|
styles [
|
||||||
|
"loading-label",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
label: "Loading...";
|
||||||
|
valign: start;
|
||||||
|
vexpand: true;
|
||||||
|
|
||||||
|
styles [
|
||||||
|
"loading-label",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
styles [
|
||||||
|
"loading-label",
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Box loading_overlay {
|
|
||||||
orientation: vertical;
|
|
||||||
|
|
||||||
Spinner {
|
|
||||||
spinning: true;
|
|
||||||
valign: end;
|
|
||||||
vexpand: true;
|
|
||||||
|
|
||||||
styles [
|
|
||||||
"loading-label",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
label: "Loading...";
|
|
||||||
valign: start;
|
|
||||||
vexpand: true;
|
|
||||||
|
|
||||||
styles [
|
|
||||||
"loading-label",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
styles [
|
|
||||||
"loading-label",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
@ -5,13 +5,12 @@ template $TotpWindow : Window {
|
|||||||
title: "TOTP Entry";
|
title: "TOTP Entry";
|
||||||
|
|
||||||
Overlay overlay {
|
Overlay overlay {
|
||||||
child: form_wrapper;
|
|
||||||
halign: center;
|
halign: center;
|
||||||
hexpand: true;
|
hexpand: true;
|
||||||
valign: center;
|
valign: center;
|
||||||
vexpand: true;
|
vexpand: true;
|
||||||
|
|
||||||
Grid form_wrapper {
|
child: Grid form_wrapper {
|
||||||
column-spacing: 5;
|
column-spacing: 5;
|
||||||
halign: center;
|
halign: center;
|
||||||
hexpand: true;
|
hexpand: true;
|
||||||
@ -49,6 +48,7 @@ template $TotpWindow : Window {
|
|||||||
|
|
||||||
Button accept_button {
|
Button accept_button {
|
||||||
label: "Accept";
|
label: "Accept";
|
||||||
|
clicked => $accepted() swapped;
|
||||||
|
|
||||||
layout {
|
layout {
|
||||||
column: "1";
|
column: "1";
|
||||||
@ -58,6 +58,7 @@ template $TotpWindow : Window {
|
|||||||
|
|
||||||
Button cancel_button {
|
Button cancel_button {
|
||||||
label: "Cancel";
|
label: "Cancel";
|
||||||
|
clicked => $canceled() swapped;
|
||||||
|
|
||||||
layout {
|
layout {
|
||||||
column: "0";
|
column: "0";
|
||||||
@ -67,6 +68,7 @@ template $TotpWindow : Window {
|
|||||||
|
|
||||||
Entry code_entry {
|
Entry code_entry {
|
||||||
placeholder-text: "TOTP Code";
|
placeholder-text: "TOTP Code";
|
||||||
|
changed => $totp_changed() swapped;
|
||||||
|
|
||||||
layout {
|
layout {
|
||||||
column: "0";
|
column: "0";
|
||||||
@ -77,6 +79,7 @@ template $TotpWindow : Window {
|
|||||||
|
|
||||||
Label error_label {
|
Label error_label {
|
||||||
label: "ERROR";
|
label: "ERROR";
|
||||||
|
visible: false;
|
||||||
|
|
||||||
styles [
|
styles [
|
||||||
"error-message-label",
|
"error-message-label",
|
||||||
@ -88,34 +91,37 @@ template $TotpWindow : Window {
|
|||||||
row: "2";
|
row: "2";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
[overlay]
|
||||||
|
Box loading_overlay {
|
||||||
|
orientation: vertical;
|
||||||
|
visible: false;
|
||||||
|
|
||||||
|
Spinner {
|
||||||
|
spinning: true;
|
||||||
|
valign: end;
|
||||||
|
vexpand: true;
|
||||||
|
|
||||||
|
styles [
|
||||||
|
"loading-label",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
label: "Loading...";
|
||||||
|
valign: start;
|
||||||
|
vexpand: true;
|
||||||
|
|
||||||
|
styles [
|
||||||
|
"loading-label",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
styles [
|
||||||
|
"loading-label",
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Box loading_overlay {
|
|
||||||
orientation: vertical;
|
|
||||||
|
|
||||||
Spinner {
|
|
||||||
spinning: true;
|
|
||||||
valign: end;
|
|
||||||
vexpand: true;
|
|
||||||
|
|
||||||
styles [
|
|
||||||
"loading-label",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
label: "Loading...";
|
|
||||||
valign: start;
|
|
||||||
vexpand: true;
|
|
||||||
|
|
||||||
styles [
|
|
||||||
"loading-label",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
styles [
|
|
||||||
"loading-label",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
@ -2,10 +2,12 @@ use crate::login_window::LoginWindow;
|
|||||||
|
|
||||||
use gtk4 as gtk;
|
use gtk4 as gtk;
|
||||||
use gtk::{glib, gdk, gio, prelude::*, subclass::prelude::*};
|
use gtk::{glib, gdk, gio, prelude::*, subclass::prelude::*};
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use simplelogin as sl;
|
||||||
|
|
||||||
mod imp {
|
mod imp {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::cell::RefCell;
|
use glib::closure_local;
|
||||||
|
|
||||||
#[derive(Debug, glib::Properties)]
|
#[derive(Debug, glib::Properties)]
|
||||||
#[properties(wrapper_type = super::Application)]
|
#[properties(wrapper_type = super::Application)]
|
||||||
@ -13,6 +15,7 @@ mod imp {
|
|||||||
#[property(name = "api-key", type = Option<String>,
|
#[property(name = "api-key", type = Option<String>,
|
||||||
get = Self::api_key, set = Self::set_api_key, nullable)]
|
get = Self::api_key, set = Self::set_api_key, nullable)]
|
||||||
pub settings: RefCell<gio::Settings>,
|
pub settings: RefCell<gio::Settings>,
|
||||||
|
pub context: RefCell<sl::Context>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Application {
|
impl Application {
|
||||||
@ -21,14 +24,39 @@ mod imp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn set_api_key(&self, value: Option<String>) {
|
fn set_api_key(&self, value: Option<String>) {
|
||||||
_ = self.settings.borrow().set("api-key", value).unwrap();
|
_ = self.settings.borrow().set("api-key", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn activate_no_api_key(&self) {
|
||||||
|
let login_window = LoginWindow::new(&self.obj());
|
||||||
|
login_window.connect_closure(
|
||||||
|
"logged-in", false,
|
||||||
|
closure_local!(@weak-allow-none self as opt_this
|
||||||
|
=> move |win: LoginWindow| {
|
||||||
|
win.close();
|
||||||
|
if let Some(this) = opt_this {
|
||||||
|
let obj = this.obj();
|
||||||
|
let ctx = obj.context().borrow();
|
||||||
|
let api_key = ctx.api_key();
|
||||||
|
this.set_api_key(api_key.clone());
|
||||||
|
this.activate_has_api_key();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
login_window.present();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn activate_has_api_key(&self) {
|
||||||
|
println!("Logged in with API Key: {}", self.api_key().unwrap());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Application {
|
impl Default for Application {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
let settings = RefCell::new(gio::Settings::new(crate::APP_ID));
|
||||||
|
let api_key: Option<String> = settings.borrow().value("api-key").get();
|
||||||
Self {
|
Self {
|
||||||
settings: RefCell::new(gio::Settings::new(crate::APP_ID)),
|
settings,
|
||||||
|
context: RefCell::new(sl::Context::new(api_key.as_deref())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -42,17 +70,15 @@ mod imp {
|
|||||||
|
|
||||||
impl ApplicationImpl for Application {
|
impl ApplicationImpl for Application {
|
||||||
fn activate(&self) {
|
fn activate(&self) {
|
||||||
let css = gtk::CssProvider::new();
|
let css = gtk::CssProvider::new();
|
||||||
gtk::style_context_add_provider_for_display(
|
gtk::style_context_add_provider_for_display(
|
||||||
&gdk::Display::default().unwrap(),
|
&gdk::Display::default().unwrap(),
|
||||||
&css, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION
|
&css, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION
|
||||||
);
|
);
|
||||||
css.load_from_string(include_str!("../data/global.css"));
|
css.load_from_string(include_str!("../data/global.css"));
|
||||||
if self.api_key().is_none() {
|
match self.api_key() {
|
||||||
let login_window = LoginWindow::new(&self.obj());
|
Some(_) => self.activate_has_api_key(),
|
||||||
login_window.present();
|
None => self.activate_no_api_key(),
|
||||||
} else {
|
|
||||||
todo!();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -72,4 +98,8 @@ impl Application {
|
|||||||
pub fn new(app_id: &str) -> Self {
|
pub fn new(app_id: &str) -> Self {
|
||||||
glib::Object::builder().property("application-id", app_id).build()
|
glib::Object::builder().property("application-id", app_id).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn context(&self) -> &RefCell<sl::Context> {
|
||||||
|
&self.imp().context
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
use gtk4 as gtk;
|
use gtk4 as gtk;
|
||||||
use gtk::{glib, gio, subclass::prelude::*};
|
use gtk::{glib, gio, prelude::*, subclass::prelude::*};
|
||||||
|
|
||||||
mod imp {
|
mod imp {
|
||||||
|
use crate::{application, totp_window};
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use glib::{subclass::Signal, clone, closure_local};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
#[derive(Debug, Default, gtk::CompositeTemplate)]
|
#[derive(Debug, Default, gtk::CompositeTemplate)]
|
||||||
#[template(file = "data/login_window.blp")]
|
#[template(file = "data/login_window.blp")]
|
||||||
@ -31,6 +34,106 @@ mod imp {
|
|||||||
pub loading_overlay: TemplateChild<gtk::Box>,
|
pub loading_overlay: TemplateChild<gtk::Box>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gtk::template_callbacks]
|
||||||
|
impl LoginWindow {
|
||||||
|
#[template_callback]
|
||||||
|
fn login_form_changed(&self, _: >k::Widget) {
|
||||||
|
let email = &self.email_entry.get().text();
|
||||||
|
let password = &self.password_entry.get().text();
|
||||||
|
let device = &self.device_entry.get().text();
|
||||||
|
self.login_button.get().set_sensitive(
|
||||||
|
!email.is_empty() && !password.is_empty() && !device.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error(&self, error: &str) {
|
||||||
|
self.set_is_loading(false);
|
||||||
|
let error_label = &self.error_label.get();
|
||||||
|
error_label.set_text(error);
|
||||||
|
error_label.set_visible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_is_loading(&self, loading: bool) {
|
||||||
|
if loading {
|
||||||
|
self.error_label.get().set_visible(false);
|
||||||
|
}
|
||||||
|
self.loading_overlay.get().set_visible(loading);
|
||||||
|
self.forms_wrapper.get().set_sensitive(!loading);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_need_totp(&self, app: &application::Application) {
|
||||||
|
let totp_window = totp_window::TotpWindow::new(app);
|
||||||
|
totp_window.connect_closure(
|
||||||
|
"totp-success", false,
|
||||||
|
closure_local!(@weak-allow-none self as opt_this =>
|
||||||
|
move |_: totp_window::TotpWindow| {
|
||||||
|
if let Some(this) = opt_this {
|
||||||
|
this.obj().emit_by_name::<()>("logged-in", &[]);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
totp_window.connect_closure(
|
||||||
|
"totp-failed", false,
|
||||||
|
closure_local!(@weak-allow-none self as opt_this
|
||||||
|
=> move |_: totp_window::TotpWindow, err: &str| {
|
||||||
|
if let Some(this) = opt_this {
|
||||||
|
this.set_error(err);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
totp_window.present();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[template_callback]
|
||||||
|
fn login_form_submit(&self, _: >k::Widget) {
|
||||||
|
let main_loop = glib::MainContext::default();
|
||||||
|
main_loop.spawn_local(clone!(@weak self as this => async move {
|
||||||
|
let email = &this.email_entry.get().text();
|
||||||
|
let password = &this.password_entry.get().text();
|
||||||
|
let device = &this.device_entry.get().text();
|
||||||
|
if !email.is_empty() && !password.is_empty() && !device.is_empty() {
|
||||||
|
this.set_is_loading(true);
|
||||||
|
let app: application::Application =
|
||||||
|
this.obj().application().unwrap().downcast().unwrap();
|
||||||
|
let mut ctx = app.context().borrow_mut();
|
||||||
|
let login_res = ctx.login(email, password, device).await;
|
||||||
|
drop(ctx);
|
||||||
|
match login_res {
|
||||||
|
Ok(need_mfa) if need_mfa => this.handle_need_totp(&app),
|
||||||
|
Ok(_) => this.obj().emit_by_name::<()>("logged-in", &[]),
|
||||||
|
Err(e) => this.set_error(e.to_string().as_str()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_api_key_from_field(&self) -> String {
|
||||||
|
self.api_key_entry.get().text().replace('\n', "")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[template_callback]
|
||||||
|
fn api_key_form_changed(&self, _: >k::Widget) {
|
||||||
|
let api_key = self.get_api_key_from_field();
|
||||||
|
self.api_key_button.get().set_sensitive(!api_key.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[template_callback]
|
||||||
|
fn api_key_form_submit(&self, _: >k::Widget) {
|
||||||
|
let api_key = self.get_api_key_from_field();
|
||||||
|
if !api_key.is_empty() {
|
||||||
|
let obj = self.obj();
|
||||||
|
let app: application::Application =
|
||||||
|
obj.application().unwrap().downcast().unwrap();
|
||||||
|
let mut ctx = app.context().borrow_mut();
|
||||||
|
ctx.set_api_key(Some(api_key.as_str()));
|
||||||
|
drop(ctx);
|
||||||
|
obj.emit_by_name::<()>("logged-in", &[]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[template_callback]
|
||||||
|
fn device_help_clicked(&self, _: gtk::EntryIconPosition) {
|
||||||
|
self.device_entry_popover.get().popup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[glib::object_subclass]
|
#[glib::object_subclass]
|
||||||
impl ObjectSubclass for LoginWindow {
|
impl ObjectSubclass for LoginWindow {
|
||||||
const NAME: &'static str = "LoginWindow";
|
const NAME: &'static str = "LoginWindow";
|
||||||
@ -39,6 +142,7 @@ mod imp {
|
|||||||
|
|
||||||
fn class_init(class: &mut Self::Class) {
|
fn class_init(class: &mut Self::Class) {
|
||||||
class.bind_template();
|
class.bind_template();
|
||||||
|
class.bind_template_callbacks();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
||||||
@ -46,7 +150,18 @@ mod imp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ObjectImpl for LoginWindow {}
|
impl ObjectImpl for LoginWindow {
|
||||||
|
fn signals() -> &'static [Signal] {
|
||||||
|
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(||
|
||||||
|
vec![Signal::builder("logged-in").build()]);
|
||||||
|
SIGNALS.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn constructed(&self) {
|
||||||
|
self.parent_constructed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl WidgetImpl for LoginWindow {}
|
impl WidgetImpl for LoginWindow {}
|
||||||
impl WindowImpl for LoginWindow {}
|
impl WindowImpl for LoginWindow {}
|
||||||
impl ApplicationWindowImpl for LoginWindow {}
|
impl ApplicationWindowImpl for LoginWindow {}
|
||||||
|
@ -8,7 +8,8 @@ use gtk::glib::{self, g_info};
|
|||||||
|
|
||||||
const APP_ID: &str = "im.zander.SimpleLogin";
|
const APP_ID: &str = "im.zander.SimpleLogin";
|
||||||
|
|
||||||
fn main() -> glib::ExitCode {
|
#[tokio::main]
|
||||||
|
async fn main() -> glib::ExitCode {
|
||||||
#[cfg(debug_assertions)] {
|
#[cfg(debug_assertions)] {
|
||||||
std::env::set_var("G_MESSAGES_DEBUG", "SimpleLogin");
|
std::env::set_var("G_MESSAGES_DEBUG", "SimpleLogin");
|
||||||
// allow us to use a test schema for debug runs
|
// allow us to use a test schema for debug runs
|
||||||
|
@ -2,12 +2,84 @@ use gtk4 as gtk;
|
|||||||
use gtk::{glib, gio, prelude::*, subclass::prelude::*};
|
use gtk::{glib, gio, prelude::*, subclass::prelude::*};
|
||||||
|
|
||||||
mod imp {
|
mod imp {
|
||||||
|
use crate::application;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use glib::{clone, subclass::Signal};
|
||||||
|
|
||||||
#[derive(Debug, Default, gtk::CompositeTemplate)]
|
#[derive(Debug, Default, gtk::CompositeTemplate)]
|
||||||
#[template(file = "data/totp_window.blp")]
|
#[template(file = "data/totp_window.blp")]
|
||||||
pub struct TotpWindow {
|
pub struct TotpWindow {
|
||||||
|
#[template_child]
|
||||||
|
pub overlay: gtk::TemplateChild<gtk::Overlay>,
|
||||||
|
#[template_child]
|
||||||
|
pub form_wrapper: gtk::TemplateChild<gtk::Grid>,
|
||||||
|
#[template_child]
|
||||||
|
pub accept_button: gtk::TemplateChild<gtk::Button>,
|
||||||
|
#[template_child]
|
||||||
|
pub cancel_button: gtk::TemplateChild<gtk::Button>,
|
||||||
|
#[template_child]
|
||||||
|
pub code_entry: gtk::TemplateChild<gtk::Entry>,
|
||||||
|
#[template_child]
|
||||||
|
pub error_label: gtk::TemplateChild<gtk::Label>,
|
||||||
|
#[template_child]
|
||||||
|
pub loading_overlay: gtk::TemplateChild<gtk::Box>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gtk::template_callbacks]
|
||||||
|
impl TotpWindow {
|
||||||
|
#[template_callback]
|
||||||
|
fn totp_changed(&self, _: >k::Entry) {
|
||||||
|
let totp = &self.code_entry.get().text();
|
||||||
|
self.accept_button.get().set_sensitive(!totp.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[template_callback]
|
||||||
|
fn canceled(&self, _: >k::Button) {
|
||||||
|
self.obj().close();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[template_callback]
|
||||||
|
fn accepted(&self, _: >k::Button) {
|
||||||
|
let main_loop = glib::MainContext::default();
|
||||||
|
main_loop.spawn_local(clone!(@weak self as this => async move {
|
||||||
|
let obj = this.obj();
|
||||||
|
this.set_is_loading(true);
|
||||||
|
this.loading_overlay.get().set_visible(true);
|
||||||
|
let app: application::Application =
|
||||||
|
obj.application().unwrap().downcast().unwrap();
|
||||||
|
let mut ctx = app.context().borrow_mut();
|
||||||
|
let totp = &this.code_entry.get().text();
|
||||||
|
let mfa_res = ctx.mfa(totp).await;
|
||||||
|
drop(ctx);
|
||||||
|
match mfa_res {
|
||||||
|
Ok(()) => {
|
||||||
|
obj.emit_by_name::<()>("totp-success", &[]);
|
||||||
|
obj.close();
|
||||||
|
},
|
||||||
|
Err(e) if e.is_json() => this.set_error(e.to_string().as_str()),
|
||||||
|
Err(e) => {
|
||||||
|
obj.emit_by_name::<()>("totp-failed", &[&e.to_string()]);
|
||||||
|
obj.close();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error(&self, error: &str) {
|
||||||
|
self.set_is_loading(false);
|
||||||
|
let error_label = &self.error_label.get();
|
||||||
|
error_label.set_text(error);
|
||||||
|
error_label.set_visible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_is_loading(&self, loading: bool) {
|
||||||
|
if loading {
|
||||||
|
self.error_label.get().set_visible(false);
|
||||||
|
}
|
||||||
|
self.loading_overlay.get().set_visible(loading);
|
||||||
|
self.form_wrapper.get().set_sensitive(!loading);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[glib::object_subclass]
|
#[glib::object_subclass]
|
||||||
@ -18,6 +90,7 @@ mod imp {
|
|||||||
|
|
||||||
fn class_init(class: &mut Self::Class) {
|
fn class_init(class: &mut Self::Class) {
|
||||||
class.bind_template();
|
class.bind_template();
|
||||||
|
class.bind_template_callbacks();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
||||||
@ -25,7 +98,22 @@ mod imp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ObjectImpl for TotpWindow {}
|
impl ObjectImpl for TotpWindow {
|
||||||
|
fn signals() -> &'static [Signal] {
|
||||||
|
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(||
|
||||||
|
vec![Signal::builder("totp-success").build(),
|
||||||
|
Signal::builder("totp-failed")
|
||||||
|
.param_types([str::static_type()])
|
||||||
|
.build()]);
|
||||||
|
SIGNALS.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn constructed(&self) {
|
||||||
|
self.parent_constructed();
|
||||||
|
self.overlay.add_overlay(&self.loading_overlay.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl WidgetImpl for TotpWindow {}
|
impl WidgetImpl for TotpWindow {}
|
||||||
impl WindowImpl for TotpWindow {}
|
impl WindowImpl for TotpWindow {}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
use core::fmt;
|
use std::fmt::{self, Display};
|
||||||
|
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Context {
|
pub struct Context {
|
||||||
client: reqwest::Client,
|
client: reqwest::Client,
|
||||||
api_key: Option<String>,
|
api_key: Option<String>,
|
||||||
@ -13,6 +14,16 @@ pub struct Context {
|
|||||||
device: Option<String>,
|
device: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Display for Context {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||||
|
let api_key = match &self.api_key {
|
||||||
|
Some(key) => format!("\"{}\"", key),
|
||||||
|
None => "None".to_owned(),
|
||||||
|
};
|
||||||
|
writeln!(f, "Context(api_key: \"{}\", url: \"{}\")", api_key, self.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct ErrorResponse {
|
struct ErrorResponse {
|
||||||
error: String,
|
error: String,
|
||||||
@ -113,15 +124,15 @@ impl Context {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn api_key(&self) -> Option<&String> {
|
pub fn api_key(&self) -> &Option<String> {
|
||||||
self.api_key.as_ref()
|
&self.api_key
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_api_key(&mut self, api_key: Option<&str>) {
|
pub fn set_api_key(&mut self, api_key: Option<&str>) {
|
||||||
self.api_key = api_key.map(|s| s.to_owned());
|
self.api_key = api_key.map(|s| s.to_owned());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_error_json(error_body: &String) -> String {
|
fn parse_error_json(error_body: &str) -> String {
|
||||||
match serde_json::from_str(error_body) {
|
match serde_json::from_str(error_body) {
|
||||||
Ok::<ErrorResponse, _>(obj) => obj.error,
|
Ok::<ErrorResponse, _>(obj) => obj.error,
|
||||||
Err(_) => "Could not parse error response JSON".to_owned(),
|
Err(_) => "Could not parse error response JSON".to_owned(),
|
||||||
@ -136,7 +147,7 @@ impl Context {
|
|||||||
let status = resp.status();
|
let status = resp.status();
|
||||||
let resp_text = resp.text().await
|
let resp_text = resp.text().await
|
||||||
.map_err(|e| Error::new(ErrorKind::Http, e.to_string()))?;
|
.map_err(|e| Error::new(ErrorKind::Http, e.to_string()))?;
|
||||||
return if status.is_success() {
|
if status.is_success() {
|
||||||
Ok(resp_text)
|
Ok(resp_text)
|
||||||
} else if status.is_client_error() {
|
} else if status.is_client_error() {
|
||||||
Err(Error::new(ErrorKind::Auth, Self::parse_error_json(&resp_text)))
|
Err(Error::new(ErrorKind::Auth, Self::parse_error_json(&resp_text)))
|
||||||
@ -190,15 +201,13 @@ impl Context {
|
|||||||
} else {
|
} else {
|
||||||
Err(Error::new(ErrorKind::Json, "Error response did not contain mfa key"))
|
Err(Error::new(ErrorKind::Json, "Error response did not contain mfa key"))
|
||||||
}
|
}
|
||||||
|
} else if resp_obj.api_key.is_some() {
|
||||||
|
self.api_key = resp_obj.api_key;
|
||||||
|
self.mfa_key = None;
|
||||||
|
self.device = None;
|
||||||
|
Ok(true)
|
||||||
} else {
|
} else {
|
||||||
if resp_obj.api_key.is_some() {
|
Err(Error::new(ErrorKind::Json, "Response did not contain api key"))
|
||||||
self.api_key = resp_obj.api_key;
|
|
||||||
self.mfa_key = None;
|
|
||||||
self.device = None;
|
|
||||||
Ok(true)
|
|
||||||
} else {
|
|
||||||
Err(Error::new(ErrorKind::Json, "Response did not contain api key"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user