Add new "v1" API, rename "v2" and "v3" to legacy
This commit is contained in:
parent
134581f8ad
commit
b3347a6e53
3 changed files with 487 additions and 13 deletions
73
src/main.rs
73
src/main.rs
|
@ -25,15 +25,21 @@ macro_rules! api_base {
|
|||
};
|
||||
}
|
||||
|
||||
macro_rules! api_base_1 {
|
||||
() => {
|
||||
"/api/v1"
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! api_base_2 {
|
||||
() => {
|
||||
"/api/v2"
|
||||
"/api/v2-l"
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! api_base_3 {
|
||||
() => {
|
||||
"/api/v3"
|
||||
"/api/v3-l"
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -208,9 +214,27 @@ async fn main() -> Result<()> {
|
|||
)]
|
||||
struct ApiDocV3;
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(
|
||||
v1::get_user,
|
||||
v1::create_user,
|
||||
v1::update_user,
|
||||
v1::delete_user
|
||||
),
|
||||
components(schemas(
|
||||
v1::schemas::GetUserParams,
|
||||
v1::schemas::UpdateUserParams,
|
||||
v1::schemas::DeleteUserParams,
|
||||
v1::schemas::User
|
||||
))
|
||||
)]
|
||||
struct ApiDocV1;
|
||||
|
||||
let openapi_urls = vec![
|
||||
Url::new("v2", concat!(api_base_2!(), "/openapi.json")),
|
||||
Url::new("v3", concat!(api_base_3!(), "/openapi.json")),
|
||||
Url::new("v1", concat!(api_base_1!(), "/openapi.json")),
|
||||
Url::new("v2-L", concat!(api_base_2!(), "/openapi.json")),
|
||||
Url::new("v3-L", concat!(api_base_3!(), "/openapi.json")),
|
||||
];
|
||||
|
||||
let is_alive: Arc<AtomicBool> = Arc::new(AtomicBool::new(true));
|
||||
|
@ -218,15 +242,31 @@ async fn main() -> Result<()> {
|
|||
loop {
|
||||
let db_auth_tokens = config.auth.clone();
|
||||
let pool = PgPool::connect(dotenv!("DATABASE_URL")).await.unwrap();
|
||||
|
||||
let pool_copy = pool.clone();
|
||||
let shutdown_clone = shutdown.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_v1 = ApiDocV1::openapi();
|
||||
openapi_v1.info.title = "Chellaris Rust API v1".to_string();
|
||||
|
||||
let mut openapi_v3 = ApiDocV3::openapi();
|
||||
openapi_v3.info.title = "Chellaris Rust API".to_string();
|
||||
let mut openapi_v2_l = ApiDocV2::openapi();
|
||||
openapi_v2_l.info.title = "Legacy Chellaris Rust API v2".to_string();
|
||||
|
||||
let mut openapi_v3_l = ApiDocV3::openapi();
|
||||
openapi_v3_l.info.title = "Legacy Chellaris Rust API v3".to_string();
|
||||
|
||||
println!("Serving API on: ");
|
||||
println!(" -> http://[{}]:{}", Ipv6Addr::UNSPECIFIED, 8080);
|
||||
println!(" -> http://{}:{}", Ipv4Addr::UNSPECIFIED, 8080);
|
||||
|
||||
let watchdog_thread = tokio::spawn(async move { postgres_watchdog(pool_copy, shutdown_clone) });
|
||||
tokio::spawn(async move {
|
||||
actix_web::rt::signal::ctrl_c().await.unwrap();
|
||||
println!("Ctrl-C received, killing Server");
|
||||
abort()
|
||||
});
|
||||
|
||||
let server = HttpServer::new(move || {
|
||||
App::new()
|
||||
|
@ -257,17 +297,26 @@ async fn main() -> Result<()> {
|
|||
.service(v3::delete_empire)
|
||||
.service(v3::get_ethics)
|
||||
.service(v3::get_phenotypes)
|
||||
// API v1 Endpoints
|
||||
.service(v1::get_user)
|
||||
.service(v1::create_user)
|
||||
.service(v1::update_user)
|
||||
.service(v1::delete_user)
|
||||
// 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("v1", concat!(api_base_1!(), "/openapi.json")),
|
||||
openapi_v1.clone(),
|
||||
),
|
||||
(
|
||||
Url::new("v3", concat!(api_base_3!(), "/openapi.json")),
|
||||
openapi_v3.clone(),
|
||||
Url::new("v2-l", concat!(api_base_2!(), "/openapi.json")),
|
||||
openapi_v2_l.clone(),
|
||||
),
|
||||
(
|
||||
Url::new("v3-l", concat!(api_base_3!(), "/openapi.json")),
|
||||
openapi_v3_l.clone(),
|
||||
),
|
||||
])
|
||||
.config(swagger_config.clone()),
|
||||
|
|
360
src/v1/mod.rs
Normal file
360
src/v1/mod.rs
Normal file
|
@ -0,0 +1,360 @@
|
|||
use crate::{db, AppState};
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{delete, get, post, put, web, HttpRequest, HttpResponse, Responder};
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
use rand::distributions::Alphanumeric;
|
||||
use rand::{Rng, thread_rng};
|
||||
use sqlx::QueryBuilder;
|
||||
|
||||
pub(crate) mod schemas;
|
||||
|
||||
fn get_auth_header<'a>(req: &'a HttpRequest) -> Option<&'a str> {
|
||||
req.headers().get("x-api-key")?.to_str().ok()
|
||||
}
|
||||
|
||||
async fn verify_user_auth(data: &web::Data<AppState>, auth_token: &str, user_token: &str, table: schemas::TablePermission, permissions_only: bool) -> bool {
|
||||
let user: db::schemas::User = match sqlx::query_as!(
|
||||
db::schemas::User,
|
||||
"SELECT * FROM public.users WHERE token = $1",
|
||||
auth_token
|
||||
)
|
||||
.fetch_one(&data.db)
|
||||
.await
|
||||
{
|
||||
Ok(data) => data,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
if user.token == user_token && !permissions_only {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
match table {
|
||||
schemas::TablePermission::Game => {
|
||||
return user.data_permissions;
|
||||
},
|
||||
schemas::TablePermission::Empire => {
|
||||
return user.empire_permissions;
|
||||
},
|
||||
schemas::TablePermission::Data => {
|
||||
return user.data_permissions;
|
||||
},
|
||||
schemas::TablePermission::User => {
|
||||
return user.user_permissions;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// User Endpoints
|
||||
#[utoipa::path(
|
||||
request_body = schemas::GetUserParams,
|
||||
responses(
|
||||
(status = 200, description = "OK", body = User),
|
||||
(status = 403, description = "Unauthorized"),
|
||||
(status = 500, description = "Internal Server Error")
|
||||
),
|
||||
security(
|
||||
("api_key" = [])
|
||||
),
|
||||
)]
|
||||
#[get("/api/v1/user")]
|
||||
async fn get_user(
|
||||
data: web::Data<AppState>,
|
||||
params: web::Json<schemas::GetUserParams>,
|
||||
req: HttpRequest,
|
||||
) -> impl Responder {
|
||||
let auth_header = get_auth_header(&req);
|
||||
let params = params.into_inner();
|
||||
|
||||
let auth_token: String;
|
||||
|
||||
match auth_header {
|
||||
Some(token) => auth_token = token.to_string(),
|
||||
None => return HttpResponse::Unauthorized().finish(),
|
||||
};
|
||||
|
||||
let auth = verify_user_auth(&data, &auth_token, ¶ms.user_token, schemas::TablePermission::User, false).await;
|
||||
|
||||
if auth {
|
||||
let user: db::schemas::User = match sqlx::query_as!(
|
||||
db::schemas::User,
|
||||
"SELECT * FROM public.users WHERE token = $1",
|
||||
params.user_token
|
||||
)
|
||||
.fetch_one(&data.db)
|
||||
.await
|
||||
{
|
||||
Ok(data) => data,
|
||||
Err(_) => return HttpResponse::InternalServerError().finish(),
|
||||
};
|
||||
|
||||
let mut permissions: HashMap<String, bool> = HashMap::new();
|
||||
|
||||
permissions.insert("game_permissions".to_string(), user.game_permissions);
|
||||
permissions.insert("empire_permissions".to_string(), user.empire_permissions);
|
||||
permissions.insert("data_permissions".to_string(), user.data_permissions);
|
||||
permissions.insert("user_permissions".to_string(), user.user_permissions);
|
||||
|
||||
let return_data = schemas::User {
|
||||
user_token: user.token,
|
||||
discord_handle: user.discord_id,
|
||||
profile_picture: user.picture_url,
|
||||
permissions: permissions
|
||||
};
|
||||
|
||||
return HttpResponse::Ok().json(return_data);
|
||||
}
|
||||
else {
|
||||
return HttpResponse::Unauthorized().finish();
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
responses(
|
||||
(status = 200, description = "OK", body = User),
|
||||
(status = 500, description = "Internal Server Error")
|
||||
),
|
||||
)]
|
||||
#[post("/api/v1/user")]
|
||||
pub(crate) async fn create_user(
|
||||
data: web::Data<AppState>,
|
||||
) -> impl Responder {
|
||||
let user: db::schemas::User;
|
||||
|
||||
let mut rng = thread_rng();
|
||||
|
||||
let user_tokens = match sqlx::query_scalar!(
|
||||
"SELECT token FROM public.users"
|
||||
)
|
||||
.fetch_all(&data.db)
|
||||
.await
|
||||
{
|
||||
Ok(data) => data,
|
||||
Err(_) => return HttpResponse::InternalServerError().finish(),
|
||||
};
|
||||
|
||||
let new_token: String;
|
||||
|
||||
loop {
|
||||
let mut chars: String = (0..6).map(|_| rng.sample(Alphanumeric) as char).collect();
|
||||
chars = chars.to_uppercase();
|
||||
if !user_tokens.contains(&chars) {
|
||||
new_token = chars;
|
||||
break;
|
||||
}
|
||||
else {
|
||||
println!("looping");
|
||||
}
|
||||
}
|
||||
|
||||
user = match sqlx::query_as!(
|
||||
db::schemas::User,
|
||||
"INSERT INTO public.users(token, game_permissions, empire_permissions, data_permissions, user_permissions) VALUES ($1, $2, $3, $4, $5) RETURNING * ",
|
||||
new_token,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
|
||||
)
|
||||
.fetch_one(&data.db)
|
||||
.await
|
||||
{
|
||||
Ok(data) => data,
|
||||
Err(_) => return HttpResponse::InternalServerError().finish(),
|
||||
};
|
||||
|
||||
return HttpResponse::Ok().json(user)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
request_body = UpdateUserParams,
|
||||
responses(
|
||||
(status = 200, description = "OK"),
|
||||
(status = 403, description = "Unauthorized"),
|
||||
(status = 500, description = "Internal Server Error")
|
||||
),
|
||||
security(
|
||||
("api_key" = [])
|
||||
),
|
||||
)]
|
||||
#[put("/api/v1/user")]
|
||||
pub(crate) async fn update_user(
|
||||
data: web::Data<AppState>,
|
||||
params: web::Json<schemas::UpdateUserParams>,
|
||||
req: HttpRequest,
|
||||
) -> impl Responder {
|
||||
let auth_header = get_auth_header(&req);
|
||||
let params = params.into_inner();
|
||||
|
||||
let auth_token: String;
|
||||
|
||||
match auth_header {
|
||||
Some(token) => auth_token = token.to_string(),
|
||||
None => return HttpResponse::Unauthorized().finish(),
|
||||
};
|
||||
|
||||
let mut elevated_auth = false;
|
||||
if params.permissions["game_permissions"] || params.permissions["empire_permissions"] || params.permissions["data_permissions"] || params.permissions["user_permissions"] {
|
||||
elevated_auth = true;
|
||||
}
|
||||
|
||||
let auth = verify_user_auth(&data, &auth_token, ¶ms.user_token, schemas::TablePermission::User, elevated_auth).await;
|
||||
|
||||
// SQL Queries
|
||||
// TODO: Optimize by utilizing some SQL magic, for now this has to do
|
||||
if auth {
|
||||
let user: db::schemas::User;
|
||||
|
||||
let mut user_query = QueryBuilder::<sqlx::Postgres>::new("UPDATE public.users SET ");
|
||||
let mut user_query_separated = user_query.separated(", ");
|
||||
let mut any_param_present = false;
|
||||
|
||||
if let Some(discord_handle) = params.discord_handle {
|
||||
user_query_separated.push(" discord_id = ").push_bind_unseparated(discord_handle);
|
||||
any_param_present = true;
|
||||
}
|
||||
|
||||
if let Some(profile_picture) = params.profile_picture {
|
||||
user_query_separated.push(" picture_url = ");
|
||||
match any_param_present {
|
||||
true => user_query_separated.push_bind(profile_picture),
|
||||
false => user_query_separated.push_bind_unseparated(profile_picture)
|
||||
};
|
||||
any_param_present = true;
|
||||
}
|
||||
|
||||
for (entry, value) in params.permissions.iter() {
|
||||
match entry.deref() {
|
||||
"game_permissions" => {
|
||||
user_query_separated.push( " game_permissions = ");
|
||||
match any_param_present {
|
||||
true => user_query_separated.push_bind(value),
|
||||
false => user_query_separated.push_bind_unseparated(value)
|
||||
};
|
||||
any_param_present = true;
|
||||
},
|
||||
"empire_permissions" => {
|
||||
user_query_separated.push( " empire_permissions = ");
|
||||
match any_param_present {
|
||||
true => user_query_separated.push_bind(value),
|
||||
false => user_query_separated.push_bind_unseparated(value)
|
||||
};
|
||||
any_param_present = true;
|
||||
},
|
||||
"data_permissions" => {
|
||||
user_query_separated.push( " data_permissions = ");
|
||||
match any_param_present {
|
||||
true => user_query_separated.push_bind(value),
|
||||
false => user_query_separated.push_bind_unseparated(value)
|
||||
};
|
||||
any_param_present = true;
|
||||
},
|
||||
"user_permissions" => {
|
||||
user_query_separated.push( " user_permissions = ");
|
||||
match any_param_present {
|
||||
true => user_query_separated.push_bind(value),
|
||||
false => user_query_separated.push_bind_unseparated(value)
|
||||
};
|
||||
any_param_present = true;
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if any_param_present {
|
||||
user_query_separated.push_unseparated(" WHERE token = ").push_bind_unseparated(params.user_token);
|
||||
user_query_separated.push_unseparated(" RETURNING *");
|
||||
|
||||
user = match user_query
|
||||
.build_query_as::<db::schemas::User>()
|
||||
.fetch_one(&data.db)
|
||||
.await
|
||||
{
|
||||
Ok(data) => data,
|
||||
Err(_) => return HttpResponse::InternalServerError().finish(),
|
||||
};
|
||||
} else {
|
||||
user = match sqlx::query_as!(
|
||||
db::schemas::User,
|
||||
"SELECT * FROM public.users WHERE token = $1",
|
||||
params.user_token
|
||||
)
|
||||
.fetch_one(&data.db)
|
||||
.await
|
||||
{
|
||||
Ok(data) => data,
|
||||
Err(_) => return HttpResponse::InternalServerError().finish(),
|
||||
};
|
||||
}
|
||||
|
||||
let mut permissions: HashMap<String, bool> = HashMap::new();
|
||||
permissions.insert("game_permissions".to_string(), user.game_permissions);
|
||||
permissions.insert("empire_permissions".to_string(), user.empire_permissions);
|
||||
permissions.insert("data_permissions".to_string(), user.data_permissions);
|
||||
permissions.insert("user_permissions".to_string(), user.user_permissions);
|
||||
|
||||
let return_data = schemas::User {
|
||||
user_token: user.token,
|
||||
discord_handle: user.discord_id,
|
||||
profile_picture: user.picture_url,
|
||||
permissions: permissions
|
||||
};
|
||||
return HttpResponse::Ok().json(return_data);
|
||||
} else {
|
||||
return HttpResponse::Unauthorized().finish();
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
request_body = schemas::DeleteUserParams,
|
||||
responses(
|
||||
(status = 200, description = "OK"),
|
||||
(status = 403, description = "Unauthorized"),
|
||||
(status = 500, description = "Internal Server Error")
|
||||
),
|
||||
security(
|
||||
("api_key" = [])
|
||||
),
|
||||
)]
|
||||
#[delete("/api/v1/user")]
|
||||
pub(crate) async fn delete_user(
|
||||
data: web::Data<AppState>,
|
||||
params: web::Query<schemas::DeleteUserParams>,
|
||||
req: HttpRequest,
|
||||
) -> impl Responder {
|
||||
let auth_header = get_auth_header(&req);
|
||||
let params = params.into_inner();
|
||||
|
||||
let auth_token: String;
|
||||
|
||||
match auth_header {
|
||||
Some(token) => auth_token = token.to_string(),
|
||||
None => return HttpResponse::Unauthorized().finish(),
|
||||
};
|
||||
|
||||
let auth = verify_user_auth(&data, &auth_token, ¶ms.user_token, schemas::TablePermission::User, false).await;
|
||||
|
||||
// SQL Queries
|
||||
// TODO: Optimize by utilizing some SQL magic, for now this has to do
|
||||
if auth {
|
||||
match sqlx::query!(
|
||||
"DELETE FROM public.users WHERE token = $1",
|
||||
params.user_token
|
||||
)
|
||||
.execute(&data.db)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
return HttpResponse::InternalServerError().finish();
|
||||
}
|
||||
};
|
||||
|
||||
return HttpResponse::Ok().into();
|
||||
} else {
|
||||
return HttpResponse::Unauthorized().finish();
|
||||
}
|
||||
}
|
65
src/v1/schemas.rs
Normal file
65
src/v1/schemas.rs
Normal file
|
@ -0,0 +1,65 @@
|
|||
use std::collections::HashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
use crate::v3::schemas::ChellarisGameLegacy;
|
||||
|
||||
// DB Permission Enums
|
||||
|
||||
pub enum TablePermission {
|
||||
Game,
|
||||
Empire,
|
||||
Data,
|
||||
User
|
||||
}
|
||||
|
||||
// User Structs
|
||||
|
||||
#[derive(Serialize, ToSchema, Debug)]
|
||||
pub struct User {
|
||||
#[schema(example = "abcdef")]
|
||||
pub user_token: String,
|
||||
#[schema(example = "discorduser")]
|
||||
pub discord_handle: Option<String>,
|
||||
#[schema(example = "/assets/avatars/124677612.png")]
|
||||
pub profile_picture: Option<String>,
|
||||
#[schema(example = "\
|
||||
{\
|
||||
[\"game_permissions\"]: true,
|
||||
[\"empire_permissions\"]: true,
|
||||
[\"data_permissions\"]: false,
|
||||
[\"user_permissions\"]: false,
|
||||
}\
|
||||
")]
|
||||
pub permissions: HashMap<String, bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||
pub struct GetUserParams {
|
||||
#[schema(example = "abcdef")]
|
||||
pub user_token: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||
pub struct UpdateUserParams {
|
||||
#[schema(example = "abcdef")]
|
||||
pub user_token: String,
|
||||
#[schema(example = "discorduser")]
|
||||
pub discord_handle: Option<String>,
|
||||
#[schema(example = "/assets/avatars/124677612.png")]
|
||||
pub profile_picture: Option<String>,
|
||||
#[schema(example = "\
|
||||
{\
|
||||
[\"game_permissions\"]: true,
|
||||
[\"empire_permissions\"]: true,
|
||||
[\"data_permissions\"]: false,
|
||||
[\"user_permissions\"]: false,
|
||||
}\
|
||||
")]
|
||||
pub permissions: HashMap<String, bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||
pub struct DeleteUserParams {
|
||||
#[schema(example = "abcdef")]
|
||||
pub user_token: String,
|
||||
}
|
Reference in a new issue