mod client; mod util; use std::sync::Arc; use client::{sqldb::SqliteClient, Store, StoreError}; use derive_more::Display; use email_address::EmailAddress; use log::{error, info}; use serde::Serialize; use time::OffsetDateTime; use uuid::Uuid; #[derive(Copy, Display, Clone, Debug)] pub enum OauthProvider { Amazon, Apple, Dropbox, Facebook, Github, Gitlab, Google, Instagram, LinkedIn, Microsoft, Paypal, Reddit, Spotify, Strava, Stripe, Twitch, Twitter, WeChat, } #[derive(Display, Debug)] pub enum AuthStore { Sqlite, Postgres, MySql, Mongo, Dynamo, Redis, } pub type OauthClientId = String; pub type OauthClientSecretEncrypted = String; pub type OauthConsentUri = String; pub type IdentityId = Uuid; ////////////////////////////////////////////////// // Resources #[derive(sqlx::FromRow, Debug, Serialize)] pub struct Identity { #[sqlx(rename = "identity_public_id")] id: Uuid, created_at: sqlx::types::time::OffsetDateTime, #[serde(skip_serializing_if = "Option::is_none")] data: Option, } pub struct ApiKey {} pub struct Session {} pub struct ValidationRequest {} ////////////////////////////////////////////////// #[derive(Debug, derive_more::Display, thiserror::Error)] pub enum SecdError { InvalidEmailAddress, SqliteInitializationFailure(sqlx::Error), StoreError(#[from] StoreError), EmailValidationRequestError, Unknown, } pub struct Secd { store: Arc, } impl Secd { pub async fn init( auth_store: AuthStore, conn_string: Option, // TODO: Turn Secd into a trait and impl separately. // TODO: initialize email and SMS templates with secd ) -> Result { let store = match auth_store { AuthStore::Sqlite => SqliteClient::new( sqlx::sqlite::SqlitePoolOptions::new() .connect(conn_string.unwrap_or("sqlite::memory:".into()).as_str()) .await .map_err(|e| SecdError::SqliteInitializationFailure(e))?, ), // TODO: if AuthStore is provided, then configure the client. _ => return Err(SecdError::Unknown), } .await; Ok(Secd { store: Arc::new(store), }) } pub async fn create_identity(&self) -> Result { let id = Uuid::new_v4(); self.store .write_identity(&Identity { id, created_at: OffsetDateTime::now_utc(), data: None, }) .await?; Ok(id) } pub async fn create_validation_request(&self, email: Option<&str>) -> Result<(), SecdError> { // TODO: refactor based on email, phone, or some other template? Or break up the API? let email = match email { Some(ea) => { if EmailAddress::is_valid(ea) { ea } else { return Err(SecdError::InvalidEmailAddress); } } None => return Err(SecdError::InvalidEmailAddress), }; match self.store.find_identity(None, Some(email)).await? { Some(identity) => { error!("TODO: implement email send with LOGIN template"); error!("TODO: send to: {}", email); let req_id = self .store .write_email_validation_request(identity.id, email) .await?; // TODO: provide some dummy email handlers that are used when testing locally... error!("TODO: when the request comes back, it needs to hit something like /iam/identity/1234/email-validation/1234?code=2345"); error!("TODO: consequently, we may want to shorten the url by providing a quick access code and/or /iam/email-validation/1234/validate"); } None => { let identity = Identity { id: Uuid::new_v4(), created_at: OffsetDateTime::now_utc(), data: None, }; self.store.write_identity(&identity).await?; self.store.write_email(identity.id, email).await?; error!("TODO: implement email send with SIGN_UP template"); self.store .write_email_validation_request(identity.id, email) .await?; } } error!("TODO: think about returning the identity id for which this validation request was created"); Ok(()) } pub async fn get_identity(&self, id: IdentityId) -> Result { Ok(self.store.read_identity(&id).await?) } pub async fn create_email_validation(email: String) -> Result<(), SecdError> { Ok(()) } }