You've already forked AstralRinth
Allow users to manage their own affiliate codes (#4392)
* Allow users to manage their own affiliate codes * Add a badge to restrict access to affiliate codes * sqlx prepare and clippy
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, created_at, created_by, affiliate\n FROM affiliate_codes WHERE id = $1",
|
||||
"query": "SELECT id, created_at, created_by, affiliate, source_name\n FROM affiliate_codes WHERE affiliate = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -22,6 +22,11 @@
|
||||
"ordinal": 3,
|
||||
"name": "affiliate",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "source_name",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -33,8 +38,9 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "81cfa59dafa8dac87a917a3ddda93d412a579ab17d9dad754e7d9e26d78e0e80"
|
||||
"hash": "057b0fda8e0ad34fc880121b6461ddfc3c61d4a0bf95e376e24440b6a58d2844"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO affiliate_codes (id, created_at, created_by, affiliate)\n VALUES ($1, $2, $3, $4)",
|
||||
"query": "INSERT INTO affiliate_codes (id, created_at, created_by, affiliate, source_name)\n VALUES ($1, $2, $3, $4, $5)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -8,10 +8,11 @@
|
||||
"Int8",
|
||||
"Timestamptz",
|
||||
"Int8",
|
||||
"Int8"
|
||||
"Int8",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d51d96a9771ce1c3a2987e0790ef25bc55122c934c73418a97ee9cd81f56b251"
|
||||
"hash": "0e6d18643a4a7834eb34fe519b073e290b1089d2d0cfdfdb45b5125a931d08ca"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, created_at, created_by, affiliate\n FROM affiliate_codes WHERE affiliate = $1",
|
||||
"query": "SELECT id, created_at, created_by, affiliate, source_name\n FROM affiliate_codes WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -22,6 +22,11 @@
|
||||
"ordinal": 3,
|
||||
"name": "affiliate",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "source_name",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -33,8 +38,9 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "6ef8402f0f0685acda118c024d82e31b1b235ab7c5ec00a86af4dfbe81342a58"
|
||||
"hash": "1bd7365eaeac25b1286030a900767eef3b1b6e200ab0dbb3a4274eeba95f9568"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, created_at, created_by, affiliate\n FROM affiliate_codes ORDER BY created_at DESC",
|
||||
"query": "SELECT id, created_at, created_by, affiliate, source_name\n FROM affiliate_codes ORDER BY created_at DESC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -22,6 +22,11 @@
|
||||
"ordinal": 3,
|
||||
"name": "affiliate",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "source_name",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -31,8 +36,9 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b27a1495f0a937e1bae944cfe6e46a4f702536816e5259c4aeec8b905b507473"
|
||||
"hash": "cc95f1b143399b5ecebc91fa74820bfe8c1057c26471b17efa4213a09520a65e"
|
||||
}
|
||||
15
apps/labrinth/.sqlx/query-d307116366d03315a8a01d1b62c5ca81624a42d01a43e1d5adf8881f8a71d495.json
generated
Normal file
15
apps/labrinth/.sqlx/query-d307116366d03315a8a01d1b62c5ca81624a42d01a43e1d5adf8881f8a71d495.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE affiliate_codes SET source_name = $1 WHERE id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d307116366d03315a8a01d1b62c5ca81624a42d01a43e1d5adf8881f8a71d495"
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE affiliate_codes
|
||||
ADD COLUMN source_name VARCHAR(255) NOT NULL DEFAULT '(unnamed)';
|
||||
@@ -9,6 +9,7 @@ pub struct DBAffiliateCode {
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub created_by: DBUserId,
|
||||
pub affiliate: DBUserId,
|
||||
pub source_name: String,
|
||||
}
|
||||
|
||||
impl DBAffiliateCode {
|
||||
@@ -17,7 +18,7 @@ impl DBAffiliateCode {
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
||||
) -> Result<Option<DBAffiliateCode>, DatabaseError> {
|
||||
let record = sqlx::query!(
|
||||
"SELECT id, created_at, created_by, affiliate
|
||||
"SELECT id, created_at, created_by, affiliate, source_name
|
||||
FROM affiliate_codes WHERE id = $1",
|
||||
id as DBAffiliateCodeId
|
||||
)
|
||||
@@ -29,6 +30,7 @@ impl DBAffiliateCode {
|
||||
created_at: record.created_at,
|
||||
created_by: DBUserId(record.created_by),
|
||||
affiliate: DBUserId(record.affiliate),
|
||||
source_name: record.source_name,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -37,7 +39,7 @@ impl DBAffiliateCode {
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
||||
) -> Result<Vec<DBAffiliateCode>, DatabaseError> {
|
||||
let records = sqlx::query!(
|
||||
"SELECT id, created_at, created_by, affiliate
|
||||
"SELECT id, created_at, created_by, affiliate, source_name
|
||||
FROM affiliate_codes WHERE affiliate = $1",
|
||||
affiliate as DBUserId
|
||||
)
|
||||
@@ -49,6 +51,7 @@ impl DBAffiliateCode {
|
||||
created_at: record.created_at,
|
||||
created_by: DBUserId(record.created_by),
|
||||
affiliate: DBUserId(record.affiliate),
|
||||
source_name: record.source_name,
|
||||
})
|
||||
})
|
||||
.try_collect::<Vec<_>>()
|
||||
@@ -62,12 +65,13 @@ impl DBAffiliateCode {
|
||||
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)",
|
||||
"INSERT INTO affiliate_codes (id, created_at, created_by, affiliate, source_name)
|
||||
VALUES ($1, $2, $3, $4, $5)",
|
||||
self.id as DBAffiliateCodeId,
|
||||
self.created_at,
|
||||
self.created_by as DBUserId,
|
||||
self.affiliate as DBUserId
|
||||
self.affiliate as DBUserId,
|
||||
self.source_name
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
@@ -92,11 +96,27 @@ impl DBAffiliateCode {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_source_name(
|
||||
id: DBAffiliateCodeId,
|
||||
source_name: &str,
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
||||
) -> Result<bool, DatabaseError> {
|
||||
let result = sqlx::query!(
|
||||
"UPDATE affiliate_codes SET source_name = $1 WHERE id = $2",
|
||||
source_name,
|
||||
id as DBAffiliateCodeId
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
|
||||
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
|
||||
"SELECT id, created_at, created_by, affiliate, source_name
|
||||
FROM affiliate_codes ORDER BY created_at DESC"
|
||||
)
|
||||
.fetch(exec)
|
||||
@@ -107,6 +127,7 @@ impl DBAffiliateCode {
|
||||
created_at: record.created_at,
|
||||
created_by: DBUserId(record.created_by),
|
||||
affiliate: DBUserId(record.affiliate),
|
||||
source_name: record.source_name,
|
||||
})
|
||||
})
|
||||
.try_collect::<Vec<_>>()
|
||||
|
||||
@@ -17,9 +17,7 @@ bitflags::bitflags! {
|
||||
const ALPHA_TESTER = 1 << 4;
|
||||
const CONTRIBUTOR = 1 << 5;
|
||||
const TRANSLATOR = 1 << 6;
|
||||
|
||||
const ALL = 0b1111111;
|
||||
const NONE = 0b0;
|
||||
const AFFILIATE = 1 << 7;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +25,7 @@ bitflags_serde_impl!(Badges, u64);
|
||||
|
||||
impl Default for Badges {
|
||||
fn default() -> Badges {
|
||||
Badges::NONE
|
||||
Badges::empty()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::{
|
||||
models::{
|
||||
ids::AffiliateCodeId,
|
||||
pats::Scopes,
|
||||
users::Badges,
|
||||
v3::affiliate_code::{AdminAffiliateCode, AffiliateCode},
|
||||
},
|
||||
queue::session::AuthQueue,
|
||||
@@ -25,23 +26,25 @@ 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)),
|
||||
.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)),
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CodeGetAllResponse(Vec<AdminAffiliateCode>);
|
||||
struct AdminGetAllResponse(Vec<AdminAffiliateCode>);
|
||||
|
||||
async fn code_get_all(
|
||||
async fn admin_get_all(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<Json<CodeGetAllResponse>, ApiError> {
|
||||
) -> Result<Json<AdminGetAllResponse>, ApiError> {
|
||||
let (_, user) = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
@@ -64,24 +67,25 @@ async fn code_get_all(
|
||||
.map(AdminAffiliateCode::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(Json(CodeGetAllResponse(codes)))
|
||||
Ok(Json(AdminGetAllResponse(codes)))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct CodeCreateRequest {
|
||||
struct AdminCreateRequest {
|
||||
affiliate: UserId,
|
||||
source_name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CodeCreateResponse(AdminAffiliateCode);
|
||||
struct AdminCreateResponse(AdminAffiliateCode);
|
||||
|
||||
async fn code_create(
|
||||
async fn admin_create(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
body: web::Json<CodeCreateRequest>,
|
||||
) -> Result<Json<CodeCreateResponse>, ApiError> {
|
||||
body: web::Json<AdminCreateRequest>,
|
||||
) -> Result<Json<AdminCreateResponse>, ApiError> {
|
||||
let (_, creator) = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
@@ -119,24 +123,25 @@ async fn code_create(
|
||||
created_at: Utc::now(),
|
||||
created_by: creator_id,
|
||||
affiliate: affiliate_id,
|
||||
source_name: body.source_name.clone(),
|
||||
};
|
||||
code.insert(&mut *transaction).await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(Json(CodeCreateResponse(AdminAffiliateCode::from(code))))
|
||||
Ok(Json(AdminCreateResponse(AdminAffiliateCode::from(code))))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CodeGetResponse(AdminAffiliateCode);
|
||||
struct AdminGetResponse(AdminAffiliateCode);
|
||||
|
||||
async fn code_get(
|
||||
async fn admin_get(
|
||||
req: HttpRequest,
|
||||
path: web::Path<(AffiliateCodeId,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<Json<CodeGetResponse>, ApiError> {
|
||||
) -> Result<Json<AdminGetResponse>, ApiError> {
|
||||
let (_, user) = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
@@ -159,13 +164,13 @@ async fn code_get(
|
||||
DBAffiliateCode::get_by_id(affiliate_code_id, &**pool).await?
|
||||
{
|
||||
let model = AdminAffiliateCode::from(model);
|
||||
Ok(Json(CodeGetResponse(model)))
|
||||
Ok(Json(AdminGetResponse(model)))
|
||||
} else {
|
||||
Err(ApiError::NotFound)
|
||||
}
|
||||
}
|
||||
|
||||
async fn code_delete(
|
||||
async fn admin_delete(
|
||||
req: HttpRequest,
|
||||
path: web::Path<(AffiliateCodeId,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
@@ -201,14 +206,14 @@ async fn code_delete(
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SelfGetResponse(Vec<AffiliateCode>);
|
||||
struct SelfGetAllResponse(Vec<AffiliateCode>);
|
||||
|
||||
async fn self_get(
|
||||
async fn self_get_all(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<Json<SelfGetResponse>, ApiError> {
|
||||
) -> Result<Json<SelfGetAllResponse>, ApiError> {
|
||||
let (_, user) = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
@@ -218,6 +223,13 @@ async fn self_get(
|
||||
)
|
||||
.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?;
|
||||
@@ -227,5 +239,96 @@ async fn self_get(
|
||||
.map(AffiliateCode::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(Json(SelfGetResponse(codes)))
|
||||
Ok(Json(SelfGetAllResponse(codes)))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SelfPatchRequest {
|
||||
id: AffiliateCodeId,
|
||||
source_name: String,
|
||||
}
|
||||
|
||||
async fn self_patch(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
body: web::Json<SelfPatchRequest>,
|
||||
) -> 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 update your affiliate codes!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let affiliate_code_id = DBAffiliateCodeId::from(body.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) {
|
||||
return Err(ApiError::NotFound);
|
||||
}
|
||||
|
||||
DBAffiliateCode::update_source_name(
|
||||
affiliate_code_id,
|
||||
&body.source_name,
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
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