Skip to content
Snippets Groups Projects
Select Git revision
  • 4515566189a7af6f0a0a70e200cda228d0cf9b4a
  • master default protected
2 results

+server.js

Blame
  • Aaron Dötsch's avatar
    Aaron Dötsch authored
    Until now the timestamp of the last listed item was used to query the next entries with an earlier
    timestamp. That works well most of the time but sometimes it can happen that entries have the
    exact same timestamp, especially for item transactions for when you buy multiple products at once.
    Now it is using the ids for each type respectively and not the timestamp anymore, which should
    make it work perfectly.
    45155661
    History
    Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    +server.js 9.45 KiB
    import * as devalue from "devalue";
    import { getPriceModifier } from "../../admin/api/articles";
    import { Flags } from "$lib/flags";
    import { getArticles, getBuyableVouchers } from "../articles";
    import { buyArticles, fetchTransactions, 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}});
    				}
    				// no need to await
    				sendNotification(event.locals.user, NotificationType.USE_VOUCHER, {
    					voucher: {code: result.code, value: result.amount},
    					balanceBefore: event.locals.user.balance,
    					balanceAfter: event.locals.user.balance + result.amount
    				});
    				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}));
    		case "transactions": {
    			try {
    				const userId = parseInt(req.url.searchParams.get("userId"));
    				const balance = parseInt(req.url.searchParams.get("balance"));
    				const lastItemTransactionId = req.url.searchParams.get("lastItemTransactionId");
    				const lastMoneyTransactionId = req.url.searchParams.get("lastMoneyTransactionId");
    				const lastMoneyTransferId = req.url.searchParams.get("lastMoneyTransferId");
    				const lastVoucherUseId = req.url.searchParams.get("lastVoucherUseId");
    				const beforeIds = {
    					itemTransactions: lastItemTransactionId ? parseInt(lastItemTransactionId) : undefined,
    					moneyTransactions: lastMoneyTransactionId ? parseInt(lastMoneyTransactionId) : undefined,
    					moneyTransfers: lastMoneyTransferId ? parseInt(lastMoneyTransferId) : undefined,
    					voucherUses: lastVoucherUseId ? parseInt(lastVoucherUseId) : undefined
    				}
    				if (isNaN(userId) || isNaN(balance) || Object.values(beforeIds).some(id => id!==undefined && isNaN(id))) {
    					return new Response(JSON.stringify({ message: "Invalid fields" }), { status: 400 });
    				}
    				if(userId !== req.locals.user.id && !(req.locals.user.perms & Flags.ADMIN)){
    					return new Response(JSON.stringify({ message: "You can't view other users' transactions" }), { status: 403 });
    				}
    				try {
    					const transactions = await fetchTransactions(userId, balance, beforeIds, 100);
    					// use devalue because JSON.stringify/JSON.parse doesn't really support Date
    					return new Response(devalue.stringify(transactions), { status: 200 });
    				} catch (error) {
    					console.error(error);
    					return new Response(JSON.stringify({ error: "Internal server error" }), { status: 500 });
    				}
    			} catch (e) {
    				return new Response(JSON.stringify({ message: "Invalid data" }), { headers: { 'content-type': 'application/json', 'status': 400 } });
    			}
    		}
    		default:
    			return new Response("Unknown endpoint", {status: 400});
    	}
    }