CVE-2023-46943
Description
An issue was discovered in NPM's package @evershop/evershop before version 1.0.0-rc.8. The HMAC secret used for generating tokens is hardcoded as "secret". A weak HMAC secret poses a risk because attackers can use the predictable secret to create valid JSON Web Tokens (JWTs), allowing them access to important information and actions within the application.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
EverShop's HMAC secret is hardcoded as 'secret', letting attackers forge valid JWTs and gain unauthorized access.
Root
Cause
The @evershop/evershop package for Node.js (prior to version 1.0.0-rc.8) contains a hardcoded HMAC secret value of "secret" used for generating JSON Web Tokens (JWTs). This was reported as CVE-2023-46943 [1] and is categorized under CWE-798 (Use of Hard-coded Credentials) [4]. The hardcoded secret makes the token signing key predictable to anyone who inspects the source code or the compiled application.
Exploitation
An attacker does not need to be authenticated; knowledge of the HMAC secret is sufficient. With the predictable secret "secret", an attacker can forge valid JWTs with arbitrary claims [1][4]. The attacker does not need to intercept an existing token — they can craft a new token from scratch. If the attacker also knows the token structure and claims expected by the application (which can be obtained through code inspection or error messages), they can impersonate any user or administrative role.
Impact
Using the forged JWT, an attacker can gain access to sensitive information and perform privileged actions within the EverShop application [1][4]. The impact includes unauthorized access to admin panels, customer data, and the ability to modify store settings, products, or orders. The confidentiality, integrity, and availability of the application are at risk [2][4].
Mitigation
The fix was implemented in commit 96d9ca3 and released in version 1.0.0-rc.8 of the package [2]. The vulnerability was addressed by moving to session-based authentication instead of the flawed JWT approach [2][4]. All users should upgrade to version 1.0.0-rc.8 or later. There is no workaround that can securely replace the hardcoded secret without a code change.
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@evershop/evershopnpm | < 1.0.0-rc.9 | 1.0.0-rc.9 |
Affected products
2- @evershop/evershopdescription
Patches
196d9ca3e024eMerge pull request #320 from evershopcommerce/dev
116 files changed · +1116 −1525
extensions/productComment/api/productComment/addComment.js+0 −19 removed@@ -1,19 +0,0 @@ -const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); -const { insert } = require('@evershop/postgres-query-builder'); - -module.exports = async function graphql(request, response, delegate, next) { - try { - const { body: { product_id, user_name, comment } } = request; - // Insert the comment into the database - await insert('product_comment') - .given({ - product_id, - user_name, - comment - }) - .execute(pool); - response.json({ success: true }); - } catch (error) { - next(error); - } -} \ No newline at end of file
extensions/productComment/api/productComment/[context]bodyParser[auth].js+0 −5 removed@@ -1,5 +0,0 @@ -const bodyParser = require('body-parser'); - -module.exports = (request, response, deledate, next) => { - bodyParser.json({ inflate: false })(request, response, next); -} \ No newline at end of file
extensions/productComment/api/productComment/payloadSchema.json+0 −20 removed@@ -1,20 +0,0 @@ -{ - "type": "object", - "properties": { - "product_id": { - "type": "string" - }, - "user_name": { - "type": "string" - }, - "comment": { - "type": "string" - } - }, - "required": [ - "product_id", - "user_name", - "comment" - ], - "additionalProperties": true -}
extensions/productComment/api/productComment/route.json+0 −7 removed@@ -1,7 +0,0 @@ -{ - "methods": [ - "POST" - ], - "path": "/productComments", - "access": "public" -} \ No newline at end of file
extensions/productComment/bootstrap.js+0 −11 removed@@ -1,11 +0,0 @@ -const config = require('config'); - -module.exports = () => { - // Default configuration - const checkoutConfig = { - name: 'Shipped', - badge: 'success', - progress: 'complete' - }; - config.util.setModuleDefaults('checkout.order.shipmentStatus.shipped', checkoutConfig); -};
extensions/productComment/graphql/types/Comment/Comment.graphql+0 −10 removed@@ -1,10 +0,0 @@ -type Comment { - commentId: Int! - userName: String - comment: String - createdAt: String -} - -extend type Query { - comments(productId: Int!): [Comment] -} \ No newline at end of file
extensions/productComment/graphql/types/Comment/Comment.resolvers.js+0 −15 removed@@ -1,15 +0,0 @@ -const { camelCase } = require('@evershop/evershop/src/lib/util/camelCase'); -const { select } = require('@evershop/postgres-query-builder'); - -module.exports = { - Query: { - comments: async (root, { productId }, { pool }) => { - const comments = await select() - .from('product_comment') - .where('product_id', '=', productId) - .execute(pool); - - return comments.map(comment => camelCase(comment)); - } - } -} \ No newline at end of file
extensions/productComment/migration/Version-1.0.0.js+0 −14 removed@@ -1,14 +0,0 @@ -const { execute } = require('@evershop/postgres-query-builder'); - -// eslint-disable-next-line no-multi-assign -module.exports = exports = async (connection) => { - await execute(connection, `CREATE TABLE "product_comment" ( - "comment_id" INT GENERATED ALWAYS AS IDENTITY (START WITH 1 INCREMENT BY 1) PRIMARY KEY, - "product_id" INT NOT NULL, - "user_name" varchar NOT NULL, - "comment" varchar DEFAULT NULL, - "created_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT "FK_PRODUCT_COMMENT" FOREIGN KEY ("product_id") REFERENCES "product" ("product_id") ON DELETE CASCADE -) -`); -};
extensions/productComment/package.json+0 −10 removed@@ -1,10 +0,0 @@ -{ - "name": "@evershop/productcomment", - "version": "1.0.2", - "description": "", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "The Nguyen(https://evershop.io/)", - "license": "ISC" -} \ No newline at end of file
extensions/productComment/pages/frontStore/productView/CommentForm.jsx+0 −62 removed@@ -1,62 +0,0 @@ -import React from 'react'; -import { Form } from '@components/common/form/Form'; -import { Field } from '@components/common/form/Field'; - -export default function ComponentForm({ action, product }) { - const [error, setError] = React.useState(null); - - const onSuccess = (response) => { - if (response.success) { - window.location.reload(); - } else { - setError(response.error.message); - } - } - - return ( - <div className='product-comment-form'> - <h3>Your comment</h3> - {error && <div className='error text-critical'>{error}</div>} - <Form - id="comment-form" - action={action} - method="POST" - btnText="Submit" - onSuccess={onSuccess} - isJSON={true} - > - <Field - name="user_name" - label="Your Name" - type="text" - validationRules={['notEmpty']} - /> - <Field - name="comment" - label="Your Comment" - type="textarea" - validationRules={['notEmpty']} - /> - <Field - type='hidden' - name='product_id' - value={product.productId} - /> - </Form> - </div> - ); -} - -export const layout = { - areaId: 'productPageMiddleLeft', - sortOrder: 50 -} - -export const query = ` - query { - action: url(routeId: "productComment"), - product: product(id: getContextValue("productId")) { - productId - } - } -`; \ No newline at end of file
extensions/productComment/pages/frontStore/productView/Comments.jsx+0 −32 removed@@ -1,32 +0,0 @@ -import React from 'react'; -import './Comments.scss'; - -export default function Comments({ comments = [] }) { - return <div id="productComments"> - <h3>Comments</h3> - <ul className="comment-list"> - {comments.map((comment) => ( - <li key={comment.commentId}> - <div className='user-name'>{comment.userName}</div> - <p className='comment'>{comment.comment}</p> - </li> - ))} - </ul> - </div>; -} - -export const layout = { - areaId: 'productPageMiddleLeft', - sortOrder: 45 -} - -export const query = ` - query { - comments(productId: getContextValue("productId")) { - commentId - userName - comment - createdAt - } - } -`; \ No newline at end of file
extensions/productComment/pages/frontStore/productView/Comments.scss+0 −18 removed@@ -1,18 +0,0 @@ - -.comment-list { - margin-bottom: 20px; - li { - padding: 10px 0; - border-bottom: 1px solid #eee; - :last-child { - border-bottom: 0; - } - } - .user-name { - font-weight: bold; - margin-bottom: 5px; - } - .comment{ - font-style: italic; - } -} \ No newline at end of file
extensions/productComment/README.md+0 −25 removed@@ -1,25 +0,0 @@ -# EverShop product comment extension - -This is the source code for the EverShop extension development guide: [EverShop product comment extension](https://evershop.io/docs/development/module/create-first-extension). - -## Installation - -```bash -npm install @evershop/productcomment -``` - -Add the extension to your `config/default.json` file: - -```json -{ - "system": { - "extensions": [ - { - "name": "productcomment", - "resolve": "node_modules/@evershop/productcomment", - "enabled": true - } - ] - } -} -``` \ No newline at end of file
.gitignore+1 −1 modified@@ -1,7 +1,6 @@ node_modules ./dist /extensions/* -!/extensions/productComment themes .idea/* .evershop @@ -17,3 +16,4 @@ cypress /config .prettierignore .prettierrc +mysqlToPostgres.js \ No newline at end of file
mysqlToPostgres.js+0 −170 removed@@ -1,170 +0,0 @@ -const mysql = require("mysql2/promise"); -const { Pool } = require("pg"); - -// MySQL database configuration -const mysqlConfig = { - host: "localhost", - user: "root", - password: "123456", - database: "evershop", -}; - -// PostgreSQL database configuration -const pgConfig = { - host: "localhost", - user: "postgres", - password: "123456", - database: "evershop", - port: 5432, -}; - -// List of tables to copy -const tables = [ - "admin_user", - "attribute", - "attribute_group", - "attribute_group_link", - "attribute_option", - "cart", - "cart_address", - "cart_item", - "category", - "category_description", - "cms_page", - "cms_page_description", - "coupon", - "customer", - "customer_address", - "customer_group", - "order", - "order_activity", - "order_address", - "order_item", - "payment_transaction", - "product", - "product_attribute_value_index", - "product_category", - "product_custom_option", - "product_custom_option_value", - "product_description", - "product_image", - "setting", - "shipment", - "user_token_secret", - "variant_group", -]; - -async function copyData(mysqlConn, pgClient, table) { - try { - // Disabling foreign key constraints on PostgreSQL table - const disableFKQuery = `ALTER TABLE "${table}" DISABLE TRIGGER ALL`; - await pgClient.query(disableFKQuery); - - // Fetching data from MySQL table - const mysqlQuery = `SELECT * FROM \`${table}\``; - let [rows, fields] = await mysqlConn.execute(mysqlQuery); - console.log(`Data fetched successfully for ${table}`, rows); - // Checking if the table has data - if (rows.length === 0) { - console.log(`Table ${table} has no data`); - return; - } - - const outdatedColumns = ["uuid", "row_id", "attribute_six", "attribute_sevent", "attribute_eight", "attribute_nine", "attribute_ten" ]; - - // Inserting the data into the PostgreSQL database for each table - const placeholders = new Array( - Object.keys(rows[0]).filter((col) => !outdatedColumns.includes(col)).length - ) - .fill("") - .map((_, index) => `$${index + 1}`) - .join(","); - const pgQuery = `INSERT INTO "${table}" (${Object.keys(rows[0]) - .filter((col) => !outdatedColumns.includes(col)) - .join(",")}) OVERRIDING SYSTEM VALUE VALUES (${placeholders})`; - // If the table is product_description, we need to make the url_key unique - if (table === "product_description") { - rows = rows.map((row) => { - row.url_key = row.url_key + "-" + row.product_description_id; - return row; - }); - } - // Loop through each row of data and insert it into PostgreSQL table - for (const row of rows) { - // Check if the row exists in the PostgreSQL table - const check = await pgClient.query( - `SELECT * FROM "${table}" WHERE ${Object.keys(row)[0]} = $1`, - [row[Object.keys(row)[0]]] - ); - if (check.rows.length > 0) { - // Update the row if it exists - - const updateData = Object.assign({}, row); - delete updateData[Object.keys(row)[0]]; - outdatedColumns.forEach((col) => delete updateData[col]); - const updateQuery = `UPDATE "${table}" SET ${Object.keys(updateData) - .map((col, index) => `${col} = $${index + 1}`) - .join(",")} WHERE ${Object.keys(row)[0]} = ${ - row[Object.keys(row)[0]] - }`; - await pgClient.query(updateQuery, Object.values(updateData)); - } else { - // Transforming the data into a PostgreSQL-compatible format - const newRow = Object.assign({}, row); - outdatedColumns.forEach((col) => delete newRow[col]); - const data = Object.values(newRow); - await pgClient.query(pgQuery, data); - } - } - // Set the sequence value to the max value of the id column - const maxId = await pgClient.query( - `SELECT MAX(${Object.keys(rows[0])[0]}) FROM "${table}"` - ); - - // Get the sequence name - const sequenceName = await pgClient.query( - `SELECT pg_get_serial_sequence('${table}', '${Object.keys(rows[0])[0]}')` - ); - // Set the sequence value - await pgClient.query( - `ALTER SEQUENCE ${ - sequenceName.rows[0].pg_get_serial_sequence - } RESTART WITH ${maxId.rows[0].max + 1}` - ); - - // Re-enabling foreign key constraints on PostgreSQL table - const enableFKQuery = `ALTER TABLE "${table}" ENABLE TRIGGER ALL`; - await pgClient.query(enableFKQuery); - console.log(`Data copied successfully for ${table}`); - } catch (err) { - // Rolling back the transaction if there is an error - await pgClient.query("ROLLBACK"); - console.error(`Error copying data for ${table}: ${err.message}`); - throw err; - } -} - -(async () => { - const mysqlConn = await mysql.createConnection(mysqlConfig); - const postgresPool = new Pool(pgConfig); - const client = await postgresPool.connect(); - - try { - // Start postgresql transaction - await client.query("BEGIN"); - - // Delete data form attributes table - await client.query("DELETE FROM attribute"); - // Fetch data from mysql table - for (const table of tables) { - await copyData(mysqlConn, client, table); - } - // Committing the transaction on PostgreSQL database - await client.query("COMMIT"); - process.exit(0); - } catch (err) { - // Rolling back the transaction if there is an error - await client.query("ROLLBACK"); - process.exit(1); - } -})();
package-lock.json+174 −108 modified@@ -3756,6 +3756,68 @@ "version": "4.0.0", "license": "MIT" }, + "node_modules/@types/pg": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.10.2.tgz", + "integrity": "sha512-MKFs9P6nJ+LAeHLU3V0cODEOgyThJ3OAnmOlsZsxux6sfQs3HRXR5bBn7xG5DjckEFhTAxsXi7k7cd0pCMxpJw==", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^4.0.1" + } + }, + "node_modules/@types/pg/node_modules/pg-types": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.1.tgz", + "integrity": "sha512-hRCSDuLII9/LE3smys1hRHcu5QGcLs9ggT7I/TCs0IE+2Eesxi9+9RWAAwZ0yaGjxoWICF/YHLOEjydGujoJ+g==", + "dependencies": { + "pg-int8": "1.0.1", + "pg-numeric": "1.0.2", + "postgres-array": "~3.0.1", + "postgres-bytea": "~3.0.0", + "postgres-date": "~2.0.1", + "postgres-interval": "^3.0.0", + "postgres-range": "^1.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/pg/node_modules/postgres-array": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", + "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-bytea": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "dependencies": { + "obuf": "~1.1.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/pg/node_modules/postgres-date": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.0.1.tgz", + "integrity": "sha512-YtMKdsDt5Ojv1wQRvUhnyDJNSr2dGIC96mQVKz7xufp07nfuFONzdaowrMHjlAzY6GDLd4f+LUHHAAM1h4MdUw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-interval": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "engines": { + "node": ">=12" + } + }, "node_modules/@types/prettier": { "version": "2.6.3", "dev": true, @@ -5349,10 +5411,6 @@ "node": "*" } }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "license": "BSD-3-Clause" - }, "node_modules/buffer-from": { "version": "1.1.2", "license": "MIT" @@ -5914,6 +5972,18 @@ "dev": true, "license": "MIT" }, + "node_modules/connect-pg-simple": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-9.0.0.tgz", + "integrity": "sha512-tPHR01VQU/qnOl8bMsORD1WyrNkbVkyNmnXfXqaH4NZfxWb3zQkCTPbVHjkhjFpKCQpHA+Ry9Y7medndwyvq6A==", + "dependencies": { + "@types/pg": "^8.10.2", + "pg": "^8.8.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/consola": { "version": "2.15.3", "license": "MIT" @@ -6911,13 +6981,6 @@ "safer-buffer": "^2.1.0" } }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, "node_modules/ee-first": { "version": "1.1.1", "license": "MIT" @@ -7836,7 +7899,8 @@ }, "node_modules/express-session": { "version": "1.17.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", + "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==", "dependencies": { "cookie": "0.4.2", "cookie-signature": "1.0.6", @@ -11472,35 +11536,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/jsonwebtoken": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", - "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", - "dependencies": { - "jws": "^3.2.2", - "lodash": "^4.17.21", - "ms": "^2.1.1", - "semver": "^7.3.8" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/jsonwebtoken/node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/jsprim": { "version": "2.0.2", "dev": true, @@ -11527,23 +11562,6 @@ "node": ">=4.0" } }, - "node_modules/jwa": { - "version": "1.4.1", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "3.2.2", - "license": "MIT", - "dependencies": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, "node_modules/kind-of": { "version": "6.0.3", "devOptional": true, @@ -15485,6 +15503,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" + }, "node_modules/on-finished": { "version": "2.4.1", "license": "MIT", @@ -15833,6 +15856,14 @@ "node": ">=4.0.0" } }, + "node_modules/pg-numeric": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", + "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", + "engines": { + "node": ">=4" + } + }, "node_modules/pg-pool": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.0.tgz", @@ -16184,6 +16215,11 @@ "node": ">=0.10.0" } }, + "node_modules/postgres-range": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.3.tgz", + "integrity": "sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g==" + }, "node_modules/prebuild-install": { "version": "7.1.1", "license": "MIT", @@ -19907,7 +19943,7 @@ }, "packages/evershop": { "name": "@evershop/evershop", - "version": "1.0.0-rc.6", + "version": "1.0.0-rc.7", "license": "GNU GENERAL PUBLIC LICENSE 3.0", "dependencies": { "@babel/core": "^7.20.12", @@ -19936,6 +19972,7 @@ "chokidar": "^3.5.3", "clean-css": "^5.3.1", "config": "^3.3.6", + "connect-pg-simple": "^9.0.0", "cookie-parser": "^1.4.6", "cross-spawn": "^7.0.3", "css-loader": "^6.7.1", @@ -19945,7 +19982,7 @@ "dependency-tree": "^8.1.2", "enquirer": "^2.3.6", "express": "^4.18.1", - "express-session": "^1.17.1", + "express-session": "^1.17.3", "flatpickr": "^4.6.9", "graphql": "^16.6.0", "graphql-tag": "^2.12.6", @@ -19956,7 +19993,6 @@ "is-object": "^1.0.2", "jsesc": "^3.0.2", "json5": "^2.2.1", - "jsonwebtoken": "^9.0.0", "kleur": "3.0.3", "lodash": "^4.17.21", "luxon": "^2.0.2", @@ -21556,6 +21592,7 @@ "chokidar": "^3.5.3", "clean-css": "^5.3.1", "config": "^3.3.6", + "connect-pg-simple": "^9.0.0", "cookie-parser": "^1.4.6", "cross-spawn": "^7.0.3", "css-loader": "^6.7.1", @@ -21565,7 +21602,7 @@ "dependency-tree": "^8.1.2", "enquirer": "^2.3.6", "express": "^4.18.1", - "express-session": "^1.17.1", + "express-session": "^1.17.3", "flatpickr": "^4.6.9", "graphql": "^16.6.0", "graphql-tag": "^2.12.6", @@ -21576,7 +21613,6 @@ "is-object": "^1.0.2", "jsesc": "^3.0.2", "json5": "^2.2.1", - "jsonwebtoken": "^9.0.0", "kleur": "3.0.3", "lodash": "^4.17.21", "luxon": "^2.0.2", @@ -22583,6 +22619,55 @@ "@types/parse-json": { "version": "4.0.0" }, + "@types/pg": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.10.2.tgz", + "integrity": "sha512-MKFs9P6nJ+LAeHLU3V0cODEOgyThJ3OAnmOlsZsxux6sfQs3HRXR5bBn7xG5DjckEFhTAxsXi7k7cd0pCMxpJw==", + "requires": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^4.0.1" + }, + "dependencies": { + "pg-types": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.1.tgz", + "integrity": "sha512-hRCSDuLII9/LE3smys1hRHcu5QGcLs9ggT7I/TCs0IE+2Eesxi9+9RWAAwZ0yaGjxoWICF/YHLOEjydGujoJ+g==", + "requires": { + "pg-int8": "1.0.1", + "pg-numeric": "1.0.2", + "postgres-array": "~3.0.1", + "postgres-bytea": "~3.0.0", + "postgres-date": "~2.0.1", + "postgres-interval": "^3.0.0", + "postgres-range": "^1.1.1" + } + }, + "postgres-array": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", + "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==" + }, + "postgres-bytea": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "requires": { + "obuf": "~1.1.2" + } + }, + "postgres-date": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.0.1.tgz", + "integrity": "sha512-YtMKdsDt5Ojv1wQRvUhnyDJNSr2dGIC96mQVKz7xufp07nfuFONzdaowrMHjlAzY6GDLd4f+LUHHAAM1h4MdUw==" + }, + "postgres-interval": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==" + } + } + }, "@types/prettier": { "version": "2.6.3", "dev": true @@ -23601,9 +23686,6 @@ "version": "0.2.13", "dev": true }, - "buffer-equal-constant-time": { - "version": "1.0.1" - }, "buffer-from": { "version": "1.1.2" }, @@ -23965,6 +24047,15 @@ "version": "1.0.11", "dev": true }, + "connect-pg-simple": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-9.0.0.tgz", + "integrity": "sha512-tPHR01VQU/qnOl8bMsORD1WyrNkbVkyNmnXfXqaH4NZfxWb3zQkCTPbVHjkhjFpKCQpHA+Ry9Y7medndwyvq6A==", + "requires": { + "@types/pg": "^8.10.2", + "pg": "^8.8.0" + } + }, "consola": { "version": "2.15.3" }, @@ -24649,12 +24740,6 @@ "safer-buffer": "^2.1.0" } }, - "ecdsa-sig-formatter": { - "version": "1.0.11", - "requires": { - "safe-buffer": "^5.0.1" - } - }, "ee-first": { "version": "1.1.1" }, @@ -25289,6 +25374,8 @@ }, "express-session": { "version": "1.17.3", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", + "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==", "requires": { "cookie": "0.4.2", "cookie-signature": "1.0.6", @@ -27521,27 +27608,6 @@ "universalify": "^2.0.0" } }, - "jsonwebtoken": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", - "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", - "requires": { - "jws": "^3.2.2", - "lodash": "^4.17.21", - "ms": "^2.1.1", - "semver": "^7.3.8" - }, - "dependencies": { - "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, "jsprim": { "version": "2.0.2", "dev": true, @@ -27560,21 +27626,6 @@ "object.assign": "^4.1.2" } }, - "jwa": { - "version": "1.4.1", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "3.2.2", - "requires": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, "kind-of": { "version": "6.0.3", "devOptional": true @@ -30191,6 +30242,11 @@ "es-abstract": "^1.19.1" } }, + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" + }, "on-finished": { "version": "2.4.1", "requires": { @@ -30406,6 +30462,11 @@ "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" }, + "pg-numeric": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", + "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==" + }, "pg-pool": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.0.tgz", @@ -30602,6 +30663,11 @@ "xtend": "^4.0.0" } }, + "postgres-range": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.3.tgz", + "integrity": "sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g==" + }, "prebuild-install": { "version": "7.1.1", "requires": {
packages/evershop/bin/lib/addDefaultMiddlewareFuncs.js+63 −10 modified@@ -1,4 +1,6 @@ const cookieParser = require('cookie-parser'); +const session = require('express-session'); +const sessionStorage = require('connect-pg-simple'); const pathToRegexp = require('path-to-regexp'); const webpack = require('webpack'); const { debug } = require('@evershop/evershop/src/lib/log/debuger'); @@ -20,6 +22,17 @@ const { const { translate } = require('@evershop/evershop/src/lib/locale/translate/translate'); +const isProductionMode = require('@evershop/evershop/src/lib/util/isProductionMode'); +const { getConfig } = require('@evershop/evershop/src/lib/util/getConfig'); +const { + getAdminSessionCookieName +} = require('@evershop/evershop/src/modules/auth/services/getAdminSessionCookieName'); +const { + getFrontStoreSessionCookieName +} = require('@evershop/evershop/src/modules/auth/services/getFrontStoreSessionCookieName'); +const { + getCookieSecret +} = require('@evershop/evershop/src/modules/auth/services/getCookieSecret'); module.exports = exports = {}; @@ -51,6 +64,52 @@ exports.addDefaultMiddlewareFuncs = function addDefaultMiddlewareFuncs( // Add theme public static middleware app.use(themePublicStatic); + // Express session + const cookieSecret = getCookieSecret(); + const sess = { + store: + process.env.NODE_ENV === 'test' + ? undefined + : new (sessionStorage(session))({ + pool: pool + }), + secret: cookieSecret, + cookie: { + maxAge: 24 * 60 * 60 * 1000 + }, + resave: getConfig('system.session.resave', false), + saveUninitialized: true + }; + + if (isProductionMode()) { + app.set('trust proxy', 1); + sess.cookie.secure = false; + } + + const adminSessionMiddleware = session({ + ...sess, + name: getAdminSessionCookieName() + }); + + const frontStoreSessionMiddleware = session({ + ...sess, + name: getFrontStoreSessionCookieName() + }); + + const sessionMiddleware = (request, response, next) => { + const currentRoute = request.currentRoute; + if (currentRoute?.isApi) { + // We don't need session for api routes. Restful api should be stateless + next(); + } else { + if (currentRoute?.isAdmin) { + adminSessionMiddleware(request, response, next); + } else { + frontStoreSessionMiddleware(request, response, next); + } + } + }; + routes.forEach((r) => { const currentRouteMiddleware = (request, response, next) => { // eslint-disable-next-line no-underscore-dangle @@ -92,20 +151,14 @@ exports.addDefaultMiddlewareFuncs = function addDefaultMiddlewareFuncs( next(); } }); - // Cookie parser - app.use(cookieParser()); - // TODO:Termporary comment this code, Because some API requires body raw data. Like Stripe - // if (r.isApi) { - // app.all(r.path, bodyParser.json({ inflate: false })); - // app.all(r.path, bodyParser.urlencoded({ extended: true })); - // } - // eslint-disable-next-line no-underscore-dangle - // eslint-disable-next-line no-param-reassign - // eslint-disable-next-line no-underscore-dangle + // Cookie parser + app.use(cookieParser(cookieSecret)); r.__BUILDREQUIRED__ = true; }); + app.use(sessionMiddleware); + app.use(async (request, response, next) => { // Get the request path, remove '/' from both ends const path = request.originalUrl.split('?')[0].replace(/^\/|\/$/g, '');
packages/evershop/package.json+3 −3 modified@@ -1,6 +1,6 @@ { "name": "@evershop/evershop", - "version": "1.0.0-rc.6", + "version": "1.0.0-rc.7", "description": "The React Ecommerce platform. Built with React and Postgres. Open-source and free. Fast and customizable.", "files": [ "bin", @@ -60,6 +60,7 @@ "chokidar": "^3.5.3", "clean-css": "^5.3.1", "config": "^3.3.6", + "connect-pg-simple": "^9.0.0", "cookie-parser": "^1.4.6", "cross-spawn": "^7.0.3", "css-loader": "^6.7.1", @@ -69,7 +70,7 @@ "dependency-tree": "^8.1.2", "enquirer": "^2.3.6", "express": "^4.18.1", - "express-session": "^1.17.1", + "express-session": "^1.17.3", "flatpickr": "^4.6.9", "graphql": "^16.6.0", "graphql-tag": "^2.12.6", @@ -80,7 +81,6 @@ "is-object": "^1.0.2", "jsesc": "^3.0.2", "json5": "^2.2.1", - "jsonwebtoken": "^9.0.0", "kleur": "3.0.3", "lodash": "^4.17.21", "luxon": "^2.0.2",
packages/evershop/README.md+1 −1 modified@@ -59,7 +59,7 @@ Explore our demo store. </p> <b>Demo user:</b> -Email: demo@gmail.com<br/> +Email: demo@evershop.io<br/> Password: 123456 <p align="left">
packages/evershop/src/components/common/grid/Pagination.jsx+69 −52 modified@@ -1,19 +1,19 @@ import PropTypes from 'prop-types'; import React from 'react'; import { Input } from '@components/common/form/fields/Input'; +import ChevronDoubleLeftIcon from '@heroicons/react/outline/ChevronDoubleLeftIcon'; +import ChevronDoubleRightIcon from '@heroicons/react/outline/ChevronDoubleRightIcon'; import './Pagination.scss'; +import { Select } from '@components/common/form/fields/Select'; export default function Pagination({ total, limit, page }) { - const pageInput = React.useRef(null); const limitInput = React.useRef(null); React.useEffect(() => { - pageInput.current.value = page; limitInput.current.value = limit; }, []); const onKeyPress = (e) => { - if (e.which !== 13) return; e.preventDefault(); let pageNumber = parseInt(e.target.value, 10); if (page < 1) pageNumber = 1; @@ -25,7 +25,7 @@ export default function Pagination({ total, limit, page }) { const onPrev = (e) => { e.preventDefault(); - const prev = page - 1; + const prev = parseInt(page) - 1; if (page === 1) return; const url = new URL(document.location); url.searchParams.set('page', prev); @@ -34,7 +34,7 @@ export default function Pagination({ total, limit, page }) { const onNext = (e) => { e.preventDefault(); - const next = page + 1; + const next = parseInt(page) + 1; if (page * limit >= total) return; const url = new URL(document.location); url.searchParams.set('page', next); @@ -85,57 +85,74 @@ export default function Pagination({ total, limit, page }) { </div> <div className="flex space-x-1"> {page > 1 && ( - <div className="prev self-center"> - <a href="#" onClick={(e) => onPrev(e)}> - <svg - xmlns="http://www.w3.org/2000/svg" - className="h-6 w-6" - fill="none" - viewBox="0 0 24 24" - stroke="currentColor" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - strokeWidth={2} - d="M15 19l-7-7 7-7" - /> - </svg> - </a> - </div> + <> + <div className="first self-center"> + <a href="#" onClick={(e) => onFirst(e)}> + <ChevronDoubleLeftIcon width={20} height={20} /> + </a> + </div> + <div className="prev self-center"> + <a href="#" onClick={(e) => onPrev(e)}> + <svg + xmlns="http://www.w3.org/2000/svg" + className="h-6 w-6" + fill="none" + viewBox="0 0 24 24" + stroke="currentColor" + > + <path + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + d="M15 19l-7-7 7-7" + /> + </svg> + </a> + </div> + </> )} - <div className="first self-center"> - <a href="#" onClick={(e) => onFirst(e)}> - 1 - </a> - </div> <div className="current" style={{ width: '5rem' }}> - <Input ref={pageInput} onKeyPress={(e) => onKeyPress(e)} /> - </div> - <div className="last self-center"> - <a href="#" onClick={(e) => onLast(e)}> - {Math.ceil(total / limit)} - </a> + <Select + placeholder={page} + onChange={(e) => { + onKeyPress(e); + }} + // List all possible page + options={Array.from( + { length: Math.ceil(total / limit) }, + (_, i) => i + 1 + ).map((item) => ({ + value: item, + text: item + }))} + /> </div> {page * limit < total && ( - <div className="next self-center"> - <a href="#" onClick={(e) => onNext(e)}> - <svg - xmlns="http://www.w3.org/2000/svg" - className="h-6 w-6" - fill="none" - viewBox="0 0 24 24" - stroke="currentColor" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - strokeWidth={2} - d="M9 5l7 7-7 7" - /> - </svg> - </a> - </div> + <> + <div className="next self-center"> + <a href="#" onClick={(e) => onNext(e)}> + <svg + xmlns="http://www.w3.org/2000/svg" + className="h-6 w-6" + fill="none" + viewBox="0 0 24 24" + stroke="currentColor" + > + <path + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + d="M9 5l7 7-7 7" + /> + </svg> + </a> + </div> + <div className="last self-center"> + <a href="#" onClick={(e) => onLast(e)}> + <ChevronDoubleRightIcon width={20} height={20} /> + </a> + </div> + </> )} <div className="self-center"> <span>{total} records</span>
packages/evershop/src/components/common/grid/Pagination.scss+10 −0 modified@@ -1,4 +1,6 @@ .next a, +.first a, +.last a, .prev a { width: 3.5rem; height: 3.5rem; @@ -7,10 +9,18 @@ border: 1px solid var(--divider); border-radius: 3px; justify-content: center; + align-items: center; svg { width: 55%; } } .pagination input { text-align: center; } +.current .field-suffix { + display: none; + padding: 0; +} +.current select{ + text-align: center; +} \ No newline at end of file
packages/evershop/src/lib/middleware/tests/app/modules/authcopy/api/global/[context]auth.js+0 −0 renamedpackages/evershop/src/lib/middleware/tests/app/modules/authcopy/api/global/[jwtTokenVerify]auth.js+0 −3 removed@@ -1,3 +0,0 @@ -module.exports = (request, response) => { - // Do nothing -};
packages/evershop/src/modules/auth/api/createAdminUserSession/[context]bodyParser[auth].js+0 −5 removed@@ -1,5 +0,0 @@ -const bodyParser = require('body-parser'); - -module.exports = (request, response, delegate, next) => { - bodyParser.json({ inflate: false })(request, response, next); -};
packages/evershop/src/modules/auth/api/createAdminUserSession/logIn.js+0 −82 removed@@ -1,82 +0,0 @@ -const { select, insert } = require('@evershop/postgres-query-builder'); -const { compareSync } = require('bcryptjs'); -const { sign } = require('jsonwebtoken'); -const { v4: uuidv4 } = require('uuid'); -const { camelCase } = require('@evershop/evershop/src/lib/util/camelCase'); -const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); -const { - getAdminTokenCookieId -} = require('../../services/getAdminTokenCookieId'); -const { - INVALID_PAYLOAD, - OK -} = require('@evershop/evershop/src/lib/util/httpStatus'); - -// eslint-disable-next-line no-unused-vars -module.exports = async (request, response, delegate, next) => { - const { body } = request; - const { email, password } = body; - const user = await select() - .from('admin_user') - .where('email', '=', email) - .load(pool); - - if (!user) { - return response.status(INVALID_PAYLOAD).json({ - error: { - message: 'Invalid email or password', - status: INVALID_PAYLOAD - } - }); - } else { - const { password: hash } = user; - const result = compareSync(password, hash); - - if (!result) { - return response.status(INVALID_PAYLOAD).json({ - error: { - message: 'Invalid email or password', - status: INVALID_PAYLOAD - } - }); - } else { - const JWT_SECRET = uuidv4(); - const sid = uuidv4(); - // Save the JWT_SECRET to the database - await insert('user_token_secret') - .given({ - user_id: user.uuid, - sid, - secret: JWT_SECRET - }) - .execute(pool); - - delete user.password; - const token = sign( - { - user: { - ...camelCase(user), - isAdmin: true - }, - sid - }, - JWT_SECRET, - { - expiresIn: '1d' - } - ); - // Send a response with the cookie - response.cookie(getAdminTokenCookieId(), token, { - httpOnly: true, - maxAge: 1000 * 60 * 60 * 24 - }); - - return response.status(OK).json({ - data: { - token, - sid - } - }); - } - } -};
packages/evershop/src/modules/auth/api/createAdminUserSession/route.json+0 −7 removed@@ -1,7 +0,0 @@ -{ - "name": "User authentication", - "description": "User authentication", - "methods": ["POST"], - "path": "/user/sessions", - "access": "public" -}
packages/evershop/src/modules/auth/api/deleteAdminUserSession/logout.js+0 −26 removed@@ -1,26 +0,0 @@ -const { del } = require('@evershop/postgres-query-builder'); -const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); -const { - OK, - INTERNAL_SERVER_ERROR -} = require('@evershop/evershop/src/lib/util/httpStatus'); - -// eslint-disable-next-line no-unused-vars -module.exports = async (request, response, delegate, next) => { - try { - const sid = request.params.id; - // Delete the session from the database - await del('user_token_secret').where('sid', '=', sid).execute(pool); - - return response.status(OK).json({ - data: {} - }); - } catch (e) { - return response.status(INTERNAL_SERVER_ERROR).json({ - error: { - message: e.message, - status: INTERNAL_SERVER_ERROR - } - }); - } -};
packages/evershop/src/modules/auth/api/deleteAdminUserSession/route.json+0 −5 removed@@ -1,5 +0,0 @@ -{ - "methods": ["DELETE"], - "path": "/user/sessions/:id", - "access": "public" -}
packages/evershop/src/modules/auth/api/global/[context]auth.js+96 −0 added@@ -0,0 +1,96 @@ +const sessionStorage = require('connect-pg-simple'); +const util = require('util'); +const { UNAUTHORIZED } = require('@evershop/evershop/src/lib/util/httpStatus'); +const { + getAdminSessionCookieName +} = require('../../services/getAdminSessionCookieName'); +const { select } = require('@evershop/postgres-query-builder'); +const session = require('express-session'); +const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); + +/** + * This is the session based authentication middleware. + * We do not implement session middleware on API routes, instead we only load the session from the database and set the user in the context. + * @param {*} request + * @param {*} response + * @param {*} delegate + * @param {*} next + * @returns + */ +module.exports = async (request, response, delegate, next) => { + // Get the current route + const { currentRoute } = request; + + // Check if the user is authenticated, if yes we assume previous authentication middleware has set the user in the context + let currentAdminUser = request.getCurrentUser(); + if (!currentAdminUser) { + try { + // Get the sesionID cookies + const cookies = request.signedCookies; + const adminSessionCookieName = getAdminSessionCookieName(); + // Check if the sessionID cookie is present + const sessionID = cookies[adminSessionCookieName]; + if (sessionID) { + const storage = new (sessionStorage(session))({ + pool: pool + }); + // Load the session using session storage + const getSession = util.promisify(storage.get).bind(storage); + const adminSessionData = await getSession(sessionID); + if (adminSessionData) { + // Set the user in the context + currentAdminUser = await select() + .from('admin_user') + .where('admin_user_id', '=', adminSessionData.userID) + .and('status', '=', 1) + .load(pool); + + if (currentAdminUser) { + // Delete the password field + delete currentAdminUser.password; + request.locals.user = currentAdminUser; + } + } + } + } catch (e) { + // Do nothing, the user is not logged in + } + } + + // If the current route is public, continue to the next middleware + // Missing access property means private + if (currentRoute?.access === 'public') { + next(); + return; + } + + if (!currentAdminUser?.uuid) { + // Response with 401 status code + response.status(UNAUTHORIZED); + response.json({ + error: { + status: UNAUTHORIZED, + message: 'Unauthorized' + } + }); + } else { + // Get user roles + let userRoles = currentAdminUser.roles || '*'; + if (userRoles === '*') { + next(); + } else { + userRoles = userRoles.split(','); + if (userRoles.includes(currentRoute.id)) { + next(); + } else { + response.status(UNAUTHORIZED); + response.json({ + error: { + status: UNAUTHORIZED, + message: 'Unauthorized' + } + }); + } + } + } +};
packages/evershop/src/modules/auth/api/global/[context]demoAccountBlocking[auth].js+2 −3 renamed@@ -1,13 +1,12 @@ const { getConfig } = require('@evershop/evershop/src/lib/util/getConfig'); const { UNAUTHORIZED } = require('@evershop/evershop/src/lib/util/httpStatus'); -const { getContextValue } = require('../../../graphql/services/contextHelper'); module.exports = (request, response, delegate, next) => { if (request.method === 'GET' || request.currentRoute?.id === 'adminGraphql') { next(); } else { - const userTokenPayload = getContextValue(request, 'userTokenPayload'); - const currentUserEmail = userTokenPayload?.user?.email; + const user = request.getCurrentUser(); + const currentUserEmail = user?.email; const demoUserEmail = getConfig('system.demoUser'); if (demoUserEmail && currentUserEmail === demoUserEmail) { response.status(UNAUTHORIZED).json({
packages/evershop/src/modules/auth/api/global/[context]userTokenVerify.js+0 −65 removed@@ -1,65 +0,0 @@ -const { select } = require('@evershop/postgres-query-builder'); -const jwt = require('jsonwebtoken'); -const { v4: uuidv4 } = require('uuid'); -const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); -const { get } = require('@evershop/evershop/src/lib/util/get'); -const { UNAUTHORIZED } = require('@evershop/evershop/src/lib/util/httpStatus'); -const { setContextValue } = require('../../../graphql/services/contextHelper'); -const { generateToken } = require('../../services/generateToken'); -const { - getAdminTokenCookieId -} = require('../../services/getAdminTokenCookieId'); -const { getTokenSecret } = require('../../services/getTokenSecret'); - -module.exports = async (request, response, delegate, next) => { - const message = 'Unauthorized'; - // Get the jwt token from the cookies - const cookieId = getAdminTokenCookieId(); - const token = request.cookies[cookieId]; - // If there is no token, generate a new one for guest user - if (!token) { - // Issue a new token for guest user - const payload = { user: null, sid: uuidv4() }; - const newToken = generateToken(payload, getTokenSecret()); - // Set the new token in the cookies - response.cookie(cookieId, newToken, { maxAge: 1.728e8, httpOnly: true }); - setContextValue(request, 'userTokenPayload', payload); - setContextValue(request, 'user', null); - // Continue to the next middleware - next(); - } else { - // Get user from token - const tokenPayload = jwt.decode(token, { complete: true, json: true }); - let secret; - // Get the secret from database - const check = await select() - .from('user_token_secret') - .where('user_id', '=', get(tokenPayload, 'payload.user.uuid', null)) - .and('sid', '=', get(tokenPayload, 'payload.sid', null)) - .load(pool); - - if (!check) { - // This is guest user - secret = getTokenSecret(); - } else { - secret = check.secret; - } - - // Verify the token - jwt.verify(token, secret, (err, decoded) => { - if (err) { - response.status(UNAUTHORIZED).json({ - error: { - message, - status: UNAUTHORIZED - } - }); - } else { - setContextValue(request, 'userTokenPayload', decoded); - // TODO: Get user roles from database - setContextValue(request, 'user', { ...decoded.user, roles: '*' }); - next(); - } - }); - } -};
packages/evershop/src/modules/auth/api/global/[userTokenVerify]auth.js+0 −45 removed@@ -1,45 +0,0 @@ -const { UNAUTHORIZED } = require('@evershop/evershop/src/lib/util/httpStatus'); -const { getContextValue } = require('../../../graphql/services/contextHelper'); - -module.exports = (request, response, delegate, next) => { - // Get the current route - const { currentRoute } = request; - // If the current route is public, continue to the next middleware - // Missing access property means private - if (currentRoute?.access === 'public') { - next(); - return; - } - - // Get the user from the context. - const user = getContextValue(request, 'user'); - if (!user?.uuid) { - // Response with 401 status code - response.status(UNAUTHORIZED); - response.json({ - error: { - status: UNAUTHORIZED, - message: 'Unauthorized' - } - }); - } else { - // Get user roles - let userRoles = user.roles; - if (userRoles === '*') { - next(); - } else { - userRoles = userRoles.split(','); - if (userRoles.includes(currentRoute.id)) { - next(); - } else { - response.status(UNAUTHORIZED); - response.json({ - error: { - status: UNAUTHORIZED, - message: 'Unauthorized' - } - }); - } - } - } -};
packages/evershop/src/modules/auth/bootstrap.js+45 −0 added@@ -0,0 +1,45 @@ +const { request } = require('express'); +const { select } = require('@evershop/postgres-query-builder'); +const { compareSync } = require('bcryptjs'); +const { pool } = require('../../lib/postgres/connection'); + +module.exports = () => { + request.loginUserWithEmail = async function loginUserWithEmail( + email, + password, + callback + ) { + const user = await select() + .from('admin_user') + .where('email', '=', email) + .and('status', '=', 1) + .load(pool); + const result = compareSync(password, user ? user.password : ''); + if (!user || !result) { + throw new Error('Invalid email or password'); + } + this.session.userID = user.admin_user_id; + // Delete the password field + delete user.password; + + // Save the user in the request + this.locals.user = user; + + this.session.save(callback); + }; + + request.logoutUser = function logoutUser(callback) { + this.session.userID = undefined; + this.locals.user = undefined; + + this.session.save(callback); + }; + + request.isUserLoggedIn = function isUserLoggedIn() { + return !!this.session.userID; + }; + + request.getCurrentUser = function getCurrentUser() { + return this.locals.user; + }; +};
packages/evershop/src/modules/auth/graphql/types/AdminUser/AdminUser.graphql+1 −0 modified@@ -21,5 +21,6 @@ type AdminUserCollection { extend type Query { adminUser(id: Int): AdminUser + currentAdminUser: AdminUser adminUsers(filters: [FilterInput]): AdminUserCollection }
packages/evershop/src/modules/auth/graphql/types/AdminUser/AdminUser.resolvers.js+8 −6 modified@@ -1,22 +1,24 @@ const { select } = require('@evershop/postgres-query-builder'); const { camelCase } = require('@evershop/evershop/src/lib/util/camelCase'); -const { get } = require('@evershop/evershop/src/lib/util/get'); module.exports = { Query: { - adminUser: async (root, { id }, { pool, userTokenPayload }) => { - if (!get(userTokenPayload, 'user.isAdmin', false)) { + adminUser: async (root, { id }, { pool, user }) => { + if (!user) { return null; } const query = select().from('admin_user'); query.where('admin_user_id', '=', id); - const user = await query.load(pool); + const adminUser = await query.load(pool); + return adminUser ? camelCase(adminUser) : null; + }, + currentAdminUser: (root, args, { pool, user }) => { return user ? camelCase(user) : null; }, - adminUsers: async (_, { filters = [] }, { pool, userTokenPayload }) => { + adminUsers: async (_, { filters = [] }, { pool, user }) => { // This field is for admin only - if (!get(userTokenPayload, 'user.isAdmin', false)) { + if (!user) { return []; } const query = select().from('admin_user');
packages/evershop/src/modules/auth/migration/Version-1.0.1.js+24 −0 added@@ -0,0 +1,24 @@ +const { execute } = require('@evershop/postgres-query-builder'); + +// eslint-disable-next-line no-multi-assign +module.exports = exports = async (connection) => { + // Remove user_token_secret table + await execute(connection, `DROP TABLE IF EXISTS user_token_secret;`); + + // Create a session table following the `connect-pg-simple` package + await execute( + connection, + `CREATE TABLE IF NOT EXISTS session ( + sid varchar NOT NULL COLLATE "default", + sess json NOT NULL, + expire timestamp(6) NOT NULL + ) + WITH (OIDS=FALSE); + ALTER TABLE session ADD CONSTRAINT "SESSION_PKEY" PRIMARY KEY ("sid") NOT DEFERRABLE INITIALLY IMMEDIATE;` + ); + + await execute( + connection, + `CREATE INDEX "IDX_SESSION_EXPIRE" ON "session" ("expire");` + ); +};
packages/evershop/src/modules/auth/pages/admin/adminLogin/index.js+2 −3 modified@@ -1,13 +1,12 @@ const { buildUrl } = require('@evershop/evershop/src/lib/router/buildUrl'); const { - getContextValue, setContextValue } = require('../../../../graphql/services/contextHelper'); module.exports = (request, response, delegate, next) => { // Check if the user is logged in - const userTokenPayload = getContextValue(request, 'userTokenPayload'); - if (userTokenPayload && userTokenPayload?.user?.uuid) { + const user = request.getCurrentUser(); + if (user) { // Redirect to admin dashboard response.redirect(buildUrl('dashboard')); } else {
packages/evershop/src/modules/auth/pages/admin/adminLoginJson/[bodyParser]logIn.js+39 −0 added@@ -0,0 +1,39 @@ +const { + INVALID_PAYLOAD, + OK, + INTERNAL_SERVER_ERROR +} = require('@evershop/evershop/src/lib/util/httpStatus'); + +// eslint-disable-next-line no-unused-vars +module.exports = async (request, response, delegate, next) => { + try { + const { body } = request; + const { email, password } = body; + await request.loginUserWithEmail(email, password, (error) => { + if (error) { + response.status(INTERNAL_SERVER_ERROR); + return response.json({ + error: { + status: INTERNAL_SERVER_ERROR, + message: message + } + }); + } else { + response.status(OK); + response.$body = { + data: { + sid: request.sessionID + } + }; + next(); + } + }); + } catch (error) { + return response.status(INVALID_PAYLOAD).json({ + error: { + message: error.message, + status: INVALID_PAYLOAD + } + }); + } +};
packages/evershop/src/modules/auth/pages/admin/adminLoginJson/payloadSchema.json+0 −0 renamedpackages/evershop/src/modules/auth/pages/admin/adminLoginJson/route.json+6 −0 added@@ -0,0 +1,6 @@ +{ + "name": "Admin User Login", + "description": "Admin User Login", + "methods": ["POST"], + "path": "/user/login" +}
packages/evershop/src/modules/auth/pages/admin/adminLogin/LoginForm.jsx+1 −1 modified@@ -79,7 +79,7 @@ export const layout = { export const query = ` query Query { - authUrl: url(routeId: "createAdminUserSession") + authUrl: url(routeId: "adminLoginJson") dashboardUrl: url(routeId: "dashboard") } `;
packages/evershop/src/modules/auth/pages/admin/adminLogoutJson/logout.js+35 −0 added@@ -0,0 +1,35 @@ +const { + OK, + INTERNAL_SERVER_ERROR +} = require('@evershop/evershop/src/lib/util/httpStatus'); + +// eslint-disable-next-line no-unused-vars +module.exports = (request, response, delegate, next) => { + try { + request.logoutUser((error) => { + if (error) { + response.status(INTERNAL_SERVER_ERROR); + return response.json({ + error: { + status: INTERNAL_SERVER_ERROR, + message: error.message + } + }); + } else { + response.status(OK); + response.$body = { + data: {} + }; + next(); + } + }); + } catch (error) { + response.status(INTERNAL_SERVER_ERROR); + response.json({ + error: { + status: INTERNAL_SERVER_ERROR, + message: error.message + } + }); + } +};
packages/evershop/src/modules/auth/pages/admin/adminLogoutJson/route.json+4 −0 added@@ -0,0 +1,4 @@ +{ + "methods": ["GET", "POST"], + "path": "/user/logout" +}
packages/evershop/src/modules/auth/pages/admin/all/AdminUser.jsx+3 −3 modified@@ -13,7 +13,7 @@ export default function AdminUser({ adminUser, logoutUrl, loginPage }) { const logout = async () => { const response = await fetch(logoutUrl, { - method: 'DELETE', + method: 'GET', headers: { 'Content-Type': 'application/json' } @@ -79,12 +79,12 @@ export const layout = { export const query = ` query Query { - adminUser(id: getContextValue("userId", null)) { + adminUser: currentAdminUser { adminUserId fullName email }, - logoutUrl: url(routeId: "deleteAdminUserSession", params: [{ key: "id", value: getContextValue('sid') }]), + logoutUrl: url(routeId: "adminLogoutJson"), loginPage: url(routeId: "adminLogin") } `;
packages/evershop/src/modules/auth/pages/admin/all/[context]auth.js+34 −0 added@@ -0,0 +1,34 @@ +const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); +const { buildUrl } = require('@evershop/evershop/src/lib/router/buildUrl'); +const { select } = require('@evershop/postgres-query-builder'); + +module.exports = async (request, response, delegate, next) => { + const userID = request.session.userID; + // Load the user from the database + const user = await select() + .from('admin_user') + .where('admin_user_id', '=', userID) + .and('status', '=', 1) + .load(pool); + + if (!user) { + // The user may not be logged in, or the account may be disabled + // Logout the user + request.logoutUser(() => { + // Check if current route is adminLogin + if ( + request.currentRoute.id === 'adminLogin' || + request.currentRoute.id === 'adminLoginJson' + ) { + next(); + } else { + return response.redirect(buildUrl('adminLogin')); + } + }); + } else { + // Delete the password field + delete user.password; + request.locals.user = user; + next(); + } +};
packages/evershop/src/modules/auth/pages/admin/all/[context]userTokenVerify.js+0 −72 removed@@ -1,72 +0,0 @@ -const jwt = require('jsonwebtoken'); -const { v4: uuidv4 } = require('uuid'); -const { select } = require('@evershop/postgres-query-builder'); -const { - setContextValue -} = require('../../../../graphql/services/contextHelper'); -const { getTokenSecret } = require('../../../services/getTokenSecret'); -const { generateToken } = require('../../../services/generateToken'); -const { get } = require('@evershop/evershop/src/lib/util/get'); -const { buildUrl } = require('@evershop/evershop/src/lib/router/buildUrl'); -const { - getAdminTokenCookieId -} = require('../../../services/getAdminTokenCookieId'); -const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); - -module.exports = async (request, response, delegate, next) => { - const cookieId = getAdminTokenCookieId(); - // Get the jwt token from the cookies - const token = request.cookies[cookieId]; - const sid = uuidv4(); - const guestPayload = { user: null, sid }; - // If there is no token, generate a new one for guest user - if (!token) { - // Issue a new token for guest user - const newToken = generateToken(guestPayload, getTokenSecret()); - // Set the new token in the cookies - response.cookie(cookieId, newToken, { maxAge: 1.728e8, httpOnly: true }); - setContextValue(request, 'userTokenPayload', guestPayload); - setContextValue(request, 'sid', sid); - // Continue to the next middleware - next(); - } else { - // Get user from token - const tokenPayload = jwt.decode(token, { complete: true, json: true }); - let secret; - // Get the secret from database - const check = await select() - .from('user_token_secret') - .where('sid', '=', get(tokenPayload, 'payload.sid', null)) - .and('user_id', '=', get(tokenPayload, 'payload.user.uuid', null)) - .load(pool); - - if (!check) { - // This is guest user - secret = getTokenSecret(); - } else { - secret = check.secret; - } - - // Verify the token - jwt.verify(token, secret, (err, decoded) => { - if (err) { - // Issue a new token for guest user - const newToken = generateToken(guestPayload, getTokenSecret()); - setContextValue(request, 'userTokenPayload', guestPayload); - setContextValue(request, 'sid', sid); - // Set the new token in the cookies - response.cookie(cookieId, newToken, { - maxAge: 1.728e8, - httpOnly: true - }); - // Redirect to home page - response.redirect(buildUrl('adminLogin')); - } else { - setContextValue(request, 'userTokenPayload', decoded); - setContextValue(request, 'user', { ...decoded.user, roles: '*' }); - setContextValue(request, 'sid', decoded.sid); - next(); - } - }); - } -};
packages/evershop/src/modules/auth/pages/admin/all/[userTokenVerify]auth.js+0 −27 removed@@ -1,27 +0,0 @@ -const { - getContextValue, - setContextValue -} = require('../../../../graphql/services/contextHelper'); -const { buildUrl } = require('@evershop/evershop/src/lib/router/buildUrl'); -const { get } = require('@evershop/evershop/src/lib/util/get'); - -module.exports = (request, response, delegate, next) => { - // Get the token Payload - const tokenPayLoad = getContextValue(request, 'userTokenPayload'); - // If there is no token or is not admin, redirect to login page - if (!tokenPayLoad || !get(tokenPayLoad, 'user.isAdmin')) { - // Check if current route is adminLogin - if (request.currentRoute.id === 'adminLogin') { - next(); - } else { - response.redirect(buildUrl('adminLogin')); - } - } else { - setContextValue( - request, - 'userId', - parseInt(tokenPayLoad.user.adminUserId, 10) - ); - next(); - } -};
packages/evershop/src/modules/auth/services/adminSessionMiddleware.js+0 −0 addedpackages/evershop/src/modules/auth/services/generateToken.js+0 −11 removed@@ -1,11 +0,0 @@ -const jwt = require('jsonwebtoken'); -const { getTokenLifeTime } = require('./getTokenLifeTime'); - -module.exports.generateToken = (payload, secret, options = {}, cb = null) => { - const { expiresIn = getTokenLifeTime() } = options; - const token = jwt.sign(payload, secret, { expiresIn }); - if (cb) { - cb(token); - } - return token; -};
packages/evershop/src/modules/auth/services/getAdminSessionCookieName.js+4 −0 added@@ -0,0 +1,4 @@ +const { getConfig } = require('@evershop/evershop/src/lib/util/getConfig'); + +module.exports.getAdminSessionCookieName = () => + getConfig('system.session.adminCookieName', 'asid');
packages/evershop/src/modules/auth/services/getAdminTokenCookieId.js+0 −4 removed@@ -1,4 +0,0 @@ -const { getConfig } = require('@evershop/evershop/src/lib/util/getConfig'); - -module.exports.getAdminTokenCookieId = () => - getConfig('jwt.adminCookieId', 'admin_token');
packages/evershop/src/modules/auth/services/getCookieSecret.js+5 −0 added@@ -0,0 +1,5 @@ +const { getConfig } = require('@evershop/evershop/src/lib/util/getConfig'); + +module.exports.getCookieSecret = () => { + return getConfig('system.session.cookieSecret', 'keyboard cat'); +};
packages/evershop/src/modules/auth/services/getFrontStoreSessionCookieName.js+4 −0 added@@ -0,0 +1,4 @@ +const { getConfig } = require('@evershop/evershop/src/lib/util/getConfig'); + +module.exports.getFrontStoreSessionCookieName = () => + getConfig('system.session.cookieName', 'sid');
packages/evershop/src/modules/auth/services/getSessionConfig.js+20 −0 added@@ -0,0 +1,20 @@ +const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); +const { getConfig } = require('@evershop/evershop/src/lib/util/getConfig'); +const sessionStorage = require('connect-pg-simple'); +const session = require('express-session'); + +module.exports.getSessionConfig = (cookieSecret) => { + const sess = { + store: new (sessionStorage(session))({ + pool: pool + }), + secret: cookieSecret, + cookie: { + maxAge: 24 * 60 * 60 * 1000 + }, + resave: getConfig('system.session.resave', false), + saveUninitialized: getConfig('system.session.saveUninitialized', false) + }; + + return sess; +};
packages/evershop/src/modules/auth/services/getSessionId.js+58 −0 added@@ -0,0 +1,58 @@ +module.exports.getSessionId = function getSessionId(request, name, secrets) { + var header = request.headers.cookie; + var raw; + var val; + + // read from cookie header + if (header) { + var cookies = cookie.parse(header); + + raw = cookies[name]; + + if (raw) { + if (raw.substr(0, 2) === 's:') { + val = unsigncookie(raw.slice(2), secrets); + + if (val === false) { + debug('cookie signature invalid'); + val = undefined; + } + } else { + debug('cookie unsigned'); + } + } + } + + // back-compat read from cookieParser() signedCookies data + if (!val && req.signedCookies) { + val = req.signedCookies[name]; + + if (val) { + deprecate('cookie should be available in req.headers.cookie'); + } + } + + // back-compat read from cookieParser() cookies data + if (!val && req.cookies) { + raw = req.cookies[name]; + + if (raw) { + if (raw.substr(0, 2) === 's:') { + val = unsigncookie(raw.slice(2), secrets); + + if (val) { + deprecate('cookie should be available in req.headers.cookie'); + } + + if (val === false) { + debug('cookie signature invalid'); + val = undefined; + } + } else { + debug('cookie unsigned'); + } + } + } + + return val; +};
packages/evershop/src/modules/auth/services/getTokenCookieId.js+0 −3 removed@@ -1,3 +0,0 @@ -const { getConfig } = require('@evershop/evershop/src/lib/util/getConfig'); - -module.exports.getTokenCookieId = () => getConfig('jwt.cookieId', 'token');
packages/evershop/src/modules/auth/services/getTokenLifeTime.js+0 −4 removed@@ -1,4 +0,0 @@ -const { getConfig } = require('@evershop/evershop/src/lib/util/getConfig'); - -module.exports.getTokenLifeTime = () => - getConfig('jwt.web_token_life_time', '2d');
packages/evershop/src/modules/auth/services/getTokenSecret.js+0 −4 removed@@ -1,4 +0,0 @@ -const { getConfig } = require('@evershop/evershop/src/lib/util/getConfig'); - -module.exports.getTokenSecret = () => - getConfig('jwt.web_token_secret', 'secret');
packages/evershop/src/modules/base/api/global/context.js+1 −0 modified@@ -39,4 +39,5 @@ module.exports = (request, response) => { setContextValue(request, 'stale', request.stale); setContextValue(request, 'subdomains', request.subdomains); setContextValue(request, 'xhr', request.xhr); + setContextValue(request, 'sid', request.sessionID); };
packages/evershop/src/modules/base/pages/global/context.js+1 −0 modified@@ -40,4 +40,5 @@ module.exports = (request, response) => { setContextValue(request, 'stale', request.stale); setContextValue(request, 'subdomains', request.subdomains); setContextValue(request, 'xhr', request.xhr); + setContextValue(request, 'sid', request.sessionID); };
packages/evershop/src/modules/catalog/graphql/types/Product/Variant/Variant.resolvers.js+3 −3 modified@@ -5,7 +5,7 @@ const { camelCase } = require('@evershop/evershop/src/lib/util/camelCase'); module.exports = { Product: { - variantGroup: async (product, _, { pool, userTokenPayload }) => { + variantGroup: async (product, _, { pool, user }) => { const { variantGroupId } = product; if (!variantGroupId) { return null; @@ -52,14 +52,14 @@ module.exports = { 'IN', Object.values(group).filter((v) => Number.isInteger(v)) ); - if (!userTokenPayload?.user?.uuid) { + if (!user) { query.andWhere('status', '=', 1); } const vs = await query.execute(pool); // Filter the vs array, make sure that each product has all the attributes // that are in the variant group. let filteredVs; - if (!userTokenPayload?.user?.uuid) { + if (!user) { filteredVs = vs.filter((v) => { const attributes = Object.values(group).filter((attr) => Number.isInteger(attr)
packages/evershop/src/modules/catalog/migration/Version-1.0.4.js+10 −0 modified@@ -23,6 +23,16 @@ module.exports = exports = async (connection) => { EXECUTE PROCEDURE add_product_inventory_updated_event();` ); + // Check if a default collection called "Featured Products" already exists + const featuredProductsExists = await execute( + connection, + `SELECT EXISTS (SELECT 1 FROM collection WHERE code = 'homepage');` + ); + + if (featuredProductsExists.rows[0].exists) { + return; + } + // Create a default collection called "Featured Products" const featuredProducts = await insert('collection') .given({
packages/evershop/src/modules/checkout/api/addMineCartItem/[context]customerTokenVerify.js+0 −5 removed@@ -1,5 +0,0 @@ -const customerTokenVerify = require('../../../customer/pages/frontStore/all/[context]customerTokenVerify'); - -module.exports = async (request, response, delegate, next) => { - customerTokenVerify(request, response, delegate, next); -};
packages/evershop/src/modules/checkout/api/addMineCartItem/[customerAuth]detectCurrentCart.js+32 −0 added@@ -0,0 +1,32 @@ +const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); +const { select } = require('@evershop/postgres-query-builder'); +const { setContextValue } = require('../../../graphql/services/contextHelper'); +const { UNAUTHORIZED } = require('@evershop/evershop/src/lib/util/httpStatus'); + +module.exports = async (request, response, delegate, next) => { + /** + * We firstly get the sessionID from the request. + * This API needs the client to send the session cookie in the request. + * Base on the sessionID, we can get the cart + */ + const { sessionID } = request.locals; + if (!sessionID) { + response.status(UNAUTHORIZED); + return response.json({ + error: { + status: UNAUTHORIZED, + message: 'Unauthorized' + } + }); + } else { + const cart = await select() + .from('cart') + .where('sid', '=', sessionID) + .and('status', '=', 1) + .load(pool); + if (cart) { + setContextValue(request, 'cartId', cart.uuid); + } + next(); + } +};
packages/evershop/src/modules/checkout/api/addMineCartItem/[customerTokenVerify]detectCurrentCart[auth].js+0 −3 removed@@ -1,3 +0,0 @@ -const detectCurrentCartMiddleware = require('../../pages/frontStore/all/[customerTokenVerify]detectCurrentCart[auth]'); - -module.exports = detectCurrentCartMiddleware;
packages/evershop/src/modules/checkout/api/addMineCartItem/[detectCurrentCart]addItemToCart.js+6 −7 modified@@ -12,19 +12,18 @@ const { } = require('@evershop/evershop/src/lib/util/httpStatus'); const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); const { createNewCart } = require('../../services/createNewCart'); +const { + translate +} = require('@evershop/evershop/src/lib/locale/translate/translate'); module.exports = async (request, response, delegate, next) => { try { let cartId = getContextValue(request, 'cartId'); let cart; if (!cartId) { // Create a new cart - const customerTokenPayload = getContextValue( - request, - 'customerTokenPayload', - {} - ); - cart = await createNewCart(customerTokenPayload); + const { sessionID, customer } = request.locals; + cart = await createNewCart(sessionID, customer || {}); cartId = cart.getData('uuid'); } else { cart = await getCartByUUID(cartId); // Cart object @@ -43,7 +42,7 @@ module.exports = async (request, response, delegate, next) => { response.json({ error: { status: INVALID_PAYLOAD, - message: 'Product not found' + message: translate('Product not found') } }); return;
packages/evershop/src/modules/checkout/api/removeMineCartItem/[context]customerTokenVerify.js+0 −5 removed@@ -1,5 +0,0 @@ -const customerTokenVerify = require('../../../customer/pages/frontStore/all/[context]customerTokenVerify'); - -module.exports = async (request, response, delegate, next) => { - customerTokenVerify(request, response, delegate, next); -};
packages/evershop/src/modules/checkout/api/removeMineCartItem/[customerAuth]detectCurrentCart.js+3 −0 added@@ -0,0 +1,3 @@ +const detectCurrentCartMiddleware = require('../../api/addMineCartItem/[customerAuth]detectCurrentCart'); + +module.exports = detectCurrentCartMiddleware;
packages/evershop/src/modules/checkout/api/removeMineCartItem/[customerTokenVerify]detectCurrentCart[auth].js+0 −3 removed@@ -1,3 +0,0 @@ -const detectCurrentCartMiddleware = require('../../pages/frontStore/all/[customerTokenVerify]detectCurrentCart[auth]'); - -module.exports = detectCurrentCartMiddleware;
packages/evershop/src/modules/checkout/api/removeMineCartItem/[detectCurrentCart]removeItem.js+17 −4 modified@@ -3,24 +3,37 @@ const { INTERNAL_SERVER_ERROR } = require('@evershop/evershop/src/lib/util/httpStatus'); const { getContextValue } = require('../../../graphql/services/contextHelper'); -const removeItem = require('../removeCartItem/removeItem'); +const { + translate +} = require('@evershop/evershop/src/lib/locale/translate/translate'); +const { saveCart } = require('../../services/saveCart'); +const { getCartByUUID } = require('../../services/getCartByUUID'); module.exports = async (request, response, delegate, next) => { try { const cartId = getContextValue(request, 'cartId'); + const { item_id } = request.params; if (!cartId) { response.status(INVALID_PAYLOAD); response.json({ error: { - message: 'Invalid cart', + message: translate('Invalid cart'), status: INVALID_PAYLOAD } }); return; } - request.params.cart_id = cartId; - await removeItem(request, response, delegate, next); + const cart = await getCartByUUID(cartId); + const item = await cart.removeItem(item_id); + await saveCart(cart); + response.status(OK); + response.$body = { + data: { + item: item.export() + } + }; + next(); } catch (error) { response.status(INTERNAL_SERVER_ERROR); response.json({
packages/evershop/src/modules/checkout/api/testShippingCostApi/loadData.js+0 −11 removed@@ -1,11 +0,0 @@ -const { select } = require('@evershop/postgres-query-builder'); -const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); - -// eslint-disable-next-line no-unused-vars -module.exports = async (request, response, stack, next) => { - response.json({ - data: { - cost: 900 - } - }); -};
packages/evershop/src/modules/checkout/api/testShippingCostApi/route.json+0 −5 removed@@ -1,5 +0,0 @@ -{ - "methods": ["GET"], - "path": "/tablerate/calculate/:cart_id/:method_id", - "access": "public" -}
packages/evershop/src/modules/checkout/bootstrap.js+0 −30 removed@@ -1,30 +0,0 @@ -const { request } = require('express'); -const { getContextValue } = require('../graphql/services/contextHelper'); -const { getCartByUUID } = require('./services/getCartByUUID'); - -module.exports = () => { - /** - * This method get the current cart object - * It requires a jwt token must be available, else it will return null - * If this function is called with a cartId, - * it will return the cart by that id. Else it will return the cart by the token payload - * @returns {Promise<Cart>} - * @returns {Promise<null>} - */ - request.getCart = async function getCart(uuid) { - this.locals = this.locals || {}; - if (this.locals?.cart) { - return this.locals.cart; - } - if (uuid) { - this.locals.cart = await getCartByUUID(uuid); - } else { - const id = getContextValue(this, 'cartId'); - if (id) { - this.locals.cart = await getCartByUUID(id); - } - } - - return this.locals.cart || null; - }; -};
packages/evershop/src/modules/checkout/pages/frontStore/all/[auth]addCustomerToCart.js+13 −18 renamed@@ -1,38 +1,33 @@ const { select, update } = require('@evershop/postgres-query-builder'); const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); -const { getContextValue } = require('../../../graphql/services/contextHelper'); +const { debug } = require('@evershop/evershop/src/lib/log/debuger'); module.exports = async (request, response, delegate, next) => { try { - const customerTokenPayload = getContextValue( - request, - 'customerTokenPayload' - ); - const { - sid, - customer: { fullName, email, customerId, groupId } - } = customerTokenPayload; + const sessionID = request.sessionID; + const customer = request.getCurrentCustomer(); + if (!customer) { + return next(); + } // Check if there is any cart with the same sid const cart = await select() .from('cart') - .where('sid', '=', sid) + .where('sid', '=', sessionID) .and('status', '=', 1) .load(pool); if (cart) { await update('cart') .given({ - customer_group_id: groupId, - customer_id: customerId, - customer_full_name: fullName, - customer_email: email + customer_group_id: customer.group_id, + customer_id: customer.customer_id, + customer_full_name: customer.full_name, + customer_email: customer.email }) .where('cart_id', '=', cart.cart_id) .execute(pool); } - next(); } catch (error) { - // TODO: Log error - // Let the request continue - next(); + debug('critical', error); } + next(); };
packages/evershop/src/modules/checkout/pages/frontStore/all/[context]detectCurrentCart[auth].js+8 −12 renamed@@ -1,44 +1,40 @@ const { select, update } = require('@evershop/postgres-query-builder'); const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); const { - getContextValue, setContextValue } = require('../../../../graphql/services/contextHelper'); module.exports = async (request, response, delegate, next) => { - const customerTokenPayload = getContextValue(request, 'customerTokenPayload'); - const { sid } = customerTokenPayload; - // Check if any cart is associated with the session id const cart = await select() .from('cart') - .where('sid', '=', sid) + .where('sid', '=', request.sessionID) .andWhere('status', '=', 1) .load(pool); if (cart) { setContextValue(request, 'cartId', cart.uuid); } else { - // Get the customer id from the token payload - const customerId = customerTokenPayload.customer?.customerId || null; - if (customerId) { + // Get the customer id from the session + const customerID = request.session.customerID || null; + if (customerID) { // Check if any cart is associated with the customer id const customerCart = await select() .from('cart') - .where('customer_id', '=', customerId) + .where('customer_id', '=', customerID) .andWhere('status', '=', 1) .load(pool); if (customerCart) { // Update the cart with the session id await update('cart') - .given({ sid }) + .given({ sid: request.sessionID }) .where('uuid', '=', customerCart.uuid) .execute(pool); - + request.session.cartID = customerCart.uuid; setContextValue(request, 'cartId', customerCart.uuid); } else { - setContextValue(request, 'cartId', null); + setContextValue(request, 'cartId', undefined); } } }
packages/evershop/src/modules/checkout/pages/frontStore/cart/ShoppingCart.jsx+1 −1 modified@@ -86,7 +86,7 @@ export const layout = { export const query = ` query Query { - cart(id: getContextValue('cartId', null)) { + cart { totalQty uuid items {
packages/evershop/src/modules/checkout/pages/frontStore/checkout/index.js+2 −4 modified@@ -3,15 +3,13 @@ const { setContextValue, getContextValue } = require('../../../../graphql/services/contextHelper'); -const { getCustomerCart } = require('../../../services/getCustomerCart'); +const { getCurrentCart } = require('../../../services/getCurrentCart'); const { translate } = require('@evershop/evershop/src/lib/locale/translate/translate'); module.exports = async (request, response, delegate, next) => { - const cart = await getCustomerCart( - getContextValue(request, 'customerTokenPayload', {}) - ); + const cart = await getCurrentCart(request.sessionID); if (!cart) { response.redirect(302, buildUrl('cart')); return;
packages/evershop/src/modules/checkout/services/cart/CartFactory.js+0 −46 removed@@ -1,46 +0,0 @@ -// This factory creates one Cart object per request -const { getContextValue } = require('../../../graphql/services/contextHelper'); -const { getCustomerCart } = require('../getCustomerCart'); - -const CartFactory = {}; - -CartFactory.init = function init(request, response) { - // Get the token payload from the request - const customerTokenPayload = getContextValue(request, 'customerTokenPayload'); - this.carts = { - [customerTokenPayload.sid]: { - request, - response - } - }; - - // Subscribe to the 'finish' event of the response - response.on('finish', () => { - // When the response is finished, delete the cart from the factory - delete this.carts[customerTokenPayload.sid]; - }); -}; - -/** - * @param {string} uuid - * @returns {Promise<Cart>} - * @throws {Error} - */ -CartFactory.getCart = async function getCart(sid) { - if (!sid) { - throw new Error('Session ID is required'); - } else { - if (this.carts[sid]?.cart === undefined) { - this.carts[sid].cart = getCustomerCart( - getContextValue(this.carts[sid].request, 'customerTokenPayload') - ); - } - - const cart = await this.carts[sid].cart; - return cart; - } -}; - -// eslint-disable-next-line no-multi-assign -exports = module.exports = {}; -exports.CartFactory = CartFactory;
packages/evershop/src/modules/checkout/services/createNewCart.js+4 −5 modified@@ -4,14 +4,13 @@ const { Cart } = require('./cart/Cart'); module.exports = exports; /** - * This function return a Cart object by customerTokenPayload. Do not use this function directly. + * This function return a Cart object by the session ID and the customer object * Use CartFactory.getCart() instead. - * @param {*} tokenPayLoad : The payload of the jwt token + * @param {string} sid : The session ID + * @param {Object} customer : The customer object * @returns {Promise<Cart>} */ -exports.createNewCart = async (customerTokenPayload = {}) => { - const customer = customerTokenPayload?.customer || {}; - const sid = customerTokenPayload?.sid || null; +exports.createNewCart = async (sid, customer = {}) => { // Extract the customer info const { customerId: customer_id,
packages/evershop/src/modules/checkout/services/getCurrentCart.js+28 −0 added@@ -0,0 +1,28 @@ +/* eslint-disable camelcase */ +const { select } = require('@evershop/postgres-query-builder'); +const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); +const { Cart } = require('./cart/Cart'); + +module.exports = exports; + +/** + * This function return a Cart object by the session ID. + * @param {string} sid : The session ID + * @returns {Promise<Cart>} + */ +exports.getCurrentCart = async (sid) => { + // Try to get the cart by the session id first + const cartBySid = await select() + .from('cart') + .where('status', '=', 1) + .andWhere('sid', '=', sid) + .load(pool); + + if (cartBySid) { + const cart = new Cart(cartBySid); + await cart.build(); + return cart; + } else { + return null; + } +};
packages/evershop/src/modules/checkout/services/getCustomerCart.js+0 −74 removed@@ -1,74 +0,0 @@ -/* eslint-disable camelcase */ -const { select } = require('@evershop/postgres-query-builder'); -const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); -const { Cart } = require('./cart/Cart'); - -module.exports = exports; - -/** - * This function return a Cart object by customerTokenPayload. Do not use this function directly. - * Use CartFactory.getCart() instead. - * @param {*} customerTokenPayload : The payload of the jwt token - * @returns {Promise<Cart>} - */ -exports.getCustomerCart = async (customerTokenPayload) => { - const customer = customerTokenPayload?.customer || {}; - const sid = customerTokenPayload?.sid || null; - const customerId = customer?.customerId || null; - let cart; - // Extract the customer info - const { - customerId: customer_id, - email: customer_email, - groupId: customer_group_id, - fullName: customer_full_name - } = customer; - - // Try to get the cart by the session id first - const cartBySid = await select() - .from('cart') - .where('status', '=', 1) - .andWhere('sid', '=', sid) - .load(pool); - - if (cartBySid) { - cart = new Cart({ - sid, - customer_id, - customer_email, - customer_group_id, - customer_full_name, - ...cartBySid - }); - } else { - // Try to get the cart by the customer id - const cartByCustomerId = await select() - .from('cart') - .where('status', '=', 1) - .andWhere('customer_id', '=', customerId) - .load(pool); - - if (cartByCustomerId) { - cart = new Cart({ - sid, - customer_id, - customer_email, - customer_group_id, - customer_full_name, - ...cartByCustomerId - }); - } else { - // Create a new cart - cart = new Cart({ - sid, - customer_id, - customer_email, - customer_group_id, - customer_full_name - }); - } - } - - await cart.build(); - return cart; -};
packages/evershop/src/modules/customer/api/createCustomerSession/[bodyParser,customerTokenVerify]login.js+0 −85 removed@@ -1,85 +0,0 @@ -const { select, insertOnUpdate } = require('@evershop/postgres-query-builder'); -const { compareSync } = require('bcryptjs'); -const { sign } = require('jsonwebtoken'); -const { v4: uuidv4 } = require('uuid'); -const { camelCase } = require('@evershop/evershop/src/lib/util/camelCase'); -const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); -const { getTokenCookieId } = require('../../../auth/services/getTokenCookieId'); -const { - getContextValue, - setContextValue -} = require('../../../graphql/services/contextHelper'); -const { - INVALID_PAYLOAD, - OK -} = require('@evershop/evershop/src/lib/util/httpStatus'); - -// eslint-disable-next-line no-unused-vars -module.exports = async (request, response, delegate, next) => { - const { body } = request; - const { email, password } = body; - const customer = await select() - .from('customer') - .where('email', '=', email) - .and('status', '=', 1) - .load(pool); - - if (!customer) { - response.status(INVALID_PAYLOAD); - response.json({ - error: { - status: INVALID_PAYLOAD, - message: 'Invalid email or password' - } - }); - } else { - const { password: hash } = customer; - const result = compareSync(password, hash); - - if (!result) { - response.status(INVALID_PAYLOAD); - response.json({ - error: { - status: INVALID_PAYLOAD, - message: 'Invalid email or password' - } - }); - } else { - // Get the tokenPayload - const currentTokenPayload = getContextValue( - request, - 'customerTokenPayload' - ); - const JWT_SECRET = uuidv4(); - // Save the JWT_SECRET to the database - await insertOnUpdate('user_token_secret', ['sid']) - .given({ - user_id: customer.uuid, - sid: currentTokenPayload.sid, - secret: JWT_SECRET - }) - .execute(pool); - - delete customer.password; - const newPayload = { - ...currentTokenPayload, - customer: { ...camelCase(customer) } - }; - const token = sign(newPayload, JWT_SECRET); - setContextValue(request, 'customerTokenPayload', newPayload); - // Send a response with the cookie - response.cookie(getTokenCookieId(), token, { - httpOnly: true, - maxAge: 1.728e8 - }); - response.status(OK); - response.$body = { - data: { - token, - sid: currentTokenPayload.sid - } - }; - next(); - } - } -};
packages/evershop/src/modules/customer/api/createCustomerSession/[context]bodyParser[auth].js+0 −5 removed@@ -1,5 +0,0 @@ -const bodyParser = require('body-parser'); - -module.exports = (request, response, delegate, next) => { - bodyParser.json({ inflate: false })(request, response, next); -};
packages/evershop/src/modules/customer/api/createCustomerSession/[context]customerTokenVerify.js+0 −5 removed@@ -1,5 +0,0 @@ -const customerTokenVerify = require('../../pages/frontStore/all/[context]customerTokenVerify'); - -module.exports = async (request, response, delegate, next) => { - customerTokenVerify(request, response, delegate, next); -};
packages/evershop/src/modules/customer/api/createCustomerSession/route.json+0 −5 removed@@ -1,5 +0,0 @@ -{ - "methods": ["POST"], - "path": "/customers/sessions", - "access": "public" -}
packages/evershop/src/modules/customer/api/deleteCustomerSession/logout.js+0 −28 removed@@ -1,28 +0,0 @@ -const { del } = require('@evershop/postgres-query-builder'); -const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); -const { - OK, - INTERNAL_SERVER_ERROR -} = require('@evershop/evershop/src/lib/util/httpStatus'); - -// eslint-disable-next-line no-unused-vars -module.exports = async (request, response, delegate, next) => { - try { - const { id } = request.params; - // Delete the secret from the database - await del('user_token_secret').where('user_id', '=', id).execute(pool); - response.status(OK); - response.$body = { - data: {} - }; - next(); - } catch (e) { - response.status(INTERNAL_SERVER_ERROR); - return response.json({ - error: { - status: INTERNAL_SERVER_ERROR, - message: e.message - } - }); - } -};
packages/evershop/src/modules/customer/api/deleteCustomerSession/route.json+0 −5 removed@@ -1,5 +0,0 @@ -{ - "methods": ["DELETE"], - "path": "/customers/sessions/:id", - "access": "public" -}
packages/evershop/src/modules/customer/api/global/[context]customerAuth[auth].js+61 −0 added@@ -0,0 +1,61 @@ +const sessionStorage = require('connect-pg-simple'); +const util = require('util'); +const { select } = require('@evershop/postgres-query-builder'); +const session = require('express-session'); +const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); +const { + getFrontStoreSessionCookieName +} = require('../../../auth/services/getFrontStoreSessionCookieName'); + +/** + * This is the session based authentication middleware. + * We do not implement session middleware on API routes, + * instead we only load the session from the database and set the customer in the context. + * @param {*} request + * @param {*} response + * @param {*} delegate + * @param {*} next + * @returns + */ +module.exports = async (request, response, delegate, next) => { + // Check if the customer is authenticated + // if yes we assume previous authentication middleware has set the customer in the context + let currentCustomer = request.getCurrentCustomer(); + if (!currentCustomer) { + try { + // Get the sesionID cookies + const cookies = request.signedCookies; + const storeFrontSessionCookieName = getFrontStoreSessionCookieName(); + // Check if the sessionID cookie is present + const sessionID = cookies[storeFrontSessionCookieName]; + if (sessionID) { + const storage = new (sessionStorage(session))({ + pool: pool + }); + // Load the session using session storage + const getSession = util.promisify(storage.get).bind(storage); + const customerSessionData = await getSession(sessionID); + if (customerSessionData) { + // Set the customer in the context + currentCustomer = await select() + .from('customer') + .where('customer_id', '=', customerSessionData.customerID) + .and('status', '=', 1) + .load(pool); + + if (currentCustomer) { + // Delete the password field + delete currentCustomer.password; + request.locals.customer = currentCustomer; + } + } + // We also keep the session id in the request. + // This is for anonymous customer authentication. + request.locals.sessionID = sessionID; + } + } catch (e) { + // Do nothing, the customer is not logged in + } + } + next(); +};
packages/evershop/src/modules/customer/bootstrap.js+35 −38 modified@@ -1,17 +1,8 @@ const { request } = require('express'); const { Cart } = require('../checkout/services/cart/Cart'); -const { v4: uuidv4 } = require('uuid'); -const { - getTokenCookieId -} = require('@evershop/evershop/src/modules/auth/services/getTokenCookieId'); -const { - setContextValue, - getContextValue -} = require('@evershop/evershop/src/modules/graphql/services/contextHelper'); -const { sign } = require('jsonwebtoken'); const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); -const { select, insertOnUpdate } = require('@evershop/postgres-query-builder'); -const { camelCase } = require('@evershop/evershop/src/lib/util/camelCase'); +const { select } = require('@evershop/postgres-query-builder'); +const { compareSync } = require('bcryptjs'); module.exports = () => { Cart.addField('customer_id', function resolver() { @@ -30,41 +21,47 @@ module.exports = () => { return this.dataSource?.customer_full_name ?? null; }); - request.login = async function login(customerId) { + /** + * This function will login the customer with email and password + * @param {*} email + * @param {*} password + * @param {*} callback + */ + request.loginCustomerWithEmail = async function loginCustomerWithEmail( + email, + password, + callback + ) { const customer = await select() .from('customer') - .where('uuid', '=', customerId) + .where('email', '=', email) + .and('status', '=', 1) .load(pool); - const JWT_SECRET = uuidv4(); - const sid = uuidv4(); - // Save the JWT_SECRET to the database - await insertOnUpdate('user_token_secret', ['sid']) - .given({ - user_id: customer.uuid, - sid: sid, - secret: JWT_SECRET - }) - .execute(pool); + const result = compareSync(password, customer ? customer.password : ''); + if (!customer || !result) { + throw new Error('Invalid email or password'); + } + this.session.customerID = customer.customer_id; + // Delete the password field delete customer.password; - const payload = { - sid: sid, - customer: { ...camelCase(customer) } - }; - const token = sign(payload, JWT_SECRET); - setContextValue(this, 'customerTokenPayload', payload); - setContextValue(this, 'sid', sid); - return token; - // Send a response with the cookie - response.cookie(getTokenCookieId(), token, { - httpOnly: true, - maxAge: 1.728e8 - }); + // Save the customer in the request + this.locals.customer = customer; + this.session.save(callback); + }; + + request.logoutCustomer = function logoutCustomer(callback) { + this.session.customerID = undefined; + this.locals.customer = undefined; + + this.session.save(callback); }; request.isCustomerLoggedIn = function isCustomerLoggedIn() { - const customerTokenPayload = getContextValue(this, 'customerTokenPayload'); + return !!this.session?.customerID; + }; - return customerTokenPayload?.customer?.customerId ? true : false; + request.getCurrentCustomer = function getCurrentCustomer() { + return this.locals?.customer; }; };
packages/evershop/src/modules/customer/graphql/types/Customer/Customer.graphql+1 −0 modified@@ -29,5 +29,6 @@ type CustomerCollection { extend type Query { customer(id: String): Customer + currentCustomer: Customer customers(filters: [FilterInput]): CustomerCollection }
packages/evershop/src/modules/customer/graphql/types/Customer/Customer.resolvers.js+3 −0 modified@@ -15,6 +15,9 @@ module.exports = { const customer = await query.load(pool); return customer ? camelCase(customer) : null; }, + currentCustomer: async (root, args, { customer }) => { + return customer ? camelCase(customer) : null; + }, customers: async (_, { filters = [] }, { user }) => { // This field is for admin only if (!user) {
packages/evershop/src/modules/customer/graphql/types/CustomerGroup/CustomerGroup.resolvers.js+4 −4 modified@@ -4,8 +4,8 @@ const { camelCase } = require('@evershop/evershop/src/lib/util/camelCase'); module.exports = { Query: { - customerGroup: async (root, { id }, { pool, userTokenPayload }) => { - if (!userTokenPayload?.user?.uuid) { + customerGroup: async (root, { id }, { pool, user }) => { + if (!user) { return null; } const group = await select() @@ -15,8 +15,8 @@ module.exports = { return group ? camelCase(group) : null; }, - customerGroups: async (root, _, { pool, userTokenPayload }) => { - if (!userTokenPayload?.user?.uuid) { + customerGroups: async (root, _, { pool, user }) => { + if (!user) { return []; } const groups = await select().from('customer_group').execute(pool);
packages/evershop/src/modules/customer/pages/frontStore/account/AccountDetails.jsx+1 −1 modified@@ -40,7 +40,7 @@ export const layout = { export const query = ` query Query { - account: customer(id: getContextValue("customerId", null)) { + account: currentCustomer { uuid fullName email
packages/evershop/src/modules/customer/pages/frontStore/account/index.js+3 −5 modified@@ -1,18 +1,16 @@ const { buildUrl } = require('@evershop/evershop/src/lib/router/buildUrl'); const { - getContextValue, setContextValue } = require('../../../../graphql/services/contextHelper'); const { translate } = require('@evershop/evershop/src/lib/locale/translate/translate'); module.exports = (request, response, delegate, next) => { - // Check if the user is logged in - const customerTokenPayload = getContextValue(request, 'customerTokenPayload'); - if (!customerTokenPayload || !customerTokenPayload.customer?.customerId) { + // Check if the customer is logged in + if (!request.isCustomerLoggedIn()) { // Redirect to admin dashboard - response.redirect(buildUrl('homepage')); + response.redirect(buildUrl('login')); } else { setContextValue(request, 'pageInfo', { title: translate('Account details'),
packages/evershop/src/modules/customer/pages/frontStore/account/Layout.jsx+5 −9 modified@@ -4,11 +4,11 @@ import { toast } from 'react-toastify'; import Area from '@components/common/Area'; import { _ } from '@evershop/evershop/src/lib/locale/translate'; -export default function Layout({ account: { logoutApi } }) { +export default function Layout({ logoutUrl }) { const logout = async (e) => { e.preventDefault(); - const respone = await fetch(logoutApi, { - method: 'DELETE' + const respone = await fetch(logoutUrl, { + method: 'GET' }); const data = await respone.json(); if (data.error) { @@ -42,9 +42,7 @@ export default function Layout({ account: { logoutApi } }) { } Layout.propTypes = { - account: PropTypes.shape({ - logoutApi: PropTypes.string.isRequired - }).isRequired + logoutUrl: PropTypes.string.isRequired }; export const layout = { @@ -54,8 +52,6 @@ export const layout = { export const query = ` query Query { - account: customer(id: getContextValue("customerId", null)) { - logoutApi - } + logoutUrl: url(routeId: "customerLogoutJson") } `;
packages/evershop/src/modules/customer/pages/frontStore/account/OrderHistory.jsx+1 −1 modified@@ -67,7 +67,7 @@ export const layout = { export const query = ` query Query { - customer (id: getContextValue("customerId", null)) { + customer: currentCustomer { orders { orderId orderNumber
packages/evershop/src/modules/customer/pages/frontStore/all/[context]auth.js+35 −0 added@@ -0,0 +1,35 @@ +const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); +const { buildUrl } = require('@evershop/evershop/src/lib/router/buildUrl'); +const { select } = require('@evershop/postgres-query-builder'); + +module.exports = async (request, response, delegate, next) => { + const customerID = request.session.customerID; + if (!customerID) { + delete request.locals.customer; + next(); + return; + } + // Load the customer from the database + const customer = await select() + .from('customer') + .where('customer_id', '=', customerID) + .and('status', '=', 1) + .load(pool); + + if (!customer) { + // The customer may not be logged in, or the account may be disabled + // Logout the customer + request.logoutCustomer((error) => { + if (error) { + next(error); + } else { + return response.redirect(buildUrl('homepage')); + } + }); + } else { + // Delete the password field + delete customer.password; + request.locals.customer = customer; + next(); + } +};
packages/evershop/src/modules/customer/pages/frontStore/all/[context]customerTokenVerify.js+0 −71 removed@@ -1,71 +0,0 @@ -const jwt = require('jsonwebtoken'); -const { v4: uuidv4 } = require('uuid'); -const { select } = require('@evershop/postgres-query-builder'); -const { - setContextValue -} = require('../../../../graphql/services/contextHelper'); -const { get } = require('@evershop/evershop/src/lib/util/get'); -const { buildUrl } = require('@evershop/evershop/src/lib/router/buildUrl'); -const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); -const { getTokenSecret } = require('../../../../auth/services/getTokenSecret'); -const { generateToken } = require('../../../../auth/services/generateToken'); -const { - getTokenCookieId -} = require('../../../../auth/services/getTokenCookieId'); - -module.exports = async (request, response, delegate, next) => { - const cookieId = getTokenCookieId(); - // Get the jwt token from the cookies - const token = request.cookies[cookieId]; - const sid = uuidv4(); - const guestPayload = { customer: null, sid }; - // If there is no token, generate a new one for guest user - if (!token) { - // Issue a new token for guest user - const newToken = generateToken(guestPayload, getTokenSecret()); - // Set the new token in the cookies - response.cookie(cookieId, newToken, { maxAge: 1.728e8, httpOnly: true }); - setContextValue(request, 'customerTokenPayload', guestPayload); - setContextValue(request, 'sid', sid); - // Continue to the next middleware - next(); - } else { - // Get user from token - const tokenPayload = jwt.decode(token, { complete: true, json: true }); - let secret; - // Get the secret from database - const check = await select() - .from('user_token_secret') - .where('sid', '=', get(tokenPayload, 'payload.sid', null)) - .and('user_id', '=', get(tokenPayload, 'payload.customer.uuid', null)) - .load(pool); - - if (!check) { - // This is guest user - secret = getTokenSecret(); - } else { - secret = check.secret; - } - - // Verify the token - jwt.verify(token, secret, (err, decoded) => { - if (err) { - // Issue a new token for guest user - const newToken = generateToken(guestPayload, getTokenSecret()); - setContextValue(request, 'customerTokenPayload', guestPayload); - setContextValue(request, 'sid', sid); - // Set the new token in the cookies - response.cookie(cookieId, newToken, { - maxAge: 1.728e8, - httpOnly: true - }); - // Redirect to home page - response.redirect(buildUrl('homepage')); - } else { - setContextValue(request, 'customerTokenPayload', decoded); - setContextValue(request, 'sid', decoded.sid); - next(); - } - }); - } -};
packages/evershop/src/modules/customer/pages/frontStore/all/[customerTokenVerify]auth.js+0 −14 removed@@ -1,14 +0,0 @@ -const { - getContextValue, - setContextValue -} = require('../../../../graphql/services/contextHelper'); -const { get } = require('@evershop/evershop/src/lib/util/get'); - -module.exports = (request, response, delegate, next) => { - // Get the token Payload - const tokenPayLoad = getContextValue(request, 'customerTokenPayload'); - if (get(tokenPayLoad, 'customer.uuid')) { - setContextValue(request, 'customerId', tokenPayLoad.customer?.uuid || null); - } - next(); -};
packages/evershop/src/modules/customer/pages/frontStore/all/UserIcon.jsx+2 −2 modified@@ -34,12 +34,12 @@ export const layout = { export const query = ` query Query { - customer(id: getContextValue("customerId", null)) { + customer: currentCustomer { uuid fullName email } accountUrl: url(routeId: "account") - loginUrl: url(routeId: "login") + loginUrl: url(routeId: "customerLoginJson") } `;
packages/evershop/src/modules/customer/pages/frontStore/customerLoginJson/[bodyParser]login.js+44 −0 added@@ -0,0 +1,44 @@ +const { + translate +} = require('@evershop/evershop/src/lib/locale/translate/translate'); +const { + INVALID_PAYLOAD, + OK, + INTERNAL_SERVER_ERROR +} = require('@evershop/evershop/src/lib/util/httpStatus'); + +// eslint-disable-next-line no-unused-vars +module.exports = async (request, response, delegate, next) => { + const { body } = request; + const { email, password } = body; + const message = translate('Invalid email or password'); + try { + await request.loginCustomerWithEmail(email, password, (error) => { + if (error) { + response.status(INTERNAL_SERVER_ERROR); + return response.json({ + error: { + status: INTERNAL_SERVER_ERROR, + message: message + } + }); + } else { + response.status(OK); + response.$body = { + data: { + sid: request.sessionID + } + }; + next(); + } + }); + } catch (error) { + response.status(INVALID_PAYLOAD); + response.json({ + error: { + status: INVALID_PAYLOAD, + message: message + } + }); + } +};
packages/evershop/src/modules/customer/pages/frontStore/customerLoginJson/payloadSchema.json+0 −0 renamedpackages/evershop/src/modules/customer/pages/frontStore/customerLoginJson/route.json+4 −0 added@@ -0,0 +1,4 @@ +{ + "methods": ["POST"], + "path": "/customer/login" +}
packages/evershop/src/modules/customer/pages/frontStore/customerLogoutJson/logout.js+35 −0 added@@ -0,0 +1,35 @@ +const { + INTERNAL_SERVER_ERROR, + OK +} = require('@evershop/evershop/src/lib/util/httpStatus'); + +// eslint-disable-next-line no-unused-vars +module.exports = (request, response, delegate, next) => { + try { + request.logoutCustomer((error) => { + if (error) { + response.status(INTERNAL_SERVER_ERROR); + return response.json({ + error: { + status: INTERNAL_SERVER_ERROR, + message: error.message + } + }); + } else { + response.status(OK); + response.$body = { + data: {} + }; + next(); + } + }); + } catch (e) { + response.status(INTERNAL_SERVER_ERROR); + response.json({ + error: { + status: INTERNAL_SERVER_ERROR, + message: error.message + } + }); + } +};
packages/evershop/src/modules/customer/pages/frontStore/customerLogoutJson/route.json+4 −0 added@@ -0,0 +1,4 @@ +{ + "methods": ["GET", "POST"], + "path": "/customer/logout" +}
packages/evershop/src/modules/customer/pages/frontStore/login/index.js+2 −4 modified@@ -1,6 +1,5 @@ const { buildUrl } = require('@evershop/evershop/src/lib/router/buildUrl'); const { - getContextValue, setContextValue } = require('../../../../graphql/services/contextHelper'); const { @@ -9,9 +8,8 @@ const { module.exports = (request, response, delegate, next) => { // Check if the user is logged in - const customerTokenPayload = getContextValue(request, 'customerTokenPayload'); - if (customerTokenPayload && customerTokenPayload.customer?.customerId) { - // Redirect to admin dashboard + if (request.isCustomerLoggedIn()) { + // Redirect to homepage response.redirect(buildUrl('homepage')); } else { setContextValue(request, 'pageInfo', {
packages/evershop/src/modules/customer/pages/frontStore/login/LoginForm.jsx+1 −1 modified@@ -109,7 +109,7 @@ export const layout = { export const query = ` query Query { homeUrl: url(routeId: "homepage") - action: url(routeId: "createCustomerSession") + action: url(routeId: "customerLoginJson") registerUrl: url(routeId: "register") forgotPasswordUrl: url(routeId: "resetPasswordPage") }
packages/evershop/src/modules/customer/pages/frontStore/resetPasswordPage/index.js+3 −5 modified@@ -1,17 +1,15 @@ const { buildUrl } = require('@evershop/evershop/src/lib/router/buildUrl'); const { - getContextValue, setContextValue } = require('../../../../graphql/services/contextHelper'); const { translate } = require('@evershop/evershop/src/lib/locale/translate/translate'); module.exports = (request, response, delegate, next) => { - // Check if the user is logged in - const customerTokenPayload = getContextValue(request, 'customerTokenPayload'); - if (customerTokenPayload && customerTokenPayload.customer?.customerId) { - // Redirect to admin dashboard + // Check if the customer is logged in + if (request.isCustomerLoggedIn()) { + // Redirect to homepage response.redirect(buildUrl('homepage')); } else { setContextValue(request, 'pageInfo', {
packages/evershop/src/modules/customer/pages/frontStore/updatePasswordPage/index.js+3 −5 modified@@ -1,17 +1,15 @@ const { buildUrl } = require('@evershop/evershop/src/lib/router/buildUrl'); const { - getContextValue, setContextValue } = require('../../../../graphql/services/contextHelper'); const { translate } = require('@evershop/evershop/src/lib/locale/translate/translate'); module.exports = (request, response, delegate, next) => { - // Check if the user is logged in - const customerTokenPayload = getContextValue(request, 'customerTokenPayload'); - if (customerTokenPayload && customerTokenPayload.customer?.customerId) { - // Redirect to admin dashboard + // Check if the customer is logged in + if (request.isCustomerLoggedIn()) { + // Redirect to homepage response.redirect(buildUrl('homepage')); } else { setContextValue(request, 'pageInfo', {
packages/evershop/src/modules/graphql/api/graphql/[auth]removeUser[graphql].js+1 −3 renamed@@ -1,8 +1,6 @@ -const { setContextValue } = require('../../services/contextHelper'); - module.exports = (request, response, delegate, next) => { // The graphql API supposed to be public // We will remove user from the contex, if you want to use the user in the graphql API, you need to use the admin graphql API - setContextValue(request, 'user', null); + delete request.locals.user; next(); };
packages/evershop/src/modules/graphql/pages/global/bodyParser[buildQuery].js+1 −1 modified@@ -1,6 +1,6 @@ const bodyParser = require('body-parser'); -module.exports = (request, response, stack, next) => { +module.exports = (request, response, delegate, next) => { bodyParser.json({ inflate: false })(request, response, () => { bodyParser.urlencoded({ extended: true })(request, response, next); });
packages/evershop/src/modules/graphql/pages/global/[buildQuery]graphql[notification].js+6 −1 modified@@ -29,9 +29,14 @@ module.exports = async function graphql(request, response, delegate, next) { // eslint-disable-next-line global-require schema = require('../../services/buildSchema'); } + const context = getContext(request); + // Add current user to context + context.user = request.locals.user; + // Add current customer to context + context.customer = request.locals.customer; const data = await execute({ schema, - contextValue: getContext(request), + contextValue: context, document, variableValues: graphqlVariables });
packages/evershop/src/modules/paypal/graphql/types/PaypalSetting/PaypalSetting.resolvers.js+4 −4 modified@@ -48,12 +48,12 @@ module.exports = { return null; } }, - paypalClientSecret: (setting, _, { userTokenPayload }) => { + paypalClientSecret: (setting, _, { user }) => { const paypalConfig = getConfig('system.paypal', {}); if (paypalConfig.clientSecret) { return '*******************************'; } - if (userTokenPayload && userTokenPayload?.user?.uuid) { + if (user) { const paypalClientSecret = setting.find( (s) => s.name === 'paypalClientSecret' ); @@ -66,12 +66,12 @@ module.exports = { return null; } }, - paypalWebhookSecret: (setting, _, { userTokenPayload }) => { + paypalWebhookSecret: (setting, _, { user }) => { const paypalConfig = getConfig('system.paypal', {}); if (paypalConfig.webhookSecret) { return '*******************************'; } - if (userTokenPayload && userTokenPayload?.user?.uuid) { + if (user) { const paypalWebhookSecret = setting.find( (s) => s.name === 'paypalWebhookSecret' );
packages/evershop/src/modules/stripe/graphql/types/StripeSetting/StripeSetting.resolvers.js+4 −4 modified@@ -40,12 +40,12 @@ module.exports = { return null; } }, - stripeSecretKey: (setting, _, { userTokenPayload }) => { + stripeSecretKey: (setting, _, { user }) => { const stripeConfig = getConfig('system.stripe', {}); if (stripeConfig.secretKey) { return '*******************************'; } - if (userTokenPayload && userTokenPayload?.user?.uuid) { + if (user) { const stripeSecretKey = setting.find( (s) => s.name === 'stripeSecretKey' ); @@ -58,12 +58,12 @@ module.exports = { return null; } }, - stripeEndpointSecret: (setting, _, { userTokenPayload }) => { + stripeEndpointSecret: (setting, _, { user }) => { const stripeConfig = getConfig('system.stripe', {}); if (stripeConfig.endpointSecret) { return '*******************************'; } - if (userTokenPayload && userTokenPayload?.user?.uuid) { + if (user) { const stripeEndpointSecret = setting.find( (s) => s.name === 'stripeEndpointSecret' );
packages/google_login/pages/frontStore/gauth/[auth]gAuth.js+0 −0 renamedpackages/google_login/pages/frontStore/gcallback/gCallback.js+12 −11 modified@@ -1,9 +1,6 @@ const { pool } = require('@evershop/evershop/src/lib/postgres/connection'); const { buildUrl } = require('@evershop/evershop/src/lib/router/buildUrl'); const { getConfig } = require('@evershop/evershop/src/lib/util/getConfig'); -const { - getTokenCookieId -} = require('@evershop/evershop/src/modules/auth/services/getTokenCookieId'); const { getGoogleAuthToken } = require('@evershop/google_login/services/getGoogleAuthToken'); @@ -63,15 +60,19 @@ module.exports = async (request, response, delegate, next) => { .execute(pool); } // Login the customer - const token = await request.login(customer.uuid); - // Send a response with the token cookie - response.cookie(getTokenCookieId(), token, { - httpOnly: true, - maxAge: 1.728e8 + request.session.customerID = customer.customer_id; + // Delete the password field + delete customer.password; + // Save the customer in the request + request.locals.customer = customer; + request.session.save((error) => { + if (error) { + debug('critical', error); + response.redirect(failureUrl); + } else { + response.redirect(successUrl); + } }); - - // redirect to home page - response.redirect(successUrl); } catch (error) { debug('critical', error); response.redirect(failureUrl);
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-32r3-57hp-cgfwghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-46943ghsaADVISORY
- advisory.checkmarx.net/advisory/CVE-2023-46943ghsaWEB
- devhub.checkmarx.com/cve-details/CVE-2023-46943ghsaWEB
- github.com/evershopcommerce/evershop/commit/96d9ca3e024e0b63c538911e4a914df3d287cc9fghsaWEB
- advisory.checkmarx.net/advisory/CVE-2023-46943/mitre
- devhub.checkmarx.com/cve-details/CVE-2023-46943/mitre
News mentions
0No linked articles in our index yet.