Skip to content
This repository has been archived by the owner on Oct 19, 2024. It is now read-only.

Commit

Permalink
Switch to trolley for payments
Browse files Browse the repository at this point in the history
  • Loading branch information
Geometrically committed Oct 10, 2023
1 parent f6da325 commit 33ea8cd
Show file tree
Hide file tree
Showing 12 changed files with 363 additions and 66 deletions.
11 changes: 7 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ sha1 = { version = "0.6.1", features = ["std"] }
sha2 = "0.9.9"
hmac = "0.11.0"
argon2 = { version = "0.5.0", features = ["std"] }
bitflags = "1.3.2"
bitflags = { version = "2.4.0", features = ["serde"] }
hex = "0.4.3"
zxcvbn = "2.2.2"
totp-rs = { version = "5.0.2", features = ["gen_secret"] }
Expand Down
5 changes: 4 additions & 1 deletion migrations/20230919183129_trolley.sql
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ ALTER TABLE users

ALTER TABLE historical_payouts
ADD COLUMN batch_id text NULL,
ADD COLUMN payment_id text NULL;
ADD COLUMN payment_id text NULL;

UPDATE historical_payouts
SET status = 'processed'
83 changes: 78 additions & 5 deletions src/auth/flows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ use crate::file_hosting::FileHost;
use crate::models::ids::base62_impl::{parse_base62, to_base62};
use crate::models::ids::random_base62_rng;
use crate::models::pats::Scopes;
use crate::models::users::{Badges, Role};
use crate::models::users::{Badges, RecipientStatus, Role};
use crate::parse_strings_from_var;
use crate::queue::payouts::{AccountUser, PayoutsQueue};
use crate::queue::session::AuthQueue;
use crate::queue::socket::ActiveSockets;
use crate::routes::ApiError;
Expand All @@ -29,7 +30,7 @@ use serde::{Deserialize, Serialize};
use sqlx::postgres::PgPool;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio::sync::{Mutex, RwLock};
use validator::Validate;

pub fn config(cfg: &mut ServiceConfig) {
Expand All @@ -50,7 +51,8 @@ pub fn config(cfg: &mut ServiceConfig) {
.service(resend_verify_email)
.service(set_email)
.service(verify_email)
.service(subscribe_newsletter),
.service(subscribe_newsletter)
.service(link_trolley),
);
}

Expand Down Expand Up @@ -224,6 +226,8 @@ impl TempUser {
role: Role::Developer.to_string(),
badges: Badges::default(),
balance: Decimal::ZERO,
trolley_id: None,
trolley_account_status: None,
}
.insert(transaction)
.await?;
Expand Down Expand Up @@ -1009,7 +1013,7 @@ pub async fn auth_callback(

let sockets = active_sockets.clone();
let state = state_string.clone();
let res: Result<HttpResponse, AuthenticationError> = (|| async move {
let res: Result<HttpResponse, AuthenticationError> = async move {

let flow = Flow::get(&state, &redis).await?;

Expand Down Expand Up @@ -1171,7 +1175,7 @@ pub async fn auth_callback(
} else {
Err::<HttpResponse, AuthenticationError>(AuthenticationError::InvalidCredentials)
}
})().await;
}.await;

// Because this is callback route, if we have an error, we need to ensure we close the original socket if it exists
if let Err(ref e) = res {
Expand Down Expand Up @@ -1381,6 +1385,8 @@ pub async fn create_account_with_password(
role: Role::Developer.to_string(),
badges: Badges::default(),
balance: Decimal::ZERO,
trolley_id: None,
trolley_account_status: None,
}
.insert(&mut transaction)
.await?;
Expand Down Expand Up @@ -2004,6 +2010,7 @@ pub async fn set_email(
redis: Data<deadpool_redis::Pool>,
email: web::Json<SetEmail>,
session_queue: Data<AuthQueue>,
payouts_queue: Data<Mutex<PayoutsQueue>>,
) -> Result<HttpResponse, ApiError> {
email
.0
Expand Down Expand Up @@ -2057,6 +2064,15 @@ pub async fn set_email(
"We need to verify your email address.",
)?;

if let Some(payout_data) = user.payout_data {
if let Some(trolley_id) = payout_data.trolley_id {
let queue = payouts_queue.lock().await;
queue
.update_recipient_email(&trolley_id, &email.email)
.await?;
}
}

crate::database::models::User::clear_caches(&[(user.id.into(), None)], &redis).await?;
transaction.commit().await?;

Expand Down Expand Up @@ -2199,3 +2215,60 @@ fn send_email_verify(
Some(("Verify email", &format!("{}/{}?flow={}", dotenvy::var("SITE_URL")?, dotenvy::var("SITE_VERIFY_EMAIL_PATH")?, flow))),
)
}

#[post("trolley/link")]
pub async fn link_trolley(
req: HttpRequest,
pool: Data<PgPool>,
redis: Data<deadpool_redis::Pool>,
session_queue: Data<AuthQueue>,
payouts_queue: Data<Mutex<PayoutsQueue>>,
body: web::Json<AccountUser>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::PAYOUTS_WRITE]),
)
.await?
.1;

if let Some(payout_data) = user.payout_data {
if payout_data.trolley_id.is_some() {
return Err(ApiError::InvalidInput(
"User already has a trolley account.".to_string(),
));
}
}

if let Some(email) = user.email {
let payouts = payouts_queue.lock().await;
let id = payouts.register_recipient(&email, body.0).await?;

let mut transaction = pool.begin().await?;

sqlx::query!(
"
UPDATE users
SET trolley_id = $1, trolley_account_status = $2
WHERE id = $3
",
id,
RecipientStatus::Incomplete.as_str(),
user.id.0 as i64,
)
.execute(&mut transaction)
.await?;

transaction.commit().await?;
crate::database::models::User::clear_caches(&[(user.id.into(), None)], &redis).await?;

Ok(HttpResponse::NoContent().finish())
} else {
Err(ApiError::InvalidInput(
"User needs to have an email set on account.".to_string(),
))
}
}
8 changes: 6 additions & 2 deletions src/auth/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::auth::session::get_session_metadata;
use crate::auth::AuthenticationError;
use crate::database::models::user_item;
use crate::models::pats::Scopes;
use crate::models::users::{Role, User, UserId};
use crate::models::users::{Role, User, UserId, UserPayoutData};
use crate::queue::session::AuthQueue;
use actix_web::HttpRequest;
use chrono::Utc;
Expand Down Expand Up @@ -60,7 +60,11 @@ where
has_password: Some(db_user.password.is_some()),
has_totp: Some(db_user.totp_secret.is_some()),
github_id: None,
payout_data: None,
payout_data: Some(UserPayoutData {
balance: db_user.balance,
trolley_id: db_user.trolley_id,
trolley_status: db_user.trolley_account_status,
}),
};

if let Some(required_scopes) = required_scopes {
Expand Down
12 changes: 10 additions & 2 deletions src/database/models/user_item.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use super::ids::{ProjectId, UserId};
use crate::database::models::DatabaseError;
use crate::models::ids::base62_impl::{parse_base62, to_base62};
use crate::models::users::Badges;
use crate::models::users::{Badges, RecipientStatus};
use chrono::{DateTime, Utc};
use redis::cmd;
use rust_decimal::Decimal;
Expand Down Expand Up @@ -35,7 +35,10 @@ pub struct User {
pub created: DateTime<Utc>,
pub role: String,
pub badges: Badges,

pub balance: Decimal,
pub trolley_id: Option<String>,
pub trolley_account_status: Option<RecipientStatus>,
}

impl User {
Expand Down Expand Up @@ -205,7 +208,7 @@ impl User {
created, role, badges,
balance,
github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,
email_verified, password, totp_secret
email_verified, password, totp_secret, trolley_id, trolley_account_status
FROM users
WHERE id = ANY($1) OR LOWER(username) = ANY($2)
",
Expand Down Expand Up @@ -237,6 +240,11 @@ impl User {
balance: u.balance,
password: u.password,
totp_secret: u.totp_secret,
trolley_id: u.trolley_id,
trolley_account_status: u
.trolley_account_status
.as_ref()
.map(|x| RecipientStatus::from_string(x)),
}))
})
.try_collect::<Vec<User>>()
Expand Down
2 changes: 1 addition & 1 deletion src/models/pats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
pub struct PatId(pub u64);

bitflags::bitflags! {
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Copy, Clone)]
#[serde(transparent)]
pub struct Scopes: u64 {
// read a user's email
Expand Down
2 changes: 1 addition & 1 deletion src/models/teams.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub struct Team {
}

bitflags::bitflags! {
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Copy, Clone)]
#[serde(transparent)]
pub struct Permissions: u64 {
const UPLOAD_VERSION = 1 << 0;
Expand Down
90 changes: 88 additions & 2 deletions src/models/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pub struct UserId(pub u64);
pub const DELETED_USER: UserId = UserId(127155982985829);

bitflags::bitflags! {
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Copy, Clone, Debug)]
#[serde(transparent)]
pub struct Badges: u64 {
// 1 << 0 unused - ignore + replace with something later
Expand Down Expand Up @@ -61,7 +61,7 @@ pub struct User {
pub struct UserPayoutData {
pub balance: Decimal,
pub trolley_id: Option<String>,
pub trolley_status: Option<String>,
pub trolley_status: Option<RecipientStatus>,
}

use crate::database::models::user_item::User as DBUser;
Expand Down Expand Up @@ -132,3 +132,89 @@ impl Role {
}
}
}

#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
#[serde(rename_all = "lowercase")]
pub enum RecipientStatus {
Active,
Incomplete,
Disabled,
Archived,
Suspended,
Blocked,
}

impl RecipientStatus {
pub fn from_string(string: &str) -> RecipientStatus {
match string {
"active" => RecipientStatus::Active,
"incomplete" => RecipientStatus::Incomplete,
"disabled" => RecipientStatus::Disabled,
"archived" => RecipientStatus::Archived,
"suspended" => RecipientStatus::Suspended,
"blocked" => RecipientStatus::Blocked,
_ => RecipientStatus::Disabled,
}
}

pub fn as_str(&self) -> &'static str {
match self {
RecipientStatus::Active => "active",
RecipientStatus::Incomplete => "incomplete",
RecipientStatus::Disabled => "disabled",
RecipientStatus::Archived => "archived",
RecipientStatus::Suspended => "suspended",
RecipientStatus::Blocked => "blocked",
}
}
}

#[derive(Serialize)]
pub struct Payout {
pub created: DateTime<Utc>,
pub amount: Decimal,
pub status: PayoutStatus,
}

#[derive(Serialize, Deserialize, PartialEq, Eq, Clone)]
#[serde(rename_all = "lowercase")]
pub enum PayoutStatus {
Pending,
Failed,
Processed,
Returned,
Processing,
}

impl PayoutStatus {
pub fn from_string(string: &str) -> PayoutStatus {
match string {
"pending" => PayoutStatus::Pending,
"failed" => PayoutStatus::Failed,
"processed" => PayoutStatus::Processed,
"returned" => PayoutStatus::Returned,
"processing" => PayoutStatus::Processing,
_ => PayoutStatus::Processing,
}
}

pub fn as_str(&self) -> &'static str {
match self {
PayoutStatus::Pending => "pending",
PayoutStatus::Failed => "failed",
PayoutStatus::Processed => "processed",
PayoutStatus::Returned => "returned",
PayoutStatus::Processing => "processing",
}
}

pub fn is_failed(&self) -> bool {
match self {
PayoutStatus::Pending => false,
PayoutStatus::Failed => true,
PayoutStatus::Processed => false,
PayoutStatus::Returned => true,
PayoutStatus::Processing => false,
}
}
}
Loading

0 comments on commit 33ea8cd

Please sign in to comment.