You've already forked AstralRinth
Initial affiliate codes implementation (#4382)
* Initial affiliate codes implementation * some more docs to codes * sqlx prepare * Address PR comments * Address more PR comments * fix clippy * Switch to using Json<T> for type-safe responses
This commit is contained in:
14
apps/labrinth/.sqlx/query-10200da00caaa8f1c0976b9b705bf6d74a59bb62ee5da6dccffec99f36eddac7.json
generated
Normal file
14
apps/labrinth/.sqlx/query-10200da00caaa8f1c0976b9b705bf6d74a59bb62ee5da6dccffec99f36eddac7.json
generated
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "DELETE FROM affiliate_codes WHERE id = $1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "10200da00caaa8f1c0976b9b705bf6d74a59bb62ee5da6dccffec99f36eddac7"
|
||||||
|
}
|
||||||
40
apps/labrinth/.sqlx/query-6ef8402f0f0685acda118c024d82e31b1b235ab7c5ec00a86af4dfbe81342a58.json
generated
Normal file
40
apps/labrinth/.sqlx/query-6ef8402f0f0685acda118c024d82e31b1b235ab7c5ec00a86af4dfbe81342a58.json
generated
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "SELECT id, created_at, created_by, affiliate\n FROM affiliate_codes WHERE affiliate = $1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "id",
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "created_at",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "created_by",
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "affiliate",
|
||||||
|
"type_info": "Int8"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "6ef8402f0f0685acda118c024d82e31b1b235ab7c5ec00a86af4dfbe81342a58"
|
||||||
|
}
|
||||||
40
apps/labrinth/.sqlx/query-81cfa59dafa8dac87a917a3ddda93d412a579ab17d9dad754e7d9e26d78e0e80.json
generated
Normal file
40
apps/labrinth/.sqlx/query-81cfa59dafa8dac87a917a3ddda93d412a579ab17d9dad754e7d9e26d78e0e80.json
generated
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "SELECT id, created_at, created_by, affiliate\n FROM affiliate_codes WHERE id = $1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "id",
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "created_at",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "created_by",
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "affiliate",
|
||||||
|
"type_info": "Int8"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "81cfa59dafa8dac87a917a3ddda93d412a579ab17d9dad754e7d9e26d78e0e80"
|
||||||
|
}
|
||||||
38
apps/labrinth/.sqlx/query-b27a1495f0a937e1bae944cfe6e46a4f702536816e5259c4aeec8b905b507473.json
generated
Normal file
38
apps/labrinth/.sqlx/query-b27a1495f0a937e1bae944cfe6e46a4f702536816e5259c4aeec8b905b507473.json
generated
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "SELECT id, created_at, created_by, affiliate\n FROM affiliate_codes ORDER BY created_at DESC",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "id",
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "created_at",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "created_by",
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "affiliate",
|
||||||
|
"type_info": "Int8"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": []
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "b27a1495f0a937e1bae944cfe6e46a4f702536816e5259c4aeec8b905b507473"
|
||||||
|
}
|
||||||
22
apps/labrinth/.sqlx/query-b36457e1aa73b5517ca4f41e4144084a0aa996168d2660c13a1d4c9fc4594859.json
generated
Normal file
22
apps/labrinth/.sqlx/query-b36457e1aa73b5517ca4f41e4144084a0aa996168d2660c13a1d4c9fc4594859.json
generated
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "SELECT EXISTS(SELECT 1 FROM affiliate_codes WHERE id=$1)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "exists",
|
||||||
|
"type_info": "Bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
null
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "b36457e1aa73b5517ca4f41e4144084a0aa996168d2660c13a1d4c9fc4594859"
|
||||||
|
}
|
||||||
17
apps/labrinth/.sqlx/query-d51d96a9771ce1c3a2987e0790ef25bc55122c934c73418a97ee9cd81f56b251.json
generated
Normal file
17
apps/labrinth/.sqlx/query-d51d96a9771ce1c3a2987e0790ef25bc55122c934c73418a97ee9cd81f56b251.json
generated
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "INSERT INTO affiliate_codes (id, created_at, created_by, affiliate)\n VALUES ($1, $2, $3, $4)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8",
|
||||||
|
"Timestamptz",
|
||||||
|
"Int8",
|
||||||
|
"Int8"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "d51d96a9771ce1c3a2987e0790ef25bc55122c934c73418a97ee9cd81f56b251"
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE affiliate_codes (
|
||||||
|
id bigint PRIMARY KEY,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
created_by bigint NOT NULL REFERENCES users(id),
|
||||||
|
affiliate bigint NOT NULL REFERENCES users(id),
|
||||||
|
-- left nullable so we can explicitly set payouts if we need to,
|
||||||
|
-- and use a global default if unset
|
||||||
|
revenue_split float
|
||||||
|
);
|
||||||
117
apps/labrinth/src/database/models/affiliate_code_item.rs
Normal file
117
apps/labrinth/src/database/models/affiliate_code_item.rs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use futures::{StreamExt, TryStreamExt};
|
||||||
|
|
||||||
|
use crate::database::models::{DBAffiliateCodeId, DBUserId, DatabaseError};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DBAffiliateCode {
|
||||||
|
pub id: DBAffiliateCodeId,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub created_by: DBUserId,
|
||||||
|
pub affiliate: DBUserId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DBAffiliateCode {
|
||||||
|
pub async fn get_by_id(
|
||||||
|
id: DBAffiliateCodeId,
|
||||||
|
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
||||||
|
) -> Result<Option<DBAffiliateCode>, DatabaseError> {
|
||||||
|
let record = sqlx::query!(
|
||||||
|
"SELECT id, created_at, created_by, affiliate
|
||||||
|
FROM affiliate_codes WHERE id = $1",
|
||||||
|
id as DBAffiliateCodeId
|
||||||
|
)
|
||||||
|
.fetch_optional(exec)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(record.map(|record| DBAffiliateCode {
|
||||||
|
id: DBAffiliateCodeId(record.id),
|
||||||
|
created_at: record.created_at,
|
||||||
|
created_by: DBUserId(record.created_by),
|
||||||
|
affiliate: DBUserId(record.affiliate),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_by_affiliate(
|
||||||
|
affiliate: DBUserId,
|
||||||
|
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
||||||
|
) -> Result<Vec<DBAffiliateCode>, DatabaseError> {
|
||||||
|
let records = sqlx::query!(
|
||||||
|
"SELECT id, created_at, created_by, affiliate
|
||||||
|
FROM affiliate_codes WHERE affiliate = $1",
|
||||||
|
affiliate as DBUserId
|
||||||
|
)
|
||||||
|
.fetch(exec)
|
||||||
|
.map(|record| {
|
||||||
|
let record = record?;
|
||||||
|
Ok::<_, DatabaseError>(DBAffiliateCode {
|
||||||
|
id: DBAffiliateCodeId(record.id),
|
||||||
|
created_at: record.created_at,
|
||||||
|
created_by: DBUserId(record.created_by),
|
||||||
|
affiliate: DBUserId(record.affiliate),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.try_collect::<Vec<_>>()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(records)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn insert(
|
||||||
|
&self,
|
||||||
|
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
||||||
|
) -> Result<(), DatabaseError> {
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO affiliate_codes (id, created_at, created_by, affiliate)
|
||||||
|
VALUES ($1, $2, $3, $4)",
|
||||||
|
self.id as DBAffiliateCodeId,
|
||||||
|
self.created_at,
|
||||||
|
self.created_by as DBUserId,
|
||||||
|
self.affiliate as DBUserId
|
||||||
|
)
|
||||||
|
.execute(exec)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove(
|
||||||
|
id: DBAffiliateCodeId,
|
||||||
|
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
||||||
|
) -> Result<Option<()>, DatabaseError> {
|
||||||
|
let result = sqlx::query!(
|
||||||
|
"DELETE FROM affiliate_codes WHERE id = $1",
|
||||||
|
id as DBAffiliateCodeId
|
||||||
|
)
|
||||||
|
.execute(exec)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if result.rows_affected() > 0 {
|
||||||
|
Ok(Some(()))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_all(
|
||||||
|
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
||||||
|
) -> Result<Vec<DBAffiliateCode>, DatabaseError> {
|
||||||
|
let records = sqlx::query!(
|
||||||
|
"SELECT id, created_at, created_by, affiliate
|
||||||
|
FROM affiliate_codes ORDER BY created_at DESC"
|
||||||
|
)
|
||||||
|
.fetch(exec)
|
||||||
|
.map(|record| {
|
||||||
|
let record = record?;
|
||||||
|
Ok::<_, DatabaseError>(DBAffiliateCode {
|
||||||
|
id: DBAffiliateCodeId(record.id),
|
||||||
|
created_at: record.created_at,
|
||||||
|
created_by: DBUserId(record.created_by),
|
||||||
|
affiliate: DBUserId(record.affiliate),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.try_collect::<Vec<_>>()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(records)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use super::DatabaseError;
|
use super::DatabaseError;
|
||||||
use crate::models::ids::{
|
use crate::models::ids::{
|
||||||
ChargeId, CollectionId, FileId, ImageId, NotificationId,
|
AffiliateCodeId, ChargeId, CollectionId, FileId, ImageId, NotificationId,
|
||||||
OAuthAccessTokenId, OAuthClientAuthorizationId, OAuthClientId,
|
OAuthAccessTokenId, OAuthClientAuthorizationId, OAuthClientId,
|
||||||
OAuthRedirectUriId, OrganizationId, PatId, PayoutId, ProductId,
|
OAuthRedirectUriId, OrganizationId, PatId, PayoutId, ProductId,
|
||||||
ProductPriceId, ProjectId, ReportId, SessionId, SharedInstanceId,
|
ProductPriceId, ProjectId, ReportId, SessionId, SharedInstanceId,
|
||||||
@@ -263,6 +263,10 @@ db_id_interface!(
|
|||||||
VersionId,
|
VersionId,
|
||||||
generator: generate_version_id @ "versions",
|
generator: generate_version_id @ "versions",
|
||||||
);
|
);
|
||||||
|
db_id_interface!(
|
||||||
|
AffiliateCodeId,
|
||||||
|
generator: generate_affiliate_code_id @ "affiliate_codes",
|
||||||
|
);
|
||||||
|
|
||||||
short_id_type!(CategoryId);
|
short_id_type!(CategoryId);
|
||||||
short_id_type!(GameId);
|
short_id_type!(GameId);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
pub mod affiliate_code_item;
|
||||||
pub mod categories;
|
pub mod categories;
|
||||||
pub mod charge_item;
|
pub mod charge_item;
|
||||||
pub mod collection_item;
|
pub mod collection_item;
|
||||||
@@ -34,6 +35,7 @@ pub mod users_notifications_preferences_item;
|
|||||||
pub mod users_redeemals;
|
pub mod users_redeemals;
|
||||||
pub mod version_item;
|
pub mod version_item;
|
||||||
|
|
||||||
|
pub use affiliate_code_item::DBAffiliateCode;
|
||||||
pub use collection_item::DBCollection;
|
pub use collection_item::DBCollection;
|
||||||
pub use ids::*;
|
pub use ids::*;
|
||||||
pub use image_item::DBImage;
|
pub use image_item::DBImage;
|
||||||
|
|||||||
67
apps/labrinth/src/models/v3/affiliate_code.rs
Normal file
67
apps/labrinth/src/models/v3/affiliate_code.rs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
use ariadne::ids::UserId;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::models::ids::AffiliateCodeId;
|
||||||
|
|
||||||
|
/// Affiliate code used to track referral purchases.
|
||||||
|
///
|
||||||
|
/// See [`AffiliateCode`].
|
||||||
|
///
|
||||||
|
/// This struct contains information which should only be visible to admins.
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct AdminAffiliateCode {
|
||||||
|
/// Affiliate code ID.
|
||||||
|
pub id: AffiliateCodeId,
|
||||||
|
/// When the code was created.
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
/// User who created the code.
|
||||||
|
pub created_by: UserId,
|
||||||
|
/// User who refers the purchaser.
|
||||||
|
pub affiliate: UserId,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Affiliate code used to track referral purchases.
|
||||||
|
///
|
||||||
|
/// When a user follows a URL with [`AffiliateCode::id`] as an affiliate
|
||||||
|
/// parameter, the code will be saved as a cookie. When the same user purchases
|
||||||
|
/// a product with an affiliate code cookie, the purchase under that code is
|
||||||
|
/// tracked.
|
||||||
|
///
|
||||||
|
/// This struct contains information which is allowed to be seen by an
|
||||||
|
/// affiliate.
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct AffiliateCode {
|
||||||
|
/// Affiliate code ID.
|
||||||
|
pub id: AffiliateCodeId,
|
||||||
|
/// User who refers the purchaser.
|
||||||
|
pub affiliate: UserId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<crate::database::models::affiliate_code_item::DBAffiliateCode>
|
||||||
|
for AdminAffiliateCode
|
||||||
|
{
|
||||||
|
fn from(
|
||||||
|
data: crate::database::models::affiliate_code_item::DBAffiliateCode,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
id: data.id.into(),
|
||||||
|
created_at: data.created_at,
|
||||||
|
created_by: data.created_by.into(),
|
||||||
|
affiliate: data.affiliate.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<crate::database::models::affiliate_code_item::DBAffiliateCode>
|
||||||
|
for AffiliateCode
|
||||||
|
{
|
||||||
|
fn from(
|
||||||
|
data: crate::database::models::affiliate_code_item::DBAffiliateCode,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
id: data.id.into(),
|
||||||
|
affiliate: data.affiliate.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,3 +25,4 @@ base62_id!(ThreadId);
|
|||||||
base62_id!(ThreadMessageId);
|
base62_id!(ThreadMessageId);
|
||||||
base62_id!(UserSubscriptionId);
|
base62_id!(UserSubscriptionId);
|
||||||
base62_id!(VersionId);
|
base62_id!(VersionId);
|
||||||
|
base62_id!(AffiliateCodeId);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod affiliate_code;
|
||||||
pub mod analytics;
|
pub mod analytics;
|
||||||
pub mod billing;
|
pub mod billing;
|
||||||
pub mod collections;
|
pub mod collections;
|
||||||
|
|||||||
231
apps/labrinth/src/routes/internal/affiliate.rs
Normal file
231
apps/labrinth/src/routes/internal/affiliate.rs
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
use crate::{
|
||||||
|
auth::get_user_from_headers,
|
||||||
|
database::{
|
||||||
|
models::{DBAffiliateCode, DBAffiliateCodeId, DBUser, DBUserId},
|
||||||
|
redis::RedisPool,
|
||||||
|
},
|
||||||
|
models::{
|
||||||
|
ids::AffiliateCodeId,
|
||||||
|
pats::Scopes,
|
||||||
|
v3::affiliate_code::{AdminAffiliateCode, AffiliateCode},
|
||||||
|
},
|
||||||
|
queue::session::AuthQueue,
|
||||||
|
};
|
||||||
|
use actix_web::{
|
||||||
|
HttpRequest, HttpResponse,
|
||||||
|
web::{self, Json},
|
||||||
|
};
|
||||||
|
use ariadne::ids::UserId;
|
||||||
|
use chrono::Utc;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
use crate::routes::ApiError;
|
||||||
|
|
||||||
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg.service(
|
||||||
|
web::scope("affiliate")
|
||||||
|
.route("/code", web::get().to(code_get_all))
|
||||||
|
.route("/code", web::put().to(code_create))
|
||||||
|
.route("/code/{id}", web::get().to(code_get))
|
||||||
|
.route("/code/{id}", web::delete().to(code_delete))
|
||||||
|
.route("/self", web::get().to(self_get)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct CodeGetAllResponse(Vec<AdminAffiliateCode>);
|
||||||
|
|
||||||
|
async fn code_get_all(
|
||||||
|
req: HttpRequest,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
redis: web::Data<RedisPool>,
|
||||||
|
session_queue: web::Data<AuthQueue>,
|
||||||
|
) -> Result<Json<CodeGetAllResponse>, ApiError> {
|
||||||
|
let (_, user) = get_user_from_headers(
|
||||||
|
&req,
|
||||||
|
&**pool,
|
||||||
|
&redis,
|
||||||
|
&session_queue,
|
||||||
|
Scopes::SESSION_ACCESS,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !user.role.is_admin() {
|
||||||
|
return Err(ApiError::CustomAuthentication(
|
||||||
|
"You do not have permission to read all affiliate codes!"
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let codes = DBAffiliateCode::get_all(&**pool).await?;
|
||||||
|
let codes = codes
|
||||||
|
.into_iter()
|
||||||
|
.map(AdminAffiliateCode::from)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(Json(CodeGetAllResponse(codes)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct CodeCreateRequest {
|
||||||
|
affiliate: UserId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct CodeCreateResponse(AdminAffiliateCode);
|
||||||
|
|
||||||
|
async fn code_create(
|
||||||
|
req: HttpRequest,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
redis: web::Data<RedisPool>,
|
||||||
|
session_queue: web::Data<AuthQueue>,
|
||||||
|
body: web::Json<CodeCreateRequest>,
|
||||||
|
) -> Result<Json<CodeCreateResponse>, ApiError> {
|
||||||
|
let (_, creator) = get_user_from_headers(
|
||||||
|
&req,
|
||||||
|
&**pool,
|
||||||
|
&redis,
|
||||||
|
&session_queue,
|
||||||
|
Scopes::SESSION_ACCESS,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !creator.role.is_admin() {
|
||||||
|
return Err(ApiError::CustomAuthentication(
|
||||||
|
"You do not have permission to create an affiliate code!"
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let creator_id = DBUserId::from(creator.id);
|
||||||
|
let affiliate_id = DBUserId::from(body.affiliate);
|
||||||
|
let Some(_affiliate_user) =
|
||||||
|
DBUser::get_id(affiliate_id, &**pool, &redis).await?
|
||||||
|
else {
|
||||||
|
return Err(ApiError::CustomAuthentication(
|
||||||
|
"Affiliate user not found!".to_string(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut transaction = pool.begin().await?;
|
||||||
|
|
||||||
|
let affiliate_code_id =
|
||||||
|
crate::database::models::generate_affiliate_code_id(&mut transaction)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let code = DBAffiliateCode {
|
||||||
|
id: affiliate_code_id,
|
||||||
|
created_at: Utc::now(),
|
||||||
|
created_by: creator_id,
|
||||||
|
affiliate: affiliate_id,
|
||||||
|
};
|
||||||
|
code.insert(&mut *transaction).await?;
|
||||||
|
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
Ok(Json(CodeCreateResponse(AdminAffiliateCode::from(code))))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct CodeGetResponse(AdminAffiliateCode);
|
||||||
|
|
||||||
|
async fn code_get(
|
||||||
|
req: HttpRequest,
|
||||||
|
path: web::Path<(AffiliateCodeId,)>,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
redis: web::Data<RedisPool>,
|
||||||
|
session_queue: web::Data<AuthQueue>,
|
||||||
|
) -> Result<Json<CodeGetResponse>, ApiError> {
|
||||||
|
let (_, user) = get_user_from_headers(
|
||||||
|
&req,
|
||||||
|
&**pool,
|
||||||
|
&redis,
|
||||||
|
&session_queue,
|
||||||
|
Scopes::SESSION_ACCESS,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !user.role.is_admin() {
|
||||||
|
return Err(ApiError::CustomAuthentication(
|
||||||
|
"You do not have permission to read an affiliate code!".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (affiliate_code_id,) = path.into_inner();
|
||||||
|
let affiliate_code_id = DBAffiliateCodeId::from(affiliate_code_id);
|
||||||
|
|
||||||
|
if let Some(model) =
|
||||||
|
DBAffiliateCode::get_by_id(affiliate_code_id, &**pool).await?
|
||||||
|
{
|
||||||
|
let model = AdminAffiliateCode::from(model);
|
||||||
|
Ok(Json(CodeGetResponse(model)))
|
||||||
|
} else {
|
||||||
|
Err(ApiError::NotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn code_delete(
|
||||||
|
req: HttpRequest,
|
||||||
|
path: web::Path<(AffiliateCodeId,)>,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
redis: web::Data<RedisPool>,
|
||||||
|
session_queue: web::Data<AuthQueue>,
|
||||||
|
) -> Result<HttpResponse, ApiError> {
|
||||||
|
let (_, user) = get_user_from_headers(
|
||||||
|
&req,
|
||||||
|
&**pool,
|
||||||
|
&redis,
|
||||||
|
&session_queue,
|
||||||
|
Scopes::SESSION_ACCESS,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !user.role.is_admin() {
|
||||||
|
return Err(ApiError::CustomAuthentication(
|
||||||
|
"You do not have permission to delete an affiliate code!"
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (affiliate_code_id,) = path.into_inner();
|
||||||
|
let affiliate_code_id = DBAffiliateCodeId::from(affiliate_code_id);
|
||||||
|
|
||||||
|
let result = DBAffiliateCode::remove(affiliate_code_id, &**pool).await?;
|
||||||
|
|
||||||
|
if result.is_some() {
|
||||||
|
Ok(HttpResponse::NoContent().finish())
|
||||||
|
} else {
|
||||||
|
Err(ApiError::NotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct SelfGetResponse(Vec<AffiliateCode>);
|
||||||
|
|
||||||
|
async fn self_get(
|
||||||
|
req: HttpRequest,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
redis: web::Data<RedisPool>,
|
||||||
|
session_queue: web::Data<AuthQueue>,
|
||||||
|
) -> Result<Json<SelfGetResponse>, ApiError> {
|
||||||
|
let (_, user) = get_user_from_headers(
|
||||||
|
&req,
|
||||||
|
&**pool,
|
||||||
|
&redis,
|
||||||
|
&session_queue,
|
||||||
|
Scopes::SESSION_ACCESS,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let codes =
|
||||||
|
DBAffiliateCode::get_by_affiliate(DBUserId::from(user.id), &**pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let codes = codes
|
||||||
|
.into_iter()
|
||||||
|
.map(AffiliateCode::from)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(Json(SelfGetResponse(codes)))
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
pub(crate) mod admin;
|
pub(crate) mod admin;
|
||||||
|
pub mod affiliate;
|
||||||
pub mod billing;
|
pub mod billing;
|
||||||
pub mod external_notifications;
|
pub mod external_notifications;
|
||||||
pub mod flows;
|
pub mod flows;
|
||||||
@@ -28,6 +29,7 @@ pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
|||||||
.configure(gdpr::config)
|
.configure(gdpr::config)
|
||||||
.configure(statuses::config)
|
.configure(statuses::config)
|
||||||
.configure(medal::config)
|
.configure(medal::config)
|
||||||
.configure(external_notifications::config),
|
.configure(external_notifications::config)
|
||||||
|
.configure(affiliate::config),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user