From dc454ccf3ad32c3a4fd56b620f7abd0689b27a1b Mon Sep 17 00:00:00 2001 From: Joshua Higgins Date: Sat, 12 Oct 2024 01:28:24 -0400 Subject: [PATCH] Logging in and Signing up on the client done Tons of bug fixes too --- .gitignore | 4 +- auth/.env | 12 - auth/Cargo.toml | 2 +- auth/example.env | 12 + login_email.html => auth/login_email.html | 4 +- .../20240725215330_create_everything.sql | 6 +- auth/src/server.rs | 211 ++++++++---------- auth/src/types.rs | 12 +- client/Cargo.toml | 8 +- client/src/app.rs | 62 ++++- client/src/main.rs | 3 +- client/src/types.rs | 9 + client/src/ui/panels.rs | 181 +++++++++++++-- login_email.txt | 10 - server/.env | 4 - server/example.env | 4 + server/src/server.rs | 2 +- shared/src/types.rs | 2 + 18 files changed, 367 insertions(+), 181 deletions(-) delete mode 100644 auth/.env create mode 100644 auth/example.env rename login_email.html => auth/login_email.html (73%) delete mode 100644 login_email.txt delete mode 100644 server/.env create mode 100644 server/example.env diff --git a/.gitignore b/.gitignore index 32148b5..7f0cc7a 100644 --- a/.gitignore +++ b/.gitignore @@ -19,8 +19,8 @@ client/.gradle client/build server/.idea -#server/.env -#auth/.env +server/.env +auth/.env *.db *.db-shm diff --git a/auth/.env b/auth/.env deleted file mode 100644 index 2e80e23..0000000 --- a/auth/.env +++ /dev/null @@ -1,12 +0,0 @@ -DATABASE_URL=sqlite:auth.db -PORT=5052 -GOOGLE_CLIENT_ID=972646764847-j9o7v3th4bf1ln18hitgolqp03s6doo4.apps.googleusercontent.com -GOOGLE_PRIVATE_KEY=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 -DOMAIN=realmchat.com - -SERVER_MAIL_PORT=587 -SERVER_MAIL_ADDRESS=mail.abunchofknowitalls.com -SERVER_MAIL_NAME="Realm Chat Support" -SERVER_MAIL_FROM_ADDRESS=realmchat@abunchofknowitalls.com -SERVER_MAIL_USERNAME=realmchat@abunchofknowitalls.com -SERVER_MAIL_PASSWORD=9BB*bD9RnSKMAf \ No newline at end of file diff --git a/auth/Cargo.toml b/auth/Cargo.toml index bde448f..cdf0328 100644 --- a/auth/Cargo.toml +++ b/auth/Cargo.toml @@ -17,6 +17,6 @@ sqlx = { version = "0.8.2", features = [ "runtime-tokio", "tls-rustls", "sqlite" sha3 = "0.10.8" hex = "0.4.3" rand = "0.8.5" -mail-send = "0.4.9" +lettre = "0.11.9" regex = "1.10.5" realm_shared = { path = "../shared" } diff --git a/auth/example.env b/auth/example.env new file mode 100644 index 0000000..1945256 --- /dev/null +++ b/auth/example.env @@ -0,0 +1,12 @@ +DATABASE_URL=sqlite:auth.db +PORT=5052 +GOOGLE_CLIENT_ID= +GOOGLE_PRIVATE_KEY= +DOMAIN= + +SERVER_MAIL_PORT= +SERVER_MAIL_ADDRESS= +SERVER_MAIL_NAME= +SERVER_MAIL_FROM_ADDRESS= +SERVER_MAIL_USERNAME= +SERVER_MAIL_PASSWORD= \ No newline at end of file diff --git a/login_email.html b/auth/login_email.html similarity index 73% rename from login_email.html rename to auth/login_email.html index d533898..766d755 100644 --- a/login_email.html +++ b/auth/login_email.html @@ -2,11 +2,11 @@

Confirm your email address

Your 6 digit code is below – enter it into Realm and you will be signed in

-

{}

+

{$LOGIN_CODE}

If you didn't request this email, there's nothing to worry about it– you can safely ignore it.

Realm
- Need help? Email realm-support@abunchofknowtiwalls.com
+ Need help? Email realm@abunchofknowtiwalls.com
© 2024 Realm, Inc.

\ No newline at end of file diff --git a/auth/migrations/20240725215330_create_everything.sql b/auth/migrations/20240725215330_create_everything.sql index 0164f6b..e16c2db 100644 --- a/auth/migrations/20240725215330_create_everything.sql +++ b/auth/migrations/20240725215330_create_everything.sql @@ -7,9 +7,5 @@ CREATE TABLE IF NOT EXISTS user ( avatar TEXT NOT NULL, servers TEXT NOT NULL, login_code INT(6), - tokens TEXT, - google_oauth VARCHAR(255), - apple_oauth VARCHAR(255), - github_oauth VARCHAR(255), - discord_oauth VARCHAR(255) + tokens TEXT ); \ No newline at end of file diff --git a/auth/src/server.rs b/auth/src/server.rs index c0bb4ce..e53a70d 100644 --- a/auth/src/server.rs +++ b/auth/src/server.rs @@ -2,17 +2,17 @@ use std::env; use std::net::SocketAddr; use chrono::Utc; -use mail_send::{Credentials, SmtpClientBuilder}; -use mail_send::mail_builder::MessageBuilder; +use lettre::{Message, SmtpTransport, Transport}; +use lettre::message::header::ContentType; +use lettre::message::Mailbox; +use lettre::transport::smtp::authentication::Credentials; use rand::Rng; use regex::Regex; use sha3::{Digest, Sha3_256}; use sha3::digest::Update; use sqlx::{Pool, query, Sqlite}; -use sqlx::sqlite::{SqliteQueryResult, SqliteRow}; use tarpc::context::Context; use tracing::*; -use tracing::log::__private_api::log; use crate::types::{AuthEmail, AuthUser, RealmAuth}; use realm_shared::types::ErrorCode; use realm_shared::types::ErrorCode::*; @@ -23,7 +23,6 @@ pub struct RealmAuthServer { pub db_pool: Pool, pub auth_email: AuthEmail, pub template_html: String, - pub template_txt: String, pub domain: String, } @@ -34,27 +33,21 @@ impl RealmAuthServer { db_pool, auth_email, template_html: std::fs::read_to_string("./login_email.html").expect("A login_email.html file is needed"), - template_txt: std::fs::read_to_string("./login_email.txt").expect("A login_email.txt file is needed"), domain: env::var("DOMAIN").expect("DOMAIN must be set"), } } - fn gen_login_code(&self) -> u16 { + fn gen_login_code(&self) -> u32 { let mut rng = rand::thread_rng(); - let mut login_code: u16 = 0; - for n in 1..=6 { - if n == 1 { - login_code += rng.gen_range(1..=9); - } - login_code += rng.gen_range(0..=9) * (10^n); - } + let first_digit: u32 = rng.gen_range(1..10); + let remaining_digits: u32 = rng.gen_range(0..100_000); - login_code + first_digit * 100_000 + remaining_digits } async fn is_username_taken(&self, username: &str) -> Result { - let result = query!("SELECT NOT EXISTS (SELECT 1 FROM user WHERE username = ?) AS does_exist", username).fetch_one(&self.db_pool).await; + let result = query!("SELECT EXISTS (SELECT 1 FROM user WHERE username = ?) AS does_exist", username).fetch_one(&self.db_pool).await; match result { Ok(row) => Ok(row.does_exist != 0), @@ -63,7 +56,7 @@ impl RealmAuthServer { } async fn is_email_taken(&self, email: &str) -> Result { - let result = query!("SELECT NOT EXISTS (SELECT 1 FROM user WHERE email = ?) AS does_exist", email).fetch_one(&self.db_pool).await; + let result = query!("SELECT EXISTS (SELECT 1 FROM user WHERE email = ?) AS does_exist", email).fetch_one(&self.db_pool).await; match result { Ok(row) => Ok(row.does_exist != 0), @@ -81,57 +74,54 @@ impl RealmAuthServer { for i in 0..tokens.len() { if tokens.get(i).unwrap() == &token { - return Ok(true) + return Ok(true); } } Ok(false) - }, + } Err(_) => Err(InvalidUsername), } } - async fn send_login_message(&self, username: &str, email: &str, login_code: u16) -> Result<(), ErrorCode> { - let message = MessageBuilder::new() - .from((self.auth_email.auth_name.clone(), self.auth_email.auth_username.clone())) - .to(vec![ - (username, email), - ]) - .subject(format!("Realm confirmation code: {}", &login_code)) - .html_body(self.template_html.replace("{}", &login_code.to_string())) - .text_body(self.template_txt.replace("{}", &login_code.to_string())); + fn send_login_message(&self, username: &str, email: &str, login_code: u32) { // -> Result<(), ErrorCode> + let auth_email = self.auth_email.clone(); + let template_html = self.template_html.clone(); + let username = username.to_string(); + let email = email.to_string(); - let result = SmtpClientBuilder::new(&self.auth_email.server_address, self.auth_email.server_port) - .implicit_tls(false) - .credentials(Credentials::new(&self.auth_email.auth_username, &self.auth_email.auth_password)) - .connect() - .await; + tokio::spawn(async move { + let email = Message::builder() + .from(Mailbox::new(Some(auth_email.auth_name.clone()), auth_email.auth_username.clone().parse().unwrap())) + .to(Mailbox::new(Some(username.clone()), email.clone().parse().unwrap())) + .subject(format!("Realm confirmation code: {}", &login_code)) + .header(ContentType::TEXT_HTML) + .body(template_html.replace("{$LOGIN_CODE}", &login_code.to_string())) + .unwrap(); - match result { - Ok(mut client) => { - let result = client.send(message).await; - match result { - Ok(_) => { - Ok(()) - } - Err(_) => { - Err(UnableToSendMail) - } - } + let creds = Credentials::new(auth_email.auth_username, auth_email.auth_password); + + // Open a remote connection to gmail + let mailer = SmtpTransport::relay(&auth_email.server_address) + .unwrap() + .credentials(creds) + .build(); + + // Send the email + match mailer.send(&email) { + Ok(_) => info!("Email sent successfully!"), + Err(e) => error!("Could not send email: {e:?}"), } - Err(_) => { - Err(UnableToConnectToMail) - } - } + }); } - async fn is_login_code_valid(&self, username: &str, login_code: u16) -> Result { + async fn is_login_code_valid(&self, username: &str, login_code: u32) -> Result { let result = query!("SELECT login_code FROM user WHERE username = ?;", username).fetch_one(&self.db_pool).await; match result { Ok(row) => { - if row.login_code.unwrap() as u16 != login_code { - return Ok(false) + if row.login_code.unwrap() as u32 != login_code { + return Ok(false); } Ok(true) } @@ -141,19 +131,19 @@ impl RealmAuthServer { fn is_username_valid(&self, username: &str) -> bool { if !username.starts_with('@') || !username.contains(':') { - return false + return false; } let name = &username[1..username.find(':').unwrap()]; - let domain = &username[username.find(':').unwrap()+1..]; + let domain = &username[username.find(':').unwrap() + 1..]; let re = Regex::new(r"^[a-zA-Z0-9]+$").unwrap(); if !re.is_match(name) { - return false + return false; } if !domain.eq(&self.domain) { - return false + return false; } true @@ -188,12 +178,12 @@ impl RealmAuth for RealmAuthServer { for token in tokens { let hash = Sha3_256::new().chain(format!("{}{}{}{}", token, server_id, domain, tarpc_port)).finalize(); if hex::encode(hash) == server_token { - return true + return true; } } false - }, + } Err(_) => false, } } @@ -202,26 +192,29 @@ impl RealmAuth for RealmAuthServer { info!("API Request: create_account_flow( username -> {}, email -> {} )", username, email); if !self.is_username_valid(&username) { - return Err(InvalidUsername) + return Err(InvalidUsername); } if self.is_username_taken(&username).await? { - return Err(UsernameTaken) + return Err(UsernameTaken); } if self.is_email_taken(&email).await? { - return Err(EmailTaken) + return Err(EmailTaken); } let code = self.gen_login_code(); - self.send_login_message(&username, &email, code).await?; + self.send_login_message(&username, &email, code); - let result = query!("INSERT INTO user (username, email, avatar, login_code, tokens) VALUES (?, ?, '', ?, '')", username, email, code) + let result = query!("INSERT INTO user (username, email, new_email, avatar, servers, login_code, tokens) VALUES (?, ?, '', '', '', ?, '')", username, email, code) .execute(&self.db_pool).await; match result { Ok(_) => Ok(()), - Err(_) => Err(Error) + Err(e) => { + error!("Error creating account: {e:?}"); + Err(Error) + } } } @@ -229,7 +222,7 @@ impl RealmAuth for RealmAuthServer { info!("API Request: create_login_flow( username -> {}, email -> {} )", username.clone().unwrap_or("None".to_string()), email.clone().unwrap_or("None".to_string())); if username.is_none() && email.is_none() { - return Err(Error) + return Err(Error); } if username.is_none() { @@ -264,17 +257,20 @@ impl RealmAuth for RealmAuthServer { .execute(&self.db_pool).await; match result { - Ok(_) => self.send_login_message(&username.unwrap(), &email.unwrap(), code).await, + Ok(_) => { + self.send_login_message(&username.unwrap(), &email.unwrap(), code); + Ok(()) + } Err(_) => Err(InvalidUsername) } } - async fn finish_login_flow(self, _: Context, username: String, login_code: u16) -> Result { + async fn finish_login_flow(self, _: Context, username: String, login_code: u32) -> Result { info!("API Request: finish_login_flow( username -> {}, login_code -> {} )", username, login_code); if !self.is_login_code_valid(&username, login_code).await? { error!("Unauthorized request made for finish_login_flow() (bad login code)! username -> {}, login_code -> {}", username, login_code); - return Err(InvalidLoginCode) + return Err(InvalidLoginCode); } self.reset_login_code(&username).await?; @@ -307,11 +303,11 @@ impl RealmAuth for RealmAuthServer { info!("API Request: change_email_flow( username -> {}, new_email -> {}, token -> {} )", username, new_email, token); if !self.is_authorized(&username, &token).await? { - return Err(Unauthorized) + return Err(Unauthorized); } if self.is_email_taken(&new_email).await? { - return Err(EmailTaken) + return Err(EmailTaken); } let _ = query!("UPDATE user SET new_email = ? WHERE username = ?", new_email, username).execute(&self.db_pool).await.unwrap(); @@ -322,27 +318,30 @@ impl RealmAuth for RealmAuthServer { .execute(&self.db_pool).await; match result { - Ok(_) => self.send_login_message(&username, &new_email, code).await, + Ok(_) => { + self.send_login_message(&username, &new_email, code); + Ok(()) + } Err(_) => Err(InvalidUsername) } } - async fn finish_change_email_flow(self, _: Context, username: String, new_email: String, token: String, login_code: u16) -> Result<(), ErrorCode> { + async fn finish_change_email_flow(self, _: Context, username: String, new_email: String, token: String, login_code: u32) -> Result<(), ErrorCode> { info!("API Request: finish_change_email_flow( username -> {}, new_email -> {}, token -> {}, login_code -> {} )", username, new_email, token, login_code); if !self.is_authorized(&username, &token).await? { error!("Unauthorized request made for finish_change_email_flow() (bad token)! username -> {}, token -> {}", username, token); - return Err(Unauthorized) + return Err(Unauthorized); } if self.is_email_taken(&new_email).await? { error!("Email already taken for email change (but its the end of the flow?!) username -> {}, new_email -> {}", username, new_email); - return Err(EmailTaken) + return Err(EmailTaken); } if !self.is_login_code_valid(&username, login_code).await? { error!("Unauthorized request made for finish_change_email_flow() (bad login code)! username -> {}, login_code -> {}", username, login_code); - return Err(InvalidLoginCode) + return Err(InvalidLoginCode); } let _ = query!("UPDATE user SET new_email = NULL WHERE username = ?", username).execute(&self.db_pool).await; @@ -354,37 +353,12 @@ impl RealmAuth for RealmAuthServer { Ok(()) } - // async fn change_username(self, _: Context, username: String, token: String, new_username: String) -> Result<(), ErrorCode> { - // info!("API Request: change_username( username -> {}, token -> {}, new_username -> {} )", username, token, new_username); - // - // if !self.is_authorized(&username, &token).await? { - // error!("Unauthorized request made for change_username()! username -> {}, token -> {}", username, token); - // return Err(Unauthorized) - // } - // - // if !self.is_username_valid(&new_username) { - // error!("Malformed username in request for change_username()! new_username -> {}", new_username); - // return Err(InvalidUsername) - // } - // - // if self.is_username_taken(&new_username).await? { - // error!("Username is taken for change_username()! new_username -> {}", new_username); - // return Err(UsernameTaken) - // } - // - // let result = query!("UPDATE user SET username = ? WHERE username = ?", new_username, username).execute(&self.db_pool).await; - // match result { - // Ok(_) => Ok(()), - // Err(_) => Err(Error) - // } - // } - async fn change_avatar(self, _: Context, username: String, token: String, new_avatar: String) -> Result<(), ErrorCode> { info!("API Request: change_avatar( username -> {}, token -> {}, new_avatar -> {} )", username, token, new_avatar); if !self.is_authorized(&username, &token).await? { error!("Unauthorized request made for change_avatar()! username -> {}, token -> {}", username, token); - return Err(Unauthorized) + return Err(Unauthorized); } let result = query!("UPDATE user SET avatar = ? WHERE username = ?", new_avatar, username).execute(&self.db_pool).await; @@ -399,7 +373,7 @@ impl RealmAuth for RealmAuthServer { if !self.is_authorized(&username, &token).await? { error!("Unauthorized request made for get_all_data()! username -> {}, token -> {}", username, token); - return Err(Unauthorized) + return Err(Unauthorized); } let result = query!(r"SELECT * FROM user WHERE username = ?", username).fetch_one(&self.db_pool).await; @@ -413,10 +387,6 @@ impl RealmAuth for RealmAuthServer { servers: row.servers, login_code: None, bigtoken: row.tokens, - google_oauth: row.google_oauth, - apple_oauth: row.apple_oauth, - github_oauth: row.github_oauth, - discord_oauth: row.discord_oauth, }) } Err(_) => { @@ -458,11 +428,11 @@ impl RealmAuth for RealmAuthServer { error!("Unauthorized request made for sign_out()! username -> {}, token -> {}", username, token); Err(Unauthorized) - }, + } Err(_) => { error!("Invalid username in request for get_avatar_for_user()! username -> {}", username); Err(InvalidUsername) - }, + } } } @@ -470,7 +440,7 @@ impl RealmAuth for RealmAuthServer { info!("API Request: delete_account_flow( username -> {}, token -> {} )", username, token); if !self.is_authorized(&username, &token).await? { - return Err(Unauthorized) + return Err(Unauthorized); } let email = match query!("SELECT email FROM user WHERE username = ?;", username).fetch_one(&self.db_pool).await { @@ -484,20 +454,23 @@ impl RealmAuth for RealmAuthServer { .execute(&self.db_pool).await; match result { - Ok(_) => self.send_login_message(&username, &email, code).await, + Ok(_) => { + self.send_login_message(&username, &email, code); + Ok(()) + } Err(_) => Err(InvalidUsername) } } - async fn finish_delete_account_flow(self, _: Context, username: String, token: String, login_code: u16) -> Result<(), ErrorCode> { + async fn finish_delete_account_flow(self, _: Context, username: String, token: String, login_code: u32) -> Result<(), ErrorCode> { info!("API Request: finish_delete_account_flow( username -> {}, token -> {}, login_code -> {} )", username, token, login_code); if !self.is_authorized(&username, &token).await? { - return Err(Unauthorized) + return Err(Unauthorized); } if !self.is_login_code_valid(&username, login_code).await? { - return Err(InvalidLoginCode) + return Err(InvalidLoginCode); } self.reset_login_code(&username).await?; @@ -511,7 +484,7 @@ impl RealmAuth for RealmAuthServer { async fn add_server(self, _: Context, username: String, token: String, domain: String) -> Result<(), ErrorCode> { if !self.is_authorized(&username, &token).await? { - return Err(Unauthorized) + return Err(Unauthorized); } let result = query!("SELECT servers FROM user WHERE username = ?", username).fetch_one(&self.db_pool).await; @@ -520,7 +493,7 @@ impl RealmAuth for RealmAuthServer { let mut vec_servers = row.servers.split('|').collect::>(); for server in &vec_servers { if server.eq(&domain) { - return Err(AlreadyJoinedServer) + return Err(AlreadyJoinedServer); } } @@ -539,7 +512,7 @@ impl RealmAuth for RealmAuthServer { async fn remove_server(self, _: Context, username: String, token: String, domain: String) -> Result<(), ErrorCode> { if !self.is_authorized(&username, &token).await? { - return Err(Unauthorized) + return Err(Unauthorized); } let result = query!("SELECT servers FROM user WHERE username = ?", username).fetch_one(&self.db_pool).await; @@ -555,10 +528,10 @@ impl RealmAuth for RealmAuthServer { return match result { Ok(_) => Ok(()), Err(_) => Err(Error) - } + }; } } - + Err(NotInServer) } Err(_) => Err(Error) @@ -567,7 +540,7 @@ impl RealmAuth for RealmAuthServer { async fn get_joined_servers(self, _: Context, username: String, token: String) -> Result, ErrorCode> { if !self.is_authorized(&username, &token).await? { - return Err(Unauthorized) + return Err(Unauthorized); } let result = query!("SELECT servers FROM user WHERE username = ?", username).fetch_one(&self.db_pool).await; @@ -578,7 +551,7 @@ impl RealmAuth for RealmAuthServer { for server in vec_servers { servers.push(server.to_string()) } - + Ok(servers) } Err(_) => Err(Error) diff --git a/auth/src/types.rs b/auth/src/types.rs index ea53da6..0af42fa 100644 --- a/auth/src/types.rs +++ b/auth/src/types.rs @@ -7,17 +7,17 @@ pub trait RealmAuth { async fn server_token_validation(server_token: String, username: String, server_id: String, domain: String, tarpc_port: u16) -> bool; async fn create_account_flow(username: String, email: String) -> Result<(), ErrorCode>; //NOTE: Still require sign in flow async fn create_login_flow(username: Option, email: Option) -> Result<(), ErrorCode>; - async fn finish_login_flow(username: String, login_code: u16) -> Result; + async fn finish_login_flow(username: String, login_code: u32) -> Result; //NOTE: Need to be the user async fn change_email_flow(username: String, new_email: String, token: String) -> Result<(), ErrorCode>; - async fn finish_change_email_flow(username: String, new_email: String, token: String, login_code: u16) -> Result<(), ErrorCode>; + async fn finish_change_email_flow(username: String, new_email: String, token: String, login_code: u32) -> Result<(), ErrorCode>; // async fn change_username(username: String, token: String, new_username: String) -> Result<(), ErrorCode>; async fn change_avatar(username: String, token: String, new_avatar: String) -> Result<(), ErrorCode>; async fn get_all_data(username: String, token: String) -> Result; async fn sign_out(username: String, token: String) -> Result<(), ErrorCode>; async fn delete_account_flow(username: String, token: String) -> Result<(), ErrorCode>; - async fn finish_delete_account_flow(username: String, token: String, login_code: u16) -> Result<(), ErrorCode>; + async fn finish_delete_account_flow(username: String, token: String, login_code: u32) -> Result<(), ErrorCode>; async fn add_server(username: String, token: String, domain: String) -> Result<(), ErrorCode>; async fn remove_server(username: String, token: String, domain: String) -> Result<(), ErrorCode>; async fn get_joined_servers(username: String, token: String) -> Result, ErrorCode>; @@ -34,12 +34,8 @@ pub struct AuthUser { pub email: String, pub avatar: String, pub servers: String, - pub login_code: Option, + pub login_code: Option, pub bigtoken: Option, - pub google_oauth: Option, - pub apple_oauth: Option, - pub github_oauth: Option, - pub discord_oauth: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/client/Cargo.toml b/client/Cargo.toml index 8744f89..70d8bc8 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -14,4 +14,10 @@ eframe = { version = "0.29", default-features = false, features = [ "persistence", # Enable restoring app state when restarting the app. ] } serde = { version = "1", features = ["derive"] } -env_logger = "0.11.5" \ No newline at end of file +env_logger = "0.11.5" +tokio = "1.40.0" +tarpc = { version = "0.34.0", features = ["full"] } +tracing = "0.1.40" +tracing-subscriber = "0.3.18" +regex = "1.10.6" +native-dialog = "0.7.0" \ No newline at end of file diff --git a/client/src/app.rs b/client/src/app.rs index 00e88eb..79281d9 100644 --- a/client/src/app.rs +++ b/client/src/app.rs @@ -1,3 +1,8 @@ +use tokio::sync::broadcast; +use tokio::sync::broadcast::{Receiver, Sender}; +use tracing::log::info; +use realm_shared::types::ErrorCode; +use crate::types::ClientUser; use crate::ui::panels; /// We derive Deserialize/Serialize so we can persist app state on shutdown. @@ -9,9 +14,32 @@ pub struct TemplateApp { pub selected: bool, pub selected_serverid: Option, pub selected_roomid: Option, + + pub current_user: Option, - #[serde(skip)] // This how you opt-out of serialization of a field + #[serde(skip)] pub value: f32, + #[serde(skip)] + pub login_window_open: bool, + #[serde(skip)] + pub login_window_username: String, + #[serde(skip)] + pub login_window_code: String, + #[serde(skip)] + pub login_window_server_address: String, + #[serde(skip)] + pub login_window_email: String, + + #[serde(skip)] + pub login_ready_for_code_input: bool, + + #[serde(skip)] + pub signup_window_open: bool, + + #[serde(skip)] + pub login_start_channel: (Sender>, Receiver>), + #[serde(skip)] + pub login_ending_channel: (Sender>, Receiver>), } impl Default for TemplateApp { @@ -22,7 +50,19 @@ impl Default for TemplateApp { selected: false, selected_serverid: None, selected_roomid: None, + current_user: None, value: 2.7, + + login_window_open: false, + login_window_username: String::new(), + login_window_code: String::new(), + login_window_server_address: String::new(), + login_start_channel: broadcast::channel(10), + login_ending_channel: broadcast::channel(10), + login_ready_for_code_input: false, + login_window_email: String::new(), + + signup_window_open: false, } } } @@ -49,8 +89,26 @@ impl eframe::App for TemplateApp { // Put your widgets into a `SidePanel`, `TopBottomPanel`, `CentralPanel`, `Window` or `Area`. // For inspiration and more examples, go to https://emilk.github.io/egui + while let Ok(result) = self.login_start_channel.1.try_recv() { + match result { + Ok(_) => self.login_ready_for_code_input = true, + Err(e) => tracing::error!("Error in login/account creation flow: {:?}", e), + } + } + + while let Ok(result) = self.login_ending_channel.1.try_recv() { + match result { + Ok(token) => { + info!("Login successful! Token: {token}"); + self.login_ready_for_code_input = false; + + }, + Err(e) => tracing::error!("Error in login flow: {:?}", e), + } + } + // File -> Quit - panels::top_panel(ctx); + panels::top_panel(self, ctx); panels::servers(self, ctx); diff --git a/client/src/main.rs b/client/src/main.rs index 464d2a9..28c3757 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -1,4 +1,5 @@ -fn main() -> eframe::Result { +#[tokio::main] +async fn main() -> eframe::Result { env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). let native_options = eframe::NativeOptions { diff --git a/client/src/types.rs b/client/src/types.rs index e69de29..f71a9c1 100644 --- a/client/src/types.rs +++ b/client/src/types.rs @@ -0,0 +1,9 @@ +#[derive(serde::Deserialize, serde::Serialize)] +pub struct ClientUser { + pub id: i64, + pub username: String, + pub email: String, + pub avatar: String, + pub servers: Vec, + pub token: String, +} \ No newline at end of file diff --git a/client/src/ui/panels.rs b/client/src/ui/panels.rs index d0af77b..af9d346 100644 --- a/client/src/ui/panels.rs +++ b/client/src/ui/panels.rs @@ -1,25 +1,180 @@ use egui::{Context, SelectableLabel}; +use tarpc::context; +use tarpc::tokio_serde::formats::Json; +use realm_auth::types::RealmAuthClient; +use realm_shared::types::ErrorCode::RPCError; +use regex::Regex; use crate::app::TemplateApp; -pub fn top_panel(ctx: &Context) { +pub fn top_panel(app: &mut TemplateApp, ctx: &Context) { egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { - // The top panel is often a good place for a menu bar: - egui::menu::bar(ui, |ui| { - // NOTE: no File->Quit on web pages! - let is_web = cfg!(target_arch = "wasm32"); - if !is_web { - ui.menu_button("File", |ui| { - if ui.button("Quit").clicked() { - ctx.send_viewport_cmd(egui::ViewportCommand::Close); - } - }); - ui.add_space(16.0); - } + ui.menu_button("File", |ui| { + if ui.button("Sign Up").clicked() { + app.signup_window_open = true; + } + + if ui.button("Login").clicked() { + app.login_window_open = true; + } + + if ui.button("Logout").clicked() { + // TODO: Logout + } + + if ui.button("Quit").clicked() { + ctx.send_viewport_cmd(egui::ViewportCommand::Close); + } + }); + ui.add_space(16.0); egui::widgets::global_dark_light_mode_buttons(ui); }); }); + + egui::Window::new("Signup") + .open(&mut app.signup_window_open) + .min_size((500.0, 200.0)) + .show(ctx, |ui| { + ui.horizontal(|ui| { + ui.label("Server Address: "); + ui.text_edit_singleline(&mut app.login_window_server_address); + }); + + ui.horizontal(|ui| { + ui.label("Username: "); + ui.text_edit_singleline(&mut app.login_window_username); + }); + + ui.horizontal(|ui| { + ui.label("Email: "); + ui.text_edit_singleline(&mut app.login_window_email); + }); + + if ui.button("Create Account").clicked() { + let login_window_server_address = app.login_window_server_address.clone(); + let login_window_username = app.login_window_username.clone(); + let login_window_email = app.login_window_email.clone(); + let send_channel = app.login_start_channel.0.clone(); + + let _handle = tokio::spawn(async move { + let mut transport = tarpc::serde_transport::tcp::connect(login_window_server_address, Json::default); + transport.config_mut().max_frame_length(usize::MAX); + + let result = transport.await; + let connection = match result { + Ok(connection) => connection, + Err(e) => { + tracing::error!("Failed to connect to server: {}", e); + return; + } + }; + + let client = RealmAuthClient::new(tarpc::client::Config::default(), connection).spawn(); + let result = client.create_account_flow(context::current(), login_window_username, login_window_email).await; + + match result { + Ok(r) => send_channel.send(r).unwrap(), + Err(_) => send_channel.send(Err(RPCError)).unwrap(), + }; + }); + + ui.close_menu() + } + }); + + egui::Window::new("Login") + .open(&mut app.login_window_open) + .min_size((500.0, 200.0)) + .show(ctx, |ui| { + ui.horizontal(|ui| { + ui.label("Server Address: "); + ui.text_edit_singleline(&mut app.login_window_server_address); + }); + + ui.horizontal(|ui| { + ui.label("Username: "); + ui.text_edit_singleline(&mut app.login_window_username); + }); + + if ui.button("Send Login Code").clicked() { + let login_window_server_address = app.login_window_server_address.clone(); + let login_window_username = app.login_window_username.clone(); + let send_channel = app.login_start_channel.0.clone(); + + let _handle = tokio::spawn(async move { + let mut transport = tarpc::serde_transport::tcp::connect(login_window_server_address, Json::default); + transport.config_mut().max_frame_length(usize::MAX); + + let result = transport.await; + let connection = match result { + Ok(connection) => connection, + Err(e) => { + tracing::error!("Failed to connect to server: {}", e); + return; + } + }; + + let client = RealmAuthClient::new(tarpc::client::Config::default(), connection).spawn(); + let result = client.create_login_flow(context::current(), Some(login_window_username), None).await; + + match result { + Ok(r) => send_channel.send(r).unwrap(), + Err(_) => send_channel.send(Err(RPCError)).unwrap(), + }; + }); + + ui.close_menu() + } + }); + + egui::Window::new("Auth Code") + .open(&mut app.login_ready_for_code_input) + .min_size((500.0, 200.0)) + .show(ctx, |ui| { + ui.horizontal(|ui| { + ui.label("Code: "); + if ui.text_edit_singleline(&mut app.login_window_code).changed() { + let re = Regex::new(r"[^0-9]+").unwrap(); + app.login_window_code = re.replace_all(&app.login_window_code, "").to_string(); + } + }); + + if ui.button("Login").clicked() { + let login_window_server_address = app.login_window_server_address.clone(); + let login_window_code = app.login_window_code.clone(); + let login_window_username = app.login_window_username.clone(); + let send_channel = app.login_ending_channel.0.clone(); + + let _handle = tokio::spawn(async move { + let mut transport = tarpc::serde_transport::tcp::connect(login_window_server_address, Json::default); + transport.config_mut().max_frame_length(usize::MAX); + + let result = transport.await; + let connection = match result { + Ok(connection) => connection, + Err(e) => { + tracing::error!("Failed to connect to server: {}", e); + return; + } + }; + + let client = RealmAuthClient::new(tarpc::client::Config::default(), connection).spawn(); + let result = client.finish_login_flow(context::current(), login_window_username, login_window_code.parse::().unwrap()).await; + + match result { + Ok(r) => { + send_channel.send(r).unwrap(); + } + Err(e) => { + send_channel.send(Err(RPCError)).unwrap(); + } + } + }); + + ui.close_menu() + } + }); } pub fn servers(app: &mut TemplateApp, ctx: &Context) { diff --git a/login_email.txt b/login_email.txt deleted file mode 100644 index f24a21a..0000000 --- a/login_email.txt +++ /dev/null @@ -1,10 +0,0 @@ -Confirm your email address -Your 6 digit code is below-- enter it into Realm and you will be signed in - -{} - -If you didn't request this email, there's nothing to worry about it-- you can safely ignore it. - - Realm - Need help? Email realm-support@abunchofknowtiwalls.com - © 2024 Realm, Inc. diff --git a/server/.env b/server/.env deleted file mode 100644 index 425d538..0000000 --- a/server/.env +++ /dev/null @@ -1,4 +0,0 @@ -DATABASE_URL=sqlite:server.db -DOMAIN=server.realmchat.com -SERVER_ID=tester -PORT=5051 \ No newline at end of file diff --git a/server/example.env b/server/example.env new file mode 100644 index 0000000..6cb6ed1 --- /dev/null +++ b/server/example.env @@ -0,0 +1,4 @@ +DATABASE_URL=sqlite:server.db +DOMAIN= +SERVER_ID= +PORT=5051 \ No newline at end of file diff --git a/server/src/server.rs b/server/src/server.rs index 7412318..a53aae9 100644 --- a/server/src/server.rs +++ b/server/src/server.rs @@ -127,7 +127,7 @@ impl RealmChatServer { } async fn is_user_in_server(&self, userid: &str) -> bool { - let result = query!("SELECT NOT EXISTS (SELECT 1 FROM user WHERE userid = ?) AS does_exist", userid).fetch_one(&self.db_pool).await; + let result = query!("SELECT EXISTS (SELECT 1 FROM user WHERE userid = ?) AS does_exist", userid).fetch_one(&self.db_pool).await; match result { Ok(record) => record.does_exist != 0, diff --git a/shared/src/types.rs b/shared/src/types.rs index 26c2fc9..d6c2d89 100644 --- a/shared/src/types.rs +++ b/shared/src/types.rs @@ -21,4 +21,6 @@ pub enum ErrorCode { UserNotFound, DepthTooLarge, MalformedDBResponse, + + RPCError, } \ No newline at end of file