mahjong_service/http_server/
mod.rs1use self::user::get_user_scope;
2use crate::auth::{
3 AuthHandler, AuthInfoData, GetAuthInfo, GithubAuth, GithubCallbackQuery, UnauthorizedError,
4};
5use crate::common::Storage;
6use crate::env::ENV_FRONTEND_URL;
7use crate::games_loop::GamesLoop;
8use crate::http_server::admin::get_admin_scope;
9pub use crate::http_server::base::{DataSocketServer, DataStorage, GamesManager};
10use crate::service_error::{ResponseCommon, ServiceError};
11use crate::socket::{MahjongWebsocketServer, MahjongWebsocketSession};
12use actix::prelude::*;
13use actix_cors::Cors;
14use actix_files::{Files, NamedFile};
15use actix_web::{
16 dev::{fn_service, ServiceRequest, ServiceResponse},
17 get,
18 http::StatusCode,
19 post, web, App, Error, HttpRequest, HttpResponse, HttpServer, Responder,
20};
21use actix_web_actors::ws;
22use mahjong_core::deck::DEFAULT_DECK;
23use serde::{Deserialize, Serialize};
24use service_contracts::{GetDeckResponse, WebSocketQuery};
25use std::sync::{Arc, Mutex};
26use std::time::Instant;
27use tracing::warn;
28
29mod admin;
30mod base;
31mod user;
32
33#[get("/api/health")]
34async fn get_health() -> impl Responder {
35 HttpResponse::Ok().body("OK")
36}
37
38#[get("/api/v1/deck")]
39async fn get_deck() -> ResponseCommon {
40 let response = GetDeckResponse(DEFAULT_DECK.clone().0);
41
42 Ok(HttpResponse::Ok().json(response))
43}
44
45#[get("/api/v1/github_callback")]
46async fn github_callback(req: HttpRequest, storage: DataStorage) -> Result<impl Responder, Error> {
47 let query = web::Query::<GithubCallbackQuery>::from_query(req.query_string());
48
49 if query.is_err() {
50 return Ok(HttpResponse::BadRequest().json("Invalid query"));
51 }
52
53 let query = query.unwrap();
54
55 let mut auth_handler = AuthHandler::new(&storage, &req);
56 let result = GithubAuth::handle_callback(query, &storage, &mut auth_handler)
57 .await
58 .ok_or(ServiceError::Custom("Error handling callback"))?;
59
60 let response_qs =
61 serde_qs::to_string(&result).map_err(|_| ServiceError::Custom("Error parsing response"))?;
62
63 let frontend_url = std::env::var(ENV_FRONTEND_URL).unwrap();
64 let redirect = HttpResponse::Found()
65 .append_header(("Location", format!("{}?{}", frontend_url, response_qs)))
66 .finish();
67
68 Ok(redirect)
69}
70
71#[get("/api/v1/ws")]
72async fn get_ws(
73 req: HttpRequest,
74 stream: web::Payload,
75 srv: DataSocketServer,
76 storage: DataStorage,
77) -> Result<impl Responder, Error> {
78 let params = web::Query::<WebSocketQuery>::from_query(req.query_string())
79 .map_err(|_| ServiceError::Custom("Invalid query parameters"))?;
80
81 let auth_handler = AuthHandler::new(&storage, &req);
82
83 if (params.player_id.is_some()
84 && !auth_handler.verify_user_token(¶ms.player_id.clone().unwrap(), ¶ms.token))
85 || (params.player_id.is_none() && !auth_handler.verify_admin_token(¶ms.token))
86 {
87 return Ok(AuthHandler::get_unauthorized());
88 }
89
90 let addr = loop {
91 if let Ok(srv) = srv.lock() {
92 break srv.clone();
93 }
94 std::thread::sleep(std::time::Duration::from_millis(1));
95 };
96
97 ws::start(
98 MahjongWebsocketSession {
99 addr,
100 hb: Instant::now(),
101 id: rand::random(),
102 room: MahjongWebsocketSession::get_room_id(¶ms.game_id, params.player_id.as_ref()),
103 },
104 &req,
105 stream,
106 )
107}
108
109#[post("/api/v1/test/delete-games")]
110async fn test_post_delete_games(req: HttpRequest, storage: DataStorage) -> ResponseCommon {
111 let user_id = AuthHandler::new(&storage, &req).get_user_from_token()?;
112
113 let auth_info = storage
117 .get_auth_info(GetAuthInfo::PlayerId(user_id.clone()))
118 .await
119 .map_err(|_| UnauthorizedError)?
120 .ok_or(UnauthorizedError)?;
121
122 if let AuthInfoData::Email(auth_info_email) = auth_info.data {
123 if auth_info_email.username != "test" {
124 return Ok(HttpResponse::Unauthorized().finish());
125 }
126 } else {
127 return Ok(HttpResponse::Unauthorized().finish());
128 }
129
130 let games = storage.get_player_games(&Some(user_id.clone())).await;
131 if games.is_err() {
132 return Ok(HttpResponse::InternalServerError().finish());
133 }
134 let games = games.unwrap();
135 let games_ids: Vec<_> = games.iter().map(|g| g.id.clone()).collect();
136
137 storage
138 .delete_games(&games_ids)
139 .await
140 .map_err(|_| ServiceError::Custom("Error deleting games"))?;
141
142 #[derive(Deserialize, Serialize)]
143 struct DeleteGames {
144 test_delete_games: bool,
145 }
146
147 Ok(HttpResponse::Ok().json({
148 DeleteGames {
149 test_delete_games: true,
150 }
151 }))
152}
153
154pub async fn start_server(storage: Box<dyn Storage>) -> std::io::Result<()> {
155 let port = 3000;
156 let address = "0.0.0.0";
157
158 warn!("Starting the Mahjong HTTP server on http://{address}:{port}");
159
160 let games_manager = GamesManager::default();
161 let games_manager_arc = Arc::new(Mutex::new(games_manager));
162 let loop_games_manager_arc = games_manager_arc.clone();
163 let storage_arc = Arc::new(storage);
164 let loop_storage_arc = storage_arc.clone();
165 let socket_server = Arc::new(Mutex::new(MahjongWebsocketServer::default().start()));
166 let loop_socket_server = socket_server.clone();
167
168 GamesLoop::new(loop_storage_arc, loop_socket_server, loop_games_manager_arc).run();
169
170 HttpServer::new(move || {
171 let storage_data: DataStorage = web::Data::new(storage_arc.clone());
172 let games_manager_data = web::Data::new(games_manager_arc.clone());
173 let cors = Cors::permissive();
174 let endpoints_server = socket_server.clone();
175
176 let user_scope = get_user_scope();
177 let admin_scope = get_admin_scope();
178
179 let static_files = Files::new("/", "./static")
180 .index_file("index.html")
181 .redirect_to_slash_directory()
182 .default_handler(fn_service(|req: ServiceRequest| async {
183 let (req, _) = req.into_parts();
184
185 let file = NamedFile::open_async("./static/404/index.html").await?;
186
187 let res = file
188 .customize()
189 .with_status(StatusCode::NOT_FOUND)
190 .respond_to(&req)
191 .map_into_boxed_body();
192
193 Ok(ServiceResponse::new(req, res))
194 }));
195
196 App::new()
197 .app_data(games_manager_data)
198 .app_data(storage_data)
199 .app_data(web::Data::new(endpoints_server))
200 .service(admin_scope)
201 .service(get_deck)
202 .service(get_health)
203 .service(get_ws)
204 .service(github_callback)
205 .service(test_post_delete_games)
206 .service(user_scope)
207 .service(static_files)
208 .wrap(cors)
209 })
210 .bind((address, port))?
211 .run()
212 .await
213}