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:
aecsocket
2025-09-18 16:43:34 +01:00
committed by GitHub
parent 6da190ed01
commit 4def0e8407
15 changed files with 607 additions and 2 deletions

View 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)))
}

View File

@@ -1,4 +1,5 @@
pub(crate) mod admin;
pub mod affiliate;
pub mod billing;
pub mod external_notifications;
pub mod flows;
@@ -28,6 +29,7 @@ pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
.configure(gdpr::config)
.configure(statuses::config)
.configure(medal::config)
.configure(external_notifications::config),
.configure(external_notifications::config)
.configure(affiliate::config),
);
}