installer.ts 5.5 KB

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