use crate::cmd::backend::{create_prod_system, create_system, ApiConfig, ClientConfig, ClockConfig, StoreConfig};
use axum::http::header;
use axum::http::{StatusCode, Uri};
use clap::Parser;
use etwin_config::Config;
use etwin_core::core::LocaleId;
use etwin_core::forum::{ForumSectionDisplayName, ForumSectionKey, UpsertSystemSectionOptions};
use etwin_core::oauth::{OauthClientDisplayName, OauthClientKey, UpsertSystemClientOptions};
use etwin_core::password::Password;
use etwin_core::types::AnyError;
use std::net::SocketAddr;
use std::str::FromStr;
#[derive(Debug, Parser)]
pub struct Args {}
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.http_port);
let backend = etwin_rest::app(api).fallback(fallback);
eprintln!("started server at http://localhost:{}", listen_addr.port());
axum::Server::bind(&listen_addr)
.serve(backend.into_make_service())
.await
.unwrap();
Ok(())
}
async fn fallback(uri: Uri) -> (StatusCode, [(header::HeaderName, &'static str); 2], Vec<u8>) {
const INDEX: &str = "index.html";
let path = uri.path();
let path = path.strip_prefix('/').unwrap_or(path);
let file = if path == INDEX {
None
} else {
etwin_app::BROWSER.get_file(path)
};
let (media_type, file) = match file {
None => {
let index = match etwin_app::BROWSER.get_file(INDEX) {
None => {
eprintln!("<-> GET {} 1ms Not Found", uri.path(),);
return (
StatusCode::INTERNAL_SERVER_ERROR,
[
(header::X_CONTENT_TYPE_OPTIONS, "nosniff"),
(header::CONTENT_TYPE, "text/plain; charset=utf-8"),
],
"Not Found".to_string().into_bytes(),
);
}
Some(index) => index,
};
("text/html; charset=utf-8", index)
}
Some(file) => {
let extension = match path.rfind('.').map(|idx| &path[idx..]) {
Some(".css") => "text/css; charset=utf-8",
Some(".gif") => "image/gif",
Some(".ico") => "image/vnd.microsoft.icon",
Some(".jpg") => "image/jpeg",
Some(".jpeg") => "image/jpeg",
Some(".js") => "text/javascript; charset=utf-8",
Some(".png") => "image/png",
Some(".svg") => "image/svg+xml; charset=utf-8",
Some(".woff2") => "font/woff2",
ext => {
if let Some(ext) = ext {
eprintln!("unknown file extension: {ext}");
}
"application/octet-stream"
}
};
(extension, file)
}
};
eprintln!("<-> GET {} 1ms OK", uri.path(),);
(
StatusCode::OK,
[
(header::X_CONTENT_TYPE_OPTIONS, "nosniff"),
(header::CONTENT_TYPE, media_type),
],
file.contents().to_vec(),
)
}