installer.ts 5.4 KB

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