installer.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. import type { IPage, IUser, Lang } from '@growi/core';
  2. import { addSeconds } from 'date-fns/addSeconds';
  3. import ExtensibleCustomError from 'extensible-custom-error';
  4. import fs from 'graceful-fs';
  5. import mongoose from 'mongoose';
  6. import path from 'path';
  7. import loggerFactory from '~/utils/logger';
  8. import type Crowi from '../crowi';
  9. import { configManager } from './config-manager';
  10. const logger = loggerFactory('growi:service:installer');
  11. export class FailedToCreateAdminUserError extends ExtensibleCustomError {}
  12. export type AutoInstallOptions = {
  13. allowGuestMode?: boolean;
  14. serverDate?: Date;
  15. };
  16. export class InstallerService {
  17. crowi: Crowi;
  18. constructor(crowi: Crowi) {
  19. this.crowi = crowi;
  20. }
  21. private async initSearchIndex() {
  22. const { searchService } = this.crowi;
  23. if (searchService == null || !searchService.isReachable) {
  24. return;
  25. }
  26. try {
  27. await searchService.rebuildIndex();
  28. } catch (err) {
  29. logger.error('Rebuild index failed', err);
  30. }
  31. }
  32. private async createPage(filePath, pagePath): Promise<IPage | undefined> {
  33. const { pageService } = this.crowi;
  34. try {
  35. const markdown = fs.readFileSync(filePath);
  36. return pageService.forceCreateBySystem(pagePath, markdown.toString(), {});
  37. } catch (err) {
  38. logger.error(`Failed to create ${pagePath}`, err);
  39. }
  40. }
  41. private async createInitialPages(
  42. lang: Lang,
  43. initialPagesCreatedAt?: Date,
  44. ): Promise<any> {
  45. const { localeDir } = this.crowi;
  46. // create /Sandbox/*
  47. /*
  48. * Keep in this order to
  49. * 1. avoid creating the same pages
  50. * 2. avoid difference for order in VRT
  51. */
  52. await this.createPage(path.join(localeDir, lang, 'sandbox.md'), '/Sandbox');
  53. await this.createPage(
  54. path.join(localeDir, lang, 'sandbox-markdown.md'),
  55. '/Sandbox/Markdown',
  56. );
  57. await this.createPage(
  58. path.join(localeDir, lang, 'sandbox-bootstrap5.md'),
  59. '/Sandbox/Bootstrap5',
  60. );
  61. await this.createPage(
  62. path.join(localeDir, lang, 'sandbox-diagrams.md'),
  63. '/Sandbox/Diagrams',
  64. );
  65. await this.createPage(
  66. path.join(localeDir, lang, 'sandbox-math.md'),
  67. '/Sandbox/Math',
  68. );
  69. // update createdAt and updatedAt fields of all pages
  70. if (initialPagesCreatedAt != null) {
  71. try {
  72. // TODO typescriptize models/user.js and remove eslint-disable-next-line
  73. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  74. const Page = mongoose.model('Page') as any;
  75. // Increment timestamp to avoid difference for order in VRT
  76. const pagePaths = [
  77. '/Sandbox',
  78. '/Sandbox/Bootstrap4',
  79. '/Sandbox/Diagrams',
  80. '/Sandbox/Math',
  81. ];
  82. const promises = pagePaths.map(async (path: string, idx: number) => {
  83. const date = addSeconds(initialPagesCreatedAt, idx);
  84. return Page.update(
  85. { path },
  86. {
  87. createdAt: date,
  88. updatedAt: date,
  89. },
  90. );
  91. });
  92. await Promise.all(promises);
  93. } catch (err) {
  94. logger.error('Failed to update createdAt', err);
  95. }
  96. }
  97. try {
  98. await this.initSearchIndex();
  99. } catch (err) {
  100. logger.error('Failed to build Elasticsearch Indices', err);
  101. }
  102. }
  103. /**
  104. * Execute only once for installing application
  105. */
  106. private async initDB(
  107. globalLang: Lang,
  108. options?: AutoInstallOptions,
  109. ): Promise<void> {
  110. await configManager.updateConfigs(
  111. {
  112. 'app:installed': true,
  113. 'app:fileUpload': true,
  114. 'app:isV5Compatible': true,
  115. 'app:globalLang': globalLang,
  116. },
  117. { skipPubsub: true },
  118. );
  119. if (options?.allowGuestMode) {
  120. await configManager.updateConfig(
  121. 'security:restrictGuestMode',
  122. 'Readonly',
  123. { skipPubsub: true },
  124. );
  125. }
  126. }
  127. async install(
  128. firstAdminUserToSave: Pick<
  129. IUser,
  130. 'name' | 'username' | 'email' | 'password'
  131. >,
  132. globalLang: Lang,
  133. options?: AutoInstallOptions,
  134. ): Promise<IUser> {
  135. await this.initDB(globalLang, options);
  136. const User = mongoose.model<IUser, { createUser }>('User');
  137. // create portal page for '/' before creating admin user
  138. try {
  139. await this.createPage(
  140. path.join(this.crowi.localeDir, globalLang, 'welcome.md'),
  141. '/',
  142. );
  143. } catch (err) {
  144. logger.error(err);
  145. throw err;
  146. }
  147. try {
  148. // create first admin user
  149. const { name, username, email, password } = firstAdminUserToSave;
  150. const adminUser = await User.createUser(
  151. name,
  152. username,
  153. email,
  154. password,
  155. globalLang,
  156. );
  157. await (adminUser as any).asyncGrantAdmin();
  158. // create initial pages
  159. await this.createInitialPages(globalLang, options?.serverDate);
  160. return adminUser;
  161. } catch (err) {
  162. logger.error(err);
  163. throw new FailedToCreateAdminUserError(err);
  164. }
  165. }
  166. }