diff --git a/apps/app/.eslintrc.js b/apps/app/.eslintrc.js index 1deda116511..9bc90909989 100644 --- a/apps/app/.eslintrc.js +++ b/apps/app/.eslintrc.js @@ -71,6 +71,7 @@ module.exports = { 'src/server/routes/apiv3/security-settings/**', 'src/server/routes/apiv3/app-settings/**', 'src/server/routes/apiv3/page/**', + 'src/server/routes/apiv3/*.ts', ], settings: { // resolve path aliases by eslint-import-resolver-typescript diff --git a/apps/app/src/server/routes/apiv3/activity.ts b/apps/app/src/server/routes/apiv3/activity.ts index 8f95b86f098..667be43e4f6 100644 --- a/apps/app/src/server/routes/apiv3/activity.ts +++ b/apps/app/src/server/routes/apiv3/activity.ts @@ -1,11 +1,11 @@ +import { SCOPE } from '@growi/core/dist/interfaces'; import { serializeUserSecurely } from '@growi/core/dist/models/serializers'; -import { parseISO, addMinutes, isValid } from 'date-fns'; +import { addMinutes, isValid, parseISO } from 'date-fns'; import type { Request, Router } from 'express'; import express from 'express'; import { query } from 'express-validator'; import type { IActivity, ISearchFilter } from '~/interfaces/activity'; -import { SCOPE } from '@growi/core/dist/interfaces'; import { accessTokenParser } from '~/server/middlewares/access-token-parser'; import Activity from '~/server/models/activity'; import { configManager } from '~/server/service/config-manager'; @@ -13,18 +13,21 @@ import loggerFactory from '~/utils/logger'; import type Crowi from '../../crowi'; import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator'; - import type { ApiV3Response } from './interfaces/apiv3-response'; - const logger = loggerFactory('growi:routes:apiv3:activity'); - const validator = { list: [ - query('limit').optional().isInt({ max: 100 }).withMessage('limit must be a number less than or equal to 100'), + query('limit') + .optional() + .isInt({ max: 100 }) + .withMessage('limit must be a number less than or equal to 100'), query('offset').optional().isInt().withMessage('page must be a number'), - query('searchFilter').optional().isString().withMessage('query must be a string'), + query('searchFilter') + .optional() + .isString() + .withMessage('query must be a string'), ], }; @@ -171,7 +174,9 @@ const validator = { module.exports = (crowi: Crowi): Router => { const adminRequired = require('../../middlewares/admin-required')(crowi); - const loginRequiredStrictly = require('../../middlewares/login-required')(crowi); + const loginRequiredStrictly = require('../../middlewares/login-required')( + crowi, + ); const router = express.Router(); @@ -209,9 +214,14 @@ module.exports = (crowi: Crowi): Router => { * schema: * $ref: '#/components/schemas/ActivityResponse' */ - router.get('/', + router.get( + '/', accessTokenParser([SCOPE.READ.ADMIN.AUDIT_LOG], { acceptLegacy: true }), - loginRequiredStrictly, adminRequired, validator.list, apiV3FormValidator, async(req: Request, res: ApiV3Response) => { + loginRequiredStrictly, + adminRequired, + validator.list, + apiV3FormValidator, + async (req: Request, res: ApiV3Response) => { const auditLogEnabled = configManager.getConfig('app:auditLogEnabled'); if (!auditLogEnabled) { const msg = 'AuditLog is not enabled'; @@ -219,28 +229,36 @@ module.exports = (crowi: Crowi): Router => { return res.apiv3Err(msg, 405); } - const limit = req.query.limit || configManager.getConfig('customize:showPageLimitationS'); + const limit = + req.query.limit || + configManager.getConfig('customize:showPageLimitationS'); const offset = req.query.offset || 1; const query = {}; try { - const parsedSearchFilter = JSON.parse(req.query.searchFilter as string) as ISearchFilter; + const parsedSearchFilter = JSON.parse( + req.query.searchFilter as string, + ) as ISearchFilter; // add username to query - const canContainUsernameFilterToQuery = ( - parsedSearchFilter.usernames != null - && parsedSearchFilter.usernames.length > 0 - && parsedSearchFilter.usernames.every(u => typeof u === 'string') - ); + const canContainUsernameFilterToQuery = + parsedSearchFilter.usernames != null && + parsedSearchFilter.usernames.length > 0 && + parsedSearchFilter.usernames.every((u) => typeof u === 'string'); if (canContainUsernameFilterToQuery) { - Object.assign(query, { 'snapshot.username': parsedSearchFilter.usernames }); + Object.assign(query, { + 'snapshot.username': parsedSearchFilter.usernames, + }); } // add action to query if (parsedSearchFilter.actions != null) { - const availableActions = crowi.activityService.getAvailableActions(false); - const searchableActions = parsedSearchFilter.actions.filter(action => availableActions.includes(action)); + const availableActions = + crowi.activityService.getAvailableActions(false); + const searchableActions = parsedSearchFilter.actions.filter( + (action) => availableActions.includes(action), + ); Object.assign(query, { action: searchableActions }); } @@ -255,8 +273,7 @@ module.exports = (crowi: Crowi): Router => { $lt: addMinutes(endDate, 1439), }, }); - } - else if (isValid(startDate) && !isValid(endDate)) { + } else if (isValid(startDate) && !isValid(endDate)) { Object.assign(query, { createdAt: { $gte: startDate, @@ -265,23 +282,19 @@ module.exports = (crowi: Crowi): Router => { }, }); } - } - catch (err) { + } catch (err) { logger.error('Invalid value', err); return res.apiv3Err(err, 400); } try { - const paginateResult = await Activity.paginate( - query, - { - lean: true, - limit, - offset, - sort: { createdAt: -1 }, - populate: 'user', - }, - ); + const paginateResult = await Activity.paginate(query, { + lean: true, + limit, + offset, + sort: { createdAt: -1 }, + populate: 'user', + }); const serializedDocs = paginateResult.docs.map((doc: IActivity) => { const { user, ...rest } = doc; @@ -297,12 +310,12 @@ module.exports = (crowi: Crowi): Router => { }; return res.apiv3({ serializedPaginationResult }); - } - catch (err) { + } catch (err) { logger.error('Failed to get paginated activity', err); return res.apiv3Err(err, 500); } - }); + }, + ); return router; }; diff --git a/apps/app/src/server/routes/apiv3/admin-home.ts b/apps/app/src/server/routes/apiv3/admin-home.ts index b8f60c577c3..8d3c26af20e 100644 --- a/apps/app/src/server/routes/apiv3/admin-home.ts +++ b/apps/app/src/server/routes/apiv3/admin-home.ts @@ -1,4 +1,5 @@ import { SCOPE } from '@growi/core/dist/interfaces'; + import { accessTokenParser } from '~/server/middlewares/access-token-parser'; import { configManager } from '~/server/service/config-manager'; import { getGrowiVersion } from '~/utils/growi-version'; @@ -60,7 +61,9 @@ const router = express.Router(); */ /** @param {import('~/server/crowi').default} crowi Crowi instance */ module.exports = (crowi) => { - const loginRequiredStrictly = require('../../middlewares/login-required')(crowi); + const loginRequiredStrictly = require('../../middlewares/login-required')( + crowi, + ); const adminRequired = require('../../middlewares/admin-required')(crowi); /** @@ -83,22 +86,30 @@ module.exports = (crowi) => { * adminHomeParams: * $ref: "#/components/schemas/SystemInformationParams" */ - router.get('/', accessTokenParser([SCOPE.READ.ADMIN.TOP]), loginRequiredStrictly, adminRequired, async(req, res) => { - const { getRuntimeVersions } = await import('~/server/util/runtime-versions'); - const runtimeVersions = await getRuntimeVersions(); + router.get( + '/', + accessTokenParser([SCOPE.READ.ADMIN.TOP]), + loginRequiredStrictly, + adminRequired, + async (req, res) => { + const { getRuntimeVersions } = await import( + '~/server/util/runtime-versions' + ); + const runtimeVersions = await getRuntimeVersions(); - const adminHomeParams = { - growiVersion: getGrowiVersion(), - nodeVersion: runtimeVersions.node ?? '-', - npmVersion: runtimeVersions.npm ?? '-', - pnpmVersion: runtimeVersions.pnpm ?? '-', - envVars: configManager.getManagedEnvVars(), - isV5Compatible: configManager.getConfig('app:isV5Compatible'), - isMaintenanceMode: configManager.getConfig('app:isMaintenanceMode'), - }; + const adminHomeParams = { + growiVersion: getGrowiVersion(), + nodeVersion: runtimeVersions.node ?? '-', + npmVersion: runtimeVersions.npm ?? '-', + pnpmVersion: runtimeVersions.pnpm ?? '-', + envVars: configManager.getManagedEnvVars(), + isV5Compatible: configManager.getConfig('app:isV5Compatible'), + isMaintenanceMode: configManager.getConfig('app:isMaintenanceMode'), + }; - return res.apiv3({ adminHomeParams }); - }); + return res.apiv3({ adminHomeParams }); + }, + ); return router; }; diff --git a/apps/app/src/server/routes/apiv3/bookmark-folder.ts b/apps/app/src/server/routes/apiv3/bookmark-folder.ts index 54cc3923a68..2210df91372 100644 --- a/apps/app/src/server/routes/apiv3/bookmark-folder.ts +++ b/apps/app/src/server/routes/apiv3/bookmark-folder.ts @@ -1,9 +1,9 @@ +import { SCOPE } from '@growi/core/dist/interfaces'; import { ErrorV3 } from '@growi/core/dist/models'; import { body } from 'express-validator'; import type { Types } from 'mongoose'; import type { BookmarkFolderItems } from '~/interfaces/bookmark-info'; -import { SCOPE } from '@growi/core/dist/interfaces'; import { accessTokenParser } from '~/server/middlewares/access-token-parser'; import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator'; import { InvalidParentBookmarkFolderError } from '~/server/models/errors'; @@ -99,29 +99,44 @@ const router = express.Router(); const validator = { bookmarkFolder: [ body('name').isString().withMessage('name must be a string'), - body('parent').isMongoId().optional({ nullable: true }) - .custom(async(parent: string) => { + body('parent') + .isMongoId() + .optional({ nullable: true }) + .custom(async (parent: string) => { const parentFolder = await BookmarkFolder.findById(parent); if (parentFolder == null || parentFolder.parent != null) { throw new Error('Maximum folder hierarchy of 2 levels'); } }), - body('childFolder').optional().isArray().withMessage('Children must be an array'), - body('bookmarkFolderId').optional().isMongoId().withMessage('Bookark Folder ID must be a valid mongo ID'), + body('childFolder') + .optional() + .isArray() + .withMessage('Children must be an array'), + body('bookmarkFolderId') + .optional() + .isMongoId() + .withMessage('Bookark Folder ID must be a valid mongo ID'), ], bookmarkPage: [ body('pageId').isMongoId().withMessage('Page ID must be a valid mongo ID'), - body('folderId').optional({ nullable: true }).isMongoId().withMessage('Folder ID must be a valid mongo ID'), + body('folderId') + .optional({ nullable: true }) + .isMongoId() + .withMessage('Folder ID must be a valid mongo ID'), ], bookmark: [ body('pageId').isMongoId().withMessage('Page ID must be a valid mongo ID'), - body('status').isBoolean().withMessage('status must be one of true or false'), + body('status') + .isBoolean() + .withMessage('status must be one of true or false'), ], }; /** @param {import('~/server/crowi').default} crowi Crowi instance */ module.exports = (crowi) => { - const loginRequiredStrictly = require('../../middlewares/login-required')(crowi); + const loginRequiredStrictly = require('../../middlewares/login-required')( + crowi, + ); /** * @swagger @@ -157,28 +172,36 @@ module.exports = (crowi) => { * type: object * $ref: '#/components/schemas/BookmarkFolder' */ - router.post('/', + router.post( + '/', accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK], { acceptLegacy: true }), - loginRequiredStrictly, validator.bookmarkFolder, apiV3FormValidator, async(req, res) => { + loginRequiredStrictly, + validator.bookmarkFolder, + apiV3FormValidator, + async (req, res) => { const owner = req.user?._id; const { name, parent } = req.body; const params = { - name, owner, parent, + name, + owner, + parent, }; try { const bookmarkFolder = await BookmarkFolder.createByParameters(params); logger.debug('bookmark folder created', bookmarkFolder); return res.apiv3({ bookmarkFolder }); - } - catch (err) { + } catch (err) { logger.error(err); if (err instanceof InvalidParentBookmarkFolderError) { - return res.apiv3Err(new ErrorV3(err.message, 'failed_to_create_bookmark_folder')); + return res.apiv3Err( + new ErrorV3(err.message, 'failed_to_create_bookmark_folder'), + ); } return res.apiv3Err(err, 500); } - }); + }, + ); /** * @swagger @@ -211,63 +234,75 @@ module.exports = (crowi) => { * type: object * $ref: '#/components/schemas/BookmarkFolder' */ - router.get('/list/:userId', accessTokenParser([SCOPE.READ.FEATURES.BOOKMARK], { acceptLegacy: true }), loginRequiredStrictly, async(req, res) => { - const { userId } = req.params; + router.get( + '/list/:userId', + accessTokenParser([SCOPE.READ.FEATURES.BOOKMARK], { acceptLegacy: true }), + loginRequiredStrictly, + async (req, res) => { + const { userId } = req.params; - const getBookmarkFolders = async( + const getBookmarkFolders = async ( userId: Types.ObjectId | string, parentFolderId?: Types.ObjectId | string, - ) => { - const folders = await BookmarkFolder.find({ owner: userId, parent: parentFolderId }) - .populate('childFolder') - .populate({ - path: 'bookmarks', - model: 'Bookmark', - populate: { - path: 'page', - model: 'Page', + ) => { + const folders = (await BookmarkFolder.find({ + owner: userId, + parent: parentFolderId, + }) + .populate('childFolder') + .populate({ + path: 'bookmarks', + model: 'Bookmark', populate: { - path: 'lastUpdateUser', - model: 'User', + path: 'page', + model: 'Page', + populate: { + path: 'lastUpdateUser', + model: 'User', + }, }, - }, - }).exec() as never as BookmarkFolderItems[]; + }) + .exec()) as never as BookmarkFolderItems[]; - const returnValue: BookmarkFolderItems[] = []; + const returnValue: BookmarkFolderItems[] = []; - const promises = folders.map(async(folder: BookmarkFolderItems) => { - const childFolder = await getBookmarkFolders(userId, folder._id); + const promises = folders.map(async (folder: BookmarkFolderItems) => { + const childFolder = await getBookmarkFolders(userId, folder._id); - // !! DO NOT THIS SERIALIZING OUTSIDE OF PROMISES !! -- 05.23.2023 ryoji-s - // Serializing outside of promises will cause not populated. - const bookmarks = folder.bookmarks.map(bookmark => serializeBookmarkSecurely(bookmark)); + // !! DO NOT THIS SERIALIZING OUTSIDE OF PROMISES !! -- 05.23.2023 ryoji-s + // Serializing outside of promises will cause not populated. + const bookmarks = folder.bookmarks.map((bookmark) => + serializeBookmarkSecurely(bookmark), + ); - const res = { - _id: folder._id.toString(), - name: folder.name, - owner: folder.owner, - bookmarks, - childFolder, - parent: folder.parent, - }; - return res; - }); + const res = { + _id: folder._id.toString(), + name: folder.name, + owner: folder.owner, + bookmarks, + childFolder, + parent: folder.parent, + }; + return res; + }); - const results = await Promise.all(promises) as unknown as BookmarkFolderItems[]; - returnValue.push(...results); - return returnValue; - }; + const results = (await Promise.all( + promises, + )) as unknown as BookmarkFolderItems[]; + returnValue.push(...results); + return returnValue; + }; - try { - const bookmarkFolderItems = await getBookmarkFolders(userId, undefined); + try { + const bookmarkFolderItems = await getBookmarkFolders(userId, undefined); - return res.apiv3({ bookmarkFolderItems }); - } - catch (err) { - logger.error(err); - return res.apiv3Err(err, 500); - } - }); + return res.apiv3({ bookmarkFolderItems }); + } catch (err) { + logger.error(err); + return res.apiv3Err(err, 500); + } + }, + ); /** * @swagger @@ -299,18 +334,22 @@ module.exports = (crowi) => { * description: Number of deleted folders * example: 1 */ - router.delete('/:id', accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK], { acceptLegacy: true }), loginRequiredStrictly, async(req, res) => { - const { id } = req.params; - try { - const result = await BookmarkFolder.deleteFolderAndChildren(id); - const { deletedCount } = result; - return res.apiv3({ deletedCount }); - } - catch (err) { - logger.error(err); - return res.apiv3Err(err, 500); - } - }); + router.delete( + '/:id', + accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK], { acceptLegacy: true }), + loginRequiredStrictly, + async (req, res) => { + const { id } = req.params; + try { + const result = await BookmarkFolder.deleteFolderAndChildren(id); + const { deletedCount } = result; + return res.apiv3({ deletedCount }); + } catch (err) { + logger.error(err); + return res.apiv3Err(err, 500); + } + }, + ); /** * @swagger @@ -355,20 +394,27 @@ module.exports = (crowi) => { * type: object * $ref: '#/components/schemas/BookmarkFolder' */ - router.put('/', - accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK], { acceptLegacy: true }), loginRequiredStrictly, validator.bookmarkFolder, async(req, res) => { - const { - bookmarkFolderId, name, parent, childFolder, - } = req.body; + router.put( + '/', + accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK], { acceptLegacy: true }), + loginRequiredStrictly, + validator.bookmarkFolder, + async (req, res) => { + const { bookmarkFolderId, name, parent, childFolder } = req.body; try { - const bookmarkFolder = await BookmarkFolder.updateBookmarkFolder(bookmarkFolderId, name, parent, childFolder); + const bookmarkFolder = await BookmarkFolder.updateBookmarkFolder( + bookmarkFolderId, + name, + parent, + childFolder, + ); return res.apiv3({ bookmarkFolder }); - } - catch (err) { + } catch (err) { logger.error(err); return res.apiv3Err(err, 500); } - }); + }, + ); /** * @swagger @@ -405,22 +451,31 @@ module.exports = (crowi) => { * type: object * $ref: '#/components/schemas/BookmarkFolder' */ - router.post('/add-bookmark-to-folder', - accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK], { acceptLegacy: true }), loginRequiredStrictly, validator.bookmarkPage, apiV3FormValidator, - async(req, res) => { + router.post( + '/add-bookmark-to-folder', + accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK], { acceptLegacy: true }), + loginRequiredStrictly, + validator.bookmarkPage, + apiV3FormValidator, + async (req, res) => { const userId = req.user?._id; const { pageId, folderId } = req.body; try { - const bookmarkFolder = await BookmarkFolder.insertOrUpdateBookmarkedPage(pageId, userId, folderId); + const bookmarkFolder = + await BookmarkFolder.insertOrUpdateBookmarkedPage( + pageId, + userId, + folderId, + ); logger.debug('bookmark added to folder', bookmarkFolder); return res.apiv3({ bookmarkFolder }); - } - catch (err) { + } catch (err) { logger.error(err); return res.apiv3Err(err, 500); } - }); + }, + ); /** * @swagger @@ -456,18 +511,26 @@ module.exports = (crowi) => { * type: object * $ref: '#/components/schemas/BookmarkFolder' */ - router.put('/update-bookmark', - accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK], { acceptLegacy: true }), loginRequiredStrictly, validator.bookmark, async(req, res) => { + router.put( + '/update-bookmark', + accessTokenParser([SCOPE.WRITE.FEATURES.BOOKMARK], { acceptLegacy: true }), + loginRequiredStrictly, + validator.bookmark, + async (req, res) => { const { pageId, status } = req.body; const userId = req.user?._id; try { - const bookmarkFolder = await BookmarkFolder.updateBookmark(pageId, status, userId); + const bookmarkFolder = await BookmarkFolder.updateBookmark( + pageId, + status, + userId, + ); return res.apiv3({ bookmarkFolder }); - } - catch (err) { + } catch (err) { logger.error(err); return res.apiv3Err(err, 500); } - }); + }, + ); return router; }; diff --git a/apps/app/src/server/routes/apiv3/g2g-transfer.ts b/apps/app/src/server/routes/apiv3/g2g-transfer.ts index f45757cee8f..f3dc4efd9c4 100644 --- a/apps/app/src/server/routes/apiv3/g2g-transfer.ts +++ b/apps/app/src/server/routes/apiv3/g2g-transfer.ts @@ -1,13 +1,13 @@ -import { createReadStream } from 'fs'; -import path from 'path'; - import { SCOPE } from '@growi/core/dist/interfaces'; import { ErrorV3 } from '@growi/core/dist/models'; import type { NextFunction, Request, Router } from 'express'; import express from 'express'; import { body } from 'express-validator'; +import { createReadStream } from 'fs'; import multer from 'multer'; +import path from 'path'; +import type { GrowiArchiveImportOption } from '~/models/admin/growi-archive-import-option'; import { accessTokenParser } from '~/server/middlewares/access-token-parser'; import { isG2GTransferError } from '~/server/models/vo/g2g-transfer-error'; import { configManager } from '~/server/service/config-manager'; @@ -19,15 +19,13 @@ import { getImportService } from '~/server/service/import'; import loggerFactory from '~/utils/logger'; import { TransferKey } from '~/utils/vo/transfer-key'; - import type Crowi from '../../crowi'; import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator'; import { Attachment } from '../../models/attachment'; - import type { ApiV3Response } from './interfaces/apiv3-response'; interface AuthorizedRequest extends Request { - user?: any + user?: any; } const logger = loggerFactory('growi:routes:apiv3:transfer'); @@ -76,20 +74,27 @@ const validator = { * type: string * containerName: * type: string -*/ + */ /* * Routes */ module.exports = (crowi: Crowi): Router => { const { - g2gTransferPusherService, g2gTransferReceiverService, + g2gTransferPusherService, + g2gTransferReceiverService, growiBridgeService, } = crowi; const importService = getImportService(); - if (g2gTransferPusherService == null || g2gTransferReceiverService == null || exportService == null || importService == null - || growiBridgeService == null || configManager == null) { + if ( + g2gTransferPusherService == null || + g2gTransferReceiverService == null || + exportService == null || + importService == null || + growiBridgeService == null || + configManager == null + ) { throw Error('GROWI is not ready for g2g transfer'); } @@ -126,10 +131,16 @@ module.exports = (crowi: Crowi): Router => { const isInstalled = configManager.getConfig('app:installed'); const adminRequired = require('../../middlewares/admin-required')(crowi); - const loginRequiredStrictly = require('../../middlewares/login-required')(crowi); + const loginRequiredStrictly = require('../../middlewares/login-required')( + crowi, + ); // Middleware - const adminRequiredIfInstalled = (req: Request, res: ApiV3Response, next: NextFunction) => { + const adminRequiredIfInstalled = ( + req: Request, + res: ApiV3Response, + next: NextFunction, + ) => { if (!isInstalled) { next(); return; @@ -139,29 +150,47 @@ module.exports = (crowi: Crowi): Router => { }; // Middleware - const appSiteUrlRequiredIfNotInstalled = (req: Request, res: ApiV3Response, next: NextFunction) => { + const appSiteUrlRequiredIfNotInstalled = ( + req: Request, + res: ApiV3Response, + next: NextFunction, + ) => { if (!isInstalled && req.body.appSiteUrl != null) { next(); return; } - if (configManager.getConfig('app:siteUrl') != null || req.body.appSiteUrl != null) { + if ( + configManager.getConfig('app:siteUrl') != null || + req.body.appSiteUrl != null + ) { next(); return; } - return res.apiv3Err(new ErrorV3('Body param "appSiteUrl" is required when GROWI is NOT installed yet'), 400); + return res.apiv3Err( + new ErrorV3( + 'Body param "appSiteUrl" is required when GROWI is NOT installed yet', + ), + 400, + ); }; // Local middleware to check if key is valid or not - const validateTransferKey = async(req: Request, res: ApiV3Response, next: NextFunction) => { + const validateTransferKey = async ( + req: Request, + res: ApiV3Response, + next: NextFunction, + ) => { const transferKey = req.headers[X_GROWI_TRANSFER_KEY_HEADER_NAME] as string; try { await g2gTransferReceiverService.validateTransferKey(transferKey); - } - catch (err) { - return res.apiv3Err(new ErrorV3('Invalid transfer key', 'invalid_transfer_key'), 403); + } catch (err) { + return res.apiv3Err( + new ErrorV3('Invalid transfer key', 'invalid_transfer_key'), + 403, + ); } next(); @@ -200,10 +229,14 @@ module.exports = (crowi: Crowi): Router => { * type: number * description: The size of the file */ - receiveRouter.get('/files', validateTransferKey, async(req: Request, res: ApiV3Response) => { - const files = await crowi.fileUploadService.listFiles(); - return res.apiv3({ files }); - }); + receiveRouter.get( + '/files', + validateTransferKey, + async (req: Request, res: ApiV3Response) => { + const files = await crowi.fileUploadService.listFiles(); + return res.apiv3({ files }); + }, + ); /** * @swagger @@ -251,88 +284,122 @@ module.exports = (crowi: Crowi): Router => { * type: string * description: The message of the result */ - receiveRouter.post('/', validateTransferKey, uploads.single('transferDataZipFile'), async(req: Request & { file: any; }, res: ApiV3Response) => { - const { file } = req; - const { - collections: strCollections, - optionsMap: strOptionsMap, - operatorUserId, - uploadConfigs: strUploadConfigs, - } = req.body; - - /* - * parse multipart form data - */ - let collections; - let optionsMap; - let sourceGROWIUploadConfigs; - try { - collections = JSON.parse(strCollections); - optionsMap = JSON.parse(strOptionsMap); - sourceGROWIUploadConfigs = JSON.parse(strUploadConfigs); - } - catch (err) { - logger.error(err); - return res.apiv3Err(new ErrorV3('Failed to parse request body.', 'parse_failed'), 500); - } + receiveRouter.post( + '/', + validateTransferKey, + uploads.single('transferDataZipFile'), + async (req: Request & { file: any }, res: ApiV3Response) => { + const { file } = req; + const { + collections: strCollections, + optionsMap: strOptionsMap, + operatorUserId, + uploadConfigs: strUploadConfigs, + } = req.body; + + /* + * parse multipart form data + */ + let collections: string[]; + let optionsMap: { [key: string]: GrowiArchiveImportOption }; + let sourceGROWIUploadConfigs: any; + try { + collections = JSON.parse(strCollections); + optionsMap = JSON.parse(strOptionsMap); + sourceGROWIUploadConfigs = JSON.parse(strUploadConfigs); + } catch (err) { + logger.error(err); + return res.apiv3Err( + new ErrorV3('Failed to parse request body.', 'parse_failed'), + 500, + ); + } - /* - * unzip and parse - */ - let meta; - let innerFileStats; - try { - const zipFile = importService.getFile(file.filename); - await importService.unzip(zipFile); + /* + * unzip and parse + */ + let meta: object | undefined; + let innerFileStats: { + fileName: string; + collectionName: string; + size: number; + }[]; + try { + const zipFile = importService.getFile(file.filename); + await importService.unzip(zipFile); - const zipFileStat = await growiBridgeService.parseZipFile(zipFile); - innerFileStats = zipFileStat?.innerFileStats; - meta = zipFileStat?.meta; - } - catch (err) { - logger.error(err); - return res.apiv3Err(new ErrorV3('Failed to validate transfer data file.', 'validation_failed'), 500); - } + const zipFileStat = await growiBridgeService.parseZipFile(zipFile); + innerFileStats = zipFileStat?.innerFileStats ?? []; + meta = zipFileStat?.meta; + } catch (err) { + logger.error(err); + return res.apiv3Err( + new ErrorV3( + 'Failed to validate transfer data file.', + 'validation_failed', + ), + 500, + ); + } - /* - * validate meta.json - */ - try { - importService.validate(meta); - } - catch (err) { - logger.error(err); - return res.apiv3Err( - new ErrorV3( - 'The version of this GROWI and the uploaded GROWI data are not the same', - 'version_incompatible', - ), - 500, - ); - } + /* + * validate meta.json + */ + try { + importService.validate(meta); + } catch (err) { + logger.error(err); + return res.apiv3Err( + new ErrorV3( + 'The version of this GROWI and the uploaded GROWI data are not the same', + 'version_incompatible', + ), + 500, + ); + } - /* - * generate maps of ImportSettings to import - */ - let importSettingsMap: Map; - try { - importSettingsMap = g2gTransferReceiverService.getImportSettingMap(innerFileStats, optionsMap, operatorUserId); - } - catch (err) { - logger.error(err); - return res.apiv3Err(new ErrorV3('Import settings are invalid. See GROWI docs about details.', 'import_settings_invalid')); - } + /* + * generate maps of ImportSettings to import + */ + let importSettingsMap: Map; + try { + importSettingsMap = g2gTransferReceiverService.getImportSettingMap( + innerFileStats, + optionsMap, + operatorUserId, + ); + } catch (err) { + logger.error(err); + return res.apiv3Err( + new ErrorV3( + 'Import settings are invalid. See GROWI docs about details.', + 'import_settings_invalid', + ), + ); + } - try { - await g2gTransferReceiverService.importCollections(collections, importSettingsMap, sourceGROWIUploadConfigs); - } - catch (err) { - logger.error(err); - return res.apiv3Err(new ErrorV3('Failed to import MongoDB collections', 'mongo_collection_import_failure'), 500); - } + try { + await g2gTransferReceiverService.importCollections( + collections, + importSettingsMap, + sourceGROWIUploadConfigs, + ); + } catch (err) { + logger.error(err); + return res.apiv3Err( + new ErrorV3( + 'Failed to import MongoDB collections', + 'mongo_collection_import_failure', + ), + 500, + ); + } - return res.apiv3({ message: 'Successfully started to receive transfer data.' }); - }); + return res.apiv3({ + message: 'Successfully started to receive transfer data.', + }); + }, + ); /** * @swagger @@ -370,54 +437,101 @@ module.exports = (crowi: Crowi): Router => { * description: The message of the result */ // This endpoint uses multer's MemoryStorage since the received data should be persisted directly on attachment storage. - receiveRouter.post('/attachment', validateTransferKey, uploadsForAttachment.single('content'), - async(req: Request & { file: any; }, res: ApiV3Response) => { + receiveRouter.post( + '/attachment', + validateTransferKey, + uploadsForAttachment.single('content'), + async (req: Request & { file: any }, res: ApiV3Response) => { const { file } = req; const { attachmentMetadata } = req.body; - let attachmentMap; + let attachmentMap: { fileName: any; fileSize: any }; try { attachmentMap = JSON.parse(attachmentMetadata); - } - catch (err) { + } catch (err) { logger.error(err); - return res.apiv3Err(new ErrorV3('Failed to parse body.', 'parse_failed'), 500); + return res.apiv3Err( + new ErrorV3('Failed to parse body.', 'parse_failed'), + 500, + ); } try { const { fileName, fileSize } = attachmentMap; - if (typeof fileName !== 'string' || fileName.length === 0 || fileName.length > 256) { + if ( + typeof fileName !== 'string' || + fileName.length === 0 || + fileName.length > 256 + ) { logger.warn('Invalid fileName in attachment metadata.', { fileName }); - return res.apiv3Err(new ErrorV3('Invalid fileName in attachment metadata.', 'invalid_metadata'), 400); + return res.apiv3Err( + new ErrorV3( + 'Invalid fileName in attachment metadata.', + 'invalid_metadata', + ), + 400, + ); } - if (typeof fileSize !== 'number' || !Number.isInteger(fileSize) || fileSize < 0) { + if ( + typeof fileSize !== 'number' || + !Number.isInteger(fileSize) || + fileSize < 0 + ) { logger.warn('Invalid fileSize in attachment metadata.', { fileSize }); - return res.apiv3Err(new ErrorV3('Invalid fileSize in attachment metadata.', 'invalid_metadata'), 400); + return res.apiv3Err( + new ErrorV3( + 'Invalid fileSize in attachment metadata.', + 'invalid_metadata', + ), + 400, + ); } const count = await Attachment.countDocuments({ fileName, fileSize }); if (count === 0) { - logger.warn('Attachment not found in collection.', { fileName, fileSize }); - return res.apiv3Err(new ErrorV3('Attachment not found in collection.', 'attachment_not_found'), 404); + logger.warn('Attachment not found in collection.', { + fileName, + fileSize, + }); + return res.apiv3Err( + new ErrorV3( + 'Attachment not found in collection.', + 'attachment_not_found', + ), + 404, + ); } - } - catch (err) { + } catch (err) { logger.error(err); - return res.apiv3Err(new ErrorV3('Failed to check attachment existence.', 'attachment_check_failed'), 500); + return res.apiv3Err( + new ErrorV3( + 'Failed to check attachment existence.', + 'attachment_check_failed', + ), + 500, + ); } const fileStream = createReadStream(file.path, { - flags: 'r', mode: 0o666, autoClose: true, + flags: 'r', + mode: 0o666, + autoClose: true, }); try { - await g2gTransferReceiverService.receiveAttachment(fileStream, attachmentMap); - } - catch (err) { + await g2gTransferReceiverService.receiveAttachment( + fileStream, + attachmentMap, + ); + } catch (err) { logger.error(err); - return res.apiv3Err(new ErrorV3('Failed to upload.', 'upload_failed'), 500); + return res.apiv3Err( + new ErrorV3('Failed to upload.', 'upload_failed'), + 500, + ); } return res.apiv3({ message: 'Successfully imported attached file.' }); - }); + }, + ); /** * @swagger @@ -439,23 +553,32 @@ module.exports = (crowi: Crowi): Router => { * growiInfo: * $ref: '#/components/schemas/GrowiInfo' */ - receiveRouter.get('/growi-info', validateTransferKey, async(req: Request, res: ApiV3Response) => { - let growiInfo: IDataGROWIInfo; - try { - growiInfo = await g2gTransferReceiverService.answerGROWIInfo(); - } - catch (err) { - logger.error(err); + receiveRouter.get( + '/growi-info', + validateTransferKey, + async (req: Request, res: ApiV3Response) => { + let growiInfo: IDataGROWIInfo; + try { + growiInfo = await g2gTransferReceiverService.answerGROWIInfo(); + } catch (err) { + logger.error(err); - if (!isG2GTransferError(err)) { - return res.apiv3Err(new ErrorV3('Failed to prepare GROWI info', 'failed_to_prepare_growi_info'), 500); - } + if (!isG2GTransferError(err)) { + return res.apiv3Err( + new ErrorV3( + 'Failed to prepare GROWI info', + 'failed_to_prepare_growi_info', + ), + 500, + ); + } - return res.apiv3Err(new ErrorV3(err.message, err.code), 500); - } + return res.apiv3Err(new ErrorV3(err.message, err.code), 500); + } - return res.apiv3({ growiInfo }); - }); + return res.apiv3({ growiInfo }); + }, + ); /** * @swagger @@ -489,32 +612,46 @@ module.exports = (crowi: Crowi): Router => { * type: string * description: The transfer key */ - receiveRouter.post('/generate-key', + receiveRouter.post( + '/generate-key', accessTokenParser([SCOPE.WRITE.ADMIN.EXPORT_DATA], { acceptLegacy: true }), - adminRequiredIfInstalled, appSiteUrlRequiredIfNotInstalled, async(req: Request, res: ApiV3Response) => { - const appSiteUrl = req.body.appSiteUrl ?? configManager.getConfig('app:siteUrl'); + adminRequiredIfInstalled, + appSiteUrlRequiredIfNotInstalled, + async (req: Request, res: ApiV3Response) => { + const appSiteUrl = + req.body.appSiteUrl ?? configManager.getConfig('app:siteUrl'); let appSiteUrlOrigin: string; try { appSiteUrlOrigin = new URL(appSiteUrl).origin; - } - catch (err) { + } catch (err) { logger.error(err); - return res.apiv3Err(new ErrorV3('appSiteUrl may be wrong', 'failed_to_generate_key_string')); + return res.apiv3Err( + new ErrorV3( + 'appSiteUrl may be wrong', + 'failed_to_generate_key_string', + ), + ); } // Save TransferKey document let transferKeyString: string; try { - transferKeyString = await g2gTransferReceiverService.createTransferKey(appSiteUrlOrigin); - } - catch (err) { + transferKeyString = + await g2gTransferReceiverService.createTransferKey(appSiteUrlOrigin); + } catch (err) { logger.error(err); - return res.apiv3Err(new ErrorV3('Error occurred while generating transfer key.', 'failed_to_generate_key')); + return res.apiv3Err( + new ErrorV3( + 'Error occurred while generating transfer key.', + 'failed_to_generate_key', + ), + ); } return res.apiv3({ transferKey: transferKeyString }); - }); + }, + ); /** * @swagger @@ -556,44 +693,65 @@ module.exports = (crowi: Crowi): Router => { * type: string * description: The message of the result */ - pushRouter.post('/transfer', + pushRouter.post( + '/transfer', accessTokenParser([SCOPE.WRITE.ADMIN.EXPORT_DATA], { acceptLegacy: true }), - loginRequiredStrictly, adminRequired, validator.transfer, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response) => { + loginRequiredStrictly, + adminRequired, + validator.transfer, + apiV3FormValidator, + async (req: AuthorizedRequest, res: ApiV3Response) => { const { transferKey, collections, optionsMap } = req.body; // Parse transfer key let tk: TransferKey; try { tk = TransferKey.parse(transferKey); - } - catch (err) { + } catch (err) { logger.error(err); - return res.apiv3Err(new ErrorV3('Transfer key is invalid', 'transfer_key_invalid'), 400); + return res.apiv3Err( + new ErrorV3('Transfer key is invalid', 'transfer_key_invalid'), + 400, + ); } // get growi info let destGROWIInfo: IDataGROWIInfo; try { destGROWIInfo = await g2gTransferPusherService.askGROWIInfo(tk); - } - catch (err) { + } catch (err) { logger.error(err); - return res.apiv3Err(new ErrorV3('Error occurred while asking GROWI info.', 'failed_to_ask_growi_info')); + return res.apiv3Err( + new ErrorV3( + 'Error occurred while asking GROWI info.', + 'failed_to_ask_growi_info', + ), + ); } // Check if can transfer - const transferability = await g2gTransferPusherService.getTransferability(destGROWIInfo); + const transferability = + await g2gTransferPusherService.getTransferability(destGROWIInfo); if (!transferability.canTransfer) { - return res.apiv3Err(new ErrorV3(transferability.reason, 'growi_incompatible_to_transfer')); + return res.apiv3Err( + new ErrorV3(transferability.reason, 'growi_incompatible_to_transfer'), + ); } // Start transfer // DO NOT "await". Let it run in the background. // Errors should be emitted through websocket. - g2gTransferPusherService.startTransfer(tk, req.user, collections, optionsMap, destGROWIInfo); + g2gTransferPusherService.startTransfer( + tk, + req.user, + collections, + optionsMap, + destGROWIInfo, + ); return res.apiv3({ message: 'Successfully requested auto transfer.' }); - }); + }, + ); // Merge receiveRouter and pushRouter router.use(receiveRouter, pushRouter); diff --git a/apps/app/src/server/routes/apiv3/healthcheck.ts b/apps/app/src/server/routes/apiv3/healthcheck.ts index 8bdd0126da7..7c6e53f96f2 100644 --- a/apps/app/src/server/routes/apiv3/healthcheck.ts +++ b/apps/app/src/server/routes/apiv3/healthcheck.ts @@ -5,10 +5,8 @@ import nocache from 'nocache'; import loggerFactory from '~/utils/logger'; import { Config } from '../../models/config'; - import type { ApiV3Response } from './interfaces/apiv3-response'; - const logger = loggerFactory('growi:routes:apiv3:healthcheck'); const router = express.Router(); @@ -79,15 +77,19 @@ const router = express.Router(); */ /** @param {import('~/server/crowi').default} crowi Crowi instance */ module.exports = (crowi) => { - async function checkMongo(errors, info) { try { await Config.findOne({}); info.mongo = 'OK'; - } - catch (err) { - errors.push(new ErrorV3(`MongoDB is not connectable - ${err.message}`, 'healthcheck-mongodb-unhealthy', err.stack)); + } catch (err) { + errors.push( + new ErrorV3( + `MongoDB is not connectable - ${err.message}`, + 'healthcheck-mongodb-unhealthy', + err.stack, + ), + ); } } @@ -97,9 +99,14 @@ module.exports = (crowi) => { try { info.searchInfo = await searchService.getInfoForHealth(); searchService.resetErrorStatus(); - } - catch (err) { - errors.push(new ErrorV3(`The Search Service is not connectable - ${err.message}`, 'healthcheck-search-unhealthy', err.stack)); + } catch (err) { + errors.push( + new ErrorV3( + `The Search Service is not connectable - ${err.message}`, + 'healthcheck-search-unhealthy', + err.stack, + ), + ); } } } @@ -165,20 +172,26 @@ module.exports = (crowi) => { * info: * $ref: '#/components/schemas/HealthcheckInfo' */ - router.get('/', nocache(), async(req, res: ApiV3Response) => { + router.get('/', nocache(), async (req, res: ApiV3Response) => { let checkServices = (() => { if (req.query.checkServices == null) return []; - return Array.isArray(req.query.checkServices) ? req.query.checkServices : [req.query.checkServices]; + return Array.isArray(req.query.checkServices) + ? req.query.checkServices + : [req.query.checkServices]; })(); let isStrictly = req.query.strictly != null; // for backward compatibility if (req.query.connectToMiddlewares != null) { - logger.warn('The param \'connectToMiddlewares\' is deprecated. Use \'checkServices[]\' instead.'); + logger.warn( + "The param 'connectToMiddlewares' is deprecated. Use 'checkServices[]' instead.", + ); checkServices = ['mongo', 'search']; } if (req.query.checkMiddlewaresStrictly != null) { - logger.warn('The param \'checkMiddlewaresStrictly\' is deprecated. Use \'checkServices[]\' and \'strictly\' instead.'); + logger.warn( + "The param 'checkMiddlewaresStrictly' is deprecated. Use 'checkServices[]' and 'strictly' instead.", + ); checkServices = ['mongo', 'search']; isStrictly = true; } diff --git a/apps/app/src/server/routes/apiv3/import.ts b/apps/app/src/server/routes/apiv3/import.ts index fe7f86d353d..5f8d7b0acc6 100644 --- a/apps/app/src/server/routes/apiv3/import.ts +++ b/apps/app/src/server/routes/apiv3/import.ts @@ -1,5 +1,6 @@ import { SCOPE } from '@growi/core/dist/interfaces'; import { ErrorV3 } from '@growi/core/dist/models'; +import type { Router } from 'express'; import { SupportedAction } from '~/interfaces/activity'; import type { GrowiArchiveImportOption } from '~/models/admin/growi-archive-import-option'; @@ -8,11 +9,11 @@ import { accessTokenParser } from '~/server/middlewares/access-token-parser'; import type { ImportSettings } from '~/server/service/import'; import { getImportService } from '~/server/service/import'; import { generateOverwriteParams } from '~/server/service/import/overwrite-params'; +import type { ZipFileStat } from '~/server/service/interfaces/export'; import loggerFactory from '~/utils/logger'; import { generateAddActivityMiddleware } from '../../middlewares/add-activity'; - const logger = loggerFactory('growi:routes:apiv3:import'); // eslint-disable-line no-unused-vars const path = require('path'); @@ -20,7 +21,6 @@ const path = require('path'); const express = require('express'); const multer = require('multer'); - const router = express.Router(); /** @@ -126,7 +126,7 @@ const router = express.Router(); * type: integer * nullable: true */ -export default function route(crowi: Crowi): void { +export default function route(crowi: Crowi): Router { const { growiBridgeService, socketIoService } = crowi; const importService = getImportService(); @@ -201,22 +201,35 @@ export default function route(crowi: Crowi): void { * type: string * description: the access token of qiita.com */ - router.get('/', accessTokenParser([SCOPE.READ.ADMIN.IMPORT_DATA], { acceptLegacy: true }), loginRequired, adminRequired, async(req, res) => { - try { - const importSettingsParams = { - esaTeamName: await crowi.configManager.getConfig('importer:esa:team_name'), - esaAccessToken: await crowi.configManager.getConfig('importer:esa:access_token'), - qiitaTeamName: await crowi.configManager.getConfig('importer:qiita:team_name'), - qiitaAccessToken: await crowi.configManager.getConfig('importer:qiita:access_token'), - }; - return res.apiv3({ - importSettingsParams, - }); - } - catch (err) { - return res.apiv3Err(err, 500); - } - }); + router.get( + '/', + accessTokenParser([SCOPE.READ.ADMIN.IMPORT_DATA], { acceptLegacy: true }), + loginRequired, + adminRequired, + async (req, res) => { + try { + const importSettingsParams = { + esaTeamName: await crowi.configManager.getConfig( + 'importer:esa:team_name', + ), + esaAccessToken: await crowi.configManager.getConfig( + 'importer:esa:access_token', + ), + qiitaTeamName: await crowi.configManager.getConfig( + 'importer:qiita:team_name', + ), + qiitaAccessToken: await crowi.configManager.getConfig( + 'importer:qiita:access_token', + ), + }; + return res.apiv3({ + importSettingsParams, + }); + } catch (err) { + return res.apiv3Err(err, 500); + } + }, + ); /** * @swagger @@ -239,15 +252,20 @@ export default function route(crowi: Crowi): void { * status: * $ref: '#/components/schemas/ImportStatus' */ - router.get('/status', accessTokenParser([SCOPE.READ.ADMIN.IMPORT_DATA], { acceptLegacy: true }), loginRequired, adminRequired, async(req, res) => { - try { - const status = await importService.getStatus(); - return res.apiv3(status); - } - catch (err) { - return res.apiv3Err(err, 500); - } - }); + router.get( + '/status', + accessTokenParser([SCOPE.READ.ADMIN.IMPORT_DATA], { acceptLegacy: true }), + loginRequired, + adminRequired, + async (req, res) => { + try { + const status = await importService.getStatus(); + return res.apiv3(status); + } catch (err) { + return res.apiv3Err(err, 500); + } + }, + ); /** * @swagger @@ -286,103 +304,131 @@ export default function route(crowi: Crowi): void { * 200: * description: Import process has requested */ - router.post('/', accessTokenParser([SCOPE.WRITE.ADMIN.IMPORT_DATA], { acceptLegacy: true }), loginRequired, adminRequired, addActivity, async(req, res) => { - // TODO: add express validator - const { fileName, collections, options } = req.body; - - // pages collection can only be imported by upsert if isV5Compatible is true - const isV5Compatible = crowi.configManager.getConfig('app:isV5Compatible'); - const isImportPagesCollection = collections.includes('pages'); - if (isV5Compatible && isImportPagesCollection) { - /** @type {ImportOptionForPages} */ - const option = options.find(opt => opt.collectionName === 'pages'); - if (option.mode !== 'upsert') { - return res.apiv3Err(new ErrorV3('Upsert is only available for importing pages collection.', 'only_upsert_available')); + router.post( + '/', + accessTokenParser([SCOPE.WRITE.ADMIN.IMPORT_DATA], { acceptLegacy: true }), + loginRequired, + adminRequired, + addActivity, + async (req, res) => { + // TODO: add express validator + const { fileName, collections, options } = req.body; + + // pages collection can only be imported by upsert if isV5Compatible is true + const isV5Compatible = + crowi.configManager.getConfig('app:isV5Compatible'); + const isImportPagesCollection = collections.includes('pages'); + if (isV5Compatible && isImportPagesCollection) { + /** @type {ImportOptionForPages} */ + const option = options.find((opt) => opt.collectionName === 'pages'); + if (option.mode !== 'upsert') { + return res.apiv3Err( + new ErrorV3( + 'Upsert is only available for importing pages collection.', + 'only_upsert_available', + ), + ); + } } - } - - const isMaintenanceMode = crowi.appService.isMaintenanceMode(); - if (!isMaintenanceMode) { - return res.apiv3Err(new ErrorV3('GROWI is not maintenance mode. To import data, please activate the maintenance mode first.', 'not_maintenance_mode')); - } + const isMaintenanceMode = crowi.appService.isMaintenanceMode(); + if (!isMaintenanceMode) { + return res.apiv3Err( + new ErrorV3( + 'GROWI is not maintenance mode. To import data, please activate the maintenance mode first.', + 'not_maintenance_mode', + ), + ); + } - const zipFile = importService.getFile(fileName); - - // return response first - res.apiv3(); + const zipFile = importService.getFile(fileName); - /* - * unzip, parse - */ - let meta; - let fileStatsToImport; - try { - // unzip - await importService.unzip(zipFile); + // return response first + res.apiv3(); - // eslint-disable-next-line no-unused-vars - const parseZipResult = await growiBridgeService.parseZipFile(zipFile); - if (parseZipResult == null) { - throw new Error('parseZipFile returns null'); + /* + * unzip, parse + */ + let meta: object; + let fileStatsToImport: { + fileName: string; + collectionName: string; + size: number; + }[]; + try { + // unzip + await importService.unzip(zipFile); + + // eslint-disable-next-line no-unused-vars + const parseZipResult = await growiBridgeService.parseZipFile(zipFile); + if (parseZipResult == null) { + throw new Error('parseZipFile returns null'); + } + + meta = parseZipResult.meta; + + // filter innerFileStats + fileStatsToImport = parseZipResult.innerFileStats.filter( + ({ collectionName }) => { + return collections.includes(collectionName); + }, + ); + } catch (err) { + logger.error(err); + adminEvent.emit('onErrorForImport', { message: err.message }); + return; } - meta = parseZipResult.meta; + /* + * validate with meta.json + */ + try { + importService.validate(meta); + } catch (err) { + logger.error(err); + adminEvent.emit('onErrorForImport', { message: err.message }); + return; + } - // filter innerFileStats - fileStatsToImport = parseZipResult.innerFileStats.filter(({ collectionName }) => { - return collections.includes(collectionName); + // generate maps of ImportSettings to import + // Use the Map for a potential fix for the code scanning alert no. 895: Prototype-polluting assignment + const importSettingsMap = new Map(); + fileStatsToImport.forEach(({ fileName, collectionName }) => { + // instanciate GrowiArchiveImportOption + const option: GrowiArchiveImportOption = options.find( + (opt) => opt.collectionName === collectionName, + ); + + // generate options + const importSettings = { + mode: option.mode, + jsonFileName: fileName, + overwriteParams: generateOverwriteParams( + collectionName, + req.user._id, + option, + ), + } satisfies ImportSettings; + + importSettingsMap.set(collectionName, importSettings); }); - } - catch (err) { - logger.error(err); - adminEvent.emit('onErrorForImport', { message: err.message }); - return; - } - - /* - * validate with meta.json - */ - try { - importService.validate(meta); - } - catch (err) { - logger.error(err); - adminEvent.emit('onErrorForImport', { message: err.message }); - return; - } - - // generate maps of ImportSettings to import - // Use the Map for a potential fix for the code scanning alert no. 895: Prototype-polluting assignment - const importSettingsMap = new Map(); - fileStatsToImport.forEach(({ fileName, collectionName }) => { - // instanciate GrowiArchiveImportOption - const option: GrowiArchiveImportOption = options.find(opt => opt.collectionName === collectionName); - - // generate options - const importSettings = { - mode: option.mode, - jsonFileName: fileName, - overwriteParams: generateOverwriteParams(collectionName, req.user._id, option), - } satisfies ImportSettings; - - importSettingsMap.set(collectionName, importSettings); - }); - - /* - * import - */ - try { - importService.import(collections, importSettingsMap); - - const parameters = { action: SupportedAction.ACTION_ADMIN_GROWI_DATA_IMPORTED }; - activityEvent.emit('update', res.locals.activity._id, parameters); - } - catch (err) { - logger.error(err); - adminEvent.emit('onErrorForImport', { message: err.message }); - } - }); + + /* + * import + */ + try { + importService.import(collections, importSettingsMap); + + const parameters = { + action: SupportedAction.ACTION_ADMIN_GROWI_DATA_IMPORTED, + }; + activityEvent.emit('update', res.locals.activity._id, parameters); + } catch (err) { + logger.error(err); + adminEvent.emit('onErrorForImport', { message: err.message }); + } + }, + ); /** * @swagger @@ -412,36 +458,43 @@ export default function route(crowi: Crowi): void { * schema: * $ref: '#/components/schemas/FileImportResponse' */ - router.post('/upload', - accessTokenParser([SCOPE.WRITE.ADMIN.IMPORT_DATA], { acceptLegacy: true }), loginRequired, adminRequired, uploads.single('file'), addActivity, - async(req, res) => { + router.post( + '/upload', + accessTokenParser([SCOPE.WRITE.ADMIN.IMPORT_DATA], { acceptLegacy: true }), + loginRequired, + adminRequired, + uploads.single('file'), + addActivity, + async (req, res) => { const { file } = req; const zipFile = importService.getFile(file.filename); - let data; + let data: ZipFileStat | null; try { data = await growiBridgeService.parseZipFile(zipFile); - } - catch (err) { - // TODO: use ApiV3Error + } catch (err) { + // TODO: use ApiV3Error logger.error(err); return res.status(500).send({ status: 'ERROR' }); } try { - // validate with meta.json - importService.validate(data.meta); + // validate with meta.json + importService.validate(data?.meta); - const parameters = { action: SupportedAction.ACTION_ADMIN_ARCHIVE_DATA_UPLOAD }; + const parameters = { + action: SupportedAction.ACTION_ADMIN_ARCHIVE_DATA_UPLOAD, + }; activityEvent.emit('update', res.locals.activity._id, parameters); return res.apiv3(data); - } - catch { - const msg = 'The version of this GROWI and the uploaded GROWI data are not the same'; + } catch { + const msg = + 'The version of this GROWI and the uploaded GROWI data are not the same'; const validationErr = 'versions-are-not-met'; return res.apiv3Err(new ErrorV3(msg, validationErr), 500); } - }); + }, + ); /** * @swagger @@ -458,17 +511,22 @@ export default function route(crowi: Crowi): void { * 200: * description: all files are deleted */ - router.delete('/all', accessTokenParser([SCOPE.WRITE.ADMIN.IMPORT_DATA], { acceptLegacy: true }), loginRequired, adminRequired, async(req, res) => { - try { - importService.deleteAllZipFiles(); - - return res.apiv3(); - } - catch (err) { - logger.error(err); - return res.apiv3Err(err, 500); - } - }); + router.delete( + '/all', + accessTokenParser([SCOPE.WRITE.ADMIN.IMPORT_DATA], { acceptLegacy: true }), + loginRequired, + adminRequired, + async (req, res) => { + try { + importService.deleteAllZipFiles(); + + return res.apiv3(); + } catch (err) { + logger.error(err); + return res.apiv3Err(err, 500); + } + }, + ); return router; } diff --git a/apps/app/src/server/routes/apiv3/in-app-notification.ts b/apps/app/src/server/routes/apiv3/in-app-notification.ts index f3a3621899f..51de912f4ac 100644 --- a/apps/app/src/server/routes/apiv3/in-app-notification.ts +++ b/apps/app/src/server/routes/apiv3/in-app-notification.ts @@ -1,17 +1,15 @@ +import { SCOPE } from '@growi/core/dist/interfaces'; import { serializeUserSecurely } from '@growi/core/dist/models/serializers'; import express from 'express'; import { SupportedAction } from '~/interfaces/activity'; import type { CrowiRequest } from '~/interfaces/crowi-request'; -import { SCOPE } from '@growi/core/dist/interfaces'; import { accessTokenParser } from '~/server/middlewares/access-token-parser'; import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity'; import type { IInAppNotification } from '../../../interfaces/in-app-notification'; - import type { ApiV3Response } from './interfaces/apiv3-response'; - const router = express.Router(); /** @@ -88,7 +86,9 @@ const router = express.Router(); */ /** @param {import('~/server/crowi').default} crowi Crowi instance */ module.exports = (crowi) => { - const loginRequiredStrictly = require('../../middlewares/login-required')(crowi); + const loginRequiredStrictly = require('../../middlewares/login-required')( + crowi, + ); const addActivity = generateAddActivityMiddleware(); const inAppNotificationService = crowi.inAppNotificationService; @@ -97,7 +97,6 @@ module.exports = (crowi) => { const activityEvent = crowi.event('activity'); - /** * @swagger * @@ -133,15 +132,21 @@ module.exports = (crowi) => { * schema: * $ref: '#/components/schemas/InAppNotificationListResponse' */ - router.get('/list', accessTokenParser([SCOPE.READ.USER_SETTINGS.IN_APP_NOTIFICATION], { acceptLegacy: true }), loginRequiredStrictly, - async(req: CrowiRequest, res: ApiV3Response) => { - // user must be set by loginRequiredStrictly - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + router.get( + '/list', + accessTokenParser([SCOPE.READ.USER_SETTINGS.IN_APP_NOTIFICATION], { + acceptLegacy: true, + }), + loginRequiredStrictly, + async (req: CrowiRequest, res: ApiV3Response) => { + // user must be set by loginRequiredStrictly + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const user = req.user!; - const limit = req.query.limit != null - ? parseInt(req.query.limit.toString()) || 10 - : 10; + const limit = + req.query.limit != null + ? parseInt(req.query.limit.toString()) || 10 + : 10; let offset = 0; if (req.query.offset != null) { @@ -158,37 +163,42 @@ module.exports = (crowi) => { Object.assign(queryOptions, { status: req.query.status }); } - const paginationResult = await inAppNotificationService.getLatestNotificationsByUser(user._id, queryOptions); - - - const getActionUsersFromActivities = function(activities) { - return activities.map(({ user }) => user).filter((user, i, self) => self.indexOf(user) === i); - }; - - const serializedDocs: Array = paginationResult.docs.map((doc) => { - if (doc.user != null && doc.user instanceof User) { - doc.user = serializeUserSecurely(doc.user); - } - // To add a new property into mongoose doc, need to change the format of doc to an object - const docObj: IInAppNotification = doc.toObject(); - const actionUsersNew = getActionUsersFromActivities(doc.activities); - - const serializedActionUsers = actionUsersNew.map((actionUser) => { - return serializeUserSecurely(actionUser); + const paginationResult = + await inAppNotificationService.getLatestNotificationsByUser( + user._id, + queryOptions, + ); + + const getActionUsersFromActivities = (activities) => + activities + .map(({ user }) => user) + .filter((user, i, self) => self.indexOf(user) === i); + + const serializedDocs: Array = + paginationResult.docs.map((doc) => { + if (doc.user != null && doc.user instanceof User) { + doc.user = serializeUserSecurely(doc.user); + } + // To add a new property into mongoose doc, need to change the format of doc to an object + const docObj: IInAppNotification = doc.toObject(); + const actionUsersNew = getActionUsersFromActivities(doc.activities); + + const serializedActionUsers = actionUsersNew.map((actionUser) => { + return serializeUserSecurely(actionUser); + }); + + docObj.actionUsers = serializedActionUsers; + return docObj; }); - docObj.actionUsers = serializedActionUsers; - return docObj; - }); - const serializedPaginationResult = { ...paginationResult, docs: serializedDocs, }; return res.apiv3(serializedPaginationResult); - }); - + }, + ); /** * @swagger @@ -212,20 +222,27 @@ module.exports = (crowi) => { * type: integer * description: Count of unread notifications */ - router.get('/status', accessTokenParser([SCOPE.READ.USER_SETTINGS.IN_APP_NOTIFICATION], { acceptLegacy: true }), loginRequiredStrictly, - async(req: CrowiRequest, res: ApiV3Response) => { - // user must be set by loginRequiredStrictly - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + router.get( + '/status', + accessTokenParser([SCOPE.READ.USER_SETTINGS.IN_APP_NOTIFICATION], { + acceptLegacy: true, + }), + loginRequiredStrictly, + async (req: CrowiRequest, res: ApiV3Response) => { + // user must be set by loginRequiredStrictly + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const user = req.user!; try { - const count = await inAppNotificationService.getUnreadCountByUser(user._id); + const count = await inAppNotificationService.getUnreadCountByUser( + user._id, + ); return res.apiv3({ count }); - } - catch (err) { + } catch (err) { return res.apiv3Err(err); } - }); + }, + ); /** * @swagger @@ -256,10 +273,15 @@ module.exports = (crowi) => { * schema: * type: object */ - router.post('/open', accessTokenParser([SCOPE.WRITE.USER_SETTINGS.IN_APP_NOTIFICATION], { acceptLegacy: true }), loginRequiredStrictly, - async(req: CrowiRequest, res: ApiV3Response) => { - // user must be set by loginRequiredStrictly - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + router.post( + '/open', + accessTokenParser([SCOPE.WRITE.USER_SETTINGS.IN_APP_NOTIFICATION], { + acceptLegacy: true, + }), + loginRequiredStrictly, + async (req: CrowiRequest, res: ApiV3Response) => { + // user must be set by loginRequiredStrictly + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const user = req.user!; const id = req.body.id; @@ -268,11 +290,11 @@ module.exports = (crowi) => { const notification = await inAppNotificationService.open(user, id); const result = { notification }; return res.apiv3(result); - } - catch (err) { + } catch (err) { return res.apiv3Err(err); } - }); + }, + ); /** * @swagger @@ -289,24 +311,31 @@ module.exports = (crowi) => { * 200: * description: All notifications opened successfully */ - router.put('/all-statuses-open', - accessTokenParser([SCOPE.WRITE.USER_SETTINGS.IN_APP_NOTIFICATION], { acceptLegacy: true }), loginRequiredStrictly, addActivity, - async(req: CrowiRequest, res: ApiV3Response) => { - // user must be set by loginRequiredStrictly - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + router.put( + '/all-statuses-open', + accessTokenParser([SCOPE.WRITE.USER_SETTINGS.IN_APP_NOTIFICATION], { + acceptLegacy: true, + }), + loginRequiredStrictly, + addActivity, + async (req: CrowiRequest, res: ApiV3Response) => { + // user must be set by loginRequiredStrictly + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const user = req.user!; try { await inAppNotificationService.updateAllNotificationsAsOpened(user); - activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_IN_APP_NOTIFICATION_ALL_STATUSES_OPEN }); + activityEvent.emit('update', res.locals.activity._id, { + action: SupportedAction.ACTION_IN_APP_NOTIFICATION_ALL_STATUSES_OPEN, + }); return res.apiv3(); - } - catch (err) { + } catch (err) { return res.apiv3Err(err); } - }); + }, + ); return router; }; diff --git a/apps/app/src/server/routes/apiv3/installer.ts b/apps/app/src/server/routes/apiv3/installer.ts index ffa10b912f5..3dca5133ae1 100644 --- a/apps/app/src/server/routes/apiv3/installer.ts +++ b/apps/app/src/server/routes/apiv3/installer.ts @@ -1,3 +1,4 @@ +import type { IUser } from '@growi/core'; import { ErrorV3 } from '@growi/core/dist/models'; import type { Request, Router } from 'express'; import express from 'express'; @@ -9,17 +10,20 @@ import loggerFactory from '~/utils/logger'; import type Crowi from '../../crowi'; import { generateAddActivityMiddleware } from '../../middlewares/add-activity'; import * as applicationNotInstalled from '../../middlewares/application-not-installed'; -import { registerRules, registerValidation } from '../../middlewares/register-form-validator'; -import { InstallerService, FailedToCreateAdminUserError } from '../../service/installer'; - +import { + registerRules, + registerValidation, +} from '../../middlewares/register-form-validator'; +import { + FailedToCreateAdminUserError, + InstallerService, +} from '../../service/installer'; import type { ApiV3Response } from './interfaces/apiv3-response'; - // eslint-disable-next-line @typescript-eslint/no-unused-vars const logger = loggerFactory('growi:routes:apiv3:installer'); - -type FormRequest = Request & { form: any, logIn: any }; +type FormRequest = Request & { form: any; logIn: any }; module.exports = (crowi: Crowi): Router => { const addActivity = generateAddActivityMiddleware(); @@ -78,53 +82,68 @@ module.exports = (crowi: Crowi): Router => { * example: Installation completed (Logged in as an admin user) */ // eslint-disable-next-line max-len - router.post('/', registerRules(minPasswordLength), registerValidation, addActivity, async(req: FormRequest, res: ApiV3Response) => { - - if (!req.form.isValid) { - const errors = req.form.errors; - return res.apiv3Err(errors, 400); - } - - const registerForm = req.body.registerForm || {}; - - const name = registerForm.name; - const username = registerForm.username; - const email = registerForm.email; - const password = registerForm.password; - const language = registerForm['app:globalLang'] || 'en_US'; - - const installerService = new InstallerService(crowi); - - let adminUser; - try { - adminUser = await installerService.install({ - name, - username, - email, - password, - }, language); - } - catch (err) { - if (err instanceof FailedToCreateAdminUserError) { - return res.apiv3Err(new ErrorV3(err.message, 'failed_to_create_admin_user')); + router.post( + '/', + registerRules(minPasswordLength), + registerValidation, + addActivity, + async (req: FormRequest, res: ApiV3Response) => { + if (!req.form.isValid) { + const errors = req.form.errors; + return res.apiv3Err(errors, 400); } - return res.apiv3Err(new ErrorV3(err, 'failed_to_install')); - } - await crowi.appService.setupAfterInstall(); - - const parameters = { action: SupportedAction.ACTION_USER_REGISTRATION_SUCCESS }; - activityEvent.emit('update', res.locals.activity._id, parameters); - - // login with passport - req.logIn(adminUser, (err) => { - if (err != null) { - return res.apiv3Err(new ErrorV3(err, 'failed_to_login_after_install')); + const registerForm = req.body.registerForm || {}; + + const name = registerForm.name; + const username = registerForm.username; + const email = registerForm.email; + const password = registerForm.password; + const language = registerForm['app:globalLang'] || 'en_US'; + + const installerService = new InstallerService(crowi); + + let adminUser: IUser; + try { + adminUser = await installerService.install( + { + name, + username, + email, + password, + }, + language, + ); + } catch (err) { + if (err instanceof FailedToCreateAdminUserError) { + return res.apiv3Err( + new ErrorV3(err.message, 'failed_to_create_admin_user'), + ); + } + return res.apiv3Err(new ErrorV3(err, 'failed_to_install')); } - return res.apiv3({ message: 'Installation completed (Logged in as an admin user)' }); - }); - }); + await crowi.appService.setupAfterInstall(); + + const parameters = { + action: SupportedAction.ACTION_USER_REGISTRATION_SUCCESS, + }; + activityEvent.emit('update', res.locals.activity._id, parameters); + + // login with passport + req.logIn(adminUser, (err) => { + if (err != null) { + return res.apiv3Err( + new ErrorV3(err, 'failed_to_login_after_install'), + ); + } + + return res.apiv3({ + message: 'Installation completed (Logged in as an admin user)', + }); + }); + }, + ); return router; }; diff --git a/apps/app/src/server/routes/apiv3/invited.ts b/apps/app/src/server/routes/apiv3/invited.ts index e89d657dc35..6241567312e 100644 --- a/apps/app/src/server/routes/apiv3/invited.ts +++ b/apps/app/src/server/routes/apiv3/invited.ts @@ -6,16 +6,19 @@ import mongoose from 'mongoose'; import loggerFactory from '~/utils/logger'; import type Crowi from '../../crowi'; -import { invitedRules, invitedValidation } from '../../middlewares/invited-form-validator'; - +import { + invitedRules, + invitedValidation, +} from '../../middlewares/invited-form-validator'; import type { ApiV3Response } from './interfaces/apiv3-response'; const logger = loggerFactory('growi:routes:login'); -type InvitedFormRequest = Request & { form: any, user: any }; +type InvitedFormRequest = Request & { form: any; user: any }; module.exports = (crowi: Crowi): Router => { - const applicationInstalled = require('../../middlewares/application-installed')(crowi); + const applicationInstalled = + require('../../middlewares/application-installed')(crowi); const router = express.Router(); /** @@ -59,44 +62,53 @@ module.exports = (crowi: Crowi): Router => { * type: string * description: URL to redirect after successful activation. */ - router.post('/', applicationInstalled, invitedRules(), invitedValidation, async(req: InvitedFormRequest, res: ApiV3Response) => { - if (!req.user) { - return res.apiv3({ redirectTo: '/login' }); - } + router.post( + '/', + applicationInstalled, + invitedRules(), + invitedValidation, + async (req: InvitedFormRequest, res: ApiV3Response) => { + if (!req.user) { + return res.apiv3({ redirectTo: '/login' }); + } - if (!req.form.isValid) { - return res.apiv3Err(req.form.errors, 400); - } + if (!req.form.isValid) { + return res.apiv3Err(req.form.errors, 400); + } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const User = mongoose.model('User'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const User = mongoose.model('User'); - const user = req.user; - const invitedForm = req.form.invitedForm || {}; - const username = invitedForm.username; - const name = invitedForm.name; - const password = invitedForm.password; + const user = req.user; + const invitedForm = req.form.invitedForm || {}; + const username = invitedForm.username; + const name = invitedForm.name; + const password = invitedForm.password; - // check user upper limit - const isUserCountExceedsUpperLimit = await User.isUserCountExceedsUpperLimit(); - if (isUserCountExceedsUpperLimit) { - return res.apiv3Err('message.can_not_activate_maximum_number_of_users', 403); - } + // check user upper limit + const isUserCountExceedsUpperLimit = + await User.isUserCountExceedsUpperLimit(); + if (isUserCountExceedsUpperLimit) { + return res.apiv3Err( + 'message.can_not_activate_maximum_number_of_users', + 403, + ); + } - const creatable = await User.isRegisterableUsername(username); - if (!creatable) { - logger.debug('username', username); - return res.apiv3Err('message.unable_to_use_this_user', 403); - } + const creatable = await User.isRegisterableUsername(username); + if (!creatable) { + logger.debug('username', username); + return res.apiv3Err('message.unable_to_use_this_user', 403); + } - try { - await user.activateInvitedUser(username, name, password); - return res.apiv3({ redirectTo: '/' }); - } - catch (err) { - return res.apiv3Err('message.failed_to_activate', 403); - } - }); + try { + await user.activateInvitedUser(username, name, password); + return res.apiv3({ redirectTo: '/' }); + } catch (err) { + return res.apiv3Err('message.failed_to_activate', 403); + } + }, + ); return router; }; diff --git a/apps/app/src/server/routes/apiv3/page-listing.ts b/apps/app/src/server/routes/apiv3/page-listing.ts index 277539098f0..0de7c0d207b 100644 --- a/apps/app/src/server/routes/apiv3/page-listing.ts +++ b/apps/app/src/server/routes/apiv3/page-listing.ts @@ -1,12 +1,10 @@ -import type { - IPageInfoForListing, IPageInfo, IUserHasId, -} from '@growi/core'; +import type { IPageInfo, IPageInfoForListing, IUserHasId } from '@growi/core'; import { getIdForRef, isIPageInfoForEntity } from '@growi/core'; import { SCOPE } from '@growi/core/dist/interfaces'; import { ErrorV3 } from '@growi/core/dist/models'; import type { Request, Router } from 'express'; import express from 'express'; -import { query, oneOf } from 'express-validator'; +import { oneOf, query } from 'express-validator'; import type { HydratedDocument } from 'mongoose'; import mongoose from 'mongoose'; @@ -20,39 +18,37 @@ import loggerFactory from '~/utils/logger'; import type Crowi from '../../crowi'; import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator'; import type { PageDocument, PageModel } from '../../models/page'; - import type { ApiV3Response } from './interfaces/apiv3-response'; - const logger = loggerFactory('growi:routes:apiv3:page-tree'); /* * Types & Interfaces */ interface AuthorizedRequest extends Request { - user?: IUserHasId, + user?: IUserHasId; } /* * Validators */ const validator = { - pagePathRequired: [ - query('path').isString().withMessage('path is required'), - ], - pageIdOrPathRequired: oneOf([ - query('id').isMongoId(), - query('path').isString(), - ], 'id or path is required'), + pagePathRequired: [query('path').isString().withMessage('path is required')], + pageIdOrPathRequired: oneOf( + [query('id').isMongoId(), query('path').isString()], + 'id or path is required', + ), pageIdsOrPathRequired: [ // type check independent of existence check - query('pageIds').isArray().optional(), + query('pageIds') + .isArray() + .optional(), query('path').isString().optional(), // existence check - oneOf([ - query('pageIds').exists(), - query('path').exists(), - ], 'pageIds or path is required'), + oneOf( + [query('pageIds').exists(), query('path').exists()], + 'pageIds or path is required', + ), ], infoParams: [ query('attachBookmarkCount').isBoolean().optional(), @@ -64,11 +60,13 @@ const validator = { * Routes */ const routerFactory = (crowi: Crowi): Router => { - const loginRequired = require('../../middlewares/login-required')(crowi, true); + const loginRequired = require('../../middlewares/login-required')( + crowi, + true, + ); const router = express.Router(); - /** * @swagger * @@ -91,16 +89,20 @@ const routerFactory = (crowi: Crowi): Router => { * rootPage: * $ref: '#/components/schemas/PageForTreeItem' */ - router.get('/root', - accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }), loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => { + router.get( + '/root', + accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }), + loginRequired, + async (req: AuthorizedRequest, res: ApiV3Response) => { try { - const rootPage: IPageForTreeItem = await pageListingService.findRootByViewer(req.user); + const rootPage: IPageForTreeItem = + await pageListingService.findRootByViewer(req.user); return res.apiv3({ rootPage }); - } - catch (err) { + } catch (err) { return res.apiv3Err(new ErrorV3('rootPage not found')); } - }); + }, + ); /** * @swagger @@ -138,25 +140,39 @@ const routerFactory = (crowi: Crowi): Router => { /* * In most cases, using id should be prioritized */ - router.get('/children', + router.get( + '/children', accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }), - loginRequired, validator.pageIdOrPathRequired, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response) => { + loginRequired, + validator.pageIdOrPathRequired, + apiV3FormValidator, + async (req: AuthorizedRequest, res: ApiV3Response) => { const { id, path } = req.query; - const hideRestrictedByOwner = await configManager.getConfig('security:list-policy:hideRestrictedByOwner'); - const hideRestrictedByGroup = await configManager.getConfig('security:list-policy:hideRestrictedByGroup'); + const hideRestrictedByOwner = await configManager.getConfig( + 'security:list-policy:hideRestrictedByOwner', + ); + const hideRestrictedByGroup = await configManager.getConfig( + 'security:list-policy:hideRestrictedByGroup', + ); try { - const pages = await pageListingService.findChildrenByParentPathOrIdAndViewer( - (id || path) as string, req.user, !hideRestrictedByOwner, !hideRestrictedByGroup, - ); + const pages = + await pageListingService.findChildrenByParentPathOrIdAndViewer( + (id || path) as string, + req.user, + !hideRestrictedByOwner, + !hideRestrictedByGroup, + ); return res.apiv3({ children: pages }); - } - catch (err) { + } catch (err) { logger.error('Error occurred while finding children.', err); - return res.apiv3Err(new ErrorV3('Error occurred while finding children.')); + return res.apiv3Err( + new ErrorV3('Error occurred while finding children.'), + ); } - }); + }, + ); /** * @swagger @@ -200,17 +216,26 @@ const routerFactory = (crowi: Crowi): Router => { * additionalProperties: * $ref: '#/components/schemas/PageInfoAll' */ - router.get('/info', + router.get( + '/info', accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }), - validator.pageIdsOrPathRequired, validator.infoParams, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response) => { + validator.pageIdsOrPathRequired, + validator.infoParams, + apiV3FormValidator, + async (req: AuthorizedRequest, res: ApiV3Response) => { const { - pageIds, path, attachBookmarkCount: attachBookmarkCountParam, attachShortBody: attachShortBodyParam, + pageIds, + path, + attachBookmarkCount: attachBookmarkCountParam, + attachShortBody: attachShortBodyParam, } = req.query; const attachBookmarkCount: boolean = attachBookmarkCountParam === 'true'; const attachShortBody: boolean = attachShortBodyParam === 'true'; - const Page = mongoose.model, PageModel>('Page'); + const Page = mongoose.model, PageModel>( + 'Page', + ); // eslint-disable-next-line @typescript-eslint/no-explicit-any const Bookmark = mongoose.model('Bookmark'); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -219,32 +244,55 @@ const routerFactory = (crowi: Crowi): Router => { const pageGrantService: IPageGrantService = crowi.pageGrantService!; try { - const pages = pageIds != null - ? await Page.findByIdsAndViewer(pageIds as string[], req.user, null, true) - : await Page.findByPathAndViewer(path as string, req.user, null, false, true); + const pages = + pageIds != null + ? await Page.findByIdsAndViewer( + pageIds as string[], + req.user, + null, + true, + ) + : await Page.findByPathAndViewer( + path as string, + req.user, + null, + false, + true, + ); - const foundIds = pages.map(page => page._id); + const foundIds = pages.map((page) => page._id); - let shortBodiesMap; + let shortBodiesMap: Record | undefined; if (attachShortBody) { - shortBodiesMap = await pageService.shortBodiesMapByPageIds(foundIds, req.user); + shortBodiesMap = await pageService.shortBodiesMapByPageIds( + foundIds, + req.user, + ); } - let bookmarkCountMap; + let bookmarkCountMap: Record | undefined; if (attachBookmarkCount) { - bookmarkCountMap = await Bookmark.getPageIdToCountMap(foundIds) as Record; + bookmarkCountMap = (await Bookmark.getPageIdToCountMap( + foundIds, + )) as Record; } - const idToPageInfoMap: Record = {}; + const idToPageInfoMap: Record = + {}; const isGuestUser = req.user == null; - const userRelatedGroups = await pageGrantService.getUserRelatedGroups(req.user); + const userRelatedGroups = await pageGrantService.getUserRelatedGroups( + req.user, + ); for (const page of pages) { const basicPageInfo = { ...pageService.constructBasicPageInfo(page, isGuestUser), - bookmarkCount: bookmarkCountMap != null ? bookmarkCountMap[page._id.toString()] ?? 0 : 0, + bookmarkCount: + bookmarkCountMap != null + ? (bookmarkCountMap[page._id.toString()] ?? 0) + : 0, }; // TODO: use pageService.getCreatorIdForCanDelete to get creatorId (https://redmine.weseek.co.jp/issues/140574) @@ -256,24 +304,29 @@ const routerFactory = (crowi: Crowi): Router => { userRelatedGroups, ); // use normal delete config - const pageInfo = (!isIPageInfoForEntity(basicPageInfo)) + const pageInfo = !isIPageInfoForEntity(basicPageInfo) ? basicPageInfo - : { - ...basicPageInfo, - isAbleToDeleteCompletely: canDeleteCompletely, - revisionShortBody: shortBodiesMap != null ? shortBodiesMap[page._id.toString()] : undefined, - } satisfies IPageInfoForListing; + : ({ + ...basicPageInfo, + isAbleToDeleteCompletely: canDeleteCompletely, + revisionShortBody: + shortBodiesMap != null + ? (shortBodiesMap[page._id.toString()] ?? undefined) + : undefined, + } satisfies IPageInfoForListing); idToPageInfoMap[page._id.toString()] = pageInfo; } return res.apiv3(idToPageInfoMap); - } - catch (err) { + } catch (err) { logger.error('Error occurred while fetching page informations.', err); - return res.apiv3Err(new ErrorV3('Error occurred while fetching page informations.')); + return res.apiv3Err( + new ErrorV3('Error occurred while fetching page informations.'), + ); } - }); + }, + ); return router; }; diff --git a/apps/app/src/server/routes/apiv3/user-activation.ts b/apps/app/src/server/routes/apiv3/user-activation.ts index b46c9296f14..f3942ddb27e 100644 --- a/apps/app/src/server/routes/apiv3/user-activation.ts +++ b/apps/app/src/server/routes/apiv3/user-activation.ts @@ -1,10 +1,9 @@ -import path from 'path'; - import type { IUser } from '@growi/core'; import { ErrorV3 } from '@growi/core/dist/models'; import { format, subSeconds } from 'date-fns'; import { body, validationResult } from 'express-validator'; import mongoose from 'mongoose'; +import path from 'path'; import { SupportedAction } from '~/interfaces/activity'; import { RegistrationMode } from '~/interfaces/registration-mode'; @@ -34,7 +33,9 @@ export const completeRegistrationRules = () => { .matches(/^[\x20-\x7F]*$/) .withMessage('Password has invalid character') .isLength({ min: PASSOWRD_MINIMUM_NUMBER }) - .withMessage('Password minimum character should be more than 8 characters') + .withMessage( + 'Password minimum character should be more than 8 characters', + ) .not() .isEmpty() .withMessage('Password field is required'), @@ -49,12 +50,19 @@ export const validateCompleteRegistration = (req, res, next) => { } const extractedErrors: string[] = []; - errors.array().map(err => extractedErrors.push(err.msg)); + errors.array().map((err) => extractedErrors.push(err.msg)); return res.apiv3Err(extractedErrors); }; -async function sendEmailToAllAdmins(userData, admins, appTitle, mailService, template, url) { +async function sendEmailToAllAdmins( + userData, + admins, + appTitle, + mailService, + template, + url, +) { admins.map((admin) => { return mailService.send({ to: admin.email, @@ -110,29 +118,47 @@ async function sendEmailToAllAdmins(userData, admins, appTitle, mailService, tem * type: string */ export const completeRegistrationAction = (crowi: Crowi) => { - const User = mongoose.model('User'); + const User = mongoose.model< + IUser, + { isEmailValid; isRegisterable; createUserByEmailAndPassword; findAdmins } + >('User'); const activityEvent = crowi.event('activity'); - const { - aclService, - appService, - mailService, - } = crowi; + const { aclService, appService, mailService } = crowi; - return async function(req, res) { + return async (req, res) => { const { t } = await getTranslation(); if (req.user != null) { - return res.apiv3Err(new ErrorV3('You have been logged in', 'registration-failed'), 403); + return res.apiv3Err( + new ErrorV3('You have been logged in', 'registration-failed'), + 403, + ); } // error when registration is not allowed - if (configManager.getConfig('security:registrationMode') === aclService.labels.SECURITY_REGISTRATION_MODE_CLOSED) { - return res.apiv3Err(new ErrorV3('Registration closed', 'registration-failed'), 403); + if ( + configManager.getConfig('security:registrationMode') === + aclService.labels.SECURITY_REGISTRATION_MODE_CLOSED + ) { + return res.apiv3Err( + new ErrorV3('Registration closed', 'registration-failed'), + 403, + ); } // error when email authentication is disabled - if (configManager.getConfig('security:passport-local:isEmailAuthenticationEnabled') !== true) { - return res.apiv3Err(new ErrorV3('Email authentication configuration is disabled', 'registration-failed'), 403); + if ( + configManager.getConfig( + 'security:passport-local:isEmailAuthenticationEnabled', + ) !== true + ) { + return res.apiv3Err( + new ErrorV3( + 'Email authentication configuration is disabled', + 'registration-failed', + ), + 403, + ); } const { userRegistrationOrder } = req; @@ -162,65 +188,94 @@ export const completeRegistrationAction = (crowi: Crowi) => { } } if (isError) { - return res.apiv3Err(new ErrorV3(errorMessage, 'registration-failed'), 403); + return res.apiv3Err( + new ErrorV3(errorMessage, 'registration-failed'), + 403, + ); } - User.createUserByEmailAndPassword(name, username, email, password, undefined, async(err, userData) => { - if (err) { - if (err.name === 'UserUpperLimitException') { - errorMessage = t('message.can_not_register_maximum_number_of_users'); - } - else { - errorMessage = t('message.failed_to_register'); - } - return res.apiv3Err(new ErrorV3(errorMessage, 'registration-failed'), 403); - } - - const parameters = { action: SupportedAction.ACTION_USER_REGISTRATION_SUCCESS }; - activityEvent.emit('update', res.locals.activity._id, parameters); - - userRegistrationOrder.revokeOneTimeToken(); - - if (configManager.getConfig('security:registrationMode') === aclService.labels.SECURITY_REGISTRATION_MODE_RESTRICTED) { - const isMailerSetup = mailService.isMailerSetup ?? false; - - if (isMailerSetup) { - const admins = await User.findAdmins(); - const appTitle = appService.getAppTitle(); - const locale = configManager.getConfig('app:globalLang'); - const template = path.join(crowi.localeDir, `${locale}/admin/userWaitingActivation.ejs`); - const url = growiInfoService.getSiteUrl(); - - sendEmailToAllAdmins(userData, admins, appTitle, mailService, template, url); - } - // This 'completeRegistrationAction' should not be able to be called if the email settings is not set up in the first place. - // So this method dows not stop processing as an error, but only displays a warning. -- 2022.11.01 Yuki Takei - else { - logger.warn('E-mail Settings must be set up.'); - } - - return res.apiv3({}); - } - - req.login(userData, (err) => { + User.createUserByEmailAndPassword( + name, + username, + email, + password, + undefined, + async (err, userData) => { if (err) { - logger.debug(err); + if (err.name === 'UserUpperLimitException') { + errorMessage = t( + 'message.can_not_register_maximum_number_of_users', + ); + } else { + errorMessage = t('message.failed_to_register'); + } + return res.apiv3Err( + new ErrorV3(errorMessage, 'registration-failed'), + 403, + ); } - else { - // update lastLoginAt - userData.updateLastLoginAt(new Date(), (err) => { - if (err) { - logger.error(`updateLastLoginAt dumps error: ${err}`); - } - }); + + const parameters = { + action: SupportedAction.ACTION_USER_REGISTRATION_SUCCESS, + }; + activityEvent.emit('update', res.locals.activity._id, parameters); + + userRegistrationOrder.revokeOneTimeToken(); + + if ( + configManager.getConfig('security:registrationMode') === + aclService.labels.SECURITY_REGISTRATION_MODE_RESTRICTED + ) { + const isMailerSetup = mailService.isMailerSetup ?? false; + + if (isMailerSetup) { + const admins = await User.findAdmins(); + const appTitle = appService.getAppTitle(); + const locale = configManager.getConfig('app:globalLang'); + const template = path.join( + crowi.localeDir, + `${locale}/admin/userWaitingActivation.ejs`, + ); + const url = growiInfoService.getSiteUrl(); + + sendEmailToAllAdmins( + userData, + admins, + appTitle, + mailService, + template, + url, + ); + } + // This 'completeRegistrationAction' should not be able to be called if the email settings is not set up in the first place. + // So this method dows not stop processing as an error, but only displays a warning. -- 2022.11.01 Yuki Takei + else { + logger.warn('E-mail Settings must be set up.'); + } + + return res.apiv3({}); } - // userData.password can't be empty but, prepare redirect because password property in User Model is optional - // https://github.com/growilabs/growi/pull/6670 - const redirectTo = userData.password != null ? '/' : '/me#password_settings'; - return res.apiv3({ redirectTo }); - }); - }); + req.login(userData, (err) => { + if (err) { + logger.debug(err); + } else { + // update lastLoginAt + userData.updateLastLoginAt(new Date(), (err) => { + if (err) { + logger.error(`updateLastLoginAt dumps error: ${err}`); + } + }); + } + + // userData.password can't be empty but, prepare redirect because password property in User Model is optional + // https://github.com/growilabs/growi/pull/6670 + const redirectTo = + userData.password != null ? '/' : '/me#password_settings'; + return res.apiv3({ redirectTo }); + }); + }, + ); }); }; }; @@ -244,17 +299,13 @@ export const validateRegisterForm = (req, res, next) => { } const extractedErrors: string[] = []; - errors.array().map(err => extractedErrors.push(err.msg)); + errors.array().map((err) => extractedErrors.push(err.msg)); return res.apiv3Err(extractedErrors, 400); }; async function makeRegistrationEmailToken(email, crowi: Crowi) { - const { - mailService, - localeDir, - appService, - } = crowi; + const { mailService, localeDir, appService } = crowi; const isMailerSetup = mailService.isMailerSetup ?? false; if (!isMailerSetup) { @@ -264,17 +315,24 @@ async function makeRegistrationEmailToken(email, crowi: Crowi) { const locale = configManager.getConfig('app:globalLang'); const appUrl = growiInfoService.getSiteUrl(); - const userRegistrationOrder = await UserRegistrationOrder.createUserRegistrationOrder(email); + const userRegistrationOrder = + await UserRegistrationOrder.createUserRegistrationOrder(email); const grwTzoffsetSec = crowi.appService.getTzoffset() * 60; const expiredAt = subSeconds(userRegistrationOrder.expiredAt, grwTzoffsetSec); const formattedExpiredAt = format(expiredAt, 'yyyy/MM/dd HH:mm'); - const url = new URL(`/user-activation/${userRegistrationOrder.token}`, appUrl); + const url = new URL( + `/user-activation/${userRegistrationOrder.token}`, + appUrl, + ); const oneTimeUrl = url.href; return mailService.send({ to: email, subject: '[GROWI] User Activation', - template: path.join(localeDir, `${locale}/notifications/userActivation.ejs`), + template: path.join( + localeDir, + `${locale}/notifications/userActivation.ejs`, + ), vars: { appTitle: appService.getAppTitle(), email, @@ -285,13 +343,17 @@ async function makeRegistrationEmailToken(email, crowi: Crowi) { } export const registerAction = (crowi: Crowi) => { - const User = mongoose.model('User'); + const User = mongoose.model( + 'User', + ); - return async function(req, res) { + return async (req, res) => { const registerForm = req.body.registerForm || {}; const email = registerForm.email; const isRegisterableEmail = await User.isRegisterableEmail(email); - const registrationMode = configManager.getConfig('security:registrationMode'); + const registrationMode = configManager.getConfig( + 'security:registrationMode', + ); const isEmailValid = await User.isEmailValid(email); if (registrationMode === RegistrationMode.CLOSED) { @@ -309,8 +371,7 @@ export const registerAction = (crowi: Crowi) => { try { await makeRegistrationEmailToken(email, crowi); - } - catch (err) { + } catch (err) { return res.apiv3Err(err); } diff --git a/apps/app/src/server/routes/apiv3/user-activities.ts b/apps/app/src/server/routes/apiv3/user-activities.ts index cae069a4872..1b05cb6a90e 100644 --- a/apps/app/src/server/routes/apiv3/user-activities.ts +++ b/apps/app/src/server/routes/apiv3/user-activities.ts @@ -3,7 +3,7 @@ import { serializeUserSecurely } from '@growi/core/dist/models/serializers'; import type { Request, Router } from 'express'; import express from 'express'; import { query } from 'express-validator'; -import type { PipelineStage, PaginateResult } from 'mongoose'; +import type { PaginateResult, PipelineStage } from 'mongoose'; import { Types } from 'mongoose'; import type { IActivity } from '~/interfaces/activity'; @@ -12,22 +12,32 @@ import Activity from '~/server/models/activity'; import { configManager } from '~/server/service/config-manager'; import loggerFactory from '~/utils/logger'; - import type Crowi from '../../crowi'; import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator'; - import type { ApiV3Response } from './interfaces/apiv3-response'; const logger = loggerFactory('growi:routes:apiv3:activity'); const validator = { list: [ - query('limit').optional().isInt({ max: 100 }).withMessage('limit must be a number less than or equal to 100') + query('limit') + .optional() + .isInt({ max: 100 }) + .withMessage('limit must be a number less than or equal to 100') .toInt(), - query('offset').optional().isInt().withMessage('page must be a number') + query('offset') + .optional() + .isInt() + .withMessage('page must be a number') .toInt(), - query('searchFilter').optional().isString().withMessage('query must be a string'), - query('targetUserId').optional().isMongoId().withMessage('user ID must be a MongoDB ID'), + query('searchFilter') + .optional() + .isString() + .withMessage('query must be a string'), + query('targetUserId') + .optional() + .isMongoId() + .withMessage('user ID must be a MongoDB ID'), ], }; @@ -41,17 +51,16 @@ interface StrictActivityQuery { type CustomRequest< TQuery = Request['query'], TBody = any, - TParams = any + TParams = any, > = Omit, 'query'> & { - query: TQuery & Request['query']; - user?: IUserHasId; + query: TQuery & Request['query']; + user?: IUserHasId; }; type AuthorizedRequest = CustomRequest; type ActivityPaginationResult = PaginateResult; - /** * @swagger * @@ -134,7 +143,9 @@ type ActivityPaginationResult = PaginateResult; */ module.exports = (crowi: Crowi): Router => { - const loginRequiredStrictly = require('../../middlewares/login-required')(crowi); + const loginRequiredStrictly = require('../../middlewares/login-required')( + crowi, + ); const router = express.Router(); @@ -173,10 +184,15 @@ module.exports = (crowi: Crowi): Router => { * schema: * $ref: '#/components/schemas/ActivityResponse' */ - router.get('/', - loginRequiredStrictly, validator.list, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response) => { - - const defaultLimit = configManager.getConfig('customize:showPageLimitationS'); + router.get( + '/', + loginRequiredStrictly, + validator.list, + apiV3FormValidator, + async (req: AuthorizedRequest, res: ApiV3Response) => { + const defaultLimit = configManager.getConfig( + 'customize:showPageLimitationS', + ); const limit = req.query.limit || defaultLimit || 10; const offset = req.query.offset || 0; @@ -187,10 +203,12 @@ module.exports = (crowi: Crowi): Router => { } if (!targetUserId) { - return res.apiv3Err('Target user ID is missing and authenticated user ID is unavailable.', 400); + return res.apiv3Err( + 'Target user ID is missing and authenticated user ID is unavailable.', + 400, + ); } - try { const userObjectId = new Types.ObjectId(targetUserId); @@ -203,9 +221,7 @@ module.exports = (crowi: Crowi): Router => { }, { $facet: { - totalCount: [ - { $count: 'count' }, - ], + totalCount: [{ $count: 'count' }], docs: [ { $sort: { createdAt: -1 } }, { $skip: offset }, @@ -256,7 +272,8 @@ module.exports = (crowi: Crowi): Router => { }, ]; - const [activityResults] = await Activity.aggregate(userActivityPipeline); + const [activityResults] = + await Activity.aggregate(userActivityPipeline); const serializedResults = activityResults.docs.map((doc: IActivity) => { const { user, ...rest } = doc; @@ -266,7 +283,10 @@ module.exports = (crowi: Crowi): Router => { }; }); - const totalDocs = activityResults.totalCount.length > 0 ? activityResults.totalCount[0].count : 0; + const totalDocs = + activityResults.totalCount.length > 0 + ? activityResults.totalCount[0].count + : 0; const totalPages = Math.ceil(totalDocs / limit); const page = Math.floor(offset / limit) + 1; @@ -289,12 +309,12 @@ module.exports = (crowi: Crowi): Router => { }; return res.apiv3({ serializedPaginationResult }); - } - catch (err) { + } catch (err) { logger.error('Failed to get paginated activity', err); return res.apiv3Err(err, 500); } - }); + }, + ); return router; }; diff --git a/apps/app/src/server/routes/apiv3/user-ui-settings.ts b/apps/app/src/server/routes/apiv3/user-ui-settings.ts index f7416db6b04..e93fd96ed4a 100644 --- a/apps/app/src/server/routes/apiv3/user-ui-settings.ts +++ b/apps/app/src/server/routes/apiv3/user-ui-settings.ts @@ -13,10 +13,13 @@ const logger = loggerFactory('growi:routes:apiv3:user-ui-settings'); const router = express.Router(); module.exports = () => { - const validatorForPut = [ - body('settings').exists().withMessage('The body param \'settings\' is required'), - body('settings.currentSidebarContents').optional().isIn(AllSidebarContentsType), + body('settings') + .exists() + .withMessage("The body param 'settings' is required"), + body('settings.currentSidebarContents') + .optional() + .isIn(AllSidebarContentsType), body('settings.currentProductNavWidth').optional().isNumeric(), body('settings.preferCollapsedModeByUser').optional().isBoolean(), ]; @@ -67,55 +70,58 @@ module.exports = () => { * type: boolean */ // eslint-disable-next-line @typescript-eslint/no-explicit-any - router.put('/', validatorForPut, apiV3FormValidator, async(req: any, res: any) => { - const { user } = req; - const { settings } = req.body; + router.put( + '/', + validatorForPut, + apiV3FormValidator, + async (req: any, res: any) => { + const { user } = req; + const { settings } = req.body; - // extract only necessary params - const updateData = { - currentSidebarContents: settings.currentSidebarContents, - currentProductNavWidth: settings.currentProductNavWidth, - preferCollapsedModeByUser: settings.preferCollapsedModeByUser, - }; + // extract only necessary params + const updateData = { + currentSidebarContents: settings.currentSidebarContents, + currentProductNavWidth: settings.currentProductNavWidth, + preferCollapsedModeByUser: settings.preferCollapsedModeByUser, + }; - if (user == null) { - if (req.session.uiSettings == null) { - req.session.uiSettings = {}; + if (user == null) { + if (req.session.uiSettings == null) { + req.session.uiSettings = {}; + } + Object.keys(updateData).forEach((setting) => { + if (updateData[setting] != null) { + req.session.uiSettings[setting] = updateData[setting]; + } + }); + return res.apiv3(updateData); } - Object.keys(updateData).forEach((setting) => { - if (updateData[setting] != null) { - req.session.uiSettings[setting] = updateData[setting]; + + // remove the keys that have null value + Object.keys(updateData).forEach((key) => { + if (updateData[key] == null) { + delete updateData[key]; } }); - return res.apiv3(updateData); - } - - // remove the keys that have null value - Object.keys(updateData).forEach((key) => { - if (updateData[key] == null) { - delete updateData[key]; - } - }); - - try { - const updatedSettings = await UserUISettings.findOneAndUpdate( - { user: user._id }, - { - $set: { - user: user._id, - ...updateData, + try { + const updatedSettings = await UserUISettings.findOneAndUpdate( + { user: user._id }, + { + $set: { + user: user._id, + ...updateData, + }, }, - }, - { upsert: true, new: true }, - ); - return res.apiv3(updatedSettings); - } - catch (err) { - logger.error('Error', err); - return res.apiv3Err(new ErrorV3(err)); - } - }); + { upsert: true, new: true }, + ); + return res.apiv3(updatedSettings); + } catch (err) { + logger.error('Error', err); + return res.apiv3Err(new ErrorV3(err)); + } + }, + ); return router; }; diff --git a/biome.json b/biome.json index 93675e850bb..62eb8828cdf 100644 --- a/biome.json +++ b/biome.json @@ -31,7 +31,6 @@ "!apps/app/src/client", "!apps/app/src/server/middlewares", "!apps/app/src/server/routes/apiv3/*.js", - "!apps/app/src/server/routes/apiv3/*.ts", "!apps/app/src/server/service" ] },