diff --git a/client/assets/styles/styles.css b/client/assets/styles/styles.css index 41de24c7..63dd89a3 100644 --- a/client/assets/styles/styles.css +++ b/client/assets/styles/styles.css @@ -463,6 +463,7 @@ header { } .stripe-btn { + width: 100%; padding: 0 0.5em; background: whitesmoke; color: black; diff --git a/client/src/components/account.status.jsx b/client/src/components/account.status.jsx index 33092653..5fbe9282 100644 --- a/client/src/components/account.status.jsx +++ b/client/src/components/account.status.jsx @@ -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 ); @@ -72,7 +76,7 @@ function AccountStatus(args) {
{ping}ms
- + diff --git a/client/src/socket.jsx b/client/src/socket.jsx index 919c90cb..2d48179d 100644 --- a/client/src/socket.jsx +++ b/client/src/socket.jsx @@ -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); diff --git a/ops/mnml.gg.DEV.nginx.conf b/ops/mnml.gg.DEV.nginx.conf new file mode 100644 index 00000000..7634a96b --- /dev/null +++ b/ops/mnml.gg.DEV.nginx.conf @@ -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; +} + diff --git a/ops/mnml.gg.nginx.conf b/ops/mnml.gg.PRODUCTION.nginx.conf similarity index 99% rename from ops/mnml.gg.nginx.conf rename to ops/mnml.gg.PRODUCTION.nginx.conf index 55a25a4a..c4da6d7e 100644 --- a/ops/mnml.gg.nginx.conf +++ b/ops/mnml.gg.PRODUCTION.nginx.conf @@ -7,6 +7,7 @@ map $http_upgrade $connection_upgrade { '' close; } +# PRODUCTION server { root /home/git/mnml/client/dist/; index index.html; diff --git a/server/.env b/server/.env index cf48a7c1..c5e1d4fe 100644 --- a/server/.env +++ b/server/.env @@ -1,2 +1 @@ DATABASE_URL=postgres://mnml:craftbeer@localhost/mnml -DEV_CORS=true diff --git a/server/Cargo.toml b/server/Cargo.toml index 26d72aef..2f62374a 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -31,3 +31,4 @@ actix-web-actors = "1.0.0" futures = "0.1" bytes = "0.4" +stripe-rust = "0.10.2" diff --git a/server/src/main.rs b/server/src/main.rs index c408c99c..94f3df8e 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -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}; diff --git a/server/src/mtx.rs b/server/src/mtx.rs new file mode 100644 index 00000000..3d113323 --- /dev/null +++ b/server/src/mtx.rs @@ -0,0 +1,15 @@ +use actix_web::{web, HttpResponse}; +use stripe::{CheckoutSession}; + +use net::{State, MnmlError}; + +pub fn stripe_payment(state: web::Data, body: web::Json::) -> Result { + 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()) +} diff --git a/server/src/net.rs b/server/src/net.rs index a24ff8be..afebdf91 100644 --- a/server/src/net.rs +++ b/server/src/net.rs @@ -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; pub type PgPool = Pool; 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, -} - -impl Actor for MnmlSocket { - type Context = ws::WebsocketContext; - - // 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 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, account: Option) -> 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 ::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, stream: web::Payload) -> Result { - let account: Option = 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 { .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"); } diff --git a/server/src/rpc.rs b/server/src/rpc.rs index ad359bc2..c85d646b 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -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}; diff --git a/server/src/ws.rs b/server/src/ws.rs new file mode 100644 index 00000000..22ab9389 --- /dev/null +++ b/server/src/ws.rs @@ -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, +} + +impl Actor for MnmlSocket { + type Context = ws::WebsocketContext; + + // 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 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, account: Option) -> 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 ::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, stream: web::Payload) -> Result { + let account: Option = 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) +} diff --git a/server/test/subscription.json b/server/test/subscription.json new file mode 100644 index 00000000..495be9bc --- /dev/null +++ b/server/test/subscription.json @@ -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 + } +} \ No newline at end of file