Select Git revision
moderator.js
Forked from
Video AG Infrastruktur / website
Source project has a limited visibility.
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
+server.js 7.29 KiB
import { getPriceModifier } from "../../admin/api/articles";
import { Flags } from "$lib/flags";
import { getArticles, getBuyableVouchers } from "../articles";
import { buyArticles, transferMoney, useVoucher } from "../transactions";
import { getUserById } from "../users";
import { Codes } from "$lib/customcodes";
import { AllOrNothingRateLimiter, SlidingWindowRateLimiter } from "../../../lib/ratelimit";
import { sendNotification } from "$lib/server/notifications/handler";
import { NotificationType } from "$lib/notifications/notificationTypes";
function isValidCart(cart, articles, voucherCart, vouchers, priceModifier){
return cart.every(item => {
if(item.code == Codes.Pfand && item.price > 0 && item.premium == 0) return true;
if(item.code == Codes.Custom && item.price > 0 && item.premium == 0) return true;
const article = articles[item.code];
if (!article) return false;
return item.price === article.price && item.premium === priceModifier(article.price);
}) && voucherCart.every(item => {
const voucher = vouchers[item.code];
if (!voucher) return false;
return item.price === voucher.amount && item.premium == 0;
});
}
const voucherRedeemRateLimiter = new AllOrNothingRateLimiter(
new SlidingWindowRateLimiter({duration: 60*1000, max: 5}), // 5 per minute
new SlidingWindowRateLimiter({duration: 60*60*1000, max: 20}), // 20 per hour
new SlidingWindowRateLimiter({duration: 24*60*60*1000, max: 50}) // 50 per day
);
export async function POST(event) {
switch(event.params.slug){
case "buy": {
try{
const data = await event.request.json();
if(!data.items || !data.vouchers) return new Response(JSON.stringify({message: "Missing required fields"}), {headers: {'content-type': 'application/json', 'status': 400}});
if(!Array.isArray(data.items) || !Array.isArray(data.vouchers) || (data.items.length === 0 && data.vouchers.length === 0)) return new Response(JSON.stringify({message: "Invalid data"}), {headers: {'content-type': 'application/json', 'status': 400}});
const user = event.locals.user;
// only fetch if needed
const articles = data.items.length == 0 ? {} : (await getArticles()).reduce((acc, item) => {acc[item.code] = item; return acc;}, {});
const vouchers = data.vouchers.length == 0 ? {} : (await getBuyableVouchers()).reduce((acc, voucher) => {acc[voucher.code] = voucher; return acc;}, {});
const priceModifier = getPriceModifier(user.balance, !!(user.perms & Flags.NO_PREMIUM));
if(!isValidCart(data.items, articles, data.vouchers, vouchers, priceModifier)) return new Response(JSON.stringify({message: "Invalid cart"}), {headers: {'content-type': 'application/json', 'status': 400}});
const items = [...data.items, ...data.vouchers.map(voucher => ({...voucher, code: Codes.Voucher}))];
if(user.perms & Flags.CANT_GO_NEGATIVE && items.reduce((acc, item) => acc + item.price + item.premium, 0) > user.balance) return new Response(JSON.stringify({message: "Not enough balance"}), {headers: {'content-type': 'application/json', 'status': 400}});
const res = await buyArticles(user.id, user.card, items, data.vouchers, !!(user.perms & Flags.NO_BALANCE));
const total = items.reduce((acc, item) => acc + item.price + item.premium, 0);
// no need to await
sendNotification(user, NotificationType.BUY, {items: items.map(item=>Object.assign({}, item, {name: articles[item.code]?.name})), total, balanceBefore: user.balance, balanceAfter: user.balance - total});
return new Response(JSON.stringify(res), {headers: {'content-type': 'application/json', 'status': 200}});
}catch(e){
console.error(e);
return new Response(JSON.stringify({message: "Invalid data"}), {headers: {'content-type': 'application/json', 'status': 400}});
}
}
case "transfer": {
try{
if(event.locals.user.perms & Flags.CANT_TRANSFER) return new Response(JSON.stringify({message: "You can't transfer money"}), {headers: {'content-type': 'application/json', 'status': 403}});
const data = await event.request.json();
if(!Number.isInteger(data.amount) || data.amount <= 0 || !data.target) return new Response(JSON.stringify({message: "Missing required fields"}), {headers: {'content-type': 'application/json', 'status': 400}});
const user = event.locals.user;
if(user.id == data.target) return new Response(JSON.stringify({message: "Can't transfer to yourself"}), {headers: {'content-type': 'application/json', 'status': 400}});
if(user.balance < data.amount) return new Response(JSON.stringify({message: "Not enough balance"}), {headers: {'content-type': 'application/json', 'status': 400}});
const target = await getUserById(data.target, false, true);
if(!target) return new Response(JSON.stringify({message: "Target not found"}), {headers: {'content-type': 'application/json', 'status': 404}});
const { from } = await transferMoney(user.id, user.card, target.id, data.amount);
// no need to await
sendNotification(user, NotificationType.SEND_TRANSFER, {amount: data.amount, balanceBefore: user.balance, balanceAfter: user.balance - data.amount, receiver: {name: target.name, id: target.id}});
sendNotification(target, NotificationType.RECEIVE_TRANSFER, {amount: data.amount, balanceBefore: target.balance, balanceAfter: target.balance + data.amount, sender: {name: user.name, id: user.id}});
// don't return the to-user, because it contains the target's information
return new Response(JSON.stringify(from), {headers: {'content-type': 'application/json', 'status': 200}});
}catch(e){
return new Response(JSON.stringify({message: "Invalid data"}), {headers: {'content-type': 'application/json', 'status': 400}});
}
}
case "voucher": {
const remaining = voucherRedeemRateLimiter.consume(event.locals.user.id);
if(remaining < 0) return new Response(JSON.stringify({message: "Rate limit exceeded"}), {headers: {'content-type': 'application/json', 'status': 429}});
try{
const data = await event.request.json();
const voucherCode = data.code;
if(!voucherCode) return new Response(JSON.stringify({message: "Missing required fields"}), {headers: {'content-type': 'application/json', 'status': 400}});
if(typeof voucherCode !== "string" || voucherCode.length === 0) return new Response(JSON.stringify({message: "Invalid data"}), {headers: {'content-type': 'application/json', 'status': 400}});
const result = await useVoucher(event.locals.user.id, voucherCode);
if(typeof result === "number"){
// voucher exists but is not activated yet
return new Response(JSON.stringify({message: "Voucher not activated", canBuyToActivate: true, price: result}), {headers: {'content-type': 'application/json', 'status': 400}});
}else if(!result){
return new Response(JSON.stringify({message: "Invalid voucher"}), {headers: {'content-type': 'application/json', 'status': 400}});
}
return new Response(JSON.stringify(result), {headers: {'content-type': 'application/json', 'status': 200}});
}catch(e){
console.error(e);
return new Response(JSON.stringify({message: "Invalid data"}), {headers: {'content-type': 'application/json', 'status': 400}});
}
}
default:
return new Response("Unknown endpoint", {status: 400});
}
}
export async function GET(req){
switch(req.params.slug){
case "items":
return getArticles().then(articles => new Response(JSON.stringify(articles), {status: 200}));
default:
return "";
}
}