Almost done with auth
TOOD: Shared library for ErrorCode (30m), Changing emails (1hr), Username formatting (30m), OAuth support (3hrs), Testing (infinite)
This commit is contained in:
@@ -16,3 +16,4 @@ sqlx = { version = "0.7", features = [ "runtime-tokio", "tls-rustls", "mysql", "
|
|||||||
sha3 = "0.10.8"
|
sha3 = "0.10.8"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
|
mail-send = "0.4.8"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use tarpc::server::{BaseChannel, Channel};
|
|||||||
use tarpc::server::incoming::Incoming;
|
use tarpc::server::incoming::Incoming;
|
||||||
use tarpc::tokio_serde::formats::Json;
|
use tarpc::tokio_serde::formats::Json;
|
||||||
use realm_auth::server::RealmAuthServer;
|
use realm_auth::server::RealmAuthServer;
|
||||||
use realm_auth::types::RealmAuth;
|
use realm_auth::types::{AuthEmail, RealmAuth};
|
||||||
|
|
||||||
async fn spawn(fut: impl Future<Output = ()> + Send + 'static) {
|
async fn spawn(fut: impl Future<Output = ()> + Send + 'static) {
|
||||||
tokio::spawn(fut);
|
tokio::spawn(fut);
|
||||||
@@ -18,20 +18,28 @@ async fn spawn(fut: impl Future<Output = ()> + Send + 'static) {
|
|||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
dotenv().ok();
|
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::<u16>().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()
|
let db_pool = MySqlPoolOptions::new()
|
||||||
.max_connections(64)
|
.max_connections(64)
|
||||||
.connect(env::var("DATABASE_URL").expect("DATABASE_URL must be set").as_str()).await?;
|
.connect(env::var("DATABASE_URL").expect("DATABASE_URL must be set").as_str()).await?;
|
||||||
|
|
||||||
sqlx::query(
|
//TODO: In a docker container or figure out somewhere to do this command
|
||||||
"CREATE DATABASE IF NOT EXISTS realmauth; USE realmauth;"
|
//sqlx::query("CREATE DATABASE IF NOT EXISTS realmauth").execute(&db_pool).await?;
|
||||||
).fetch_one(&db_pool).await?;
|
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"CREATE TABLE IF NOT EXISTS user (
|
"CREATE TABLE IF NOT EXISTS user (
|
||||||
id SERIAL,
|
id SERIAL,
|
||||||
username VARCHAR(255) NOT NULL,
|
username VARCHAR(255) NOT NULL,
|
||||||
email VARCHAR(255) NOT NULL,
|
email VARCHAR(255) NOT NULL,
|
||||||
avatar TEXT NOT NULL
|
avatar TEXT NOT NULL,
|
||||||
login_code INT(6),
|
login_code INT(6),
|
||||||
tokens TEXT,
|
tokens TEXT,
|
||||||
google_oauth VARCHAR(255),
|
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
|
// serve is generated by the service attribute. It takes as input any type implementing
|
||||||
// the generated World trait.
|
// the generated World trait.
|
||||||
.map(|channel| {
|
.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)
|
channel.execute(server.serve()).for_each(spawn)
|
||||||
})
|
})
|
||||||
// Max 10 channels.
|
// Max 10 channels.
|
||||||
|
|||||||
@@ -1,27 +1,34 @@
|
|||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
use chrono::Utc;
|
||||||
|
use mail_send::{Credentials, SmtpClientBuilder};
|
||||||
|
use mail_send::mail_builder::MessageBuilder;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use sha3::{Digest, Sha3_256};
|
use sha3::{Digest, Sha3_256};
|
||||||
use sha3::digest::Update;
|
use sha3::digest::Update;
|
||||||
use sqlx::{MySql, Pool, Row};
|
use sqlx::{MySql, Pool, Row};
|
||||||
|
use sqlx::mysql::{MySqlQueryResult, MySqlRow};
|
||||||
use tarpc::context::Context;
|
use tarpc::context::Context;
|
||||||
|
|
||||||
use crate::types::{AuthUser, ErrorCode, RealmAuth};
|
use crate::types::{AuthEmail, AuthUser, ErrorCode, RealmAuth};
|
||||||
use crate::types::ErrorCode::*;
|
use crate::types::ErrorCode::*;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct RealmAuthServer {
|
pub struct RealmAuthServer {
|
||||||
pub socket: SocketAddr,
|
pub socket: SocketAddr,
|
||||||
pub db_pool: Pool<MySql>,
|
pub db_pool: Pool<MySql>,
|
||||||
|
pub auth_email: AuthEmail,
|
||||||
|
pub template_html: String,
|
||||||
|
pub template_txt: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: USERNAME FORMATTING!
|
|
||||||
|
|
||||||
impl RealmAuthServer {
|
impl RealmAuthServer {
|
||||||
pub fn new(socket: SocketAddr, db_pool: Pool<MySql>) -> RealmAuthServer {
|
pub fn new(socket: SocketAddr, db_pool: Pool<MySql>, auth_email: AuthEmail) -> RealmAuthServer {
|
||||||
RealmAuthServer {
|
RealmAuthServer {
|
||||||
socket,
|
socket,
|
||||||
db_pool,
|
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),
|
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 {
|
impl RealmAuth for RealmAuthServer {
|
||||||
@@ -110,29 +151,124 @@ impl RealmAuth for RealmAuthServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn create_account_flow(self, _: Context, username: String, email: String) -> ErrorCode {
|
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<String, ErrorCode> {
|
async fn create_login_flow(self, _: Context, mut username: Option<String>, mut email: Option<String>) -> ErrorCode {
|
||||||
todo!()
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
async fn create_login_flow(self, _: Context, username: String) -> ErrorCode {
|
|
||||||
let result = sqlx::query("UPDATE user SET login_code = ? WHERE username = ?;")
|
let result = sqlx::query("UPDATE user SET login_code = ? WHERE username = ?;")
|
||||||
.bind(self.gen_login_code())
|
.bind(code)
|
||||||
.bind(username)
|
.bind(&username)
|
||||||
.execute(&self.db_pool).await;
|
.execute(&self.db_pool).await;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {
|
Ok(_) => self.send_login_message(&username.unwrap(), &email.unwrap(), code).await,
|
||||||
todo!() //TODO: Emails!
|
|
||||||
},
|
|
||||||
Err(_) => InvalidUsername
|
Err(_) => InvalidUsername
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn finish_login_flow(self, _: Context, username: String, login_code: u16) -> Result<String, ErrorCode> {
|
async fn finish_login_flow(self, _: Context, username: String, login_code: u16) -> Result<String, ErrorCode> {
|
||||||
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::<u16, _>("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::<Vec<&str>>();
|
||||||
|
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 {
|
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 {
|
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;
|
let result = self.is_authorized(&username, &token).await;
|
||||||
match result {
|
match result {
|
||||||
Ok(authorized) => {
|
Ok(authorized) => {
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ pub trait RealmAuth {
|
|||||||
async fn test(name: String) -> String;
|
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 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 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<String, ErrorCode>;
|
async fn create_login_flow(username: Option<String>, email: Option<String>) -> ErrorCode;
|
||||||
async fn create_login_flow(username: String) -> ErrorCode;
|
|
||||||
async fn finish_login_flow(username: String, login_code: u16) -> Result<String, ErrorCode>;
|
async fn finish_login_flow(username: String, login_code: u16) -> Result<String, ErrorCode>;
|
||||||
|
|
||||||
//NOTE: Need to be the user
|
//NOTE: Need to be the user
|
||||||
@@ -23,16 +22,10 @@ pub trait RealmAuth {
|
|||||||
// Create account
|
// Create account
|
||||||
// Change email
|
// Change email
|
||||||
// Change username
|
// Change username
|
||||||
// Change/Upload/Delete avatar
|
// OAuth login, check against email, store token, take avatar: Google, Apple, GitHub, Discord
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum ErrorCode {
|
pub enum ErrorCode {
|
||||||
NoError,
|
NoError,
|
||||||
Error,
|
Error,
|
||||||
@@ -42,7 +35,10 @@ pub enum ErrorCode {
|
|||||||
InvalidLoginCode,
|
InvalidLoginCode,
|
||||||
InvalidImage,
|
InvalidImage,
|
||||||
InvalidUsername,
|
InvalidUsername,
|
||||||
|
InvalidEmail,
|
||||||
InvalidToken,
|
InvalidToken,
|
||||||
|
UnableToConnectToMail,
|
||||||
|
UnableToSendMail,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -58,3 +54,13 @@ pub struct AuthUser {
|
|||||||
pub github_oauth: Option<String>,
|
pub github_oauth: Option<String>,
|
||||||
pub discord_oauth: Option<String>,
|
pub discord_oauth: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
}
|
||||||
12
login_email.html
Normal file
12
login_email.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<h1><img src="https://beeper.github.io/static/brand/email-header.20240320.2056.png" alt="Realm" with="200" height="50"></h1>
|
||||||
|
|
||||||
|
<h2>Confirm your email address</h2>
|
||||||
|
<p>Your 6 digit code is below – enter it into Realm and you will be signed in</p>
|
||||||
|
<h3 style="font-size: 2em; margin: 2em 1em">{}</h3>
|
||||||
|
<p>If you didn't request this email, there's nothing to worry about it– you can safely ignore it.</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://realm.abunchofknowtiwalls.com"><strong>Realm</strong></a><br>
|
||||||
|
Need help? Email <a href="mailto:realm-support@abunchofknowtiwalls.com">realm-support@abunchofknowtiwalls.com</a><br>
|
||||||
|
© 2024 Realm, Inc.
|
||||||
|
</p>
|
||||||
10
login_email.txt
Normal file
10
login_email.txt
Normal file
@@ -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.
|
||||||
Reference in New Issue
Block a user