Github Sync: enhance token handling and post-sync indexing

- support file-based token cipher
  - reindex references after sync
  - fix typo and clean up code
This commit is contained in:
yu-i-i
2026-09-22 02:41:43 +02:00
parent 9afab5ade1
commit bd5507262e
5 changed files with 102 additions and 23 deletions
@@ -0,0 +1,82 @@
import logger from '@overleaf/logger'
import Settings from '@overleaf/settings'
import fs from 'node:fs'
import crypto from 'node:crypto'
import Path from 'node:path'
import AccessTokenEncryptorClass from '@overleaf/access-token-encryptor'
const TOKEN_CIPHER_FILE = '/var/lib/overleaf/data/.token-cipher.json'
const TOKEN_CIPHER_LABEL = 'OL_CEP-v3'
let encryptorInstance = null
/**
* Get or create a stable encryptor data that persists across container
* recreations. Priority:
* 1. GITHUB_CIPHER_PASSWORD || TOKEN_CIPHER_PASSWORD env var (explicit user config)
* 2. JSON file with encryptor data in the persistent volume (/var/lib/overleaf/data/)
* — auto-generated on first use, survives container rebuilds
*/
function _getEncryptorData() {
const cipherPassword = process.env.GITHUB_TOKEN_CIPHER_PASSWORD || process.env.TOKEN_CIPHER_PASSWORD
const cipherLabel = process.env.GITHUB_TOKEN_CIPHER_LABEL ||
process.env.TOKEN_CIPHER_LABEL ||
TOKEN_CIPHER_LABEL
if (cipherPassword) {
return {
cipherLabel,
cipherPasswords: {
[cipherLabel]: cipherPassword
}
}
}
// cipherPassword is not set, use file
const cipherFile = process.env.GITHUB_TOKEN_CIPHER_FILE ||
process.env.TOKEN_CIPHER_FILE ||
TOKEN_CIPHER_FILE
try {
return JSON.parse(fs.readFileSync(cipherFile, 'utf8'))
} catch (err) {
if (err.code === 'ENOENT') {
logger.info({ cipherFile }, 'Creating new cipherFile')
} else {
logger.error({ err, cipherFile }, 'Bad cipherFile')
throw err
}
}
// File doesn't exist yet — generate one
try {
const encryptorData = {
cipherLabel,
cipherPasswords: {
[cipherLabel]: crypto.randomBytes(32).toString('base64')
},
}
const dir = Path.dirname(cipherFile)
fs.mkdirSync(dir, { recursive: true })
fs.writeFileSync(cipherFile, JSON.stringify(encryptorData, null, 2), { mode: 0o600 })
return encryptorData
} catch (err) {
logger.error({ err, cipherFile }, 'Cannot create cipherFile')
throw err
}
}
function _getEncryptor() {
if (!encryptorInstance) {
const encryptorData = _getEncryptorData()
encryptorInstance = new AccessTokenEncryptorClass(encryptorData)
}
return encryptorInstance
}
export const AccessTokenEncryptor = {
async encryptJson(json) {
return await _getEncryptor().promises.encryptJson(json)
},
async decryptToJson(encryptedJson) {
return await _getEncryptor().promises.decryptToJson(encryptedJson)
},
}
@@ -1,23 +1,15 @@
import logger from '@overleaf/logger' import logger from '@overleaf/logger'
import OError from '@overleaf/o-error' import OError from '@overleaf/o-error'
import AccessTokenEncryptor from '@overleaf/access-token-encryptor'
import Mongo from '../../../../app/src/Features/Helpers/Mongo.mjs' import Mongo from '../../../../app/src/Features/Helpers/Mongo.mjs'
import { GitHubSyncUserCredentials } from '../models/githubSyncUserCredentials.mjs' import { GitHubSyncUserCredentials } from '../models/githubSyncUserCredentials.mjs'
import { AccessTokenEncryptor } from './AccessTokenEncryptorHelper.mjs'
import { InvalidTokenError } from './GitSyncErrors.mjs' import { InvalidTokenError } from './GitSyncErrors.mjs'
const { normalizeQuery } = Mongo const { normalizeQuery } = Mongo
// encrypt / decrypt helpers
const accessTokenEncryptor = new AccessTokenEncryptor({
cipherLabel: process.env.GITHUB_TOKEN_CIPHER_LABEL || "OL_CEP-v3",
cipherPasswords: {
[process.env.GITHUB_TOKEN_CIPHER_LABEL || "OL_CEP-v3"]: process.env.GITHUB_TOKEN_CIPHER_PASSWORD,
},
})
async function encryptAccessToken(accessToken) { async function encryptAccessToken(accessToken) {
try { try {
return await accessTokenEncryptor.promises.encryptJson(accessToken) return await AccessTokenEncryptor.encryptJson(accessToken)
} catch (err) { } catch (err) {
throw OError.tag('failed to encrypt token', err) throw OError.tag('failed to encrypt token', err)
} }
@@ -25,7 +17,7 @@ async function encryptAccessToken(accessToken) {
async function decryptAccessToken(tokenEncrypted) { async function decryptAccessToken(tokenEncrypted) {
try { try {
return await accessTokenEncryptor.promises.decryptToJson(tokenEncrypted) return await AccessTokenEncryptor.decryptToJson(tokenEncrypted)
} catch (err) { } catch (err) {
throw new InvalidTokenError('failed to decrypt token', { status: 401 }, err) throw new InvalidTokenError('failed to decrypt token', { status: 401 }, err)
} }
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'
import { debugConsole } from '@/utils/debugging' import { debugConsole } from '@/utils/debugging'
import { postJSON } from '@/infrastructure/fetch-json' import { postJSON } from '@/infrastructure/fetch-json'
import useAsync from '@/shared/hooks/use-async' import useAsync from '@/shared/hooks/use-async'
import clientId from '@/utils/client-id'
import { import {
OLModalBody, OLModalBody,
OLModalFooter, OLModalFooter,
@@ -11,6 +12,8 @@ import OLButton from '@/shared/components/ol/ol-button'
import OLNotification from '@/shared/components/ol/ol-notification' import OLNotification from '@/shared/components/ol/ol-notification'
import { GitSyncModalStatus, ProjectSyncState } from '../../types/git-sync-types' import { GitSyncModalStatus, ProjectSyncState } from '../../types/git-sync-types'
import { useReferencesContext } from '@/features/ide-react/context/references-context'
type GitSyncMergeModalProps = { type GitSyncMergeModalProps = {
handleHide: () => void handleHide: () => void
modalStatus: GitSyncModalStatus modalStatus: GitSyncModalStatus
@@ -29,10 +32,11 @@ const GitSyncMergeModal = ({
commitMessage, commitMessage,
setCommitMessage, setCommitMessage,
projectId projectId
}: GitSynMergeModalProps) => { }: GitSyncMergeModalProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { error, isError, isLoading, setError, runAsync } = useAsync<ProjectSyncState>() const { error, isError, isLoading, setError, runAsync } = useAsync<ProjectSyncState>()
const { indexAllReferences } = useReferencesContext()
useEffect(() => { useEffect(() => {
if (modalStatus !== 'run-merge' && if (modalStatus !== 'run-merge' &&
@@ -46,11 +50,17 @@ const GitSyncMergeModal = ({
body: { message: commitMessage, claimConflictIsResolved }, body: { message: commitMessage, claimConflictIsResolved },
}) })
) )
.then(data => { .then(async data => {
switch (data.mergeStatus) { switch (data.mergeStatus) {
case 'clean': case 'clean':
setModalStatus('merge-overview') setModalStatus('merge-overview')
await postJSON(`/project/${projectId}/flush`)
.catch(e => debugConsole.error(e))
indexAllReferences(true)
break break
case 'conflict': case 'conflict':
+3 -8
View File
@@ -1,14 +1,11 @@
import Settings from '@overleaf/settings' import Settings from '@overleaf/settings'
import Modules from '../../app/src/infrastructure/Modules.mjs' import Modules from '../../app/src/infrastructure/Modules.mjs'
import logger from '@overleaf/logger' import logger from '@overleaf/logger'
import GitHubSyncRouter from './app/src/GitHubSyncRouter.mjs'
import SyncStateManager from './app/src/SyncStateManager.mjs'
import TokenManager from './app/src/TokenManager.mjs'
let GitHubSyncModule = {} let GitHubSyncModule = {}
if (process.env.GITHUB_SYNC_ENABLED?.toLowerCase() === 'true') { if (process.env.GITHUB_SYNC_ENABLED?.toLowerCase() === 'true') {
logger.debug({}, 'Enabling GitHub Sync module') logger.debug({}, 'Enabling GitHub Sync module')
/*
const [{ default: GitHubSyncRouter }, const [{ default: GitHubSyncRouter },
{ default: SyncStateManager }, { default: SyncStateManager },
{ default: TokenManager } { default: TokenManager }
@@ -18,17 +15,15 @@ if (process.env.GITHUB_SYNC_ENABLED?.toLowerCase() === 'true') {
import('./app/src/SyncStateManager.mjs'), import('./app/src/SyncStateManager.mjs'),
import('./app/src/TokenManager.mjs'), import('./app/src/TokenManager.mjs'),
]) ])
*/
// Delete project sync state from mongo (hook 'projectExpired') const siteUrl = Settings.siteUrl.replace(/\/+$/, '') || 'http://localhost'
const siteUrl = process.env.OVERLEAF_SITE_URL?.replace(/\/+$/, '') || 'http://localhost'
Settings.githubSync = { Settings.githubSync = {
clientID: process.env.GITHUB_SYNC_CLIENT_ID, clientID: process.env.GITHUB_SYNC_CLIENT_ID,
clientSecret: process.env.GITHUB_SYNC_CLIENT_SECRET, clientSecret: process.env.GITHUB_SYNC_CLIENT_SECRET,
callbackURL: `${siteUrl}/user/github-sync/oauth2/callback`, callbackURL: `${siteUrl}/user/github-sync/oauth2/callback`,
}, },
// Delete project sync state from mongo (hook 'projectExpired')
Modules.hooks.attach('projectExpired', async projectId => { Modules.hooks.attach('projectExpired', async projectId => {
try { try {
await SyncStateManager.removeProjectState(projectId) await SyncStateManager.removeProjectState(projectId)