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, ¶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 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, ¶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; } 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, ¶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(_) => { return HttpResponse::InternalServerError().finish(); } }; return HttpResponse::Ok().into(); } else { return HttpResponse::Unauthorized().finish(); } }