diff --git a/auth/Cargo.toml b/auth/Cargo.toml index d9c44a6..502997d 100644 --- a/auth/Cargo.toml +++ b/auth/Cargo.toml @@ -16,3 +16,4 @@ sqlx = { version = "0.7", features = [ "runtime-tokio", "tls-rustls", "mysql", " sha3 = "0.10.8" hex = "0.4.3" rand = "0.8.5" +mail-send = "0.4.8" diff --git a/auth/src/main.rs b/auth/src/main.rs index 2535e2e..5f26542 100644 --- a/auth/src/main.rs +++ b/auth/src/main.rs @@ -8,7 +8,7 @@ use tarpc::server::{BaseChannel, Channel}; use tarpc::server::incoming::Incoming; use tarpc::tokio_serde::formats::Json; use realm_auth::server::RealmAuthServer; -use realm_auth::types::RealmAuth; +use realm_auth::types::{AuthEmail, RealmAuth}; async fn spawn(fut: impl Future + Send + 'static) { tokio::spawn(fut); @@ -18,20 +18,28 @@ async fn spawn(fut: impl Future + Send + 'static) { async fn main() -> anyhow::Result<()> { dotenv().ok(); + let auth_email = AuthEmail { + server_address: env::var("SERVER_MAIL_ADDRESS").expect("SERVER_MAIL_ADDRESS must be set"), + server_port: env::var("SERVER_MAIL_PORT").expect("SERVER_MAIL_PORT must be set").parse::().expect("SERVER_MAIL_ADDRESS must be a number"), + auth_name: env::var("SERVER_MAIL_NAME").expect("SERVER_MAIL_NAME must be set"), + auth_from_address: env::var("SERVER_MAIL_FROM_ADDRESS").expect("SERVER_MAIL_FROM_ADDRESS must be set"), + auth_username: env::var("SERVER_MAIL_USERNAME").expect("SERVER_MAIL_USERNAME must be set"), + auth_password: env::var("SERVER_MAIL_PASSWORD").expect("SERVER_MAIL_PASSWORD must be set"), + }; + let db_pool = MySqlPoolOptions::new() .max_connections(64) .connect(env::var("DATABASE_URL").expect("DATABASE_URL must be set").as_str()).await?; - sqlx::query( - "CREATE DATABASE IF NOT EXISTS realmauth; USE realmauth;" - ).fetch_one(&db_pool).await?; + //TODO: In a docker container or figure out somewhere to do this command + //sqlx::query("CREATE DATABASE IF NOT EXISTS realmauth").execute(&db_pool).await?; sqlx::query( "CREATE TABLE IF NOT EXISTS user ( id SERIAL, username VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, - avatar TEXT NOT NULL + avatar TEXT NOT NULL, login_code INT(6), tokens TEXT, google_oauth VARCHAR(255), @@ -57,7 +65,7 @@ async fn main() -> anyhow::Result<()> { // serve is generated by the service attribute. It takes as input any type implementing // the generated World trait. .map(|channel| { - let server = RealmAuthServer::new(channel.transport().peer_addr().unwrap(), db_pool); + let server = RealmAuthServer::new(channel.transport().peer_addr().unwrap(), db_pool.clone(), auth_email.clone()); channel.execute(server.serve()).for_each(spawn) }) // Max 10 channels. diff --git a/auth/src/server.rs b/auth/src/server.rs index d26f683..be05df0 100644 --- a/auth/src/server.rs +++ b/auth/src/server.rs @@ -1,27 +1,34 @@ use std::net::SocketAddr; - +use chrono::Utc; +use mail_send::{Credentials, SmtpClientBuilder}; +use mail_send::mail_builder::MessageBuilder; use rand::Rng; use sha3::{Digest, Sha3_256}; use sha3::digest::Update; use sqlx::{MySql, Pool, Row}; +use sqlx::mysql::{MySqlQueryResult, MySqlRow}; use tarpc::context::Context; -use crate::types::{AuthUser, ErrorCode, RealmAuth}; +use crate::types::{AuthEmail, AuthUser, ErrorCode, RealmAuth}; use crate::types::ErrorCode::*; #[derive(Clone)] pub struct RealmAuthServer { pub socket: SocketAddr, pub db_pool: Pool, + pub auth_email: AuthEmail, + pub template_html: String, + pub template_txt: String, } -//TODO: USERNAME FORMATTING! - impl RealmAuthServer { - pub fn new(socket: SocketAddr, db_pool: Pool) -> RealmAuthServer { + pub fn new(socket: SocketAddr, db_pool: Pool, auth_email: AuthEmail) -> RealmAuthServer { RealmAuthServer { socket, 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"), } } @@ -81,6 +88,40 @@ impl RealmAuthServer { Err(_) => Err(InvalidUsername), } } + + pub async fn send_login_message(&self, username: &str, email: &str, login_code: u16) -> 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())); + + 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; + + match result { + Ok(mut client) => { + let result = client.send(message).await; + match result { + Ok(_) => { + NoError + } + Err(_) => { + UnableToSendMail + } + } + } + Err(_) => { + UnableToConnectToMail + } + } + } } impl RealmAuth for RealmAuthServer { @@ -110,29 +151,124 @@ impl RealmAuth for RealmAuthServer { } async fn create_account_flow(self, _: Context, username: String, email: String) -> ErrorCode { - todo!() + //TODO: USERNAME FORMATTING! + + + + let result = self.is_username_taken(&username).await; + match result { + Ok(taken) => { + if taken { + return UsernameTaken + } + } + Err(error) => return error + } + + let result = self.is_email_taken(&email).await; + match result { + Ok(taken) => { + if taken { + return EmailTaken + } + } + Err(error) => return error + } + + let code = self.gen_login_code(); + let result = self.send_login_message(&username, &email, code).await; + + if result != NoError { + return result; + } + + let result = sqlx::query("INSERT INTO user (username, email, avatar, login_code, tokens) VALUES (?, ?, '', ?, '')") + .bind(&username).bind(&email).bind(code).execute(&self.db_pool).await; + + match result { + Ok(_) => NoError, + Err(_) => Error + } } - async fn finish_account_flow(self, _: Context, username: String, login_code: u16, avatar: String) -> Result { - todo!() - } + async fn create_login_flow(self, _: Context, mut username: Option, mut email: Option) -> ErrorCode { + if username.is_none() && email.is_none() { + return Error + } + + if username.is_none() { + let result = sqlx::query("SELECT username FROM user WHERE email = ?;") + .bind(&email.clone().unwrap()) + .fetch_one(&self.db_pool).await; + + match result { + Ok(row) => { + username = row.try_get("username").unwrap(); + } + Err(_) => return InvalidEmail + } + } - async fn create_login_flow(self, _: Context, username: String) -> ErrorCode { + if email.is_none() { + let result = sqlx::query("SELECT email FROM user WHERE username = ?;") + .bind(&username.clone().unwrap()) + .fetch_one(&self.db_pool).await; + + match result { + Ok(row) => { + email = row.try_get("email").unwrap(); + } + Err(_) => return InvalidUsername + } + } + + let code = self.gen_login_code(); + let result = sqlx::query("UPDATE user SET login_code = ? WHERE username = ?;") - .bind(self.gen_login_code()) - .bind(username) + .bind(code) + .bind(&username) .execute(&self.db_pool).await; match result { - Ok(_) => { - todo!() //TODO: Emails! - }, + Ok(_) => self.send_login_message(&username.unwrap(), &email.unwrap(), code).await, Err(_) => InvalidUsername } } async fn finish_login_flow(self, _: Context, username: String, login_code: u16) -> Result { - todo!() + let result = sqlx::query("SELECT login_code FROM user WHERE username = ?;") + .bind(&username) + .fetch_one(&self.db_pool).await; + + match result { + Ok(row) => { + if row.try_get::("login_code").unwrap() != login_code { + return Err(InvalidLoginCode) + } + } + Err(_) => return Err(InvalidUsername) + } + + let _ = sqlx::query("UPDATE user SET login_code = NULL WHERE username = ?").bind(&username).execute(&self.db_pool).await; + + let hash = Sha3_256::new().chain(format!("{}{}{}", username, login_code, Utc::now().to_utc())).finalize(); + let token = hex::encode(hash); + + let result = sqlx::query("SELECT tokens FROM user WHERE username = ?").bind(&username).fetch_one(&self.db_pool).await; + match result { + Ok(row) => { + let token_long: &str = row.try_get("tokens").unwrap(); + let mut tokens = token_long.split(',').collect::>(); + tokens.push(&token); + + let result = sqlx::query("UPDATE user SET tokens = ? WHERE username = ?").bind(tokens.join(",")).bind(&username).execute(&self.db_pool).await; + match result { + Ok(_) => Ok(token), + Err(_) => Err(InvalidUsername) + } + } + Err(_) => Err(InvalidUsername) + } } async fn change_email_flow(self, _: Context, username: String, new_email: String, token: String) -> ErrorCode { @@ -144,6 +280,9 @@ impl RealmAuth for RealmAuthServer { } async fn change_username(self, _: Context, username: String, token: String, new_username: String) -> ErrorCode { + //TODO: USERNAME FORMATTING! + + let result = self.is_authorized(&username, &token).await; match result { Ok(authorized) => { diff --git a/auth/src/types.rs b/auth/src/types.rs index 9c39f6c..c107d42 100644 --- a/auth/src/types.rs +++ b/auth/src/types.rs @@ -5,8 +5,7 @@ pub trait RealmAuth { async fn test(name: String) -> String; 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) -> ErrorCode; //NOTE: Still require sign in flow - async fn finish_account_flow(username: String, login_code: u16, avatar: String) -> Result; - async fn create_login_flow(username: String) -> ErrorCode; + async fn create_login_flow(username: Option, email: Option) -> ErrorCode; async fn finish_login_flow(username: String, login_code: u16) -> Result; //NOTE: Need to be the user @@ -23,16 +22,10 @@ pub trait RealmAuth { // Create account // Change email // Change username - // Change/Upload/Delete avatar - // OAuth login, check against email, store token, take avatar - // Google, Apple, GitHub, Discord - // Get avatar - // Get all userdata if you are the user - // Server token validation - // Logout + // OAuth login, check against email, store token, take avatar: Google, Apple, GitHub, Discord } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum ErrorCode { NoError, Error, @@ -42,7 +35,10 @@ pub enum ErrorCode { InvalidLoginCode, InvalidImage, InvalidUsername, + InvalidEmail, InvalidToken, + UnableToConnectToMail, + UnableToSendMail, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -57,4 +53,14 @@ pub struct AuthUser { pub apple_oauth: Option, pub github_oauth: Option, pub discord_oauth: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AuthEmail { + pub server_address: String, + pub server_port: u16, + pub auth_name: String, + pub auth_from_address: String, + pub auth_username: String, + pub auth_password: String, } \ No newline at end of file diff --git a/login_email.html b/login_email.html new file mode 100644 index 0000000..d533898 --- /dev/null +++ b/login_email.html @@ -0,0 +1,12 @@ +

Realm

+ +

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. +

\ No newline at end of file diff --git a/login_email.txt b/login_email.txt new file mode 100644 index 0000000..f24a21a --- /dev/null +++ b/login_email.txt @@ -0,0 +1,10 @@ +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.