commons.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. import type { ColorScheme, IUserHasId, Locale } from '@growi/core';
  2. import { Lang, AllLang } from '@growi/core';
  3. import { DevidedPagePath } from '@growi/core/dist/models';
  4. import { isServer } from '@growi/core/dist/utils';
  5. import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
  6. import type { SSRConfig, UserConfig } from 'next-i18next';
  7. import * as nextI18NextConfig from '^/config/next-i18next.config';
  8. import { type SupportedActionType } from '~/interfaces/activity';
  9. import type { CrowiRequest } from '~/interfaces/crowi-request';
  10. import type { ISidebarConfig } from '~/interfaces/sidebar-config';
  11. import type { IUserUISettings } from '~/interfaces/user-ui-settings';
  12. import type { PageDocument } from '~/server/models/page';
  13. import type { UserUISettingsDocument } from '~/server/models/user-ui-settings';
  14. import { detectLocaleFromBrowserAcceptLanguage } from '~/server/util/locale-utils';
  15. import {
  16. useCurrentProductNavWidth, useCurrentSidebarContents, usePreferCollapsedMode,
  17. } from '~/stores/ui';
  18. import { getGrowiVersion } from '~/utils/growi-version';
  19. export type CommonProps = {
  20. namespacesRequired: string[], // i18next
  21. currentPathname: string,
  22. appTitle: string,
  23. siteUrl: string | undefined,
  24. confidential: string,
  25. customTitleTemplate: string,
  26. csrfToken: string,
  27. isContainerFluid: boolean,
  28. growiVersion: string,
  29. isMaintenanceMode: boolean,
  30. redirectDestination: string | null,
  31. isDefaultLogo: boolean,
  32. growiCloudUri: string | undefined,
  33. isAccessDeniedForNonAdminUser?: boolean,
  34. currentUser?: IUserHasId,
  35. forcedColorScheme?: ColorScheme,
  36. userUISettings?: IUserUISettings
  37. } & Partial<SSRConfig>;
  38. // eslint-disable-next-line max-len
  39. export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(context: GetServerSidePropsContext) => {
  40. const getModelSafely = await import('~/server/util/mongoose-utils').then(mod => mod.getModelSafely);
  41. const req = context.req as CrowiRequest;
  42. const { crowi, user } = req;
  43. const {
  44. appService, configManager, customizeService, attachmentService,
  45. } = crowi;
  46. const url = new URL(context.resolvedUrl, 'http://example.com');
  47. const currentPathname = decodeURIComponent(url.pathname);
  48. const isMaintenanceMode = appService.isMaintenanceMode();
  49. let currentUser;
  50. if (user != null) {
  51. currentUser = user.toObject();
  52. }
  53. // Redirect destination for page transition by next/link
  54. let redirectDestination: string | null = null;
  55. if (!crowi.aclService.isGuestAllowedToRead() && currentUser == null) {
  56. redirectDestination = '/login';
  57. }
  58. else if (!isMaintenanceMode && currentPathname === '/maintenance') {
  59. redirectDestination = '/';
  60. }
  61. else if (isMaintenanceMode && !currentPathname.match('/admin/*') && !(currentPathname === '/maintenance')) {
  62. redirectDestination = '/maintenance';
  63. }
  64. else {
  65. redirectDestination = null;
  66. }
  67. const isCustomizedLogoUploaded = await attachmentService.isBrandLogoExist();
  68. const isDefaultLogo = crowi.configManager.getConfig('customize:isDefaultLogo') || !isCustomizedLogoUploaded;
  69. const forcedColorScheme = crowi.customizeService.forcedColorScheme;
  70. // retrieve UserUISett ings
  71. const UserUISettings = getModelSafely<UserUISettingsDocument>('UserUISettings');
  72. const userUISettings = user != null && UserUISettings != null
  73. ? await UserUISettings.findOne({ user: user._id }).exec()
  74. : req.session.uiSettings; // for guests
  75. const props: CommonProps = {
  76. namespacesRequired: ['translation'],
  77. currentPathname,
  78. appTitle: appService.getAppTitle(),
  79. siteUrl: configManager.getConfig('app:siteUrl'), // DON'T USE appService.getSiteUrl()
  80. confidential: appService.getAppConfidential() || '',
  81. customTitleTemplate: customizeService.customTitleTemplate,
  82. csrfToken: req.csrfToken(),
  83. isContainerFluid: configManager.getConfig('customize:isContainerFluid') ?? false,
  84. growiVersion: getGrowiVersion(),
  85. isMaintenanceMode,
  86. redirectDestination,
  87. currentUser,
  88. isDefaultLogo,
  89. forcedColorScheme,
  90. growiCloudUri: configManager.getConfig('app:growiCloudUri'),
  91. userUISettings: userUISettings?.toObject?.() ?? userUISettings,
  92. };
  93. return { props };
  94. };
  95. export type LangMap = {
  96. readonly [key in Lang]: Locale;
  97. };
  98. export const langMap: LangMap = {
  99. [Lang.ja_JP]: 'ja-JP',
  100. [Lang.en_US]: 'en-US',
  101. [Lang.zh_CN]: 'zh-CN',
  102. [Lang.fr_FR]: 'fr-FR',
  103. } as const;
  104. // use this function to translate content
  105. export const getLangAtServerSide = (req: CrowiRequest): Lang => {
  106. const { user, headers } = req;
  107. const { configManager } = req.crowi;
  108. return user == null ? detectLocaleFromBrowserAcceptLanguage(headers)
  109. : (user.lang ?? configManager.getConfig('app:globalLang') ?? Lang.en_US) ?? Lang.en_US;
  110. };
  111. // use this function to get locale for html lang attribute
  112. export const getLocaleAtServerSide = (req: CrowiRequest): Locale => {
  113. return langMap[getLangAtServerSide(req)];
  114. };
  115. export const getNextI18NextConfig = async(
  116. // 'serverSideTranslations' method should be given from Next.js Page
  117. // because importing it in this file causes https://github.com/isaachinman/next-i18next/issues/1545
  118. serverSideTranslations: (
  119. initialLocale: string, namespacesRequired?: string[] | undefined, configOverride?: UserConfig | null, extraLocales?: string[] | false
  120. ) => Promise<SSRConfig>,
  121. context: GetServerSidePropsContext, namespacesRequired?: string[] | undefined, preloadAllLang = false,
  122. ): Promise<SSRConfig> => {
  123. // determine language
  124. const req: CrowiRequest = context.req as CrowiRequest;
  125. const lang = getLangAtServerSide(req);
  126. const namespaces = ['commons'];
  127. if (namespacesRequired != null) {
  128. namespaces.push(...namespacesRequired);
  129. }
  130. // TODO: deprecate 'translation.json' in the future
  131. else {
  132. namespaces.push('translation');
  133. }
  134. // The first argument must be a language code with an underscore, such as en_US
  135. return serverSideTranslations(lang, namespaces, nextI18NextConfig, preloadAllLang ? AllLang : false);
  136. };
  137. /**
  138. * Generate whole title string for the specified title
  139. * @param props
  140. * @param title
  141. */
  142. export const generateCustomTitle = (props: CommonProps, title: string): string => {
  143. return props.customTitleTemplate
  144. .replace('{{sitename}}', props.appTitle)
  145. .replace('{{pagepath}}', title)
  146. .replace('{{pagename}}', title);
  147. };
  148. /**
  149. * Generate whole title string for the specified page path
  150. * @param props
  151. * @param pagePath
  152. */
  153. export const generateCustomTitleForPage = (props: CommonProps, pagePath: string): string => {
  154. const dPagePath = new DevidedPagePath(pagePath, true, true);
  155. return props.customTitleTemplate
  156. .replace('{{sitename}}', props.appTitle)
  157. .replace('{{pagepath}}', pagePath)
  158. .replace('{{pagename}}', dPagePath.latter);
  159. };
  160. export const useInitSidebarConfig = (sidebarConfig: ISidebarConfig, userUISettings?: IUserUISettings): void => {
  161. // UserUISettings
  162. usePreferCollapsedMode(userUISettings?.preferCollapsedModeByUser ?? sidebarConfig.isSidebarCollapsedMode);
  163. useCurrentSidebarContents(userUISettings?.currentSidebarContents);
  164. useCurrentProductNavWidth(userUISettings?.currentProductNavWidth);
  165. };
  166. export const skipSSR = async(page: PageDocument, ssrMaxRevisionBodyLength: number): Promise<boolean> => {
  167. if (!isServer()) {
  168. throw new Error('This method is not available on the client-side');
  169. }
  170. const latestRevisionBodyLength = await page.getLatestRevisionBodyLength();
  171. if (latestRevisionBodyLength == null) {
  172. return true;
  173. }
  174. return ssrMaxRevisionBodyLength < latestRevisionBodyLength;
  175. };
  176. export const addActivity = async(context: GetServerSidePropsContext, action: SupportedActionType): Promise<void> => {
  177. const req = context.req as CrowiRequest;
  178. const parameters = {
  179. ip: req.ip,
  180. endpoint: req.originalUrl,
  181. action,
  182. user: req.user?._id,
  183. snapshot: {
  184. username: req.user?.username,
  185. },
  186. };
  187. await req.crowi.activityService.createActivity(parameters);
  188. };