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