admin.js 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401
  1. /* eslint-disable no-use-before-define */
  2. module.exports = function(crowi, app) {
  3. const debug = require('debug')('growi:routes:admin');
  4. const logger = require('@alias/logger')('growi:routes:admin');
  5. const models = crowi.models;
  6. const Page = models.Page;
  7. const User = models.User;
  8. const ExternalAccount = models.ExternalAccount;
  9. const UserGroup = models.UserGroup;
  10. const UserGroupRelation = models.UserGroupRelation;
  11. const GlobalNotificationSetting = models.GlobalNotificationSetting;
  12. const GlobalNotificationMailSetting = models.GlobalNotificationMailSetting;
  13. const GlobalNotificationSlackSetting = models.GlobalNotificationSlackSetting; // eslint-disable-line no-unused-vars
  14. const {
  15. configManager,
  16. aclService,
  17. slackNotificationService,
  18. customizeService,
  19. } = crowi;
  20. const recommendedWhitelist = require('@commons/service/xss/recommended-whitelist');
  21. const PluginUtils = require('../plugins/plugin-utils');
  22. const ApiResponse = require('../util/apiResponse');
  23. const importer = require('../util/importer')(crowi);
  24. const searchEvent = crowi.event('search');
  25. const pluginUtils = new PluginUtils();
  26. const MAX_PAGE_LIST = 50;
  27. const actions = {};
  28. function createPager(total, limit, page, pagesCount, maxPageList) {
  29. const pager = {
  30. page,
  31. pagesCount,
  32. pages: [],
  33. total,
  34. previous: null,
  35. previousDots: false,
  36. next: null,
  37. nextDots: false,
  38. };
  39. if (page > 1) {
  40. pager.previous = page - 1;
  41. }
  42. if (page < pagesCount) {
  43. pager.next = page + 1;
  44. }
  45. let pagerMin = Math.max(1, Math.ceil(page - maxPageList / 2));
  46. let pagerMax = Math.min(pagesCount, Math.floor(page + maxPageList / 2));
  47. if (pagerMin === 1) {
  48. if (MAX_PAGE_LIST < pagesCount) {
  49. pagerMax = MAX_PAGE_LIST;
  50. }
  51. else {
  52. pagerMax = pagesCount;
  53. }
  54. }
  55. if (pagerMax === pagesCount) {
  56. if ((pagerMax - MAX_PAGE_LIST) < 1) {
  57. pagerMin = 1;
  58. }
  59. else {
  60. pagerMin = pagerMax - MAX_PAGE_LIST;
  61. }
  62. }
  63. pager.previousDots = null;
  64. if (pagerMin > 1) {
  65. pager.previousDots = true;
  66. }
  67. pager.nextDots = null;
  68. if (pagerMax < pagesCount) {
  69. pager.nextDots = true;
  70. }
  71. for (let i = pagerMin; i <= pagerMax; i++) {
  72. pager.pages.push(i);
  73. }
  74. return pager;
  75. }
  76. actions.index = function(req, res) {
  77. return res.render('admin/index', {
  78. plugins: pluginUtils.listPlugins(crowi.rootDir),
  79. });
  80. };
  81. // app.get('/admin/app' , admin.app.index);
  82. actions.app = {};
  83. actions.app.index = function(req, res) {
  84. return res.render('admin/app');
  85. };
  86. actions.app.settingUpdate = function(req, res) {
  87. };
  88. // app.get('/admin/security' , admin.security.index);
  89. actions.security = {};
  90. actions.security.index = function(req, res) {
  91. return res.render('admin/security');
  92. };
  93. // app.get('/admin/markdown' , admin.markdown.index);
  94. actions.markdown = {};
  95. actions.markdown.index = function(req, res) {
  96. const markdownSetting = configManager.getConfigByPrefix('markdown', 'markdown:');
  97. return res.render('admin/markdown', {
  98. markdownSetting,
  99. recommendedWhitelist,
  100. });
  101. };
  102. // app.post('/admin/markdown/lineBreaksSetting' , admin.markdown.lineBreaksSetting);
  103. actions.markdown.lineBreaksSetting = async function(req, res) {
  104. const markdownSetting = req.form.markdownSetting;
  105. if (req.form.isValid) {
  106. await configManager.updateConfigsInTheSameNamespace('markdown', markdownSetting);
  107. req.flash('successMessage', ['Successfully updated!']);
  108. }
  109. else {
  110. req.flash('errorMessage', req.form.errors);
  111. }
  112. return res.redirect('/admin/markdown');
  113. };
  114. // app.post('/admin/markdown/presentationSetting' , admin.markdown.presentationSetting);
  115. actions.markdown.presentationSetting = async function(req, res) {
  116. const markdownSetting = req.form.markdownSetting;
  117. if (req.form.isValid) {
  118. await configManager.updateConfigsInTheSameNamespace('markdown', markdownSetting);
  119. req.flash('successMessage', ['Successfully updated!']);
  120. }
  121. else {
  122. req.flash('errorMessage', req.form.errors);
  123. }
  124. return res.redirect('/admin/markdown');
  125. };
  126. // app.post('/admin/markdown/xss-setting' , admin.markdown.xssSetting);
  127. actions.markdown.xssSetting = async function(req, res) {
  128. const xssSetting = req.form.markdownSetting;
  129. xssSetting['markdown:xss:tagWhiteList'] = csvToArray(xssSetting['markdown:xss:tagWhiteList']);
  130. xssSetting['markdown:xss:attrWhiteList'] = csvToArray(xssSetting['markdown:xss:attrWhiteList']);
  131. if (req.form.isValid) {
  132. await configManager.updateConfigsInTheSameNamespace('markdown', xssSetting);
  133. req.flash('successMessage', ['Successfully updated!']);
  134. }
  135. else {
  136. req.flash('errorMessage', req.form.errors);
  137. }
  138. return res.redirect('/admin/markdown');
  139. };
  140. const csvToArray = (string) => {
  141. const array = string.split(',');
  142. return array.map((item) => { return item.trim() });
  143. };
  144. // app.get('/admin/customize' , admin.customize.index);
  145. actions.customize = {};
  146. actions.customize.index = function(req, res) {
  147. const settingForm = configManager.getConfigByPrefix('crowi', 'customize:');
  148. /* eslint-disable quote-props, no-multi-spaces */
  149. const highlightJsCssSelectorOptions = {
  150. 'github': { name: '[Light] GitHub', border: false },
  151. 'github-gist': { name: '[Light] GitHub Gist', border: true },
  152. 'atom-one-light': { name: '[Light] Atom One Light', border: true },
  153. 'xcode': { name: '[Light] Xcode', border: true },
  154. 'vs': { name: '[Light] Vs', border: true },
  155. 'atom-one-dark': { name: '[Dark] Atom One Dark', border: false },
  156. 'hybrid': { name: '[Dark] Hybrid', border: false },
  157. 'monokai': { name: '[Dark] Monokai', border: false },
  158. 'tomorrow-night': { name: '[Dark] Tomorrow Night', border: false },
  159. 'vs2015': { name: '[Dark] Vs 2015', border: false },
  160. };
  161. /* eslint-enable quote-props, no-multi-spaces */
  162. return res.render('admin/customize', {
  163. settingForm,
  164. highlightJsCssSelectorOptions,
  165. });
  166. };
  167. // app.get('/admin/notification' , admin.notification.index);
  168. actions.notification = {};
  169. actions.notification.index = async(req, res) => {
  170. const UpdatePost = crowi.model('UpdatePost');
  171. let slackSetting = configManager.getConfigByPrefix('notification', 'slack:');
  172. const hasSlackIwhUrl = !!configManager.getConfig('notification', 'slack:incomingWebhookUrl');
  173. const hasSlackToken = !!configManager.getConfig('notification', 'slack:token');
  174. if (!hasSlackIwhUrl) {
  175. slackSetting['slack:incomingWebhookUrl'] = '';
  176. }
  177. if (req.session.slackSetting) {
  178. slackSetting = req.session.slackSetting;
  179. req.session.slackSetting = null;
  180. }
  181. const globalNotifications = await GlobalNotificationSetting.findAll();
  182. const userNotifications = await UpdatePost.findAll();
  183. return res.render('admin/notification', {
  184. userNotifications,
  185. slackSetting,
  186. hasSlackIwhUrl,
  187. hasSlackToken,
  188. globalNotifications,
  189. });
  190. };
  191. // app.post('/admin/notification/slackSetting' , admin.notification.slackauth);
  192. actions.notification.slackSetting = async function(req, res) {
  193. const slackSetting = req.form.slackSetting;
  194. if (req.form.isValid) {
  195. await configManager.updateConfigsInTheSameNamespace('notification', slackSetting);
  196. req.flash('successMessage', ['Successfully Updated!']);
  197. // Re-setup
  198. crowi.setupSlack().then(() => {
  199. });
  200. }
  201. else {
  202. req.flash('errorMessage', req.form.errors);
  203. }
  204. return res.redirect('/admin/notification');
  205. };
  206. // app.get('/admin/notification/slackAuth' , admin.notification.slackauth);
  207. actions.notification.slackAuth = function(req, res) {
  208. const code = req.query.code;
  209. if (!code || !slackNotificationService.hasSlackConfig()) {
  210. return res.redirect('/admin/notification');
  211. }
  212. const slack = crowi.slack;
  213. slack.getOauthAccessToken(code)
  214. .then(async(data) => {
  215. debug('oauth response', data);
  216. try {
  217. await configManager.updateConfigsInTheSameNamespace('notification', { 'slack:token': data.access_token });
  218. req.flash('successMessage', ['Successfully Connected!']);
  219. }
  220. catch (err) {
  221. req.flash('errorMessage', ['Failed to save access_token. Please try again.']);
  222. }
  223. return res.redirect('/admin/notification');
  224. })
  225. .catch((err) => {
  226. debug('oauth response ERROR', err);
  227. req.flash('errorMessage', ['Failed to fetch access_token. Please do connect again.']);
  228. return res.redirect('/admin/notification');
  229. });
  230. };
  231. // app.post('/admin/notification/slackIwhSetting' , admin.notification.slackIwhSetting);
  232. actions.notification.slackIwhSetting = async function(req, res) {
  233. const slackIwhSetting = req.form.slackIwhSetting;
  234. if (req.form.isValid) {
  235. await configManager.updateConfigsInTheSameNamespace('notification', slackIwhSetting);
  236. req.flash('successMessage', ['Successfully Updated!']);
  237. // Re-setup
  238. crowi.setupSlack().then(() => {
  239. return res.redirect('/admin/notification#slack-incoming-webhooks');
  240. });
  241. }
  242. else {
  243. req.flash('errorMessage', req.form.errors);
  244. return res.redirect('/admin/notification#slack-incoming-webhooks');
  245. }
  246. };
  247. // app.post('/admin/notification/slackSetting/disconnect' , admin.notification.disconnectFromSlack);
  248. actions.notification.disconnectFromSlack = async function(req, res) {
  249. await configManager.updateConfigsInTheSameNamespace('notification', { 'slack:token': '' });
  250. req.flash('successMessage', ['Successfully Disconnected!']);
  251. return res.redirect('/admin/notification');
  252. };
  253. actions.globalNotification = {};
  254. actions.globalNotification.detail = async(req, res) => {
  255. const notificationSettingId = req.params.id;
  256. const renderVars = {};
  257. if (notificationSettingId) {
  258. try {
  259. renderVars.setting = await GlobalNotificationSetting.findOne({ _id: notificationSettingId });
  260. }
  261. catch (err) {
  262. logger.error(`Error in finding a global notification setting with {_id: ${notificationSettingId}}`);
  263. }
  264. }
  265. return res.render('admin/global-notification-detail', renderVars);
  266. };
  267. actions.globalNotification.create = (req, res) => {
  268. const form = req.form.notificationGlobal;
  269. let setting;
  270. switch (form.notifyToType) {
  271. case 'mail':
  272. setting = new GlobalNotificationMailSetting(crowi);
  273. setting.toEmail = form.toEmail;
  274. break;
  275. // case 'slack':
  276. // setting = new GlobalNotificationSlackSetting(crowi);
  277. // setting.slackChannels = form.slackChannels;
  278. // break;
  279. default:
  280. logger.error('GlobalNotificationSetting Type Error: undefined type');
  281. req.flash('errorMessage', 'Error occurred in creating a new global notification setting: undefined notification type');
  282. return res.redirect('/admin/notification#global-notification');
  283. }
  284. setting.triggerPath = form.triggerPath;
  285. setting.triggerEvents = getNotificationEvents(form);
  286. setting.save();
  287. return res.redirect('/admin/notification#global-notification');
  288. };
  289. actions.globalNotification.update = async(req, res) => {
  290. const form = req.form.notificationGlobal;
  291. const setting = await GlobalNotificationSetting.findOne({ _id: form.id });
  292. switch (form.notifyToType) {
  293. case 'mail':
  294. setting.toEmail = form.toEmail;
  295. break;
  296. // case 'slack':
  297. // setting.slackChannels = form.slackChannels;
  298. // break;
  299. default:
  300. logger.error('GlobalNotificationSetting Type Error: undefined type');
  301. req.flash('errorMessage', 'Error occurred in updating the global notification setting: undefined notification type');
  302. return res.redirect('/admin/notification#global-notification');
  303. }
  304. setting.triggerPath = form.triggerPath;
  305. setting.triggerEvents = getNotificationEvents(form);
  306. setting.save();
  307. return res.redirect('/admin/notification#global-notification');
  308. };
  309. actions.globalNotification.remove = async(req, res) => {
  310. const id = req.params.id;
  311. try {
  312. await GlobalNotificationSetting.findOneAndRemove({ _id: id });
  313. return res.redirect('/admin/notification#global-notification');
  314. }
  315. catch (err) {
  316. req.flash('errorMessage', 'Error in deleting global notification setting');
  317. return res.redirect('/admin/notification#global-notification');
  318. }
  319. };
  320. const getNotificationEvents = (form) => {
  321. const triggerEvents = [];
  322. const triggerEventKeys = Object.keys(form).filter((key) => { return key.match(/^triggerEvent/) });
  323. triggerEventKeys.forEach((key) => {
  324. if (form[key]) {
  325. triggerEvents.push(form[key]);
  326. }
  327. });
  328. return triggerEvents;
  329. };
  330. actions.search = {};
  331. actions.search.index = function(req, res) {
  332. const search = crowi.getSearcher();
  333. if (!search) {
  334. return res.redirect('/admin');
  335. }
  336. return res.render('admin/search', {});
  337. };
  338. actions.user = {};
  339. actions.user.index = async function(req, res) {
  340. const activeUsers = await User.countListByStatus(User.STATUS_ACTIVE);
  341. const userUpperLimit = aclService.userUpperLimit();
  342. const isUserCountExceedsUpperLimit = await User.isUserCountExceedsUpperLimit();
  343. const page = parseInt(req.query.page) || 1;
  344. const result = await User.findUsersWithPagination({ page });
  345. const pager = createPager(result.total, result.limit, result.page, result.pages, MAX_PAGE_LIST);
  346. return res.render('admin/users', {
  347. users: result.docs,
  348. pager,
  349. activeUsers,
  350. userUpperLimit,
  351. isUserCountExceedsUpperLimit,
  352. });
  353. };
  354. actions.user.invite = function(req, res) {
  355. const form = req.form.inviteForm;
  356. const toSendEmail = form.sendEmail || false;
  357. if (req.form.isValid) {
  358. User.createUsersByInvitation(form.emailList.split('\n'), toSendEmail, (err, userList) => {
  359. if (err) {
  360. req.flash('errorMessage', req.form.errors.join('\n'));
  361. }
  362. else {
  363. req.flash('createdUser', userList);
  364. }
  365. return res.redirect('/admin/users');
  366. });
  367. }
  368. else {
  369. req.flash('errorMessage', req.form.errors.join('\n'));
  370. return res.redirect('/admin/users');
  371. }
  372. };
  373. actions.user.makeAdmin = function(req, res) {
  374. const id = req.params.id;
  375. User.findById(id, (err, userData) => {
  376. userData.makeAdmin((err, userData) => {
  377. if (err === null) {
  378. req.flash('successMessage', `${userData.name}さんのアカウントを管理者に設定しました。`);
  379. }
  380. else {
  381. req.flash('errorMessage', '更新に失敗しました。');
  382. debug(err, userData);
  383. }
  384. return res.redirect('/admin/users');
  385. });
  386. });
  387. };
  388. actions.user.removeFromAdmin = function(req, res) {
  389. const id = req.params.id;
  390. User.findById(id, (err, userData) => {
  391. userData.removeFromAdmin((err, userData) => {
  392. if (err === null) {
  393. req.flash('successMessage', `${userData.name}さんのアカウントを管理者から外しました。`);
  394. }
  395. else {
  396. req.flash('errorMessage', '更新に失敗しました。');
  397. debug(err, userData);
  398. }
  399. return res.redirect('/admin/users');
  400. });
  401. });
  402. };
  403. actions.user.activate = async function(req, res) {
  404. // check user upper limit
  405. const isUserCountExceedsUpperLimit = await User.isUserCountExceedsUpperLimit();
  406. if (isUserCountExceedsUpperLimit) {
  407. req.flash('errorMessage', 'ユーザーが上限に達したため有効化できません。');
  408. return res.redirect('/admin/users');
  409. }
  410. const id = req.params.id;
  411. User.findById(id, (err, userData) => {
  412. userData.statusActivate((err, userData) => {
  413. if (err === null) {
  414. req.flash('successMessage', `${userData.name}さんのアカウントを有効化しました`);
  415. }
  416. else {
  417. req.flash('errorMessage', '更新に失敗しました。');
  418. debug(err, userData);
  419. }
  420. return res.redirect('/admin/users');
  421. });
  422. });
  423. };
  424. actions.user.suspend = function(req, res) {
  425. const id = req.params.id;
  426. User.findById(id, (err, userData) => {
  427. userData.statusSuspend((err, userData) => {
  428. if (err === null) {
  429. req.flash('successMessage', `${userData.name}さんのアカウントを利用停止にしました`);
  430. }
  431. else {
  432. req.flash('errorMessage', '更新に失敗しました。');
  433. debug(err, userData);
  434. }
  435. return res.redirect('/admin/users');
  436. });
  437. });
  438. };
  439. actions.user.remove = function(req, res) {
  440. const id = req.params.id;
  441. let username = '';
  442. return new Promise((resolve, reject) => {
  443. User.findById(id, (err, userData) => {
  444. username = userData.username;
  445. return resolve(userData);
  446. });
  447. })
  448. .then((userData) => {
  449. return new Promise((resolve, reject) => {
  450. userData.statusDelete((err, userData) => {
  451. if (err) {
  452. reject(err);
  453. }
  454. resolve(userData);
  455. });
  456. });
  457. })
  458. .then((userData) => {
  459. // remove all External Accounts
  460. return ExternalAccount.remove({ user: userData }).then(() => { return userData });
  461. })
  462. .then((userData) => {
  463. return Page.removeByPath(`/user/${username}`).then(() => { return userData });
  464. })
  465. .then((userData) => {
  466. req.flash('successMessage', `${username} さんのアカウントを削除しました`);
  467. return res.redirect('/admin/users');
  468. })
  469. .catch((err) => {
  470. req.flash('errorMessage', '削除に失敗しました。');
  471. return res.redirect('/admin/users');
  472. });
  473. };
  474. // これやったときの relation の挙動未確認
  475. actions.user.removeCompletely = function(req, res) {
  476. // ユーザーの物理削除
  477. const id = req.params.id;
  478. User.removeCompletelyById(id, (err, removed) => {
  479. if (err) {
  480. debug('Error while removing user.', err, id);
  481. req.flash('errorMessage', '完全な削除に失敗しました。');
  482. }
  483. else {
  484. req.flash('successMessage', '削除しました');
  485. }
  486. return res.redirect('/admin/users');
  487. });
  488. };
  489. // app.post('/_api/admin/users.resetPassword' , admin.api.usersResetPassword);
  490. actions.user.resetPassword = function(req, res) {
  491. const id = req.body.user_id;
  492. const User = crowi.model('User');
  493. User.resetPasswordByRandomString(id)
  494. .then((data) => {
  495. data.user = User.filterToPublicFields(data.user);
  496. return res.json(ApiResponse.success(data));
  497. })
  498. .catch((err) => {
  499. debug('Error on reseting password', err);
  500. return res.json(ApiResponse.error('Error'));
  501. });
  502. };
  503. actions.externalAccount = {};
  504. actions.externalAccount.index = function(req, res) {
  505. const page = parseInt(req.query.page) || 1;
  506. ExternalAccount.findAllWithPagination({ page })
  507. .then((result) => {
  508. const pager = createPager(result.total, result.limit, result.page, result.pages, MAX_PAGE_LIST);
  509. return res.render('admin/external-accounts', {
  510. accounts: result.docs,
  511. pager,
  512. });
  513. });
  514. };
  515. actions.externalAccount.remove = async function(req, res) {
  516. const id = req.params.id;
  517. let account = null;
  518. try {
  519. account = await ExternalAccount.findByIdAndRemove(id);
  520. if (account == null) {
  521. throw new Error('削除に失敗しました。');
  522. }
  523. }
  524. catch (err) {
  525. req.flash('errorMessage', err.message);
  526. return res.redirect('/admin/users/external-accounts');
  527. }
  528. req.flash('successMessage', `外部アカウント '${account.providerType}/${account.accountId}' を削除しました`);
  529. return res.redirect('/admin/users/external-accounts');
  530. };
  531. actions.userGroup = {};
  532. actions.userGroup.index = function(req, res) {
  533. const page = parseInt(req.query.page) || 1;
  534. const isAclEnabled = aclService.getIsPublicWikiOnly();
  535. const renderVar = {
  536. userGroups: [],
  537. userGroupRelations: new Map(),
  538. pager: null,
  539. isAclEnabled,
  540. };
  541. UserGroup.findUserGroupsWithPagination({ page })
  542. .then((result) => {
  543. const pager = createPager(result.total, result.limit, result.page, result.pages, MAX_PAGE_LIST);
  544. const userGroups = result.docs;
  545. renderVar.userGroups = userGroups;
  546. renderVar.pager = pager;
  547. return userGroups.map((userGroup) => {
  548. return new Promise((resolve, reject) => {
  549. UserGroupRelation.findAllRelationForUserGroup(userGroup)
  550. .then((relations) => {
  551. return resolve([userGroup, relations]);
  552. });
  553. });
  554. });
  555. })
  556. .then((allRelationsPromise) => {
  557. return Promise.all(allRelationsPromise);
  558. })
  559. .then((relations) => {
  560. renderVar.userGroupRelations = new Map(relations);
  561. debug('in findUserGroupsWithPagination findAllRelationForUserGroupResult', renderVar.userGroupRelations);
  562. return res.render('admin/user-groups', renderVar);
  563. })
  564. .catch((err) => {
  565. debug('Error on find all relations', err);
  566. return res.json(ApiResponse.error('Error'));
  567. });
  568. };
  569. // グループ詳細
  570. actions.userGroup.detail = async function(req, res) {
  571. const userGroupId = req.params.id;
  572. const renderVar = {
  573. userGroup: null,
  574. userGroupRelations: [],
  575. notRelatedusers: [],
  576. relatedPages: [],
  577. };
  578. const userGroup = await UserGroup.findOne({ _id: userGroupId });
  579. if (userGroup == null) {
  580. logger.error('no userGroup is exists. ', userGroupId);
  581. req.flash('errorMessage', 'グループがありません');
  582. return res.redirect('/admin/user-groups');
  583. }
  584. renderVar.userGroup = userGroup;
  585. const resolves = await Promise.all([
  586. // get all user and group relations
  587. UserGroupRelation.findAllRelationForUserGroup(userGroup),
  588. // get all not related users for group
  589. UserGroupRelation.findUserByNotRelatedGroup(userGroup),
  590. // get all related pages
  591. Page.find({ grant: Page.GRANT_USER_GROUP, grantedGroup: { $in: [userGroup] } }),
  592. ]);
  593. renderVar.userGroupRelations = resolves[0];
  594. renderVar.notRelatedusers = resolves[1];
  595. renderVar.relatedPages = resolves[2];
  596. return res.render('admin/user-group-detail', renderVar);
  597. };
  598. // グループの生成
  599. actions.userGroup.create = function(req, res) {
  600. const form = req.form.createGroupForm;
  601. if (req.form.isValid) {
  602. const userGroupName = crowi.xss.process(form.userGroupName);
  603. UserGroup.createGroupByName(userGroupName)
  604. .then((newUserGroup) => {
  605. req.flash('successMessage', newUserGroup.name);
  606. req.flash('createdUserGroup', newUserGroup);
  607. return res.redirect('/admin/user-groups');
  608. })
  609. .catch((err) => {
  610. debug('create userGroup error:', err);
  611. req.flash('errorMessage', '同じグループ名が既に存在します。');
  612. });
  613. }
  614. else {
  615. req.flash('errorMessage', req.form.errors.join('\n'));
  616. return res.redirect('/admin/user-groups');
  617. }
  618. };
  619. //
  620. actions.userGroup.update = function(req, res) {
  621. const userGroupId = req.params.userGroupId;
  622. const name = crowi.xss.process(req.body.name);
  623. UserGroup.findById(userGroupId)
  624. .then((userGroupData) => {
  625. if (userGroupData == null) {
  626. req.flash('errorMessage', 'グループの検索に失敗しました。');
  627. return new Promise();
  628. }
  629. // 名前存在チェック
  630. return UserGroup.isRegisterableName(name)
  631. .then((isRegisterableName) => {
  632. // 既に存在するグループ名に更新しようとした場合はエラー
  633. if (!isRegisterableName) {
  634. req.flash('errorMessage', 'グループ名が既に存在します。');
  635. }
  636. else {
  637. return userGroupData.updateName(name)
  638. .then(() => {
  639. req.flash('successMessage', 'グループ名を更新しました。');
  640. })
  641. .catch((err) => {
  642. req.flash('errorMessage', 'グループ名の更新に失敗しました。');
  643. });
  644. }
  645. });
  646. })
  647. .then(() => {
  648. return res.redirect(`/admin/user-group-detail/${userGroupId}`);
  649. });
  650. };
  651. // app.post('/_api/admin/user-group/delete' , admin.userGroup.removeCompletely);
  652. actions.userGroup.removeCompletely = async(req, res) => {
  653. const { deleteGroupId, actionName, selectedGroupId } = req.body;
  654. try {
  655. await UserGroup.removeCompletelyById(deleteGroupId, actionName, selectedGroupId);
  656. req.flash('successMessage', '削除しました');
  657. }
  658. catch (err) {
  659. debug('Error while removing userGroup.', err, deleteGroupId);
  660. req.flash('errorMessage', '完全な削除に失敗しました。');
  661. }
  662. return res.redirect('/admin/user-groups');
  663. };
  664. actions.userGroupRelation = {};
  665. actions.userGroupRelation.index = function(req, res) {
  666. };
  667. actions.userGroupRelation.create = function(req, res) {
  668. const User = crowi.model('User');
  669. const UserGroup = crowi.model('UserGroup');
  670. const UserGroupRelation = crowi.model('UserGroupRelation');
  671. // req params
  672. const userName = req.body.user_name;
  673. const userGroupId = req.body.user_group_id;
  674. let user = null;
  675. let userGroup = null;
  676. Promise.all([
  677. // ユーザグループをIDで検索
  678. UserGroup.findById(userGroupId),
  679. // ユーザを名前で検索
  680. User.findUserByUsername(userName),
  681. ])
  682. .then((resolves) => {
  683. userGroup = resolves[0];
  684. user = resolves[1];
  685. // Relation を作成
  686. UserGroupRelation.createRelation(userGroup, user);
  687. })
  688. .then((result) => {
  689. return res.redirect(`/admin/user-group-detail/${userGroup.id}`);
  690. })
  691. .catch((err) => {
  692. debug('Error on create user-group relation', err);
  693. req.flash('errorMessage', 'Error on create user-group relation');
  694. return res.redirect(`/admin/user-group-detail/${userGroup.id}`);
  695. });
  696. };
  697. actions.userGroupRelation.remove = function(req, res) {
  698. const UserGroupRelation = crowi.model('UserGroupRelation');
  699. const userGroupId = req.params.id;
  700. const relationId = req.params.relationId;
  701. UserGroupRelation.removeById(relationId)
  702. .then(() => {
  703. return res.redirect(`/admin/user-group-detail/${userGroupId}`);
  704. })
  705. .catch((err) => {
  706. debug('Error on remove user-group-relation', err);
  707. req.flash('errorMessage', 'グループのユーザ削除に失敗しました。');
  708. });
  709. };
  710. // Importer management
  711. actions.importer = {};
  712. actions.importer.index = function(req, res) {
  713. const settingForm = configManager.getConfigByPrefix('crowi', 'importer:');
  714. return res.render('admin/importer', {
  715. settingForm,
  716. });
  717. };
  718. actions.api = {};
  719. actions.api.appSetting = async function(req, res) {
  720. const form = req.form.settingForm;
  721. if (req.form.isValid) {
  722. debug('form content', form);
  723. // mail setting ならここで validation
  724. if (form['mail:from']) {
  725. validateMailSetting(req, form, async(err, data) => {
  726. debug('Error validate mail setting: ', err, data);
  727. if (err) {
  728. req.form.errors.push('SMTPを利用したテストメール送信に失敗しました。設定をみなおしてください。');
  729. return res.json({ status: false, message: req.form.errors.join('\n') });
  730. }
  731. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  732. return res.json({ status: true });
  733. });
  734. }
  735. else {
  736. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  737. return res.json({ status: true });
  738. }
  739. }
  740. else {
  741. return res.json({ status: false, message: req.form.errors.join('\n') });
  742. }
  743. };
  744. actions.api.asyncAppSetting = async(req, res) => {
  745. const form = req.form.settingForm;
  746. if (!req.form.isValid) {
  747. return res.json({ status: false, message: req.form.errors.join('\n') });
  748. }
  749. debug('form content', form);
  750. try {
  751. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  752. return res.json({ status: true });
  753. }
  754. catch (err) {
  755. logger.error(err);
  756. return res.json({ status: false });
  757. }
  758. };
  759. actions.api.securitySetting = async function(req, res) {
  760. if (!req.form.isValid) {
  761. return res.json({ status: false, message: req.form.errors.join('\n') });
  762. }
  763. const form = req.form.settingForm;
  764. if (aclService.getIsPublicWikiOnly()) {
  765. const guestMode = form['security:restrictGuestMode'];
  766. if (guestMode === 'Deny') {
  767. req.form.errors.push('Private Wikiへの設定変更はできません。');
  768. return res.json({ status: false, message: req.form.errors.join('\n') });
  769. }
  770. }
  771. try {
  772. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  773. return res.json({ status: true });
  774. }
  775. catch (err) {
  776. logger.error(err);
  777. return res.json({ status: false });
  778. }
  779. };
  780. actions.api.securityPassportLdapSetting = function(req, res) {
  781. const form = req.form.settingForm;
  782. if (!req.form.isValid) {
  783. return res.json({ status: false, message: req.form.errors.join('\n') });
  784. }
  785. debug('form content', form);
  786. return configManager.updateConfigsInTheSameNamespace('crowi', form)
  787. .then(() => {
  788. // reset strategy
  789. crowi.passportService.resetLdapStrategy();
  790. // setup strategy
  791. if (configManager.getConfig('crowi', 'security:passport-ldap:isEnabled')) {
  792. crowi.passportService.setupLdapStrategy(true);
  793. }
  794. return;
  795. })
  796. .then(() => {
  797. res.json({ status: true });
  798. });
  799. };
  800. actions.api.securityPassportSamlSetting = async(req, res) => {
  801. const form = req.form.settingForm;
  802. validateSamlSettingForm(req.form, req.t);
  803. if (!req.form.isValid) {
  804. return res.json({ status: false, message: req.form.errors.join('\n') });
  805. }
  806. debug('form content', form);
  807. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  808. // reset strategy
  809. await crowi.passportService.resetSamlStrategy();
  810. // setup strategy
  811. if (configManager.getConfig('crowi', 'security:passport-saml:isEnabled')) {
  812. try {
  813. await crowi.passportService.setupSamlStrategy(true);
  814. }
  815. catch (err) {
  816. // reset
  817. await crowi.passportService.resetSamlStrategy();
  818. return res.json({ status: false, message: err.message });
  819. }
  820. }
  821. return res.json({ status: true });
  822. };
  823. actions.api.securityPassportBasicSetting = async(req, res) => {
  824. const form = req.form.settingForm;
  825. if (!req.form.isValid) {
  826. return res.json({ status: false, message: req.form.errors.join('\n') });
  827. }
  828. debug('form content', form);
  829. await saveSettingAsync(form);
  830. const config = await crowi.getConfig();
  831. // reset strategy
  832. await crowi.passportService.resetBasicStrategy();
  833. // setup strategy
  834. if (Config.isEnabledPassportBasic(config)) {
  835. try {
  836. await crowi.passportService.setupBasicStrategy(true);
  837. }
  838. catch (err) {
  839. // reset
  840. await crowi.passportService.resetBasicStrategy();
  841. return res.json({ status: false, message: err.message });
  842. }
  843. }
  844. return res.json({ status: true });
  845. };
  846. actions.api.securityPassportGoogleSetting = async(req, res) => {
  847. const form = req.form.settingForm;
  848. if (!req.form.isValid) {
  849. return res.json({ status: false, message: req.form.errors.join('\n') });
  850. }
  851. debug('form content', form);
  852. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  853. // reset strategy
  854. await crowi.passportService.resetGoogleStrategy();
  855. // setup strategy
  856. if (configManager.getConfig('crowi', 'security:passport-google:isEnabled')) {
  857. try {
  858. await crowi.passportService.setupGoogleStrategy(true);
  859. }
  860. catch (err) {
  861. // reset
  862. await crowi.passportService.resetGoogleStrategy();
  863. return res.json({ status: false, message: err.message });
  864. }
  865. }
  866. return res.json({ status: true });
  867. };
  868. actions.api.securityPassportGitHubSetting = async(req, res) => {
  869. const form = req.form.settingForm;
  870. if (!req.form.isValid) {
  871. return res.json({ status: false, message: req.form.errors.join('\n') });
  872. }
  873. debug('form content', form);
  874. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  875. // reset strategy
  876. await crowi.passportService.resetGitHubStrategy();
  877. // setup strategy
  878. if (configManager.getConfig('crowi', 'security:passport-github:isEnabled')) {
  879. try {
  880. await crowi.passportService.setupGitHubStrategy(true);
  881. }
  882. catch (err) {
  883. // reset
  884. await crowi.passportService.resetGitHubStrategy();
  885. return res.json({ status: false, message: err.message });
  886. }
  887. }
  888. return res.json({ status: true });
  889. };
  890. actions.api.securityPassportTwitterSetting = async(req, res) => {
  891. const form = req.form.settingForm;
  892. if (!req.form.isValid) {
  893. return res.json({ status: false, message: req.form.errors.join('\n') });
  894. }
  895. debug('form content', form);
  896. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  897. // reset strategy
  898. await crowi.passportService.resetTwitterStrategy();
  899. // setup strategy
  900. if (configManager.getConfig('crowi', 'security:passport-twitter:isEnabled')) {
  901. try {
  902. await crowi.passportService.setupTwitterStrategy(true);
  903. }
  904. catch (err) {
  905. // reset
  906. await crowi.passportService.resetTwitterStrategy();
  907. return res.json({ status: false, message: err.message });
  908. }
  909. }
  910. return res.json({ status: true });
  911. };
  912. actions.api.securityPassportOidcSetting = async(req, res) => {
  913. const form = req.form.settingForm;
  914. if (!req.form.isValid) {
  915. return res.json({ status: false, message: req.form.errors.join('\n') });
  916. }
  917. debug('form content', form);
  918. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  919. // reset strategy
  920. await crowi.passportService.resetOidcStrategy();
  921. // setup strategy
  922. if (configManager.getConfig('crowi', 'security:passport-oidc:isEnabled')) {
  923. try {
  924. await crowi.passportService.setupOidcStrategy(true);
  925. }
  926. catch (err) {
  927. // reset
  928. await crowi.passportService.resetOidcStrategy();
  929. return res.json({ status: false, message: err.message });
  930. }
  931. }
  932. return res.json({ status: true });
  933. };
  934. actions.api.customizeSetting = async function(req, res) {
  935. const form = req.form.settingForm;
  936. if (req.form.isValid) {
  937. debug('form content', form);
  938. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  939. customizeService.initCustomCss();
  940. customizeService.initCustomTitle();
  941. return res.json({ status: true });
  942. }
  943. return res.json({ status: false, message: req.form.errors.join('\n') });
  944. };
  945. // app.post('/_api/admin/notifications.add' , admin.api.notificationAdd);
  946. actions.api.notificationAdd = function(req, res) {
  947. const UpdatePost = crowi.model('UpdatePost');
  948. const pathPattern = req.body.pathPattern;
  949. const channel = req.body.channel;
  950. debug('notification.add', pathPattern, channel);
  951. UpdatePost.create(pathPattern, channel, req.user)
  952. .then((doc) => {
  953. debug('Successfully save updatePost', doc);
  954. // fixme: うーん
  955. doc.creator = doc.creator._id.toString();
  956. return res.json(ApiResponse.success({ updatePost: doc }));
  957. })
  958. .catch((err) => {
  959. debug('Failed to save updatePost', err);
  960. return res.json(ApiResponse.error());
  961. });
  962. };
  963. // app.post('/_api/admin/notifications.remove' , admin.api.notificationRemove);
  964. actions.api.notificationRemove = function(req, res) {
  965. const UpdatePost = crowi.model('UpdatePost');
  966. const id = req.body.id;
  967. UpdatePost.remove(id)
  968. .then(() => {
  969. debug('Successfully remove updatePost');
  970. return res.json(ApiResponse.success({}));
  971. })
  972. .catch((err) => {
  973. debug('Failed to remove updatePost', err);
  974. return res.json(ApiResponse.error());
  975. });
  976. };
  977. // app.get('/_api/admin/users.search' , admin.api.userSearch);
  978. actions.api.usersSearch = function(req, res) {
  979. const User = crowi.model('User');
  980. const email = req.query.email;
  981. User.findUsersByPartOfEmail(email, {})
  982. .then((users) => {
  983. const result = {
  984. data: users,
  985. };
  986. return res.json(ApiResponse.success(result));
  987. })
  988. .catch((err) => {
  989. return res.json(ApiResponse.error());
  990. });
  991. };
  992. actions.api.toggleIsEnabledForGlobalNotification = async(req, res) => {
  993. const id = req.query.id;
  994. const isEnabled = (req.query.isEnabled === 'true');
  995. try {
  996. if (isEnabled) {
  997. await GlobalNotificationSetting.enable(id);
  998. }
  999. else {
  1000. await GlobalNotificationSetting.disable(id);
  1001. }
  1002. return res.json(ApiResponse.success());
  1003. }
  1004. catch (err) {
  1005. return res.json(ApiResponse.error());
  1006. }
  1007. };
  1008. /**
  1009. * save esa settings, update config cache, and response json
  1010. *
  1011. * @param {*} req
  1012. * @param {*} res
  1013. */
  1014. actions.api.importerSettingEsa = async(req, res) => {
  1015. const form = req.form.settingForm;
  1016. if (!req.form.isValid) {
  1017. return res.json({ status: false, message: req.form.errors.join('\n') });
  1018. }
  1019. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  1020. importer.initializeEsaClient(); // let it run in the back aftert res
  1021. return res.json({ status: true });
  1022. };
  1023. /**
  1024. * save qiita settings, update config cache, and response json
  1025. *
  1026. * @param {*} req
  1027. * @param {*} res
  1028. */
  1029. actions.api.importerSettingQiita = async(req, res) => {
  1030. const form = req.form.settingForm;
  1031. if (!req.form.isValid) {
  1032. return res.json({ status: false, message: req.form.errors.join('\n') });
  1033. }
  1034. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  1035. importer.initializeQiitaClient(); // let it run in the back aftert res
  1036. return res.json({ status: true });
  1037. };
  1038. /**
  1039. * Import all posts from esa
  1040. *
  1041. * @param {*} req
  1042. * @param {*} res
  1043. */
  1044. actions.api.importDataFromEsa = async(req, res) => {
  1045. const user = req.user;
  1046. let errors;
  1047. try {
  1048. errors = await importer.importDataFromEsa(user);
  1049. }
  1050. catch (err) {
  1051. errors = [err];
  1052. }
  1053. if (errors.length > 0) {
  1054. return res.json({ status: false, message: `<br> - ${errors.join('<br> - ')}` });
  1055. }
  1056. return res.json({ status: true });
  1057. };
  1058. /**
  1059. * Import all posts from qiita
  1060. *
  1061. * @param {*} req
  1062. * @param {*} res
  1063. */
  1064. actions.api.importDataFromQiita = async(req, res) => {
  1065. const user = req.user;
  1066. let errors;
  1067. try {
  1068. errors = await importer.importDataFromQiita(user);
  1069. }
  1070. catch (err) {
  1071. errors = [err];
  1072. }
  1073. if (errors.length > 0) {
  1074. return res.json({ status: false, message: `<br> - ${errors.join('<br> - ')}` });
  1075. }
  1076. return res.json({ status: true });
  1077. };
  1078. /**
  1079. * Test connection to esa and response result with json
  1080. *
  1081. * @param {*} req
  1082. * @param {*} res
  1083. */
  1084. actions.api.testEsaAPI = async(req, res) => {
  1085. try {
  1086. await importer.testConnectionToEsa();
  1087. return res.json({ status: true });
  1088. }
  1089. catch (err) {
  1090. return res.json({ status: false, message: `${err}` });
  1091. }
  1092. };
  1093. /**
  1094. * Test connection to qiita and response result with json
  1095. *
  1096. * @param {*} req
  1097. * @param {*} res
  1098. */
  1099. actions.api.testQiitaAPI = async(req, res) => {
  1100. try {
  1101. await importer.testConnectionToQiita();
  1102. return res.json({ status: true });
  1103. }
  1104. catch (err) {
  1105. return res.json({ status: false, message: `${err}` });
  1106. }
  1107. };
  1108. actions.api.searchBuildIndex = async function(req, res) {
  1109. const search = crowi.getSearcher();
  1110. if (!search) {
  1111. return res.json(ApiResponse.error('ElasticSearch Integration is not set up.'));
  1112. }
  1113. // first, delete index
  1114. try {
  1115. await search.deleteIndex();
  1116. }
  1117. catch (err) {
  1118. logger.warn('Delete index Error, but if it is initialize, its ok.', err);
  1119. }
  1120. // second, create index
  1121. try {
  1122. await search.buildIndex();
  1123. }
  1124. catch (err) {
  1125. logger.error('Error', err);
  1126. return res.json(ApiResponse.error(err));
  1127. }
  1128. searchEvent.on('addPageProgress', (total, current, skip) => {
  1129. crowi.getIo().sockets.emit('admin:addPageProgress', { total, current, skip });
  1130. });
  1131. searchEvent.on('finishAddPage', (total, current, skip) => {
  1132. crowi.getIo().sockets.emit('admin:finishAddPage', { total, current, skip });
  1133. });
  1134. // add all page
  1135. search
  1136. .addAllPages()
  1137. .then(() => {
  1138. debug('Data is successfully indexed. ------------------ ✧✧');
  1139. })
  1140. .catch((err) => {
  1141. logger.error('Error', err);
  1142. });
  1143. return res.json(ApiResponse.success());
  1144. };
  1145. actions.api.userGroups = async(req, res) => {
  1146. try {
  1147. const userGroups = await UserGroup.find();
  1148. return res.json(ApiResponse.success({ userGroups }));
  1149. }
  1150. catch (err) {
  1151. logger.error('Error', err);
  1152. return res.json(ApiResponse.error('Error'));
  1153. }
  1154. };
  1155. function validateMailSetting(req, form, callback) {
  1156. const mailer = crowi.mailer;
  1157. const option = {
  1158. host: form['mail:smtpHost'],
  1159. port: form['mail:smtpPort'],
  1160. };
  1161. if (form['mail:smtpUser'] && form['mail:smtpPassword']) {
  1162. option.auth = {
  1163. user: form['mail:smtpUser'],
  1164. pass: form['mail:smtpPassword'],
  1165. };
  1166. }
  1167. if (option.port === 465) {
  1168. option.secure = true;
  1169. }
  1170. const smtpClient = mailer.createSMTPClient(option);
  1171. debug('mailer setup for validate SMTP setting', smtpClient);
  1172. smtpClient.sendMail({
  1173. from: form['mail:from'],
  1174. to: req.user.email,
  1175. subject: 'Wiki管理設定のアップデートによるメール通知',
  1176. text: 'このメールは、WikiのSMTP設定のアップデートにより送信されています。',
  1177. }, callback);
  1178. }
  1179. /**
  1180. * validate setting form values for SAML
  1181. *
  1182. * This validation checks, for the value of each mandatory items,
  1183. * whether it from the environment variables is empty and form value to update it is empty.
  1184. */
  1185. function validateSamlSettingForm(form, t) {
  1186. for (const key of crowi.passportService.mandatoryConfigKeysForSaml) {
  1187. const formValue = form.settingForm[key];
  1188. if (configManager.getConfigFromEnvVars('crowi', key) === null && formValue === '') {
  1189. const formItemName = t(`security_setting.form_item_name.${key}`);
  1190. form.errors.push(t('form_validation.required', formItemName));
  1191. }
  1192. }
  1193. }
  1194. return actions;
  1195. };