diff --git a/apps/labrinth/.sqlx/query-81cfa59dafa8dac87a917a3ddda93d412a579ab17d9dad754e7d9e26d78e0e80.json b/apps/labrinth/.sqlx/query-057b0fda8e0ad34fc880121b6461ddfc3c61d4a0bf95e376e24440b6a58d2844.json similarity index 65% rename from apps/labrinth/.sqlx/query-81cfa59dafa8dac87a917a3ddda93d412a579ab17d9dad754e7d9e26d78e0e80.json rename to apps/labrinth/.sqlx/query-057b0fda8e0ad34fc880121b6461ddfc3c61d4a0bf95e376e24440b6a58d2844.json index 7b257455a7..f050a76328 100644 --- a/apps/labrinth/.sqlx/query-81cfa59dafa8dac87a917a3ddda93d412a579ab17d9dad754e7d9e26d78e0e80.json +++ b/apps/labrinth/.sqlx/query-057b0fda8e0ad34fc880121b6461ddfc3c61d4a0bf95e376e24440b6a58d2844.json @@ -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" } diff --git a/apps/labrinth/.sqlx/query-d51d96a9771ce1c3a2987e0790ef25bc55122c934c73418a97ee9cd81f56b251.json b/apps/labrinth/.sqlx/query-0e6d18643a4a7834eb34fe519b073e290b1089d2d0cfdfdb45b5125a931d08ca.json similarity index 58% rename from apps/labrinth/.sqlx/query-d51d96a9771ce1c3a2987e0790ef25bc55122c934c73418a97ee9cd81f56b251.json rename to apps/labrinth/.sqlx/query-0e6d18643a4a7834eb34fe519b073e290b1089d2d0cfdfdb45b5125a931d08ca.json index 5a0d6d37a2..cdd66f6800 100644 --- a/apps/labrinth/.sqlx/query-d51d96a9771ce1c3a2987e0790ef25bc55122c934c73418a97ee9cd81f56b251.json +++ b/apps/labrinth/.sqlx/query-0e6d18643a4a7834eb34fe519b073e290b1089d2d0cfdfdb45b5125a931d08ca.json @@ -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" } diff --git a/apps/labrinth/.sqlx/query-6ef8402f0f0685acda118c024d82e31b1b235ab7c5ec00a86af4dfbe81342a58.json b/apps/labrinth/.sqlx/query-1bd7365eaeac25b1286030a900767eef3b1b6e200ab0dbb3a4274eeba95f9568.json similarity index 65% rename from apps/labrinth/.sqlx/query-6ef8402f0f0685acda118c024d82e31b1b235ab7c5ec00a86af4dfbe81342a58.json rename to apps/labrinth/.sqlx/query-1bd7365eaeac25b1286030a900767eef3b1b6e200ab0dbb3a4274eeba95f9568.json index 3e5ac7b853..53c10e0e79 100644 --- a/apps/labrinth/.sqlx/query-6ef8402f0f0685acda118c024d82e31b1b235ab7c5ec00a86af4dfbe81342a58.json +++ b/apps/labrinth/.sqlx/query-1bd7365eaeac25b1286030a900767eef3b1b6e200ab0dbb3a4274eeba95f9568.json @@ -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" } diff --git a/apps/labrinth/.sqlx/query-b27a1495f0a937e1bae944cfe6e46a4f702536816e5259c4aeec8b905b507473.json b/apps/labrinth/.sqlx/query-cc95f1b143399b5ecebc91fa74820bfe8c1057c26471b17efa4213a09520a65e.json similarity index 64% rename from apps/labrinth/.sqlx/query-b27a1495f0a937e1bae944cfe6e46a4f702536816e5259c4aeec8b905b507473.json rename to apps/labrinth/.sqlx/query-cc95f1b143399b5ecebc91fa74820bfe8c1057c26471b17efa4213a09520a65e.json index b19b23b2a5..df982d84b2 100644 --- a/apps/labrinth/.sqlx/query-b27a1495f0a937e1bae944cfe6e46a4f702536816e5259c4aeec8b905b507473.json +++ b/apps/labrinth/.sqlx/query-cc95f1b143399b5ecebc91fa74820bfe8c1057c26471b17efa4213a09520a65e.json @@ -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" } diff --git a/apps/labrinth/.sqlx/query-d307116366d03315a8a01d1b62c5ca81624a42d01a43e1d5adf8881f8a71d495.json b/apps/labrinth/.sqlx/query-d307116366d03315a8a01d1b62c5ca81624a42d01a43e1d5adf8881f8a71d495.json new file mode 100644 index 0000000000..22fd002cab --- /dev/null +++ b/apps/labrinth/.sqlx/query-d307116366d03315a8a01d1b62c5ca81624a42d01a43e1d5adf8881f8a71d495.json @@ -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" +} diff --git a/apps/labrinth/migrations/20250919172248_affiliate_codes_managed_by_affiliates.sql b/apps/labrinth/migrations/20250919172248_affiliate_codes_managed_by_affiliates.sql new file mode 100644 index 0000000000..d3acc164fe --- /dev/null +++ b/apps/labrinth/migrations/20250919172248_affiliate_codes_managed_by_affiliates.sql @@ -0,0 +1,2 @@ +ALTER TABLE affiliate_codes +ADD COLUMN source_name VARCHAR(255) NOT NULL DEFAULT '(unnamed)'; diff --git a/apps/labrinth/src/database/models/affiliate_code_item.rs b/apps/labrinth/src/database/models/affiliate_code_item.rs index 5a4a9e63f6..8ef5ca09d2 100644 --- a/apps/labrinth/src/database/models/affiliate_code_item.rs +++ b/apps/labrinth/src/database/models/affiliate_code_item.rs @@ -9,6 +9,7 @@ pub struct DBAffiliateCode { pub created_at: DateTime, 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, 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, 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::>() @@ -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 { + 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, 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::>() diff --git a/apps/labrinth/src/models/v3/users.rs b/apps/labrinth/src/models/v3/users.rs index c658a86ec5..d8b0a2e822 100644 --- a/apps/labrinth/src/models/v3/users.rs +++ b/apps/labrinth/src/models/v3/users.rs @@ -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() } } diff --git a/apps/labrinth/src/routes/internal/affiliate.rs b/apps/labrinth/src/routes/internal/affiliate.rs index be56d9b7a8..c73858e671 100644 --- a/apps/labrinth/src/routes/internal/affiliate.rs +++ b/apps/labrinth/src/routes/internal/affiliate.rs @@ -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); +struct AdminGetAllResponse(Vec); -async fn code_get_all( +async fn admin_get_all( req: HttpRequest, pool: web::Data, redis: web::Data, session_queue: web::Data, -) -> Result, ApiError> { +) -> Result, ApiError> { let (_, user) = get_user_from_headers( &req, &**pool, @@ -64,24 +67,25 @@ async fn code_get_all( .map(AdminAffiliateCode::from) .collect::>(); - 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, redis: web::Data, session_queue: web::Data, - body: web::Json, -) -> Result, ApiError> { + body: web::Json, +) -> Result, 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, redis: web::Data, session_queue: web::Data, -) -> Result, ApiError> { +) -> Result, 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, @@ -201,14 +206,14 @@ async fn code_delete( } #[derive(Serialize)] -struct SelfGetResponse(Vec); +struct SelfGetAllResponse(Vec); -async fn self_get( +async fn self_get_all( req: HttpRequest, pool: web::Data, redis: web::Data, session_queue: web::Data, -) -> Result, ApiError> { +) -> Result, 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::>(); - 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, + redis: web::Data, + session_queue: web::Data, + body: web::Json, +) -> Result { + 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, + redis: web::Data, + session_queue: web::Data, +) -> Result { + 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) + } }