use clap::Parser;
use etwin_auth_store::mem::MemAuthStore;
use etwin_auth_store::pg::PgAuthStore;
use etwin_config::{AuthConfig, Config, DbConfig};
use etwin_core::auth::AuthStore;
use etwin_core::clock::{Clock, SystemClock, VirtualClock};
use etwin_core::core::{Instant, LocaleId, Secret};
use etwin_core::dinoparc::{DinoparcClient, DinoparcStore};
use etwin_core::email::{EmailFormatter, Mailer};
use etwin_core::forum::{ForumSectionDisplayName, ForumSectionKey, ForumStore, UpsertSystemSectionOptions};
use etwin_core::hammerfest::{HammerfestClient, HammerfestStore};
use etwin_core::link::LinkStore;
use etwin_core::oauth::{OauthClientDisplayName, OauthClientKey, OauthProviderStore, UpsertSystemClientOptions};
use etwin_core::password::{Password, PasswordService};
use etwin_core::token::TokenStore;
use etwin_core::twinoid::{TwinoidClient, TwinoidStore};
use etwin_core::types::AnyError;
use etwin_core::user::UserStore;
use etwin_core::uuid::{Uuid4Generator, UuidGenerator};
use etwin_dinoparc_client::http::HttpDinoparcClient;
use etwin_dinoparc_client::mem::MemDinoparcClient;
use etwin_dinoparc_store::mem::MemDinoparcStore;
use etwin_dinoparc_store::pg::PgDinoparcStore;
use etwin_email_formatter::json::JsonEmailFormatter;
use etwin_forum_store::mem::MemForumStore;
use etwin_forum_store::pg::PgForumStore;
use etwin_hammerfest_client::{HttpHammerfestClient, MemHammerfestClient};
use etwin_hammerfest_store::mem::MemHammerfestStore;
use etwin_hammerfest_store::pg::PgHammerfestStore;
use etwin_link_store::mem::MemLinkStore;
use etwin_link_store::pg::PgLinkStore;
use etwin_log::NoopLogger;
use etwin_mailer::mem::MemMailer;
use etwin_oauth_client::http::{HttpRfcOauthClient, HttpRfcOauthClientOptions};
use etwin_oauth_client::mem::MemRfcOauthClient;
use etwin_oauth_client::RfcOauthClient;
use etwin_oauth_provider_store::mem::MemOauthProviderStore;
use etwin_oauth_provider_store::pg::PgOauthProviderStore;
use etwin_password::scrypt::ScryptPasswordService;
use etwin_rest::{DevApi, RouterApi};
use etwin_services::auth::AuthService;
use etwin_services::dinoparc::DinoparcService;
use etwin_services::forum::ForumService;
use etwin_services::hammerfest::HammerfestService;
use etwin_services::oauth::OauthService;
use etwin_services::twinoid::TwinoidService;
use etwin_services::user::UserService;
use etwin_token_store::mem::MemTokenStore;
use etwin_token_store::pg::PgTokenStore;
use etwin_twinoid_client::http::HttpTwinoidClient;
use etwin_twinoid_client::mem::MemTwinoidClient;
use etwin_twinoid_store::mem::MemTwinoidStore;
use etwin_twinoid_store::pg::PgTwinoidStore;
use etwin_user_store::mem::MemUserStore;
use etwin_user_store::pg::PgUserStore;
use sqlx::postgres::{PgConnectOptions, PgPoolOptions};
use sqlx::PgPool;
use std::net::SocketAddr;
use std::str::FromStr;
use std::sync::Arc;
use thiserror::Error;
use url::Url;
#[derive(Debug, Parser)]
pub struct Args {}
pub(crate) async fn create_prod_system(
external_uri: &Url,
db_config: &DbConfig,
auth_config: &AuthConfig,
secret: &str,
) -> RouterApi {
create_system(
ApiConfig {
clock: ClockConfig::System,
store: StoreConfig::Postgres,
client: ClientConfig::Http,
},
external_uri,
Some(db_config),
Some(auth_config),
false,
secret,
)
.await
.unwrap()
}
pub(crate) enum ClockConfig {
System,
Virtual,
}
pub(crate) enum StoreConfig {
Postgres,
Memory,
}
pub(crate) enum ClientConfig {
Http,
Memory,
}
pub(crate) struct ApiConfig {
pub(crate) clock: ClockConfig,
pub(crate) store: StoreConfig,
pub(crate) client: ClientConfig,
}
struct LazyPgPool<'a> {
config: Option<&'a DbConfig>,
pool: Option<Arc<PgPool>>,
}
#[derive(Debug, Error)]
pub(crate) enum GetPgPoolError {
#[error("missing database configuration")]
MissingConfig,
#[error("failed to connect to the database")]
Connect(#[from] sqlx::Error),
}
impl<'a> LazyPgPool<'a> {
pub fn new(config: Option<&'a DbConfig>) -> Self {
Self { config, pool: None }
}
pub async fn get(&mut self) -> Result<Arc<PgPool>, GetPgPoolError> {
let pool = match &self.pool {
Some(pool) => pool,
None => {
let config: &DbConfig = match self.config {
Some(c) => c,
None => return Err(GetPgPoolError::MissingConfig),
};
let pool: PgPool = PgPoolOptions::new()
.max_connections(50)
.connect_with(
PgConnectOptions::new()
.host(&config.host)
.port(config.port)
.database(&config.name)
.username(&config.user)
.password(&config.password),
)
.await?;
self.pool.insert(Arc::new(pool))
}
};
Ok(Arc::clone(pool))
}
}
#[derive(Debug, Error)]
#[allow(clippy::enum_variant_names)]
pub(crate) enum CreateSystemError {
#[error("failed to acquire database pool")]
PgPool(#[from] GetPgPoolError),
#[error("failed to create PgDinoparcStore")]
PgDinoparcStore(#[source] AnyError),
#[error("failed to create PgHammerfestStore")]
PgHammerfestStore(#[source] AnyError),
#[error("failed to create PgTokenStore")]
PgTokenStore(#[source] AnyError),
}
pub(crate) async fn create_system(
config: ApiConfig,
external_uri: &Url,
db_config: Option<&DbConfig>,
auth_config: Option<&AuthConfig>,
_dev_endpoints: bool,
secret: &str,
) -> Result<RouterApi, CreateSystemError> {
let dev = DevApi::new();
let clock: Arc<dyn Clock> = match config.clock {
ClockConfig::System => Arc::new(SystemClock),
ClockConfig::Virtual => Arc::new(VirtualClock::new(Instant::ymd_hms(2020, 1, 1, 0, 0, 0))),
};
let mut db = LazyPgPool::new(db_config);
let db_secret = Secret::new(secret.to_string());
let internal_auth_key = secret.as_bytes().to_vec();
let auth_secret = secret.as_bytes().to_vec();
let uuid_generator: Arc<dyn UuidGenerator> = Arc::new(Uuid4Generator);
let email_formatter: Arc<dyn EmailFormatter> = Arc::new(JsonEmailFormatter);
let mailer: Arc<dyn Mailer> = Arc::new(MemMailer::new());
let password: Arc<dyn PasswordService> = Arc::new(ScryptPasswordService::recommended_for_tests());
let dinoparc_client: Arc<dyn DinoparcClient> = match config.client {
ClientConfig::Http => Arc::new(HttpDinoparcClient::new(Arc::clone(&clock), Arc::new(NoopLogger)).unwrap()),
ClientConfig::Memory => Arc::new(MemDinoparcClient::new(Arc::clone(&clock))),
};
let hammerfest_client: Arc<dyn HammerfestClient> = match config.client {
ClientConfig::Http => Arc::new(HttpHammerfestClient::new(Arc::clone(&clock)).unwrap()),
ClientConfig::Memory => Arc::new(MemHammerfestClient::new(Arc::clone(&clock))),
};
let twinoid_client: Arc<dyn TwinoidClient> = match config.client {
ClientConfig::Http => Arc::new(HttpTwinoidClient::new(Arc::clone(&clock)).unwrap()),
ClientConfig::Memory => Arc::new(MemTwinoidClient::new()),
};
let twinoid_oauth_client: Arc<dyn RfcOauthClient> = match config.client {
ClientConfig::Http => {
let auth_config = auth_config.expect("missing auth config");
Arc::new(
HttpRfcOauthClient::new(HttpRfcOauthClientOptions {
authorization_endpoint: Url::parse("https://twinoid.com/oauth/auth").unwrap(),
token_endpoint: Url::parse("https://twinoid.com/oauth/token").unwrap(),
callback_endpoint: {
let mut u: Url = external_uri.clone();
u.path_segments_mut().unwrap().extend(["oauth", "callback"]);
u
},
client_id: auth_config.twinoid.client_id.to_string(),
client_secret: auth_config.twinoid.secret.to_string(),
})
.unwrap(),
)
}
ClientConfig::Memory => Arc::new(MemRfcOauthClient::new()),
};
let auth_store: Arc<dyn AuthStore> = match config.store {
StoreConfig::Postgres => Arc::new(PgAuthStore::new(
Arc::clone(&clock),
db.get().await?,
Arc::clone(&uuid_generator),
db_secret.clone(),
)),
StoreConfig::Memory => Arc::new(MemAuthStore::new(Arc::clone(&clock), Arc::clone(&uuid_generator))),
};
let dinoparc_store: Arc<dyn DinoparcStore> = match config.store {
StoreConfig::Postgres => Arc::new(
PgDinoparcStore::new(Arc::clone(&clock), db.get().await?, Arc::clone(&uuid_generator))
.await
.map_err(CreateSystemError::PgDinoparcStore)?,
),
StoreConfig::Memory => Arc::new(MemDinoparcStore::new(Arc::clone(&clock))),
};
let forum_store: Arc<dyn ForumStore> = match config.store {
StoreConfig::Postgres => Arc::new(PgForumStore::new(
Arc::clone(&clock),
db.get().await?,
Arc::clone(&uuid_generator),
)),
StoreConfig::Memory => Arc::new(MemForumStore::new(Arc::clone(&clock), Arc::clone(&uuid_generator))),
};
let hammerfest_store: Arc<dyn HammerfestStore> = match config.store {
StoreConfig::Postgres => Arc::new(
PgHammerfestStore::new(
Arc::clone(&clock),
db.get().await?,
db_secret.clone(),
Arc::clone(&uuid_generator),
)
.await
.map_err(CreateSystemError::PgHammerfestStore)?,
),
StoreConfig::Memory => Arc::new(MemHammerfestStore::new(Arc::clone(&clock))),
};
let link_store: Arc<dyn LinkStore> = match config.store {
StoreConfig::Postgres => Arc::new(PgLinkStore::new(Arc::clone(&clock), db.get().await?)),
StoreConfig::Memory => Arc::new(MemLinkStore::new(Arc::clone(&clock))),
};
let oauth_provider_store: Arc<dyn OauthProviderStore> = match config.store {
StoreConfig::Postgres => Arc::new(PgOauthProviderStore::new(
Arc::clone(&clock),
db.get().await?,
Arc::clone(&password),
Arc::clone(&uuid_generator),
db_secret.clone(),
)),
StoreConfig::Memory => Arc::new(MemOauthProviderStore::new(
Arc::clone(&clock),
Arc::clone(&password),
Arc::clone(&uuid_generator),
)),
};
let user_store: Arc<dyn UserStore> = match config.store {
StoreConfig::Postgres => Arc::new(PgUserStore::new(
Arc::clone(&clock),
db.get().await?,
db_secret.clone(),
Arc::clone(&uuid_generator),
)),
StoreConfig::Memory => Arc::new(MemUserStore::new(Arc::clone(&clock), Arc::clone(&uuid_generator))),
};
let token_store: Arc<dyn TokenStore> = match config.store {
StoreConfig::Postgres => Arc::new(
PgTokenStore::new(Arc::clone(&clock), db.get().await?, db_secret.clone())
.await
.map_err(CreateSystemError::PgTokenStore)?,
),
StoreConfig::Memory => Arc::new(MemTokenStore::new(Arc::clone(&clock))),
};
let twinoid_store: Arc<dyn TwinoidStore> = match config.store {
StoreConfig::Postgres => Arc::new(PgTwinoidStore::new(Arc::clone(&clock), db.get().await?)),
StoreConfig::Memory => Arc::new(MemTwinoidStore::new(Arc::clone(&clock))),
};
let auth = Arc::new(AuthService::new(
Arc::clone(&auth_store),
Arc::clone(&clock),
Arc::clone(&dinoparc_client),
Arc::clone(&dinoparc_store),
Arc::clone(&email_formatter),
Arc::clone(&hammerfest_client),
Arc::clone(&hammerfest_store),
Arc::clone(&link_store),
Arc::clone(&mailer),
Arc::clone(&oauth_provider_store),
Arc::clone(&password),
Arc::clone(&user_store),
Arc::clone(&twinoid_client),
Arc::clone(&twinoid_oauth_client),
Arc::clone(&twinoid_store),
Arc::clone(&uuid_generator),
internal_auth_key,
auth_secret,
));
let dinoparc = Arc::new(DinoparcService::new(
Arc::clone(&dinoparc_client),
Arc::clone(&dinoparc_store),
Arc::clone(&link_store),
Arc::clone(&token_store),
Arc::clone(&user_store),
));
let forum = Arc::new(ForumService::new(
Arc::clone(&clock) as Arc<dyn Clock>,
Arc::clone(&forum_store),
Arc::clone(&user_store),
));
let hammerfest = Arc::new(HammerfestService::new(
Arc::clone(&hammerfest_client),
Arc::clone(&hammerfest_store),
Arc::clone(&link_store),
Arc::clone(&token_store),
Arc::clone(&user_store),
));
let oauth = Arc::new(OauthService::new(
Arc::clone(&oauth_provider_store),
Arc::clone(&user_store),
));
let twinoid = Arc::new(TwinoidService::new(
Arc::clone(&link_store),
Arc::clone(&twinoid_store),
Arc::clone(&user_store),
));
let user = Arc::new(UserService::new(
Arc::clone(&clock) as Arc<dyn Clock>,
dinoparc_client,
Arc::clone(&dinoparc_store),
Arc::clone(&hammerfest_client),
Arc::clone(&hammerfest_store),
Arc::clone(&link_store),
password,
Arc::clone(&token_store),
twinoid_client,
twinoid_store,
user_store,
));
Ok(RouterApi {
dev,
auth,
clock,
dinoparc,
forum,
hammerfest,
oauth,
twinoid,
user,
})
}
pub async fn run(_args: &Args) -> Result<(), AnyError> {
let config: Config = etwin_config::find_config(std::env::current_dir().unwrap()).unwrap();
let secret = config.etwin.secret.as_str();
let api = if std::env::var("NODE_ENV").as_deref() == Ok("production") {
create_prod_system(&config.etwin.external_uri, &config.db, &config.auth, secret).await
} else if config.etwin.api.as_str() == "postgres" {
create_system(
ApiConfig {
clock: ClockConfig::System,
store: StoreConfig::Postgres,
client: ClientConfig::Http,
},
&config.etwin.external_uri,
Some(&config.db),
Some(&config.auth),
false,
secret,
)
.await
.unwrap()
} else {
create_system(
ApiConfig {
clock: ClockConfig::Virtual,
store: StoreConfig::Memory,
client: ClientConfig::Memory,
},
&config.etwin.external_uri,
None,
Some(&config.auth),
true,
secret,
)
.await
.unwrap()
};
eprintln!("loaded internal Eternaltwin system");
for (client_key, client_config) in config.clients {
api
.oauth
.as_ref()
.upsert_system_client(&UpsertSystemClientOptions {
key: OauthClientKey::from_str(format!("{client_key}@clients").as_str())
.unwrap_or_else(|e| panic!("invalid client key {client_key:?}: {e}")),
display_name: OauthClientDisplayName::from_str(client_config.display_name.as_str())
.unwrap_or_else(|e| panic!("invalid client display name {:?}: {e}", client_config.display_name)),
app_uri: client_config.app_uri,
callback_uri: client_config.callback_uri,
secret: Password::from(client_config.secret.as_bytes()),
})
.await
.unwrap_or_else(|e| panic!("failed client upsert {client_key:?}: {e}"));
}
for (section_key, section_config) in config.forum.sections {
api
.forum
.as_ref()
.upsert_system_section(&UpsertSystemSectionOptions {
key: ForumSectionKey::from_str(section_key.as_str())
.unwrap_or_else(|e| panic!("invalid section key {section_key:?}: {e}")),
display_name: ForumSectionDisplayName::from_str(section_config.display_name.as_str())
.unwrap_or_else(|e| panic!("invalid section display name {:?}: {e}", section_config.display_name)),
locale: section_config
.locale
.as_deref()
.map(LocaleId::from_str)
.transpose()
.unwrap_or_else(|e| panic!("invalid locale id {:?}: {e}", section_config.locale)),
})
.await
.unwrap_or_else(|e| panic!("failed section upsert {section_key:?}: {e}"));
}
eprintln!("initialization complete");
let mut listen_addr: SocketAddr = "[::]:80".parse().unwrap();
listen_addr.set_port(config.etwin.backend_port);
eprintln!("started server at http://localhost:{}", listen_addr.port());
axum::Server::bind(&listen_addr)
.serve(etwin_rest::app(api).into_make_service())
.await
.unwrap();
Ok(())
}