mahjong_service/http_server/
mod.rs

1use 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(&params.player_id.clone().unwrap(), &params.token))
85        || (params.player_id.is_none() && !auth_handler.verify_admin_token(&params.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(&params.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    // If deleting for normal users, should check if any active running at the moment by using
114    // the web socket
115
116    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}