Initial content
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
# secret used to sign session cookies
|
||||
COOKIE_SECRET="sportsstoresecret"
|
||||
|
||||
GOOGLE_CLIENT_ID=<enter your ID>
|
||||
GOOGLE_CLIENT_SECRET=<enter your secret>>
|
||||
@@ -0,0 +1,54 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createAuthentication = void 0;
|
||||
const config_1 = require("./config");
|
||||
const passport_1 = __importDefault(require("passport"));
|
||||
const passport_google_oauth20_1 = require("passport-google-oauth20");
|
||||
const data_1 = require("./data");
|
||||
const callbackURL = (0, config_1.getConfig)("auth:openauth:redirectionUrl");
|
||||
const clientID = (0, config_1.getSecret)("GOOGLE_CLIENT_ID");
|
||||
const clientSecret = (0, config_1.getSecret)("GOOGLE_CLIENT_SECRET");
|
||||
const authCallbackURL = (0, config_1.getConfig)("admin:openauth:redirectionUrl");
|
||||
const createAuthentication = (app) => {
|
||||
passport_1.default.use("admin-auth", new passport_google_oauth20_1.Strategy({
|
||||
clientID, clientSecret, callbackURL: authCallbackURL,
|
||||
scope: ["email", "profile"],
|
||||
state: true
|
||||
}, (accessToken, refreshToken, profile, callback) => {
|
||||
return callback(null, {
|
||||
name: profile.displayName,
|
||||
email: profile.emails?.[0].value ?? "",
|
||||
federatedId: profile.id,
|
||||
adminUser: true
|
||||
});
|
||||
}));
|
||||
passport_1.default.use(new passport_google_oauth20_1.Strategy({
|
||||
clientID, clientSecret, callbackURL,
|
||||
scope: ["email", "profile"],
|
||||
state: true
|
||||
}, async (accessToken, refreshToken, profile, callback) => {
|
||||
const emailAddr = profile.emails?.[0].value ?? "";
|
||||
const customer = await data_1.customer_repository.storeCustomer({
|
||||
name: profile.displayName, email: emailAddr,
|
||||
federatedId: profile.id
|
||||
});
|
||||
const { id, name, email } = customer;
|
||||
return callback(null, { id, name, email });
|
||||
}));
|
||||
passport_1.default.serializeUser((user, callback) => {
|
||||
callback(null, user.adminUser ? JSON.stringify(user) : user.id);
|
||||
});
|
||||
passport_1.default.deserializeUser((id, callbackFunc) => {
|
||||
if (typeof id == "string") {
|
||||
callbackFunc(null, JSON.parse(id));
|
||||
}
|
||||
else {
|
||||
data_1.customer_repository.getCustomer(id).then(user => callbackFunc(null, user));
|
||||
}
|
||||
});
|
||||
app.use(passport_1.default.session());
|
||||
};
|
||||
exports.createAuthentication = createAuthentication;
|
||||
@@ -0,0 +1,14 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getEnvironment = exports.Env = void 0;
|
||||
var Env;
|
||||
(function (Env) {
|
||||
Env["Development"] = "development";
|
||||
Env["Production"] = "production";
|
||||
})(Env || (exports.Env = Env = {}));
|
||||
const getEnvironment = () => {
|
||||
const env = process.env.NODE_ENV;
|
||||
return env === undefined || env === Env.Development
|
||||
? Env.Development : Env.Production;
|
||||
};
|
||||
exports.getEnvironment = getEnvironment;
|
||||
@@ -0,0 +1,37 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Env = exports.getEnvironment = exports.getSecret = exports.getConfig = void 0;
|
||||
const fs_1 = require("fs");
|
||||
const environment_1 = require("./environment");
|
||||
Object.defineProperty(exports, "getEnvironment", { enumerable: true, get: function () { return environment_1.getEnvironment; } });
|
||||
Object.defineProperty(exports, "Env", { enumerable: true, get: function () { return environment_1.Env; } });
|
||||
const merge_1 = require("./merge");
|
||||
const dotenv_1 = require("dotenv");
|
||||
const file = process.env.SERVER_CONFIG ?? "server.config.json";
|
||||
const data = JSON.parse((0, fs_1.readFileSync)(file).toString());
|
||||
(0, dotenv_1.config)({
|
||||
path: (0, environment_1.getEnvironment)().toString() + ".env"
|
||||
});
|
||||
try {
|
||||
const envFile = (0, environment_1.getEnvironment)().toString() + "." + file;
|
||||
const envData = JSON.parse((0, fs_1.readFileSync)(envFile).toString());
|
||||
(0, merge_1.merge)(data, envData);
|
||||
}
|
||||
catch {
|
||||
// do nothing - file doesn't exist or isn't readable
|
||||
}
|
||||
const getConfig = (path, defaultVal = undefined) => {
|
||||
const paths = path.split(":");
|
||||
let val = data;
|
||||
paths.forEach(p => val = val[p]);
|
||||
return val ?? defaultVal;
|
||||
};
|
||||
exports.getConfig = getConfig;
|
||||
const getSecret = (name) => {
|
||||
const secret = process.env[name];
|
||||
if (secret === undefined) {
|
||||
throw new Error(`Undefined secret: ${name}`);
|
||||
}
|
||||
return secret;
|
||||
};
|
||||
exports.getSecret = getSecret;
|
||||
@@ -0,0 +1,20 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.merge = void 0;
|
||||
const merge = (target, source) => {
|
||||
Object.keys(source).forEach(key => {
|
||||
if (typeof source[key] === "object"
|
||||
&& !Array.isArray(source[key])) {
|
||||
if (Object.hasOwn(target, key)) {
|
||||
(0, exports.merge)(target[key], source[key]);
|
||||
}
|
||||
else {
|
||||
Object.assign(target, source[key]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
target[key] = source[key];
|
||||
}
|
||||
});
|
||||
};
|
||||
exports.merge = merge;
|
||||
@@ -0,0 +1,20 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.merge = void 0;
|
||||
const merge = (target, source) => {
|
||||
Object.keys(source).forEach(key => {
|
||||
if (typeof source[key] === "object"
|
||||
&& !Array.isArray(source[key])) {
|
||||
if (Object.hasOwn(target, key)) {
|
||||
(0, exports.merge)(target[key], source[key]);
|
||||
}
|
||||
else {
|
||||
Object.assign(target, source[key]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
target[key] = source[key];
|
||||
}
|
||||
});
|
||||
};
|
||||
exports.merge = merge;
|
||||
@@ -0,0 +1,17 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getCartDetail = void 0;
|
||||
const _1 = require(".");
|
||||
const getCartDetail = async (cart) => {
|
||||
const ids = cart.lines.map(l => l.productId);
|
||||
const db_data = await _1.catalog_repository.getProductDetails(ids);
|
||||
const products = Object.fromEntries(db_data.map(p => [p.id, p]));
|
||||
const lines = cart.lines.map(line => ({
|
||||
product: products[line.productId],
|
||||
quantity: line.quantity,
|
||||
subtotal: products[line.productId].price * line.quantity
|
||||
}));
|
||||
const total = lines.reduce((total, line) => total + line.subtotal, 0);
|
||||
return { lines, total };
|
||||
};
|
||||
exports.getCartDetail = getCartDetail;
|
||||
@@ -0,0 +1,19 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.removeLine = exports.addLine = exports.createCart = void 0;
|
||||
const createCart = () => ({ lines: [] });
|
||||
exports.createCart = createCart;
|
||||
const addLine = (cart, productId, quantity) => {
|
||||
const line = cart.lines.find(l => l.productId == productId);
|
||||
if (line !== undefined) {
|
||||
line.quantity += quantity;
|
||||
}
|
||||
else {
|
||||
cart.lines.push({ productId, quantity });
|
||||
}
|
||||
};
|
||||
exports.addLine = addLine;
|
||||
const removeLine = (cart, productId) => {
|
||||
cart.lines = cart.lines.filter(l => l.productId !== productId);
|
||||
};
|
||||
exports.removeLine = removeLine;
|
||||
@@ -0,0 +1,2 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
@@ -0,0 +1,2 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
@@ -0,0 +1,2 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
@@ -0,0 +1,2 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
@@ -0,0 +1,8 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.customer_repository = exports.order_repository = exports.catalog_repository = void 0;
|
||||
const orm_1 = require("./orm");
|
||||
const repo = new orm_1.CatalogRepoImpl();
|
||||
exports.catalog_repository = repo;
|
||||
exports.order_repository = repo;
|
||||
exports.customer_repository = repo;
|
||||
@@ -0,0 +1,2 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
@@ -0,0 +1,2 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
@@ -0,0 +1,38 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.BaseRepo = void 0;
|
||||
const sequelize_1 = require("sequelize");
|
||||
const config_1 = require("../../config");
|
||||
const models_1 = require("./models");
|
||||
const fs_1 = require("fs");
|
||||
const config = (0, config_1.getConfig)("catalog:orm_repo");
|
||||
const logging = config.logging
|
||||
? { logging: console.log, logQueryParameters: true }
|
||||
: { logging: false };
|
||||
class BaseRepo {
|
||||
sequelize;
|
||||
constructor() {
|
||||
this.sequelize = new sequelize_1.Sequelize({ ...config.settings, ...logging });
|
||||
this.initModelsAndDatabase();
|
||||
}
|
||||
async initModelsAndDatabase() {
|
||||
(0, models_1.initializeModels)(this.sequelize);
|
||||
if (config.reset_db) {
|
||||
await this.sequelize.drop();
|
||||
await this.sequelize.sync();
|
||||
await this.addSeedData();
|
||||
}
|
||||
else {
|
||||
await this.sequelize.sync();
|
||||
}
|
||||
}
|
||||
async addSeedData() {
|
||||
const data = JSON.parse((0, fs_1.readFileSync)(config.seed_file).toString());
|
||||
await this.sequelize.transaction(async (transaction) => {
|
||||
await models_1.SupplierModel.bulkCreate(data.suppliers, { transaction });
|
||||
await models_1.CategoryModel.bulkCreate(data.categories, { transaction });
|
||||
await models_1.ProductModel.bulkCreate(data.products, { transaction });
|
||||
});
|
||||
}
|
||||
}
|
||||
exports.BaseRepo = BaseRepo;
|
||||
@@ -0,0 +1,44 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.AddCustomers = void 0;
|
||||
const customer_models_1 = require("./models/customer_models");
|
||||
const order_models_1 = require("./models/order_models");
|
||||
function AddCustomers(Base) {
|
||||
return class extends Base {
|
||||
getCustomer(id) {
|
||||
return customer_models_1.CustomerModel.findByPk(id, {
|
||||
raw: true
|
||||
});
|
||||
}
|
||||
getCustomerByFederatedId(id) {
|
||||
return customer_models_1.CustomerModel.findOne({
|
||||
where: { federatedId: id },
|
||||
raw: true
|
||||
});
|
||||
}
|
||||
getCustomerAddress(id) {
|
||||
return order_models_1.AddressModel.findOne({
|
||||
include: [{
|
||||
model: order_models_1.OrderModel,
|
||||
where: { customerId: id },
|
||||
attributes: []
|
||||
}],
|
||||
order: [["updatedAt", "DESC"]]
|
||||
});
|
||||
}
|
||||
async storeCustomer(customer) {
|
||||
const [data, created] = await customer_models_1.CustomerModel.findOrCreate({
|
||||
where: { email: customer.email },
|
||||
defaults: customer,
|
||||
});
|
||||
if (!created) {
|
||||
data.name = customer.name;
|
||||
data.email = customer.email;
|
||||
data.federatedId = customer.federatedId;
|
||||
await data.save();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
};
|
||||
}
|
||||
exports.AddCustomers = AddCustomers;
|
||||
@@ -0,0 +1,13 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.CatalogRepoImpl = void 0;
|
||||
const core_1 = require("./core");
|
||||
const queries_1 = require("./queries");
|
||||
const storage_1 = require("./storage");
|
||||
const order_queries_1 = require("./order_queries");
|
||||
const order_storage_1 = require("./order_storage");
|
||||
const customers_1 = require("./customers");
|
||||
const CatalogRepo = (0, storage_1.AddStorage)((0, queries_1.AddQueries)(core_1.BaseRepo));
|
||||
const RepoWithOrders = (0, order_storage_1.AddOrderStorage)((0, order_queries_1.AddOrderQueries)(CatalogRepo));
|
||||
const RepoWithCustomers = (0, customers_1.AddCustomers)(RepoWithOrders);
|
||||
exports.CatalogRepoImpl = RepoWithCustomers;
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.initializeCatalogModels = void 0;
|
||||
const sequelize_1 = require("sequelize");
|
||||
const catalog_models_1 = require("./catalog_models");
|
||||
const primaryKey = {
|
||||
id: { type: sequelize_1.DataTypes.INTEGER, autoIncrement: true, primaryKey: true }
|
||||
};
|
||||
const initializeCatalogModels = (sequelize) => {
|
||||
catalog_models_1.ProductModel.init({
|
||||
...primaryKey,
|
||||
name: { type: sequelize_1.DataTypes.STRING },
|
||||
description: { type: sequelize_1.DataTypes.STRING },
|
||||
price: { type: sequelize_1.DataTypes.DECIMAL(10, 2) }
|
||||
}, { sequelize });
|
||||
catalog_models_1.CategoryModel.init({
|
||||
...primaryKey,
|
||||
name: { type: sequelize_1.DataTypes.STRING }
|
||||
}, { sequelize });
|
||||
catalog_models_1.SupplierModel.init({
|
||||
...primaryKey,
|
||||
name: { type: sequelize_1.DataTypes.STRING }
|
||||
}, { sequelize });
|
||||
catalog_models_1.ProductModel.belongsTo(catalog_models_1.CategoryModel, { foreignKey: "categoryId", as: "category" });
|
||||
catalog_models_1.ProductModel.belongsTo(catalog_models_1.SupplierModel, { foreignKey: "supplierId", as: "supplier" });
|
||||
catalog_models_1.CategoryModel.hasMany(catalog_models_1.ProductModel, { foreignKey: "categoryId", as: "products" });
|
||||
catalog_models_1.SupplierModel.hasMany(catalog_models_1.ProductModel, { foreignKey: "supplierId", as: "products" });
|
||||
};
|
||||
exports.initializeCatalogModels = initializeCatalogModels;
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.SupplierModel = exports.CategoryModel = exports.ProductModel = void 0;
|
||||
const sequelize_1 = require("sequelize");
|
||||
class ProductModel extends sequelize_1.Model {
|
||||
}
|
||||
exports.ProductModel = ProductModel;
|
||||
class CategoryModel extends sequelize_1.Model {
|
||||
}
|
||||
exports.CategoryModel = CategoryModel;
|
||||
class SupplierModel extends sequelize_1.Model {
|
||||
}
|
||||
exports.SupplierModel = SupplierModel;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.initializeCustomerModels = void 0;
|
||||
const sequelize_1 = require("sequelize");
|
||||
const customer_models_1 = require("./customer_models");
|
||||
const initializeCustomerModels = (sequelize) => {
|
||||
customer_models_1.CustomerModel.init({
|
||||
id: { type: sequelize_1.DataTypes.INTEGER, autoIncrement: true, primaryKey: true },
|
||||
name: { type: sequelize_1.DataTypes.STRING },
|
||||
email: { type: sequelize_1.DataTypes.STRING },
|
||||
federatedId: { type: sequelize_1.DataTypes.STRING }
|
||||
}, { sequelize });
|
||||
};
|
||||
exports.initializeCustomerModels = initializeCustomerModels;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.CustomerModel = void 0;
|
||||
const sequelize_1 = require("sequelize");
|
||||
class CustomerModel extends sequelize_1.Model {
|
||||
}
|
||||
exports.CustomerModel = CustomerModel;
|
||||
@@ -0,0 +1,16 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.initializeModels = exports.SupplierModel = exports.CategoryModel = exports.ProductModel = void 0;
|
||||
const catalog_helpers_1 = require("./catalog_helpers");
|
||||
const customer_helpers_1 = require("./customer_helpers");
|
||||
const order_helpers_1 = require("./order_helpers");
|
||||
var catalog_models_1 = require("./catalog_models");
|
||||
Object.defineProperty(exports, "ProductModel", { enumerable: true, get: function () { return catalog_models_1.ProductModel; } });
|
||||
Object.defineProperty(exports, "CategoryModel", { enumerable: true, get: function () { return catalog_models_1.CategoryModel; } });
|
||||
Object.defineProperty(exports, "SupplierModel", { enumerable: true, get: function () { return catalog_models_1.SupplierModel; } });
|
||||
const initializeModels = (sequelize) => {
|
||||
(0, catalog_helpers_1.initializeCatalogModels)(sequelize);
|
||||
(0, customer_helpers_1.initializeCustomerModels)(sequelize);
|
||||
(0, order_helpers_1.initializeOrderModels)(sequelize);
|
||||
};
|
||||
exports.initializeModels = initializeModels;
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.initializeOrderModels = void 0;
|
||||
const sequelize_1 = require("sequelize");
|
||||
const order_models_1 = require("./order_models");
|
||||
const customer_models_1 = require("./customer_models");
|
||||
const _1 = require(".");
|
||||
const primaryKey = {
|
||||
id: { type: sequelize_1.DataTypes.INTEGER, autoIncrement: true, primaryKey: true }
|
||||
};
|
||||
const initializeOrderModels = (sequelize) => {
|
||||
order_models_1.OrderModel.init({
|
||||
...primaryKey, shipped: sequelize_1.DataTypes.BOOLEAN
|
||||
}, { sequelize });
|
||||
order_models_1.ProductSelectionModel.init({
|
||||
...primaryKey,
|
||||
quantity: sequelize_1.DataTypes.INTEGER, price: sequelize_1.DataTypes.DECIMAL(10, 2)
|
||||
}, { sequelize });
|
||||
order_models_1.AddressModel.init({
|
||||
...primaryKey,
|
||||
street: sequelize_1.DataTypes.STRING, city: sequelize_1.DataTypes.STRING,
|
||||
state: sequelize_1.DataTypes.STRING, zip: sequelize_1.DataTypes.STRING,
|
||||
}, { sequelize });
|
||||
order_models_1.OrderModel.belongsTo(customer_models_1.CustomerModel, { as: "customer" });
|
||||
order_models_1.OrderModel.belongsTo(order_models_1.AddressModel, { foreignKey: "addressId", as: "address" });
|
||||
order_models_1.OrderModel.belongsToMany(order_models_1.ProductSelectionModel, { through: "OrderProductJunction",
|
||||
foreignKey: "orderId", as: "selections" });
|
||||
order_models_1.ProductSelectionModel.belongsTo(_1.ProductModel, { as: "product" });
|
||||
order_models_1.AddressModel.hasMany(order_models_1.OrderModel, { foreignKey: "addressId" });
|
||||
};
|
||||
exports.initializeOrderModels = initializeOrderModels;
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.AddressModel = exports.ProductSelectionModel = exports.OrderModel = void 0;
|
||||
const sequelize_1 = require("sequelize");
|
||||
class OrderModel extends sequelize_1.Model {
|
||||
}
|
||||
exports.OrderModel = OrderModel;
|
||||
class ProductSelectionModel extends sequelize_1.Model {
|
||||
}
|
||||
exports.ProductSelectionModel = ProductSelectionModel;
|
||||
class AddressModel extends sequelize_1.Model {
|
||||
}
|
||||
exports.AddressModel = AddressModel;
|
||||
@@ -0,0 +1,24 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.AddOrderQueries = void 0;
|
||||
const order_models_1 = require("./models/order_models");
|
||||
const customer_models_1 = require("./models/customer_models");
|
||||
const queryConfig = {
|
||||
include: [
|
||||
{ model: order_models_1.AddressModel, as: "address" },
|
||||
{ model: customer_models_1.CustomerModel, as: "customer" }
|
||||
],
|
||||
raw: true, nest: true
|
||||
};
|
||||
function AddOrderQueries(Base) {
|
||||
return class extends Base {
|
||||
getOrder(id) {
|
||||
return order_models_1.OrderModel.findByPk(id, queryConfig);
|
||||
}
|
||||
getOrders(excludeShipped) {
|
||||
return order_models_1.OrderModel.findAll(excludeShipped ?
|
||||
{ ...queryConfig, where: { shipped: false } } : queryConfig);
|
||||
}
|
||||
};
|
||||
}
|
||||
exports.AddOrderQueries = AddOrderQueries;
|
||||
@@ -0,0 +1,38 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.AddOrderStorage = void 0;
|
||||
const order_models_1 = require("./models/order_models");
|
||||
const customer_models_1 = require("./models/customer_models");
|
||||
function AddOrderStorage(Base) {
|
||||
return class extends Base {
|
||||
storeOrder(order) {
|
||||
return this.sequelize.transaction(async (transaction) => {
|
||||
const { id, shipped } = order;
|
||||
const [stored] = await order_models_1.OrderModel.upsert({ id, shipped }, { transaction });
|
||||
if (order.customer) {
|
||||
const [{ id }] = await customer_models_1.CustomerModel.findOrCreate({
|
||||
where: { email: order.customer.email },
|
||||
defaults: order.customer,
|
||||
transaction
|
||||
});
|
||||
stored.customerId = id;
|
||||
}
|
||||
if (order.address) {
|
||||
const [{ id }] = await order_models_1.AddressModel.findOrCreate({
|
||||
where: { ...order.address },
|
||||
defaults: order.address,
|
||||
transaction
|
||||
});
|
||||
stored.addressId = id;
|
||||
}
|
||||
await stored.save({ transaction });
|
||||
if (order.selections) {
|
||||
const sels = await order_models_1.ProductSelectionModel.bulkCreate(order.selections, { transaction });
|
||||
await stored.setSelections(sels, { transaction });
|
||||
}
|
||||
return stored;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
exports.AddOrderStorage = AddOrderStorage;
|
||||
@@ -0,0 +1,49 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.AddQueries = void 0;
|
||||
const models_1 = require("./models");
|
||||
const sequelize_1 = require("sequelize");
|
||||
function AddQueries(Base) {
|
||||
return class extends Base {
|
||||
async getProducts(params) {
|
||||
const opts = {};
|
||||
if (params?.page && params.pageSize) {
|
||||
opts.limit = params?.pageSize,
|
||||
opts.offset = (params.page - 1) * params.pageSize;
|
||||
}
|
||||
if (params?.searchTerm) {
|
||||
const searchOp = { [sequelize_1.Op.like]: "%" + params.searchTerm + "%" };
|
||||
opts.where = {
|
||||
[sequelize_1.Op.or]: { name: searchOp, description: searchOp }
|
||||
};
|
||||
}
|
||||
if (params?.category) {
|
||||
opts.where = {
|
||||
...opts.where, categoryId: params.category
|
||||
};
|
||||
}
|
||||
const result = await models_1.ProductModel.findAndCountAll({
|
||||
include: [
|
||||
{ model: models_1.SupplierModel, as: "supplier" },
|
||||
{ model: models_1.CategoryModel, as: "category" }
|
||||
],
|
||||
raw: true, nest: true,
|
||||
...opts
|
||||
});
|
||||
const categories = await this.getCategories();
|
||||
return { products: result.rows, totalCount: result.count, categories };
|
||||
}
|
||||
getCategories() {
|
||||
return models_1.CategoryModel.findAll({ raw: true, nest: true });
|
||||
}
|
||||
getSuppliers() {
|
||||
return models_1.SupplierModel.findAll({ raw: true, nest: true });
|
||||
}
|
||||
getProductDetails(ids) {
|
||||
return models_1.ProductModel.findAll({
|
||||
where: { id: { [sequelize_1.Op.in]: ids } }, raw: true, nest: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
exports.AddQueries = AddQueries;
|
||||
@@ -0,0 +1,37 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.AddStorage = void 0;
|
||||
const models_1 = require("./models");
|
||||
function AddStorage(Base) {
|
||||
return class extends Base {
|
||||
storeProduct(p) {
|
||||
return this.sequelize.transaction(async (transaction) => {
|
||||
if (p.category) {
|
||||
p.category = await this.storeCategory(p.category);
|
||||
}
|
||||
if (p.supplier) {
|
||||
p.supplier = await this.storeSupplier(p.supplier);
|
||||
}
|
||||
const [stored] = await models_1.ProductModel.upsert({
|
||||
id: p.id, name: p.name, description: p.description,
|
||||
price: p.price, categoryId: p.category?.id,
|
||||
supplierId: p.supplier?.id
|
||||
}, { transaction });
|
||||
return stored;
|
||||
});
|
||||
}
|
||||
async storeCategory(c, transaction) {
|
||||
const [stored] = await models_1.CategoryModel.upsert({
|
||||
id: c.id, name: c.name
|
||||
}, { transaction });
|
||||
return stored;
|
||||
}
|
||||
async storeSupplier(s, transaction) {
|
||||
const [stored] = await models_1.SupplierModel.upsert({
|
||||
id: s.id, name: s.name
|
||||
}, { transaction });
|
||||
return stored;
|
||||
}
|
||||
};
|
||||
}
|
||||
exports.AddStorage = AddStorage;
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.no_op = exports.required = exports.email = exports.minLength = void 0;
|
||||
const validator_1 = __importDefault(require("validator"));
|
||||
const minLength = (min) => (status) => {
|
||||
if (!validator_1.default.isLength(status.value, { min })) {
|
||||
status.setInvalid(true);
|
||||
status.messages.push(`Enter at least ${min} characters`);
|
||||
}
|
||||
};
|
||||
exports.minLength = minLength;
|
||||
const email = (status) => {
|
||||
if (!validator_1.default.isEmail(status.value)) {
|
||||
status.setInvalid(true);
|
||||
status.messages.push("Enter an email address");
|
||||
}
|
||||
};
|
||||
exports.email = email;
|
||||
const required = (status) => {
|
||||
if (validator_1.default.isEmpty(status.value.toString(), { ignore_whitespace: true })) {
|
||||
status.setInvalid(true);
|
||||
status.messages.push("A value is required");
|
||||
}
|
||||
};
|
||||
exports.required = required;
|
||||
const no_op = (status) => { };
|
||||
exports.no_op = no_op;
|
||||
@@ -0,0 +1,21 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
||||
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
__exportStar(require("./validation_types"), exports);
|
||||
__exportStar(require("./validator"), exports);
|
||||
__exportStar(require("./basic_rules"), exports);
|
||||
__exportStar(require("./order_rules"), exports);
|
||||
__exportStar(require("./product_dto_rules"), exports);
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.AddressValidator = exports.CustomerValidator = void 0;
|
||||
const validator_1 = require("./validator");
|
||||
const basic_rules_1 = require("./basic_rules");
|
||||
exports.CustomerValidator = new validator_1.Validator({
|
||||
name: [basic_rules_1.required, (0, basic_rules_1.minLength)(6)],
|
||||
email: basic_rules_1.email,
|
||||
federatedId: basic_rules_1.no_op
|
||||
});
|
||||
exports.AddressValidator = new validator_1.Validator({
|
||||
street: basic_rules_1.required,
|
||||
city: basic_rules_1.required,
|
||||
state: basic_rules_1.required,
|
||||
zip: basic_rules_1.no_op
|
||||
});
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ProductDTOValidator = void 0;
|
||||
const validator_1 = require("./validator");
|
||||
const basic_rules_1 = require("./basic_rules");
|
||||
const models_1 = require("../orm/models");
|
||||
const supplierExists = async (status) => {
|
||||
const count = await models_1.SupplierModel.count({ where: { id: status.value } });
|
||||
if (count !== 1) {
|
||||
status.setInvalid(true);
|
||||
status.messages.push("A valid supplier is required");
|
||||
}
|
||||
};
|
||||
const categoryExists = async (status) => {
|
||||
const count = await models_1.CategoryModel.count({ where: { id: status.value } });
|
||||
if (count !== 1) {
|
||||
status.setInvalid(true);
|
||||
status.messages.push("A valid category is required");
|
||||
}
|
||||
};
|
||||
exports.ProductDTOValidator = new validator_1.Validator({
|
||||
name: [basic_rules_1.required, (0, basic_rules_1.minLength)(3)],
|
||||
description: basic_rules_1.required,
|
||||
categoryId: categoryExists,
|
||||
supplierId: supplierExists,
|
||||
price: basic_rules_1.required,
|
||||
});
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ValidationStatus = void 0;
|
||||
class ValidationStatus {
|
||||
value;
|
||||
invalid = false;
|
||||
constructor(value) {
|
||||
this.value = value;
|
||||
}
|
||||
get isInvalid() {
|
||||
return this.invalid;
|
||||
}
|
||||
setInvalid(newValue) {
|
||||
this.invalid = newValue || this.invalid;
|
||||
}
|
||||
messages = [];
|
||||
}
|
||||
exports.ValidationStatus = ValidationStatus;
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getData = exports.isValid = exports.Validator = void 0;
|
||||
const validation_types_1 = require("./validation_types");
|
||||
class Validator {
|
||||
rules;
|
||||
breakOnInvalid;
|
||||
constructor(rules, breakOnInvalid = true) {
|
||||
this.rules = rules;
|
||||
this.breakOnInvalid = breakOnInvalid;
|
||||
}
|
||||
async validate(data) {
|
||||
const vdata = Object.entries(this.rules).map(async ([key, rules]) => {
|
||||
const status = new validation_types_1.ValidationStatus(data?.[key] ?? "");
|
||||
const rs = (Array.isArray(rules) ? rules : [rules]);
|
||||
for (const r of rs) {
|
||||
if (!status.isInvalid || !this.breakOnInvalid) {
|
||||
await r(status);
|
||||
}
|
||||
}
|
||||
return [key, status];
|
||||
});
|
||||
const done = await Promise.all(vdata);
|
||||
return Object.fromEntries(done);
|
||||
}
|
||||
validateOriginal(data) {
|
||||
const vdata = Object.entries(this.rules).map(([key, rules]) => {
|
||||
const status = new validation_types_1.ValidationStatus(data?.[key] ?? "");
|
||||
(Array.isArray(rules) ? rules : [rules])
|
||||
.forEach(async (rule) => {
|
||||
if (!status.isInvalid || !this.breakOnInvalid) {
|
||||
await rule(status);
|
||||
}
|
||||
});
|
||||
return [key, status];
|
||||
});
|
||||
return Object.fromEntries(vdata);
|
||||
}
|
||||
}
|
||||
exports.Validator = Validator;
|
||||
function isValid(result) {
|
||||
return Object.values(result)
|
||||
.every(r => r.isInvalid === false);
|
||||
}
|
||||
exports.isValid = isValid;
|
||||
function getData(result) {
|
||||
return Object.fromEntries(Object.entries(result)
|
||||
.map(([key, status]) => [key, status.value]));
|
||||
}
|
||||
exports.getData = getData;
|
||||
@@ -0,0 +1,28 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createErrorHandlers = void 0;
|
||||
const config_1 = require("./config");
|
||||
require("express-async-errors");
|
||||
const template400 = (0, config_1.getConfig)("errors:400");
|
||||
const template500 = (0, config_1.getConfig)("errors:500");
|
||||
const createErrorHandlers = (app) => {
|
||||
app.use((req, resp) => {
|
||||
resp.statusCode = 404;
|
||||
resp.render(template400);
|
||||
});
|
||||
const handler = (error, req, resp, next) => {
|
||||
console.log(error);
|
||||
if (resp.headersSent) {
|
||||
return next(error);
|
||||
}
|
||||
try {
|
||||
resp.statusCode = 500;
|
||||
resp.render(template500, { error });
|
||||
}
|
||||
catch (newErr) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
app.use(handler);
|
||||
};
|
||||
exports.createErrorHandlers = createErrorHandlers;
|
||||
@@ -0,0 +1,13 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.total = exports.first = exports.selected = exports.disabled = exports.buttonClass = void 0;
|
||||
const buttonClass = (btn, mode) => btn == mode ? "btn-secondary" : "btn-outline-secondary";
|
||||
exports.buttonClass = buttonClass;
|
||||
const disabled = (val) => val == "ID" ? "disabled" : "";
|
||||
exports.disabled = disabled;
|
||||
const selected = (val1, val2) => val1 == val2 ? "selected" : "";
|
||||
exports.selected = selected;
|
||||
const first = (index) => index == 0;
|
||||
exports.first = first;
|
||||
const total = (sels) => sels.reduce((total, s) => total += (s.quantity * s.product.price), 0);
|
||||
exports.total = total;
|
||||
@@ -0,0 +1,5 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.countCartItems = void 0;
|
||||
const countCartItems = (cart) => cart.lines.reduce((total, line) => total + line.quantity, 0);
|
||||
exports.countCartItems = countCartItems;
|
||||
@@ -0,0 +1,70 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.currency = exports.highlight = exports.categoryButtons = exports.pageSizeOptions = exports.pageButtons = exports.escapeUrl = exports.navigationUrl = void 0;
|
||||
const handlebars_1 = __importDefault(require("handlebars"));
|
||||
const querystring_1 = require("querystring");
|
||||
const querystring_2 = require("querystring");
|
||||
const getData = (options) => {
|
||||
return { ...options.data.root, ...options.hash };
|
||||
};
|
||||
const navigationUrl = (options) => {
|
||||
const { page, pageSize, category, searchTerm } = getData(options);
|
||||
return "/?" + (0, querystring_1.stringify)({ page, pageSize, category, searchTerm });
|
||||
};
|
||||
exports.navigationUrl = navigationUrl;
|
||||
const escapeUrl = (url) => (0, querystring_2.escape)(url);
|
||||
exports.escapeUrl = escapeUrl;
|
||||
const pageButtons = (options) => {
|
||||
const { page, pageCount } = getData(options);
|
||||
let output = "";
|
||||
for (let i = 1; i <= pageCount; i++) {
|
||||
output += options.fn({
|
||||
page, pageCount, index: i, selected: i === page
|
||||
});
|
||||
}
|
||||
return output;
|
||||
};
|
||||
exports.pageButtons = pageButtons;
|
||||
const pageSizeOptions = (options) => {
|
||||
const { pageSize } = getData(options);
|
||||
let output = "";
|
||||
[3, 6, 9].forEach(size => {
|
||||
output += options.fn({ size,
|
||||
selected: pageSize === size ? "selected" : "" });
|
||||
});
|
||||
return output;
|
||||
};
|
||||
exports.pageSizeOptions = pageSizeOptions;
|
||||
const categoryButtons = (options) => {
|
||||
const { category, categories } = getData(options);
|
||||
let output = "";
|
||||
for (let i = 0; i < categories.length; i++) {
|
||||
output += options.fn({
|
||||
id: categories[i].id,
|
||||
name: categories[i].name,
|
||||
selected: category === categories[i].id
|
||||
});
|
||||
}
|
||||
return output;
|
||||
};
|
||||
exports.categoryButtons = categoryButtons;
|
||||
const highlight = (value, options) => {
|
||||
const { searchTerm } = getData(options);
|
||||
if (searchTerm && searchTerm !== "") {
|
||||
const regexp = new RegExp(searchTerm, "ig");
|
||||
const mod = value.replaceAll(regexp, "<strong>$&</strong>");
|
||||
return new handlebars_1.default.SafeString(mod);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
exports.highlight = highlight;
|
||||
const formatter = new Intl.NumberFormat("en-us", {
|
||||
style: "currency", currency: "USD"
|
||||
});
|
||||
const currency = (value) => {
|
||||
return formatter.format(value);
|
||||
};
|
||||
exports.currency = currency;
|
||||
@@ -0,0 +1,8 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.isDevelopment = void 0;
|
||||
const config_1 = require("../config");
|
||||
const isDevelopment = (value) => {
|
||||
return (0, config_1.getEnvironment)() === config_1.Env.Development;
|
||||
};
|
||||
exports.isDevelopment = isDevelopment;
|
||||
@@ -0,0 +1,45 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createTemplates = void 0;
|
||||
const config_1 = require("../config");
|
||||
const express_handlebars_1 = require("express-handlebars");
|
||||
const env_helpers = __importStar(require("./env"));
|
||||
const catalog_helpers = __importStar(require("./catalog_helpers"));
|
||||
const cart_helpers = __importStar(require("./cart_helpers"));
|
||||
const order_helpers = __importStar(require("./order_helpers"));
|
||||
const admin_helpers = __importStar(require("./admin_helpers"));
|
||||
const location = (0, config_1.getConfig)("templates:location");
|
||||
const config = (0, config_1.getConfig)("templates:config");
|
||||
const createTemplates = (app) => {
|
||||
app.set("views", location);
|
||||
app.engine("handlebars", (0, express_handlebars_1.engine)({
|
||||
...config,
|
||||
helpers: { ...env_helpers, ...catalog_helpers, ...cart_helpers,
|
||||
...order_helpers, ...admin_helpers }
|
||||
}));
|
||||
app.set("view engine", "handlebars");
|
||||
};
|
||||
exports.createTemplates = createTemplates;
|
||||
@@ -0,0 +1,11 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.get = exports.getValue = exports.lower = exports.toArray = void 0;
|
||||
const toArray = (...args) => args.slice(0, -1);
|
||||
exports.toArray = toArray;
|
||||
const lower = (val) => val.toLowerCase();
|
||||
exports.lower = lower;
|
||||
const getValue = (val, prop) => val?.[prop.toLowerCase()] ?? {};
|
||||
exports.getValue = getValue;
|
||||
const get = (val) => val ?? {};
|
||||
exports.get = get;
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createAdminCatalogRoutes = void 0;
|
||||
const models_1 = require("../../data/orm/models");
|
||||
const validation_1 = require("../../data/validation");
|
||||
const createAdminCatalogRoutes = (router) => {
|
||||
router.get("/table", async (req, resp) => {
|
||||
const products = await models_1.ProductModel.findAll({
|
||||
include: [
|
||||
{ model: models_1.SupplierModel, as: "supplier" },
|
||||
{ model: models_1.CategoryModel, as: "category" }
|
||||
],
|
||||
raw: true, nest: true
|
||||
});
|
||||
resp.render("admin/product_table", { products });
|
||||
});
|
||||
router.delete("/:id", async (req, resp) => {
|
||||
const id = req.params.id;
|
||||
const count = await models_1.ProductModel.destroy({ where: { id } });
|
||||
if (count == 1) {
|
||||
resp.end();
|
||||
}
|
||||
else {
|
||||
throw Error(`Unexpected deletion count result: ${count}`);
|
||||
}
|
||||
});
|
||||
router.get("/edit/:id", async (req, resp) => {
|
||||
const id = req.params.id;
|
||||
const data = {
|
||||
product: { id: { value: id },
|
||||
...await validation_1.ProductDTOValidator.validate(await models_1.ProductModel.findByPk(id, { raw: true })) },
|
||||
suppliers: await models_1.SupplierModel.findAll({ raw: true }),
|
||||
categories: await models_1.CategoryModel.findAll({ raw: true })
|
||||
};
|
||||
resp.render("admin/product_editor", data);
|
||||
});
|
||||
router.put("/:id", async (req, resp) => {
|
||||
const validation = await validation_1.ProductDTOValidator.validate(req.body);
|
||||
if ((0, validation_1.isValid)(validation)) {
|
||||
await models_1.ProductModel.update((0, validation_1.getData)(validation), { where: { id: req.params.id } });
|
||||
resp.redirect(303, "/api/products/table");
|
||||
}
|
||||
else {
|
||||
resp.render("admin/product_editor", {
|
||||
product: { id: { value: req.params.id }, ...validation },
|
||||
suppliers: await models_1.SupplierModel.findAll({ raw: true }),
|
||||
categories: await models_1.CategoryModel.findAll({ raw: true })
|
||||
});
|
||||
}
|
||||
});
|
||||
router.get("/create", async (req, resp) => {
|
||||
const data = {
|
||||
product: {},
|
||||
suppliers: await models_1.SupplierModel.findAll({ raw: true }),
|
||||
categories: await models_1.CategoryModel.findAll({ raw: true }),
|
||||
create: true
|
||||
};
|
||||
resp.render("admin/product_editor", data);
|
||||
});
|
||||
router.post("/create", async (req, resp) => {
|
||||
const validation = await validation_1.ProductDTOValidator.validate(req.body);
|
||||
if ((0, validation_1.isValid)(validation)) {
|
||||
await models_1.ProductModel.create((0, validation_1.getData)(validation));
|
||||
resp.redirect(303, "/api/products/table");
|
||||
}
|
||||
else {
|
||||
resp.render("admin/product_editor", {
|
||||
product: validation,
|
||||
suppliers: await models_1.SupplierModel.findAll({ raw: true }),
|
||||
categories: await models_1.CategoryModel.findAll({ raw: true }),
|
||||
create: true
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
exports.createAdminCatalogRoutes = createAdminCatalogRoutes;
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createAdminOrderRoutes = void 0;
|
||||
const order_models_1 = require("../../data/orm/models/order_models");
|
||||
const customer_models_1 = require("../../data/orm/models/customer_models");
|
||||
const models_1 = require("../../data/orm/models");
|
||||
const createAdminOrderRoutes = (router) => {
|
||||
router.get("/table", async (req, resp) => {
|
||||
const orders = (await order_models_1.OrderModel.findAll({
|
||||
include: [
|
||||
{ model: customer_models_1.CustomerModel, as: "customer" },
|
||||
{ model: order_models_1.AddressModel, as: "address" },
|
||||
{ model: order_models_1.ProductSelectionModel, as: "selections",
|
||||
include: [{ model: models_1.ProductModel, as: "product" }]
|
||||
}
|
||||
],
|
||||
order: ["shipped", "id"]
|
||||
})).map(o => o.toJSON());
|
||||
resp.render("admin/order_table", { orders });
|
||||
});
|
||||
router.post("/ship", async (req, resp) => {
|
||||
const { id, shipped } = req.body;
|
||||
const [rows] = await order_models_1.OrderModel.update({ shipped }, { where: { id } });
|
||||
if (rows === 1) {
|
||||
resp.redirect(303, "/api/orders/table");
|
||||
}
|
||||
else {
|
||||
throw new Error(`Expected 1 row updated, but got ${rows}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
exports.createAdminOrderRoutes = createAdminOrderRoutes;
|
||||
@@ -0,0 +1,58 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createAdminRoutes = void 0;
|
||||
const express_1 = require("express");
|
||||
const admin_catalog_routes_1 = require("./admin_catalog_routes");
|
||||
const admin_order_routes_1 = require("./admin_order_routes");
|
||||
const passport_1 = __importDefault(require("passport"));
|
||||
const config_1 = require("../../config");
|
||||
const users = (0, config_1.getConfig)("admin:users", []);
|
||||
const createAdminRoutes = (app) => {
|
||||
app.use((req, resp, next) => {
|
||||
resp.locals.layout = false;
|
||||
resp.locals.user = req.user;
|
||||
next();
|
||||
});
|
||||
app.get("/admin/signin", (req, resp) => resp.render("admin/signin"));
|
||||
app.post("/admin/signout", (req, resp) => req.logOut(() => { resp.redirect("/admin/signin"); }));
|
||||
app.get("/admin/google", passport_1.default.authenticate("admin-auth"));
|
||||
app.get("/auth-signin-google", passport_1.default.authenticate("admin-auth", {
|
||||
successRedirect: "/admin/products", keepSessionInfo: true
|
||||
}));
|
||||
const authCheck = (r) => users.find(u => r.user?.email === u);
|
||||
const apiAuth = (req, resp, next) => {
|
||||
if (!authCheck(req)) {
|
||||
return resp.sendStatus(401);
|
||||
}
|
||||
next();
|
||||
};
|
||||
const cat_router = (0, express_1.Router)();
|
||||
(0, admin_catalog_routes_1.createAdminCatalogRoutes)(cat_router);
|
||||
app.use("/api/products", apiAuth, cat_router);
|
||||
const order_router = (0, express_1.Router)();
|
||||
(0, admin_order_routes_1.createAdminOrderRoutes)(order_router);
|
||||
app.use("/api/orders", apiAuth, order_router);
|
||||
const userAuth = (req, resp, next) => {
|
||||
if (!authCheck(req)) {
|
||||
return resp.redirect("/admin/signin");
|
||||
}
|
||||
next();
|
||||
};
|
||||
app.get("/admin", userAuth, (req, resp) => resp.redirect("/admin/products"));
|
||||
app.get("/admin/products", userAuth, (req, resp) => {
|
||||
resp.locals.content = "/api/products/table";
|
||||
resp.render("admin/admin_layout");
|
||||
});
|
||||
app.get("/admin/products/edit/:id", userAuth, (req, resp) => {
|
||||
resp.locals.content = `/api/products/edit/${req.params.id}`;
|
||||
resp.render("admin/admin_layout");
|
||||
});
|
||||
app.get("/admin/orders", userAuth, (req, resp) => {
|
||||
resp.locals.content = "/api/orders/table";
|
||||
resp.render("admin/admin_layout");
|
||||
});
|
||||
};
|
||||
exports.createAdminRoutes = createAdminRoutes;
|
||||
@@ -0,0 +1,61 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createCartRoutes = exports.createCartMiddleware = void 0;
|
||||
const querystring_1 = require("querystring");
|
||||
const cart_models_1 = require("../data/cart_models");
|
||||
const cart_helpers = __importStar(require("../data/cart_helpers"));
|
||||
const createCartMiddleware = (app) => {
|
||||
app.use((req, resp, next) => {
|
||||
resp.locals.cart = req.session.cart = req.session.cart ?? (0, cart_models_1.createCart)();
|
||||
next();
|
||||
});
|
||||
};
|
||||
exports.createCartMiddleware = createCartMiddleware;
|
||||
const createCartRoutes = (app) => {
|
||||
app.post("/cart", (req, resp) => {
|
||||
const productId = Number.parseInt(req.body.productId);
|
||||
if (isNaN(productId)) {
|
||||
throw new Error("ID must be an integer");
|
||||
}
|
||||
(0, cart_models_1.addLine)(req.session.cart, productId, 1);
|
||||
resp.redirect(`/cart?returnUrl=${(0, querystring_1.escape)(req.body.returnUrl ?? "/")}`);
|
||||
});
|
||||
app.get("/cart", async (req, resp) => {
|
||||
const cart = req.session.cart;
|
||||
resp.render("cart", {
|
||||
cart: await cart_helpers.getCartDetail(cart),
|
||||
returnUrl: (0, querystring_1.unescape)(req.query.returnUrl?.toString() ?? "/")
|
||||
});
|
||||
});
|
||||
app.post("/cart/remove", (req, resp) => {
|
||||
const id = Number.parseInt(req.body.id);
|
||||
if (!isNaN(id)) {
|
||||
(0, cart_models_1.removeLine)(req.session.cart, id);
|
||||
}
|
||||
resp.redirect(`/cart?returnUrl=${(0, querystring_1.escape)(req.body.returnUrl ?? "/")}`);
|
||||
});
|
||||
};
|
||||
exports.createCartRoutes = createCartRoutes;
|
||||
@@ -0,0 +1,19 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createCatalogRoutes = void 0;
|
||||
const data_1 = require("../data");
|
||||
const createCatalogRoutes = (app) => {
|
||||
app.get("/", async (req, resp) => {
|
||||
const page = Number.parseInt(req.query.page?.toString() ?? "1");
|
||||
const pageSize = Number.parseInt(req.query.pageSize?.toString() ?? "3");
|
||||
const searchTerm = req.query.searchTerm?.toString();
|
||||
const category = Number.parseInt(req.query.category?.toString() ?? "");
|
||||
const res = await data_1.catalog_repository.getProducts({ page, pageSize,
|
||||
searchTerm, category });
|
||||
resp.render("index", { ...res, page, pageSize,
|
||||
pageCount: Math.ceil(res.totalCount / (pageSize ?? 1)),
|
||||
searchTerm, category, show_cart: true
|
||||
});
|
||||
});
|
||||
};
|
||||
exports.createCatalogRoutes = createCatalogRoutes;
|
||||
@@ -0,0 +1,15 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createRoutes = void 0;
|
||||
const catalog_1 = require("./catalog");
|
||||
const cart_1 = require("./cart");
|
||||
const orders_1 = require("./orders");
|
||||
const admin_1 = require("./admin");
|
||||
const createRoutes = (app) => {
|
||||
(0, cart_1.createCartMiddleware)(app);
|
||||
(0, catalog_1.createCatalogRoutes)(app);
|
||||
(0, cart_1.createCartRoutes)(app);
|
||||
(0, orders_1.createOrderRoutes)(app);
|
||||
(0, admin_1.createAdminRoutes)(app);
|
||||
};
|
||||
exports.createRoutes = createRoutes;
|
||||
@@ -0,0 +1,18 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createAndStoreOrder = void 0;
|
||||
const data_1 = require("../data");
|
||||
const createAndStoreOrder = async (customer, address, cart) => {
|
||||
const product_ids = cart.lines.map(l => l.productId) ?? [];
|
||||
const product_details = Object.fromEntries((await data_1.catalog_repository.getProductDetails(product_ids))
|
||||
.map(p => [p.id ?? 0, p.price ?? 0]));
|
||||
const selections = cart.lines.map(l => ({
|
||||
productId: l.productId, quantity: l.quantity,
|
||||
price: product_details[l.productId]
|
||||
}));
|
||||
return data_1.order_repository.storeOrder({
|
||||
customer, address,
|
||||
selections, shipped: false
|
||||
});
|
||||
};
|
||||
exports.createAndStoreOrder = createAndStoreOrder;
|
||||
@@ -0,0 +1,53 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createOrderRoutes = void 0;
|
||||
const validation_1 = require("../data/validation");
|
||||
const order_helpers_1 = require("./order_helpers");
|
||||
const data_1 = require("../data");
|
||||
const passport_1 = __importDefault(require("passport"));
|
||||
const createOrderRoutes = (app) => {
|
||||
app.get("/checkout/google", passport_1.default.authenticate("google"));
|
||||
app.get("/signin-google", passport_1.default.authenticate("google", { successRedirect: "/checkout", keepSessionInfo: true }));
|
||||
app.get("/checkout", async (req, resp) => {
|
||||
if (!req.session.orderData && req.user) {
|
||||
req.session.orderData = {
|
||||
customer: await validation_1.CustomerValidator.validate(req.user),
|
||||
address: await validation_1.AddressValidator.validate(await data_1.customer_repository.getCustomerAddress(req.user?.id ?? 0) ?? {})
|
||||
};
|
||||
}
|
||||
req.session.pageSize =
|
||||
req.session.pageSize ?? req.query.pageSize?.toString() ?? "3";
|
||||
resp.render("order_details", {
|
||||
order: req.session.orderData,
|
||||
page: 1,
|
||||
pageSize: req.session.pageSize
|
||||
});
|
||||
});
|
||||
app.post("/checkout", async (req, resp) => {
|
||||
const { customer, address } = req.body;
|
||||
const data = req.session.orderData = {
|
||||
customer: await validation_1.CustomerValidator.validate(customer),
|
||||
address: await validation_1.AddressValidator.validate(address)
|
||||
};
|
||||
if ((0, validation_1.isValid)(data.customer) && (0, validation_1.isValid)(data.address)
|
||||
&& req.session.cart) {
|
||||
const order = await (0, order_helpers_1.createAndStoreOrder)((0, validation_1.getData)(data.customer), (0, validation_1.getData)(data.address), req.session.cart);
|
||||
resp.redirect(`/checkout/${order.id}`);
|
||||
req.session.cart = undefined;
|
||||
req.session.orderData = undefined;
|
||||
}
|
||||
else {
|
||||
resp.redirect("/checkout");
|
||||
}
|
||||
});
|
||||
app.get("/checkout/:id", (req, resp) => {
|
||||
resp.render("order_complete", {
|
||||
id: req.params.id,
|
||||
pageSize: req.session.pageSize ?? 3
|
||||
});
|
||||
});
|
||||
};
|
||||
exports.createOrderRoutes = createOrderRoutes;
|
||||
@@ -0,0 +1,37 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const http_1 = require("http");
|
||||
const express_1 = __importDefault(require("express"));
|
||||
const helmet_1 = __importDefault(require("helmet"));
|
||||
const config_1 = require("./config");
|
||||
const routes_1 = require("./routes");
|
||||
const helpers_1 = require("./helpers");
|
||||
const errors_1 = require("./errors");
|
||||
const sessions_1 = require("./sessions");
|
||||
const authentication_1 = require("./authentication");
|
||||
const http_proxy_1 = __importDefault(require("http-proxy"));
|
||||
const port = (0, config_1.getConfig)("http:port", 5000);
|
||||
const expressApp = (0, express_1.default)();
|
||||
expressApp.use((0, helmet_1.default)((0, config_1.getConfig)("http:content_security", {})));
|
||||
expressApp.use(express_1.default.json());
|
||||
expressApp.use(express_1.default.urlencoded({ extended: true }));
|
||||
expressApp.use(express_1.default.static("node_modules/bootstrap/dist"));
|
||||
expressApp.use(express_1.default.static("node_modules/bootstrap-icons"));
|
||||
expressApp.use(express_1.default.static("node_modules/htmx.org/dist"));
|
||||
(0, helpers_1.createTemplates)(expressApp);
|
||||
(0, sessions_1.createSessions)(expressApp);
|
||||
(0, authentication_1.createAuthentication)(expressApp);
|
||||
(0, routes_1.createRoutes)(expressApp);
|
||||
const server = (0, http_1.createServer)(expressApp);
|
||||
if ((0, config_1.getEnvironment)() === config_1.Env.Development) {
|
||||
const proxy = http_proxy_1.default.createProxyServer({
|
||||
target: "http://localhost:5100", ws: true
|
||||
});
|
||||
expressApp.use("/admin", (req, resp) => proxy.web(req, resp));
|
||||
server.on('upgrade', (req, socket, head) => proxy.ws(req, socket, head));
|
||||
}
|
||||
(0, errors_1.createErrorHandlers)(expressApp);
|
||||
server.listen(port, () => console.log(`HTTP Server listening on port ${port}`));
|
||||
@@ -0,0 +1,38 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createSessions = void 0;
|
||||
const sequelize_1 = require("sequelize");
|
||||
const config_1 = require("./config");
|
||||
const express_session_1 = __importDefault(require("express-session"));
|
||||
const connect_session_sequelize_1 = __importDefault(require("connect-session-sequelize"));
|
||||
const config = (0, config_1.getConfig)("sessions");
|
||||
const secret = (0, config_1.getSecret)("COOKIE_SECRET");
|
||||
const logging = config.orm.logging
|
||||
? { logging: console.log, logQueryParameters: true }
|
||||
: { logging: false };
|
||||
const createSessions = (app) => {
|
||||
const sequelize = new sequelize_1.Sequelize({
|
||||
...config.orm.settings, ...logging
|
||||
});
|
||||
const store = new ((0, connect_session_sequelize_1.default)(express_session_1.default.Store))({
|
||||
db: sequelize
|
||||
});
|
||||
if (config.reset_db === true) {
|
||||
sequelize.drop().then(() => store.sync());
|
||||
}
|
||||
else {
|
||||
store.sync();
|
||||
}
|
||||
app.use((0, express_session_1.default)({
|
||||
secret, store,
|
||||
resave: false, saveUninitialized: true,
|
||||
cookie: {
|
||||
maxAge: config.maxAgeHrs * 60 * 60 * 1000,
|
||||
sameSite: false, httpOnly: false, secure: false
|
||||
}
|
||||
}));
|
||||
};
|
||||
exports.createSessions = createSessions;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "sportsstore",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"watch": "tsc-watch --noClear --onsuccess \"node dist/server.js\"",
|
||||
"server": "nodemon --exec npm run watch",
|
||||
"client": "webpack serve",
|
||||
"start": "npm-run-all --parallel server client"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"ext": "js,handlebars,json",
|
||||
"ignore": [
|
||||
"dist/**",
|
||||
"node_modules/**",
|
||||
"templates/admin/**"
|
||||
]
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@tsconfig/node20": "^20.1.2",
|
||||
"@types/cookie-parser": "^1.4.6",
|
||||
"@types/express": "^4.17.20",
|
||||
"@types/express-session": "^1.17.10",
|
||||
"@types/node": "^20.6.1",
|
||||
"@types/passport": "^1.0.16",
|
||||
"@types/passport-google-oauth20": "^2.0.14",
|
||||
"@types/validator": "^13.11.5",
|
||||
"nodemon": "^3.0.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"tsc-watch": "^6.0.4",
|
||||
"typescript": "^5.2.2",
|
||||
"webpack": "^5.89.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^4.15.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap": "^5.3.2",
|
||||
"bootstrap-icons": "^1.11.3",
|
||||
"connect-session-sequelize": "^7.1.7",
|
||||
"dotenv": "^16.4.4",
|
||||
"express": "^4.18.2",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"express-handlebars": "^7.1.2",
|
||||
"express-session": "^1.17.3",
|
||||
"handlebars": "^4.7.8",
|
||||
"helmet": "^7.1.0",
|
||||
"htmx.org": "^1.9.10",
|
||||
"http-proxy": "^1.18.1",
|
||||
"passport": "^0.7.0",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"sequelize": "^6.35.1",
|
||||
"sqlite3": "^5.1.6",
|
||||
"validator": "^13.11.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"suppliers": [
|
||||
{ "id": 1, "name": "Acme Industries"},
|
||||
{ "id": 2, "name": "Big Boat Co"},
|
||||
{ "id": 3, "name": "London Chess"}
|
||||
],
|
||||
"categories": [
|
||||
{ "id": 1, "name": "Watersports"},
|
||||
{ "id": 2, "name": "Soccer"},
|
||||
{ "id": 3, "name": "Chess"}
|
||||
],
|
||||
"products": [
|
||||
{"id": 1, "name": "Kayak", "description": "A boat for one person",
|
||||
"price": 275.00, "categoryId": 1, "supplierId": 2 },
|
||||
{"id": 2, "name": "Lifejacket",
|
||||
"description": "Protective and fashionable",
|
||||
"price": 48.95, "categoryId": 1, "supplierId": 2 },
|
||||
{ "id": 3, "name": "Soccer Ball",
|
||||
"description": "FIFA-approved size and weight",
|
||||
"price": 19.50, "categoryId": 2, "supplierId": 1 },
|
||||
{ "id": 4, "name": "Corner Flags",
|
||||
"description": "Give your playing field a professional touch",
|
||||
"price": 34.95, "categoryId": 2, "supplierId": 1 },
|
||||
{ "id": 5, "name": "Stadium",
|
||||
"description": "Flat-packed 35,000-seat stadium",
|
||||
"price": 79500, "categoryId": 2, "supplierId": 1 },
|
||||
{ "id": 6, "name": "Thinking Cap",
|
||||
"description": "Improve brain efficiency by 75%", "price": 16,
|
||||
"categoryId": 3, "supplierId": 3 },
|
||||
{ "id": 7, "name": "Unsteady Chair",
|
||||
"description": "Secretly give your opponent a disadvantage",
|
||||
"price": 29.95, "categoryId": 3, "supplierId": 3 },
|
||||
{ "id": 8, "name": "Human Chess Board",
|
||||
"description": "A fun game for the family", "price": 75,
|
||||
"categoryId": 3, "supplierId": 3 },
|
||||
{ "id": 9, "name": "Bling King",
|
||||
"description": "Gold-plated, diamond-studded King",
|
||||
"price": 1200, "categoryId": 3, "supplierId": 3 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"http": {
|
||||
"port": 5000,
|
||||
"content_security": {
|
||||
"contentSecurityPolicy": {
|
||||
"directives": {
|
||||
"upgradeInsecureRequests": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"templates": {
|
||||
"location": "templates",
|
||||
"config": {
|
||||
"layoutsDir": "templates",
|
||||
"defaultLayout": "main_layout.handlebars",
|
||||
"partialsDir": "templates"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"400": "not_found",
|
||||
"500": "error"
|
||||
},
|
||||
"catalog": {
|
||||
"orm_repo": {
|
||||
"settings": {
|
||||
"dialect": "sqlite",
|
||||
"storage": "catalog.db"
|
||||
},
|
||||
"logging": true,
|
||||
"reset_db": true,
|
||||
"seed_file": "products.json"
|
||||
}
|
||||
},
|
||||
"sessions": {
|
||||
"maxAgeHrs": 2,
|
||||
"reset_db": true,
|
||||
"orm": {
|
||||
"settings": {
|
||||
"dialect": "sqlite",
|
||||
"storage": "sessions.db"
|
||||
},
|
||||
"logging": true
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"openauth": {
|
||||
"redirectionUrl": "http://localhost:5000/signin-google"
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
"openauth": {
|
||||
"redirectionUrl": "http://localhost:5000/auth-signin-google"
|
||||
},
|
||||
"users": ["alice@example.com", "your_account@google.com"]
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// do nothing
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Express } from "express";
|
||||
import { getConfig, getSecret } from "./config";
|
||||
import passport from "passport";
|
||||
import { Strategy as GoogleStrategy, Profile, VerifyCallback }
|
||||
from "passport-google-oauth20";
|
||||
import { customer_repository } from "./data";
|
||||
import { Customer } from "./data/customer_models";
|
||||
|
||||
const callbackURL: string = getConfig("auth:openauth:redirectionUrl");
|
||||
const clientID = getSecret("GOOGLE_CLIENT_ID");
|
||||
const clientSecret = getSecret("GOOGLE_CLIENT_SECRET");
|
||||
|
||||
const authCallbackURL: string = getConfig("admin:openauth:redirectionUrl")
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface User extends Customer {
|
||||
adminUser?: boolean;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const createAuthentication = (app:Express) => {
|
||||
|
||||
passport.use("admin-auth", new GoogleStrategy({
|
||||
clientID, clientSecret, callbackURL: authCallbackURL,
|
||||
scope: ["email", "profile"],
|
||||
state: true
|
||||
}, (accessToken: string, refreshToken: string,
|
||||
profile: Profile, callback: VerifyCallback) => {
|
||||
return callback(null, {
|
||||
name: profile.displayName,
|
||||
email: profile.emails?.[0].value ?? "",
|
||||
federatedId: profile.id,
|
||||
adminUser: true
|
||||
})
|
||||
}));
|
||||
|
||||
passport.use(new GoogleStrategy({
|
||||
clientID, clientSecret, callbackURL,
|
||||
scope: ["email", "profile"],
|
||||
state: true
|
||||
} , async (accessToken: string, refreshToken: string,
|
||||
profile: Profile, callback: VerifyCallback) => {
|
||||
const emailAddr = profile.emails?.[0].value ?? "";
|
||||
const customer = await customer_repository.storeCustomer({
|
||||
name: profile.displayName, email: emailAddr,
|
||||
federatedId: profile.id
|
||||
});
|
||||
const { id, name, email } = customer;
|
||||
return callback(null, { id, name, email });
|
||||
}));
|
||||
|
||||
passport.serializeUser((user, callback) => {
|
||||
callback(null, user.adminUser ? JSON.stringify(user) : user.id);
|
||||
});
|
||||
|
||||
passport.deserializeUser((id: number | string , callbackFunc) => {
|
||||
if (typeof id == "string") {
|
||||
callbackFunc(null, JSON.parse(id));
|
||||
} else {
|
||||
customer_repository.getCustomer(id).then(user =>
|
||||
callbackFunc(null, user));
|
||||
}
|
||||
});
|
||||
|
||||
app.use(passport.session());
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export enum Env {
|
||||
Development = "development", Production = "production"
|
||||
}
|
||||
|
||||
export const getEnvironment = () : Env => {
|
||||
const env = process.env.NODE_ENV;
|
||||
return env === undefined || env === Env.Development
|
||||
? Env.Development : Env.Production;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { readFileSync } from "fs";
|
||||
import { getEnvironment, Env } from "./environment";
|
||||
import { merge } from "./merge";
|
||||
import { config as dotenvconfig } from "dotenv";
|
||||
|
||||
const file = process.env.SERVER_CONFIG ?? "server.config.json"
|
||||
const data = JSON.parse(readFileSync(file).toString());
|
||||
|
||||
dotenvconfig({
|
||||
path: getEnvironment().toString() + ".env"
|
||||
})
|
||||
|
||||
try {
|
||||
const envFile = getEnvironment().toString() + "." + file;
|
||||
const envData = JSON.parse(readFileSync(envFile).toString());
|
||||
merge(data, envData);
|
||||
} catch {
|
||||
// do nothing - file doesn't exist or isn't readable
|
||||
}
|
||||
|
||||
export const getConfig = (path: string, defaultVal: any = undefined) => {
|
||||
const paths = path.split(":");
|
||||
let val = data;
|
||||
paths.forEach(p => val = val[p]);
|
||||
return val ?? defaultVal;
|
||||
}
|
||||
|
||||
export const getSecret = (name: string) => {
|
||||
const secret = process.env[name];
|
||||
if (secret === undefined) {
|
||||
throw new Error(`Undefined secret: ${name}`);
|
||||
}
|
||||
return secret;
|
||||
}
|
||||
|
||||
export { getEnvironment, Env };
|
||||
@@ -0,0 +1,14 @@
|
||||
export const merge = (target: any, source: any) : any => {
|
||||
Object.keys(source).forEach(key => {
|
||||
if (typeof source[key] === "object"
|
||||
&& !Array.isArray(source[key])) {
|
||||
if (Object.hasOwn(target, key)) {
|
||||
merge(target[key], source[key]);
|
||||
} else {
|
||||
Object.assign(target, source[key])
|
||||
}
|
||||
} else {
|
||||
target[key] = source[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { catalog_repository } from ".";
|
||||
import { Cart } from "./cart_models";
|
||||
import { Product } from "./catalog_models"
|
||||
|
||||
export interface CartDetail {
|
||||
lines: {
|
||||
product: Product,
|
||||
quantity: number,
|
||||
subtotal: number
|
||||
}[],
|
||||
total: number;
|
||||
}
|
||||
|
||||
export const getCartDetail = async (cart: Cart) : Promise<CartDetail> => {
|
||||
|
||||
const ids = cart.lines.map(l => l.productId);
|
||||
const db_data = await catalog_repository.getProductDetails(ids);
|
||||
|
||||
const products = Object.fromEntries(db_data.map(p => [p.id, p]));
|
||||
|
||||
const lines = cart.lines.map(line => ({
|
||||
product: products[line.productId],
|
||||
quantity: line.quantity,
|
||||
subtotal: products[line.productId].price * line.quantity
|
||||
}));
|
||||
|
||||
const total = lines.reduce((total, line) => total + line.subtotal, 0);
|
||||
|
||||
return { lines, total }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export interface CartLine {
|
||||
productId: number;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
export interface Cart {
|
||||
lines: CartLine[];
|
||||
}
|
||||
|
||||
export const createCart = () : Cart => ({ lines: [] });
|
||||
|
||||
export const addLine = (cart: Cart, productId: number, quantity: number) => {
|
||||
const line = cart.lines.find(l => l.productId == productId);
|
||||
if (line !== undefined) {
|
||||
line.quantity += quantity;
|
||||
} else {
|
||||
cart.lines.push({ productId, quantity })
|
||||
}
|
||||
}
|
||||
|
||||
export const removeLine = (cart: Cart, productId: number) => {
|
||||
cart.lines = cart.lines.filter(l => l.productId !== productId);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
export interface Product {
|
||||
id?: number;
|
||||
name: string;
|
||||
description: string;
|
||||
price: number;
|
||||
|
||||
category?: Category;
|
||||
supplier?: Supplier;
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
id?: number;
|
||||
name: string;
|
||||
|
||||
products?: Product[];
|
||||
}
|
||||
|
||||
export interface Supplier {
|
||||
id?: number;
|
||||
name: string;
|
||||
|
||||
products?: Product[];
|
||||
}
|
||||
|
||||
export interface ProductQueryParameters {
|
||||
pageSize?: number;
|
||||
page?: number;
|
||||
category?: number;
|
||||
searchTerm?: string;
|
||||
}
|
||||
|
||||
export interface ProductQueryResult {
|
||||
products: Product[];
|
||||
totalCount: number;
|
||||
categories: Category[];
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Category, Product, Supplier, ProductQueryParameters,
|
||||
ProductQueryResult } from "./catalog_models";
|
||||
|
||||
export interface CatalogRepository {
|
||||
|
||||
getProducts(params?: ProductQueryParameters): Promise<ProductQueryResult>;
|
||||
|
||||
getProductDetails(ids: number[]): Promise<Product[]>;
|
||||
|
||||
storeProduct(p: Product): Promise<Product>;
|
||||
|
||||
getCategories() : Promise<Category[]>;
|
||||
|
||||
storeCategory(c: Category): Promise<Category>;
|
||||
|
||||
getSuppliers(): Promise<Supplier[]>;
|
||||
|
||||
storeSupplier(s: Supplier): Promise<Supplier>;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface Customer {
|
||||
id?: number;
|
||||
name: string;
|
||||
email: string;
|
||||
|
||||
federatedId?: string;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Customer } from "./customer_models";
|
||||
import { Address } from "./order_models";
|
||||
|
||||
export interface CustomerRepository {
|
||||
|
||||
getCustomer(id: number) : Promise<Customer | null>;
|
||||
|
||||
getCustomerByFederatedId(id: string): Promise<Customer | null>;
|
||||
|
||||
getCustomerAddress(id: number): Promise<Address | null>;
|
||||
|
||||
storeCustomer(customer: Customer): Promise<Customer>;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { CatalogRepository } from "./catalog_repository";
|
||||
import { CatalogRepoImpl} from "./orm";
|
||||
import { OrderRepository } from "./order_repository";
|
||||
import { CustomerRepository } from "./customer_repository";
|
||||
|
||||
const repo = new CatalogRepoImpl();
|
||||
|
||||
export const catalog_repository: CatalogRepository = repo;
|
||||
export const order_repository: OrderRepository = repo;
|
||||
export const customer_repository: CustomerRepository = repo;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Product } from "./catalog_models";
|
||||
import { Customer } from "./customer_models";
|
||||
|
||||
export interface Order {
|
||||
id?: number;
|
||||
|
||||
customer?: Customer;
|
||||
selections?: ProductSelection[];
|
||||
address?: Address;
|
||||
|
||||
shipped: boolean;
|
||||
}
|
||||
|
||||
export interface ProductSelection {
|
||||
id?: number;
|
||||
productId?: number;
|
||||
quantity: number;
|
||||
price: number;
|
||||
}
|
||||
|
||||
export interface Address {
|
||||
id?: number;
|
||||
street: string;
|
||||
city: string;
|
||||
state: string;
|
||||
zip: string;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Order } from "./order_models";
|
||||
|
||||
export interface OrderRepository {
|
||||
|
||||
getOrder(id: number): Promise<Order| null>;
|
||||
|
||||
getOrders(excludeShipped: boolean): Promise<Order[]>;
|
||||
|
||||
storeOrder(order: Order): Promise<Order>;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Sequelize } from "sequelize";
|
||||
import { getConfig } from "../../config";
|
||||
import { initializeModels, CategoryModel, ProductModel, SupplierModel }
|
||||
from "./models";
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
const config = getConfig("catalog:orm_repo");
|
||||
const logging = config.logging
|
||||
? { logging: console.log, logQueryParameters: true}
|
||||
: { logging: false };
|
||||
|
||||
export class BaseRepo {
|
||||
sequelize: Sequelize;
|
||||
|
||||
constructor() {
|
||||
this.sequelize = new Sequelize({ ...config.settings, ...logging })
|
||||
this.initModelsAndDatabase();
|
||||
}
|
||||
|
||||
async initModelsAndDatabase() : Promise<void> {
|
||||
initializeModels(this.sequelize);
|
||||
if (config.reset_db) {
|
||||
await this.sequelize.drop();
|
||||
await this.sequelize.sync();
|
||||
await this.addSeedData();
|
||||
} else {
|
||||
await this.sequelize.sync();
|
||||
}
|
||||
}
|
||||
|
||||
async addSeedData() {
|
||||
const data = JSON.parse(readFileSync(config.seed_file).toString());
|
||||
await this.sequelize.transaction(async (transaction) => {
|
||||
await SupplierModel.bulkCreate(data.suppliers, { transaction });
|
||||
await CategoryModel.bulkCreate(data.categories, { transaction });
|
||||
await ProductModel.bulkCreate(data.products, { transaction });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export type Constructor<T = {}> = new (...args: any[]) => T;
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Customer } from "../customer_models";
|
||||
import { CustomerRepository } from "../customer_repository";
|
||||
import { Address } from "../order_models";
|
||||
import { BaseRepo, Constructor } from "./core"
|
||||
import { CustomerModel } from "./models/customer_models";
|
||||
import { AddressModel, OrderModel } from "./models/order_models";
|
||||
|
||||
export function AddCustomers<TBase extends
|
||||
Constructor<BaseRepo>>(Base: TBase) {
|
||||
|
||||
return class extends Base implements CustomerRepository {
|
||||
|
||||
getCustomer(id: number): Promise<Customer | null> {
|
||||
return CustomerModel.findByPk(id, {
|
||||
raw: true
|
||||
});
|
||||
}
|
||||
|
||||
getCustomerByFederatedId(id: string): Promise<Customer | null> {
|
||||
return CustomerModel.findOne({
|
||||
where: { federatedId: id },
|
||||
raw: true
|
||||
})
|
||||
}
|
||||
|
||||
getCustomerAddress(id: number): Promise<Address | null> {
|
||||
return AddressModel.findOne({
|
||||
include: [{
|
||||
model: OrderModel,
|
||||
where: { customerId: id },
|
||||
attributes: []
|
||||
}],
|
||||
order: [["updatedAt", "DESC"]]
|
||||
});
|
||||
}
|
||||
|
||||
async storeCustomer(customer: Customer): Promise<Customer> {
|
||||
const [data, created] = await CustomerModel.findOrCreate({
|
||||
where: { email: customer.email },
|
||||
defaults: customer,
|
||||
});
|
||||
if (!created) {
|
||||
data.name = customer.name;
|
||||
data.email = customer.email;
|
||||
data.federatedId = customer.federatedId;
|
||||
await data.save();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { BaseRepo } from "./core";
|
||||
import { AddQueries } from "./queries";
|
||||
import { AddStorage } from "./storage";
|
||||
import { AddOrderQueries } from "./order_queries";
|
||||
import { AddOrderStorage } from "./order_storage";
|
||||
import { AddCustomers } from "./customers";
|
||||
|
||||
const CatalogRepo = AddStorage(AddQueries(BaseRepo));
|
||||
const RepoWithOrders = AddOrderStorage(AddOrderQueries(CatalogRepo));
|
||||
const RepoWithCustomers = AddCustomers(RepoWithOrders);
|
||||
|
||||
export const CatalogRepoImpl = RepoWithCustomers;
|
||||
@@ -0,0 +1,35 @@
|
||||
import { DataTypes, Sequelize } from "sequelize";
|
||||
import { CategoryModel, ProductModel, SupplierModel } from "./catalog_models";
|
||||
|
||||
const primaryKey = {
|
||||
id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true }
|
||||
};
|
||||
|
||||
export const initializeCatalogModels = (sequelize: Sequelize) => {
|
||||
|
||||
ProductModel.init({
|
||||
...primaryKey,
|
||||
name: { type: DataTypes.STRING},
|
||||
description: { type: DataTypes.STRING},
|
||||
price: { type: DataTypes.DECIMAL(10, 2) }
|
||||
}, { sequelize })
|
||||
|
||||
CategoryModel.init({
|
||||
...primaryKey,
|
||||
name: { type: DataTypes.STRING}
|
||||
}, { sequelize });
|
||||
|
||||
SupplierModel.init({
|
||||
...primaryKey,
|
||||
name: { type: DataTypes.STRING}
|
||||
}, { sequelize})
|
||||
|
||||
ProductModel.belongsTo(CategoryModel,
|
||||
{ foreignKey: "categoryId", as: "category"});
|
||||
ProductModel.belongsTo(SupplierModel,
|
||||
{ foreignKey: "supplierId", as: "supplier"});
|
||||
CategoryModel.hasMany(ProductModel,
|
||||
{ foreignKey: "categoryId", as: "products"});
|
||||
SupplierModel.hasMany(ProductModel,
|
||||
{ foreignKey: "supplierId", as: "products"});
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Model, CreationOptional, ForeignKey, InferAttributes,
|
||||
InferCreationAttributes } from "sequelize";
|
||||
|
||||
export class ProductModel extends Model<InferAttributes<ProductModel>,
|
||||
InferCreationAttributes<ProductModel>> {
|
||||
|
||||
declare id?: CreationOptional<number>;
|
||||
|
||||
declare name: string;
|
||||
declare description: string;
|
||||
declare price: number;
|
||||
|
||||
declare categoryId: ForeignKey<CategoryModel["id"]>;
|
||||
declare supplierId: ForeignKey<SupplierModel["id"]>;
|
||||
|
||||
declare category?: InferAttributes<CategoryModel>
|
||||
declare supplier?: InferAttributes<SupplierModel>
|
||||
}
|
||||
|
||||
export class CategoryModel extends Model<InferAttributes<CategoryModel>,
|
||||
InferCreationAttributes<CategoryModel>> {
|
||||
|
||||
declare id?: CreationOptional<number>;
|
||||
declare name: string;
|
||||
|
||||
declare products?: InferAttributes<ProductModel>[];
|
||||
}
|
||||
|
||||
export class SupplierModel extends Model<InferAttributes<SupplierModel>,
|
||||
InferCreationAttributes<SupplierModel>> {
|
||||
|
||||
declare id?: CreationOptional<number>;
|
||||
declare name: string;
|
||||
|
||||
declare products?: InferAttributes<ProductModel>[];
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { DataTypes, Sequelize } from "sequelize";
|
||||
import { CustomerModel } from "./customer_models";
|
||||
|
||||
export const initializeCustomerModels = (sequelize: Sequelize) => {
|
||||
|
||||
CustomerModel.init({
|
||||
id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true},
|
||||
name: { type: DataTypes.STRING},
|
||||
email: { type: DataTypes.STRING },
|
||||
federatedId: { type: DataTypes.STRING }
|
||||
}, { sequelize})
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Model, CreationOptional, InferAttributes, InferCreationAttributes }
|
||||
from "sequelize";
|
||||
import { Customer } from "../../customer_models";
|
||||
|
||||
export class CustomerModel extends Model<InferAttributes<CustomerModel>,
|
||||
InferCreationAttributes<CustomerModel>> implements Customer {
|
||||
|
||||
declare id?: CreationOptional<number>;
|
||||
declare name: string;
|
||||
declare email: string;
|
||||
|
||||
declare federatedId?: string;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Sequelize } from "sequelize";
|
||||
import { initializeCatalogModels } from "./catalog_helpers";
|
||||
import { initializeCustomerModels } from "./customer_helpers";
|
||||
import { initializeOrderModels } from "./order_helpers";
|
||||
|
||||
export { ProductModel, CategoryModel, SupplierModel } from "./catalog_models";
|
||||
|
||||
export const initializeModels = (sequelize: Sequelize) => {
|
||||
initializeCatalogModels(sequelize);
|
||||
initializeCustomerModels(sequelize);
|
||||
initializeOrderModels(sequelize);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { DataTypes, Sequelize } from "sequelize";
|
||||
import { OrderModel, ProductSelectionModel, AddressModel }
|
||||
from "./order_models";
|
||||
import { CustomerModel } from "./customer_models";
|
||||
import { ProductModel } from ".";
|
||||
|
||||
const primaryKey = {
|
||||
id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true }
|
||||
};
|
||||
|
||||
export const initializeOrderModels = (sequelize: Sequelize) => {
|
||||
|
||||
OrderModel.init({
|
||||
...primaryKey, shipped: DataTypes.BOOLEAN
|
||||
}, {sequelize});
|
||||
|
||||
ProductSelectionModel.init({
|
||||
...primaryKey,
|
||||
quantity: DataTypes.INTEGER, price: DataTypes.DECIMAL(10, 2)
|
||||
}, {sequelize});
|
||||
|
||||
AddressModel.init({
|
||||
...primaryKey,
|
||||
street: DataTypes.STRING, city: DataTypes.STRING,
|
||||
state: DataTypes.STRING, zip: DataTypes.STRING,
|
||||
}, {sequelize});
|
||||
|
||||
OrderModel.belongsTo(CustomerModel, { as: "customer"});
|
||||
OrderModel.belongsTo(AddressModel,
|
||||
{foreignKey: "addressId", as: "address"});
|
||||
OrderModel.belongsToMany(ProductSelectionModel,
|
||||
{ through: "OrderProductJunction",
|
||||
foreignKey: "orderId", as: "selections" });
|
||||
ProductSelectionModel.belongsTo(ProductModel, { as: "product"});
|
||||
|
||||
AddressModel.hasMany(OrderModel, { foreignKey: "addressId"});
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Model, CreationOptional, ForeignKey, InferAttributes,
|
||||
InferCreationAttributes,
|
||||
HasManySetAssociationsMixin} from "sequelize";
|
||||
import { ProductModel } from "./catalog_models";
|
||||
import { CustomerModel } from "./customer_models";
|
||||
import { Address, Order, ProductSelection } from "../../order_models";
|
||||
|
||||
export class OrderModel extends Model<InferAttributes<OrderModel>,
|
||||
InferCreationAttributes<OrderModel>> implements Order {
|
||||
|
||||
declare id?: CreationOptional<number>;
|
||||
declare shipped: boolean;
|
||||
|
||||
declare customerId: ForeignKey<CustomerModel["id"]>;
|
||||
declare customer?: InferAttributes<CustomerModel>
|
||||
|
||||
declare addressId: ForeignKey<AddressModel["id"]>;
|
||||
declare address?: InferAttributes<AddressModel>;
|
||||
|
||||
declare selections?: InferAttributes<ProductSelectionModel>[];
|
||||
|
||||
declare setSelections:
|
||||
HasManySetAssociationsMixin<ProductSelectionModel, number>;
|
||||
}
|
||||
|
||||
export class ProductSelectionModel extends
|
||||
Model<InferAttributes<ProductSelectionModel>,
|
||||
InferCreationAttributes<ProductSelectionModel>>
|
||||
implements ProductSelection {
|
||||
|
||||
declare id?: CreationOptional<number>;
|
||||
|
||||
declare productId: ForeignKey<ProductModel["id"]>;
|
||||
declare product?: InferAttributes<ProductModel>
|
||||
|
||||
declare quantity: number;
|
||||
declare price: number;
|
||||
|
||||
declare orderId: ForeignKey<OrderModel["id"]>;
|
||||
declare order?: InferAttributes<OrderModel>;
|
||||
}
|
||||
|
||||
export class AddressModel extends Model<InferAttributes<AddressModel>,
|
||||
InferCreationAttributes<AddressModel>> implements Address {
|
||||
|
||||
declare id?: CreationOptional<number>;
|
||||
|
||||
declare street: string;
|
||||
declare city: string;
|
||||
declare state: string;
|
||||
declare zip: string;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Attributes, FindOptions } from "sequelize";
|
||||
import { Order } from "../order_models"
|
||||
import { BaseRepo, Constructor } from "./core"
|
||||
import { AddressModel, OrderModel } from "./models/order_models";
|
||||
import { CustomerModel } from "./models/customer_models";
|
||||
|
||||
const queryConfig: FindOptions<Attributes<OrderModel>> = {
|
||||
include: [
|
||||
{ model: AddressModel, as: "address"},
|
||||
{ model: CustomerModel, as: "customer" }
|
||||
],
|
||||
raw: true, nest: true
|
||||
}
|
||||
|
||||
export function AddOrderQueries<TBase
|
||||
extends Constructor<BaseRepo>>(Base: TBase) {
|
||||
|
||||
return class extends Base {
|
||||
|
||||
getOrder(id: number) : Promise<Order | null> {
|
||||
return OrderModel.findByPk(id, queryConfig);
|
||||
}
|
||||
|
||||
getOrders(excludeShipped: boolean): Promise<Order[]> {
|
||||
return OrderModel.findAll(
|
||||
excludeShipped ?
|
||||
{ ...queryConfig, where: { shipped: false}} : queryConfig
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Order } from "../order_models"
|
||||
import { BaseRepo, Constructor } from "./core"
|
||||
import { AddressModel, OrderModel, ProductSelectionModel }
|
||||
from "./models/order_models";
|
||||
import { CustomerModel } from "./models/customer_models";
|
||||
|
||||
export function AddOrderStorage<TBase extends
|
||||
Constructor<BaseRepo>>(Base: TBase) {
|
||||
|
||||
return class extends Base {
|
||||
|
||||
storeOrder(order: Order): Promise<Order> {
|
||||
return this.sequelize.transaction(async (transaction) => {
|
||||
const { id, shipped } = order;
|
||||
const [stored] =
|
||||
await OrderModel.upsert({ id, shipped }, {transaction});
|
||||
|
||||
if (order.customer) {
|
||||
|
||||
const [{id}] = await CustomerModel.findOrCreate({
|
||||
where: { email: order.customer.email},
|
||||
defaults: order.customer,
|
||||
transaction
|
||||
});
|
||||
stored.customerId = id;
|
||||
}
|
||||
if (order.address) {
|
||||
|
||||
const [{id}] = await AddressModel.findOrCreate({
|
||||
where: { ...order.address },
|
||||
defaults: order.address,
|
||||
transaction
|
||||
});
|
||||
stored.addressId = id;
|
||||
}
|
||||
await stored.save({transaction});
|
||||
if (order.selections) {
|
||||
const sels = await ProductSelectionModel.bulkCreate(
|
||||
order.selections, { transaction});
|
||||
await stored.setSelections(
|
||||
sels, { transaction });
|
||||
}
|
||||
return stored;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { CategoryModel, ProductModel, SupplierModel } from "./models";
|
||||
import { BaseRepo, Constructor } from "./core"
|
||||
import { ProductQueryParameters } from "../catalog_models";
|
||||
import { Op } from "sequelize";
|
||||
|
||||
export function AddQueries<TBase extends Constructor<BaseRepo>>(Base: TBase) {
|
||||
return class extends Base {
|
||||
|
||||
async getProducts(params?: ProductQueryParameters) {
|
||||
const opts: any = {};
|
||||
if (params?.page && params.pageSize) {
|
||||
opts.limit = params?.pageSize,
|
||||
opts.offset = (params.page -1) * params.pageSize
|
||||
}
|
||||
if(params?.searchTerm) {
|
||||
const searchOp = { [Op.like]: "%" + params.searchTerm + "%"};
|
||||
opts.where = {
|
||||
[Op.or]: { name: searchOp, description: searchOp }
|
||||
}
|
||||
}
|
||||
if (params?.category) {
|
||||
opts.where = {
|
||||
...opts.where, categoryId: params.category
|
||||
}
|
||||
}
|
||||
const result = await ProductModel.findAndCountAll({
|
||||
include: [
|
||||
{model: SupplierModel, as: "supplier" },
|
||||
{model: CategoryModel, as: "category"}],
|
||||
raw: true, nest: true,
|
||||
...opts
|
||||
});
|
||||
const categories = await this.getCategories();
|
||||
return { products: result.rows, totalCount: result.count, categories };
|
||||
}
|
||||
|
||||
getCategories() {
|
||||
return CategoryModel.findAll({raw: true, nest: true})
|
||||
}
|
||||
|
||||
getSuppliers() {
|
||||
return SupplierModel.findAll({raw: true, nest: true});
|
||||
}
|
||||
|
||||
getProductDetails(ids: number[]) {
|
||||
return ProductModel.findAll({
|
||||
where: { id: { [Op.in]: ids }}, raw: true, nest: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Transaction } from "sequelize";
|
||||
import { Category, Product, Supplier } from "../catalog_models";
|
||||
import { CategoryModel, ProductModel, SupplierModel } from "./models";
|
||||
import { BaseRepo, Constructor } from "./core"
|
||||
|
||||
export function AddStorage<TBase extends Constructor<BaseRepo>>(Base: TBase) {
|
||||
return class extends Base {
|
||||
|
||||
storeProduct(p: Product) {
|
||||
return this.sequelize.transaction(async (transaction) => {
|
||||
|
||||
if (p.category) {
|
||||
p.category = await this.storeCategory(p.category)
|
||||
}
|
||||
if (p.supplier) {
|
||||
p.supplier = await this.storeSupplier(p.supplier);
|
||||
}
|
||||
|
||||
const [stored] = await ProductModel.upsert({
|
||||
id: p.id, name: p.name, description: p.description,
|
||||
price: p.price, categoryId: p.category?.id,
|
||||
supplierId: p.supplier?.id
|
||||
}, { transaction });
|
||||
return stored;
|
||||
});
|
||||
}
|
||||
|
||||
async storeCategory(c: Category, transaction?: Transaction) {
|
||||
const [stored] = await CategoryModel.upsert({
|
||||
id: c.id, name: c.name
|
||||
}, { transaction});
|
||||
return stored;
|
||||
}
|
||||
|
||||
async storeSupplier(s: Supplier, transaction?: Transaction) {
|
||||
const [stored] = await SupplierModel.upsert({
|
||||
id: s.id, name: s.name
|
||||
}, {transaction});
|
||||
return stored;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import validator from "validator";
|
||||
import { ValidationStatus } from "./validation_types";
|
||||
|
||||
export const minLength = (min: number) => (status: ValidationStatus) => {
|
||||
if (!validator.isLength(status.value, { min })) {
|
||||
status.setInvalid(true);
|
||||
status.messages.push(`Enter at least ${min} characters`);
|
||||
}
|
||||
};
|
||||
|
||||
export const email = (status: ValidationStatus) => {
|
||||
if (!validator.isEmail(status.value)) {
|
||||
status.setInvalid(true);
|
||||
status.messages.push("Enter an email address");
|
||||
}
|
||||
};
|
||||
|
||||
export const required = (status: ValidationStatus) => {
|
||||
if (validator.isEmpty(status.value.toString(), { ignore_whitespace: true})) {
|
||||
status.setInvalid(true);
|
||||
status.messages.push("A value is required");
|
||||
}
|
||||
};
|
||||
|
||||
export const no_op = (status: ValidationStatus) => { /* do nothing */ }
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./validation_types";
|
||||
export * from "./validator";
|
||||
export * from "./basic_rules";
|
||||
export * from "./order_rules";
|
||||
export * from "./product_dto_rules";
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Validator } from "./validator";
|
||||
import { required, minLength, email, no_op } from "./basic_rules";
|
||||
import { Address } from "../order_models";
|
||||
import { Customer } from "../customer_models";
|
||||
|
||||
export const CustomerValidator = new Validator<Customer>({
|
||||
name: [required, minLength(6)],
|
||||
email: email,
|
||||
federatedId: no_op
|
||||
});
|
||||
|
||||
export const AddressValidator = new Validator<Address>({
|
||||
street: required,
|
||||
city: required,
|
||||
state: required,
|
||||
zip: no_op
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Validator } from "./validator";
|
||||
import { required, minLength } from "./basic_rules";
|
||||
import { ValidationStatus } from ".";
|
||||
import { CategoryModel, SupplierModel } from "../orm/models";
|
||||
|
||||
type ProductDTO = {
|
||||
name: string, description: string, categoryId: number,
|
||||
supplierId: number, price: number
|
||||
}
|
||||
|
||||
const supplierExists = async (status: ValidationStatus) => {
|
||||
const count = await SupplierModel.count({ where: { id: status.value } });
|
||||
if (count !== 1) {
|
||||
status.setInvalid(true);
|
||||
status.messages.push("A valid supplier is required");
|
||||
}
|
||||
}
|
||||
|
||||
const categoryExists = async (status: ValidationStatus) => {
|
||||
const count = await CategoryModel.count({ where: { id: status.value } });
|
||||
if (count !== 1) {
|
||||
status.setInvalid(true);
|
||||
status.messages.push("A valid category is required");
|
||||
}
|
||||
}
|
||||
|
||||
export const ProductDTOValidator = new Validator<ProductDTO>({
|
||||
name: [required, minLength(3)],
|
||||
description: required,
|
||||
categoryId : categoryExists,
|
||||
supplierId: supplierExists,
|
||||
price: required,
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
export class ValidationStatus {
|
||||
private invalid: boolean = false;
|
||||
|
||||
constructor(public readonly value: any) {}
|
||||
|
||||
get isInvalid() : boolean {
|
||||
return this.invalid
|
||||
}
|
||||
|
||||
setInvalid(newValue: boolean) {
|
||||
this.invalid = newValue || this.invalid;
|
||||
}
|
||||
|
||||
messages: string[] = [];
|
||||
}
|
||||
|
||||
export type ValidationRule = (status: ValidationStatus)
|
||||
=> void | Promise<void>;
|
||||
|
||||
export type ValidationRuleSet<T> = {
|
||||
[key in keyof Omit<Required<T>, "id">]: ValidationRule | ValidationRule[];
|
||||
}
|
||||
|
||||
export type ValidationResults<T> = {
|
||||
[key in keyof Omit<Required<T>, "id">]: ValidationStatus;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ValidationResults, ValidationRule, ValidationRuleSet,
|
||||
ValidationStatus } from "./validation_types";
|
||||
|
||||
export class Validator<T>{
|
||||
|
||||
constructor(public rules: ValidationRuleSet<T>,
|
||||
public breakOnInvalid = true) {}
|
||||
|
||||
async validate(data: any): Promise<ValidationResults<T>> {
|
||||
const vdata = Object.entries(this.rules).map(async ([key, rules]) => {
|
||||
const status = new ValidationStatus(data?.[key] ?? "");
|
||||
const rs = (Array.isArray(rules) ? rules: [rules]);
|
||||
for (const r of rs) {
|
||||
if (!status.isInvalid || !this.breakOnInvalid) {
|
||||
await r(status);
|
||||
}
|
||||
}
|
||||
return [key, status];
|
||||
});
|
||||
const done = await Promise.all(vdata);
|
||||
return Object.fromEntries(done);
|
||||
}
|
||||
|
||||
validateOriginal(data: any): ValidationResults<T> {
|
||||
const vdata = Object.entries(this.rules).map(([key, rules]) => {
|
||||
const status = new ValidationStatus(data?.[key] ?? "");
|
||||
(Array.isArray(rules) ? rules: [rules])
|
||||
.forEach(async (rule: ValidationRule) => {
|
||||
if (!status.isInvalid || !this.breakOnInvalid) {
|
||||
await rule(status);
|
||||
}
|
||||
});
|
||||
return [key, status];
|
||||
});
|
||||
return Object.fromEntries(vdata);
|
||||
}
|
||||
}
|
||||
|
||||
export function isValid<T>(result: ValidationResults<T>) {
|
||||
return Object.values<ValidationStatus>(result)
|
||||
.every(r => r.isInvalid === false);
|
||||
}
|
||||
|
||||
export function getData<T>(result: ValidationResults<T>): T {
|
||||
return Object.fromEntries (Object.entries<ValidationStatus>(result)
|
||||
.map(([key, status]) => [key, status.value])) as T;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Express, ErrorRequestHandler } from "express";
|
||||
import { getConfig } from "./config";
|
||||
import "express-async-errors";
|
||||
|
||||
const template400 = getConfig("errors:400");
|
||||
const template500 = getConfig("errors:500");
|
||||
|
||||
export const createErrorHandlers = (app: Express) => {
|
||||
|
||||
app.use((req, resp) => {
|
||||
resp.statusCode = 404;
|
||||
resp.render(template400);
|
||||
});
|
||||
|
||||
const handler: ErrorRequestHandler = (error, req, resp, next) => {
|
||||
console.log(error);
|
||||
if (resp.headersSent) {
|
||||
return next(error);
|
||||
}
|
||||
try {
|
||||
resp.statusCode = 500;
|
||||
resp.render(template500, { error} );
|
||||
} catch (newErr) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
app.use(handler);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export const buttonClass = (btn: string, mode: string) =>
|
||||
btn == mode ? "btn-secondary" : "btn-outline-secondary";
|
||||
|
||||
export const disabled = (val: any) => val == "ID" ? "disabled" : "";
|
||||
|
||||
export const selected = (val1: any, val2: any) =>
|
||||
val1 == val2 ? "selected" : "";
|
||||
|
||||
export const first = (index: number) => index == 0;
|
||||
|
||||
export const total = (sels: any[]) =>
|
||||
sels.reduce((total, s) => total += (s.quantity * s.product.price), 0);
|
||||
@@ -0,0 +1,4 @@
|
||||
import { Cart } from "../data/cart_models";
|
||||
|
||||
export const countCartItems = (cart: Cart) : number =>
|
||||
cart.lines.reduce((total, line) => total + line.quantity, 0);
|
||||
@@ -0,0 +1,68 @@
|
||||
import Handlebars, { HelperOptions } from "handlebars";
|
||||
import { stringify } from "querystring";
|
||||
import { escape } from "querystring";
|
||||
|
||||
const getData = (options:HelperOptions) => {
|
||||
return {...options.data.root, ...options.hash}
|
||||
};
|
||||
|
||||
export const navigationUrl = (options: HelperOptions) => {
|
||||
const { page, pageSize, category, searchTerm } = getData(options);
|
||||
return "/?" + stringify({ page, pageSize, category, searchTerm });
|
||||
}
|
||||
|
||||
export const escapeUrl = (url: string) => escape(url);
|
||||
|
||||
export const pageButtons = (options: HelperOptions) => {
|
||||
const { page, pageCount } = getData(options);
|
||||
|
||||
let output = "";
|
||||
for (let i = 1; i <= pageCount; i++) {
|
||||
output += options.fn({
|
||||
page, pageCount, index: i, selected: i === page
|
||||
});
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
export const pageSizeOptions = (options: HelperOptions) => {
|
||||
const { pageSize } = getData(options);
|
||||
let output = "";
|
||||
[3, 6, 9].forEach(size => {
|
||||
output += options.fn({ size,
|
||||
selected: pageSize === size ? "selected": ""})
|
||||
})
|
||||
return output;
|
||||
}
|
||||
|
||||
export const categoryButtons = (options: HelperOptions) => {
|
||||
const { category, categories } = getData(options);
|
||||
|
||||
let output = "";
|
||||
for (let i = 0; i < categories.length; i++) {
|
||||
output += options.fn({
|
||||
id: categories[i].id,
|
||||
name: categories[i].name,
|
||||
selected: category === categories[i].id
|
||||
})
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
export const highlight = (value: string, options: HelperOptions) => {
|
||||
const { searchTerm } = getData(options);
|
||||
if (searchTerm && searchTerm !== "") {
|
||||
const regexp = new RegExp(searchTerm, "ig");
|
||||
const mod = value.replaceAll(regexp, "<strong>$&</strong>");
|
||||
return new Handlebars.SafeString(mod);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
const formatter = new Intl.NumberFormat("en-us", {
|
||||
style: "currency", currency: "USD"
|
||||
})
|
||||
|
||||
export const currency = (value: number) => {
|
||||
return formatter.format(value);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Env, getEnvironment } from "../config";
|
||||
|
||||
export const isDevelopment = (value: any) => {
|
||||
return getEnvironment() === Env.Development
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Express } from "express";
|
||||
import { getConfig } from "../config";
|
||||
import { engine } from "express-handlebars";
|
||||
import * as env_helpers from "./env";
|
||||
import * as catalog_helpers from "./catalog_helpers";
|
||||
import * as cart_helpers from "./cart_helpers";
|
||||
import * as order_helpers from "./order_helpers";
|
||||
import * as admin_helpers from "./admin_helpers";
|
||||
|
||||
const location = getConfig("templates:location");
|
||||
const config = getConfig("templates:config");
|
||||
|
||||
export const createTemplates = (app: Express) => {
|
||||
|
||||
app.set("views", location);
|
||||
app.engine("handlebars", engine({
|
||||
...config,
|
||||
helpers: {...env_helpers, ...catalog_helpers, ...cart_helpers,
|
||||
...order_helpers, ...admin_helpers}
|
||||
}));
|
||||
app.set("view engine", "handlebars");
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export const toArray = (...args: any[]) => args.slice(0, -1);
|
||||
|
||||
export const lower = (val: string) => val.toLowerCase();
|
||||
|
||||
export const getValue = (val: any, prop: string) =>
|
||||
val?.[prop.toLowerCase()] ?? {};
|
||||
|
||||
export const get = (val: any) => val ?? {};
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Router } from "express";
|
||||
import { CategoryModel, ProductModel, SupplierModel }
|
||||
from "../../data/orm/models";
|
||||
import { ProductDTOValidator, getData, isValid } from "../../data/validation";
|
||||
|
||||
export const createAdminCatalogRoutes = (router: Router) => {
|
||||
|
||||
router.get("/table", async (req, resp) => {
|
||||
const products = await ProductModel.findAll({
|
||||
include: [
|
||||
{model: SupplierModel, as: "supplier" },
|
||||
{model: CategoryModel, as: "category" }],
|
||||
raw: true, nest: true
|
||||
});
|
||||
resp.render("admin/product_table", { products });
|
||||
});
|
||||
|
||||
router.delete("/:id", async (req, resp) => {
|
||||
const id = req.params.id;
|
||||
const count = await ProductModel.destroy({ where: { id }});
|
||||
if (count == 1) {
|
||||
resp.end();
|
||||
} else {
|
||||
throw Error(`Unexpected deletion count result: ${count}`)
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
router.get("/edit/:id", async (req, resp) => {
|
||||
const id = req.params.id;
|
||||
const data = {
|
||||
product: { id: { value: id },
|
||||
...await ProductDTOValidator.validate(
|
||||
await ProductModel.findByPk(id, { raw: true}))},
|
||||
suppliers: await SupplierModel.findAll({raw: true}),
|
||||
categories: await CategoryModel.findAll({raw: true})
|
||||
};
|
||||
resp.render("admin/product_editor", data);
|
||||
});
|
||||
|
||||
router.put("/:id", async (req, resp) => {
|
||||
const validation = await ProductDTOValidator.validate(req.body);
|
||||
if (isValid(validation)) {
|
||||
await ProductModel.update(
|
||||
getData(validation), { where: { id: req.params.id}}
|
||||
);
|
||||
resp.redirect(303, "/api/products/table");
|
||||
} else {
|
||||
resp.render("admin/product_editor", {
|
||||
product: { id: { value: req.params.id} , ...validation },
|
||||
suppliers: await SupplierModel.findAll({raw: true}),
|
||||
categories: await CategoryModel.findAll({raw: true})
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/create", async (req, resp) => {
|
||||
const data = {
|
||||
product: {},
|
||||
suppliers: await SupplierModel.findAll({raw: true}),
|
||||
categories: await CategoryModel.findAll({raw: true}),
|
||||
create: true
|
||||
};
|
||||
resp.render("admin/product_editor", data);
|
||||
});
|
||||
|
||||
router.post("/create", async (req, resp) => {
|
||||
const validation = await ProductDTOValidator.validate(req.body);
|
||||
if (isValid(validation)) {
|
||||
await ProductModel.create(getData(validation));
|
||||
resp.redirect(303, "/api/products/table");
|
||||
} else {
|
||||
resp.render("admin/product_editor", {
|
||||
product: validation,
|
||||
suppliers: await SupplierModel.findAll({raw: true}),
|
||||
categories: await CategoryModel.findAll({raw: true}),
|
||||
create: true
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Router } from "express";
|
||||
import { AddressModel, OrderModel, ProductSelectionModel }
|
||||
from "../../data/orm/models/order_models";
|
||||
import { CustomerModel } from "../../data/orm/models/customer_models";
|
||||
import { ProductModel } from "../../data/orm/models";
|
||||
|
||||
export const createAdminOrderRoutes = (router: Router) => {
|
||||
|
||||
router.get("/table", async (req, resp) => {
|
||||
const orders = (await OrderModel.findAll({
|
||||
include: [
|
||||
{ model: CustomerModel, as: "customer"},
|
||||
{ model: AddressModel, as: "address"},
|
||||
{ model: ProductSelectionModel, as: "selections",
|
||||
include: [{ model: ProductModel, as: "product"}]
|
||||
}
|
||||
],
|
||||
order: ["shipped", "id"]
|
||||
})).map(o => o.toJSON())
|
||||
|
||||
resp.render("admin/order_table", { orders });
|
||||
});
|
||||
|
||||
router.post("/ship", async (req, resp) => {
|
||||
const { id, shipped } = req.body;
|
||||
const [rows] = await OrderModel.update({ shipped },{ where: { id }});
|
||||
if (rows === 1) {
|
||||
resp.redirect(303, "/api/orders/table");
|
||||
} else {
|
||||
throw new Error(`Expected 1 row updated, but got ${rows}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Express, NextFunction, Request, Response, Router } from "express";
|
||||
import { createAdminCatalogRoutes } from "./admin_catalog_routes";
|
||||
import { createAdminOrderRoutes } from "./admin_order_routes";
|
||||
import passport from "passport";
|
||||
import { getConfig} from "../../config";
|
||||
|
||||
const users: string[] = getConfig("admin:users", []);
|
||||
|
||||
export const createAdminRoutes = (app: Express) => {
|
||||
|
||||
app.use((req, resp, next) => {
|
||||
resp.locals.layout = false;
|
||||
resp.locals.user = req.user;
|
||||
next();
|
||||
});
|
||||
|
||||
app.get("/admin/signin", (req, resp) => resp.render("admin/signin"));
|
||||
|
||||
app.post("/admin/signout", (req, resp) =>
|
||||
req.logOut(() => { resp.redirect("/admin/signin") }));
|
||||
|
||||
app.get("/admin/google", passport.authenticate("admin-auth"));
|
||||
|
||||
app.get("/auth-signin-google", passport.authenticate("admin-auth", {
|
||||
successRedirect: "/admin/products", keepSessionInfo: true
|
||||
}));
|
||||
|
||||
const authCheck = (r: Request) => users.find(u => r.user?.email === u);
|
||||
|
||||
const apiAuth = (req: Request, resp: Response, next: NextFunction) => {
|
||||
if (!authCheck(req)) {
|
||||
return resp.sendStatus(401)
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
const cat_router = Router();
|
||||
createAdminCatalogRoutes(cat_router);
|
||||
app.use("/api/products", apiAuth, cat_router);
|
||||
|
||||
const order_router = Router();
|
||||
createAdminOrderRoutes(order_router);
|
||||
app.use("/api/orders", apiAuth, order_router);
|
||||
|
||||
const userAuth = (req: Request, resp: Response, next: NextFunction) => {
|
||||
if (!authCheck(req)) {
|
||||
return resp.redirect("/admin/signin");
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
app.get("/admin", userAuth, (req, resp) =>
|
||||
resp.redirect("/admin/products"));
|
||||
|
||||
app.get("/admin/products", userAuth, (req, resp) => {
|
||||
resp.locals.content = "/api/products/table";
|
||||
resp.render("admin/admin_layout");
|
||||
})
|
||||
|
||||
app.get("/admin/products/edit/:id", userAuth, (req, resp) => {
|
||||
resp.locals.content = `/api/products/edit/${req.params.id}`;
|
||||
resp.render("admin/admin_layout");
|
||||
})
|
||||
|
||||
app.get("/admin/orders", userAuth, (req, resp) => {
|
||||
resp.locals.content = "/api/orders/table";
|
||||
resp.render("admin/admin_layout");
|
||||
})
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user