Add optional personal access tokens for git bridge (#15209)

GitOrigin-RevId: 50d4c0e11728e014e81172c062a3b22fefa6286c
This commit is contained in:
Alf Eaton 2023-10-19 09:26:45 +01:00 committed by Copybot
parent 6408d150d5
commit 749aef1c6f
20 changed files with 198 additions and 73 deletions

View file

@ -648,6 +648,21 @@ const ProjectController = {
}
)
},
personalAccessTokenAssignment(cb) {
SplitTestHandler.getAssignment(
req,
res,
'personal-access-token',
(error, assignment) => {
// do not fail editor load if assignment fails
if (error) {
cb(null, { variant: 'default' })
} else {
cb(null, assignment)
}
}
)
},
historyViewAssignment(cb) {
SplitTestHandler.getAssignment(
req,
@ -716,6 +731,7 @@ const ProjectController = {
historyViewAssignment,
reviewPanelAssignment,
idePageAssignment,
personalAccessTokenAssignment,
projectTags,
}
) => {
@ -821,6 +837,10 @@ const ProjectController = {
!Features.hasFeature('saas') ||
req.query?.personal_access_token === 'true'
const optionalPersonalAccessToken =
!showPersonalAccessToken &&
personalAccessTokenAssignment.variant === 'enabled' // `?personal-access-token=enabled`
const idePageReact = idePageAssignment.variant === 'react'
const template =
@ -903,6 +923,7 @@ const ProjectController = {
isReviewPanelReact: reviewPanelAssignment.variant === 'react',
idePageReact,
showPersonalAccessToken,
optionalPersonalAccessToken,
hasTrackChangesFeature: Features.hasFeature('track-changes'),
projectTags,
})

View file

@ -10,6 +10,7 @@ const SubscriptionLocator = require('../Subscription/SubscriptionLocator')
const _ = require('lodash')
const { expressify } = require('../../util/promises')
const Features = require('../../infrastructure/Features')
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
async function settingsPage(req, res) {
const userId = SessionManager.getLoggedInUserId(req.session)
@ -69,8 +70,27 @@ async function settingsPage(req, res) {
const showPersonalAccessToken =
!Features.hasFeature('saas') || req.query?.personal_access_token === 'true'
// if not already enabled, use a split test to determine whether to offer personal access tokens
let optionalPersonalAccessToken = false
if (!showPersonalAccessToken) {
try {
const { variant } = await SplitTestHandler.promises.getAssignment(
req,
res,
'personal-access-token'
)
optionalPersonalAccessToken = variant === 'enabled' // `?personal-access-token=enabled`
} catch (error) {
logger.error(
{ err: error },
'Failed to get personal-access-token split test assignment'
)
}
}
let personalAccessTokens
if (showPersonalAccessToken) {
if (showPersonalAccessToken || optionalPersonalAccessToken) {
try {
// require this here because module may not be included in some versions
const PersonalAccessTokenManager = require('../../../../modules/oauth2-server/app/src/OAuthPersonalAccessTokenManager')
@ -133,6 +153,7 @@ async function settingsPage(req, res) {
thirdPartyIds: UserPagesController._restructureThirdPartyIds(user),
projectSyncSuccessMessage,
showPersonalAccessToken,
optionalPersonalAccessToken,
personalAccessTokens,
emailAddressLimit: Settings.emailAddressLimit,
isManagedAccount: !!req.managedBy,

View file

@ -36,6 +36,7 @@ meta(name="ol-showSupport", data-type="boolean" content=showSupport)
meta(name="ol-showTemplatesServerPro", data-type="boolean" content=showTemplatesServerPro)
meta(name="ol-showCM6SwitchAwaySurvey", data-type="boolean" content=showCM6SwitchAwaySurvey)
meta(name="ol-showPersonalAccessToken", data-type="boolean" content=showPersonalAccessToken)
meta(name="ol-optionalPersonalAccessToken", data-type="boolean" content=optionalPersonalAccessToken)
meta(name="ol-isReviewPanelReact", data-type="boolean" content=isReviewPanelReact)
meta(name="ol-hasTrackChangesFeature", data-type="boolean" content=hasTrackChangesFeature)
meta(name="ol-mathJax3Path" content=mathJax3Path)

View file

@ -23,6 +23,7 @@ block append meta
meta(name="ol-github" data-type="json" content=github)
meta(name="ol-projectSyncSuccessMessage", content=projectSyncSuccessMessage)
meta(name="ol-showPersonalAccessToken", data-type="boolean" content=showPersonalAccessToken)
meta(name="ol-optionalPersonalAccessToken", data-type="boolean" content=optionalPersonalAccessToken)
meta(name="ol-personalAccessTokens", data-type="json" content=personalAccessTokens)
meta(name="ol-emailAddressLimit", data-type="json", content=emailAddressLimit)
meta(name="ol-currentManagedUserAdminEmail" data-type="string" content=currentManagedUserAdminEmail)

View file

@ -400,9 +400,11 @@
"git_bridge_modal_click_generate": "",
"git_bridge_modal_description": "",
"git_bridge_modal_enter_authentication_token": "",
"git_bridge_modal_git_authentication_tokens": "",
"git_bridge_modal_see_once": "",
"git_bridge_modal_tokens_description": "",
"git_bridge_modal_use_previous_token": "",
"git_bridge_modal_you_can_also_git_clone": "",
"git_integration": "",
"git_integration_info": "",
"github_commit_message_placeholder": "",

View file

@ -31,11 +31,11 @@ function LinkingSection() {
path: string
}[]
const showPersonalAccessToken = getMeta(
'ol-showPersonalAccessToken'
) as boolean
const showPersonalAccessTokenComponents: boolean =
getMeta('ol-showPersonalAccessToken') ||
getMeta('ol-optionalPersonalAccessToken')
const allIntegrationLinkingWidgets = showPersonalAccessToken
const allIntegrationLinkingWidgets = showPersonalAccessTokenComponents
? integrationLinkingWidgets.concat(oauth2ServerComponents)
: integrationLinkingWidgets

View file

@ -6,7 +6,7 @@ import Tooltip from '../../../shared/components/tooltip'
import Icon from '../../../shared/components/icon'
import { useShareProjectContext } from './share-project-modal'
import { setProjectAccessLevel } from '../utils/api'
import CopyLink from '../../../shared/components/copy-link'
import { CopyToClipboard } from '@/shared/components/copy-to-clipboard'
import { useProjectContext } from '../../../shared/context/project-context'
import * as eventTracking from '../../../infrastructure/event-tracking'
import { useUserContext } from '../../../shared/context/user-context'
@ -265,10 +265,10 @@ function AccessToken({ token, path, tooltipId }) {
const link = `${origin}${path}${token}`
return (
<pre className="access-token">
<span>{link}</span>
<CopyLink link={link} tooltipId={tooltipId} />
</pre>
<div className="access-token">
<code>{link}</code>
<CopyToClipboard content={link} tooltipId={tooltipId} />
</div>
)
}

View file

@ -1,52 +0,0 @@
import { useCallback, useState } from 'react'
import { Button } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import Tooltip from './tooltip'
import Icon from './icon'
type CopyLinkProps = {
link: string
tooltipId: string
}
function CopyLink({ link, tooltipId }: CopyLinkProps) {
const { t } = useTranslation()
const [copied, setCopied] = useState(false)
const handleClick = useCallback(() => {
navigator.clipboard.writeText(link).then(() => {
setCopied(true)
window.setTimeout(() => {
setCopied(false)
}, 1500)
})
}, [link])
if (!navigator.clipboard?.writeText) {
return null
}
return (
<Tooltip
id={tooltipId}
description={copied ? 'Copied!' : <Trans i18nKey="copy" />}
overlayProps={{
delayHide: copied ? 1000 : 250,
shouldUpdatePosition: true,
}}
>
<Button
onClick={handleClick}
bsSize="xsmall"
bsStyle="link"
className="copy-button"
aria-label={t('copy')}
>
{copied ? <Icon type="check" /> : <Icon type="clipboard" />}
</Button>
</Tooltip>
)
}
export default CopyLink

View file

@ -0,0 +1,84 @@
import { FC, memo, MouseEventHandler, useCallback, useState } from 'react'
import { Button } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import Tooltip from './tooltip'
import Icon from './icon'
export const CopyToClipboard = memo<{
content: string
tooltipId: string
kind?: 'text' | 'icon'
}>(({ content, tooltipId, kind = 'icon' }) => {
const { t } = useTranslation()
const [copied, setCopied] = useState(false)
const handleClick = useCallback(() => {
navigator.clipboard.writeText(content).then(() => {
setCopied(true)
window.setTimeout(() => {
setCopied(false)
}, 1500)
})
}, [content])
if (!navigator.clipboard?.writeText) {
return null
}
return (
<Tooltip
id={tooltipId}
description={copied ? `${t('copied')}!` : t('copy')}
overlayProps={{
delayHide: copied ? 1000 : 250,
shouldUpdatePosition: true,
}}
>
<span>
{kind === 'text' ? (
<TextButton handleClick={handleClick} />
) : (
<IconButton handleClick={handleClick} copied={copied} />
)}
</span>
</Tooltip>
)
})
CopyToClipboard.displayName = 'CopyToClipboard'
const TextButton: FC<{
handleClick: MouseEventHandler<Button>
}> = ({ handleClick }) => {
const { t } = useTranslation()
return (
<Button
onClick={handleClick}
bsSize="xsmall"
bsStyle="secondary"
className="copy-button"
>
{t('copy')}
</Button>
)
}
const IconButton: FC<{
handleClick: MouseEventHandler<Button>
copied: boolean
}> = ({ handleClick, copied }) => {
const { t } = useTranslation()
return (
<Button
onClick={handleClick}
bsSize="xsmall"
bsStyle="link"
className="copy-button"
aria-label={t('copy')}
>
<Icon type={copied ? 'check' : 'clipboard'} />
</Button>
)
}

View file

@ -9,6 +9,7 @@ import {
mockCompileError,
} from '../fixtures/compile'
import useFetchMock from '../hooks/use-fetch-mock'
import { useMeta } from '../hooks/use-meta'
const scopeWatchers: [string, (value: any) => void][] = []
@ -210,7 +211,8 @@ type ScopeDecoratorOptions = {
export const ScopeDecorator = (
Story: any,
opts: ScopeDecoratorOptions = { mockCompileOnLoad: true }
opts: ScopeDecoratorOptions = { mockCompileOnLoad: true },
meta: Record<string, any> = {}
) => {
// mock compile on load
useFetchMock(fetchMock => {
@ -232,6 +234,9 @@ export const ScopeDecorator = (
return initialize()
}, [])
// set values on window.metaAttributesCache (created in initialize, above)
useMeta(meta)
return (
<ContextRoot ide={ide}>
<Story />

View file

@ -117,6 +117,6 @@
// TODO: find a way for modules to add styles dynamically
@import 'modules/symbol-palette.less';
@import 'modules/managed-users.less';
@import 'modules/git-bridge-modal.less';
@import 'modules/admin-panel.less';
@import 'modules/overleaf-integration.less';

View file

@ -148,5 +148,6 @@
// TODO: find a way for modules to add styles dynamically
@import 'modules/symbol-palette.less';
@import 'modules/admin-panel.less';
@import 'modules/git-bridge-modal.less';
@import 'modules/managed-users.less';
@import 'modules/overleaf-integration.less';

View file

@ -0,0 +1,33 @@
.git-bridge-copy {
background: @neutral-10;
color: @neutral-90;
padding: @padding-sm;
border-radius: @border-radius-large;
display: flex;
justify-content: center;
gap: @padding-sm;
align-items: center;
margin: @margin-lg 0;
& code {
word-break: break-word;
}
}
.git-bridge-optional-tokens {
border: 1px solid @gray-lightest;
border-radius: @border-radius-large;
padding: @padding-md;
margin: @margin-lg 0;
}
.git-bridge-optional-tokens-header {
font-family: @font-family-sans-serif;
font-size: 120%;
font-weight: bold;
margin-bottom: @margin-sm;
}
.git-bridge-optional-tokens-actions {
margin-top: @margin-sm;
}

View file

@ -577,8 +577,8 @@
"git_authentication_token": "Git autentificeringsnøgle",
"git_authentication_token_create_modal_info_1": "Dette er din Git autentificeringsnøgle. Du skal indtaste den når du bliver spurgt om et kodeord.",
"git_authentication_token_create_modal_info_2": "<0>Du kan kun se denne autentificeringsnøgle én gang så kopier den venligst og opbevar den sikkert.</0> For flere instruktioner omkring brugen af autentificeringsnøgler, besøg vores <1>hjælpeside</1>.",
"git_bridge_modal_click_generate": "Klik “Generér nøgle” for at generere din autentificeringsnøgle. Du kan også gøre det senere i dine kontoindstillinger.",
"git_bridge_modal_description": "Du kan <code>git</code> <code>clone</code> dit projekt med linket herunder.",
"git_bridge_modal_click_generate": "Klik <strong>“Generér nøgle”</strong> for at generere din autentificeringsnøgle. Du kan også gøre det senere i dine kontoindstillinger.",
"git_bridge_modal_description": "Du kan git clone dit projekt med linket herunder.",
"git_bridge_modal_enter_authentication_token": "Når du bliver spurgt om en kode, indtast da din nye autentificeringsnøgle:",
"git_bridge_modal_see_once": "Du kan kun se denne autentificeringsnøgle én gang. For at slette den eller generere en ny, gå til dine brugerindstilinger. For detalerede instruktioner og fejlsøgning, læs vores <0>hjælpeside</0>.",
"git_bridge_modal_tokens_description": "For at git clone dit projekt har du brug for linket herunder og en git autentificeringsnøgle.",

View file

@ -582,8 +582,8 @@
"git_authentication_token": "Git Anmeldungs-Token",
"git_authentication_token_create_modal_info_1": "Das ist dein Git Anmeldungs-Token. Verwende ihn wenn Du nach einem Passwort gefragt wirst.",
"git_authentication_token_create_modal_info_2": "<0>Du bekommst diesen Anmelde-Token nur einmal angezeigt, bitte kopiere ihn und bewahre ihn sicher auf.</0> Für weitere Anweisungen zur Verwendung von Anmelde-Tokens, besuche unsere <1>Hilfe-Seite</1>.",
"git_bridge_modal_click_generate": "Klicke jetzt auf Token generieren um deinen ersten Anmeldungs-Token zu erstellen. Oder erstelle ihn später in deinen Kontoeinstellungen.",
"git_bridge_modal_description": "Du kannst <code>git</code> <code>clone</code> für dein Projekt über den unten angezeigten Link ausführen.",
"git_bridge_modal_click_generate": "Klicke jetzt auf <strong>Token generieren</strong> um deinen ersten Anmeldungs-Token zu erstellen. Oder erstelle ihn später in deinen Kontoeinstellungen.",
"git_bridge_modal_description": "Du kannst git clone für dein Projekt über den unten angezeigten Link ausführen.",
"git_bridge_modal_enter_authentication_token": "Wenn Du nach einem Passwort gefragt wirst, gib deinen neuen Anmeldungs-Token ein:",
"git_bridge_modal_see_once": "Du siehst diesen Token nur einmal. Um ihn zu löschen oder einen weiteren zu generieren, besuche die Kontoeinstellungen. Für detaillierte Anweisungen und Problembehebung, besuche unsere <0>Hilfe-Seite</0>.",
"git_bridge_modal_tokens_description": "Um einen Git-Clone deines Projekts zu erstellen, benutze folgenden Link und einen Git-Anmeldungs-Token.",

View file

@ -647,12 +647,14 @@
"git_authentication_token": "Git authentication token",
"git_authentication_token_create_modal_info_1": "This is your Git authentication token. You should enter this when prompted for a password.",
"git_authentication_token_create_modal_info_2": "<0>You will only see this authentication token once so please copy it and keep it safe</0>. For full instructions on using authentication tokens, visit our <1>help page</1>.",
"git_bridge_modal_click_generate": "Click Generate token to generate your authentication token now. Or do this later in your Account Settings.",
"git_bridge_modal_description": "You can <code>git</code> <code>clone</code> your project using the link displayed below.",
"git_bridge_modal_click_generate": "Click <strong>Generate token</strong> to generate your authentication token now. Or do this later in your Account Settings.",
"git_bridge_modal_description": "You can git clone your project using the link displayed below.",
"git_bridge_modal_enter_authentication_token": "When prompted for a password, enter your new authentication token:",
"git_bridge_modal_git_authentication_tokens": "Git authentication tokens",
"git_bridge_modal_see_once": "Youll only see this token once. To delete it or generate a new one, visit Account Settings. For detailed instructions and troubleshooting, read our <0>help page</0>.",
"git_bridge_modal_tokens_description": "To git clone your project youll need the link below and a Git authentication token.",
"git_bridge_modal_use_previous_token": "If youre prompted for a password, you can use a previously generated Git authentication token. Or you can generate a new one in Account Settings. For more support, read our <0>help page</0>.",
"git_bridge_modal_you_can_also_git_clone": "You can also git clone your project by using the link below and a Git authentication token.",
"git_integration": "Git Integration",
"git_integration_info": "With Git integration, you can clone your Overleaf projects with Git. For full instructions on how to do this, read <0>our help page</0>.",
"git_integration_lowercase": "Git integration",

View file

@ -364,7 +364,7 @@
"get_same_latex_setup": "Avec __appName__, votre configuration LaTeX vous suit partout. En travaillant avec vos collègues ou étudiant·e·s sur __appName__, vous êtes sûr·e de ne pas rencontrer dincohérences de version ou de conflits de paquets.",
"get_started_now": "Commencer maintenant",
"git": "Git",
"git_bridge_modal_description": "Vous pouvez effectuer un <code>git</code> <code>clone</code> de votre projet en utilisant le lien ci-dessous.",
"git_bridge_modal_description": "Vous pouvez effectuer un git clone de votre projet en utilisant le lien ci-dessous.",
"github_commit_message_placeholder": "Message de commit pour les changements effectués dans __appName__…",
"github_credentials_expired": "Vos identifiants GitHub ont expiré",
"github_for_link_shared_projects": "Vous avez accédé à ce projet par un partage de lien : celui-ci ne sera pas synchronisé à votre GitHub tant que vous naurez pas été invité par courriel par le propriétaire du projet.",

View file

@ -261,7 +261,7 @@
"get_same_latex_setup": "Com __appName__ você tem o mesmo aplicativo onde quer que vá. Trabalhando com seus colegas e estudantes no __appName__, você sabe que não terá problema de inconsistência ou conflito de pacotes.",
"get_started_now": "Comece agora",
"git": "Git",
"git_bridge_modal_description": "Você pode usar <code>git clone</code> no seu projeto usando o link abaixo.",
"git_bridge_modal_description": "Você pode usar git clone no seu projeto usando o link abaixo.",
"github_commit_message_placeholder": "Mensagem de commit para as alterações feitas no __appName__...",
"github_credentials_expired": "Suas credenciais de autorização do GitHub expiraram",
"github_for_link_shared_projects": "Este projeto foi acessado por meio do compartilhamento de links e não será sincronizado com o seu Github, a menos que você seja convidado por e-mail pelo proprietário do projeto.",

View file

@ -362,7 +362,7 @@
"get_same_latex_setup": "您到任何地方都可以用 __appName__ 实现LaTeX的功能。由于您和您的同事和学生可以在 __appName__ 上共同工作,不会出现版本不一致和包冲突的情况。",
"get_started_now": "立即开始",
"git": "Git",
"git_bridge_modal_description": "您可以使用下面显示的链接来<code>git</code><code>clone</code>您的项目。",
"git_bridge_modal_description": "您可以使用下面显示的链接来git clone您的项目。",
"github_commit_message_placeholder": "为 __appName__ 中的更改提交信息",
"github_credentials_expired": "您的 Github 授权凭证已过期",
"github_for_link_shared_projects": "此项目是通过链接共享访问的除非项目所有者通过电子邮件邀请您否则不会与您的Github同步。",

View file

@ -82,6 +82,11 @@ describe('UserPagesController', function () {
getAdminEmail: sinon.stub().returns(this.adminEmail),
},
}
this.SplitTestHandler = {
promises: {
getAssignment: sinon.stub().returns('default'),
},
}
this.UserPagesController = SandboxedModule.require(modulePath, {
requires: {
'@overleaf/settings': this.settings,
@ -96,6 +101,7 @@ describe('UserPagesController', function () {
'../../../../modules/oauth2-server/app/src/OAuthPersonalAccessTokenManager':
this.PersonalAccessTokenManager,
'../Authentication/SessionManager': this.SessionManager,
'../SplitTests/SplitTestHandler': this.SplitTestHandler,
request: (this.request = sinon.stub()),
},
})