This repository has been archived on 2024-08-06. You can view files and clone it, but cannot push or open issues or pull requests.
chellaris-rust-api/src/main.rs

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(())
}