326 lines
12 KiB
Rust
326 lines
12 KiB
Rust
use actix_cors::Cors;
|
|
use actix_files as fs;
|
|
use actix_identity::{CookieIdentityPolicy, Identity, IdentityService, RequestIdentity};
|
|
use actix_service::{Service, Transform};
|
|
use actix_web::{
|
|
dev::{ServiceRequest, ServiceResponse},
|
|
http::{header, StatusCode},
|
|
middleware, web, App, Error, HttpResponse, HttpServer,
|
|
};
|
|
use futures::{
|
|
future::{ok, Either, FutureResult},
|
|
Future,
|
|
};
|
|
use serde_json;
|
|
use std::env;
|
|
|
|
use crate::api;
|
|
use lootalot_db as db;
|
|
|
|
type AppPool = web::Data<db::Pool>;
|
|
type PlayerId = web::Path<i32>;
|
|
type ItemId = web::Json<i32>;
|
|
type IdList = web::Json<api::IdList>;
|
|
type BuySellParams = web::Json<api::BuySellParams>;
|
|
type NewGroupLoot = web::Json<api::NewGroupLoot>;
|
|
|
|
type MaybeForbidden =
|
|
actix_web::Either<Box<dyn Future<Item = HttpResponse, Error = Error>>, HttpResponse>;
|
|
|
|
/// Wraps call to the database query and convert its result as a async HttpResponse
|
|
fn db_call(
|
|
pool: AppPool,
|
|
query: api::ApiEndpoint,
|
|
) -> impl Future<Item = HttpResponse, Error = Error> {
|
|
let conn = pool.get().unwrap();
|
|
web::block(move || api::execute(&conn, query)).then(|res| match res {
|
|
Ok(r) => HttpResponse::Ok().json(r),
|
|
Err(e) => {
|
|
dbg!(&e);
|
|
HttpResponse::InternalServerError().finish()
|
|
}
|
|
})
|
|
}
|
|
|
|
fn restricted_to_group(id: i32, params: (AppPool, api::ApiEndpoint)) -> MaybeForbidden {
|
|
if id != 0 {
|
|
actix_web::Either::B(HttpResponse::Forbidden().finish())
|
|
} else {
|
|
actix_web::Either::A(Box::new(db_call(params.0, params.1)))
|
|
}
|
|
}
|
|
|
|
struct RestrictedAccess;
|
|
|
|
impl<S, B> Transform<S> for RestrictedAccess
|
|
where
|
|
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
|
|
S::Future: 'static,
|
|
{
|
|
type Request = ServiceRequest;
|
|
type Response = ServiceResponse<B>;
|
|
type Error = Error;
|
|
type InitError = ();
|
|
type Transform = RestrictedAccessMiddleware<S>;
|
|
type Future = FutureResult<Self::Transform, Self::InitError>;
|
|
|
|
fn new_transform(&self, service: S) -> Self::Future {
|
|
ok(RestrictedAccessMiddleware { service })
|
|
}
|
|
}
|
|
|
|
struct RestrictedAccessMiddleware<S> {
|
|
service: S,
|
|
}
|
|
|
|
impl<S, B> Service for RestrictedAccessMiddleware<S>
|
|
where
|
|
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
|
|
S::Future: 'static,
|
|
{
|
|
type Request = ServiceRequest;
|
|
type Response = ServiceResponse<B>;
|
|
type Error = Error;
|
|
type Future = Either<S::Future, FutureResult<Self::Response, Self::Error>>;
|
|
|
|
fn poll_ready(&mut self) -> futures::Poll<(), Self::Error> {
|
|
self.service.poll_ready()
|
|
}
|
|
|
|
fn call(&mut self, req: ServiceRequest) -> Self::Future {
|
|
let is_logged_in = req.get_identity().is_some();
|
|
|
|
if is_logged_in {
|
|
Either::A(self.service.call(req))
|
|
} else {
|
|
Either::B(ok(
|
|
req.into_response(HttpResponse::Forbidden().finish().into_body())
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn configure_api(config: &mut web::ServiceConfig) {
|
|
use api::ApiEndpoint as Q;
|
|
config.service(
|
|
web::scope("/api")
|
|
.wrap(RestrictedAccess)
|
|
.service(
|
|
web::scope("/players")
|
|
.service(
|
|
web::resource("/")
|
|
.route(web::get().to_async(|pool| db_call(pool, Q::PlayerList)))
|
|
.route(web::post().to_async(
|
|
|pool, player: web::Json<api::NewPlayer>| {
|
|
db_call(pool, Q::PlayerAdd(player.into_inner()))
|
|
},
|
|
)),
|
|
) // List of players
|
|
.service(
|
|
web::scope("/{player_id}")
|
|
/*.route(
|
|
"/",
|
|
web::get().to_async(|pool, player: PlayerId| {
|
|
db_call(pool, Q::FetchPlayer(*player))
|
|
}),
|
|
)*/
|
|
.route(
|
|
"/notifications",
|
|
web::get().to_async(|pool, player: PlayerId| {
|
|
db_call(pool, Q::PlayerNotifications(*player))
|
|
}),
|
|
)
|
|
.service(
|
|
web::resource("/claims")
|
|
.route(web::get().to_async(|pool, player: PlayerId| {
|
|
db_call(pool, Q::PlayerClaims(*player))
|
|
}))
|
|
.route(web::post().to_async(
|
|
|pool, (player, data): (PlayerId, IdList)| {
|
|
db_call(pool, Q::ClaimItems(*player, data.into_inner()))
|
|
},
|
|
)),
|
|
)
|
|
.service(
|
|
web::resource("/wealth")
|
|
//.route(web::get().to_async(...))
|
|
.route(web::put().to_async(
|
|
|pool, (player, data): (PlayerId, web::Json<f64>)| {
|
|
db_call(pool, Q::PlayerUpdateWealth(*player, *data))
|
|
},
|
|
)),
|
|
)
|
|
.service(
|
|
web::resource("/loot")
|
|
.route(web::get().to_async(|pool, player: PlayerId| {
|
|
db_call(pool, Q::PlayerLoot(*player))
|
|
}))
|
|
.route(web::put().to_async(
|
|
move |pool, (player, data): (PlayerId, BuySellParams)| {
|
|
db_call(pool, Q::BuyItems(*player, data.into_inner()))
|
|
},
|
|
))
|
|
.route(web::post().to(
|
|
move |pool, (player, data): (PlayerId, NewGroupLoot)| {
|
|
restricted_to_group(
|
|
*player,
|
|
(pool, Q::AddLoot(data.into_inner())),
|
|
)
|
|
},
|
|
))
|
|
.route(web::delete().to_async(
|
|
move |pool, (player, data): (PlayerId, BuySellParams)| {
|
|
db_call(pool, Q::SellItems(*player, data.into_inner()))
|
|
},
|
|
)),
|
|
)
|
|
.service(web::scope("/events").route(
|
|
"/last",
|
|
web::delete().to_async(|pool, player: PlayerId| {
|
|
db_call(pool, Q::UndoLastAction(*player))
|
|
}),
|
|
)),
|
|
),
|
|
)
|
|
.route(
|
|
"/claims",
|
|
web::get().to_async(|pool| db_call(pool, Q::ClaimsList)),
|
|
)
|
|
.service(
|
|
web::resource("/shop")
|
|
.route(web::get().to_async(|pool| db_call(pool, Q::ShopList)))
|
|
.route(
|
|
web::post().to_async(|pool, items: web::Json<api::ItemList>| {
|
|
db_call(pool, Q::RefreshShop(items.into_inner()))
|
|
}),
|
|
),
|
|
)
|
|
.service(
|
|
web::resource("/items")
|
|
.route(
|
|
web::get().to_async(move |pool: AppPool| db_call(pool, Q::InventoryList)),
|
|
)
|
|
.route(web::post().to_async(
|
|
move |pool: AppPool, items: web::Json<Vec<String>>| {
|
|
db_call(pool, Q::InventoryCheck(items.into_inner()))
|
|
},
|
|
)),
|
|
),
|
|
);
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct AuthRequest {
|
|
key: String,
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
|
|
enum SessionKind {
|
|
Player(i32),
|
|
Admin,
|
|
}
|
|
|
|
use std::collections::HashMap;
|
|
|
|
fn check_key(key: &str, db: HashMap<&str, SessionKind>) -> Option<SessionKind> {
|
|
db.get(&key).map(Clone::clone)
|
|
}
|
|
|
|
fn login(id: Identity, key: web::Query<AuthRequest>) -> HttpResponse {
|
|
if let Some(session_kind) = check_key(
|
|
&key.key.to_string(),
|
|
[
|
|
("0", SessionKind::Player(0)),
|
|
("1", SessionKind::Player(1)),
|
|
("2", SessionKind::Player(2)),
|
|
("admin", SessionKind::Admin),
|
|
]
|
|
.iter()
|
|
.cloned()
|
|
.collect::<HashMap<&str, SessionKind>>(),
|
|
) {
|
|
id.remember(serde_json::to_string(&session_kind).expect("Serialize SessionKind error"));
|
|
HttpResponse::build(StatusCode::TEMPORARY_REDIRECT)
|
|
.header(header::LOCATION, "/")
|
|
.finish()
|
|
} else {
|
|
HttpResponse::Forbidden().finish()
|
|
}
|
|
}
|
|
|
|
fn logout(id: Identity) -> HttpResponse {
|
|
id.forget();
|
|
HttpResponse::build(StatusCode::TEMPORARY_REDIRECT)
|
|
.header(header::LOCATION, "/")
|
|
.finish()
|
|
}
|
|
|
|
/// This endpoint shall be called by client,
|
|
/// at initialization, to retrieve the current
|
|
/// logging session info.
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// The player data if a player is logged in
|
|
/// The admin data if the admin is logged in
|
|
/// A Forbidden response otherwise
|
|
fn enter_session(id: Identity, pool: AppPool) -> impl Future<Item = HttpResponse, Error = Error> {
|
|
let conn = pool.get().unwrap();
|
|
let logged: SessionKind = id
|
|
.identity()
|
|
.map(|s| serde_json::from_str(&s).expect("Deserialize SessionKind error"))
|
|
// This will fail, fastest way to handle
|
|
// unlogged case with web::block below
|
|
.unwrap_or(SessionKind::Player(-1));
|
|
|
|
web::block(move || {
|
|
api::execute(
|
|
&conn,
|
|
match logged {
|
|
SessionKind::Player(id) => api::ApiEndpoint::PlayerFetch(id),
|
|
SessionKind::Admin => api::ApiEndpoint::PlayerList,
|
|
},
|
|
)
|
|
})
|
|
.then(|res| match res {
|
|
Ok(r) => HttpResponse::Ok().json(r.value),
|
|
Err(e) => {
|
|
dbg!(&e);
|
|
HttpResponse::Forbidden().finish()
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn serve() -> std::io::Result<()> {
|
|
let domain: String = env::var("DOMAIN").expect("DOMAIN must be set");
|
|
let pool = db::create_pool();
|
|
println!("Serving Loot-a-lot on {}", domain);
|
|
|
|
let key = [0; 32]; // TODO: Use a real key
|
|
|
|
HttpServer::new(move || {
|
|
App::new()
|
|
.data(pool.clone())
|
|
.configure(configure_api)
|
|
.wrap(
|
|
Cors::new()
|
|
.allowed_origin(&domain)
|
|
.allowed_methods(vec!["GET", "POST", "PUT", "DELETE", "OPTIONS"])
|
|
.max_age(3600),
|
|
)
|
|
.wrap(IdentityService::new(
|
|
CookieIdentityPolicy::new(&key)
|
|
.name("logged-in")
|
|
.secure(false),
|
|
))
|
|
//.wrap(middleware::Logger::default())
|
|
.wrap(middleware::Logger::new("%r -> %s (%{User-Agent}i)"))
|
|
.route("/session", web::get().to_async(enter_session))
|
|
.route("/login", web::get().to(login))
|
|
.route("/logout", web::get().to(logout))
|
|
//.service(fs::Files::new("/", www_root.clone()).index_file("index.html"))
|
|
})
|
|
.bind("127.0.0.1:8088")?
|
|
.run()
|
|
}
|