// StarKingdoms.IO, a browser game about drifting through space
// Copyright (C) 2023 ghostly_zsh, TerraMaster85, core
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::config::Config;
use crate::error::APIErrorResponse;
use actix_web::middleware::Logger;
use actix_web::web::{Data, JsonConfig};
use actix_web::{App, Error, HttpResponse, HttpServer};
use diesel::Connection;
use diesel_async::async_connection_wrapper::AsyncConnectionWrapper;
use diesel_async::pooled_connection::bb8::Pool;
use diesel_async::pooled_connection::AsyncDieselConnectionManager;
use diesel_async::AsyncPgConnection;
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
use hmac::digest::KeyInit;
use hmac::Hmac;
use log::{error, info};
use sha2::Sha256;
use snowflake::SnowflakeIdGenerator;
use std::fs;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::{Duration, UNIX_EPOCH};
pub mod error;
#[macro_use]
pub mod response;
pub mod config;
pub mod models;
pub mod routes;
pub mod schema;
pub mod auth;
pub mod tokens;
#[derive(Clone)]
pub struct AppState {
pub config: Config,
pub pool: bb8::Pool<AsyncDieselConnectionManager<AsyncPgConnection>>,
pub idgen: Arc<Mutex<SnowflakeIdGenerator>>,
pub key: Hmac<Sha256>,
}
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
#[actix_web::main]
async fn main() {
env_logger::init();
info!(
"StarKingdoms API v{} starting up",
env!("CARGO_PKG_VERSION")
);
let mut args = std::env::args();
let config_path = match args.nth(1) {
Some(path) => path,
None => {
eprintln!("usage: starkingdoms-api <config_path>");
std::process::exit(1);
}
};
let config_pathbuf = PathBuf::from(config_path);
info!("Loading config from {}", config_pathbuf.display());
let config_str = match fs::read_to_string(&config_pathbuf) {
Ok(c_str) => c_str,
Err(e) => {
error!(
"Error loading configuration from {}: {}",
config_pathbuf.display(),
e
);
std::process::exit(1);
}
};
let config: Config = match toml::from_str(&config_str) {
Ok(config) => config,
Err(e) => {
error!(
"Error parsing configuration in {}: {}",
config_pathbuf.display(),
e
);
std::process::exit(1);
}
};
info!("Connecting to the database...");
let pool_config = AsyncDieselConnectionManager::<AsyncPgConnection>::new(&config.database.url);
let pool = match Pool::builder().build(pool_config).await {
Ok(pool) => pool,
Err(e) => {
error!("Error while creating database pool: {}", e);
std::process::exit(1);
}
};
info!("Running pending migrations...");
let local_config = config.clone();
let db_url = config.database.url.clone();
match actix_web::rt::task::spawn_blocking(move || {
// Lock block
let mut conn = match AsyncConnectionWrapper::<AsyncPgConnection>::establish(&db_url) {
Ok(conn) => conn,
Err(e) => {
error!("Error acquiring connection from pool: {}", e);
std::process::exit(1);
}
};
match conn.run_pending_migrations(MIGRATIONS) {
Ok(_) => (),
Err(e) => {
error!("Failed to run pending migrations: {}", e);
std::process::exit(1);
}
}
})
.await
{
Ok(_) => (),
Err(e) => {
error!("Error waiting for migrations: {}", e);
std::process::exit(1);
}
}
let key = Hmac::new_from_slice(config.server.application_key.as_bytes()).unwrap();
let stk_epoch = UNIX_EPOCH + Duration::from_secs(1616260136);
let id_generator = SnowflakeIdGenerator::with_epoch(
config.server.machine_id,
config.server.node_id,
stk_epoch,
);
let app_state = Data::new(AppState {
config,
pool,
idgen: Arc::new(Mutex::new(id_generator)),
key,
});
let server = HttpServer::new(move || {
App::new()
.app_data(JsonConfig::default().error_handler(|err, _rq| {
Error::from({
let err2: APIErrorResponse = (&err).into();
actix_web::error::InternalError::from_response(
err,
HttpResponse::BadRequest().json(err2),
)
})
}))
.service(routes::signup::signup_req)
.service(routes::login::user_login_req)
.wrap(Logger::default())
.wrap(actix_cors::Cors::permissive())
.app_data(app_state.clone())
})
.bind((local_config.server.bind.ip, local_config.server.bind.port))
.unwrap();
server.run().await.unwrap();
info!("Goodbye!");
}