installer.ts 5.0 KB

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