mahjong_service/http_server/
mod.rsuse self::user::get_user_scope;
use crate::auth::{
AuthHandler, AuthInfoData, GetAuthInfo, GithubAuth, GithubCallbackQuery, UnauthorizedError,
};
use crate::common::Storage;
use crate::env::ENV_FRONTEND_URL;
use crate::games_loop::GamesLoop;
use crate::http_server::admin::get_admin_scope;
pub use crate::http_server::base::{DataSocketServer, DataStorage, GamesManager};
use crate::service_error::{ResponseCommon, ServiceError};
use crate::socket::{MahjongWebsocketServer, MahjongWebsocketSession};
use actix::prelude::*;
use actix_cors::Cors;
use actix_files::{Files, NamedFile};
use actix_web::{
dev::{fn_service, ServiceRequest, ServiceResponse},
get,
http::StatusCode,
post, web, App, Error, HttpRequest, HttpResponse, HttpServer, Responder,
};
use actix_web_actors::ws;
use mahjong_core::deck::DEFAULT_DECK;
use serde::{Deserialize, Serialize};
use service_contracts::{GetDeckResponse, WebSocketQuery};
use std::sync::{Arc, Mutex};
use std::time::Instant;
use tracing::warn;
mod admin;
mod base;
mod user;
#[get("/api/health")]
async fn get_health() -> impl Responder {
HttpResponse::Ok().body("OK")
}
#[get("/api/v1/deck")]
async fn get_deck() -> ResponseCommon {
let response = GetDeckResponse(DEFAULT_DECK.clone().0);
Ok(HttpResponse::Ok().json(response))
}
#[get("/api/v1/github_callback")]
async fn github_callback(req: HttpRequest, storage: DataStorage) -> Result<impl Responder, Error> {
let query = web::Query::<GithubCallbackQuery>::from_query(req.query_string());
if query.is_err() {
return Ok(HttpResponse::BadRequest().json("Invalid query"));
}
let query = query.unwrap();
let mut auth_handler = AuthHandler::new(&storage, &req);
let result = GithubAuth::handle_callback(query, &storage, &mut auth_handler)
.await
.ok_or(ServiceError::Custom("Error handling callback"))?;
let response_qs =
serde_qs::to_string(&result).map_err(|_| ServiceError::Custom("Error parsing response"))?;
let frontend_url = std::env::var(ENV_FRONTEND_URL).unwrap();
let redirect = HttpResponse::Found()
.append_header(("Location", format!("{}?{}", frontend_url, response_qs)))
.finish();
Ok(redirect)
}
#[get("/api/v1/ws")]
async fn get_ws(
req: HttpRequest,
stream: web::Payload,
srv: DataSocketServer,
storage: DataStorage,
) -> Result<impl Responder, Error> {
let params = web::Query::<WebSocketQuery>::from_query(req.query_string())
.map_err(|_| ServiceError::Custom("Invalid query parameters"))?;
let auth_handler = AuthHandler::new(&storage, &req);
if (params.player_id.is_some()
&& !auth_handler.verify_user_token(¶ms.player_id.clone().unwrap(), ¶ms.token))
|| (params.player_id.is_none() && !auth_handler.verify_admin_token(¶ms.token))
{
return Ok(AuthHandler::get_unauthorized());
}
let addr = loop {
if let Ok(srv) = srv.lock() {
break srv.clone();
}
std::thread::sleep(std::time::Duration::from_millis(1));
};
ws::start(
MahjongWebsocketSession {
addr,
hb: Instant::now(),
id: rand::random(),
room: MahjongWebsocketSession::get_room_id(¶ms.game_id, params.player_id.as_ref()),
},
&req,
stream,
)
}
#[post("/api/v1/test/delete-games")]
async fn test_post_delete_games(req: HttpRequest, storage: DataStorage) -> ResponseCommon {
let user_id = AuthHandler::new(&storage, &req).get_user_from_token()?;
let auth_info = storage
.get_auth_info(GetAuthInfo::PlayerId(user_id.clone()))
.await
.map_err(|_| UnauthorizedError)?
.ok_or(UnauthorizedError)?;
if let AuthInfoData::Email(auth_info_email) = auth_info.data {
if auth_info_email.username != "test" {
return Ok(HttpResponse::Unauthorized().finish());
}
} else {
return Ok(HttpResponse::Unauthorized().finish());
}
let games = storage.get_player_games(&Some(user_id.clone())).await;
if games.is_err() {
return Ok(HttpResponse::InternalServerError().finish());
}
let games = games.unwrap();
let games_ids: Vec<_> = games.iter().map(|g| g.id.clone()).collect();
storage
.delete_games(&games_ids)
.await
.map_err(|_| ServiceError::Custom("Error deleting games"))?;
#[derive(Deserialize, Serialize)]
struct DeleteGames {
test_delete_games: bool,
}
Ok(HttpResponse::Ok().json({
DeleteGames {
test_delete_games: true,
}
}))
}
pub async fn start_server(storage: Box<dyn Storage>) -> std::io::Result<()> {
let port = 3000;
let address = "0.0.0.0";
warn!("Starting the Mahjong HTTP server on http://{address}:{port}");
let games_manager = GamesManager::default();
let games_manager_arc = Arc::new(Mutex::new(games_manager));
let loop_games_manager_arc = games_manager_arc.clone();
let storage_arc = Arc::new(storage);
let loop_storage_arc = storage_arc.clone();
let socket_server = Arc::new(Mutex::new(MahjongWebsocketServer::default().start()));
let loop_socket_server = socket_server.clone();
GamesLoop::new(loop_storage_arc, loop_socket_server, loop_games_manager_arc).run();
HttpServer::new(move || {
let storage_data: DataStorage = web::Data::new(storage_arc.clone());
let games_manager_data = web::Data::new(games_manager_arc.clone());
let cors = Cors::permissive();
let endpoints_server = socket_server.clone();
let user_scope = get_user_scope();
let admin_scope = get_admin_scope();
let static_files = Files::new("/", "./static")
.index_file("index.html")
.redirect_to_slash_directory()
.default_handler(fn_service(|req: ServiceRequest| async {
let (req, _) = req.into_parts();
let file = NamedFile::open_async("./static/404/index.html").await?;
let res = file
.customize()
.with_status(StatusCode::NOT_FOUND)
.respond_to(&req)
.map_into_boxed_body();
Ok(ServiceResponse::new(req, res))
}));
App::new()
.app_data(games_manager_data)
.app_data(storage_data)
.app_data(web::Data::new(endpoints_server))
.service(admin_scope)
.service(get_deck)
.service(get_health)
.service(get_ws)
.service(github_callback)
.service(test_post_delete_games)
.service(user_scope)
.service(static_files)
.wrap(cors)
})
.bind((address, port))?
.run()
.await
}