subs
This commit is contained in:
parent
e6486dc361
commit
4921b0760c
@ -293,7 +293,7 @@ button[disabled] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
HEADER
|
account
|
||||||
*/
|
*/
|
||||||
|
|
||||||
header {
|
header {
|
||||||
@ -303,23 +303,26 @@ header {
|
|||||||
margin-bottom: 1.5em;
|
margin-bottom: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-title {
|
.account {
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-title {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-status {
|
.account-status {
|
||||||
margin: 1em 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-username {
|
.account-header {
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-status svg {
|
.account-status svg {
|
||||||
margin: 0.5em 0 0 1em;
|
margin: 0.5em 0 0 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
|
|||||||
@ -32,15 +32,20 @@ function BitsBtn(args) {
|
|||||||
clientReferenceId: account.id
|
clientReferenceId: account.id
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return (
|
|
||||||
<div>
|
const subscription = account.subscribed
|
||||||
<div id="error-message"></div>
|
? <h3 class="account-header">Subscribed</h3>
|
||||||
<button
|
: <button
|
||||||
onClick={subscribeClick}
|
onClick={subscribeClick}
|
||||||
class="stripe-btn"
|
class="stripe-btn"
|
||||||
role="link">
|
role="link">
|
||||||
Subscribe
|
Subscribe
|
||||||
</button>
|
</button>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div id="error-message"></div>
|
||||||
|
{subscription}
|
||||||
<button
|
<button
|
||||||
onClick={bitsClick}
|
onClick={bitsClick}
|
||||||
class="stripe-btn"
|
class="stripe-btn"
|
||||||
@ -84,12 +89,13 @@ function AccountStatus(args) {
|
|||||||
if (!account) return null;
|
if (!account) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div class="account">
|
||||||
<div class="header-status">
|
<div class="account-status">
|
||||||
<h2 class="header-username">{account.name}</h2>
|
<h2 class="account-header">{account.name}</h2>
|
||||||
{saw(pingColour(ping))}
|
{saw(pingColour(ping))}
|
||||||
<div class="ping-text">{ping}ms</div>
|
<div class="ping-text">{ping}ms</div>
|
||||||
</div>
|
</div>
|
||||||
|
<h3 class="account-header">{`¤${account.credits}`}</h3>
|
||||||
<Elements>
|
<Elements>
|
||||||
<StripeBitsBtn account={account} />
|
<StripeBitsBtn account={account} />
|
||||||
</Elements>
|
</Elements>
|
||||||
|
|||||||
@ -78,6 +78,7 @@ class Login extends Component {
|
|||||||
class="login-input"
|
class="login-input"
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="username"
|
placeholder="username"
|
||||||
|
tabIndex={1}
|
||||||
value={this.state.name}
|
value={this.state.name}
|
||||||
onInput={this.nameInput}
|
onInput={this.nameInput}
|
||||||
/>
|
/>
|
||||||
@ -85,6 +86,7 @@ class Login extends Component {
|
|||||||
class="login-input"
|
class="login-input"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="password"
|
placeholder="password"
|
||||||
|
tabIndex={2}
|
||||||
value={this.state.password}
|
value={this.state.password}
|
||||||
onInput={this.passwordInput}
|
onInput={this.passwordInput}
|
||||||
/>
|
/>
|
||||||
@ -92,16 +94,19 @@ class Login extends Component {
|
|||||||
class="login-input"
|
class="login-input"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="code"
|
placeholder="code"
|
||||||
|
tabIndex={3}
|
||||||
value={this.state.code}
|
value={this.state.code}
|
||||||
onInput={this.codeInput}
|
onInput={this.codeInput}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
class="login-btn"
|
class="login-btn"
|
||||||
|
tabIndex={4}
|
||||||
onClick={this.loginSubmit}>
|
onClick={this.loginSubmit}>
|
||||||
Login
|
Login
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="login-btn"
|
class="login-btn"
|
||||||
|
tabIndex={5}
|
||||||
onClick={this.registerSubmit}>
|
onClick={this.registerSubmit}>
|
||||||
Register
|
Register
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -2,14 +2,29 @@ exports.up = async knex => {
|
|||||||
return knex.schema.createTable('accounts', table => {
|
return knex.schema.createTable('accounts', table => {
|
||||||
table.uuid('id').primary();
|
table.uuid('id').primary();
|
||||||
table.timestamps(true, true);
|
table.timestamps(true, true);
|
||||||
|
|
||||||
table.string('name', 42).notNullable().unique();
|
table.string('name', 42).notNullable().unique();
|
||||||
table.string('password').notNullable();
|
table.string('password').notNullable();
|
||||||
|
|
||||||
table.string('token', 64).notNullable();
|
table.string('token', 64).notNullable();
|
||||||
table.timestamp('token_expiry').notNullable();
|
table.timestamp('token_expiry').notNullable();
|
||||||
|
|
||||||
|
table.bigInteger('credits')
|
||||||
|
.defaultTo(0)
|
||||||
|
.notNullable();
|
||||||
|
|
||||||
|
table.bool('subscribed')
|
||||||
|
.defaultTo(false)
|
||||||
|
.notNullable();
|
||||||
|
|
||||||
table.index('name');
|
table.index('name');
|
||||||
table.index('id');
|
table.index('id');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await knex.schema.raw(`
|
||||||
|
ALTER TABLE accounts
|
||||||
|
ADD CHECK (credits > 0);
|
||||||
|
`);
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.down = async () => {};
|
exports.down = async () => {};
|
||||||
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
exports.up = async knex => {
|
exports.up = async knex => {
|
||||||
await knex.schema.createTable('stripe_customers', table => {
|
await knex.schema.createTable('stripe_customers', table => {
|
||||||
table.string('customer', 64)
|
table.string('customer', 128)
|
||||||
.primary();
|
.primary();
|
||||||
|
|
||||||
table.uuid('account')
|
table.uuid('account')
|
||||||
@ -16,7 +16,7 @@ exports.up = async knex => {
|
|||||||
.inTable('accounts')
|
.inTable('accounts')
|
||||||
.onDelete('RESTRICT');
|
.onDelete('RESTRICT');
|
||||||
|
|
||||||
table.string('checkout', 64)
|
table.string('checkout', 128)
|
||||||
.notNullable()
|
.notNullable()
|
||||||
.unique();
|
.unique();
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ exports.up = async knex => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await knex.schema.createTable('stripe_subscriptions', table => {
|
await knex.schema.createTable('stripe_subscriptions', table => {
|
||||||
table.string('subscription', 64)
|
table.string('subscription', 128)
|
||||||
.primary();
|
.primary();
|
||||||
|
|
||||||
table.uuid('account')
|
table.uuid('account')
|
||||||
@ -36,14 +36,17 @@ exports.up = async knex => {
|
|||||||
.inTable('accounts')
|
.inTable('accounts')
|
||||||
.onDelete('RESTRICT');
|
.onDelete('RESTRICT');
|
||||||
|
|
||||||
table.string('customer', 64)
|
table.string('customer', 128)
|
||||||
|
.notNullable();
|
||||||
|
|
||||||
|
table.string('checkout', 128)
|
||||||
.notNullable();
|
.notNullable();
|
||||||
|
|
||||||
table.timestamps(true, true);
|
table.timestamps(true, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
await knex.schema.createTable('stripe_purchases', table => {
|
await knex.schema.createTable('stripe_purchases', table => {
|
||||||
table.string('checkout', 64)
|
table.string('checkout', 128)
|
||||||
.primary();
|
.primary();
|
||||||
|
|
||||||
table.uuid('account')
|
table.uuid('account')
|
||||||
@ -55,7 +58,7 @@ exports.up = async knex => {
|
|||||||
.inTable('accounts')
|
.inTable('accounts')
|
||||||
.onDelete('RESTRICT');
|
.onDelete('RESTRICT');
|
||||||
|
|
||||||
table.string('customer', 64)
|
table.string('customer', 128)
|
||||||
.notNullable();
|
.notNullable();
|
||||||
|
|
||||||
table.bigInteger('amount')
|
table.bigInteger('amount')
|
||||||
|
|||||||
@ -31,4 +31,9 @@ actix-web = "1.0.0"
|
|||||||
actix-web-actors = "1.0.0"
|
actix-web-actors = "1.0.0"
|
||||||
actix-cors = "0.1.0"
|
actix-cors = "0.1.0"
|
||||||
|
|
||||||
stripe-rust = { version = "0.10", features = ["webhooks"] }
|
stripe-rust = { version = "0.10.4", features = ["webhooks"] }
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
# stripe-rust = { git = "https://github.com/margh/stripe-rs.git" }
|
||||||
|
|
||||||
|
stripe-rust = { path = "/home/ntr/code/stripe-rs" }
|
||||||
|
|||||||
@ -21,15 +21,15 @@ pub struct Account {
|
|||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub credits: u32,
|
pub credits: u32,
|
||||||
|
pub subscribed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Account {
|
impl Account {
|
||||||
pub fn select(tx: &mut Transaction, id: Uuid) -> Result<Account, Error> {
|
pub fn select(tx: &mut Transaction, id: Uuid) -> Result<Account, Error> {
|
||||||
let query = "
|
let query = "
|
||||||
SELECT id, name, credits
|
SELECT id, name, credits, subscribed
|
||||||
FROM accounts
|
FROM accounts
|
||||||
WHERE id = $1
|
WHERE id = $1;
|
||||||
FOR UPDATE;
|
|
||||||
";
|
";
|
||||||
|
|
||||||
let result = tx
|
let result = tx
|
||||||
@ -38,17 +38,18 @@ impl Account {
|
|||||||
let row = result.iter().next()
|
let row = result.iter().next()
|
||||||
.ok_or(format_err!("account not found {:?}", id))?;
|
.ok_or(format_err!("account not found {:?}", id))?;
|
||||||
|
|
||||||
let db_credits: i32 = row.get(2);
|
let db_credits: i64 = row.get(2);
|
||||||
|
|
||||||
let credits = u32::try_from(db_credits)
|
let credits = u32::try_from(db_credits)
|
||||||
.or(Err(format_err!("user {:?} has unparsable balance {:?}", id, db_credits)))?;
|
.or(Err(format_err!("user {:?} has unparsable balance {:?}", id, db_credits)))?;
|
||||||
|
|
||||||
Ok(Account { id, name: row.get(1), credits })
|
let subscribed: bool = row.get(3);
|
||||||
|
|
||||||
|
Ok(Account { id, name: row.get(1), credits, subscribed })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_token(tx: &mut Transaction, token: String) -> Result<Account, Error> {
|
pub fn from_token(tx: &mut Transaction, token: String) -> Result<Account, Error> {
|
||||||
let query = "
|
let query = "
|
||||||
SELECT id, name, credits
|
SELECT id, name, subscribed, credits
|
||||||
FROM accounts
|
FROM accounts
|
||||||
WHERE token = $1
|
WHERE token = $1
|
||||||
AND token_expiry > now();
|
AND token_expiry > now();
|
||||||
@ -61,17 +62,19 @@ impl Account {
|
|||||||
.ok_or(err_msg("invalid token"))?;
|
.ok_or(err_msg("invalid token"))?;
|
||||||
|
|
||||||
let id: Uuid = row.get(0);
|
let id: Uuid = row.get(0);
|
||||||
let db_credits: i32 = row.get(2);
|
let name: String = row.get(1);
|
||||||
|
let subscribed: bool = row.get(2);
|
||||||
|
let db_credits: i64 = row.get(3);
|
||||||
|
|
||||||
let credits = u32::try_from(db_credits)
|
let credits = u32::try_from(db_credits)
|
||||||
.or(Err(format_err!("user {:?} has unparsable balance {:?}", id, db_credits)))?;
|
.or(Err(format_err!("user {:?} has unparsable balance {:?}", id, db_credits)))?;
|
||||||
|
|
||||||
Ok(Account { id, name: row.get(1), credits })
|
Ok(Account { id, name, credits, subscribed })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn login(tx: &mut Transaction, name: &String, password: &String) -> Result<Account, Error> {
|
pub fn login(tx: &mut Transaction, name: &String, password: &String) -> Result<Account, Error> {
|
||||||
let query = "
|
let query = "
|
||||||
SELECT id, password, name, credits
|
SELECT id, password, name, credits, subscribed
|
||||||
FROM accounts
|
FROM accounts
|
||||||
WHERE name = $1
|
WHERE name = $1
|
||||||
";
|
";
|
||||||
@ -97,7 +100,8 @@ impl Account {
|
|||||||
let id: Uuid = row.get(0);
|
let id: Uuid = row.get(0);
|
||||||
let hash: String = row.get(1);
|
let hash: String = row.get(1);
|
||||||
let name: String = row.get(2);
|
let name: String = row.get(2);
|
||||||
let db_credits: i32 = row.get(3);
|
let db_credits: i64 = row.get(3);
|
||||||
|
let subscribed: bool = row.get(4);
|
||||||
|
|
||||||
if !verify(password, &hash)? {
|
if !verify(password, &hash)? {
|
||||||
return Err(err_msg("password does not match"));
|
return Err(err_msg("password does not match"));
|
||||||
@ -106,10 +110,10 @@ impl Account {
|
|||||||
let credits = u32::try_from(db_credits)
|
let credits = u32::try_from(db_credits)
|
||||||
.or(Err(format_err!("user {:?} has unparsable balance {:?}", id, db_credits)))?;
|
.or(Err(format_err!("user {:?} has unparsable balance {:?}", id, db_credits)))?;
|
||||||
|
|
||||||
Ok(Account { id, name, credits })
|
Ok(Account { id, name, credits, subscribed })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_token(&self, tx: &mut Transaction) -> Result<String, Error> {
|
pub fn new_token(tx: &mut Transaction, id: Uuid) -> Result<String, Error> {
|
||||||
let mut rng = thread_rng();
|
let mut rng = thread_rng();
|
||||||
let token: String = iter::repeat(())
|
let token: String = iter::repeat(())
|
||||||
.map(|()| rng.sample(Alphanumeric))
|
.map(|()| rng.sample(Alphanumeric))
|
||||||
@ -125,26 +129,67 @@ impl Account {
|
|||||||
";
|
";
|
||||||
|
|
||||||
let result = tx
|
let result = tx
|
||||||
.query(query, &[&token, &self.id])?;
|
.query(query, &[&token, &id])?;
|
||||||
|
|
||||||
let _row = result.iter().next()
|
let row = result.iter().next()
|
||||||
.ok_or(format_err!("account not updated {:?}", self.id))?;
|
.ok_or(format_err!("account not updated {:?}", id))?;
|
||||||
|
|
||||||
info!("login account={:?}", self.name);
|
let name: String = row.get(1);
|
||||||
|
|
||||||
|
info!("login account={:?}", name);
|
||||||
|
|
||||||
Ok(token)
|
Ok(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
pub fn add_credits(tx: &mut Transaction, id: Uuid, credits: i64) -> Result<String, Error> {
|
||||||
|
let query = "
|
||||||
|
UPDATE accounts
|
||||||
|
SET credits = credits + $1
|
||||||
|
WHERE id = $2
|
||||||
|
RETURNING credits, name;
|
||||||
|
";
|
||||||
|
|
||||||
#[derive(Debug,Clone,Serialize,Deserialize)]
|
let result = tx
|
||||||
struct AccountEntry {
|
.query(query, &[&credits, &id])?;
|
||||||
id: Uuid,
|
|
||||||
name: String,
|
|
||||||
password: String,
|
|
||||||
token: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
let row = result.iter().next()
|
||||||
|
.ok_or(format_err!("account not updated {:?}", id))?;
|
||||||
|
|
||||||
|
println!("{:?}", row);
|
||||||
|
|
||||||
|
let db_credits: i64 = row.get(0);
|
||||||
|
let total = u32::try_from(db_credits)
|
||||||
|
.or(Err(format_err!("user {:?} has unparsable balance {:?}", id, db_credits)))?;
|
||||||
|
|
||||||
|
let name: String = row.get(1);
|
||||||
|
|
||||||
|
info!("account credited name={:?} credited={:?} total={:?}", name, credits, total);
|
||||||
|
|
||||||
|
Ok(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_subscribed(tx: &mut Transaction, id: Uuid, subscribed: bool) -> Result<String, Error> {
|
||||||
|
let query = "
|
||||||
|
UPDATE accounts
|
||||||
|
SET subscribed = $1
|
||||||
|
WHERE id = $2
|
||||||
|
RETURNING name;
|
||||||
|
";
|
||||||
|
|
||||||
|
let result = tx
|
||||||
|
.query(query, &[&subscribed, &id])?;
|
||||||
|
|
||||||
|
let row = result.iter().next()
|
||||||
|
.ok_or(format_err!("account not updated {:?}", id))?;
|
||||||
|
|
||||||
|
let name: String = row.get(0);
|
||||||
|
|
||||||
|
info!("account subscription status updated name={:?} subscribed={:?}", name, subscribed);
|
||||||
|
|
||||||
|
Ok(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
pub fn account_create(name: &String, password: &String, code: &String, tx: &mut Transaction) -> Result<String, Error> {
|
pub fn account_create(name: &String, password: &String, code: &String, tx: &mut Transaction) -> Result<String, Error> {
|
||||||
if password.len() < PASSWORD_MIN_LEN {
|
if password.len() < PASSWORD_MIN_LEN {
|
||||||
|
|||||||
137
server/src/mtx.rs
Normal file
137
server/src/mtx.rs
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
use uuid::Uuid;
|
||||||
|
// use rand::prelude::*;
|
||||||
|
|
||||||
|
use serde_cbor::{from_slice};
|
||||||
|
use postgres::transaction::Transaction;
|
||||||
|
|
||||||
|
use failure::Error;
|
||||||
|
use failure::err_msg;
|
||||||
|
|
||||||
|
#[derive(Debug,Copy,Clone,Serialize,Deserialize)]
|
||||||
|
pub enum MtxVariant {
|
||||||
|
ArchitectureMolecular,
|
||||||
|
ArchitectureInvader,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MtxVariant {
|
||||||
|
fn new(self, account: Uuid) -> Mtx {
|
||||||
|
match self {
|
||||||
|
MtxVariant::ArchitectureInvader => Mtx { id: Uuid::new_v4(), account, variant: self },
|
||||||
|
MtxVariant::ArchitectureMolecular => Mtx { id: Uuid::new_v4(), account, variant: self },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug,Copy,Clone,Serialize,Deserialize)]
|
||||||
|
pub struct Mtx {
|
||||||
|
id: Uuid,
|
||||||
|
account: Uuid,
|
||||||
|
variant: MtxVariant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mtx {
|
||||||
|
pub fn account_list(tx: &mut Transaction, account: Uuid) -> Result<Vec<Mtx>, Error> {
|
||||||
|
let query = "
|
||||||
|
SELECT data, id
|
||||||
|
FROM mtx
|
||||||
|
WHERE account = $1
|
||||||
|
FOR UPDATE;
|
||||||
|
";
|
||||||
|
|
||||||
|
let result = tx
|
||||||
|
.query(query, &[&account])?;
|
||||||
|
|
||||||
|
let values = result.into_iter().filter_map(|row| {
|
||||||
|
let bytes: Vec<u8> = row.get(0);
|
||||||
|
// let id: Uuid = row.get(1);
|
||||||
|
|
||||||
|
match from_slice::<Mtx>(&bytes) {
|
||||||
|
Ok(i) => Some(i),
|
||||||
|
Err(e) => {
|
||||||
|
warn!("{:?}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).collect::<Vec<Mtx>>();
|
||||||
|
|
||||||
|
return Ok(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete(tx: &mut Transaction, id: Uuid) -> Result<(), Error> {
|
||||||
|
let query = "
|
||||||
|
DELETE
|
||||||
|
FROM mtx
|
||||||
|
WHERE id = $1;
|
||||||
|
";
|
||||||
|
|
||||||
|
let result = tx
|
||||||
|
.execute(query, &[&id])?;
|
||||||
|
|
||||||
|
if result != 1 {
|
||||||
|
return Err(format_err!("unable to delete mtx {:?}", id));
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("mtx deleted {:?}", id);
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(&self, tx: &mut Transaction) -> Result<&Mtx, Error> {
|
||||||
|
let query = "
|
||||||
|
INSERT INTO mtx (id, account, variant)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
RETURNING id, account;
|
||||||
|
";
|
||||||
|
|
||||||
|
let result = tx
|
||||||
|
.query(query, &[&self.id, &self.account, &format!("{:?}", self.variant)])?;
|
||||||
|
|
||||||
|
result.iter().next().ok_or(err_msg("mtx not written"))?;
|
||||||
|
|
||||||
|
info!("wrote mtx {:?}", self);
|
||||||
|
|
||||||
|
return Ok(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
// pub fn update(&self, tx: &mut Transaction) -> Result<&Mtx, Error> {
|
||||||
|
// let query = "
|
||||||
|
// UPDATE mtx
|
||||||
|
// SET data = $1, updated_at = now()
|
||||||
|
// WHERE id = $2
|
||||||
|
// RETURNING id, data;
|
||||||
|
// ";
|
||||||
|
|
||||||
|
// let result = tx
|
||||||
|
// .query(query, &[&self.id, &to_vec(self)?])?;
|
||||||
|
|
||||||
|
// if let None = result.iter().next() {
|
||||||
|
// return Err(err_msg("mtx not written"));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// info!("wrote mtx {:?}", self);
|
||||||
|
|
||||||
|
// return Ok(self);
|
||||||
|
// }
|
||||||
|
|
||||||
|
pub fn select(tx: &mut Transaction, id: Uuid, account: Uuid) -> Result<Option<Mtx>, Error> {
|
||||||
|
let query = "
|
||||||
|
SELECT data, id
|
||||||
|
FROM mtx
|
||||||
|
WHERE account = $1
|
||||||
|
AND id = $2
|
||||||
|
FOR UPDATE;
|
||||||
|
";
|
||||||
|
|
||||||
|
let result = tx
|
||||||
|
.query(query, &[&account, &id])?;
|
||||||
|
|
||||||
|
if let Some(row) = result.iter().next() {
|
||||||
|
let bytes: Vec<u8> = row.get(0);
|
||||||
|
Ok(Some(from_slice::<Mtx>(&bytes)?))
|
||||||
|
} else {
|
||||||
|
Err(format_err!("mtx not found {:?}", id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// actual impl
|
||||||
|
}
|
||||||
@ -84,7 +84,7 @@ fn login(state: web::Data<State>, params: web::Json::<AccountLoginParams>) -> Re
|
|||||||
|
|
||||||
match Account::login(&mut tx, ¶ms.name, ¶ms.password) {
|
match Account::login(&mut tx, ¶ms.name, ¶ms.password) {
|
||||||
Ok(a) => {
|
Ok(a) => {
|
||||||
let token = a.new_token(&mut tx).or(Err(MnmlHttpError::ServerError))?;
|
let token = Account::new_token(&mut tx, a.id).or(Err(MnmlHttpError::ServerError))?;
|
||||||
tx.commit().or(Err(MnmlHttpError::ServerError))?;
|
tx.commit().or(Err(MnmlHttpError::ServerError))?;
|
||||||
Ok(login_res(token))
|
Ok(login_res(token))
|
||||||
},
|
},
|
||||||
@ -102,7 +102,7 @@ fn logout(r: HttpRequest, state: web::Data<State>) -> Result<HttpResponse, MnmlH
|
|||||||
let mut tx = db.transaction().or(Err(MnmlHttpError::ServerError))?;
|
let mut tx = db.transaction().or(Err(MnmlHttpError::ServerError))?;
|
||||||
match Account::from_token(&mut tx, t.value().to_string()) {
|
match Account::from_token(&mut tx, t.value().to_string()) {
|
||||||
Ok(a) => {
|
Ok(a) => {
|
||||||
a.new_token(&mut tx).or(Err(MnmlHttpError::Unauthorized))?;
|
Account::new_token(&mut tx, a.id).or(Err(MnmlHttpError::Unauthorized))?;
|
||||||
tx.commit().or(Err(MnmlHttpError::ServerError))?;
|
tx.commit().or(Err(MnmlHttpError::ServerError))?;
|
||||||
return Ok(logout_res());
|
return Ok(logout_res());
|
||||||
},
|
},
|
||||||
|
|||||||
@ -5,14 +5,32 @@ use postgres::transaction::Transaction;
|
|||||||
use failure::Error;
|
use failure::Error;
|
||||||
use failure::err_msg;
|
use failure::err_msg;
|
||||||
|
|
||||||
use stripe::{Event, EventObject, CheckoutSession};
|
use stripe::{Event, EventObject, CheckoutSession, SubscriptionStatus};
|
||||||
|
|
||||||
use net::{State, PgPool, MnmlHttpError};
|
use net::{State, PgPool, MnmlHttpError};
|
||||||
use account::{Account};
|
use account::{Account};
|
||||||
|
|
||||||
|
pub fn subscription_account(tx: &mut Transaction, sub: String) -> Result<Uuid, Error> {
|
||||||
|
let query = "
|
||||||
|
SELECT account
|
||||||
|
FROM stripe_subscriptions
|
||||||
|
WHERE subscription = $1;
|
||||||
|
";
|
||||||
|
|
||||||
|
let result = tx
|
||||||
|
.query(query, &[&sub])?;
|
||||||
|
|
||||||
|
let row = result.iter().next()
|
||||||
|
.ok_or(err_msg("user not subscribed"))?;
|
||||||
|
|
||||||
|
Ok(row.get(0))
|
||||||
|
}
|
||||||
|
|
||||||
// we use i64 because it is converted to BIGINT for pg
|
// we use i64 because it is converted to BIGINT for pg
|
||||||
const CREDITS_COST_CENTS: i64 = 20;
|
// and we can losslessly pull it into u32 which is big
|
||||||
const CREDITS_SUB_BONUS: i64 = 25;
|
// enough for the ballers
|
||||||
|
const CREDITS_COST_CENTS: i64 = 10;
|
||||||
|
const CREDITS_SUB_BONUS: i64 = 40;
|
||||||
|
|
||||||
// Because the client_reference_id (account.id) is only included
|
// Because the client_reference_id (account.id) is only included
|
||||||
// in the stripe CheckoutSession object
|
// in the stripe CheckoutSession object
|
||||||
@ -60,14 +78,25 @@ impl StripeData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_credits(&self, tx: &mut Transaction) -> Result<&StripeData, Error> {
|
fn side_effects(&self, tx: &mut Transaction) -> Result<&StripeData, Error> {
|
||||||
match self {
|
match self {
|
||||||
|
// when we get a subscription we just immediately set the user to be subbed
|
||||||
|
// so we don't have to deal with going to fetch all the details from
|
||||||
|
// stripe just to double check
|
||||||
|
// update webhooks will tell us when the subscription changes
|
||||||
|
// see EventObject::Subscription handler below
|
||||||
StripeData::Subscription { subscription: _, account, customer: _, checkout: _ } => {
|
StripeData::Subscription { subscription: _, account, customer: _, checkout: _ } => {
|
||||||
let account = Account::select(tx, *account)?;
|
Account::add_credits(tx, *account, CREDITS_SUB_BONUS)?;
|
||||||
|
Account::set_subscribed(tx, *account, true)?;
|
||||||
Ok(self)
|
Ok(self)
|
||||||
},
|
},
|
||||||
StripeData::Purchase { account: _, customer: _, amount, checkout: _ } => {
|
StripeData::Purchase { account, customer: _, amount, checkout: _ } => {
|
||||||
amount.checked_div(CREDITS_COST_CENTS).expect("credits cost 0");
|
let credits = amount
|
||||||
|
.checked_div(CREDITS_COST_CENTS)
|
||||||
|
.expect("credits cost 0");
|
||||||
|
|
||||||
|
Account::add_credits(tx, *account, credits)?;
|
||||||
|
|
||||||
Ok(self)
|
Ok(self)
|
||||||
},
|
},
|
||||||
_ => Ok(self),
|
_ => Ok(self),
|
||||||
@ -108,8 +137,11 @@ fn stripe_checkout_data(session: CheckoutSession) -> Result<Vec<StripeData>, Err
|
|||||||
return Ok(items);
|
return Ok(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_stripe(event: Event, pool: &PgPool) -> Result<Vec<StripeData>, Error> {
|
fn process_stripe_event(event: Event, pool: &PgPool) -> Result<String, Error> {
|
||||||
info!("stripe event {:?}", event);
|
info!("stripe event {:?}", event);
|
||||||
|
let connection = pool.get()?;
|
||||||
|
let mut tx = connection.transaction()?;
|
||||||
|
|
||||||
match event.data.object {
|
match event.data.object {
|
||||||
EventObject::CheckoutSession(s) => {
|
EventObject::CheckoutSession(s) => {
|
||||||
let data = match stripe_checkout_data(s) {
|
let data = match stripe_checkout_data(s) {
|
||||||
@ -120,28 +152,50 @@ fn process_stripe(event: Event, pool: &PgPool) -> Result<Vec<StripeData>, Error>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let connection = pool.get()?;
|
|
||||||
let mut tx = connection.transaction()?;
|
|
||||||
|
|
||||||
for item in data.iter() {
|
for item in data.iter() {
|
||||||
item.insert(&mut tx)?;
|
item.insert(&mut tx)?;
|
||||||
item.add_credits(&mut tx)?;
|
item.side_effects(&mut tx)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
tx.commit()?;
|
|
||||||
Ok(data)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// we only receive the cancelled and updated events
|
||||||
|
// because the checkout object is needed to link
|
||||||
|
// a sub to an account initially and
|
||||||
|
// stripe doesn't guarantee the order
|
||||||
|
// so this just checks if the sub is still active
|
||||||
|
EventObject::Subscription(s) => {
|
||||||
|
let account = subscription_account(&mut tx, s.id.to_string())?;
|
||||||
|
|
||||||
|
let subbed = match s.status {
|
||||||
|
SubscriptionStatus::Active => true,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
Account::set_subscribed(&mut tx, account, subbed)?;
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
error!("unhandled stripe event {:?}", event);
|
error!("unhandled stripe event {:?}", event);
|
||||||
Err(err_msg("UnhanldedEvent"))
|
return Err(err_msg("UnhanldedEvent"));
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
|
tx.commit()?;
|
||||||
|
|
||||||
|
Ok(event.id.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn post_stripe_event(state: web::Data<State>, body: web::Json::<Event>) -> Result<HttpResponse, MnmlHttpError> {
|
pub fn post_stripe_event(state: web::Data<State>, body: web::Json::<Event>) -> Result<HttpResponse, MnmlHttpError> {
|
||||||
let event: Event = body.into_inner();
|
let event: Event = body.into_inner();
|
||||||
process_stripe(event, &state.pool).or(Err(MnmlHttpError::ServerError))?;
|
match process_stripe_event(event, &state.pool) {
|
||||||
Ok(HttpResponse::Accepted().finish())
|
Ok(id)=> {
|
||||||
|
info!("event processed successfully {:?}", id);
|
||||||
|
Ok(HttpResponse::Ok().finish())
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("{:?}", e);
|
||||||
|
Err(MnmlHttpError::ServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user