From 314aaf52a260f6a5cc4673b11e8d28aff2c7f0db Mon Sep 17 00:00:00 2001 From: Butcat Date: Sat, 20 Dec 2025 19:19:47 +0100 Subject: [PATCH 1/3] added endpoints for auth to the client. --- desktop-client/src/services/api.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/desktop-client/src/services/api.rs b/desktop-client/src/services/api.rs index aecbc45..f18e5a9 100644 --- a/desktop-client/src/services/api.rs +++ b/desktop-client/src/services/api.rs @@ -46,6 +46,7 @@ impl RustySecureApi for RustySecureApiImpl { .to_string(); Ok((auth_url, state)) + //This is a test for n8n will be deleted then.. } async fn get_user_by_google_id(&self, id: String) -> Result, Error> { From d1a129a9b6b0af412901169ec7a6f8d18951a5e5 Mon Sep 17 00:00:00 2001 From: Butcat Date: Wed, 31 Dec 2025 00:32:03 +0700 Subject: [PATCH 2/3] upgrade auth api to v3 and fix problem with missing emails with user info endpoint --- api-server/src/payloads/google.rs | 1 + api-server/src/services/google_auth.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/api-server/src/payloads/google.rs b/api-server/src/payloads/google.rs index 7748a4a..981ada8 100644 --- a/api-server/src/payloads/google.rs +++ b/api-server/src/payloads/google.rs @@ -4,6 +4,7 @@ use crate::models::Token; #[derive(Deserialize, Serialize)] pub struct UserInfo { + #[serde(alias = "sub")] pub id: String, pub email: String, pub name: String, diff --git a/api-server/src/services/google_auth.rs b/api-server/src/services/google_auth.rs index b3600a2..1334321 100644 --- a/api-server/src/services/google_auth.rs +++ b/api-server/src/services/google_auth.rs @@ -37,7 +37,7 @@ impl GoogleAuthServiceImpl { auth_url: "https://accounts.google.com/o/oauth2/v2/auth".to_string(), token_url: "https://www.googleapis.com/oauth2/v3/token".to_string(), revocation_url: "https://oauth2.googleapis.com/revoke".to_string(), - user_info_url: "https://www.googleapis.com/oauth2/v2/userinfo".to_string(), + user_info_url: "https://www.googleapis.com/oauth2/v3/userinfo".to_string(), } } } From fda12c9691e948846a01da8a93438575d29eb0de Mon Sep 17 00:00:00 2001 From: Butcat Date: Sun, 4 Jan 2026 22:36:42 +0700 Subject: [PATCH 3/3] add api login capabilities with google auth integration and user saving, add ui element for the desktop app for the integration --- api-server/src/handlers/auth_handler.rs | 47 ++++++- api-server/src/handlers/picture_hander.rs | 4 +- api-server/src/models/picture.rs | 4 +- api-server/src/services/google_auth.rs | 14 ++- api-server/src/services/mod.rs | 8 +- api-server/src/services/picture.rs | 4 +- desktop-client/Cargo.toml | 12 +- desktop-client/src/app/config.rs | 2 +- desktop-client/src/app/message.rs | 10 +- desktop-client/src/app/mod.rs | 142 ++++++++++++++++++---- desktop-client/src/app/state.rs | 14 ++- desktop-client/src/main.rs | 5 +- desktop-client/src/models/auth.rs | 21 ++++ desktop-client/src/models/mod.rs | 5 +- desktop-client/src/models/user.rs | 4 +- desktop-client/src/services/api.rs | 21 ++-- 16 files changed, 254 insertions(+), 63 deletions(-) create mode 100644 desktop-client/src/models/auth.rs diff --git a/api-server/src/handlers/auth_handler.rs b/api-server/src/handlers/auth_handler.rs index 9196874..b5a3cb3 100644 --- a/api-server/src/handlers/auth_handler.rs +++ b/api-server/src/handlers/auth_handler.rs @@ -5,6 +5,8 @@ use crate::errors::Error; use crate::models::User; use crate::payloads::{AuthResponse, OAuthCallback}; +use serde::Deserialize; + #[routes] #[get("/api/auth/callback")] pub async fn callback( @@ -12,7 +14,7 @@ pub async fn callback( data: web::Data, ) -> Result { let callback_data = query.into_inner(); - let state = callback_data.state; + let state = callback_data.state.clone(); let code = callback_data.code; println!("State: {}, code: {}", state, code); let (user_info, token) = data @@ -45,18 +47,51 @@ pub async fn callback( }; let response = AuthResponse { user_info, token }; + let response_type = if callback_data.state.contains(':') { + callback_data.state.split(':').nth(1).unwrap_or("json") + } else { + "json" + }; + + // We only take care of html or always return json payload + if response_type == "html" { + let html_body = format!( + r#" + + + Login Successful + +

Login Successful!

+ + + + + + "#, + serde_json::to_string(&response).unwrap() + ); + Ok(HttpResponse::Ok().content_type("text/html").body(html_body)) + } else { + Ok(HttpResponse::Ok().json(response)) + } +} - Ok(HttpResponse::Ok().json(response)) +#[derive(Deserialize)] +pub struct AuthUrlQuery { + pub response_type: Option, } #[routes] #[get("/api/auth/url")] -pub async fn auth_url(data: web::Data) -> Result { - let auth_url = data +pub async fn auth_url( + query: web::Query, + data: web::Data, +) -> Result { + let (url, state) = data .goolge_auth_service - .get_authorisation_url() + .get_authorisation_url(query.response_type.clone()) .await .map_err(|e| Error::Internal(e.to_string()))?; - Ok(HttpResponse::Ok().json(auth_url)) + Ok(HttpResponse::Ok().json((url, state))) } diff --git a/api-server/src/handlers/picture_hander.rs b/api-server/src/handlers/picture_hander.rs index f35af96..98dc241 100644 --- a/api-server/src/handlers/picture_hander.rs +++ b/api-server/src/handlers/picture_hander.rs @@ -17,8 +17,8 @@ pub async fn post_picture( .await .map_err(|_| Error::Internal("Failed to register or upload picture".to_string()))?; - // This is for testing, this request should be used when someone review if - // the person on the picture is recognised to then authorised and sent it + // NOTE: This is for testing, this request should be used when someone review + // if the person on the picture is recognised to then authorised and sent it let _ok = data .status_service .send_status(status_response.id) diff --git a/api-server/src/models/picture.rs b/api-server/src/models/picture.rs index 1283f0d..ddec802 100644 --- a/api-server/src/models/picture.rs +++ b/api-server/src/models/picture.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; pub struct Picture { #[serde(rename = "_id")] pub id: Uuid, + pub user_id: Uuid, pub name: String, pub url: String, pub created_at: DateTime, @@ -13,9 +14,10 @@ pub struct Picture { } impl Picture { - pub fn new(name: String, url: String) -> Self { + pub fn new(user_id: Uuid, name: String, url: String) -> Self { Self { id: Uuid::new(), + user_id, name, url, created_at: Local::now(), diff --git a/api-server/src/services/google_auth.rs b/api-server/src/services/google_auth.rs index 1334321..e950665 100644 --- a/api-server/src/services/google_auth.rs +++ b/api-server/src/services/google_auth.rs @@ -44,7 +44,10 @@ impl GoogleAuthServiceImpl { #[async_trait] impl GoogleAuthService for GoogleAuthServiceImpl { - async fn get_authorisation_url(&self) -> Result<(String, String), Error> { + async fn get_authorisation_url( + &self, + response_type: Option, + ) -> Result<(String, String), Error> { let auth_url = AuthUrl::new(self.auth_url.clone()).map_err(|e| Error::Parse(e.to_string())); let token_url = @@ -62,8 +65,15 @@ impl GoogleAuthService for GoogleAuthServiceImpl { .expect("Invalid revocation endpoints URL"), ); + let random_token = CsrfToken::new_random(); + let final_token = if let Some(value) = response_type { + CsrfToken::new(format!("{}:{}", random_token.secret(), value)) + } else { + random_token + }; + let (authorize_url, crsf_state) = client - .authorize_url(CsrfToken::new_random) + .authorize_url(|| final_token) .add_scopes( self.scope .split_whitespace() diff --git a/api-server/src/services/mod.rs b/api-server/src/services/mod.rs index 86ddd17..bf3af08 100644 --- a/api-server/src/services/mod.rs +++ b/api-server/src/services/mod.rs @@ -14,7 +14,7 @@ use async_trait::async_trait; use bson::Uuid; use crate::errors::Error; -use crate::models::{Token, User}; +use crate::models::{Picture, Token, User}; use crate::payloads::{StatusResponse, UserInfo}; // NOTE: Service should return a model then the API layer convert to payload.. @@ -36,11 +36,15 @@ pub trait PictureService: Send + Sync { &self, image_data: Vec, ) -> Result; + async fn get_all(user_id: Uuid) -> Result, Error>; } #[async_trait] pub trait GoogleAuthService: Send + Sync { - async fn get_authorisation_url(&self) -> Result<(String, String), Error>; + async fn get_authorisation_url( + &self, + response_type: Option, + ) -> Result<(String, String), Error>; async fn exchange_code_for_token( &self, code: String, diff --git a/api-server/src/services/picture.rs b/api-server/src/services/picture.rs index 028c54c..b149f28 100644 --- a/api-server/src/services/picture.rs +++ b/api-server/src/services/picture.rs @@ -1,4 +1,5 @@ use async_trait::async_trait; +use bson::Uuid; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; @@ -30,7 +31,6 @@ impl PictureServiceImpl { } #[async_trait] -// TODO: Better error handle, got lazy :) impl PictureService for PictureServiceImpl { async fn upload_and_register_picture( &self, @@ -62,4 +62,6 @@ impl PictureService for PictureServiceImpl { Err(Error::Empty("Image data is empty".to_string())) } } + + async fn get_all(user_id: Uuid) -> Result, Error> {} } diff --git a/desktop-client/Cargo.toml b/desktop-client/Cargo.toml index 3d2e937..9d9a95f 100644 --- a/desktop-client/Cargo.toml +++ b/desktop-client/Cargo.toml @@ -8,6 +8,16 @@ edition = "2024" [dependencies] chrono = { version = "0.4.40", features = ["serde"] } google-oauth = "1.11.3" -iced = "0.13.1" +iced = { version = "0.13.1", features = ["tokio"] } reqwest = { version = "0.12.15", features = ["json"] } +tokio = { version = "1", features = ["full"] } serde = "1.0.228" +webbrowser = "1.0.6" +serde_json = "1.0.148" + +[package.metadata.bundle] +name = "desktop-client" +identifier = "com.iButcat.desktop-client" +icon = ["assets/icons/icon.png"] +version = "1.0.0" +copyright = "Copyright 2025 iButcat" diff --git a/desktop-client/src/app/config.rs b/desktop-client/src/app/config.rs index b08adf4..173589e 100644 --- a/desktop-client/src/app/config.rs +++ b/desktop-client/src/app/config.rs @@ -6,7 +6,7 @@ pub struct Config { impl Default for Config { fn default() -> Self { Self { - api_base_url: "http://localhost:8080".to_string(), + api_base_url: "http://0.0.0.0:8080".to_string(), } } } diff --git a/desktop-client/src/app/message.rs b/desktop-client/src/app/message.rs index 18276c1..29eaa26 100644 --- a/desktop-client/src/app/message.rs +++ b/desktop-client/src/app/message.rs @@ -1,4 +1,5 @@ use crate::app::Page; +use crate::models::User; #[derive(Debug, Clone)] pub enum Message { @@ -8,7 +9,6 @@ pub enum Message { SetError(String), ClearError, - Login(String, String), Logout, FetchData, @@ -16,7 +16,11 @@ pub enum Message { ButtonPressed, TextChanged(String), - UsernameChanged(String), - PasswordChanged(String), CheckboxToggled(bool), + + LoginWithGoogle, + AuthUrlReceived(Result<(String, String), String>), + TokenInputChanged(String), + SubmitToken, + UserFetched(Result, String>), } diff --git a/desktop-client/src/app/mod.rs b/desktop-client/src/app/mod.rs index 793b45d..de3e02d 100644 --- a/desktop-client/src/app/mod.rs +++ b/desktop-client/src/app/mod.rs @@ -6,62 +6,141 @@ use message::Message; use state::AppState; use crate::app::state::Page; +use crate::models::AuthResponse; +use crate::services::RustySecureApi; -use iced::Element; use iced::widget::{button, column, container, text, text_input}; +use iced::{Element, Task}; pub fn run() -> iced::Result { iced::run("Rusty Secure Client", update, view) } -fn update(state: &mut AppState, message: Message) { +// TODO: Handle the error correctly instead of having only Task::none() +fn update(state: &mut AppState, message: Message) -> Task { match message { Message::ButtonPressed => { println!("Button pressed"); + Task::none() } Message::TextChanged(text) => { println!("Text changed: {}", text); - } - Message::UsernameChanged(username) => { - println!("Username changed: {}", username); - state.username = username; - } - Message::PasswordChanged(password) => { - println!("Password changed: {}", password); - state.password = password; + Task::none() } Message::CheckboxToggled(checked) => { println!("Checkbox toggled: {}", checked); + Task::none() } Message::NavigateTo(page) => { state.current_page = page; + Task::none() } Message::SetLoading(loading) => { state.loading = loading; + Task::none() } Message::SetError(error) => { state.error_message = Some(error); + Task::none() } Message::ClearError => { state.error_message = None; - } - Message::Login(username, password) => { - println!("Login attempt: {} / {}", username, password); - println!( - "Got stored username: {}, and password: {}", - state.username, state.password - ); + Task::none() } Message::Logout => { println!("Logout"); + Task::none() } Message::FetchData => { println!("Fetching data..."); + Task::none() } Message::DataLoaded(result) => match result { - Ok(data) => println!("Data loaded: {:?}", data), - Err(e) => println!("Error loading data: {}", e), + Ok(data) => { + println!("Data loaded: {:?}", data); + Task::none() + } + Err(e) => { + println!("Error loading data: {}", e); + Task::none() + } }, + Message::LoginWithGoogle => { + let api = state.api_service.clone(); + + Task::perform( + async move { api.get_auth_url().await.map_err(|e| e.to_string()) }, + Message::AuthUrlReceived, + ) + } + Message::AuthUrlReceived(result) => { + match result { + Ok((url, _auth_state)) => { + println!("Go to URL: {}", url); + webbrowser::open(&url).ok(); + } + Err(e) => { + println!("Auth Error: {}", e); + state.error_message = Some(e); + } + } + Task::none() + } + Message::TokenInputChanged(input) => { + println!("Token input changed: {}", input); + state.token_input = input; + Task::none() + } + Message::SubmitToken => { + let parsed: Result = serde_json::from_str(&state.token_input); + + match parsed { + Ok(auth_data) => { + println!( + "Token valid. Fetching profile for Google ID: {}", + auth_data.user_info.id, + ); + state.set_loading(true); + + let api = state.api_service.clone(); + let google_id = auth_data.user_info.id; + + Task::perform( + async move { + api.get_user_by_google_id(google_id) + .await + .map_err(|e| e.to_string()) + }, + Message::UserFetched, + ) + } + Err(e) => { + println!("Parsing Failed: {}", e); + state.error_message = Some(format!("Invalid Token JSON: {}", e)); + Task::none() + } + } + } + Message::UserFetched(result) => { + state.set_loading(false); + + match result { + Ok(Some(user)) => { + println!("User fetched: {:?}", user); + state.user = Some(user); + state.user_logged_in = true; + state.current_page = Page::Home; + } + Ok(None) => { + state.error_message = Some("User not found in database.".to_string()); + } + Err(e) => { + println!("Failed to fetch profile: {}", e); + state.error_message = Some(format!("Failed to fetch profile: {}", e)); + } + } + Task::none() + } } } @@ -103,10 +182,27 @@ fn settings_view() -> Element<'static, Message> { fn login_view(state: &AppState) -> Element<'static, Message> { column![ - text("Login").size(24), - text_input("Username", &state.username).on_input(Message::UsernameChanged), - text_input("Password", &state.password).on_input(Message::PasswordChanged), - button("Login").on_press(Message::Login("user".to_string(), "pass".to_string())), + text("Login to Rusty Secure").size(30), + container( + column![ + text("Step 1: Open Browser").size(18), + button("Login with Google").on_press(Message::LoginWithGoogle), + ] + .spacing(10) + ) + .padding(10) + .style(iced::widget::container::bordered_box), + container( + column![ + text("Step 2: Paste code").size(18), + text("Copy the code from the browser and paste it here:"), + text_input("Paste JSON Here...", &state.token_input) + .on_input(Message::TokenInputChanged) + .on_submit(Message::SubmitToken), + button("Complete Login").on_press(Message::SubmitToken) + ] + .spacing(10) + ) ] .spacing(10) .padding(20) diff --git a/desktop-client/src/app/state.rs b/desktop-client/src/app/state.rs index 40cb853..b115475 100644 --- a/desktop-client/src/app/state.rs +++ b/desktop-client/src/app/state.rs @@ -1,3 +1,7 @@ +use reqwest::Client; + +use crate::services::RustySecureApiImpl; + #[derive(Debug, Clone)] pub enum Page { Home, @@ -10,9 +14,10 @@ pub struct AppState { pub current_page: Page, pub loading: bool, pub error_message: Option, - pub username: String, - pub password: String, pub user_logged_in: bool, + pub api_service: RustySecureApiImpl, + pub token_input: String, + pub user: Option, } impl Default for AppState { @@ -21,9 +26,10 @@ impl Default for AppState { current_page: Page::Login, loading: false, error_message: None, - username: String::new(), - password: String::new(), user_logged_in: false, + api_service: RustySecureApiImpl::new(Client::new(), "http://0.0.0.0:8080".to_string()), + token_input: String::new(), + user: None, } } } diff --git a/desktop-client/src/main.rs b/desktop-client/src/main.rs index 25fd7cb..faaaa85 100644 --- a/desktop-client/src/main.rs +++ b/desktop-client/src/main.rs @@ -1,10 +1,11 @@ mod app; use app::run; -mod services; mod errors; mod models; +mod services; -pub fn main() -> iced::Result { +#[tokio::main] +async fn main() -> iced::Result { run() } diff --git a/desktop-client/src/models/auth.rs b/desktop-client/src/models/auth.rs new file mode 100644 index 0000000..e0f0d6f --- /dev/null +++ b/desktop-client/src/models/auth.rs @@ -0,0 +1,21 @@ +use serde::Deserialize; + +#[derive(Deserialize, Debug, Clone)] +pub struct TokenData { + pub access_token: String, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct GoogleUserInfo { + #[serde(alias = "sub")] + pub id: String, + pub email: String, + pub name: String, + pub picture: Option, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct AuthResponse { + pub token: TokenData, + pub user_info: GoogleUserInfo, +} diff --git a/desktop-client/src/models/mod.rs b/desktop-client/src/models/mod.rs index 4c2bec9..1301b6f 100644 --- a/desktop-client/src/models/mod.rs +++ b/desktop-client/src/models/mod.rs @@ -1,2 +1,5 @@ pub mod user; -pub use user::User; \ No newline at end of file +pub use user::User; + +pub mod auth; +pub use auth::AuthResponse; diff --git a/desktop-client/src/models/user.rs b/desktop-client/src/models/user.rs index d27636f..9465502 100644 --- a/desktop-client/src/models/user.rs +++ b/desktop-client/src/models/user.rs @@ -7,7 +7,7 @@ pub struct User { pub email: String, pub name: String, pub picture: Option, - pub created_at: DateTime, + pub created_at: Option>, pub updated_at: Option>, } @@ -17,7 +17,7 @@ impl User { email: String, name: String, picture: Option, - created_at: DateTime, + created_at: Option>, updated_at: Option>, ) -> Self { User { diff --git a/desktop-client/src/services/api.rs b/desktop-client/src/services/api.rs index f18e5a9..1bf631f 100644 --- a/desktop-client/src/services/api.rs +++ b/desktop-client/src/services/api.rs @@ -6,6 +6,7 @@ use super::RustySecureApi; use crate::errors::Error; use crate::models::User; +#[derive(Debug, Clone)] pub struct RustySecureApiImpl { client: Client, api_base_url: String, @@ -24,7 +25,10 @@ impl RustySecureApi for RustySecureApiImpl { async fn get_auth_url(&self) -> Result<(String, String), crate::errors::Error> { let response = self .client - .get(format!("{}/api/auth/url", self.api_base_url.clone())) + .get(format!( + "{}/api/auth/url?response_type=html", + self.api_base_url.clone() + )) .send() .await .map_err(|e| Error::ApiError(e.to_string()))?; @@ -33,31 +37,24 @@ impl RustySecureApi for RustySecureApiImpl { return Err(Error::ApiError("Unexpected status code".to_string())); } - let auth_url = response - .text() + let (auth_url, state): (String, String) = response + .json() .await .map_err(|e| Error::ApiError(e.to_string()))?; - let state = response - .headers() - .get("X-State") - .unwrap() - .to_str() - .unwrap() - .to_string(); Ok((auth_url, state)) - //This is a test for n8n will be deleted then.. } async fn get_user_by_google_id(&self, id: String) -> Result, Error> { let response = self .client - .get(format!("{}/user/{}", self.api_base_url.clone(), id)) + .get(format!("{}/api/user/{}", self.api_base_url.clone(), id)) .send() .await .map_err(|e| Error::ApiError(e.to_string()))?; if response.status() != 200 { + println!("Error status: {}", response.status()); return Err(Error::ApiError("Unexpected status code".to_string())); }