diff --git a/rair-front/src/components/adminViews/ServerSettings.tsx b/rair-front/src/components/adminViews/ServerSettings.tsx index e021832a9..4527355ca 100644 --- a/rair-front/src/components/adminViews/ServerSettings.tsx +++ b/rair-front/src/components/adminViews/ServerSettings.tsx @@ -13,9 +13,16 @@ import { OptionsType } from '../common/commonTypes/InputSelectTypes.types'; import InputField from '../common/InputField'; import InputSelect from '../common/InputSelect'; +type Category = { + name: string; + _id?: string; + files?: number; +}; + 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 +67,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 +162,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 +617,73 @@ const ServerSettings = ({ fullContractData }) => { ); })} +
+

Categories

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

Footer items

{serverSettings.footerLinks && @@ -657,6 +763,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/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..d427ad06e --- /dev/null +++ b/rair-node/bin/api/categories/categories.Controller.js @@ -0,0 +1,26 @@ +const { Router } = require('express'); +const { + updateCategories, + getCategories, +} = 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, + 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 new file mode 100644 index 000000000..7d3ae62cb --- /dev/null +++ b/rair-node/bin/api/categories/categories.Service.js @@ -0,0 +1,63 @@ +const { Category, File } = require('../../models'); +const AppError = require('../../utils/errors/AppError'); + +module.exports = { + getCategories: async (req, res, next) => { + try { + 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)); + } + }, + updateCategories: async (req, res, next) => { + try { + const currentList = await Category.find().lean(); + const { list } = req.body; + // eslint-disable-next-line no-restricted-syntax + for await (const category of currentList) { + const update = list.find((item) => item?._id === category._id.toString()); + if (update) { + await Category.findByIdAndUpdate(update._id, { $set: { name: update.name } }); + } else { + const usingCategory = await File.findOne({ + category: category._id, + }); + 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 { + 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/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; } 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..0c042a2ef 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,10 @@ module.exports = { type: Joi.string(), read: Joi.boolean(), }), + dbCategory: () => ({ + 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