add: transfer refunds

This commit is contained in:
trisua 2025-08-09 14:00:46 -04:00
parent 95cb889080
commit fdaae8d977
10 changed files with 340 additions and 26 deletions

View file

@ -727,6 +727,10 @@ pub fn routes() -> Router {
// transfers
.route("/transfers", post(transfers::create_request))
.route("/transfers/ask", post(transfers::ask_request))
.route(
"/transfers/{id}/refund",
post(transfers::create_refund_request),
)
// products
.route("/products", post(products::create_request))
.route("/products/{id}", delete(products::delete_request))
@ -751,6 +755,14 @@ pub fn routes() -> Router {
post(products::update_method_request),
)
.route("/products/{id}/stock", post(products::update_stock_request))
.route(
"/products/{id}/uploads",
post(products::update_uploads_request),
)
.route(
"/products/{id}/uploads/thumbnails",
delete(products::remove_thumbnail_request),
)
}
pub fn lw_routes() -> Router {
@ -1323,3 +1335,19 @@ pub struct AddAppliedConfiguration {
pub struct RemoveAppliedConfiguration {
pub id: String,
}
#[derive(Deserialize, PartialEq, Eq)]
pub enum ProductUploadTarget {
Thumbnails,
Reward,
}
#[derive(Deserialize)]
pub struct UpdateProductUploads {
pub target: ProductUploadTarget,
}
#[derive(Deserialize)]
pub struct RemoveProductThumbnail {
pub idx: usize,
}

View file

@ -1,15 +1,21 @@
use crate::{get_user_from_token, State, cookie::CookieJar};
use crate::{
cookie::CookieJar,
get_user_from_token,
image::{save_webp_buffer, JsonMultipart},
State,
};
use axum::{extract::Path, response::IntoResponse, Extension, Json};
use tetratto_core::model::{
economy::{Product, ProductFulfillmentMethod},
oauth,
permissions::FinePermission,
uploads::{MediaType, MediaUpload},
ApiReturn, Error,
};
use super::{
CreateProduct, UpdateProductData, UpdateProductDescription, UpdateProductMethod,
UpdateProductOnSale, UpdateProductPrice, UpdateProductSingleUse, UpdateProductStock,
UpdateProductTitle,
CreateProduct, ProductUploadTarget, RemoveProductThumbnail, UpdateProductData,
UpdateProductDescription, UpdateProductMethod, UpdateProductOnSale, UpdateProductPrice,
UpdateProductSingleUse, UpdateProductStock, UpdateProductTitle, UpdateProductUploads,
};
pub async fn create_request(
@ -296,3 +302,163 @@ pub async fn buy_request(
Err(e) => Json(e.into()),
}
}
const MAXIMUM_THUMBNAIL_FILE_SIZE: usize = 2_097_152;
const MAXIMUM_REWARD_FILE_SIZE: usize = 4_194_304;
/// Update the product's uploads. Only reads one multipart file entry.
pub async fn update_uploads_request(
jar: CookieJar,
Extension(data): Extension<State>,
Path(id): Path<usize>,
JsonMultipart(bytes_parts, req): JsonMultipart<UpdateProductUploads>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageProducts) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
let mut product = match data.get_product_by_id(id).await {
Ok(x) => x,
Err(e) => return Json(e.into()),
};
if user.id != product.owner && !user.permissions.check(FinePermission::MANAGE_USERS) {
return Json(Error::NotAllowed.into());
}
// apply to target
match req.target {
ProductUploadTarget::Thumbnails => {
if product.uploads.thumbnails.len() == 4 {
return Json(
Error::MiscError("Too many thumbnails exist. Please remove one".to_string())
.into(),
);
}
// create upload
let file = match bytes_parts.get(0) {
Some(x) => x,
None => return Json(Error::Unknown.into()),
};
if file.len() > MAXIMUM_THUMBNAIL_FILE_SIZE {
return Json(Error::FileTooLarge.into());
}
let upload = match data
.create_upload(MediaUpload::new(MediaType::Webp, user.id))
.await
{
Ok(x) => x,
Err(e) => return Json(e.into()),
};
product.uploads.thumbnails.push(upload.id);
// write image
if let Err(e) =
save_webp_buffer(&upload.path(&data.0.0).to_string(), file.to_vec(), None)
{
return Json(Error::MiscError(e.to_string()).into());
}
}
ProductUploadTarget::Reward => {
// remove old
if product.uploads.reward != 0 {
if let Err(e) = data.delete_upload(product.uploads.reward).await {
return Json(e.into());
}
}
// create upload
let file = match bytes_parts.get(0) {
Some(x) => x,
None => return Json(Error::Unknown.into()),
};
if file.len() > MAXIMUM_REWARD_FILE_SIZE {
return Json(Error::FileTooLarge.into());
}
let upload = match data
.create_upload(MediaUpload::new(MediaType::Webp, user.id))
.await
{
Ok(x) => x,
Err(e) => return Json(e.into()),
};
product.uploads.reward = upload.id;
// write image
if let Err(e) =
save_webp_buffer(&upload.path(&data.0.0).to_string(), file.to_vec(), None)
{
return Json(Error::MiscError(e.to_string()).into());
}
}
}
// ...
match data
.update_product_uploads(id, &user, product.uploads)
.await
{
Ok(_) => Json(ApiReturn {
ok: true,
message: "Product updated".to_string(),
payload: (),
}),
Err(e) => Json(e.into()),
}
}
pub async fn remove_thumbnail_request(
jar: CookieJar,
Extension(data): Extension<State>,
Path(id): Path<usize>,
Json(req): Json<RemoveProductThumbnail>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageProducts) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
let mut product = match data.get_product_by_id(id).await {
Ok(x) => x,
Err(e) => return Json(e.into()),
};
if user.id != product.owner && !user.permissions.check(FinePermission::MANAGE_USERS) {
return Json(Error::NotAllowed.into());
}
// remove upload
let thumbnail = match product.uploads.thumbnails.get(req.idx) {
Some(x) => x,
None => return Json(Error::GeneralNotFound("thumbnail".to_string()).into()),
};
if let Err(e) = data.delete_upload(*thumbnail).await {
return Json(e.into());
}
product.uploads.thumbnails.remove(req.idx);
// ...
match data
.update_product_uploads(id, &user, product.uploads)
.await
{
Ok(_) => Json(ApiReturn {
ok: true,
message: "Product updated".to_string(),
payload: (),
}),
Err(e) => Json(e.into()),
}
}

View file

@ -1,5 +1,5 @@
use crate::{get_user_from_token, State, cookie::CookieJar};
use axum::{response::IntoResponse, Extension, Json};
use axum::{response::IntoResponse, Extension, Json, extract::Path};
use tetratto_core::model::{
economy::{CoinTransfer, CoinTransferMethod, CoinTransferSource},
oauth,
@ -75,3 +75,46 @@ pub async fn ask_request(
Err(e) => Json(e.into()),
}
}
pub async fn create_refund_request(
jar: CookieJar,
Extension(data): Extension<State>,
Path(id): Path<usize>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserSendCoins) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
let other_transfer = match data.get_transfer_by_id(id).await {
Ok(x) => x,
Err(e) => return Json(e.into()),
};
if user.id != other_transfer.receiver {
// only the receiver of the funds can issue a refund (atm)
return Json(Error::NotAllowed.into());
}
match data
.create_transfer(
&mut CoinTransfer::new(
other_transfer.receiver,
other_transfer.sender,
other_transfer.amount,
CoinTransferMethod::Transfer,
CoinTransferSource::Refund,
),
true,
)
.await
{
Ok(s) => Json(ApiReturn {
ok: true,
message: "Transfer created".to_string(),
payload: s.to_string(),
}),
Err(e) => Json(e.into()),
}
}