DB: Add client/user management primitives
This commit is contained in:
parent
87e10be295
commit
ed7e184bf2
|
@ -168,7 +168,6 @@ dependencies = [
|
|||
"rusqlite_migration",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_rusqlite",
|
||||
"ureq",
|
||||
]
|
||||
|
||||
|
@ -963,16 +962,6 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_rusqlite"
|
||||
version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e73509831d054bb2d2a69e3457ad7c417a5820bab80a70676182148e29a503ed"
|
||||
dependencies = [
|
||||
"rusqlite",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.6"
|
||||
|
|
|
@ -11,5 +11,4 @@ rusqlite = "*"
|
|||
rusqlite_migration = "*"
|
||||
serde = {version = "*", features = ["derive"]}
|
||||
serde_json = "*"
|
||||
serde_rusqlite = "*"
|
||||
ureq = {version = "*", features = ["json", "native-certs", "gzip"]}
|
||||
|
|
|
@ -4,6 +4,7 @@ pkgs.mkShell {
|
|||
nativeBuildInputs = [
|
||||
pkgs.rustc
|
||||
pkgs.cargo
|
||||
pkgs.clippy
|
||||
pkgs.rust-analyzer
|
||||
pkgs.pkg-config
|
||||
pkgs.gtk4
|
||||
|
|
309
src/db/mod.rs
309
src/db/mod.rs
|
@ -1,40 +1,325 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use dirs;
|
||||
use rusqlite::{Connection, Error};
|
||||
use rusqlite::{Connection, named_params};
|
||||
use rusqlite_migration::{Migrations, M};
|
||||
|
||||
pub fn open_db() -> Result<Connection,Error> {
|
||||
use crate::mastodon::oauth::RegisteredApp;
|
||||
use crate::mastodon::accounts::UserAccount;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum OpenDbError {
|
||||
RusqliteError(rusqlite::Error),
|
||||
IOError(std::io::Error)
|
||||
}
|
||||
|
||||
pub fn open_db() -> Result<Connection,OpenDbError> {
|
||||
let mut data_dir = dirs::data_dir();
|
||||
let path = data_dir.get_or_insert(PathBuf::from("./"));
|
||||
path.push("federatz");
|
||||
std::fs::create_dir_all(&path);
|
||||
std::fs::create_dir_all(&path).map_err(|err| OpenDbError::IOError(err))?;
|
||||
path.push("federatz.db");
|
||||
Connection::open(path)
|
||||
Connection::open(path).map_err(|err| OpenDbError::RusqliteError(err))
|
||||
}
|
||||
|
||||
pub fn run_migrations(mut conn: Connection) -> Result<(), rusqlite_migration::Error> {
|
||||
pub fn run_migrations(conn: &mut Connection) -> Result<(), rusqlite_migration::Error> {
|
||||
let migrations =
|
||||
Migrations::new(vec![
|
||||
M::up(r#"
|
||||
CREATE TABLE oauth_client(
|
||||
id INTEGER PRIMARY KEY,
|
||||
instance_uri TEXT NOT NULL,
|
||||
instance_fqdn TEXT NOT NULL,
|
||||
client_id TEXT NOT NULL,
|
||||
client_secret TEXT NOT NULL
|
||||
client_secret TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
redirect_uri TEXT NOT NULL,
|
||||
website TEXT
|
||||
);
|
||||
CREATE TABLE local_user(
|
||||
id INTEGER,
|
||||
token TEXT NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
locked BOOLEAN NOT NULL,
|
||||
bot BOOLEAN NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
note TEXT NOT NULL,
|
||||
follower_count INTEGER NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
avatar TEXT NOT NULL,
|
||||
avatar_static TEXT NOT NULL,
|
||||
header TEXT NOT NULL,
|
||||
header_static TEXT NOT NULL,
|
||||
followers_count INTEGER NOT NULL,
|
||||
following_count INTEGER NOT NULL,
|
||||
statuses_count INTEGER NOT NULL,
|
||||
client INTEGER,
|
||||
FOREIGN KEY(client) REFERENCES oauth_client(id)
|
||||
);
|
||||
);
|
||||
"#)
|
||||
]);
|
||||
migrations.to_latest(&mut conn)
|
||||
migrations.to_latest(conn)
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum OauthClientDatabaseError {
|
||||
NoClient,
|
||||
RusqliteError(rusqlite::Error)
|
||||
}
|
||||
|
||||
impl From<rusqlite::Error> for OauthClientDatabaseError {
|
||||
fn from(error: rusqlite::Error) -> Self {
|
||||
OauthClientDatabaseError::RusqliteError(error)
|
||||
}
|
||||
}
|
||||
pub fn new_oauth_client(conn: &Connection, client: &RegisteredApp) -> Result<i64,rusqlite::Error> {
|
||||
let mut stmt =
|
||||
conn.prepare(r#"
|
||||
INSERT INTO oauth_client
|
||||
(instance_fqdn, client_id, client_secret, name, redirect_uri, website)
|
||||
VALUES (:instance_fqdn, :client_id, :client_secret, :name, :redirect_uri, :website)"#
|
||||
)?;
|
||||
stmt.insert(
|
||||
named_params! {
|
||||
":instance_fqdn": client.instance_fqdn,
|
||||
":client_id": client.client_id,
|
||||
":client_secret": client.client_secret,
|
||||
":name": client.name,
|
||||
":redirect_uri": client.redirect_uri,
|
||||
":website": client.website,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
pub fn delete_oauth_client(conn: &Connection, client: &RegisteredApp) -> Result<usize,rusqlite::Error> {
|
||||
let mut stmt =
|
||||
conn.prepare(r#"
|
||||
DELETE FROM oauth_client
|
||||
WHERE client_id = :client_id AND client_secret = :client_secret"#
|
||||
)?;
|
||||
stmt.execute(
|
||||
named_params! {
|
||||
":client_id": client.client_id,
|
||||
":client_secret": client.client_secret
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_oauth_client(conn: &Connection, client_id: &str) -> Result<(RegisteredApp, i64), OauthClientDatabaseError> {
|
||||
let mut stmt =
|
||||
conn.prepare(
|
||||
r#"SELECT ROWID, instance_fqdn, client_id, client_secret, name, redirect_uri, website
|
||||
FROM oauth_client
|
||||
WHERE client_id = :client_id"#)?;
|
||||
let mut rows = stmt.query_and_then(
|
||||
named_params! {
|
||||
":client_id": client_id
|
||||
},
|
||||
|row| Ok((RegisteredApp {
|
||||
instance_fqdn: row.get("instance_fqdn")?,
|
||||
client_id: row.get("client_id")?,
|
||||
client_secret: row.get("client_secret")?,
|
||||
name: row.get("name")?,
|
||||
redirect_uri: row.get("redirect_uri")?,
|
||||
website: row.get("website")?,
|
||||
}, row.get("ROWID")?)))?;
|
||||
rows.next().ok_or(OauthClientDatabaseError::NoClient)?
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum GetLocalUserDatabaseError {
|
||||
NoUser,
|
||||
ManyUsers,
|
||||
RusqliteError(rusqlite::Error)
|
||||
}
|
||||
|
||||
impl From<rusqlite::Error> for GetLocalUserDatabaseError {
|
||||
fn from(error: rusqlite::Error) -> Self {
|
||||
GetLocalUserDatabaseError::RusqliteError(error)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum NewLocalUserDatabaseError {
|
||||
ClientError(OauthClientDatabaseError),
|
||||
RusqliteError(rusqlite::Error)
|
||||
}
|
||||
|
||||
impl From<rusqlite::Error> for NewLocalUserDatabaseError {
|
||||
fn from(error: rusqlite::Error) -> Self {
|
||||
NewLocalUserDatabaseError::RusqliteError(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OauthClientDatabaseError> for NewLocalUserDatabaseError {
|
||||
fn from(error:OauthClientDatabaseError) -> Self {
|
||||
NewLocalUserDatabaseError::ClientError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new user account and returns its Sqlite ROWID.
|
||||
pub fn new_local_user(conn: &Connection, user: &UserAccount, client: &RegisteredApp) -> Result<i64, NewLocalUserDatabaseError> {
|
||||
let mut stmt =
|
||||
conn.prepare(r#"
|
||||
INSERT INTO local_user (id, token, username, display_name, locked, bot, created_at, note, url, avatar,
|
||||
avatar_static, header, header_static, followers_count, following_count,
|
||||
statuses_count, client)
|
||||
VALUES (:id, :token, :username, :display_name, :locked, :bot, :created_at, :note, :url, :avatar,
|
||||
:avatar_static, :header, :header_static, :followers_count, :following_count,
|
||||
:statuses_count, :client)"#
|
||||
)?;
|
||||
let client = get_oauth_client(&conn, &client.client_id)?;
|
||||
let new_user = stmt.insert(
|
||||
named_params! {
|
||||
":id": user.id,
|
||||
":token": user.token,
|
||||
":username": user.username,
|
||||
":display_name": user.display_name,
|
||||
":locked": user.locked,
|
||||
":bot": user.bot,
|
||||
":created_at": user.created_at,
|
||||
":note": user.note,
|
||||
":url": user.url,
|
||||
":avatar": user.avatar,
|
||||
":avatar_static": user.avatar_static,
|
||||
":header": user.header,
|
||||
":header_static": user.header_static,
|
||||
":followers_count": user.followers_count,
|
||||
":following_count": user.following_count,
|
||||
":statuses_count": user.statuses_count,
|
||||
}
|
||||
)?;
|
||||
Ok(new_user)
|
||||
}
|
||||
|
||||
pub fn get_local_user(conn: &Connection) -> Result<UserAccount, GetLocalUserDatabaseError> {
|
||||
let mut stmt =
|
||||
conn.prepare(r#"
|
||||
SELECT CAST(id AS TEXT) as id, token, username, display_name, locked, bot, created_at, note, url, avatar,
|
||||
avatar_static, header, header_static, followers_count, following_count,
|
||||
statuses_count FROM local_user"#).map_err(|err| GetLocalUserDatabaseError::RusqliteError(err))?;
|
||||
let mut rows = stmt.query_and_then(
|
||||
[],
|
||||
|row| Ok(UserAccount {
|
||||
id: row.get("id")?,
|
||||
token: row.get("token")?,
|
||||
username: row.get("username")?,
|
||||
display_name: row.get("display_name")?,
|
||||
locked: row.get("locked")?,
|
||||
bot: row.get("bot")?,
|
||||
created_at: row.get("created_at")?,
|
||||
note: row.get("note")?,
|
||||
url: row.get("url")?,
|
||||
avatar: row.get("avatar")?,
|
||||
avatar_static: row.get("avatar_static")?,
|
||||
header: row.get("header")?,
|
||||
header_static: row.get("header_static")?,
|
||||
followers_count: row.get("followers_count")?,
|
||||
following_count: row.get("following_count")?,
|
||||
statuses_count: row.get("statuses_count")?,
|
||||
})).map_err(|err| GetLocalUserDatabaseError::RusqliteError(err))?;
|
||||
let user = rows.next().ok_or(GetLocalUserDatabaseError::NoUser)?;
|
||||
if rows.next().is_none() {
|
||||
user
|
||||
} else {
|
||||
Err(GetLocalUserDatabaseError::ManyUsers)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_local_user(conn: &Connection, user: &UserAccount) -> Result<usize, rusqlite::Error> {
|
||||
let mut stmt =
|
||||
conn.prepare("DELETE FROM local_user WHERE username = :username")?;
|
||||
stmt.execute(named_params! { ":username": user.username })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::db::{UserAccount, RegisteredApp};
|
||||
|
||||
fn dummy_client () -> RegisteredApp {
|
||||
RegisteredApp {
|
||||
instance_fqdn: String::from("dummy.instance"),
|
||||
client_id: String::from("dummy-client-id"),
|
||||
client_secret: String::from("dummy-client-secret"),
|
||||
name: String::from("dummy-name"),
|
||||
redirect_uri: String::from("dummy-redirect-uri"),
|
||||
website: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn dummy_local_user (id: &str) -> UserAccount {
|
||||
UserAccount {
|
||||
id: String::from(id),
|
||||
token: String::from("dummy-token"),
|
||||
username: String::from(format!("Ninjatrappeur{}", id)),
|
||||
display_name: String::from("⠴Ninjatrappeur⠦"),
|
||||
locked: false,
|
||||
bot: false,
|
||||
created_at: String::from("2018-05-26T13:07:04.000Z"),
|
||||
note: String::from("dummy-note"),
|
||||
url: String::from("https://social.alternativebit.fr/users/Ninjatrappeur"),
|
||||
avatar: String::from("https://social.alternativebit.fr/media/3f9ad2d6f473c954506f404de5a3636905035f32ec9f1f07deca0a72e88ac3e8.blob"),
|
||||
avatar_static: String::from("https://social.alternativebit.fr/media/3f9ad2d6f473c954506f404de5a3636905035f32ec9f1f07deca0a72e88ac3e8.blob"),
|
||||
header: String::from("https://social.alternativebit.fr/media/b5f2cf7e-7892-4692-b0c8-157818ad1b87/57CF8FB0FE2143FF1267A90BD3217D8D433FA2381C56E36264352A3EA0F17838.png"),
|
||||
header_static: String::from("https://social.alternativebit.fr/media/b5f2cf7e-7892-4692-b0c8-157818ad1b87/57CF8FB0FE2143FF1267A90BD3217D8D433FA2381C56E36264352A3EA0F17838.png"),
|
||||
followers_count: 615,
|
||||
following_count: 191,
|
||||
statuses_count: 1297,
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_temp_db () -> rusqlite::Connection {
|
||||
let mut conn = rusqlite::Connection::open_in_memory().unwrap();
|
||||
super::run_migrations(&mut conn).unwrap();
|
||||
conn
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_oauth_client () {
|
||||
let conn = setup_temp_db();
|
||||
let dummy_client = dummy_client();
|
||||
super::new_oauth_client(&conn, &dummy_client).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_oauth_client () {
|
||||
let conn = setup_temp_db();
|
||||
let dummy_client = dummy_client();
|
||||
super::new_oauth_client(&conn, &dummy_client).unwrap();
|
||||
assert_eq!(super::get_oauth_client(&conn, &dummy_client.client_id).unwrap().0, dummy_client);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_local_user () {
|
||||
let conn = setup_temp_db();
|
||||
let dummy_client = dummy_client();
|
||||
let dummy_local_user = dummy_local_user("1");
|
||||
super::new_oauth_client(&conn, &dummy_client).unwrap();
|
||||
super::new_local_user(&conn, &dummy_local_user, &dummy_client).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_local_user_ok () {
|
||||
let conn = setup_temp_db();
|
||||
let dummy_client = dummy_client();
|
||||
let dummy_local_user = dummy_local_user("1");
|
||||
super::new_oauth_client(&conn, &dummy_client).unwrap();
|
||||
super::new_local_user(&conn, &dummy_local_user, &dummy_client).unwrap();
|
||||
assert_eq!(super::get_local_user(&conn), Ok(dummy_local_user));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_local_user_fail_many () {
|
||||
let conn = setup_temp_db();
|
||||
let dummy_client = dummy_client();
|
||||
let dummy_local_user_1 = dummy_local_user("1");
|
||||
let dummy_local_user_2 = dummy_local_user("2");
|
||||
super::new_oauth_client(&conn, &dummy_client).unwrap();
|
||||
super::new_local_user(&conn, &dummy_local_user_1, &dummy_client).unwrap();
|
||||
super::new_local_user(&conn, &dummy_local_user_2, &dummy_client).unwrap();
|
||||
assert_eq!(super::get_local_user(&conn), Err(super::GetLocalUserDatabaseError::ManyUsers));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_local_user_fail_none () {
|
||||
let conn = setup_temp_db();
|
||||
assert_eq!(super::get_local_user(&conn), Err(super::GetLocalUserDatabaseError::NoUser));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ use gtk4 as gtk;
|
|||
use gtk::prelude::*;
|
||||
use gtk::Application;
|
||||
|
||||
mod oauth;
|
||||
mod mastodon;
|
||||
mod ui;
|
||||
mod db;
|
||||
|
||||
|
@ -11,8 +11,8 @@ use ui::widgets::oauth::create_oauth_assistant;
|
|||
fn main() {
|
||||
let conn = db::open_db();
|
||||
match conn {
|
||||
Ok(conn) => db::run_migrations(conn),
|
||||
Err(e) => panic!("Error when running the DB migrations: {}", e),
|
||||
Ok(mut conn) => db::run_migrations(&mut conn),
|
||||
Err(e) => panic!("Error when running the DB migrations: {:?}", e),
|
||||
};
|
||||
let app = Application::builder()
|
||||
.application_id("fr.alternativebit.federatz")
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::mastodon;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq)]
|
||||
pub struct UserAccount {
|
||||
pub id: String,
|
||||
pub token: String,
|
||||
pub username: String,
|
||||
pub display_name: String,
|
||||
pub locked: bool,
|
||||
pub bot: bool,
|
||||
pub created_at: String,
|
||||
pub note: String,
|
||||
pub url: String,
|
||||
pub avatar: String,
|
||||
pub avatar_static: String,
|
||||
pub header: String,
|
||||
pub header_static: String,
|
||||
pub followers_count: u32,
|
||||
pub following_count: u32,
|
||||
pub statuses_count: u32,
|
||||
}
|
||||
|
||||
pub fn verify_credentials(instance_fqdn: &str, authorization_token: &str) -> Result<UserAccount, mastodon::RequestError> {
|
||||
let resp: Result<ureq::Response, ureq::Error> = ureq::get(&format!("https://{}/api/v1/accounts/verify_credentials", instance_fqdn))
|
||||
.set("Authorization", authorization_token)
|
||||
.call();
|
||||
match resp {
|
||||
Ok(resp) =>
|
||||
resp.into_json().map_err(mastodon::RequestError::JsonError),
|
||||
Err(ureq::Error::Status(code, response)) =>
|
||||
Err(mastodon::RequestError::HttpError(code, response.status_text().to_string())),
|
||||
Err(_) =>
|
||||
Err(mastodon::RequestError::HttpError(0, "Transport error".to_string())),
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
use std::io::Error;
|
||||
|
||||
pub mod accounts;
|
||||
pub mod oauth;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RequestError {
|
||||
HttpError(u16, String),
|
||||
JsonError(Error)
|
||||
}
|
|
@ -1,36 +1,30 @@
|
|||
use std::io::Error;
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
use crate::mastodon;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq)]
|
||||
pub struct RegisteredApp {
|
||||
pub instance_fqdn: String,
|
||||
pub client_id: String,
|
||||
pub client_secret: String,
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub redirect_uri: String,
|
||||
pub website: Option<String>
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RequestError {
|
||||
HttpError(u16, String),
|
||||
JsonError(Error)
|
||||
}
|
||||
|
||||
pub fn register_app(instance_fqdn: &str, app_name: &str) -> Result<RegisteredApp, RequestError> {
|
||||
pub fn register_app(instance_fqdn: &str, app_name: &str) -> Result<RegisteredApp, mastodon::RequestError> {
|
||||
let resp: Result<ureq::Response, ureq::Error> = ureq::post(&format!("https://{}/api/v1/apps", instance_fqdn))
|
||||
.send_form(
|
||||
&[("client_name", app_name),
|
||||
("redirect_uris", "urn:ietf:wg:oauth:2.0:oob"),
|
||||
("scopes", "read write push")]);
|
||||
let io_error_to_request_error = |err| RequestError::JsonError(err);
|
||||
match resp {
|
||||
Ok(resp) =>
|
||||
resp.into_json().map_err(io_error_to_request_error),
|
||||
resp.into_json().map_err(mastodon::RequestError::JsonError),
|
||||
Err(ureq::Error::Status(code, response)) =>
|
||||
Err(RequestError::HttpError(code,response.status_text().to_string())),
|
||||
Err(mastodon::RequestError::HttpError(code,response.status_text().to_string())),
|
||||
Err(_) =>
|
||||
Err(RequestError::HttpError(0, "Transport error".to_string()))
|
||||
Err(mastodon::RequestError::HttpError(0, "Transport error".to_string()))
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ mod page1;
|
|||
mod page2;
|
||||
mod page3;
|
||||
|
||||
use crate::oauth;
|
||||
use crate::mastodon::{accounts, oauth};
|
||||
|
||||
pub fn create_oauth_assistant(app: >k::Application) -> gtk::Assistant {
|
||||
let assistant = gtk::Assistant::builder()
|
||||
|
@ -32,7 +32,6 @@ pub fn create_oauth_assistant(app: >k::Application) -> gtk::Assistant {
|
|||
let ores = oauth::register_app(&instance_uri, "federatz-0.1");
|
||||
match ores {
|
||||
Ok(res) => {
|
||||
println!("{:?}", res);
|
||||
assistant.set_page_complete(&page1, true);
|
||||
assistant.next_page();
|
||||
let auth_link = oauth::gen_authorize_url(&instance_uri, &res);
|
||||
|
@ -45,5 +44,20 @@ pub fn create_oauth_assistant(app: >k::Application) -> gtk::Assistant {
|
|||
}
|
||||
}));
|
||||
|
||||
page2.imp().authorization_token_entry.connect_activate(
|
||||
glib::clone!(@weak page2, @weak page1, @weak assistant => move |_| {
|
||||
let instance_uri = page1.imp().instance_uri_text_entry.buffer().text();
|
||||
let authorization_token = page2.imp().authorization_token_entry.buffer().text();
|
||||
match accounts::verify_credentials(&instance_uri, &authorization_token) {
|
||||
Ok(res) => {
|
||||
assistant.set_page_complete(&page2, true);
|
||||
assistant.next_page();
|
||||
// TODO: save to DB
|
||||
},
|
||||
Err(err) => println!("Error: {:?}", err),
|
||||
|
||||
}
|
||||
}));
|
||||
|
||||
assistant
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ use gtk4 as gtk;
|
|||
use gtk::subclass::prelude::*;
|
||||
use gtk::subclass::window::WindowImpl;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{glib, CompositeTemplate, Box, Label};
|
||||
use gtk::{glib, CompositeTemplate, Box, Label, Entry};
|
||||
use glib::subclass::InitializingObject;
|
||||
|
||||
#[derive(Debug, Default, CompositeTemplate)]
|
||||
|
@ -10,6 +10,8 @@ use glib::subclass::InitializingObject;
|
|||
pub struct OauthAssistantPage2 {
|
||||
#[template_child]
|
||||
pub authorization_link_label: TemplateChild<Label>,
|
||||
#[template_child]
|
||||
pub authorization_token_entry: TemplateChild<Entry>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
|
|
Loading…
Reference in New Issue