This commit is contained in:
ntr 2019-06-17 19:46:35 +10:00
parent 6f7c2cf571
commit e74c820ce4
13 changed files with 339 additions and 163 deletions

View File

@ -463,6 +463,7 @@ header {
}
.stripe-btn {
width: 100%;
padding: 0 0.5em;
background: whitesmoke;
color: black;

View File

@ -11,12 +11,16 @@ function pingColour(ping) {
}
function BitsBtn(args) {
const { stripe } = args;
const {
stripe,
account,
} = args;
function onClick(e) {
stripe.redirectToCheckout({
items: [{sku: 'sku_FGjPl1YKoT241P', quantity: 1}],
items: [{plan: 'plan_FGmRwawcOJJ7Nv', quantity: 1}],
successUrl: 'http://localhost:40080/payments/success',
cancelUrl: 'http://localhost:40080/payments/cancel',
clientReferenceId: account.id
});
}
return (
@ -27,7 +31,7 @@ function BitsBtn(args) {
class="stripe-btn"
id="checkout-button-sku_FGjPl1YKoT241P"
role="link">
Buy Bits
Subscribe
</button>
</div>
);
@ -72,7 +76,7 @@ function AccountStatus(args) {
<div class="ping-text">{ping}ms</div>
</div>
<Elements>
<StripeBitsBtn />
<StripeBitsBtn account={account} />
</Elements>
<button onClick={() => logout()}>Logout</button>
</div>

View File

@ -1,7 +1,7 @@
const toast = require('izitoast');
const cbor = require('borc');
const SOCKET_URL = process.env.NODE_ENV === 'production' ? 'wss://mnml.gg/ws' : 'ws://localhost:40000/ws/';
const SOCKET_URL = process.env.NODE_ENV === 'production' ? 'wss://mnml.gg/ws' : 'ws://localhost:40000/api/ws';
function errorToast(err) {
console.error(err);

View File

@ -0,0 +1,49 @@
upstream mnml_dev {
server 0.0.0.0:41337;
}
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# DEV
server {
root /home/git/mnml/client/dist/;
index index.html;
server_name dev.mnml.gg; # managed by Certbot
location / {
try_files $uri $uri/ =404;
}
listen [::]:443;
ssl on;
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/dev.mnml.gg/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/dev.mnml.gg/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
location /api/ws {
proxy_pass http://mnml_dev;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 600s;
}
location /api/ {
proxy_pass http://mnml_dev;
proxy_read_timeout 600s;
}
}
# http -> https
server {
server_name dev.mnml.gg;
return 301 https://$host$request_uri;
}

View File

@ -7,6 +7,7 @@ map $http_upgrade $connection_upgrade {
'' close;
}
# PRODUCTION
server {
root /home/git/mnml/client/dist/;
index index.html;

View File

@ -1,2 +1 @@
DATABASE_URL=postgres://mnml:craftbeer@localhost/mnml
DEV_CORS=true

View File

@ -31,3 +31,4 @@ actix-web-actors = "1.0.0"
futures = "0.1"
bytes = "0.4"
stripe-rust = "0.10.2"

View File

@ -21,23 +21,27 @@ extern crate serde_cbor;
extern crate fern;
#[macro_use] extern crate log;
extern crate stripe;
mod account;
mod construct;
mod effect;
mod game;
mod instance;
mod item;
mod mob;
mod mtx;
mod names;
mod net;
mod player;
mod rpc;
mod pubsub;
mod rpc;
mod skill;
mod effect;
mod spec;
mod util;
mod vbox;
mod warden;
mod ws;
use dotenv::dotenv;
use net::{start};

15
server/src/mtx.rs Normal file
View File

@ -0,0 +1,15 @@
use actix_web::{web, HttpResponse};
use stripe::{CheckoutSession};
use net::{State, MnmlError};
pub fn stripe_payment(state: web::Data<State>, body: web::Json::<CheckoutSession>) -> Result<HttpResponse, MnmlError> {
let db = state.pool.get().or(Err(MnmlError::ServerError))?;
let mut tx = db.transaction().or(Err(MnmlError::ServerError))?;
let session = body.into_inner();
info!("{:?}", session);
Ok(HttpResponse::Ok().finish())
}

View File

@ -1,129 +1,31 @@
use std::time::{Instant, Duration};
use std::env;
use serde_cbor::{to_vec};
use actix_web::{middleware, web, App, Error, HttpMessage, HttpRequest, HttpResponse, HttpServer};
use actix_web::{middleware, web, App, HttpMessage, HttpRequest, HttpResponse, HttpServer};
use actix_web::middleware::cors::Cors;
use actix_web::error::ResponseError;
use actix_web::http::{Cookie};
use actix_web::cookie::{SameSite};
use actix_web_actors::ws;
use actix::prelude::*;
use r2d2::{Pool};
use r2d2::{PooledConnection};
use r2d2_postgres::{TlsMode, PostgresConnectionManager};
use rpc::{receive, RpcResult, RpcErrorResponse, AccountLoginParams, AccountCreateParams};
use rpc::{RpcErrorResponse, AccountLoginParams, AccountCreateParams};
use warden::{Warden};
use pubsub::PubSub;
use account::{Account, account_login, account_create, account_from_token, account_set_token};
use ws::{connect};
use account::{account_login, account_create, account_from_token, account_set_token};
use mtx::{stripe_payment};
pub type Db = PooledConnection<PostgresConnectionManager>;
pub type PgPool = Pool<PostgresConnectionManager>;
const DB_POOL_SIZE: u32 = 20;
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
pub struct MnmlSocket {
hb: Instant,
pool: PgPool,
account: Option<Account>,
}
impl Actor for MnmlSocket {
type Context = ws::WebsocketContext<Self>;
// once the actor has been started this fn runs
// it starts the heartbeat interval and keepalive
fn started(&mut self, ctx: &mut Self::Context) {
self.hb(ctx);
}
}
/// Handler for `ws::Message`
impl StreamHandler<ws::Message, ws::ProtocolError> for MnmlSocket {
fn started(&mut self, ctx: &mut Self::Context) {
match self.account.as_ref() {
Some(a) => {
info!("user connected {:?}", a);
let account_state = to_vec(&RpcResult::AccountState(a.clone()))
.expect("could not serialize account state");
ctx.binary(account_state)
},
None => info!("new connection"),
}
}
fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) {
// process websocket messages
let begin = Instant::now();
debug!("msg: {:?}", msg);
match msg {
ws::Message::Ping(msg) => {
self.hb = Instant::now();
ctx.pong(&msg);
}
ws::Message::Pong(_) => {
self.hb = Instant::now();
}
ws::Message::Text(_text) => (),
ws::Message::Close(_) => {
match self.account.as_ref() {
Some(a) => info!("disconnected {:?}", a),
None => info!("disconnected"),
}
ctx.stop();
}
ws::Message::Nop => (),
ws::Message::Binary(bin) => {
let db_connection = self.pool.get().expect("unable to get db connection");
match receive(bin.to_vec(), &db_connection, ctx, begin, self.account.as_ref()) {
Ok(reply) => {
let response = to_vec(&reply)
.expect("failed to serialize response");
ctx.binary(response);
},
Err(e) => {
let response = to_vec(&RpcErrorResponse { err: e.to_string() })
.expect("failed to serialize error response");
ctx.binary(response);
}
}
}
}
}
}
impl MnmlSocket {
fn new(state: web::Data<State>, account: Option<Account>) -> MnmlSocket {
// idk why this has to be cloned again
// i guess because each socket is added as a new thread?
MnmlSocket { hb: Instant::now(), pool: state.pool.clone(), account }
}
// starts the keepalive interval once actor started
fn hb(&self, ctx: &mut <MnmlSocket as Actor>::Context) {
ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT {
info!("idle connection terminated");
// stop actor
ctx.stop();
// don't try to send a ping
return;
}
ctx.ping("");
});
}
}
#[derive(Fail, Debug)]
enum MnmlError {
pub enum MnmlError {
#[fail(display="internal server error")]
ServerError,
#[fail(display="unauthorized")]
@ -153,28 +55,6 @@ impl ResponseError for MnmlError {
}
}
// idk how this stuff works
// but the args extract what you need from the incoming requests
// this grabs
// the req obj itself which we need for cookies
// the application state
// and the websocket stream
fn connect(r: HttpRequest, state: web::Data<State>, stream: web::Payload) -> Result<HttpResponse, Error> {
let account: Option<Account> = match r.cookie("x-auth-token") {
Some(t) => {
let db = state.pool.get().or(Err(MnmlError::ServerError))?;
let mut tx = db.transaction().or(Err(MnmlError::ServerError))?;
match account_from_token(t.value().to_string(), &mut tx) {
Ok(a) => Some(a),
Err(_) => None,
}
},
None => None,
};
ws::start(MnmlSocket::new(state, account), &r, stream)
}
fn login_res(token: String, secure: bool) -> HttpResponse {
HttpResponse::Ok()
.cookie(Cookie::build("x-auth-token", token)
@ -257,8 +137,8 @@ fn create_pool(url: String) -> Pool<PostgresConnectionManager> {
.expect("Failed to create pool.")
}
struct State {
pool: PgPool,
pub struct State {
pub pool: PgPool,
secure: bool,
}
@ -274,29 +154,18 @@ pub fn start() {
let pubsub_conn = pool.get().expect("could not get pubsub pg connection");
let pubsub_addr = Supervisor::start(move |_| PubSub::new(pubsub_conn));
match env::var("DEV_CORS") {
Ok(_) => {
warn!("enabling dev CORS middleware");
HttpServer::new(move || App::new()
.data(State { pool: pool.clone(), secure: false })
.wrap(middleware::Logger::default())
.wrap(Cors::new().supports_credentials())
.service(web::resource("/login").route(web::post().to(login)))
.service(web::resource("/logout").route(web::post().to(logout)))
.service(web::resource("/register").route(web::post().to(register)))
.service(web::resource("/ws/").route(web::get().to(connect))))
.bind("127.0.0.1:40000").expect("could not bind to port")
.run().expect("could not start http server")
},
Err(_) =>
HttpServer::new(move || App::new()
.data(State { pool: pool.clone(), secure: true })
.wrap(middleware::Logger::default())
.service(web::resource("/login").route(web::post().to(login)))
.service(web::resource("/logout").route(web::post().to(logout)))
.service(web::resource("/register").route(web::post().to(register)))
.service(web::resource("/ws/").route(web::get().to(connect))))
.bind("127.0.0.1:40000").expect("could not bind to port")
.run().expect("could not start http server"),
};
HttpServer::new(move || App::new()
.data(State { pool: pool.clone(), secure: false })
.wrap(middleware::Logger::default())
.wrap(Cors::new().supports_credentials())
.service(web::resource("/api/login").route(web::post().to(login)))
.service(web::resource("/api/logout").route(web::post().to(logout)))
.service(web::resource("/api/register").route(web::post().to(register)))
.service(web::resource("/api/payments/stripe").route(web::post().to(stripe_payment)))
.service(web::resource("/api/payments/crypto").route(web::post().to(stripe_payment)))
.service(web::resource("/api/ws/").route(web::get().to(connect))))
.bind("127.0.0.1:40000").expect("could not bind to port")
.run().expect("could not start http server");
}

View File

@ -8,7 +8,8 @@ use uuid::Uuid;
use failure::Error;
use failure::err_msg;
use net::{Db, MnmlSocket};
use net::{Db};
use ws::{MnmlSocket};
use construct::{Construct, construct_spawn, construct_delete};
use game::{Game, game_state, game_skill, game_ready};
use account::{Account, account_constructs, account_instances};

131
server/src/ws.rs Normal file
View File

@ -0,0 +1,131 @@
use std::time::{Instant, Duration};
use actix_web::{web, Error, HttpMessage, HttpRequest, HttpResponse};
use actix_web_actors::ws;
use actix::prelude::*;
use account::{Account, account_from_token};
use serde_cbor::{to_vec};
use net::{PgPool, MnmlError, State};
use rpc::{receive, RpcResult, RpcErrorResponse};
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
pub struct MnmlSocket {
hb: Instant,
pool: PgPool,
account: Option<Account>,
}
impl Actor for MnmlSocket {
type Context = ws::WebsocketContext<Self>;
// once the actor has been started this fn runs
// it starts the heartbeat interval and keepalive
fn started(&mut self, ctx: &mut Self::Context) {
self.hb(ctx);
}
}
/// Handler for `ws::Message`
impl StreamHandler<ws::Message, ws::ProtocolError> for MnmlSocket {
fn started(&mut self, ctx: &mut Self::Context) {
match self.account.as_ref() {
Some(a) => {
info!("user connected {:?}", a);
let account_state = to_vec(&RpcResult::AccountState(a.clone()))
.expect("could not serialize account state");
ctx.binary(account_state)
},
None => info!("new connection"),
}
}
fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) {
// process websocket messages
let begin = Instant::now();
debug!("msg: {:?}", msg);
match msg {
ws::Message::Ping(msg) => {
self.hb = Instant::now();
ctx.pong(&msg);
}
ws::Message::Pong(_) => {
self.hb = Instant::now();
}
ws::Message::Text(_text) => (),
ws::Message::Close(_) => {
match self.account.as_ref() {
Some(a) => info!("disconnected {:?}", a),
None => info!("disconnected"),
}
ctx.stop();
}
ws::Message::Nop => (),
ws::Message::Binary(bin) => {
let db_connection = self.pool.get().expect("unable to get db connection");
match receive(bin.to_vec(), &db_connection, ctx, begin, self.account.as_ref()) {
Ok(reply) => {
let response = to_vec(&reply)
.expect("failed to serialize response");
ctx.binary(response);
},
Err(e) => {
let response = to_vec(&RpcErrorResponse { err: e.to_string() })
.expect("failed to serialize error response");
ctx.binary(response);
}
}
}
}
}
}
impl MnmlSocket {
fn new(state: web::Data<State>, account: Option<Account>) -> MnmlSocket {
// idk why this has to be cloned again
// i guess because each socket is added as a new thread?
MnmlSocket { hb: Instant::now(), pool: state.pool.clone(), account }
}
// starts the keepalive interval once actor started
fn hb(&self, ctx: &mut <MnmlSocket as Actor>::Context) {
ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT {
info!("idle connection terminated");
// stop actor
ctx.stop();
// don't try to send a ping
return;
}
ctx.ping("");
});
}
}
// idk how this stuff works
// but the args extract what you need from the incoming requests
// this grabs
// the req obj itself which we need for cookies
// the application state
// and the websocket stream
pub fn connect(r: HttpRequest, state: web::Data<State>, stream: web::Payload) -> Result<HttpResponse, Error> {
let account: Option<Account> = match r.cookie("x-auth-token") {
Some(t) => {
let db = state.pool.get().or(Err(MnmlError::ServerError))?;
let mut tx = db.transaction().or(Err(MnmlError::ServerError))?;
match account_from_token(t.value().to_string(), &mut tx) {
Ok(a) => Some(a),
Err(_) => None,
}
},
None => None,
};
ws::start(MnmlSocket::new(state, account), &r, stream)
}

View File

@ -0,0 +1,101 @@
{
"object": {
"id": "sub_FGmgBxTVX2xM7H",
"object": "subscription",
"application_fee_percent": null,
"billing": "charge_automatically",
"billing_cycle_anchor": 1560755931,
"billing_thresholds": null,
"cancel_at": null,
"cancel_at_period_end": false,
"canceled_at": null,
"collection_method": "charge_automatically",
"created": 1560755931,
"current_period_end": 1563347931,
"current_period_start": 1560755931,
"customer": "cus_FGmgIHDimD5Tei",
"days_until_due": null,
"default_payment_method": "pm_1EmF78Gf8y65MteUQBHQU1po",
"default_source": null,
"default_tax_rates": [
],
"discount": null,
"ended_at": null,
"items": {
"object": "list",
"data": [
{
"id": "si_FGmg6F7DZAyU2Y",
"object": "subscription_item",
"billing_thresholds": null,
"created": 1560755932,
"metadata": {
},
"plan": {
"id": "plan_FGmRwawcOJJ7Nv",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": 1000,
"billing_scheme": "per_unit",
"created": 1560755040,
"currency": "aud",
"interval": "month",
"interval_count": 1,
"livemode": false,
"metadata": {
},
"nickname": "basic",
"product": "prod_FGmRFYmB700pM5",
"tiers": null,
"tiers_mode": null,
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"quantity": 1,
"subscription": "sub_FGmgBxTVX2xM7H",
"tax_rates": [
]
}
],
"has_more": false,
"total_count": 1,
"url": "/v1/subscription_items?subscription=sub_FGmgBxTVX2xM7H"
},
"latest_invoice": "in_1EmF7AGf8y65MteUvH8MCela",
"livemode": false,
"metadata": {
},
"plan": {
"id": "plan_FGmRwawcOJJ7Nv",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": 1000,
"billing_scheme": "per_unit",
"created": 1560755040,
"currency": "aud",
"interval": "month",
"interval_count": 1,
"livemode": false,
"metadata": {
},
"nickname": "basic",
"product": "prod_FGmRFYmB700pM5",
"tiers": null,
"tiers_mode": null,
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"quantity": 1,
"schedule": null,
"start": 1560755931,
"start_date": 1560755931,
"status": "active",
"tax_percent": null,
"trial_end": null,
"trial_start": null
}
}