315 lines
9.6 KiB
Rust
315 lines
9.6 KiB
Rust
#[macro_use]
|
|
extern crate dotenvy_macro;
|
|
extern crate dotenvy;
|
|
|
|
use dotenvy::dotenv;
|
|
use tokio::time::sleep;
|
|
|
|
use std::{env, fs, net::Ipv4Addr, net::Ipv6Addr, time::Duration, sync::{Arc, atomic::{AtomicBool, Ordering}}};
|
|
|
|
use chrono::Local;
|
|
|
|
use actix_web::{middleware::Logger, web, App, HttpServer, Result};
|
|
use serde::{Deserialize, Serialize};
|
|
use sqlx::{PgPool, Pool, Postgres, Connection};
|
|
use utoipa::{OpenApi, openapi::security::{SecurityScheme, ApiKey, ApiKeyValue}, Modify};
|
|
use utoipa_swagger_ui::{Config, SwaggerUi, Url};
|
|
|
|
mod db;
|
|
mod v2;
|
|
mod v3;
|
|
|
|
macro_rules! api_base {
|
|
() => {
|
|
"/api"
|
|
};
|
|
}
|
|
|
|
macro_rules! api_base_2 {
|
|
() => {
|
|
"/api/v2"
|
|
};
|
|
}
|
|
|
|
macro_rules! api_base_3 {
|
|
() => {
|
|
"/api/v3"
|
|
};
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
pub(crate) struct ConfigToml {
|
|
auth: AuthenticationTokens,
|
|
port: PortConfig,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
pub(crate) struct PortConfig {
|
|
default: u16,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
pub(crate) struct AuthenticationTokens {
|
|
moderator: String,
|
|
admin: String
|
|
}
|
|
|
|
pub struct AppState {
|
|
db: Pool<Postgres>,
|
|
auth_tokens: AuthenticationTokens,
|
|
}
|
|
|
|
async fn postgres_watchdog(pool: PgPool, is_alive: Arc<AtomicBool>, shutdown: Arc<AtomicBool>) {
|
|
loop {
|
|
let start = Local::now();
|
|
|
|
let mut conn = match pool.acquire().await {
|
|
Ok(data) => data,
|
|
Err(_) => break,
|
|
};
|
|
|
|
match conn.ping().await {
|
|
Ok(_) => {println!("Pinged DB Server at {}", Local::now().format("%H:%M:%S"))},
|
|
Err(_) => {println!("Error pinging Server"); break;},
|
|
};
|
|
|
|
let url = dotenvy::var("UPTIME_KUMA_URL").unwrap_or("".to_string());
|
|
if url != "" {
|
|
match reqwest::get(
|
|
url,
|
|
).await {
|
|
Ok(_) => {}
|
|
Err(err) => println!("{}", err),
|
|
};
|
|
}
|
|
else {
|
|
println!("No Uptime Kuma URL provided!");
|
|
}
|
|
|
|
|
|
let passed = (Local::now() - start).to_std().expect(&format!("Unable to get Time Difference for '{}' and '{}'", start, Local::now()));
|
|
|
|
while Local::now() - start < chrono::Duration::seconds(15) {
|
|
sleep(Duration::from_millis(100)).await;
|
|
if shutdown.load(Ordering::Relaxed) {
|
|
break;
|
|
}
|
|
}
|
|
if shutdown.load(Ordering::Relaxed) {
|
|
break;
|
|
}
|
|
}
|
|
is_alive.store(false, Ordering::Relaxed);
|
|
}
|
|
|
|
#[actix_web::main]
|
|
async fn main() -> Result<()> {
|
|
env_logger::init();
|
|
dotenv().ok();
|
|
|
|
let shutdown: Arc<AtomicBool> = Arc::new(AtomicBool::new(false));
|
|
|
|
let shutdown_clone = Arc::clone(&shutdown);
|
|
ctrlc::set_handler(move || {
|
|
eprintln!("Ctrl-C received");
|
|
shutdown_clone.store(true, Ordering::Relaxed)
|
|
})
|
|
.expect("Error setting Ctrl-C handler");
|
|
|
|
let toml_str = fs::read_to_string("config.toml").expect("Failed to read config.toml");
|
|
|
|
let config: ConfigToml = toml::from_str(&toml_str).expect("Failed to parse config.toml");
|
|
|
|
println!("DATABASE_URL: {}", env::var("DATABASE_URL").unwrap()); // DBEUG
|
|
|
|
struct ApiSecurity;
|
|
|
|
impl Modify for ApiSecurity {
|
|
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
|
|
let components = openapi.components.as_mut().unwrap(); // we can unwrap safely since there already is components registered.
|
|
components.add_security_scheme(
|
|
"api_key",
|
|
SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("x-api-key"))),
|
|
);
|
|
}
|
|
}
|
|
|
|
#[derive(OpenApi)]
|
|
#[openapi(
|
|
paths(
|
|
v2::empire_ethics,
|
|
v2::empires,
|
|
v2::ethics,
|
|
v2::game_groups,
|
|
v2::games,
|
|
v2::portrait_groups,
|
|
v2::portraits
|
|
),
|
|
components(schemas(
|
|
v2::schemas::PortraitGroup,
|
|
v2::schemas::Portrait,
|
|
v2::schemas::Ethic,
|
|
v2::schemas::Empire,
|
|
v2::schemas::EmpireEthic,
|
|
v2::schemas::Game,
|
|
v2::schemas::GameGroup
|
|
))
|
|
)]
|
|
struct ApiDocV2;
|
|
|
|
#[derive(OpenApi)]
|
|
#[openapi(
|
|
paths(
|
|
v3::full_view_data,
|
|
v3::auth,
|
|
v3::list_games,
|
|
v3::get_game_data,
|
|
v3::create_game,
|
|
v3::edit_game,
|
|
v3::delete_game,
|
|
v3::create_group,
|
|
v3::edit_group,
|
|
v3::delete_group,
|
|
v3::get_empire,
|
|
v3::create_empire,
|
|
v3::edit_empire,
|
|
v3::delete_empire,
|
|
v3::get_ethics,
|
|
v3::get_phenotypes
|
|
),
|
|
components(schemas(
|
|
v3::schemas::AuthReturn,
|
|
v3::schemas::GetGameParam,
|
|
v3::schemas::PostGameParams,
|
|
v3::schemas::UpdateGameParams,
|
|
v3::schemas::DeleteGameParam,
|
|
v3::schemas::PostGroupParams,
|
|
v3::schemas::UpdateGroupParams,
|
|
v3::schemas::DeleteGroupParams,
|
|
v3::schemas::GetEmpireParams,
|
|
v3::schemas::PostEmpireParams,
|
|
v3::schemas::UpdateEmpireParams,
|
|
v3::schemas::DeleteEmpireParams,
|
|
v3::schemas::FullViewData,
|
|
v3::schemas::Ethic,
|
|
v3::schemas::ChellarisEthics,
|
|
v3::schemas::EmpireEthic,
|
|
v3::schemas::EmpireEthicLegacy,
|
|
v3::schemas::ChellarisGameLegacy,
|
|
v3::schemas::ChellarisGameFlat,
|
|
v3::schemas::ChellarisGame,
|
|
v3::schemas::Phenotype,
|
|
v3::schemas::ChellarisPhenotypes,
|
|
v3::schemas::ChellarisGameGroupLegacy,
|
|
v3::schemas::ChellarisGroupFlat,
|
|
v3::schemas::Species,
|
|
v3::schemas::ChellarisEmpireLegacy,
|
|
v3::schemas::ChellarisEmpireFlat,
|
|
v3::schemas::ChellarisEmpire
|
|
)),
|
|
modifiers(&ApiSecurity)
|
|
)]
|
|
struct ApiDocV3;
|
|
|
|
let openapi_urls = vec![
|
|
Url::new("v2", concat!(api_base_2!(), "/openapi.json")),
|
|
Url::new("v3", concat!(api_base_3!(), "/openapi.json")),
|
|
];
|
|
|
|
let is_alive: Arc<AtomicBool> = Arc::new(AtomicBool::new(true));
|
|
|
|
loop {
|
|
let db_auth_tokens = config.auth.clone();
|
|
let pool = PgPool::connect(dotenv!("DATABASE_URL")).await.unwrap();
|
|
let pool_copy = pool.clone();
|
|
|
|
let swagger_config = Config::new(openapi_urls.clone());
|
|
|
|
let mut openapi_v2 = ApiDocV2::openapi();
|
|
openapi_v2.info.title = "Chellaris Rust API".to_string();
|
|
|
|
let mut openapi_v3 = ApiDocV3::openapi();
|
|
openapi_v3.info.title = "Chellaris Rust API".to_string();
|
|
|
|
let server = HttpServer::new(move || {
|
|
App::new()
|
|
.app_data(web::Data::new(AppState { db: pool.clone(), auth_tokens: db_auth_tokens.clone() }))
|
|
.wrap(Logger::default())
|
|
// API v2 Endpoints
|
|
.service(v2::empire_ethics)
|
|
.service(v2::empires)
|
|
.service(v2::ethics)
|
|
.service(v2::game_groups)
|
|
.service(v2::games)
|
|
.service(v2::portrait_groups)
|
|
.service(v2::portraits)
|
|
// API v3 Endpoints
|
|
.service(v3::full_view_data)
|
|
.service(v3::auth)
|
|
.service(v3::list_games)
|
|
.service(v3::get_game_data)
|
|
.service(v3::create_game)
|
|
.service(v3::edit_game)
|
|
.service(v3::delete_game)
|
|
.service(v3::create_group)
|
|
.service(v3::edit_group)
|
|
.service(v3::delete_group)
|
|
.service(v3::get_empire)
|
|
.service(v3::create_empire)
|
|
.service(v3::edit_empire)
|
|
.service(v3::delete_empire)
|
|
.service(v3::get_ethics)
|
|
.service(v3::get_phenotypes)
|
|
// Swagger UI
|
|
.service(
|
|
SwaggerUi::new(concat!(api_base!(), "/swagger/{_:.*}"))
|
|
.urls(vec![
|
|
(
|
|
Url::new("v2", concat!(api_base_2!(), "/openapi.json")),
|
|
openapi_v2.clone(),
|
|
),
|
|
(
|
|
Url::new("v3", concat!(api_base_3!(), "/openapi.json")),
|
|
openapi_v3.clone(),
|
|
),
|
|
])
|
|
.config(swagger_config.clone()),
|
|
)
|
|
})
|
|
.bind((Ipv6Addr::UNSPECIFIED, config.port.default)).expect("Port or IP already occupied")
|
|
.run();
|
|
|
|
let server_thread = tokio::spawn(async {
|
|
println!("Awaiting server");
|
|
let _ = server.await;
|
|
println!("Stopped awaiting server");
|
|
});
|
|
|
|
println!("Started Serving API on: ");
|
|
println!(" -> http://[{}]:{}", Ipv6Addr::UNSPECIFIED, 8080);
|
|
println!(" -> http://{}:{}", Ipv4Addr::UNSPECIFIED, 8080);
|
|
|
|
let is_alive_clone = Arc::clone(&is_alive);
|
|
let shutdown_clone = Arc::clone(&shutdown);
|
|
let _ = tokio::spawn(async move { postgres_watchdog(pool_copy, is_alive_clone, shutdown_clone).await });
|
|
|
|
//watchdog_thread.await;
|
|
|
|
while is_alive.load(Ordering::Relaxed) {
|
|
let thread = tokio::spawn(async {
|
|
let _ = sleep(Duration::from_millis(100));
|
|
});
|
|
|
|
let _ = thread.await;
|
|
}
|
|
|
|
if shutdown.load(Ordering::Relaxed) {
|
|
break;
|
|
}
|
|
|
|
eprintln!("Connection died, restarting Server");
|
|
server_thread.abort();
|
|
}
|
|
|
|
Ok(())
|
|
}
|