You've already forked pages
forked from didirus/AstralRinth
Fix affiliate PUT API (#4456)
* Fix affiliate PUT API * PR fixes * wip: merge affiliate code endpoints
This commit is contained in:
@@ -10,8 +10,15 @@ Before a pull request can be opened, run `cargo clippy -p labrinth --all-targets
|
||||
|
||||
Use `cargo test -p labrinth --all-targets` to test your changes. All tests must pass, otherwise CI will fail.
|
||||
|
||||
To prepare the sqlx cache, cd into `apps/labrinth` and run `cargo sqlx prepare`. Make sure to NEVER run `cargo sqlx prepare --workspace`.
|
||||
|
||||
Read the root `docker-compose.yml` to see what running services are available while developing. Use `docker exec` to access these services.
|
||||
|
||||
When the user refers to "performing pre-PR checks", do the following:
|
||||
- Run clippy as described above
|
||||
- DO NOT run tests unless explicitly requested (they take a long time)
|
||||
- Prepare the sqlx cache
|
||||
|
||||
### Clickhouse
|
||||
|
||||
Use `docker exec labrinth-clickhouse clickhouse-client` to access the Clickhouse instance. We use the `staging_ariadne` database to store data in testing.
|
||||
|
||||
@@ -19,6 +19,25 @@ pub struct AdminAffiliateCode {
|
||||
pub created_by: UserId,
|
||||
/// User who refers the purchaser.
|
||||
pub affiliate: UserId,
|
||||
/// Affiliate-defined name for this affiliate code - where the click came
|
||||
/// from.
|
||||
pub source_name: String,
|
||||
}
|
||||
|
||||
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(),
|
||||
source_name: data.source_name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Affiliate code used to track referral purchases.
|
||||
@@ -36,21 +55,9 @@ pub struct AffiliateCode {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
/// Affiliate-defined name for this affiliate code - where the click came
|
||||
/// from.
|
||||
pub source_name: String,
|
||||
}
|
||||
|
||||
impl From<crate::database::models::affiliate_code_item::DBAffiliateCode>
|
||||
@@ -62,6 +69,7 @@ impl From<crate::database::models::affiliate_code_item::DBAffiliateCode>
|
||||
Self {
|
||||
id: data.id.into(),
|
||||
affiliate: data.affiliate.into(),
|
||||
source_name: data.source_name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,13 +12,10 @@ use crate::{
|
||||
},
|
||||
queue::session::AuthQueue,
|
||||
};
|
||||
use actix_web::{
|
||||
HttpRequest, HttpResponse,
|
||||
web::{self, Json},
|
||||
};
|
||||
use actix_web::{HttpRequest, HttpResponse, web};
|
||||
use ariadne::ids::UserId;
|
||||
use chrono::Utc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::Deserialize;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::routes::ApiError;
|
||||
@@ -26,25 +23,20 @@ use crate::routes::ApiError;
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::scope("affiliate")
|
||||
.route("/admin", web::get().to(admin_get_all))
|
||||
.route("/admin", web::put().to(admin_create))
|
||||
.route("/admin/{id}", web::get().to(admin_get))
|
||||
.route("/admin/{id}", web::delete().to(admin_delete))
|
||||
.route("/self", web::get().to(self_get_all))
|
||||
.route("/self", web::put().to(self_patch))
|
||||
.route("/self/{id}", web::delete().to(self_delete)),
|
||||
.route("", web::get().to(get_all))
|
||||
.route("", web::put().to(create))
|
||||
.route("/{id}", web::get().to(get))
|
||||
.route("/{id}", web::delete().to(delete))
|
||||
.route("/{id}", web::patch().to(patch)),
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct AdminGetAllResponse(Vec<AdminAffiliateCode>);
|
||||
|
||||
async fn admin_get_all(
|
||||
async fn get_all(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<Json<AdminGetAllResponse>, ApiError> {
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let (_, user) = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
@@ -54,38 +46,42 @@ async fn admin_get_all(
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !user.role.is_admin() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to read all affiliate codes!"
|
||||
.to_string(),
|
||||
));
|
||||
if user.role.is_admin() {
|
||||
let codes = DBAffiliateCode::get_all(&**pool).await?;
|
||||
let codes = codes
|
||||
.into_iter()
|
||||
.map(AdminAffiliateCode::from)
|
||||
.collect::<Vec<_>>();
|
||||
Ok(HttpResponse::Ok().json(codes))
|
||||
} else if user.badges.contains(Badges::AFFILIATE) {
|
||||
let codes =
|
||||
DBAffiliateCode::get_by_affiliate(DBUserId::from(user.id), &**pool)
|
||||
.await?;
|
||||
let codes = codes
|
||||
.into_iter()
|
||||
.map(AffiliateCode::from)
|
||||
.collect::<Vec<_>>();
|
||||
Ok(HttpResponse::Ok().json(codes))
|
||||
} else {
|
||||
Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to view affiliate codes!".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
let codes = DBAffiliateCode::get_all(&**pool).await?;
|
||||
let codes = codes
|
||||
.into_iter()
|
||||
.map(AdminAffiliateCode::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(Json(AdminGetAllResponse(codes)))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct AdminCreateRequest {
|
||||
affiliate: UserId,
|
||||
#[derive(Deserialize)]
|
||||
struct CreateRequest {
|
||||
affiliate: Option<UserId>,
|
||||
source_name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct AdminCreateResponse(AdminAffiliateCode);
|
||||
|
||||
async fn admin_create(
|
||||
async fn create(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
body: web::Json<AdminCreateRequest>,
|
||||
) -> Result<Json<AdminCreateResponse>, ApiError> {
|
||||
body: web::Json<CreateRequest>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let (_, creator) = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
@@ -95,7 +91,10 @@ async fn admin_create(
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !creator.role.is_admin() {
|
||||
let is_admin = creator.role.is_admin();
|
||||
let is_affiliate = creator.badges.contains(Badges::AFFILIATE);
|
||||
|
||||
if !is_admin && !is_affiliate {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to create an affiliate code!"
|
||||
.to_string(),
|
||||
@@ -103,15 +102,26 @@ async fn admin_create(
|
||||
}
|
||||
|
||||
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 affiliate_id = if is_admin {
|
||||
if let Some(affiliate) = body.affiliate {
|
||||
DBUserId::from(affiliate)
|
||||
} else {
|
||||
creator_id
|
||||
}
|
||||
} else {
|
||||
creator_id
|
||||
};
|
||||
|
||||
if affiliate_id != creator_id {
|
||||
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 =
|
||||
@@ -129,19 +139,20 @@ async fn admin_create(
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(Json(AdminCreateResponse(AdminAffiliateCode::from(code))))
|
||||
if is_admin {
|
||||
Ok(HttpResponse::Created().json(AdminAffiliateCode::from(code)))
|
||||
} else {
|
||||
Ok(HttpResponse::Created().json(AffiliateCode::from(code)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct AdminGetResponse(AdminAffiliateCode);
|
||||
|
||||
async fn admin_get(
|
||||
async fn get(
|
||||
req: HttpRequest,
|
||||
path: web::Path<(AffiliateCodeId,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<Json<AdminGetResponse>, ApiError> {
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let (_, user) = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
@@ -151,26 +162,30 @@ async fn admin_get(
|
||||
)
|
||||
.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(AdminGetResponse(model)))
|
||||
let is_admin = user.role.is_admin();
|
||||
let is_owner = model.affiliate == DBUserId::from(user.id);
|
||||
|
||||
if is_admin || is_owner {
|
||||
if is_admin {
|
||||
Ok(HttpResponse::Ok().json(AdminAffiliateCode::from(model)))
|
||||
} else {
|
||||
Ok(HttpResponse::Ok().json(AffiliateCode::from(model)))
|
||||
}
|
||||
} else {
|
||||
Err(ApiError::NotFound)
|
||||
}
|
||||
} else {
|
||||
Err(ApiError::NotFound)
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_delete(
|
||||
async fn delete(
|
||||
req: HttpRequest,
|
||||
path: web::Path<(AffiliateCodeId,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
@@ -186,74 +201,43 @@ async fn admin_delete(
|
||||
)
|
||||
.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 let Some(model) =
|
||||
DBAffiliateCode::get_by_id(affiliate_code_id, &**pool).await?
|
||||
{
|
||||
let is_admin = user.role.is_admin();
|
||||
let is_owner = model.affiliate == DBUserId::from(user.id);
|
||||
|
||||
if result.is_some() {
|
||||
Ok(HttpResponse::NoContent().finish())
|
||||
if is_admin || is_owner {
|
||||
let result =
|
||||
DBAffiliateCode::remove(affiliate_code_id, &**pool).await?;
|
||||
if result.is_some() {
|
||||
Ok(HttpResponse::NoContent().finish())
|
||||
} else {
|
||||
Err(ApiError::NotFound)
|
||||
}
|
||||
} else {
|
||||
Err(ApiError::NotFound)
|
||||
}
|
||||
} else {
|
||||
Err(ApiError::NotFound)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SelfGetAllResponse(Vec<AffiliateCode>);
|
||||
|
||||
async fn self_get_all(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<Json<SelfGetAllResponse>, ApiError> {
|
||||
let (_, user) = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::SESSION_ACCESS,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !user.badges.contains(Badges::AFFILIATE) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to view your affiliate codes!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let codes =
|
||||
DBAffiliateCode::get_by_affiliate(DBUserId::from(user.id), &**pool)
|
||||
.await?;
|
||||
|
||||
let codes = codes
|
||||
.into_iter()
|
||||
.map(AffiliateCode::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(Json(SelfGetAllResponse(codes)))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SelfPatchRequest {
|
||||
id: AffiliateCodeId,
|
||||
struct PatchRequest {
|
||||
source_name: String,
|
||||
}
|
||||
|
||||
async fn self_patch(
|
||||
async fn patch(
|
||||
req: HttpRequest,
|
||||
path: web::Path<(AffiliateCodeId,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
body: web::Json<SelfPatchRequest>,
|
||||
body: web::Json<PatchRequest>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let (_, user) = get_user_from_headers(
|
||||
&req,
|
||||
@@ -264,23 +248,26 @@ async fn self_patch(
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !user.badges.contains(Badges::AFFILIATE) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to update your affiliate codes!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let affiliate_code_id = DBAffiliateCodeId::from(body.id);
|
||||
let (affiliate_code_id,) = path.into_inner();
|
||||
let affiliate_code_id = DBAffiliateCodeId::from(affiliate_code_id);
|
||||
|
||||
let existing_code = DBAffiliateCode::get_by_id(affiliate_code_id, &**pool)
|
||||
.await?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
|
||||
if existing_code.affiliate != DBUserId::from(user.id) {
|
||||
let is_admin = user.role.is_admin();
|
||||
let is_owner = existing_code.affiliate == DBUserId::from(user.id);
|
||||
|
||||
if !is_admin && !is_owner {
|
||||
return Err(ApiError::NotFound);
|
||||
}
|
||||
|
||||
if !is_admin && !user.badges.contains(Badges::AFFILIATE) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to update affiliate codes!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
DBAffiliateCode::update_source_name(
|
||||
affiliate_code_id,
|
||||
&body.source_name,
|
||||
@@ -290,45 +277,3 @@ async fn self_patch(
|
||||
|
||||
Ok(HttpResponse::NoContent().finish())
|
||||
}
|
||||
|
||||
async fn self_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.badges.contains(Badges::AFFILIATE) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have permission to delete your affiliate codes!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let (affiliate_code_id,) = path.into_inner();
|
||||
let affiliate_code_id = DBAffiliateCodeId::from(affiliate_code_id);
|
||||
|
||||
let code = DBAffiliateCode::get_by_id(affiliate_code_id, &**pool)
|
||||
.await?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
|
||||
if code.affiliate != DBUserId::from(user.id) {
|
||||
return Err(ApiError::NotFound);
|
||||
}
|
||||
|
||||
let result = DBAffiliateCode::remove(affiliate_code_id, &**pool).await?;
|
||||
if result.is_some() {
|
||||
Ok(HttpResponse::NoContent().finish())
|
||||
} else {
|
||||
Err(ApiError::NotFound)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user