admin.js 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356
  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({
  552. id: userGroup._id,
  553. relatedUsers: relations.map((relation) => {
  554. return relation.relatedUser;
  555. }),
  556. });
  557. });
  558. });
  559. });
  560. })
  561. .then((allRelationsPromise) => {
  562. return Promise.all(allRelationsPromise);
  563. })
  564. .then((relations) => {
  565. for (const relation of relations) {
  566. renderVar.userGroupRelations[relation.id] = relation.relatedUsers;
  567. }
  568. debug('in findUserGroupsWithPagination findAllRelationForUserGroupResult', renderVar.userGroupRelations);
  569. return res.render('admin/user-groups', renderVar);
  570. })
  571. .catch((err) => {
  572. debug('Error on find all relations', err);
  573. return res.json(ApiResponse.error('Error'));
  574. });
  575. };
  576. // グループ詳細
  577. actions.userGroup.detail = async function(req, res) {
  578. const userGroupId = req.params.id;
  579. const renderVar = {
  580. userGroup: null,
  581. userGroupRelations: [],
  582. notRelatedusers: [],
  583. relatedPages: [],
  584. };
  585. const userGroup = await UserGroup.findOne({ _id: userGroupId });
  586. if (userGroup == null) {
  587. logger.error('no userGroup is exists. ', userGroupId);
  588. req.flash('errorMessage', 'グループがありません');
  589. return res.redirect('/admin/user-groups');
  590. }
  591. renderVar.userGroup = userGroup;
  592. const resolves = await Promise.all([
  593. // get all user and group relations
  594. UserGroupRelation.findAllRelationForUserGroup(userGroup),
  595. // get all not related users for group
  596. UserGroupRelation.findUserByNotRelatedGroup(userGroup),
  597. // get all related pages
  598. Page.find({ grant: Page.GRANT_USER_GROUP, grantedGroup: { $in: [userGroup] } }),
  599. ]);
  600. renderVar.userGroupRelations = resolves[0];
  601. renderVar.notRelatedusers = resolves[1];
  602. renderVar.relatedPages = resolves[2];
  603. return res.render('admin/user-group-detail', renderVar);
  604. };
  605. //
  606. actions.userGroup.update = function(req, res) {
  607. const userGroupId = req.params.userGroupId;
  608. const name = crowi.xss.process(req.body.name);
  609. UserGroup.findById(userGroupId)
  610. .then((userGroupData) => {
  611. if (userGroupData == null) {
  612. req.flash('errorMessage', 'グループの検索に失敗しました。');
  613. return new Promise();
  614. }
  615. // 名前存在チェック
  616. return UserGroup.isRegisterableName(name)
  617. .then((isRegisterableName) => {
  618. // 既に存在するグループ名に更新しようとした場合はエラー
  619. if (!isRegisterableName) {
  620. req.flash('errorMessage', 'グループ名が既に存在します。');
  621. }
  622. else {
  623. return userGroupData.updateName(name)
  624. .then(() => {
  625. req.flash('successMessage', 'グループ名を更新しました。');
  626. })
  627. .catch((err) => {
  628. req.flash('errorMessage', 'グループ名の更新に失敗しました。');
  629. });
  630. }
  631. });
  632. })
  633. .then(() => {
  634. return res.redirect(`/admin/user-group-detail/${userGroupId}`);
  635. });
  636. };
  637. actions.userGroupRelation = {};
  638. actions.userGroupRelation.index = function(req, res) {
  639. };
  640. actions.userGroupRelation.create = function(req, res) {
  641. const User = crowi.model('User');
  642. const UserGroup = crowi.model('UserGroup');
  643. const UserGroupRelation = crowi.model('UserGroupRelation');
  644. // req params
  645. const userName = req.body.user_name;
  646. const userGroupId = req.body.user_group_id;
  647. let user = null;
  648. let userGroup = null;
  649. Promise.all([
  650. // ユーザグループをIDで検索
  651. UserGroup.findById(userGroupId),
  652. // ユーザを名前で検索
  653. User.findUserByUsername(userName),
  654. ])
  655. .then((resolves) => {
  656. userGroup = resolves[0];
  657. user = resolves[1];
  658. // Relation を作成
  659. UserGroupRelation.createRelation(userGroup, user);
  660. })
  661. .then((result) => {
  662. return res.redirect(`/admin/user-group-detail/${userGroup.id}`);
  663. })
  664. .catch((err) => {
  665. debug('Error on create user-group relation', err);
  666. req.flash('errorMessage', 'Error on create user-group relation');
  667. return res.redirect(`/admin/user-group-detail/${userGroup.id}`);
  668. });
  669. };
  670. actions.userGroupRelation.remove = function(req, res) {
  671. const UserGroupRelation = crowi.model('UserGroupRelation');
  672. const userGroupId = req.params.id;
  673. const relationId = req.params.relationId;
  674. UserGroupRelation.removeById(relationId)
  675. .then(() => {
  676. return res.redirect(`/admin/user-group-detail/${userGroupId}`);
  677. })
  678. .catch((err) => {
  679. debug('Error on remove user-group-relation', err);
  680. req.flash('errorMessage', 'グループのユーザ削除に失敗しました。');
  681. });
  682. };
  683. // Importer management
  684. actions.importer = {};
  685. actions.importer.index = function(req, res) {
  686. const settingForm = configManager.getConfigByPrefix('crowi', 'importer:');
  687. return res.render('admin/importer', {
  688. settingForm,
  689. });
  690. };
  691. actions.api = {};
  692. actions.api.appSetting = async function(req, res) {
  693. const form = req.form.settingForm;
  694. if (req.form.isValid) {
  695. debug('form content', form);
  696. // mail setting ならここで validation
  697. if (form['mail:from']) {
  698. validateMailSetting(req, form, async(err, data) => {
  699. debug('Error validate mail setting: ', err, data);
  700. if (err) {
  701. req.form.errors.push('SMTPを利用したテストメール送信に失敗しました。設定をみなおしてください。');
  702. return res.json({ status: false, message: req.form.errors.join('\n') });
  703. }
  704. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  705. return res.json({ status: true });
  706. });
  707. }
  708. else {
  709. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  710. return res.json({ status: true });
  711. }
  712. }
  713. else {
  714. return res.json({ status: false, message: req.form.errors.join('\n') });
  715. }
  716. };
  717. actions.api.asyncAppSetting = async(req, res) => {
  718. const form = req.form.settingForm;
  719. if (!req.form.isValid) {
  720. return res.json({ status: false, message: req.form.errors.join('\n') });
  721. }
  722. debug('form content', form);
  723. try {
  724. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  725. return res.json({ status: true });
  726. }
  727. catch (err) {
  728. logger.error(err);
  729. return res.json({ status: false });
  730. }
  731. };
  732. actions.api.securitySetting = async function(req, res) {
  733. if (!req.form.isValid) {
  734. return res.json({ status: false, message: req.form.errors.join('\n') });
  735. }
  736. const form = req.form.settingForm;
  737. if (aclService.getIsPublicWikiOnly()) {
  738. const guestMode = form['security:restrictGuestMode'];
  739. if (guestMode === 'Deny') {
  740. req.form.errors.push('Private Wikiへの設定変更はできません。');
  741. return res.json({ status: false, message: req.form.errors.join('\n') });
  742. }
  743. }
  744. try {
  745. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  746. return res.json({ status: true });
  747. }
  748. catch (err) {
  749. logger.error(err);
  750. return res.json({ status: false });
  751. }
  752. };
  753. actions.api.securityPassportLdapSetting = function(req, res) {
  754. const form = req.form.settingForm;
  755. if (!req.form.isValid) {
  756. return res.json({ status: false, message: req.form.errors.join('\n') });
  757. }
  758. debug('form content', form);
  759. return configManager.updateConfigsInTheSameNamespace('crowi', form)
  760. .then(() => {
  761. // reset strategy
  762. crowi.passportService.resetLdapStrategy();
  763. // setup strategy
  764. if (configManager.getConfig('crowi', 'security:passport-ldap:isEnabled')) {
  765. crowi.passportService.setupLdapStrategy(true);
  766. }
  767. return;
  768. })
  769. .then(() => {
  770. res.json({ status: true });
  771. });
  772. };
  773. actions.api.securityPassportSamlSetting = async(req, res) => {
  774. const form = req.form.settingForm;
  775. validateSamlSettingForm(req.form, req.t);
  776. if (!req.form.isValid) {
  777. return res.json({ status: false, message: req.form.errors.join('\n') });
  778. }
  779. debug('form content', form);
  780. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  781. // reset strategy
  782. await crowi.passportService.resetSamlStrategy();
  783. // setup strategy
  784. if (configManager.getConfig('crowi', 'security:passport-saml:isEnabled')) {
  785. try {
  786. await crowi.passportService.setupSamlStrategy(true);
  787. }
  788. catch (err) {
  789. // reset
  790. await crowi.passportService.resetSamlStrategy();
  791. return res.json({ status: false, message: err.message });
  792. }
  793. }
  794. return res.json({ status: true });
  795. };
  796. actions.api.securityPassportBasicSetting = async(req, res) => {
  797. const form = req.form.settingForm;
  798. if (!req.form.isValid) {
  799. return res.json({ status: false, message: req.form.errors.join('\n') });
  800. }
  801. debug('form content', form);
  802. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  803. // reset strategy
  804. await crowi.passportService.resetBasicStrategy();
  805. // setup strategy
  806. if (configManager.getConfig('crowi', 'security:passport-basic:isEnabled')) {
  807. try {
  808. await crowi.passportService.setupBasicStrategy(true);
  809. }
  810. catch (err) {
  811. // reset
  812. await crowi.passportService.resetBasicStrategy();
  813. return res.json({ status: false, message: err.message });
  814. }
  815. }
  816. return res.json({ status: true });
  817. };
  818. actions.api.securityPassportGoogleSetting = async(req, res) => {
  819. const form = req.form.settingForm;
  820. if (!req.form.isValid) {
  821. return res.json({ status: false, message: req.form.errors.join('\n') });
  822. }
  823. debug('form content', form);
  824. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  825. // reset strategy
  826. await crowi.passportService.resetGoogleStrategy();
  827. // setup strategy
  828. if (configManager.getConfig('crowi', 'security:passport-google:isEnabled')) {
  829. try {
  830. await crowi.passportService.setupGoogleStrategy(true);
  831. }
  832. catch (err) {
  833. // reset
  834. await crowi.passportService.resetGoogleStrategy();
  835. return res.json({ status: false, message: err.message });
  836. }
  837. }
  838. return res.json({ status: true });
  839. };
  840. actions.api.securityPassportGitHubSetting = async(req, res) => {
  841. const form = req.form.settingForm;
  842. if (!req.form.isValid) {
  843. return res.json({ status: false, message: req.form.errors.join('\n') });
  844. }
  845. debug('form content', form);
  846. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  847. // reset strategy
  848. await crowi.passportService.resetGitHubStrategy();
  849. // setup strategy
  850. if (configManager.getConfig('crowi', 'security:passport-github:isEnabled')) {
  851. try {
  852. await crowi.passportService.setupGitHubStrategy(true);
  853. }
  854. catch (err) {
  855. // reset
  856. await crowi.passportService.resetGitHubStrategy();
  857. return res.json({ status: false, message: err.message });
  858. }
  859. }
  860. return res.json({ status: true });
  861. };
  862. actions.api.securityPassportTwitterSetting = async(req, res) => {
  863. const form = req.form.settingForm;
  864. if (!req.form.isValid) {
  865. return res.json({ status: false, message: req.form.errors.join('\n') });
  866. }
  867. debug('form content', form);
  868. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  869. // reset strategy
  870. await crowi.passportService.resetTwitterStrategy();
  871. // setup strategy
  872. if (configManager.getConfig('crowi', 'security:passport-twitter:isEnabled')) {
  873. try {
  874. await crowi.passportService.setupTwitterStrategy(true);
  875. }
  876. catch (err) {
  877. // reset
  878. await crowi.passportService.resetTwitterStrategy();
  879. return res.json({ status: false, message: err.message });
  880. }
  881. }
  882. return res.json({ status: true });
  883. };
  884. actions.api.securityPassportOidcSetting = async(req, res) => {
  885. const form = req.form.settingForm;
  886. if (!req.form.isValid) {
  887. return res.json({ status: false, message: req.form.errors.join('\n') });
  888. }
  889. debug('form content', form);
  890. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  891. // reset strategy
  892. await crowi.passportService.resetOidcStrategy();
  893. // setup strategy
  894. if (configManager.getConfig('crowi', 'security:passport-oidc:isEnabled')) {
  895. try {
  896. await crowi.passportService.setupOidcStrategy(true);
  897. }
  898. catch (err) {
  899. // reset
  900. await crowi.passportService.resetOidcStrategy();
  901. return res.json({ status: false, message: err.message });
  902. }
  903. }
  904. return res.json({ status: true });
  905. };
  906. actions.api.customizeSetting = async function(req, res) {
  907. const form = req.form.settingForm;
  908. if (req.form.isValid) {
  909. debug('form content', form);
  910. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  911. customizeService.initCustomCss();
  912. customizeService.initCustomTitle();
  913. return res.json({ status: true });
  914. }
  915. return res.json({ status: false, message: req.form.errors.join('\n') });
  916. };
  917. // app.post('/_api/admin/notifications.add' , admin.api.notificationAdd);
  918. actions.api.notificationAdd = function(req, res) {
  919. const UpdatePost = crowi.model('UpdatePost');
  920. const pathPattern = req.body.pathPattern;
  921. const channel = req.body.channel;
  922. debug('notification.add', pathPattern, channel);
  923. UpdatePost.create(pathPattern, channel, req.user)
  924. .then((doc) => {
  925. debug('Successfully save updatePost', doc);
  926. // fixme: うーん
  927. doc.creator = doc.creator._id.toString();
  928. return res.json(ApiResponse.success({ updatePost: doc }));
  929. })
  930. .catch((err) => {
  931. debug('Failed to save updatePost', err);
  932. return res.json(ApiResponse.error());
  933. });
  934. };
  935. // app.post('/_api/admin/notifications.remove' , admin.api.notificationRemove);
  936. actions.api.notificationRemove = function(req, res) {
  937. const UpdatePost = crowi.model('UpdatePost');
  938. const id = req.body.id;
  939. UpdatePost.remove(id)
  940. .then(() => {
  941. debug('Successfully remove updatePost');
  942. return res.json(ApiResponse.success({}));
  943. })
  944. .catch((err) => {
  945. debug('Failed to remove updatePost', err);
  946. return res.json(ApiResponse.error());
  947. });
  948. };
  949. // app.get('/_api/admin/users.search' , admin.api.userSearch);
  950. actions.api.usersSearch = function(req, res) {
  951. const User = crowi.model('User');
  952. const email = req.query.email;
  953. User.findUsersByPartOfEmail(email, {})
  954. .then((users) => {
  955. const result = {
  956. data: users,
  957. };
  958. return res.json(ApiResponse.success(result));
  959. })
  960. .catch((err) => {
  961. return res.json(ApiResponse.error());
  962. });
  963. };
  964. actions.api.toggleIsEnabledForGlobalNotification = async(req, res) => {
  965. const id = req.query.id;
  966. const isEnabled = (req.query.isEnabled === 'true');
  967. try {
  968. if (isEnabled) {
  969. await GlobalNotificationSetting.enable(id);
  970. }
  971. else {
  972. await GlobalNotificationSetting.disable(id);
  973. }
  974. return res.json(ApiResponse.success());
  975. }
  976. catch (err) {
  977. return res.json(ApiResponse.error());
  978. }
  979. };
  980. /**
  981. * save esa settings, update config cache, and response json
  982. *
  983. * @param {*} req
  984. * @param {*} res
  985. */
  986. actions.api.importerSettingEsa = async(req, res) => {
  987. const form = req.form.settingForm;
  988. if (!req.form.isValid) {
  989. return res.json({ status: false, message: req.form.errors.join('\n') });
  990. }
  991. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  992. importer.initializeEsaClient(); // let it run in the back aftert res
  993. return res.json({ status: true });
  994. };
  995. /**
  996. * save qiita settings, update config cache, and response json
  997. *
  998. * @param {*} req
  999. * @param {*} res
  1000. */
  1001. actions.api.importerSettingQiita = async(req, res) => {
  1002. const form = req.form.settingForm;
  1003. if (!req.form.isValid) {
  1004. return res.json({ status: false, message: req.form.errors.join('\n') });
  1005. }
  1006. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  1007. importer.initializeQiitaClient(); // let it run in the back aftert res
  1008. return res.json({ status: true });
  1009. };
  1010. /**
  1011. * Import all posts from esa
  1012. *
  1013. * @param {*} req
  1014. * @param {*} res
  1015. */
  1016. actions.api.importDataFromEsa = async(req, res) => {
  1017. const user = req.user;
  1018. let errors;
  1019. try {
  1020. errors = await importer.importDataFromEsa(user);
  1021. }
  1022. catch (err) {
  1023. errors = [err];
  1024. }
  1025. if (errors.length > 0) {
  1026. return res.json({ status: false, message: `<br> - ${errors.join('<br> - ')}` });
  1027. }
  1028. return res.json({ status: true });
  1029. };
  1030. /**
  1031. * Import all posts from qiita
  1032. *
  1033. * @param {*} req
  1034. * @param {*} res
  1035. */
  1036. actions.api.importDataFromQiita = async(req, res) => {
  1037. const user = req.user;
  1038. let errors;
  1039. try {
  1040. errors = await importer.importDataFromQiita(user);
  1041. }
  1042. catch (err) {
  1043. errors = [err];
  1044. }
  1045. if (errors.length > 0) {
  1046. return res.json({ status: false, message: `<br> - ${errors.join('<br> - ')}` });
  1047. }
  1048. return res.json({ status: true });
  1049. };
  1050. /**
  1051. * Test connection to esa and response result with json
  1052. *
  1053. * @param {*} req
  1054. * @param {*} res
  1055. */
  1056. actions.api.testEsaAPI = async(req, res) => {
  1057. try {
  1058. await importer.testConnectionToEsa();
  1059. return res.json({ status: true });
  1060. }
  1061. catch (err) {
  1062. return res.json({ status: false, message: `${err}` });
  1063. }
  1064. };
  1065. /**
  1066. * Test connection to qiita and response result with json
  1067. *
  1068. * @param {*} req
  1069. * @param {*} res
  1070. */
  1071. actions.api.testQiitaAPI = async(req, res) => {
  1072. try {
  1073. await importer.testConnectionToQiita();
  1074. return res.json({ status: true });
  1075. }
  1076. catch (err) {
  1077. return res.json({ status: false, message: `${err}` });
  1078. }
  1079. };
  1080. actions.api.searchBuildIndex = async function(req, res) {
  1081. const search = crowi.getSearcher();
  1082. if (!search) {
  1083. return res.json(ApiResponse.error('ElasticSearch Integration is not set up.'));
  1084. }
  1085. // first, delete index
  1086. try {
  1087. await search.deleteIndex();
  1088. }
  1089. catch (err) {
  1090. logger.warn('Delete index Error, but if it is initialize, its ok.', err);
  1091. }
  1092. // second, create index
  1093. try {
  1094. await search.buildIndex();
  1095. }
  1096. catch (err) {
  1097. logger.error('Error', err);
  1098. return res.json(ApiResponse.error(err));
  1099. }
  1100. searchEvent.on('addPageProgress', (total, current, skip) => {
  1101. crowi.getIo().sockets.emit('admin:addPageProgress', { total, current, skip });
  1102. });
  1103. searchEvent.on('finishAddPage', (total, current, skip) => {
  1104. crowi.getIo().sockets.emit('admin:finishAddPage', { total, current, skip });
  1105. });
  1106. // add all page
  1107. search
  1108. .addAllPages()
  1109. .then(() => {
  1110. debug('Data is successfully indexed. ------------------ ✧✧');
  1111. })
  1112. .catch((err) => {
  1113. logger.error('Error', err);
  1114. });
  1115. return res.json(ApiResponse.success());
  1116. };
  1117. function validateMailSetting(req, form, callback) {
  1118. const mailer = crowi.mailer;
  1119. const option = {
  1120. host: form['mail:smtpHost'],
  1121. port: form['mail:smtpPort'],
  1122. };
  1123. if (form['mail:smtpUser'] && form['mail:smtpPassword']) {
  1124. option.auth = {
  1125. user: form['mail:smtpUser'],
  1126. pass: form['mail:smtpPassword'],
  1127. };
  1128. }
  1129. if (option.port === 465) {
  1130. option.secure = true;
  1131. }
  1132. const smtpClient = mailer.createSMTPClient(option);
  1133. debug('mailer setup for validate SMTP setting', smtpClient);
  1134. smtpClient.sendMail({
  1135. from: form['mail:from'],
  1136. to: req.user.email,
  1137. subject: 'Wiki管理設定のアップデートによるメール通知',
  1138. text: 'このメールは、WikiのSMTP設定のアップデートにより送信されています。',
  1139. }, callback);
  1140. }
  1141. /**
  1142. * validate setting form values for SAML
  1143. *
  1144. * This validation checks, for the value of each mandatory items,
  1145. * whether it from the environment variables is empty and form value to update it is empty.
  1146. */
  1147. function validateSamlSettingForm(form, t) {
  1148. for (const key of crowi.passportService.mandatoryConfigKeysForSaml) {
  1149. const formValue = form.settingForm[key];
  1150. if (configManager.getConfigFromEnvVars('crowi', key) === null && formValue === '') {
  1151. const formItemName = t(`security_setting.form_item_name.${key}`);
  1152. form.errors.push(t('form_validation.required', formItemName));
  1153. }
  1154. }
  1155. }
  1156. return actions;
  1157. };