Переглянути джерело

[web] Personal Access Token Settings UI (#13040)

* [web] Personal Access Token Settings UI

* [web] Add Personal Access Token Settings UI to settings page

* [web] Added `personal-access-token-settings` unit tests

GitOrigin-RevId: 353b2f1a2b57c3292554f129be6cbb4f8f8382f8
Miguel Serrano 3 роки тому
батько
коміт
27c2d1c16e

+ 13 - 0
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,
   })
 }
 

+ 1 - 0
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

+ 1 - 0
services/web/config/settings.defaults.js

@@ -805,6 +805,7 @@ module.exports = {
     importProjectFromGithubMenu: [],
     editorLeftMenuSync: [],
     editorLeftMenuManageTemplate: [],
+    oauth2Server: [],
   },
 
   moduleImportSequence: [

+ 24 - 0
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": "",

+ 19 - 4
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() {
             <Alert bsStyle="success">{projectSyncSuccessMessage}</Alert>
           ) : null}
           <div className="settings-widgets-container">
-            {integrationLinkingWidgets.map(
+            {allIntegrationLinkingWidgets.map(
               ({ import: importObject, path }, widgetIndex) => (
                 <ModuleLinkingWidget
                   key={Object.keys(importObject)[0]}
                   ModuleComponent={Object.values(importObject)[0]}
-                  isLast={widgetIndex === integrationLinkingWidgets.length - 1}
+                  isLast={
+                    widgetIndex === allIntegrationLinkingWidgets.length - 1
+                  }
                 />
               )
             )}

Різницю між файлами не показано, бо вона завелика
+ 24 - 0
services/web/frontend/js/shared/svgs/git-bridge-logo.js


+ 1 - 1
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: {},

+ 1 - 1
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

+ 24 - 0
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)
+}

+ 7 - 0
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)

+ 17 - 0
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;
+}

+ 9 - 0
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

+ 23 - 0
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</0>. For full instructions on using authentication tokens, visit our <1>help page</1>.",
   "git_bridge_modal_description": "You can <code>git</code> <code>clone</code> 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</0>.",
   "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</0>.",
   "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</0> any time to share your feedback",
   "your_affiliation_is_confirmed": "Your <0>__institutionName__</0> 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</0> 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",

+ 5 - 0
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()),
       },

+ 3 - 0
services/web/types/window.ts

@@ -32,5 +32,8 @@ declare global {
     _reportAcePerf: () => void
     MathJax: Record<string, any>
     overallThemes: OverallThemeMeta[]
+    crypto: {
+      randomUUID: () => string
+    }
   }
 }

Деякі файли не було показано, через те що забагато файлів було змінено