admin.js 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139
  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 User = models.User;
  7. const ExternalAccount = models.ExternalAccount;
  8. const UserGroup = models.UserGroup;
  9. const UserGroupRelation = models.UserGroupRelation;
  10. const GlobalNotificationSetting = models.GlobalNotificationSetting;
  11. const GlobalNotificationMailSetting = models.GlobalNotificationMailSetting;
  12. const GlobalNotificationSlackSetting = models.GlobalNotificationSlackSetting; // eslint-disable-line no-unused-vars
  13. const {
  14. configManager,
  15. aclService,
  16. slackNotificationService,
  17. customizeService,
  18. exportService,
  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. const { check } = require('express-validator/check');
  29. const api = {};
  30. function createPager(total, limit, page, pagesCount, maxPageList) {
  31. const pager = {
  32. page,
  33. pagesCount,
  34. pages: [],
  35. total,
  36. previous: null,
  37. previousDots: false,
  38. next: null,
  39. nextDots: false,
  40. };
  41. if (page > 1) {
  42. pager.previous = page - 1;
  43. }
  44. if (page < pagesCount) {
  45. pager.next = page + 1;
  46. }
  47. let pagerMin = Math.max(1, Math.ceil(page - maxPageList / 2));
  48. let pagerMax = Math.min(pagesCount, Math.floor(page + maxPageList / 2));
  49. if (pagerMin === 1) {
  50. if (MAX_PAGE_LIST < pagesCount) {
  51. pagerMax = MAX_PAGE_LIST;
  52. }
  53. else {
  54. pagerMax = pagesCount;
  55. }
  56. }
  57. if (pagerMax === pagesCount) {
  58. if ((pagerMax - MAX_PAGE_LIST) < 1) {
  59. pagerMin = 1;
  60. }
  61. else {
  62. pagerMin = pagerMax - MAX_PAGE_LIST;
  63. }
  64. }
  65. pager.previousDots = null;
  66. if (pagerMin > 1) {
  67. pager.previousDots = true;
  68. }
  69. pager.nextDots = null;
  70. if (pagerMax < pagesCount) {
  71. pager.nextDots = true;
  72. }
  73. for (let i = pagerMin; i <= pagerMax; i++) {
  74. pager.pages.push(i);
  75. }
  76. return pager;
  77. }
  78. // setup websocket event for rebuild index
  79. searchEvent.on('addPageProgress', (total, current, skip) => {
  80. crowi.getIo().sockets.emit('admin:addPageProgress', { total, current, skip });
  81. });
  82. searchEvent.on('finishAddPage', (total, current, skip) => {
  83. crowi.getIo().sockets.emit('admin:finishAddPage', { total, current, skip });
  84. });
  85. actions.index = function(req, res) {
  86. return res.render('admin/index', {
  87. plugins: pluginUtils.listPlugins(crowi.rootDir),
  88. });
  89. };
  90. // app.get('/admin/app' , admin.app.index);
  91. actions.app = {};
  92. actions.app.index = function(req, res) {
  93. return res.render('admin/app');
  94. };
  95. actions.app.settingUpdate = function(req, res) {
  96. };
  97. // app.get('/admin/security' , admin.security.index);
  98. actions.security = {};
  99. actions.security.index = function(req, res) {
  100. const isWikiModeForced = aclService.isWikiModeForced();
  101. const guestModeValue = aclService.getGuestModeValue();
  102. return res.render('admin/security', {
  103. isWikiModeForced,
  104. guestModeValue,
  105. });
  106. };
  107. // app.get('/admin/markdown' , admin.markdown.index);
  108. actions.markdown = {};
  109. actions.markdown.index = function(req, res) {
  110. const markdownSetting = configManager.getConfigByPrefix('markdown', 'markdown:');
  111. return res.render('admin/markdown', {
  112. markdownSetting,
  113. recommendedWhitelist,
  114. });
  115. };
  116. // app.post('/admin/markdown/presentationSetting' , admin.markdown.presentationSetting);
  117. actions.markdown.presentationSetting = async function(req, res) {
  118. const markdownSetting = req.form.markdownSetting;
  119. if (req.form.isValid) {
  120. await configManager.updateConfigsInTheSameNamespace('markdown', markdownSetting);
  121. req.flash('successMessage', ['Successfully updated!']);
  122. }
  123. else {
  124. req.flash('errorMessage', req.form.errors);
  125. }
  126. return res.redirect('/admin/markdown');
  127. };
  128. // app.get('/admin/customize' , admin.customize.index);
  129. actions.customize = {};
  130. actions.customize.index = function(req, res) {
  131. const settingForm = configManager.getConfigByPrefix('crowi', 'customize:');
  132. /* eslint-disable quote-props, no-multi-spaces */
  133. const highlightJsCssSelectorOptions = {
  134. 'github': { name: '[Light] GitHub', border: false },
  135. 'github-gist': { name: '[Light] GitHub Gist', border: true },
  136. 'atom-one-light': { name: '[Light] Atom One Light', border: true },
  137. 'xcode': { name: '[Light] Xcode', border: true },
  138. 'vs': { name: '[Light] Vs', border: true },
  139. 'atom-one-dark': { name: '[Dark] Atom One Dark', border: false },
  140. 'hybrid': { name: '[Dark] Hybrid', border: false },
  141. 'monokai': { name: '[Dark] Monokai', border: false },
  142. 'tomorrow-night': { name: '[Dark] Tomorrow Night', border: false },
  143. 'vs2015': { name: '[Dark] Vs 2015', border: false },
  144. };
  145. /* eslint-enable quote-props, no-multi-spaces */
  146. return res.render('admin/customize', {
  147. settingForm,
  148. highlightJsCssSelectorOptions,
  149. });
  150. };
  151. // app.get('/admin/notification' , admin.notification.index);
  152. actions.notification = {};
  153. actions.notification.index = async(req, res) => {
  154. const UpdatePost = crowi.model('UpdatePost');
  155. let slackSetting = configManager.getConfigByPrefix('notification', 'slack:');
  156. const hasSlackIwhUrl = !!configManager.getConfig('notification', 'slack:incomingWebhookUrl');
  157. const hasSlackToken = !!configManager.getConfig('notification', 'slack:token');
  158. if (!hasSlackIwhUrl) {
  159. slackSetting['slack:incomingWebhookUrl'] = '';
  160. }
  161. if (req.session.slackSetting) {
  162. slackSetting = req.session.slackSetting;
  163. req.session.slackSetting = null;
  164. }
  165. const globalNotifications = await GlobalNotificationSetting.findAll();
  166. const userNotifications = await UpdatePost.findAll();
  167. return res.render('admin/notification', {
  168. userNotifications,
  169. slackSetting,
  170. hasSlackIwhUrl,
  171. hasSlackToken,
  172. globalNotifications,
  173. });
  174. };
  175. // app.post('/admin/notification/slackSetting' , admin.notification.slackauth);
  176. actions.notification.slackSetting = async function(req, res) {
  177. const slackSetting = req.form.slackSetting;
  178. if (req.form.isValid) {
  179. await configManager.updateConfigsInTheSameNamespace('notification', slackSetting);
  180. req.flash('successMessage', ['Successfully Updated!']);
  181. // Re-setup
  182. crowi.setupSlack().then(() => {
  183. });
  184. }
  185. else {
  186. req.flash('errorMessage', req.form.errors);
  187. }
  188. return res.redirect('/admin/notification');
  189. };
  190. // app.get('/admin/notification/slackAuth' , admin.notification.slackauth);
  191. actions.notification.slackAuth = function(req, res) {
  192. const code = req.query.code;
  193. if (!code || !slackNotificationService.hasSlackConfig()) {
  194. return res.redirect('/admin/notification');
  195. }
  196. const slack = crowi.slack;
  197. slack.getOauthAccessToken(code)
  198. .then(async(data) => {
  199. debug('oauth response', data);
  200. try {
  201. await configManager.updateConfigsInTheSameNamespace('notification', { 'slack:token': data.access_token });
  202. req.flash('successMessage', ['Successfully Connected!']);
  203. }
  204. catch (err) {
  205. req.flash('errorMessage', ['Failed to save access_token. Please try again.']);
  206. }
  207. return res.redirect('/admin/notification');
  208. })
  209. .catch((err) => {
  210. debug('oauth response ERROR', err);
  211. req.flash('errorMessage', ['Failed to fetch access_token. Please do connect again.']);
  212. return res.redirect('/admin/notification');
  213. });
  214. };
  215. // app.post('/admin/notification/slackIwhSetting' , admin.notification.slackIwhSetting);
  216. actions.notification.slackIwhSetting = async function(req, res) {
  217. const slackIwhSetting = req.form.slackIwhSetting;
  218. if (req.form.isValid) {
  219. await configManager.updateConfigsInTheSameNamespace('notification', slackIwhSetting);
  220. req.flash('successMessage', ['Successfully Updated!']);
  221. // Re-setup
  222. crowi.setupSlack().then(() => {
  223. return res.redirect('/admin/notification#slack-incoming-webhooks');
  224. });
  225. }
  226. else {
  227. req.flash('errorMessage', req.form.errors);
  228. return res.redirect('/admin/notification#slack-incoming-webhooks');
  229. }
  230. };
  231. // app.post('/admin/notification/slackSetting/disconnect' , admin.notification.disconnectFromSlack);
  232. actions.notification.disconnectFromSlack = async function(req, res) {
  233. await configManager.updateConfigsInTheSameNamespace('notification', { 'slack:token': '' });
  234. req.flash('successMessage', ['Successfully Disconnected!']);
  235. return res.redirect('/admin/notification');
  236. };
  237. actions.globalNotification = {};
  238. actions.globalNotification.detail = async(req, res) => {
  239. const notificationSettingId = req.params.id;
  240. const renderVars = {};
  241. if (notificationSettingId) {
  242. try {
  243. renderVars.setting = await GlobalNotificationSetting.findOne({ _id: notificationSettingId });
  244. }
  245. catch (err) {
  246. logger.error(`Error in finding a global notification setting with {_id: ${notificationSettingId}}`);
  247. }
  248. }
  249. return res.render('admin/global-notification-detail', renderVars);
  250. };
  251. actions.globalNotification.create = (req, res) => {
  252. const form = req.form.notificationGlobal;
  253. let setting;
  254. switch (form.notifyToType) {
  255. case GlobalNotificationSetting.TYPE.MAIL:
  256. setting = new GlobalNotificationMailSetting(crowi);
  257. setting.toEmail = form.toEmail;
  258. break;
  259. case GlobalNotificationSetting.TYPE.SLACK:
  260. setting = new GlobalNotificationSlackSetting(crowi);
  261. setting.slackChannels = form.slackChannels;
  262. break;
  263. default:
  264. logger.error('GlobalNotificationSetting Type Error: undefined type');
  265. req.flash('errorMessage', 'Error occurred in creating a new global notification setting: undefined notification type');
  266. return res.redirect('/admin/notification#global-notification');
  267. }
  268. setting.triggerPath = form.triggerPath;
  269. setting.triggerEvents = getNotificationEvents(form);
  270. setting.save();
  271. return res.redirect('/admin/notification#global-notification');
  272. };
  273. actions.globalNotification.update = async(req, res) => {
  274. const form = req.form.notificationGlobal;
  275. const models = {
  276. [GlobalNotificationSetting.TYPE.MAIL]: GlobalNotificationMailSetting,
  277. [GlobalNotificationSetting.TYPE.SLACK]: GlobalNotificationSlackSetting,
  278. };
  279. let setting = await GlobalNotificationSetting.findOne({ _id: form.id });
  280. setting = setting.toObject();
  281. // when switching from one type to another,
  282. // remove toEmail from slack setting and slackChannels from mail setting
  283. if (setting.__t !== form.notifyToType) {
  284. setting = models[setting.__t].hydrate(setting);
  285. setting.toEmail = undefined;
  286. setting.slackChannels = undefined;
  287. await setting.save();
  288. setting = setting.toObject();
  289. }
  290. switch (form.notifyToType) {
  291. case GlobalNotificationSetting.TYPE.MAIL:
  292. setting = GlobalNotificationMailSetting.hydrate(setting);
  293. setting.toEmail = form.toEmail;
  294. break;
  295. case GlobalNotificationSetting.TYPE.SLACK:
  296. setting = GlobalNotificationSlackSetting.hydrate(setting);
  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.__t = form.notifyToType;
  305. setting.triggerPath = form.triggerPath;
  306. setting.triggerEvents = getNotificationEvents(form);
  307. await setting.save();
  308. return res.redirect('/admin/notification#global-notification');
  309. };
  310. actions.globalNotification.remove = async(req, res) => {
  311. const id = req.params.id;
  312. try {
  313. await GlobalNotificationSetting.findOneAndRemove({ _id: id });
  314. return res.redirect('/admin/notification#global-notification');
  315. }
  316. catch (err) {
  317. req.flash('errorMessage', 'Error in deleting global notification setting');
  318. return res.redirect('/admin/notification#global-notification');
  319. }
  320. };
  321. const getNotificationEvents = (form) => {
  322. const triggerEvents = [];
  323. const triggerEventKeys = Object.keys(form).filter((key) => { return key.match(/^triggerEvent/) });
  324. triggerEventKeys.forEach((key) => {
  325. if (form[key]) {
  326. triggerEvents.push(form[key]);
  327. }
  328. });
  329. return triggerEvents;
  330. };
  331. actions.search = {};
  332. actions.search.index = function(req, res) {
  333. const search = crowi.getSearcher();
  334. if (!search) {
  335. return res.redirect('/admin');
  336. }
  337. return res.render('admin/search', {});
  338. };
  339. actions.user = {};
  340. actions.user.index = async function(req, res) {
  341. return res.render('admin/users');
  342. };
  343. // これやったときの relation の挙動未確認
  344. actions.user.removeCompletely = function(req, res) {
  345. // ユーザーの物理削除
  346. const id = req.params.id;
  347. User.removeCompletelyById(id, (err, removed) => {
  348. if (err) {
  349. debug('Error while removing user.', err, id);
  350. req.flash('errorMessage', '完全な削除に失敗しました。');
  351. }
  352. else {
  353. req.flash('successMessage', '削除しました');
  354. }
  355. return res.redirect('/admin/users');
  356. });
  357. };
  358. // app.post('/_api/admin/users.resetPassword' , admin.api.usersResetPassword);
  359. actions.user.resetPassword = async function(req, res) {
  360. const id = req.body.user_id;
  361. const User = crowi.model('User');
  362. try {
  363. const newPassword = await User.resetPasswordByRandomString(id);
  364. const user = await User.findById(id);
  365. const result = { user: user.toObject(), newPassword };
  366. return res.json(ApiResponse.success(result));
  367. }
  368. catch (err) {
  369. debug('Error on reseting password', err);
  370. return res.json(ApiResponse.error(err));
  371. }
  372. };
  373. actions.externalAccount = {};
  374. actions.externalAccount.index = function(req, res) {
  375. return res.render('admin/external-accounts');
  376. };
  377. actions.externalAccount.remove = async function(req, res) {
  378. const id = req.params.id;
  379. let account = null;
  380. try {
  381. account = await ExternalAccount.findByIdAndRemove(id);
  382. if (account == null) {
  383. throw new Error('削除に失敗しました。');
  384. }
  385. }
  386. catch (err) {
  387. req.flash('errorMessage', err.message);
  388. return res.redirect('/admin/users/external-accounts');
  389. }
  390. req.flash('successMessage', `外部アカウント '${account.providerType}/${account.accountId}' を削除しました`);
  391. return res.redirect('/admin/users/external-accounts');
  392. };
  393. actions.userGroup = {};
  394. actions.userGroup.index = function(req, res) {
  395. const page = parseInt(req.query.page) || 1;
  396. const isAclEnabled = aclService.isAclEnabled();
  397. const renderVar = {
  398. userGroups: [],
  399. userGroupRelations: new Map(),
  400. pager: null,
  401. isAclEnabled,
  402. };
  403. UserGroup.findUserGroupsWithPagination({ page })
  404. .then((result) => {
  405. const pager = createPager(result.total, result.limit, result.page, result.pages, MAX_PAGE_LIST);
  406. const userGroups = result.docs;
  407. renderVar.userGroups = userGroups;
  408. renderVar.pager = pager;
  409. return userGroups.map((userGroup) => {
  410. return new Promise((resolve, reject) => {
  411. UserGroupRelation.findAllRelationForUserGroup(userGroup)
  412. .then((relations) => {
  413. return resolve({
  414. id: userGroup._id,
  415. relatedUsers: relations.map((relation) => {
  416. return relation.relatedUser;
  417. }),
  418. });
  419. });
  420. });
  421. });
  422. })
  423. .then((allRelationsPromise) => {
  424. return Promise.all(allRelationsPromise);
  425. })
  426. .then((relations) => {
  427. for (const relation of relations) {
  428. renderVar.userGroupRelations[relation.id] = relation.relatedUsers;
  429. }
  430. debug('in findUserGroupsWithPagination findAllRelationForUserGroupResult', renderVar.userGroupRelations);
  431. return res.render('admin/user-groups', renderVar);
  432. })
  433. .catch((err) => {
  434. debug('Error on find all relations', err);
  435. return res.json(ApiResponse.error('Error'));
  436. });
  437. };
  438. // グループ詳細
  439. actions.userGroup.detail = async function(req, res) {
  440. const userGroupId = req.params.id;
  441. const userGroup = await UserGroup.findOne({ _id: userGroupId });
  442. if (userGroup == null) {
  443. logger.error('no userGroup is exists. ', userGroupId);
  444. return res.redirect('/admin/user-groups');
  445. }
  446. return res.render('admin/user-group-detail', { userGroup });
  447. };
  448. // Importer management
  449. actions.importer = {};
  450. actions.importer.api = api;
  451. api.validators = {};
  452. api.validators.importer = {};
  453. actions.importer.index = function(req, res) {
  454. const settingForm = configManager.getConfigByPrefix('crowi', 'importer:');
  455. return res.render('admin/importer', {
  456. settingForm,
  457. });
  458. };
  459. api.validators.importer.esa = function() {
  460. const validator = [
  461. check('importer:esa:team_name').not().isEmpty().withMessage('Error. Empty esa:team_name'),
  462. check('importer:esa:access_token').not().isEmpty().withMessage('Error. Empty esa:access_token'),
  463. ];
  464. return validator;
  465. };
  466. api.validators.importer.qiita = function() {
  467. const validator = [
  468. check('importer:qiita:team_name').not().isEmpty().withMessage('Error. Empty qiita:team_name'),
  469. check('importer:qiita:access_token').not().isEmpty().withMessage('Error. Empty qiita:access_token'),
  470. ];
  471. return validator;
  472. };
  473. // Export management
  474. actions.export = {};
  475. actions.export.index = (req, res) => {
  476. return res.render('admin/export');
  477. };
  478. actions.export.download = (req, res) => {
  479. // TODO: add express validator
  480. const { fileName } = req.params;
  481. try {
  482. const zipFile = exportService.getFile(fileName);
  483. return res.download(zipFile);
  484. }
  485. catch (err) {
  486. // TODO: use ApiV3Error
  487. logger.error(err);
  488. return res.json(ApiResponse.error());
  489. }
  490. };
  491. actions.api = {};
  492. actions.api.appSetting = async function(req, res) {
  493. const form = req.form.settingForm;
  494. if (req.form.isValid) {
  495. debug('form content', form);
  496. // mail setting ならここで validation
  497. if (form['mail:from']) {
  498. validateMailSetting(req, form, async(err, data) => {
  499. debug('Error validate mail setting: ', err, data);
  500. if (err) {
  501. req.form.errors.push('SMTPを利用したテストメール送信に失敗しました。設定をみなおしてください。');
  502. return res.json({ status: false, message: req.form.errors.join('\n') });
  503. }
  504. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  505. return res.json({ status: true });
  506. });
  507. }
  508. else {
  509. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  510. return res.json({ status: true });
  511. }
  512. }
  513. else {
  514. return res.json({ status: false, message: req.form.errors.join('\n') });
  515. }
  516. };
  517. actions.api.asyncAppSetting = async(req, res) => {
  518. const form = req.form.settingForm;
  519. if (!req.form.isValid) {
  520. return res.json({ status: false, message: req.form.errors.join('\n') });
  521. }
  522. debug('form content', form);
  523. try {
  524. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  525. return res.json({ status: true });
  526. }
  527. catch (err) {
  528. logger.error(err);
  529. return res.json({ status: false });
  530. }
  531. };
  532. actions.api.securitySetting = async function(req, res) {
  533. if (!req.form.isValid) {
  534. return res.json({ status: false, message: req.form.errors.join('\n') });
  535. }
  536. const form = req.form.settingForm;
  537. if (aclService.isWikiModeForced()) {
  538. logger.debug('security:restrictGuestMode will not be changed because wiki mode is forced to set');
  539. delete form['security:restrictGuestMode'];
  540. }
  541. try {
  542. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  543. return res.json({ status: true });
  544. }
  545. catch (err) {
  546. logger.error(err);
  547. return res.json({ status: false });
  548. }
  549. };
  550. actions.api.securityPassportLocalSetting = async function(req, res) {
  551. const form = req.form.settingForm;
  552. if (!req.form.isValid) {
  553. return res.json({ status: false, message: req.form.errors.join('\n') });
  554. }
  555. debug('form content', form);
  556. try {
  557. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  558. // reset strategy
  559. crowi.passportService.resetLocalStrategy();
  560. // setup strategy
  561. if (configManager.getConfig('crowi', 'security:passport-local:isEnabled')) {
  562. crowi.passportService.setupLocalStrategy(true);
  563. }
  564. }
  565. catch (err) {
  566. logger.error(err);
  567. return res.json({ status: false, message: err.message });
  568. }
  569. return res.json({ status: true });
  570. };
  571. actions.api.securityPassportLdapSetting = async function(req, res) {
  572. const form = req.form.settingForm;
  573. if (!req.form.isValid) {
  574. return res.json({ status: false, message: req.form.errors.join('\n') });
  575. }
  576. debug('form content', form);
  577. try {
  578. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  579. // reset strategy
  580. crowi.passportService.resetLdapStrategy();
  581. // setup strategy
  582. if (configManager.getConfig('crowi', 'security:passport-ldap:isEnabled')) {
  583. crowi.passportService.setupLdapStrategy(true);
  584. }
  585. }
  586. catch (err) {
  587. logger.error(err);
  588. return res.json({ status: false, message: err.message });
  589. }
  590. return res.json({ status: true });
  591. };
  592. actions.api.securityPassportSamlSetting = async(req, res) => {
  593. const form = req.form.settingForm;
  594. validateSamlSettingForm(req.form, req.t);
  595. if (!req.form.isValid) {
  596. return res.json({ status: false, message: req.form.errors.join('\n') });
  597. }
  598. debug('form content', form);
  599. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  600. // reset strategy
  601. await crowi.passportService.resetSamlStrategy();
  602. // setup strategy
  603. if (configManager.getConfig('crowi', 'security:passport-saml:isEnabled')) {
  604. try {
  605. await crowi.passportService.setupSamlStrategy(true);
  606. }
  607. catch (err) {
  608. // reset
  609. await crowi.passportService.resetSamlStrategy();
  610. return res.json({ status: false, message: err.message });
  611. }
  612. }
  613. return res.json({ status: true });
  614. };
  615. actions.api.securityPassportBasicSetting = async(req, res) => {
  616. const form = req.form.settingForm;
  617. if (!req.form.isValid) {
  618. return res.json({ status: false, message: req.form.errors.join('\n') });
  619. }
  620. debug('form content', form);
  621. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  622. // reset strategy
  623. await crowi.passportService.resetBasicStrategy();
  624. // setup strategy
  625. if (configManager.getConfig('crowi', 'security:passport-basic:isEnabled')) {
  626. try {
  627. await crowi.passportService.setupBasicStrategy(true);
  628. }
  629. catch (err) {
  630. // reset
  631. await crowi.passportService.resetBasicStrategy();
  632. return res.json({ status: false, message: err.message });
  633. }
  634. }
  635. return res.json({ status: true });
  636. };
  637. actions.api.securityPassportGoogleSetting = async(req, res) => {
  638. const form = req.form.settingForm;
  639. if (!req.form.isValid) {
  640. return res.json({ status: false, message: req.form.errors.join('\n') });
  641. }
  642. debug('form content', form);
  643. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  644. // reset strategy
  645. await crowi.passportService.resetGoogleStrategy();
  646. // setup strategy
  647. if (configManager.getConfig('crowi', 'security:passport-google:isEnabled')) {
  648. try {
  649. await crowi.passportService.setupGoogleStrategy(true);
  650. }
  651. catch (err) {
  652. // reset
  653. await crowi.passportService.resetGoogleStrategy();
  654. return res.json({ status: false, message: err.message });
  655. }
  656. }
  657. return res.json({ status: true });
  658. };
  659. actions.api.securityPassportGitHubSetting = async(req, res) => {
  660. const form = req.form.settingForm;
  661. if (!req.form.isValid) {
  662. return res.json({ status: false, message: req.form.errors.join('\n') });
  663. }
  664. debug('form content', form);
  665. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  666. // reset strategy
  667. await crowi.passportService.resetGitHubStrategy();
  668. // setup strategy
  669. if (configManager.getConfig('crowi', 'security:passport-github:isEnabled')) {
  670. try {
  671. await crowi.passportService.setupGitHubStrategy(true);
  672. }
  673. catch (err) {
  674. // reset
  675. await crowi.passportService.resetGitHubStrategy();
  676. return res.json({ status: false, message: err.message });
  677. }
  678. }
  679. return res.json({ status: true });
  680. };
  681. actions.api.securityPassportTwitterSetting = async(req, res) => {
  682. const form = req.form.settingForm;
  683. if (!req.form.isValid) {
  684. return res.json({ status: false, message: req.form.errors.join('\n') });
  685. }
  686. debug('form content', form);
  687. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  688. // reset strategy
  689. await crowi.passportService.resetTwitterStrategy();
  690. // setup strategy
  691. if (configManager.getConfig('crowi', 'security:passport-twitter:isEnabled')) {
  692. try {
  693. await crowi.passportService.setupTwitterStrategy(true);
  694. }
  695. catch (err) {
  696. // reset
  697. await crowi.passportService.resetTwitterStrategy();
  698. return res.json({ status: false, message: err.message });
  699. }
  700. }
  701. return res.json({ status: true });
  702. };
  703. actions.api.securityPassportOidcSetting = async(req, res) => {
  704. const form = req.form.settingForm;
  705. if (!req.form.isValid) {
  706. return res.json({ status: false, message: req.form.errors.join('\n') });
  707. }
  708. debug('form content', form);
  709. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  710. // reset strategy
  711. await crowi.passportService.resetOidcStrategy();
  712. // setup strategy
  713. if (configManager.getConfig('crowi', 'security:passport-oidc:isEnabled')) {
  714. try {
  715. await crowi.passportService.setupOidcStrategy(true);
  716. }
  717. catch (err) {
  718. // reset
  719. await crowi.passportService.resetOidcStrategy();
  720. return res.json({ status: false, message: err.message });
  721. }
  722. }
  723. return res.json({ status: true });
  724. };
  725. actions.api.customizeSetting = async function(req, res) {
  726. const form = req.form.settingForm;
  727. if (req.form.isValid) {
  728. debug('form content', form);
  729. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  730. customizeService.initCustomCss();
  731. customizeService.initCustomTitle();
  732. return res.json({ status: true });
  733. }
  734. return res.json({ status: false, message: req.form.errors.join('\n') });
  735. };
  736. // app.post('/_api/admin/notifications.add' , admin.api.notificationAdd);
  737. actions.api.notificationAdd = function(req, res) {
  738. const UpdatePost = crowi.model('UpdatePost');
  739. const pathPattern = req.body.pathPattern;
  740. const channel = req.body.channel;
  741. debug('notification.add', pathPattern, channel);
  742. UpdatePost.create(pathPattern, channel, req.user)
  743. .then((doc) => {
  744. debug('Successfully save updatePost', doc);
  745. // fixme: うーん
  746. doc.creator = doc.creator._id.toString();
  747. return res.json(ApiResponse.success({ updatePost: doc }));
  748. })
  749. .catch((err) => {
  750. debug('Failed to save updatePost', err);
  751. return res.json(ApiResponse.error());
  752. });
  753. };
  754. // app.post('/_api/admin/notifications.remove' , admin.api.notificationRemove);
  755. actions.api.notificationRemove = function(req, res) {
  756. const UpdatePost = crowi.model('UpdatePost');
  757. const id = req.body.id;
  758. UpdatePost.remove(id)
  759. .then(() => {
  760. debug('Successfully remove updatePost');
  761. return res.json(ApiResponse.success({}));
  762. })
  763. .catch((err) => {
  764. debug('Failed to remove updatePost', err);
  765. return res.json(ApiResponse.error());
  766. });
  767. };
  768. // app.get('/_api/admin/users.search' , admin.api.userSearch);
  769. actions.api.usersSearch = function(req, res) {
  770. const User = crowi.model('User');
  771. const email = req.query.email;
  772. User.findUsersByPartOfEmail(email, {})
  773. .then((users) => {
  774. const result = {
  775. data: users,
  776. };
  777. return res.json(ApiResponse.success(result));
  778. })
  779. .catch((err) => {
  780. return res.json(ApiResponse.error());
  781. });
  782. };
  783. actions.api.toggleIsEnabledForGlobalNotification = async(req, res) => {
  784. const id = req.query.id;
  785. const isEnabled = (req.query.isEnabled === 'true');
  786. try {
  787. if (isEnabled) {
  788. await GlobalNotificationSetting.enable(id);
  789. }
  790. else {
  791. await GlobalNotificationSetting.disable(id);
  792. }
  793. return res.json(ApiResponse.success());
  794. }
  795. catch (err) {
  796. return res.json(ApiResponse.error());
  797. }
  798. };
  799. /**
  800. * save esa settings, update config cache, and response json
  801. *
  802. * @param {*} req
  803. * @param {*} res
  804. */
  805. actions.api.importerSettingEsa = async(req, res) => {
  806. const form = req.body;
  807. const { validationResult } = require('express-validator');
  808. const errors = validationResult(req);
  809. if (!errors.isEmpty()) {
  810. return res.json(ApiResponse.error('esa.io form is blank'));
  811. }
  812. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  813. importer.initializeEsaClient(); // let it run in the back aftert res
  814. return res.json(ApiResponse.success());
  815. };
  816. /**
  817. * save qiita settings, update config cache, and response json
  818. *
  819. * @param {*} req
  820. * @param {*} res
  821. */
  822. actions.api.importerSettingQiita = async(req, res) => {
  823. const form = req.body;
  824. const { validationResult } = require('express-validator');
  825. const errors = validationResult(req);
  826. if (!errors.isEmpty()) {
  827. return res.json(ApiResponse.error('Qiita form is blank'));
  828. }
  829. await configManager.updateConfigsInTheSameNamespace('crowi', form);
  830. importer.initializeQiitaClient(); // let it run in the back aftert res
  831. return res.json(ApiResponse.success());
  832. };
  833. /**
  834. * Import all posts from esa
  835. *
  836. * @param {*} req
  837. * @param {*} res
  838. */
  839. actions.api.importDataFromEsa = async(req, res) => {
  840. const user = req.user;
  841. let errors;
  842. try {
  843. errors = await importer.importDataFromEsa(user);
  844. }
  845. catch (err) {
  846. errors = [err];
  847. }
  848. if (errors.length > 0) {
  849. return res.json(ApiResponse.error(`<br> - ${errors.join('<br> - ')}`));
  850. }
  851. return res.json(ApiResponse.success());
  852. };
  853. /**
  854. * Import all posts from qiita
  855. *
  856. * @param {*} req
  857. * @param {*} res
  858. */
  859. actions.api.importDataFromQiita = async(req, res) => {
  860. const user = req.user;
  861. let errors;
  862. try {
  863. errors = await importer.importDataFromQiita(user);
  864. }
  865. catch (err) {
  866. errors = [err];
  867. }
  868. if (errors.length > 0) {
  869. return res.json(ApiResponse.error(`<br> - ${errors.join('<br> - ')}`));
  870. }
  871. return res.json(ApiResponse.success());
  872. };
  873. /**
  874. * Test connection to esa and response result with json
  875. *
  876. * @param {*} req
  877. * @param {*} res
  878. */
  879. actions.api.testEsaAPI = async(req, res) => {
  880. try {
  881. await importer.testConnectionToEsa();
  882. return res.json(ApiResponse.success());
  883. }
  884. catch (err) {
  885. return res.json(ApiResponse.error(err));
  886. }
  887. };
  888. /**
  889. * Test connection to qiita and response result with json
  890. *
  891. * @param {*} req
  892. * @param {*} res
  893. */
  894. actions.api.testQiitaAPI = async(req, res) => {
  895. try {
  896. await importer.testConnectionToQiita();
  897. return res.json(ApiResponse.success());
  898. }
  899. catch (err) {
  900. return res.json(ApiResponse.error(err));
  901. }
  902. };
  903. actions.api.searchBuildIndex = async function(req, res) {
  904. const search = crowi.getSearcher();
  905. if (!search) {
  906. return res.json(ApiResponse.error('ElasticSearch Integration is not set up.'));
  907. }
  908. try {
  909. search.buildIndex();
  910. }
  911. catch (err) {
  912. return res.json(ApiResponse.error(err));
  913. }
  914. return res.json(ApiResponse.success());
  915. };
  916. function validateMailSetting(req, form, callback) {
  917. const mailer = crowi.mailer;
  918. const option = {
  919. host: form['mail:smtpHost'],
  920. port: form['mail:smtpPort'],
  921. };
  922. if (form['mail:smtpUser'] && form['mail:smtpPassword']) {
  923. option.auth = {
  924. user: form['mail:smtpUser'],
  925. pass: form['mail:smtpPassword'],
  926. };
  927. }
  928. if (option.port === 465) {
  929. option.secure = true;
  930. }
  931. const smtpClient = mailer.createSMTPClient(option);
  932. debug('mailer setup for validate SMTP setting', smtpClient);
  933. smtpClient.sendMail({
  934. from: form['mail:from'],
  935. to: req.user.email,
  936. subject: 'Wiki管理設定のアップデートによるメール通知',
  937. text: 'このメールは、WikiのSMTP設定のアップデートにより送信されています。',
  938. }, callback);
  939. }
  940. /**
  941. * validate setting form values for SAML
  942. *
  943. * This validation checks, for the value of each mandatory items,
  944. * whether it from the environment variables is empty and form value to update it is empty.
  945. */
  946. function validateSamlSettingForm(form, t) {
  947. for (const key of crowi.passportService.mandatoryConfigKeysForSaml) {
  948. const formValue = form.settingForm[key];
  949. if (configManager.getConfigFromEnvVars('crowi', key) === null && formValue === '') {
  950. const formItemName = t(`security_setting.form_item_name.${key}`);
  951. form.errors.push(t('form_validation.required', formItemName));
  952. }
  953. }
  954. }
  955. return actions;
  956. };