linking-section.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import { useState, ElementType } from 'react'
  2. import { Alert } from 'react-bootstrap'
  3. import { useTranslation } from 'react-i18next'
  4. import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
  5. import { useSSOContext, SSOSubscription } from '../context/sso-context'
  6. import { SSOLinkingWidget } from './linking/sso-widget'
  7. import getMeta from '../../../utils/meta'
  8. import { useBroadcastUser } from '@/shared/hooks/user-channel/use-broadcast-user'
  9. function LinkingSection() {
  10. useBroadcastUser()
  11. const { t } = useTranslation()
  12. const { subscriptions } = useSSOContext()
  13. const ssoErrorMessage = getMeta('ol-ssoErrorMessage') as string
  14. const projectSyncSuccessMessage = getMeta(
  15. 'ol-projectSyncSuccessMessage'
  16. ) as string
  17. const [integrationLinkingWidgets] = useState<any[]>(
  18. () =>
  19. getMeta('integrationLinkingWidgets') ||
  20. importOverleafModules('integrationLinkingWidgets')
  21. )
  22. const [referenceLinkingWidgets] = useState<any[]>(
  23. () =>
  24. getMeta('referenceLinkingWidgets') ||
  25. importOverleafModules('referenceLinkingWidgets')
  26. )
  27. const oauth2ServerComponents = importOverleafModules('oauth2Server') as {
  28. import: { default: ElementType }
  29. path: string
  30. }[]
  31. const showPersonalAccessTokenComponents: boolean =
  32. getMeta('ol-showPersonalAccessToken') ||
  33. getMeta('ol-optionalPersonalAccessToken')
  34. const allIntegrationLinkingWidgets = showPersonalAccessTokenComponents
  35. ? integrationLinkingWidgets.concat(oauth2ServerComponents)
  36. : integrationLinkingWidgets
  37. const hasIntegrationLinkingSection = allIntegrationLinkingWidgets.length
  38. const hasReferencesLinkingSection = referenceLinkingWidgets.length
  39. // Filter out SSO providers that are not allowed to be linked by
  40. // managed users. Allow unlinking them if they are already linked.
  41. const hideGoogleSSO = getMeta('ol-cannot-link-google-sso')
  42. const hideOtherThirdPartySSO = getMeta('ol-cannot-link-other-third-party-sso')
  43. for (const providerId in subscriptions) {
  44. const isLinked = subscriptions[providerId].linked
  45. if (providerId === 'google') {
  46. if (hideGoogleSSO && !isLinked) {
  47. delete subscriptions[providerId]
  48. }
  49. } else {
  50. if (hideOtherThirdPartySSO && !isLinked) {
  51. delete subscriptions[providerId]
  52. }
  53. }
  54. }
  55. const hasSSOLinkingSection = Object.keys(subscriptions).length > 0
  56. if (
  57. !hasIntegrationLinkingSection &&
  58. !hasReferencesLinkingSection &&
  59. !hasSSOLinkingSection
  60. ) {
  61. return null
  62. }
  63. return (
  64. <>
  65. <h3>{t('integrations')}</h3>
  66. <p className="small">{t('linked_accounts_explained')}</p>
  67. {hasIntegrationLinkingSection ? (
  68. <>
  69. <h3 id="project-sync" className="text-capitalize">
  70. {t('project_synchronisation')}
  71. </h3>
  72. {projectSyncSuccessMessage ? (
  73. <Alert bsStyle="success">{projectSyncSuccessMessage}</Alert>
  74. ) : null}
  75. <div className="settings-widgets-container">
  76. {allIntegrationLinkingWidgets.map(
  77. ({ import: importObject, path }, widgetIndex) => (
  78. <ModuleLinkingWidget
  79. key={Object.keys(importObject)[0]}
  80. ModuleComponent={Object.values(importObject)[0]}
  81. isLast={
  82. widgetIndex === allIntegrationLinkingWidgets.length - 1
  83. }
  84. />
  85. )
  86. )}
  87. </div>
  88. </>
  89. ) : null}
  90. {hasReferencesLinkingSection ? (
  91. <>
  92. <h3 id="references" className="text-capitalize">
  93. {t('reference_managers')}
  94. </h3>
  95. <div className="settings-widgets-container">
  96. {referenceLinkingWidgets.map(
  97. ({ import: importObject, path }, widgetIndex) => (
  98. <ModuleLinkingWidget
  99. key={Object.keys(importObject)[0]}
  100. ModuleComponent={Object.values(importObject)[0]}
  101. isLast={widgetIndex === referenceLinkingWidgets.length - 1}
  102. />
  103. )
  104. )}
  105. </div>
  106. </>
  107. ) : null}
  108. {hasSSOLinkingSection ? (
  109. <>
  110. <h3 id="linked-accounts" className="text-capitalize">
  111. {t('linked_accounts')}
  112. </h3>
  113. {ssoErrorMessage ? (
  114. <Alert bsStyle="danger">
  115. {t('sso_link_error')}: {ssoErrorMessage}
  116. </Alert>
  117. ) : null}
  118. <div className="settings-widgets-container">
  119. {Object.values(subscriptions).map(
  120. (subscription, subscriptionIndex) => (
  121. <SSOLinkingWidgetContainer
  122. key={subscription.providerId}
  123. subscription={subscription}
  124. isLast={
  125. subscriptionIndex === Object.keys(subscriptions).length - 1
  126. }
  127. />
  128. )
  129. )}
  130. </div>
  131. </>
  132. ) : null}
  133. {hasIntegrationLinkingSection ||
  134. hasReferencesLinkingSection ||
  135. hasSSOLinkingSection ? (
  136. <hr />
  137. ) : null}
  138. </>
  139. )
  140. }
  141. type LinkingWidgetProps = {
  142. ModuleComponent: any
  143. isLast: boolean
  144. }
  145. function ModuleLinkingWidget({ ModuleComponent, isLast }: LinkingWidgetProps) {
  146. return (
  147. <>
  148. <ModuleComponent />
  149. {isLast ? null : <hr />}
  150. </>
  151. )
  152. }
  153. type SSOLinkingWidgetContainerProps = {
  154. subscription: SSOSubscription
  155. isLast: boolean
  156. }
  157. function SSOLinkingWidgetContainer({
  158. subscription,
  159. isLast,
  160. }: SSOLinkingWidgetContainerProps) {
  161. const { t } = useTranslation()
  162. const { unlink } = useSSOContext()
  163. let description = ''
  164. switch (subscription.providerId) {
  165. case 'collabratec':
  166. description = t('linked_collabratec_description')
  167. break
  168. case 'google':
  169. description = `${t('login_with_service', {
  170. service: subscription.provider.name,
  171. })}.`
  172. break
  173. case 'twitter':
  174. description = t('login_with_service_will_stop_working_soon', {
  175. service: subscription.provider.name,
  176. })
  177. break
  178. case 'orcid':
  179. description = t('oauth_orcid_description')
  180. break
  181. }
  182. return (
  183. <>
  184. <SSOLinkingWidget
  185. providerId={subscription.providerId}
  186. title={subscription.provider.name}
  187. description={description}
  188. helpPath={subscription.provider.descriptionOptions?.link}
  189. linked={subscription.linked}
  190. linkPath={subscription.provider.linkPath}
  191. onUnlink={() => unlink(subscription.providerId)}
  192. />
  193. {isLast ? null : <hr />}
  194. </>
  195. )
  196. }
  197. export default LinkingSection