Webserver w/ api-routes, controllers, middleware & cart websocket server

This commit is contained in:
2022-12-11 18:16:40 +01:00
parent 099c3a59ac
commit 57327e63d1
12 changed files with 1246 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
import cookie from "cookie";
import type { Request, Response, NextFunction } from "express";
const cookieOptions = {
path: "/",
maxAge: 60 * 60 * 24 * 7,
};
function addAdminCookie(res: Response) {
const adminCookie = cookie.serialize("admin", true, cookieOptions);
res.setHeader("Set-Cookie", adminCookie);
}
function deleteAdminCookie(res: Response) {
const adminCookie = cookie.serialize("admin", false, {
path: "/",
maxAge: 1,
});
res.setHeader("Set-Cookie", adminCookie);
}
function login(req: Request, res: Response) {
const { username, password } = req.body;
if (username !== "admin" || password !== "admin") {
return res.status(403).send({
success: false,
message: "Feil brukernavn eller passord",
});
}
addAdminCookie(res);
res.send({
success: true,
message: "Velkommen!",
});
}
function logout(req: Request, res: Response) {
deleteAdminCookie(res);
res.status(200).send("ok");
}
export default { login, logout };

View File

@@ -0,0 +1,381 @@
import logger from "../../logger";
import OrderRepository from "../../order";
import CustomerRepository from "../../customer";
import ProductRepository from "../../product";
import WarehouseRepository from "../../warehouse";
import Cart from "../../cart/Cart";
import ICustomer from "../../interfaces/ICustomer";
import { validEmail } from "../../utils/formValidation";
import ICart from "../../interfaces/ICart";
const orderRepository = new OrderRepository();
const customerRepository = new CustomerRepository();
const productRepository = new ProductRepository();
const warehouseRepository = new WarehouseRepository();
async function validateCart(cart: ICart[]) {
const validationErrors = [];
for (let i = 0; i < cart.length; i++) {
const { sku_id, quantity, name, lineitem_id } = cart[i];
const product = await productRepository.get(sku_id);
if (!product) {
validationErrors.push({
type: "order-summary",
field: `lineitem-${lineitem_id}`,
message: `Fant ikke produktet ${name}.`,
});
}
// check if in stock
const leftInStockResponse = await warehouseRepository.checkSkuStock(sku_id);
if (!leftInStockResponse || quantity > leftInStockResponse?.stock) {
validationErrors.push({
type: "order-summary",
field: `lineitem-${lineitem_id}`,
message: `Det er bare ${leftInStockResponse?.stock} igjen av denne varen på lager.`,
});
}
}
return validationErrors;
}
function validateCustomer(customer: ICustomer) {
const {
email,
first_name,
last_name,
street_address,
zip_code,
city,
} = customer;
const validationErrors = [];
if (!email?.length) {
validationErrors.push({
type: "customer",
field: "email",
message: "Epost adresse er påkrevd",
});
}
if (!validEmail(email)) {
validationErrors.push({
type: "customer",
field: "email",
message: "Epost addressen er ikke gyldig",
});
}
if (!first_name.length) {
validationErrors.push({
type: "customer",
field: "first_name",
message: "Fornavn er påkrevd",
});
}
if (!last_name.length) {
validationErrors.push({
type: "customer",
field: "last_name",
message: "Etternavn er påkrevd",
});
}
if (!street_address.length) {
validationErrors.push({
type: "customer",
field: "street_address",
message: "Gateadresse er påkrevd",
});
}
const _zipcode = String(zip_code || "");
if (!_zipcode.length) {
validationErrors.push({
type: "customer",
field: "zip_code",
message: "Postnummer er påkrevd",
});
} else if (_zipcode.length !== 3 && _zipcode.length !== 4) {
validationErrors.push({
type: "customer",
field: "zip_code",
message: "Postnummer må være 4 siffer",
});
}
if (!city.length) {
validationErrors.push({
type: "customer",
field: "city",
message: "By er påkrevd",
});
}
return validationErrors;
}
async function getAll(req, res) {
logger.info("Getting all orders");
try {
const orders = await orderRepository.getAll();
res.send({
success: true,
orders,
});
} catch (error) {
logger.error("Error while getting all orders", { error });
res.statusCode = error?.statusCode || 500;
res.send({
success: false,
message: "Unexpected error while getting all orders",
});
}
}
// interface IOrderFormError {
// type: string (enum)
// field: string (enum) // the field is linked to html form name
// message: string
// }
async function createOrder(req, res) {
const cart: ICart[] = req.body?.cart;
const customer: ICustomer = req.body?.customer;
logger.info("Submitting new order", { customer, cart });
// check if product exists
let validationErrors = [];
try {
validationErrors = validationErrors.concat(validateCustomer(customer));
validationErrors = validationErrors.concat(await validateCart(cart));
} catch (error) {
logger.error("Error while validation order", { error });
res.statusCode = error?.statusCode || 500;
return res.send({
success: false,
form_input: null,
message: error?.message || "Unable to validate order",
});
}
if (validationErrors.length) {
logger.error("Validation error when submitting order", {
validationErrors,
});
res.statusCode = 400;
return res.send({
success: false,
validationErrors,
});
}
try {
const { customer_no } = await customerRepository.newCustomer(customer);
const { order_id } = await orderRepository.newOrder(customer_no);
await Promise.all(
cart.map((lineItem) =>
orderRepository.addOrderLineItem(
order_id,
lineItem.product_no,
lineItem.sku_id,
lineItem.price,
lineItem.quantity
)
)
);
logger.info("Sucessfully created order", { order_id, customer_no });
return res.send({
success: true,
message: "Sucessfull created order!",
order_id,
customer_no,
});
} catch (error) {
logger.error("Error while creating customer or order", { error });
res.statusCode = error?.statusCode || 500;
return res.send({
success: false,
message: error?.message || "Unexpected error while creating order",
});
}
}
async function get(req, res) {
const { order_id } = req.params;
logger.info("Getting order by id", { order_id });
// get a order
let order = null;
try {
order = await orderRepository.getOrderDetailed(order_id);
logger.info("Found order", { order });
res.send({
success: true,
order,
});
} catch (error) {
logger.error("Error while looking for order", { order_id, error });
res.statusCode = error.statusCode || 500;
res.send({
success: false,
message: error?.message || "Unexpected error while getting order",
});
}
}
async function getOrderStatus(req, res) {
const { order_id } = req.params;
logger.info("Getting order status by id", { order_id });
return await orderRepository.getOrderStatus(order_id).then((orderStatus) => {
logger.info("Found order status", { order_id, orderStatus });
if (orderStatus) {
res.send({
status: orderStatus?.status,
order_id,
success: true,
});
} else {
logger.error("Error while getting order status", { order_id });
res.status(500).send({
initiated: null,
confirmed: null,
message: "Unexpeted error! Unable to get order status",
success: false,
});
}
});
}
// async function cancelOrder(req, res) {
// const { id } = req.params;
// let orderId = id;
// const vippsId = id;
// await vippsRepository.getOrder(id).then((order) => {
// if (order && order.parent_order_id) {
// orderId = order.parent_order_id;
// }
// });
// return vippsRepository
// .cancelOrRefundPartialOrder(vippsId, req.id)
// .then((order) => PlsController.turnOff(order))
// .then((_) =>
// orderRepository.cancelOrder(orderId).then((canceled) =>
// res.send({
// success: true,
// canceled: canceled,
// })
// )
// )
// .catch((error) => {
// throw error;
// });
// }
// function verifyNoCollidingOrders(id) {
// return orderRepository
// .getOrder(id)
// .then((order) => orderRepository.getConflictingProductOrders(order))
// .then(({ order, conflicting }) => checkForConflicting(order, conflicting));
// }
// function checkForConflicting(order, conflicting) {
// const thisOrderCreated = new Date(order.created);
// for (var i = 0; i < conflicting.length; i++) {
// const thisConflictinCreated = new Date(conflicting[i].created);
// // if we are anywhere with a conflict, we need to cancel/refund this
// if (thisOrderCreated > thisConflictinCreated) {
// throw new WagoError(
// WAGO_ERROR_STATUS_CODES.CONFLICTING_ORDER_RESERVATION
// );
// }
// }
// return order;
// }
// async function extendOrder(req, res) {
// let { amount } = req.body;
// let extendeeOrderId = req.params.id;
// let extendedTimes = 1;
// const previousExtendedAndPreviousOrder = await vippsRepository
// .getOrder(extendeeOrderId)
// .then((order) => {
// if (order.parent_order_id) {
// extendeeOrderId = order.parent_order_id;
// }
// return orderRepository.getExtendedOrders(extendeeOrderId);
// });
// let orderId = `${extendeeOrderId}-ext-`;
// if (!previousExtendedAndPreviousOrder || amount < 0) {
// res.status(404).send({ success: false });
// return;
// }
// for (let i = 0; i < previousExtendedAndPreviousOrder.length; i++) {
// const currentElement = previousExtendedAndPreviousOrder[i];
// if (currentElement.order_id !== extendeeOrderId) {
// extendedTimes += 1;
// }
// }
// orderId += extendedTimes;
// const orderWithProductData = await orderRepository.getOrderWithProduct(
// extendeeOrderId
// );
// try {
// amount = parseFloat(amount);
// } catch (e) {}
// const moneyToPay = orderWithProductData.price * 100 * amount;
// return vippsRepository
// .newExtendedPayment(
// orderId,
// extendeeOrderId,
// moneyToPay,
// amount,
// orderWithProductData
// )
// .then((resp) => {
// res.send({
// success: true,
// ...resp,
// });
// })
// .catch((error) => {
// res.statusCode = error.statusCode || 500;
// res.send({
// success: false,
// ...error,
// });
// });
// }
export default {
createOrder,
get,
getAll,
getOrderStatus,
// cancelOrder,
extendOrder: () => {},
checkForConflicting: () => {},
verifyNoCollidingOrders: () => {},
};

View File

@@ -0,0 +1,237 @@
import logger from "../../logger";
import ProductRepository from "../../product";
const productRepository = new ProductRepository();
import type { Request, Response } from "express";
async function add(req: Request, res: Response) {
logger.info("Adding new product");
try {
const productId = await productRepository.add();
const product = await productRepository.get(productId);
logger.info("New product", { product });
return res.send({
success: true,
product,
});
} catch (error) {
logger.error("Error while adding product", { error });
res.statusCode = error.statusCode || 500;
return res.send({
success: false,
message: error?.message || "Unexpected error while adding product",
});
}
}
function update(req: Request, res: Response) {
const { product_id } = req.params;
logger.info("Updating product", { product_id });
return productRepository
.get(product_id)
.then((product) => {
logger.info("Updated product", { product, product_id });
res.send({
success: true,
product: product,
});
})
.catch((error) => {
logger.error("Error while updating product", { error, product_id });
res.statusCode = error.statusCode || 500;
return res.send({
success: false,
message: error?.message || "Unexpected error while updating product",
});
});
}
function getAll(req: Request, res: Response) {
logger.info("Getting all products");
return productRepository
.getAllProducts()
.then((products) => {
logger.info("Found products", { products });
res.send({
success: true,
products: products,
});
})
.catch((error) => {
logger.error("Error while getting all products", { error });
res.statusCode = error.statusCode || 500;
res.send({
success: false,
message:
error?.message || "Unexpected error while getting all products",
});
});
}
function getById(req: Request, res: Response) {
const { product_id } = req.params;
logger.info("Getting product", { product_id });
return productRepository
.get(product_id)
.then((product) => {
logger.info("Found product", { product, product_id });
res.send({
success: true,
product: product,
});
})
.catch((error) => {
logger.error("Error while getting product by id", { product_id });
res.statusCode = error.statusCode || 500;
res.send({
success: false,
message:
error?.message || "Unexpected error while getting product by id",
});
});
}
async function addSku(req: Request, res: Response) {
const { product_id } = req.params;
logger.info("Adding new sku", { product_id });
try {
await productRepository.addSku(product_id);
const skus = await productRepository.getSkus(product_id);
if (!skus.find((sku) => sku.default_price === true)) {
const setDefaultResponse = await productRepository.setSkuDefaultPrice(
product_id,
skus[skus.length - 1].sku_id
);
skus[skus.length - 1].default_price = true;
}
logger.info("New skus after add", { skus, product_id });
res.send({
success: true,
skus,
});
} catch (error) {
logger.error("Error adding sku", { error, product_id });
res.statusCode = error?.statusCode || 500;
res.send({
success: false,
message: error?.message || "Unexpected error while adding new sku",
});
}
}
async function getSkus(req: Request, res: Response) {
const { product_id } = req.params;
const skus = await productRepository.getSkus(product_id);
return res.send({
success: true,
skus,
});
}
async function updateSku(req: Request, res: Response) {
const { product_id, sku_id } = req.params;
const { stock, size, price } = req.body;
logger.info("Updating sku", { product_id, sku_id, stock, price, size });
try {
await productRepository.updateSku(product_id, sku_id, stock, size, price);
const skus = await productRepository.getSkus(product_id);
logger.info("New skus after update", { skus, product_id, sku_id });
res.send({
success: true,
skus,
});
} catch (error) {
logger.error("Error updating sku", { product_id, sku_id, error });
res.statusCode = error?.statusCode || 500;
res.send({
success: false,
message: error?.message || "Unexpected error while updating sku",
});
}
}
async function deleteSku(req: Request, res: Response) {
const { product_id, sku_id } = req.params;
try {
await productRepository.deleteSku(product_id, sku_id);
const skus = await productRepository.getSkus(product_id);
logger.info("New skus after delete", { skus, product_id, sku_id });
res.send({
success: true,
skus,
});
} catch (error) {
logger.error("Error deleting sku", { product_id, sku_id, error });
res.statusCode = error?.statusCode || 500;
res.send({
success: false,
message: error?.message || "Unexpected error while deleting sku",
});
}
}
async function setSkuDefaultPrice(req: Request, res: Response) {
const { product_id, sku_id } = req.params;
logger.info("Updating sku default price", { product_id, sku_id });
try {
await productRepository.setSkuDefaultPrice(product_id, sku_id);
const skus = await productRepository.getSkus(product_id);
logger.info("New skus after update default price", {
skus,
product_id,
sku_id,
});
res.send({
success: true,
skus,
});
} catch (error) {
logger.error("Error while updating sku default price", {
product_id,
sku_id,
error,
});
res.statusCode = error?.statusCode || 500;
res.send({
success: false,
message:
error?.message ||
"Unexpected error while updating default price for sku",
});
}
}
export default {
add,
update,
getAll,
getById,
addSku,
getSkus,
updateSku,
deleteSku,
setSkuDefaultPrice,
};

View File

@@ -0,0 +1,94 @@
import Configuration from "../../config/configuration";
import StripeApi from "../../stripe/stripeApi";
import StripeRepository from "../../stripe/stripeRepository";
import OrderRepository from "../../order";
import CustomerRepository from "../../customer";
import Stripe from "stripe";
import type { Request, Response, NextFunction } from "express";
import type { IOrder, ILineItem } from "../../interfaces/IOrder";
import type ICustomer from "../../interfaces/ICustomer";
const configuration = Configuration.getInstance();
const stripePublicKey = configuration.get("stripe", "publicKey");
const stripeSecretKey = configuration.get("stripe", "secretKey");
const stripeApi = new StripeApi(stripePublicKey, stripeSecretKey);
const stripeRepository = new StripeRepository();
const orderRepository = new OrderRepository();
const customerRepository = new CustomerRepository();
async function create(req, res) {
const clientId = req?.planetId;
const { order_id, customer_no } = req.body;
if (!order_id || !customer_no) {
return res.status(400).send({
success: false,
message: "Missing order_id and/or customer_id",
});
}
const order: IOrder = await orderRepository.getOrder(order_id);
const customer: ICustomer = await customerRepository.getCustomer(customer_no);
const sum = order.lineItems?.reduce(
(total, lineItem: ILineItem) => total + lineItem.quantity * lineItem.price,
0
);
stripeRepository
.createPayment(clientId, sum, order_id, customer)
.then((clientSecret) =>
res.send({
success: true,
clientSecret,
})
);
}
async function updatePayment(req: Request, res: Response) {
console.log("STRIPE WEBHOOK body:", req.body);
const { type, data } = req.body;
const { object } = data;
const { orderId } = object?.metadata;
if (!data) {
console.log("no data found in webhook, nothing to do");
return res.status(200).send("ok");
}
if (!orderId) {
console.log("no order_id found in webhook, nothing to do");
return res.status(200).send("ok");
}
if (type === "payment_intent.created") {
console.log("handle payment intent created... doing nothing");
} else if (type === "payment_intent.succeeded") {
console.log("handle payment succeeded", object);
await stripeRepository.updatePaymentIntent(object);
orderRepository.confirmOrder(orderId);
} else if (type === "payment_intent.payment_failed") {
console.log("handle payment failed", object);
await stripeRepository.updatePaymentIntent(object);
orderRepository.cancelOrder(orderId);
} else if (type === "charge.succeeded") {
console.log("handle charge succeeded", object);
await stripeRepository.updatePaymentCharge(object);
} else if (type === "charge.refunded") {
console.log("handle charge refunded", object);
await stripeRepository.updatePaymentCharge(object);
await orderRepository.refundOrder(orderId);
} else {
console.log(`webhook for ${type}, not setup yet`);
}
// should always return 200 but should try catch and log error
return res.status(200).send("ok");
}
export default {
create,
updatePayment,
};

View File

@@ -0,0 +1,190 @@
const { VippsApiResourceNotFound } = require(`${__base}/vipps/vippsApiErrors`);
const { WagoError, WAGO_ERROR_STATUS_CODES } = require(`${__base}/errors`);
const { VIPPS_STATUS_CODES } = require(`${__enums}/vipps`);
const OrderRepository = require(`${__base}/order`);
const OrderController = require(`${__controllers}/orderController`);
const orderRepository = new OrderRepository();
const Vipps = require(`${__base}/vipps`);
const VippsRepository = require(`${__base}/vipps/vippsRepository`);
const vipps = new Vipps();
const vippsRepository = new VippsRepository();
function getPaymentDetails(req, res) {
console.log("hit get details payment");
const { id } = req.params;
return vipps
.getPayment(id)
.then((payment) =>
res.send({
success: true,
payment,
})
)
.catch((err) => {
const { statusCode, message } = err;
return res.status(statusCode || 500).send({
success: false,
payment: null,
message: message,
});
});
}
function cancelPayment(req, res) {
console.log("hit cancelOrder endpoint");
const { id } = req.params;
return vippsRepository.cancelPayment(id).then((canceled) =>
res.send({
success: true,
canceled: canceled,
})
);
}
function rejectPayment(id, amount) {
console.log("rejectPayment");
return vippsRepository
.cancelPayment(id)
.then((_) =>
vippsRepository.updatePaymentStatus(
id,
VIPPS_STATUS_CODES.REJECTED,
amount
)
)
.then((_) => orderRepository.rejectByVippsTimeout(id));
}
function rejectByVippsTimeout(id, amount) {
console.log("reject-by-timeout");
return vippsRepository
.cancelPayment(id)
.then((_) =>
vippsRepository.updatePaymentStatus(
id,
VIPPS_STATUS_CODES.TIMED_OUT_REJECT,
amount
)
)
.then((_) => orderRepository.rejectByVippsTimeout(id));
}
function updatePayment(req, res) {
console.log("updatePayment:", req.body);
const { id } = req.params;
const {
transactionInfo: { status },
errorInfo,
} = req.body;
let statusCode;
if (status && VIPPS_STATUS_CODES[status]) {
statusCode = VIPPS_STATUS_CODES[status];
}
// TODO: Don't cancel whole order when one new order
// fails/gets rejected/cancels
if (status === VIPPS_STATUS_CODES.REJECTED) {
switch (errorInfo.errorCode) {
case 45:
default:
return vippsRepository
.cancelOrRefundPartialOrder(id)
.then((_) => orderRepository.cancelOrder(id));
}
} else if (status === VIPPS_STATUS_CODES.CANCELLED) {
return vippsRepository
.cancelOrRefundPartialOrder(id)
.then((_) => orderRepository.cancelOrder(id));
}
return vippsRepository
.getOrder(id)
.then((order) => extendOrUpdateNewOrder(order, statusCode, req))
.then((_) => res.status(200).send())
.catch((error) => {
console.log("we errored here", error);
return vippsRepository
.cancelOrRefundPartialOrder(id)
.then((_) => orderRepository.cancelOrder(id));
});
}
function extendOrUpdateNewOrder(order, statusCode, req) {
const {
transactionInfo: { transactionId, amount },
} = req.body;
const vippsId = order.order_id;
let orderId = order.order_id;
if (order.parent_order_id) {
orderId = order.parent_order_id;
}
return vippsRepository
.updatePaymentStatus(vippsId, statusCode, amount, transactionId)
.then((_) => {
if (order.parent_order_id) {
return getNewTimeToTurnOn(order);
// return getNewTimeToTurnOn(order).then(hours =>
// PlsController.extendTime(hours, order)
// );
}
return OrderController.verifyNoCollidingOrders(orderId);
// return OrderController.verifyNoCollidingOrders(orderId).then(
// async order => await PlsController.sendDuration(order)
// );
})
.then(async (plsResponse) => {
if (plsResponse.startTime) {
await orderRepository.updateStartTime(orderId, plsResponse.startTime);
}
return orderRepository
.updateEndTime(orderId, plsResponse.endTime)
.then((_) =>
vippsRepository.updateEndTime(vippsId, plsResponse.endTime)
);
})
.then((_) => vipps.captureAmount(vippsId, amount, req.id))
.then((capture) =>
vippsRepository.updatePaymentCapture(
vippsId,
capture.transactionSummary.capturedAmount
)
);
}
async function getNewTimeToTurnOn(order) {
const previousExtendedAndPreviousOrder = await orderRepository.getExtendedOrders(
order.parent_order_id
);
let hoursToExpandAndSendToPls = 0;
let currentEndTime;
const now = new Date();
for (let i = 0; i < previousExtendedAndPreviousOrder.length; i++) {
const currentElement = previousExtendedAndPreviousOrder[i];
if (currentElement.order_id === order.parent_order_id) {
currentEndTime = new Date(currentElement.end_time);
if (currentEndTime < now) {
currentEndTime = now;
}
}
}
const timeLeft = (currentEndTime.getTime() - now.getTime()) / 36e5;
hoursToExpandAndSendToPls = order.hours + timeLeft;
return hoursToExpandAndSendToPls;
}
module.exports = {
getPaymentDetails,
cancelPayment,
extendOrUpdateNewOrder,
updatePayment,
getNewTimeToTurnOn,
};

View File

@@ -0,0 +1,12 @@
const Vipps = require(`${__base}/vipps`);
const vipps = new Vipps();
import type { Request, Response } from "express";
export default function VippsTokenController(req: Request, res: Response) {
return vipps.token.then((token) =>
res.send({
success: true,
token: token,
})
);
}

View File

@@ -0,0 +1,65 @@
import logger from "../../logger";
import WarehouseRepository from "../../warehouse";
const warehouseRepository = new WarehouseRepository();
import type { Request, Response } from "express";
function getAll(req: Request, res: Response) {
logger.info("Fething all warehouse products");
return warehouseRepository
.all()
.then((warehouseProucts) => {
logger.info("Found warehouse products", { products: warehouseProucts });
res.send({
success: true,
warehouse: warehouseProucts,
});
})
.catch((error) => {
logger.error("Error fetching warehouse products", { error });
res.statusCode = error.statusCode || 500;
res.send({
success: false,
message:
error?.message ||
"Unexpected error while fetching all warehouse products",
});
});
}
function getProduct(req: Request, res: Response) {
const { productId } = req.params;
logger.info("Fetching warehouse product", { product_id: productId });
return warehouseRepository
.getProduct(productId)
.then((product) => {
logger.info("Found warehouse product", {
product,
product_id: productId,
});
res.send({
success: true,
product: product,
});
})
.catch((error) => {
logger.error("Error fetching warehouse product:", {
error,
product_id: productId,
});
res.statusCode = error.statusCode || 500;
res.send({
success: false,
message:
error?.message ||
`Unexpected error while fetching product with id: ${productId}`,
});
});
}
export default { getAll, getProduct };

View File

@@ -0,0 +1,37 @@
import cookie from "cookie";
import generateUUID from "../../utils/generateUUID";
import httpContext from "express-http-context";
import type { Request, Response, NextFunction } from "express";
const cookieClientKey = "planetId";
const cookieOptions = {
path: "/",
maxAge: 60 * 60 * 24 * 7, // 7 days
};
function setClientIdCookieHeader(res: Response, value: string) {
const setCookie = cookie.serialize("planetId", value, cookieOptions);
return res.setHeader("Set-Cookie", setCookie);
}
const getOrSetCookieForClient = (
req: Request,
res: Response,
next: NextFunction
) => {
const cookies = cookie.parse(req.headers.cookie || "");
const planetId = cookies[cookieClientKey];
if (planetId) {
req.planetId = planetId;
httpContext.set("planetId", planetId);
return next();
}
const clientId = generateUUID();
setClientIdCookieHeader(res, clientId);
next();
};
export default getOrSetCookieForClient;

View File

@@ -0,0 +1,8 @@
import type { Request, Response, NextFunction } from "express";
const openCORS = (req: Request, res: Response, next: NextFunction) => {
res.set("Access-Control-Allow-Origin", "*");
return next();
};
export default openCORS;

View File

@@ -0,0 +1,43 @@
import type { Request, Response, NextFunction } from "express";
const camelToKebabCase = (str: string) =>
str.replace(/[A-Z]/g, (letter: string) => `-${letter.toLowerCase()}`);
const mapFeaturePolicyToString = (features) => {
return Object.entries(features)
.map(([key, value]) => {
key = camelToKebabCase(key);
value = value == "*" ? value : `'${value}'`;
return `${key} ${value}`;
})
.join("; ");
};
const setupHeaders = (req: Request, res: Response, next: NextFunction) => {
res.set("Access-Control-Allow-Headers", "Content-Type");
res.set("Access-Control-Allow-Methods", "POST, PATCH, DELETE");
// Security
res.set("X-Content-Type-Options", "nosniff");
res.set("X-XSS-Protection", "1; mode=block");
res.set("X-Frame-Options", "SAMEORIGIN");
res.set("X-DNS-Prefetch-Control", "off");
res.set("X-Download-Options", "noopen");
res.set("Strict-Transport-Security", "max-age=15552000; includeSubDomains");
// Feature policy
const features = {
fullscreen: "*",
payment: "none",
microphone: "none",
camera: "self",
speaker: "*",
syncXhr: "self",
};
const featureString = mapFeaturePolicyToString(features);
res.set("Feature-Policy", featureString);
return next();
};
export default setupHeaders;

79
src/webserver/server.ts Normal file
View File

@@ -0,0 +1,79 @@
// import * as global from "../types/global";
import path from "path";
import express from "express";
import { createServer } from "http";
// services
import logger from "../logger";
import { setupCartWebsocketServer } from "./websocketCartServer";
// controllers
// const TokenController = require(`${__controllers}/tokenController`);
import OrderController from "./controllers/orderController";
import ProductController from "./controllers/productController";
import WarehouseController from "./controllers/warehouseController";
import StripePaymentController from "./controllers/stripePaymentController";
import LoginController from "./controllers/loginController";
// middleware
import httpContext from "express-http-context";
import setupCORS from "./middleware/setupCORS";
import setupHeaders from "./middleware/setupHeaders";
import getOrSetCookieForClient from "./middleware/getOrSetCookieForClient";
const app = express();
const port = process.env.PORT || 30010;
app.use(httpContext.middleware);
app.use(setupCORS);
app.use(setupHeaders);
app.use(getOrSetCookieForClient);
// parse application/json
const router = express.Router();
router.use(express.json());
router.post("/login", LoginController.login);
router.post("/logout", LoginController.logout);
router.get("/products", ProductController.getAll);
router.post("/product", ProductController.add);
router.get("/product/:product_id", ProductController.getById);
router.post("/product/:product_id/sku", ProductController.addSku);
router.patch("/product/:product_id/sku/:sku_id", ProductController.updateSku);
router.delete("/product/:product_id/sku/:sku_id", ProductController.deleteSku);
router.post(
"/product/:product_id/sku/:sku_id/default_price",
ProductController.setSkuDefaultPrice
);
router.get("/orders", OrderController.getAll);
router.post("/order", OrderController.createOrder);
router.get("/order/:order_id", OrderController.get);
router.get("/order/:order_id/status", OrderController.getOrderStatus);
router.get("/warehouse", WarehouseController.getAll);
router.get("/warehouse/:productId", WarehouseController.getProduct);
// router.get("/api/order/:id", OrderController.getOrderById);
// router.post("/api/order/:id/cancel", OrderController.cancelOrder);
// router.post("/api/order/:id/extend", OrderController.extendOrder);
router.post("/payment/stripe", StripePaymentController.create);
router.post("/webhook/stripe", StripePaymentController.updatePayment);
// router.get("/api/payment/vipps/token", VippsTokenController);
// router.get("/api/payment/:id/details", VippsPaymentController.getPaymentDetails);
// router.post(
// "/api/payment/callback/v2/payments/:id",
// VippsPaymentController.updatePayment
// );
router.get("/", (req, res) => res.send("hello"));
app.use("/api", router);
const server = createServer(app);
server.listen(port, () => logger.info(`Server started, listening at :${port}`));
setupCartWebsocketServer(server);

View File

@@ -0,0 +1,56 @@
import cookie from "cookie";
import { ClientRequest } from "http";
import { WebSocketServer, Websocket, Request } from "ws";
import WSCart from "../cart/WSCart";
import CartSession from "../cart/CartSession";
import generateUUID from "../utils/generateUUID";
function getCookieValue(cookieString: string, key: string): string | null {
const cookies = cookie.parse(cookieString || "");
return cookies[key] || null;
}
function getHeaderValue(url: string, key: string) {
const urlSegments = url.split("?");
if (urlSegments?.length < 2) return;
const query = new URLSearchParams(urlSegments[1]);
return query.get(key);
}
function setupCartWebsocketServer(server) {
const WS_OPTIONS = { server };
const wss = new WebSocketServer(WS_OPTIONS);
const cartSession = new CartSession();
// setInterval(() => cartSession.listCarts(), 3000);
setInterval(() => cartSession.removeIfNotAlive(), 1000);
wss.on("connection", (ws, req) => {
const sessionId = generateUUID();
let clientId =
getCookieValue(req.headers.cookie, "planetId") ||
getHeaderValue(req.url, "planetId");
if (clientId === null) return;
const wsCart = new WSCart(ws, clientId);
wsCart.cartSession = cartSession;
cartSession.add(sessionId, wsCart);
ws.on("message", (data, isBinary) => wsCart.handleMessage(data, isBinary));
ws.on("close", () => {
cartSession.remove(sessionId);
console.log("the client has closed the connection");
});
ws.onerror = function (error) {
console.log("unexpected ws error occured:", error);
};
});
console.log("Booted websocket cart");
}
export { setupCartWebsocketServer };