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/v1/mod.rs

369 lines
12 KiB
Rust

use crate::{db, AppState};
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(req: &HttpRequest) -> Option<&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 = GetUserParams,
responses(
(status = 200, description = "OK", body = User),
(status = 403, description = "Unauthorized"),
(status = 500, description = "Internal Server Error")
),
security(
("api_key" = [])
),
)]
#[post("/api/v1/user")]
pub(crate) 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, &params.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 user_permissions: HashMap<String, bool> = HashMap::new();
user_permissions.insert("game_permissions".to_string(), user.game_permissions);
user_permissions.insert("empire_permissions".to_string(), user.empire_permissions);
user_permissions.insert("data_permissions".to_string(), user.data_permissions);
user_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: user_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/create")]
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 user_permissions: HashMap<String, bool> = HashMap::new();
match params.permissions {
Some(data) => {user_permissions = data.clone()},
None => {},
}
let mut elevated_auth = false;
if user_permissions.len() != 0 {
if user_permissions["game_permissions"] || user_permissions["empire_permissions"] || user_permissions["data_permissions"] || user_permissions["user_permissions"] {
elevated_auth = true;
}
}
let auth = verify_user_auth(&data, &auth_token, &params.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;
}
if user_permissions.len() != 0 {
for (entry, value) in user_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 user_permissions: HashMap<String, bool> = HashMap::new();
user_permissions.insert("game_permissions".to_string(), user.game_permissions);
user_permissions.insert("empire_permissions".to_string(), user.empire_permissions);
user_permissions.insert("data_permissions".to_string(), user.data_permissions);
user_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: user_permissions
};
return HttpResponse::Ok().json(return_data);
} else {
return HttpResponse::Unauthorized().finish();
}
}
#[utoipa::path(
request_body = 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, &params.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(_) => {
return HttpResponse::InternalServerError().finish();
}
};
return HttpResponse::Ok().into();
} else {
return HttpResponse::Unauthorized().finish();
}
}