From 386dee6160cbfd5287f54a789e9313cfda282e49 Mon Sep 17 00:00:00 2001 From: Juan Miguel Sanchez Mola Date: Thu, 27 Jun 2024 21:51:29 -0400 Subject: [PATCH 1/6] Backend changes --- rair-node/bin/api/auth/auth.Service.js | 10 ++++- .../api/categories/categories.Controller.js | 44 +++++++++++++++++++ .../bin/api/categories/categories.Service.js | 42 ++++++++++++++++++ rair-node/bin/models/serverSettings.js | 1 + rair-node/bin/routes/index.js | 2 + rair-node/bin/schemas/databaseSchemas.js | 4 ++ 6 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 rair-node/bin/api/categories/categories.Controller.js create mode 100644 rair-node/bin/api/categories/categories.Service.js diff --git a/rair-node/bin/api/auth/auth.Service.js b/rair-node/bin/api/auth/auth.Service.js index fea75051b..f8e40b992 100644 --- a/rair-node/bin/api/auth/auth.Service.js +++ b/rair-node/bin/api/auth/auth.Service.js @@ -4,6 +4,7 @@ const { File, User, MediaViewLog, Unlock, ServerSetting } = require('../../model const AppError = require('../../utils/errors/AppError'); const { checkBalanceAny, checkBalanceProduct, checkAdminTokenOwns } = require('../../integrations/ethers/tokenValidation'); const { superAdminInstance } = require('../../utils/vaultSuperAdmin'); +const { emitEvent } = require('../../integrations/socket.io'); module.exports = { generateChallengeMessage: async (req, res, next) => { @@ -87,7 +88,14 @@ module.exports = { } userData.adminRights = await checkAdminTokenOwns(userData.publicAddress); - const { superAdmins, superAdminsOnVault } = await ServerSetting.findOne({}); + const { superAdmins, superAdminsOnVault, signupMessage } = await ServerSetting.findOne({}); + const socket = req.app.get('socket'); + emitEvent(socket)( + userData.publicAddress, + 'message', + signupMessage, + [], + ); userData.superAdmin = superAdminsOnVault ? await superAdminInstance.hasSuperAdminRights(userData.publicAddress) : superAdmins.includes(userData.publicAddress); diff --git a/rair-node/bin/api/categories/categories.Controller.js b/rair-node/bin/api/categories/categories.Controller.js new file mode 100644 index 000000000..dc5d2f33c --- /dev/null +++ b/rair-node/bin/api/categories/categories.Controller.js @@ -0,0 +1,44 @@ +const { Router } = require('express'); +const { + getCategories, + createCategory, + updateCategory, + deleteCategory, +} = require('./categories.Service'); +const { requireUserSession } = require('../../middleware/verifyUserSession'); +const validation = require('../../middleware/validation'); +const { verifySuperAdmin } = require('../../middleware'); + +const router = Router(); + +router.get( + '/', + getCategories, +); + +router.post( + '/', + validation(['dbCategory'], 'body'), + requireUserSession, + verifySuperAdmin, + createCategory, +); + +router.put( + '/:id', + validation(['dbCategory'], 'body'), + validation(['dbId'], 'params'), + requireUserSession, + verifySuperAdmin, + updateCategory, +); + +router.delete( + '/:id', + validation(['dbId'], 'params'), + requireUserSession, + verifySuperAdmin, + deleteCategory, +); + +module.exports = router; diff --git a/rair-node/bin/api/categories/categories.Service.js b/rair-node/bin/api/categories/categories.Service.js new file mode 100644 index 000000000..946dda607 --- /dev/null +++ b/rair-node/bin/api/categories/categories.Service.js @@ -0,0 +1,42 @@ +const { Category } = require('../../models'); +const AppError = require('../../utils/errors/AppError'); + +module.exports = { + getCategories: async (req, res, next) => { + try { + const list = await Category.find(); + return res.json({ result: list, success: true }); + } catch (error) { + return next(new AppError(error)); + } + }, + createCategory: async (req, res, next) => { + try { + const { name } = req.body; + const category = new Category({ name }); + await category.save(); + return res.json({ success: true, result: category }); + } catch (error) { + return next(new AppError(error)); + } + }, + updateCategory: async (req, res, next) => { + try { + const { id } = req.params; + const { name } = req.body; + const category = await Category.findByIdAndUpdate(id, { $set: { name } }); + return res.json({ success: true, result: category }); + } catch (error) { + return next(new AppError(error)); + } + }, + deleteCategory: async (req, res, next) => { + try { + const { id } = req.params; + await Category.findByIdAndDelete(id); + return res.json({ success: true }); + } catch (error) { + return next(new AppError(error)); + } + }, +}; diff --git a/rair-node/bin/models/serverSettings.js b/rair-node/bin/models/serverSettings.js index b0dcd83b1..710cd0cbf 100644 --- a/rair-node/bin/models/serverSettings.js +++ b/rair-node/bin/models/serverSettings.js @@ -41,6 +41,7 @@ const ServerSetting = new Schema({ legal: { type: String, required: false }, // Favicon favicon: { type: String, required: false }, + signupMessage: { type: String, required: false, default: 'Welcome' }, }, { versionKey: false, timestamps: false }); module.exports = ServerSetting; diff --git a/rair-node/bin/routes/index.js b/rair-node/bin/routes/index.js index 7bb9563b9..f9d8f43a1 100644 --- a/rair-node/bin/routes/index.js +++ b/rair-node/bin/routes/index.js @@ -17,6 +17,7 @@ const nftController = require('../api/nft/nft.Controller'); const uploadController = require('../api/upload/upload.Controller'); const favoritesController = require('../api/favorites/favorites.Controller'); const notificationsController = require('../api/notifications/notifications.Controller'); +const categoriesController = require('../api/categories/categories.Controller'); const router = Router(); router.use('/analytics', analyticsController); @@ -36,6 +37,7 @@ router.use('/settings', settingsRouter); router.use('/tokens', tokensController); router.use('/upload', uploadController); router.use('/notifications', notificationsController); +router.use('/categories', categoriesController); // Custom temporary endpoint for the monaco2021 router.use('/', require('./monaco2021')); diff --git a/rair-node/bin/schemas/databaseSchemas.js b/rair-node/bin/schemas/databaseSchemas.js index f14cda3db..2929d506d 100644 --- a/rair-node/bin/schemas/databaseSchemas.js +++ b/rair-node/bin/schemas/databaseSchemas.js @@ -21,6 +21,7 @@ module.exports = { url: Joi.string(), })), legal: Joi.string(), + signupMessage: Joi.string(), }), dbContracts: () => ({ title: Joi.string(), @@ -123,4 +124,7 @@ module.exports = { type: Joi.string(), read: Joi.boolean(), }), + dbCategory: () => ({ + name: Joi.string(), + }), }; From a5307adbcf6e4d35aa3676837b95a4f7bb61a9af Mon Sep 17 00:00:00 2001 From: Juan Miguel Sanchez Mola Date: Tue, 2 Jul 2024 10:18:42 -0400 Subject: [PATCH 2/6] Frontend update --- .../components/adminViews/ServerSettings.tsx | 111 ++++++++++++++++++ .../adminViews/useServerSettings.tsx | 4 + .../api/categories/categories.Controller.js | 24 +--- .../bin/api/categories/categories.Service.js | 60 ++++++---- rair-node/bin/schemas/databaseSchemas.js | 5 +- rair-node/bin/seeds/index.js | 19 +-- 6 files changed, 168 insertions(+), 55 deletions(-) diff --git a/rair-front/src/components/adminViews/ServerSettings.tsx b/rair-front/src/components/adminViews/ServerSettings.tsx index e021832a9..6654761a4 100644 --- a/rair-front/src/components/adminViews/ServerSettings.tsx +++ b/rair-front/src/components/adminViews/ServerSettings.tsx @@ -13,9 +13,15 @@ import { OptionsType } from '../common/commonTypes/InputSelectTypes.types'; import InputField from '../common/InputField'; import InputSelect from '../common/InputSelect'; +type Category = { + name: string; + _id?: string; +}; + const ServerSettings = ({ fullContractData }) => { const serverSettings = useServerSettings(); const [productOptions, setProductOptions] = useState(); + const [categoryList, setCategoryList] = useState([]); const [customLightModeLogo, setCustomLightModeLogo] = useState({ name: '' }); const [customDarkModeLogo, setCustomDarkModeLogo] = useState({ name: '' }); @@ -60,6 +66,17 @@ const ServerSettings = ({ fullContractData }) => { serverSettings.setFooterLinks(aux); }; + const getCategories = useCallback(async () => { + const { success, result } = await rFetch('/api/categories'); + if (success) { + setCategoryList(result); + } + }, []); + + useEffect(() => { + getCategories(); + }, [getCategories]); + const setServerSetting = useCallback( async (setting) => { const { success } = await rFetch(`/api/settings/`, { @@ -144,6 +161,27 @@ const ServerSettings = ({ fullContractData }) => { serverSettings.getServerSettings(); }, [serverSettings.getServerSettings]); + const deleteCategory = useCallback( + (index) => { + const aux = [...categoryList]; + aux.splice(index, 1); + setCategoryList(aux); + }, + [categoryList] + ); + + const updateCategory = useCallback( + (index) => (value) => { + const aux = [...categoryList]; + aux[index] = { + ...aux[index], + name: value + }; + setCategoryList(aux); + }, + [categoryList] + ); + useEffect(() => { serverSettings.setFeaturedProduct('null'); if (serverSettings.featuredContract === 'null') { @@ -578,6 +616,59 @@ const ServerSettings = ({ fullContractData }) => { ); })} +
+

Categories

+ {categoryList.map((categoryData, index) => { + return ( +
+
+ +
+ +
+ ); + })} + + +

Footer items

{serverSettings.footerLinks && @@ -657,6 +748,26 @@ const ServerSettings = ({ fullContractData }) => { Set
+
+

Default Signup Message

+ + +
); diff --git a/rair-front/src/components/adminViews/useServerSettings.tsx b/rair-front/src/components/adminViews/useServerSettings.tsx index d08dd71a0..8dda8d375 100644 --- a/rair-front/src/components/adminViews/useServerSettings.tsx +++ b/rair-front/src/components/adminViews/useServerSettings.tsx @@ -20,6 +20,7 @@ const useServerSettings = () => { import.meta.env.VITE_NODE_ADDRESS ); const [legal, setLegal] = useState(''); + const [signupMessage, setSignupMessage] = useState(''); const [isLoading, setIsLoading] = useState(false); const [featuredContract, setFeaturedContract] = useState('null'); const [featuredProduct, setFeaturedProduct] = useState('null'); @@ -47,6 +48,7 @@ const useServerSettings = () => { settings?.nodeAddress || import.meta.env.VITE_NODE_ADDRESS ); setLegal(settings?.legal || ''); + setSignupMessage(settings?.signupMessage || ''); if (settings.featuredCollection) { setFeaturedContract(settings?.featuredCollection?.contract?._id); setFeaturedProduct(settings?.featuredCollection?._id); @@ -171,6 +173,8 @@ const useServerSettings = () => { setFooterLinks, legal, setLegal, + signupMessage, + setSignupMessage, blockchainSettings, isLoading }; diff --git a/rair-node/bin/api/categories/categories.Controller.js b/rair-node/bin/api/categories/categories.Controller.js index dc5d2f33c..d427ad06e 100644 --- a/rair-node/bin/api/categories/categories.Controller.js +++ b/rair-node/bin/api/categories/categories.Controller.js @@ -1,9 +1,7 @@ const { Router } = require('express'); const { + updateCategories, getCategories, - createCategory, - updateCategory, - deleteCategory, } = require('./categories.Service'); const { requireUserSession } = require('../../middleware/verifyUserSession'); const validation = require('../../middleware/validation'); @@ -21,24 +19,8 @@ router.post( validation(['dbCategory'], 'body'), requireUserSession, verifySuperAdmin, - createCategory, -); - -router.put( - '/:id', - validation(['dbCategory'], 'body'), - validation(['dbId'], 'params'), - requireUserSession, - verifySuperAdmin, - updateCategory, -); - -router.delete( - '/:id', - validation(['dbId'], 'params'), - requireUserSession, - verifySuperAdmin, - deleteCategory, + updateCategories, + getCategories, ); module.exports = router; diff --git a/rair-node/bin/api/categories/categories.Service.js b/rair-node/bin/api/categories/categories.Service.js index 946dda607..1f5071145 100644 --- a/rair-node/bin/api/categories/categories.Service.js +++ b/rair-node/bin/api/categories/categories.Service.js @@ -1,4 +1,4 @@ -const { Category } = require('../../models'); +const { Category, File } = require('../../models'); const AppError = require('../../utils/errors/AppError'); module.exports = { @@ -10,31 +10,41 @@ module.exports = { return next(new AppError(error)); } }, - createCategory: async (req, res, next) => { + updateCategories: async (req, res, next) => { try { - const { name } = req.body; - const category = new Category({ name }); - await category.save(); - return res.json({ success: true, result: category }); - } catch (error) { - return next(new AppError(error)); - } - }, - updateCategory: async (req, res, next) => { - try { - const { id } = req.params; - const { name } = req.body; - const category = await Category.findByIdAndUpdate(id, { $set: { name } }); - return res.json({ success: true, result: category }); - } catch (error) { - return next(new AppError(error)); - } - }, - deleteCategory: async (req, res, next) => { - try { - const { id } = req.params; - await Category.findByIdAndDelete(id); - return res.json({ success: true }); + const currentList = await Category.find().lean(); + const { list } = req.body; + console.info({ currentList, list }); + // eslint-disable-next-line no-restricted-syntax + for await (const category of currentList) { + const update = list.find((item) => item?._id === category._id.toString()); + console.info({ update }); + if (update) { + await Category.findByIdAndUpdate(update._id, { $set: { name: update.name } }); + } else { + const usingCategory = await File.findOne({ + category: category._id, + }); + console.info('cannot delete', !!usingCategory); + if (!usingCategory) { + await Category.findByIdAndDelete(category._id); + } + } + } + // eslint-disable-next-line no-restricted-syntax + for await (const category of list) { + if (category._id) { + // eslint-disable-next-line no-continue + continue; + } else { + console.info('creating new', category.name); + const newCat = new Category({ + name: category.name, + }); + await newCat.save(); + } + } + return next(); } catch (error) { return next(new AppError(error)); } diff --git a/rair-node/bin/schemas/databaseSchemas.js b/rair-node/bin/schemas/databaseSchemas.js index 2929d506d..0c042a2ef 100644 --- a/rair-node/bin/schemas/databaseSchemas.js +++ b/rair-node/bin/schemas/databaseSchemas.js @@ -125,6 +125,9 @@ module.exports = { read: Joi.boolean(), }), dbCategory: () => ({ - name: Joi.string(), + list: Joi.array().items(Joi.object({ + name: Joi.string().required(), + _id: mongoId, + })), }), }; diff --git a/rair-node/bin/seeds/index.js b/rair-node/bin/seeds/index.js index 53537cec7..2dfd3d48a 100644 --- a/rair-node/bin/seeds/index.js +++ b/rair-node/bin/seeds/index.js @@ -6,18 +6,21 @@ const { Blockchain, Category, ServerSetting, Contract } = require('../models'); module.exports = async () => { try { - // eslint-disable-next-line no-restricted-syntax - for await (const category of categories) { - await Category.findOneAndUpdate( - category, - category, - { upsert: true, new: true }, - ); + const count = await Category.estimatedDocumentCount(); + if (count === 0) { + // eslint-disable-next-line no-restricted-syntax + for await (const category of categories) { + await Category.findOneAndUpdate( + category, + category, + { upsert: true, new: true }, + ); + } + log.info('Categories empty, populating with default values.'); } } catch (e) { log.error(`Error seeding categories: ${e}`); } - log.info('Categories seeded.'); try { // eslint-disable-next-line no-restricted-syntax From 0150e440b7bcd2920bd0c5b3eef24b58c9049203 Mon Sep 17 00:00:00 2001 From: Juan Miguel Sanchez Mola Date: Tue, 2 Jul 2024 18:08:49 -0400 Subject: [PATCH 3/6] Confirmation modal --- rair-front/src/components/adminViews/ServerSettings.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/rair-front/src/components/adminViews/ServerSettings.tsx b/rair-front/src/components/adminViews/ServerSettings.tsx index 6654761a4..eabbcf7e9 100644 --- a/rair-front/src/components/adminViews/ServerSettings.tsx +++ b/rair-front/src/components/adminViews/ServerSettings.tsx @@ -652,6 +652,7 @@ const ServerSettings = ({ fullContractData }) => { } }); if (result.success) { + reactSwal.fire('Success', 'Categories updated', 'success'); getCategories(); } }}> From f36e0cc70092b9780bdcea4e0856573fb85ce57a Mon Sep 17 00:00:00 2001 From: Juan Miguel Sanchez Mola Date: Wed, 3 Jul 2024 12:12:04 -0400 Subject: [PATCH 4/6] Prevent deletion of used categories --- .../components/adminViews/ServerSettings.tsx | 15 ++++++++++--- .../bin/api/categories/categories.Service.js | 21 ++++++++++++++----- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/rair-front/src/components/adminViews/ServerSettings.tsx b/rair-front/src/components/adminViews/ServerSettings.tsx index eabbcf7e9..9ba42ae55 100644 --- a/rair-front/src/components/adminViews/ServerSettings.tsx +++ b/rair-front/src/components/adminViews/ServerSettings.tsx @@ -16,6 +16,7 @@ import InputSelect from '../common/InputSelect'; type Category = { name: string; _id?: string; + files?: number; }; const ServerSettings = ({ fullContractData }) => { @@ -621,7 +622,7 @@ const ServerSettings = ({ fullContractData }) => { {categoryList.map((categoryData, index) => { return (
-
+
{ />
); diff --git a/rair-node/bin/api/categories/categories.Service.js b/rair-node/bin/api/categories/categories.Service.js index 1f5071145..7d3ae62cb 100644 --- a/rair-node/bin/api/categories/categories.Service.js +++ b/rair-node/bin/api/categories/categories.Service.js @@ -4,7 +4,22 @@ const AppError = require('../../utils/errors/AppError'); module.exports = { getCategories: async (req, res, next) => { try { - const list = await Category.find(); + const list = await Category.aggregate([{ + $lookup: { + from: 'File', + localField: '_id', + foreignField: 'category', + as: 'files', + }, + }, { + $project: { + _id: 1, + name: 1, + files: { + $size: '$files', + }, + }, + }]); return res.json({ result: list, success: true }); } catch (error) { return next(new AppError(error)); @@ -14,18 +29,15 @@ module.exports = { try { const currentList = await Category.find().lean(); const { list } = req.body; - console.info({ currentList, list }); // eslint-disable-next-line no-restricted-syntax for await (const category of currentList) { const update = list.find((item) => item?._id === category._id.toString()); - console.info({ update }); if (update) { await Category.findByIdAndUpdate(update._id, { $set: { name: update.name } }); } else { const usingCategory = await File.findOne({ category: category._id, }); - console.info('cannot delete', !!usingCategory); if (!usingCategory) { await Category.findByIdAndDelete(category._id); } @@ -37,7 +49,6 @@ module.exports = { // eslint-disable-next-line no-continue continue; } else { - console.info('creating new', category.name); const newCat = new Category({ name: category.name, }); From ce790a17c891d88d6aef1c663a2ba632ff7801c5 Mon Sep 17 00:00:00 2001 From: Juan Miguel Sanchez Mola Date: Wed, 3 Jul 2024 12:21:59 -0400 Subject: [PATCH 5/6] Error handler on NFT owner query in external --- .../bin/integrations/ethers/importContractData.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/rair-node/bin/integrations/ethers/importContractData.js b/rair-node/bin/integrations/ethers/importContractData.js index c5ef97398..d440cff50 100644 --- a/rair-node/bin/integrations/ethers/importContractData.js +++ b/rair-node/bin/integrations/ethers/importContractData.js @@ -227,8 +227,15 @@ module.exports = { limit, ], ); - const ownerResponse = await alchemySDK.nft.getOwnersForNft(nft.contract.address, nft.tokenId); - [nft.owner] = ownerResponse.owners; + try { + const ownerResponse = await alchemySDK.nft.getOwnersForNft( + nft.contract.address, + nft.tokenId, + ); + [nft.owner] = ownerResponse.owners; + } catch (err) { + log.error(`Could not query owner of NFT #${nft.tokenId}`); + } if (insertToken(nft, contract._id)) { numberOfTokensAdded += 1; } From 2368e7d8885c793beeb2ad63e1717b6de2cfdb81 Mon Sep 17 00:00:00 2001 From: Juan Miguel Sanchez Mola Date: Wed, 3 Jul 2024 13:59:15 -0400 Subject: [PATCH 6/6] Send without file data --- rair-front/src/components/adminViews/ServerSettings.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rair-front/src/components/adminViews/ServerSettings.tsx b/rair-front/src/components/adminViews/ServerSettings.tsx index 9ba42ae55..4527355ca 100644 --- a/rair-front/src/components/adminViews/ServerSettings.tsx +++ b/rair-front/src/components/adminViews/ServerSettings.tsx @@ -655,7 +655,12 @@ const ServerSettings = ({ fullContractData }) => { onClick={async () => { const result = await rFetch('/api/categories', { method: 'POST', - body: JSON.stringify({ list: categoryList }), + body: JSON.stringify({ + list: categoryList.map((item) => ({ + _id: item._id, + name: item.name + })) + }), headers: { 'Content-Type': 'application/json' }