commons.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
  2. import type { ColorScheme, IUserHasId } from '@growi/core';
  3. import mongoose from 'mongoose';
  4. import type { CrowiRequest } from '~/interfaces/crowi-request';
  5. import { getGrowiVersion } from '~/utils/growi-version';
  6. import loggerFactory from '~/utils/logger';
  7. const logger = loggerFactory('growi:pages:common-props:commons');
  8. export type CommonInitialProps = {
  9. isNextjsRoutingTypeInitial: true;
  10. appTitle: string;
  11. siteUrl: string | undefined;
  12. siteUrlWithEmptyValueWarn: string;
  13. confidential: string;
  14. growiVersion: string;
  15. isDefaultLogo: boolean;
  16. customTitleTemplate: string;
  17. growiCloudUri: string | undefined;
  18. growiAppIdForGrowiCloud: number | undefined;
  19. forcedColorScheme?: ColorScheme;
  20. };
  21. export const getServerSideCommonInitialProps: GetServerSideProps<
  22. CommonInitialProps
  23. > = async (context: GetServerSidePropsContext) => {
  24. const req = context.req as CrowiRequest;
  25. const { crowi } = req;
  26. const {
  27. appService,
  28. configManager,
  29. attachmentService,
  30. customizeService,
  31. growiInfoService,
  32. } = crowi;
  33. const isCustomizedLogoUploaded = await attachmentService.isBrandLogoExist();
  34. const isDefaultLogo =
  35. crowi.configManager.getConfig('customize:isDefaultLogo') ||
  36. !isCustomizedLogoUploaded;
  37. const forcedColorScheme = crowi.customizeService.forcedColorScheme;
  38. return {
  39. props: {
  40. isNextjsRoutingTypeInitial: true,
  41. appTitle: appService.getAppTitle(),
  42. siteUrl: configManager.getConfig('app:siteUrl'),
  43. siteUrlWithEmptyValueWarn: growiInfoService.getSiteUrl(),
  44. confidential: appService.getAppConfidential() || '',
  45. growiVersion: getGrowiVersion(),
  46. isDefaultLogo,
  47. customTitleTemplate: customizeService.customTitleTemplate,
  48. growiCloudUri: configManager.getConfig('app:growiCloudUri'),
  49. growiAppIdForGrowiCloud: configManager.getConfig(
  50. 'app:growiAppIdForCloud',
  51. ),
  52. forcedColorScheme,
  53. },
  54. };
  55. };
  56. export const isCommonInitialProps = (
  57. props: unknown,
  58. ): props is CommonInitialProps => {
  59. if (typeof props !== 'object' || props === null) {
  60. logger.warn('isCommonInitialProps: props is not an object or is null');
  61. return false;
  62. }
  63. const p = props as Record<string, unknown>;
  64. // Essential properties validation
  65. if (p.isNextjsRoutingTypeInitial !== true) {
  66. logger.warn(
  67. 'isCommonInitialProps: isNextjsRoutingTypeInitial is not true',
  68. { isNextjsRoutingTypeInitial: p.isNextjsRoutingTypeInitial },
  69. );
  70. return false;
  71. }
  72. return true;
  73. };
  74. export type CommonEachProps = {
  75. currentPathname: string;
  76. nextjsRoutingPage?: string; // must be set by each page
  77. currentUser?: IUserHasId;
  78. isMaintenanceMode: boolean;
  79. redirectDestination?: string | null;
  80. };
  81. /**
  82. * Type guard for SameRouteEachProps validation
  83. * Lightweight validation for same-route navigation
  84. */
  85. function isValidCommonEachRouteProps(
  86. props: unknown,
  87. shouldContainNextjsRoutingPage = false,
  88. ): props is CommonEachProps {
  89. if (typeof props !== 'object' || props === null) {
  90. logger.warn(
  91. 'isValidCommonEachRouteProps: props is not an object or is null',
  92. );
  93. return false;
  94. }
  95. const p = props as Record<string, unknown>;
  96. // Essential properties validation
  97. if (shouldContainNextjsRoutingPage) {
  98. if (
  99. typeof p.nextjsRoutingPage !== 'string' &&
  100. p.nextjsRoutingPage !== undefined
  101. ) {
  102. logger.warn(
  103. 'isValidCommonEachRouteProps: nextjsRoutingPage is not a string or null',
  104. { nextjsRoutingPage: p.nextjsRoutingPage },
  105. );
  106. return false;
  107. }
  108. }
  109. if (typeof p.currentPathname !== 'string') {
  110. logger.warn(
  111. 'isValidCommonEachRouteProps: currentPathname is not a string',
  112. { currentPathname: p.currentPathname },
  113. );
  114. return false;
  115. }
  116. if (typeof p.isMaintenanceMode !== 'boolean') {
  117. logger.warn(
  118. 'isValidCommonEachRouteProps: isMaintenanceMode is not a boolean',
  119. { isMaintenanceMode: p.isMaintenanceMode },
  120. );
  121. return false;
  122. }
  123. return true;
  124. }
  125. export const getServerSideCommonEachProps = async (
  126. context: GetServerSidePropsContext,
  127. nextjsRoutingPage?: string,
  128. ): ReturnType<GetServerSideProps<CommonEachProps>> => {
  129. const req = context.req as CrowiRequest;
  130. const { crowi, user } = req;
  131. const { appService } = crowi;
  132. const url = new URL(context.resolvedUrl, 'http://example.com');
  133. const currentPathname = decodeURIComponent(url.pathname);
  134. const isMaintenanceMode = appService.isMaintenanceMode();
  135. let currentUser: IUserHasId | undefined;
  136. if (user != null) {
  137. const User = mongoose.model<IUserHasId>('User');
  138. const userData = await User.findById(user.id).populate({
  139. path: 'imageAttachment',
  140. select: 'filePathProxied',
  141. });
  142. currentUser = userData?.toObject();
  143. }
  144. // Redirect destination for page transition by next/link
  145. let redirectDestination: string | null = null;
  146. if (!crowi.aclService.isGuestAllowedToRead() && currentUser == null) {
  147. redirectDestination = '/login';
  148. } else if (!isMaintenanceMode && currentPathname === '/maintenance') {
  149. redirectDestination = '/';
  150. } else if (
  151. isMaintenanceMode &&
  152. !currentPathname.match('/admin/*') &&
  153. !(currentPathname === '/maintenance')
  154. ) {
  155. redirectDestination = '/maintenance';
  156. } else {
  157. redirectDestination = null;
  158. }
  159. const props = {
  160. currentPathname,
  161. nextjsRoutingPage,
  162. currentUser,
  163. isMaintenanceMode,
  164. redirectDestination,
  165. };
  166. const shouldContainNextjsRoutingPage = nextjsRoutingPage != null;
  167. if (!isValidCommonEachRouteProps(props, shouldContainNextjsRoutingPage)) {
  168. throw new Error('Invalid common each route props structure');
  169. }
  170. return { props };
  171. };