installer.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. import path from 'path';
  2. import type {
  3. Lang, IPage, IUser,
  4. } from '@growi/core';
  5. import { addSeconds } from 'date-fns/addSeconds';
  6. import ExtensibleCustomError from 'extensible-custom-error';
  7. import fs from 'graceful-fs';
  8. import mongoose from 'mongoose';
  9. import loggerFactory from '~/utils/logger';
  10. import type Crowi from '../crowi';
  11. import { generateConfigsForInstalling } from '../models/config';
  12. import { configManager } from './config-manager';
  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. crowi: Crowi;
  22. constructor(crowi: Crowi) {
  23. this.crowi = crowi;
  24. }
  25. private async initSearchIndex() {
  26. const { searchService } = this.crowi;
  27. if (searchService == null || !searchService.isReachable) {
  28. return;
  29. }
  30. try {
  31. await searchService.rebuildIndex();
  32. }
  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 markdown = fs.readFileSync(filePath);
  41. return pageService.forceCreateBySystem(pagePath, markdown.toString(), {});
  42. }
  43. catch (err) {
  44. logger.error(`Failed to create ${pagePath}`, err);
  45. }
  46. }
  47. private async createInitialPages(lang: Lang, initialPagesCreatedAt?: Date): Promise<any> {
  48. const { localeDir } = this.crowi;
  49. // create /Sandbox/*
  50. /*
  51. * Keep in this order to
  52. * 1. avoid creating the same pages
  53. * 2. avoid difference for order in VRT
  54. */
  55. await this.createPage(path.join(localeDir, lang, 'sandbox.md'), '/Sandbox');
  56. await this.createPage(path.join(localeDir, lang, 'sandbox-markdown.md'), '/Sandbox/Markdown');
  57. await this.createPage(path.join(localeDir, lang, 'sandbox-bootstrap5.md'), '/Sandbox/Bootstrap5');
  58. await this.createPage(path.join(localeDir, lang, 'sandbox-diagrams.md'), '/Sandbox/Diagrams');
  59. await this.createPage(path.join(localeDir, lang, 'sandbox-math.md'), '/Sandbox/Math');
  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. // Increment timestamp to avoid difference for order in VRT
  67. const pagePaths = ['/Sandbox', '/Sandbox/Bootstrap4', '/Sandbox/Diagrams', '/Sandbox/Math'];
  68. const promises = pagePaths.map(async(path: string, idx: number) => {
  69. const date = addSeconds(initialPagesCreatedAt, idx);
  70. return Page.update(
  71. { path },
  72. {
  73. createdAt: date,
  74. updatedAt: date,
  75. },
  76. );
  77. });
  78. await Promise.all(promises);
  79. }
  80. catch (err) {
  81. logger.error('Failed to update createdAt', err);
  82. }
  83. }
  84. try {
  85. await this.initSearchIndex();
  86. }
  87. catch (err) {
  88. logger.error('Failed to build Elasticsearch Indices', err);
  89. }
  90. }
  91. /**
  92. * Execute only once for installing application
  93. */
  94. private async initDB(globalLang: Lang, options?: AutoInstallOptions): Promise<void> {
  95. const initialConfig = generateConfigsForInstalling();
  96. initialConfig['app:globalLang'] = globalLang;
  97. if (options?.allowGuestMode) {
  98. initialConfig['security:restrictGuestMode'] = 'Readonly';
  99. }
  100. return configManager.updateConfigsInTheSameNamespace('crowi', initialConfig, true);
  101. }
  102. async install(firstAdminUserToSave: Pick<IUser, 'name' | 'username' | 'email' | 'password'>, globalLang: Lang, options?: AutoInstallOptions): Promise<IUser> {
  103. await this.initDB(globalLang, options);
  104. const User = mongoose.model<IUser, { createUser }>('User');
  105. // create portal page for '/' before creating admin user
  106. try {
  107. await this.createPage(
  108. path.join(this.crowi.localeDir, globalLang, 'welcome.md'),
  109. '/',
  110. );
  111. }
  112. catch (err) {
  113. logger.error(err);
  114. throw err;
  115. }
  116. try {
  117. // create first admin user
  118. const {
  119. name, username, email, password,
  120. } = firstAdminUserToSave;
  121. const adminUser = await User.createUser(name, username, email, password, globalLang);
  122. await (adminUser as any).asyncGrantAdmin();
  123. // create initial pages
  124. await this.createInitialPages(globalLang, options?.serverDate);
  125. return adminUser;
  126. }
  127. catch (err) {
  128. logger.error(err);
  129. throw new FailedToCreateAdminUserError(err);
  130. }
  131. }
  132. }