diff --git a/services/web/app/src/Features/User/UserPagesController.js b/services/web/app/src/Features/User/UserPagesController.js index 4b33e60458..059b2c9a0c 100644 --- a/services/web/app/src/Features/User/UserPagesController.js +++ b/services/web/app/src/Features/User/UserPagesController.js @@ -68,6 +68,18 @@ async function settingsPage(req, res) { const showPersonalAccessToken = !Features.hasFeature('saas') || req.query?.personal_access_token === 'true' + let personalAccessTokens + if (showPersonalAccessToken) { + try { + // require this here because module may not be included in some versions + const PersonalAccessTokenManager = require('../../../../modules/oauth2-server/app/src/OAuthPersonalAccessTokenManager') + personalAccessTokens = await PersonalAccessTokenManager.listTokens( + user._id + ) + } catch (error) { + logger.error(OError.tag(error)) + } + } res.render('user/settings', { title: 'account_settings', @@ -112,6 +124,7 @@ async function settingsPage(req, res) { thirdPartyIds: UserPagesController._restructureThirdPartyIds(user), projectSyncSuccessMessage, showPersonalAccessToken, + personalAccessTokens, }) } diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug index 73fcc77bab..caf2b69fc6 100644 --- a/services/web/app/views/user/settings.pug +++ b/services/web/app/views/user/settings.pug @@ -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-personalAccessTokens", data-type="json" content=personalAccessTokens) block content main.content.content-alt#settings-page-root diff --git a/services/web/config/settings.defaults.js b/services/web/config/settings.defaults.js index c95c48cab3..46c8e7b151 100644 --- a/services/web/config/settings.defaults.js +++ b/services/web/config/settings.defaults.js @@ -805,6 +805,7 @@ module.exports = { importProjectFromGithubMenu: [], editorLeftMenuSync: [], editorLeftMenuManageTemplate: [], + oauth2Server: [], }, moduleImportSequence: [ diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 1e0157abf0..0218e4adf4 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -24,6 +24,7 @@ "add_affiliation": "", "add_another_address_line": "", "add_another_email": "", + "add_another_token": "", "add_comma_separated_emails_help": "", "add_company_details": "", "add_email_to_claim_features": "", @@ -155,6 +156,7 @@ "contact_support_to_change_group_subscription": "", "contact_us": "", "continue_github_merge": "", + "copied": "", "copy": "", "copy_project": "", "copying": "", @@ -168,6 +170,7 @@ "create_new_subscription": "", "create_new_tag": "", "create_project_in_github": "", + "created": "", "created_at": "", "creating": "", "current_password": "", @@ -184,9 +187,12 @@ "delete_acct_no_existing_pw": "", "delete_and_leave": "", "delete_and_leave_projects": "", + "delete_authentication_token": "", + "delete_authentication_token_info": "", "delete_figure": "", "delete_projects": "", "delete_tag": "", + "delete_token": "", "delete_your_account": "", "deleted_at": "", "deleting": "", @@ -259,6 +265,8 @@ "example_project": "", "existing_plan_active_until_term_end": "", "expand": "", + "expired": "", + "expires": "", "export_csv": "", "export_project_to_github": "", "fast": "", @@ -329,6 +337,7 @@ "galileo_suggestion_feedback_button": "", "galileo_suggestions_loading_error": "", "galileo_toggle_description": "", + "generate_token": "", "generic_if_problem_continues_contact_us": "", "generic_linked_file_compile_error": "", "generic_something_went_wrong": "", @@ -337,7 +346,12 @@ "get_most_subscription_by_checking_features": "", "get_most_subscription_by_checking_premium_features": "", "git": "", + "git_authentication_token": "", + "git_authentication_token_create_modal_info_1": "", + "git_authentication_token_create_modal_info_2": "", "git_bridge_modal_description": "", + "git_integration": "", + "git_integration_info": "", "github_commit_message_placeholder": "", "github_credentials_expired": "", "github_file_name_error": "", @@ -477,6 +491,7 @@ "last_name": "", "last_resort_trouble_shooting_guide": "", "last_updated_date_by_x": "", + "last_used": "", "latex_help_guide": "", "latex_places_figures_according_to_a_special_algorithm": "", "layout": "", @@ -933,6 +948,8 @@ "to_change_access_permissions": "", "to_modify_your_subscription_go_to": "", "toggle_compile_options_menu": "", + "token": "", + "token_limit_reached": "", "token_read_only": "", "token_read_write": "", "too_many_attempts": "", @@ -1071,6 +1088,13 @@ "you_have_added_x_of_group_size_y": "", "your_affiliation_is_confirmed": "", "your_browser_does_not_support_this_feature": "", + "your_git_access_info": "", + "your_git_access_info_bullet_1": "", + "your_git_access_info_bullet_2": "", + "your_git_access_info_bullet_3": "", + "your_git_access_info_bullet_4": "", + "your_git_access_info_bullet_5": "", + "your_git_access_tokens": "", "your_message_to_collaborators": "", "your_new_plan": "", "your_plan": "", diff --git a/services/web/frontend/js/features/settings/components/linking-section.tsx b/services/web/frontend/js/features/settings/components/linking-section.tsx index 9e01ea4c29..0d769d0649 100644 --- a/services/web/frontend/js/features/settings/components/linking-section.tsx +++ b/services/web/frontend/js/features/settings/components/linking-section.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useState, ElementType } from 'react' import { Alert } from 'react-bootstrap' import { useTranslation } from 'react-i18next' import importOverleafModules from '../../../../macros/import-overleaf-module.macro' @@ -24,7 +24,20 @@ function LinkingSection() { importOverleafModules('referenceLinkingWidgets') ) - const hasIntegrationLinkingSection = integrationLinkingWidgets.length + const oauth2ServerComponents = importOverleafModules('oauth2Server') as { + import: { default: ElementType } + path: string + }[] + + const showPersonalAccessToken = getMeta( + 'ol-showPersonalAccessToken' + ) as boolean + + const allIntegrationLinkingWidgets = showPersonalAccessToken + ? integrationLinkingWidgets.concat(oauth2ServerComponents) + : integrationLinkingWidgets + + const hasIntegrationLinkingSection = allIntegrationLinkingWidgets.length const hasReferencesLinkingSection = referenceLinkingWidgets.length const hasSSOLinkingSection = Object.keys(subscriptions).length > 0 @@ -49,12 +62,14 @@ function LinkingSection() { {projectSyncSuccessMessage} ) : null}
- {integrationLinkingWidgets.map( + {allIntegrationLinkingWidgets.map( ({ import: importObject, path }, widgetIndex) => ( ) )} diff --git a/services/web/frontend/js/shared/svgs/git-bridge-logo.js b/services/web/frontend/js/shared/svgs/git-bridge-logo.js new file mode 100644 index 0000000000..62b5ca05a5 --- /dev/null +++ b/services/web/frontend/js/shared/svgs/git-bridge-logo.js @@ -0,0 +1,32 @@ +function GitBridgeLogo() { + return ( + + + + + + + + + + ) +} + +export default GitBridgeLogo diff --git a/services/web/frontend/stories/decorators/scope.tsx b/services/web/frontend/stories/decorators/scope.tsx index 326fced817..2bbf058eee 100644 --- a/services/web/frontend/stories/decorators/scope.tsx +++ b/services/web/frontend/stories/decorators/scope.tsx @@ -21,7 +21,7 @@ const initialize = () => { } const project: Project = { - _id: 'a-project', + _id: '63e21c07946dd8c76505f85a', name: 'A Project', features: { mendeley: true, zotero: true }, tokens: {}, diff --git a/services/web/frontend/stories/fixtures/project.ts b/services/web/frontend/stories/fixtures/project.ts index 27acaea2e2..9cc54c4fc4 100644 --- a/services/web/frontend/stories/fixtures/project.ts +++ b/services/web/frontend/stories/fixtures/project.ts @@ -1,7 +1,7 @@ import { Project } from '../../../types/project' export const project: Project = { - _id: 'a-project', + _id: '63e21c07946dd8c76505f85a', name: 'A Project', features: { collaborators: -1, // unlimited diff --git a/services/web/frontend/stories/settings/helpers/linking.js b/services/web/frontend/stories/settings/helpers/linking.js index 295d037e5b..fd0d023356 100644 --- a/services/web/frontend/stories/settings/helpers/linking.js +++ b/services/web/frontend/stories/settings/helpers/linking.js @@ -60,3 +60,27 @@ export function setDefaultMeta() { window.metaAttributesCache.delete('referenceLinkingWidgets') window.metaAttributesCache.delete('ol-ssoErrorMessage') } + +export function setPersonalAccessTokensMeta() { + function generateToken(_id) { + const oneYearFromNow = new Date() + oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1) + + const tokenHasBeenUsed = Math.random() > 0.5 + + return { + _id, + accessTokenPartial: 'olp_abc' + _id, + createdAt: new Date(), + accessTokenExpiresAt: oneYearFromNow, + lastUsedAt: tokenHasBeenUsed ? new Date() : undefined, + } + } + const tokens = [] + for (let i = 0; i < 6; i++) { + tokens.push(generateToken(i)) + } + + window.metaAttributesCache.set('ol-personalAccessTokens', tokens) + window.metaAttributesCache.set('ol-showPersonalAccessToken', true) +} diff --git a/services/web/frontend/stories/settings/page.stories.js b/services/web/frontend/stories/settings/page.stories.js index 95989ccfd5..f3d58fb48a 100644 --- a/services/web/frontend/stories/settings/page.stories.js +++ b/services/web/frontend/stories/settings/page.stories.js @@ -19,6 +19,7 @@ import { import { setDefaultMeta as setDefaultLinkingMeta, defaultSetupMocks as defaultSetupLinkingMocks, + setPersonalAccessTokensMeta, } from './helpers/linking' import { UserProvider } from '../../js/shared/context/user-context' import { ScopeDecorator } from '../decorators/scope' @@ -44,9 +45,15 @@ export const Overleaf = args => { ) } +export const OverleafWithAccessTokens = args => { + setPersonalAccessTokensMeta() + return Overleaf(args) +} + export const ServerPro = args => { setDefaultAccountInfoMeta() setDefaultPasswordMeta() + setPersonalAccessTokensMeta() useFetchMock(fetchMock => { defaultSetupAccountInfoMocks(fetchMock) defaultSetupPasswordMocks(fetchMock) diff --git a/services/web/frontend/stylesheets/app/account-settings.less b/services/web/frontend/stylesheets/app/account-settings.less index 28f981473e..8b8ee2aa70 100644 --- a/services/web/frontend/stylesheets/app/account-settings.less +++ b/services/web/frontend/stylesheets/app/account-settings.less @@ -216,3 +216,20 @@ tbody > tr.affiliations-table-warning-row > td { .setting-reconfirm-info-right { white-space: nowrap; } + +// Prevents icon from large account linking sections, such as the git bridge, +// from rendering in the center of the widget, anchoring it to the top +.linking-icon-fixed-position { + align-self: start; + padding-top: 10px; +} + +// overrides the default `Col` padding, as the inner `affiliations-table-cell` has its own padding, and +// the content length of the git-bridge token table is pretty much fixed (tokens and dates) +.linking-git-bridge-table-cell { + padding-right: 0; +} + +.linking-git-bridge-revoke-button { + padding: 2px 4px; +} diff --git a/services/web/frontend/stylesheets/core/type.less b/services/web/frontend/stylesheets/core/type.less index bd31abc3a7..15672ec46e 100755 --- a/services/web/frontend/stylesheets/core/type.less +++ b/services/web/frontend/stylesheets/core/type.less @@ -90,6 +90,12 @@ h6, font-size: @font-size-h6; } +.ui-heading { + font-family: @font-family-sans-serif; + font-size: @font-size-h4; + font-weight: bold; +} + // Body text // ------------------------- @@ -191,6 +197,9 @@ cite { .text-uppercase { text-transform: uppercase; } +.text-italic { + font-style: italic; +} // Contextual backgrounds // For now we'll leave these alongside the text classes until v4 when we can diff --git a/services/web/locales/en.json b/services/web/locales/en.json index a11c140bf9..5d1e48d1f4 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -49,6 +49,7 @@ "add_affiliation": "Add Affiliation", "add_another_address_line": "Add another address line", "add_another_email": "Add another email", + "add_another_token": "Add another token", "add_comma_separated_emails_help": "Separate multiple email addresses using the comma (,) character.", "add_comment": "Add comment", "add_company_details": "Add Company Details", @@ -281,6 +282,7 @@ "continue_github_merge": "I have manually merged. Continue", "continue_to": "Continue to __appName__", "continue_with_free_plan": "Continue with free plan", + "copied": "Copied", "copy": "Copy", "copy_project": "Copy Project", "copying": "Copying", @@ -299,6 +301,7 @@ "create_new_tag": "Create new tag", "create_project_in_github": "Create a GitHub repository", "create_your_first_project": "Create your first project!", + "created": "created", "created_at": "Created at", "creating": "Creating", "credit_card": "Credit Card", @@ -332,10 +335,13 @@ "delete_acct_no_existing_pw": "Please use the password reset form to set a password before deleting your account", "delete_and_leave": "Delete / Leave", "delete_and_leave_projects": "Delete and Leave Projects", + "delete_authentication_token": "Delete Authentication token", + "delete_authentication_token_info": "You’re about to delete a Git authentication token. If you do, it can no longer be used to authenticate your identity when performing Git operations.", "delete_figure": "Delete figure", "delete_folder": "Delete Folder", "delete_projects": "Delete Projects", "delete_tag": "Delete Tag", + "delete_token": "Delete token", "delete_your_account": "Delete your account", "deleted_at": "Deleted At", "deleted_files": "Deleted Files", @@ -452,6 +458,7 @@ "example_project": "Example Project", "existing_plan_active_until_term_end": "Your existing plan and its features will remain active until the end of the current billing period.", "expand": "Expand", + "expires": "Expires", "expiry": "Expiry Date", "export_csv": "Export CSV", "export_project_to_github": "Export Project to GitHub", @@ -581,6 +588,7 @@ "galileo_suggestion_feedback_button": "Was this suggestion useful?", "galileo_suggestions_loading_error": "Error loading Galileo suggestions", "galileo_toggle_description": "Toggle Galileo", + "generate_token": "Generate token", "generic_history_error": "Something went wrong trying to fetch your project’s history. If the error persists, please contact us via:", "generic_if_problem_continues_contact_us": "If the problem continues please contact us", "generic_linked_file_compile_error": "This project’s output files are not available because it failed to compile. Please open the project to see the compilation error details.", @@ -595,7 +603,12 @@ "get_started_now": "Get started now", "get_the_most_out_headline": "Get the most out of __appName__ with features such as:", "git": "Git", + "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. For full instructions on using authentication tokens, visit our <1>help page.", "git_bridge_modal_description": "You can git clone your project using the link displayed below.", + "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.", "git_integration_lowercase": "Git integration", "git_integration_lowercase_info": "You can clone your Overleaf project to a local repository, treating your Overleaf project as a remote repository that changes can be pushed to and pulled from.", "github_commit_message_placeholder": "Commit message for changes made in __appName__...", @@ -814,6 +827,7 @@ "last_resort_trouble_shooting_guide": "If that doesn’t help, follow our <0>troubleshooting guide.", "last_updated": "Last Updated", "last_updated_date_by_x": "__lastUpdatedDate__ by __person__", + "last_used": "last used", "latex_editor_info": "Everything you need in a modern LaTeX editor --- spell check, intelligent autocomplete, syntax highlighting, dozens of color themes, vim and emacs bindings, help with LaTeX warnings and error messages, and much more.", "latex_guides": "LaTeX guides", "latex_help_guide": "LaTeX help guide", @@ -1571,7 +1585,9 @@ "to_many_login_requests_2_mins": "This account has had too many login requests. Please wait 2 minutes before trying to log in again", "to_modify_your_subscription_go_to": "To modify your subscription go to", "toggle_compile_options_menu": "Toggle compile options menu", + "token": "token", "token_access_failure": "Cannot grant access; contact the project owner for help", + "token_limit_reached": "You’ve reached the 10 token limit. To generate a new authentication token, please delete an existing one.", "token_read_only": "token read-only", "token_read_write": "token read-write", "too_many_attempts": "Too many attempts. Please wait for a while and try again.", @@ -1778,6 +1794,13 @@ "you_will_be_able_to_contact_us_any_time_to_share_your_feedback": "<0>You will be able to contact us any time to share your feedback", "your_affiliation_is_confirmed": "Your <0>__institutionName__ affiliation is confirmed.", "your_browser_does_not_support_this_feature": "Sorry, your browser doesn’t support this feature. Please update your browser to its latest version.", + "your_git_access_info": "Your Git authentication tokens should be entered whenever you’re prompted for a password.", + "your_git_access_info_bullet_1": "You can have up to 10 tokens.", + "your_git_access_info_bullet_2": "If you reach the maximum limit, you’ll need to delete a token before you can generate a new one.", + "your_git_access_info_bullet_3": "You can generate a token using the <0>Generate token button.", + "your_git_access_info_bullet_4": "You won’t be able to view the full token after the first time you generate it. Please copy it and keep it safe", + "your_git_access_info_bullet_5": "Previously generated tokens will be shown here.", + "your_git_access_tokens": "Your Git authentication tokens", "your_message_to_collaborators": "Send a message to your collaborators", "your_new_plan": "Your new plan", "your_password_has_been_successfully_changed": "Your password has been successfully changed", diff --git a/services/web/test/unit/src/User/UserPagesControllerTests.js b/services/web/test/unit/src/User/UserPagesControllerTests.js index 8504ffe67b..4d7bf67506 100644 --- a/services/web/test/unit/src/User/UserPagesControllerTests.js +++ b/services/web/test/unit/src/User/UserPagesControllerTests.js @@ -70,6 +70,9 @@ describe('UserPagesController', function () { this.Features = { hasFeature: sinon.stub().returns(false), } + this.PersonalAccessTokenManager = { + listTokens: sinon.stub().returns([]), + } this.UserPagesController = SandboxedModule.require(modulePath, { requires: { '@overleaf/settings': this.settings, @@ -80,6 +83,8 @@ describe('UserPagesController', function () { '../Authentication/AuthenticationController': this.AuthenticationController, '../../infrastructure/Features': this.Features, + '../../../../modules/oauth2-server/app/src/OAuthPersonalAccessTokenManager': + this.PersonalAccessTokenManager, '../Authentication/SessionManager': this.SessionManager, request: (this.request = sinon.stub()), }, diff --git a/services/web/types/window.ts b/services/web/types/window.ts index 7706f37c0c..f74b011e78 100644 --- a/services/web/types/window.ts +++ b/services/web/types/window.ts @@ -32,5 +32,8 @@ declare global { _reportAcePerf: () => void MathJax: Record overallThemes: OverallThemeMeta[] + crypto: { + randomUUID: () => string + } } }