Skip to content
Snippets Groups Projects
Select Git revision
  • 9389d068d8d32f02c5013e9c9a5756d2e2a75076
  • master default protected
  • intros
  • live_sources
  • bootstrap4
  • modules
6 results

moderator.js

Blame
  • 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 "";
    	}
    }