installer.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. import mongoose from 'mongoose';
  2. import fs from 'graceful-fs';
  3. import path from 'path';
  4. import ExtensibleCustomError from 'extensible-custom-error';
  5. import { IPage } from '~/interfaces/page';
  6. import { IUser } from '~/interfaces/user';
  7. import { Lang } from '~/interfaces/lang';
  8. import loggerFactory from '~/utils/logger';
  9. import { generateConfigsForInstalling } from '../models/config';
  10. import SearchService from './search';
  11. import ConfigManager from './config-manager';
  12. const logger = loggerFactory('growi:service:installer');
  13. export class FailedToCreateAdminUserError extends ExtensibleCustomError {
  14. }
  15. export class InstallerService {
  16. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  17. crowi: any;
  18. // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
  19. constructor(crowi: any) {
  20. this.crowi = crowi;
  21. }
  22. private async initSearchIndex() {
  23. const searchService: SearchService = this.crowi.searchService;
  24. if (!searchService.isReachable) {
  25. return;
  26. }
  27. try {
  28. await searchService.rebuildIndex();
  29. }
  30. catch (err) {
  31. logger.error('Rebuild index failed', err);
  32. }
  33. }
  34. private async createPage(filePath, pagePath, owner): Promise<IPage|undefined> {
  35. // TODO typescriptize models/user.js and remove eslint-disable-next-line
  36. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  37. const Page = mongoose.model('Page') as any;
  38. try {
  39. const markdown = fs.readFileSync(filePath);
  40. return Page.create(pagePath, markdown, owner, {}) as IPage;
  41. }
  42. catch (err) {
  43. logger.error(`Failed to create ${pagePath}`, err);
  44. }
  45. }
  46. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  47. private async createInitialPages(owner, lang: Lang, initialPagesCreatedAt?: Date): Promise<any> {
  48. const { localeDir } = this.crowi;
  49. // create /Sandbox/*
  50. /*
  51. * Keep in this order to avoid creating the same pages
  52. */
  53. await this.createPage(path.join(localeDir, lang, 'sandbox.md'), '/Sandbox', owner);
  54. await Promise.all([
  55. this.createPage(path.join(localeDir, lang, 'sandbox-diagrams.md'), '/Sandbox/Diagrams', owner),
  56. this.createPage(path.join(localeDir, lang, 'sandbox-bootstrap4.md'), '/Sandbox/Bootstrap4', owner),
  57. this.createPage(path.join(localeDir, lang, 'sandbox-math.md'), '/Sandbox/Math', owner),
  58. ]);
  59. // update createdAt and updatedAt fields of all pages
  60. if (initialPagesCreatedAt != null) {
  61. try {
  62. // TODO typescriptize models/user.js and remove eslint-disable-next-line
  63. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  64. const Page = mongoose.model('Page') as any;
  65. await Page.updateMany({}, { createdAt: initialPagesCreatedAt, updatedAt: initialPagesCreatedAt });
  66. }
  67. catch (err) {
  68. logger.error('Failed to update createdAt', err);
  69. }
  70. }
  71. try {
  72. await this.initSearchIndex();
  73. }
  74. catch (err) {
  75. logger.error('Failed to build Elasticsearch Indices', err);
  76. }
  77. }
  78. /**
  79. * Execute only once for installing application
  80. */
  81. private async initDB(globalLang: Lang): Promise<void> {
  82. const configManager: ConfigManager = this.crowi.configManager;
  83. const initialConfig = generateConfigsForInstalling();
  84. initialConfig['app:globalLang'] = globalLang;
  85. return configManager.updateConfigsInTheSameNamespace('crowi', initialConfig, true);
  86. }
  87. async install(firstAdminUserToSave: IUser, globalLang: Lang, initialPagesCreatedAt?: Date): Promise<IUser> {
  88. await this.initDB(globalLang);
  89. // TODO typescriptize models/user.js and remove eslint-disable-next-line
  90. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  91. const User = mongoose.model('User') as any;
  92. const Page = mongoose.model('Page') as any;
  93. // create portal page for '/' before creating admin user
  94. await this.createPage(
  95. path.join(this.crowi.localeDir, globalLang, 'welcome.md'),
  96. '/',
  97. { _id: '000000000000000000000000' }, // use 0 as a mock user id
  98. );
  99. // create first admin user
  100. // TODO: with transaction
  101. let adminUser;
  102. try {
  103. const {
  104. name, username, email, password,
  105. } = firstAdminUserToSave;
  106. adminUser = await User.createUser(name, username, email, password, globalLang);
  107. await adminUser.asyncMakeAdmin();
  108. }
  109. catch (err) {
  110. throw new FailedToCreateAdminUserError(err);
  111. }
  112. // add owner after creating admin user
  113. const Revision = this.crowi.model('Revision');
  114. const rootPage = await Page.findOne({ path: '/' });
  115. const rootRevision = await Revision.findOne({ path: '/' });
  116. rootPage.creator = adminUser._id;
  117. rootPage.lastUpdateUser = adminUser._id;
  118. rootRevision.creator = adminUser._id;
  119. await Promise.all([rootPage.save(), rootRevision.save()]);
  120. // create initial pages
  121. await this.createInitialPages(adminUser, globalLang, initialPagesCreatedAt);
  122. return adminUser;
  123. }
  124. }