mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 13:13:45 +01:00
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:
@@ -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)
|
||||
},
|
||||
}
|
||||
@@ -311,7 +311,7 @@ async function resolveConflictSyncState(
|
||||
}
|
||||
}
|
||||
|
||||
// ----------- detached flow
|
||||
// ----------- detached flow
|
||||
// GH repo was force pushed, lastSybc commit possibly anavailable
|
||||
async function resolveDetachedSyncState(
|
||||
token,
|
||||
@@ -574,7 +574,7 @@ async function buildDetachedSyncPlan({
|
||||
buffer = await HistoryManager.getProjectFileBuffer(projectId, currentVersion, path)
|
||||
}
|
||||
const sha = await api.uploadBlob(token, repoFullName, buffer)
|
||||
// assert sha !== localHash
|
||||
// assert sha !== localHash
|
||||
}
|
||||
|
||||
// OL == GH, do nothing
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
import logger from '@overleaf/logger'
|
||||
import OError from '@overleaf/o-error'
|
||||
import AccessTokenEncryptor from '@overleaf/access-token-encryptor'
|
||||
import Mongo from '../../../../app/src/Features/Helpers/Mongo.mjs'
|
||||
import { GitHubSyncUserCredentials } from '../models/githubSyncUserCredentials.mjs'
|
||||
import { AccessTokenEncryptor } from './AccessTokenEncryptorHelper.mjs'
|
||||
import { InvalidTokenError } from './GitSyncErrors.mjs'
|
||||
|
||||
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) {
|
||||
try {
|
||||
return await accessTokenEncryptor.promises.encryptJson(accessToken)
|
||||
return await AccessTokenEncryptor.encryptJson(accessToken)
|
||||
} catch (err) {
|
||||
throw OError.tag('failed to encrypt token', err)
|
||||
}
|
||||
@@ -25,7 +17,7 @@ async function encryptAccessToken(accessToken) {
|
||||
|
||||
async function decryptAccessToken(tokenEncrypted) {
|
||||
try {
|
||||
return await accessTokenEncryptor.promises.decryptToJson(tokenEncrypted)
|
||||
return await AccessTokenEncryptor.decryptToJson(tokenEncrypted)
|
||||
} catch (err) {
|
||||
throw new InvalidTokenError('failed to decrypt token', { status: 401 }, err)
|
||||
}
|
||||
|
||||
+12
-2
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { postJSON } from '@/infrastructure/fetch-json'
|
||||
import useAsync from '@/shared/hooks/use-async'
|
||||
import clientId from '@/utils/client-id'
|
||||
import {
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
@@ -11,6 +12,8 @@ import OLButton from '@/shared/components/ol/ol-button'
|
||||
import OLNotification from '@/shared/components/ol/ol-notification'
|
||||
import { GitSyncModalStatus, ProjectSyncState } from '../../types/git-sync-types'
|
||||
|
||||
import { useReferencesContext } from '@/features/ide-react/context/references-context'
|
||||
|
||||
type GitSyncMergeModalProps = {
|
||||
handleHide: () => void
|
||||
modalStatus: GitSyncModalStatus
|
||||
@@ -29,10 +32,11 @@ const GitSyncMergeModal = ({
|
||||
commitMessage,
|
||||
setCommitMessage,
|
||||
projectId
|
||||
}: GitSynMergeModalProps) => {
|
||||
}: GitSyncMergeModalProps) => {
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { error, isError, isLoading, setError, runAsync } = useAsync<ProjectSyncState>()
|
||||
const { indexAllReferences } = useReferencesContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (modalStatus !== 'run-merge' &&
|
||||
@@ -46,11 +50,17 @@ const GitSyncMergeModal = ({
|
||||
body: { message: commitMessage, claimConflictIsResolved },
|
||||
})
|
||||
)
|
||||
.then(data => {
|
||||
.then(async data => {
|
||||
switch (data.mergeStatus) {
|
||||
|
||||
case 'clean':
|
||||
setModalStatus('merge-overview')
|
||||
|
||||
await postJSON(`/project/${projectId}/flush`)
|
||||
.catch(e => debugConsole.error(e))
|
||||
|
||||
indexAllReferences(true)
|
||||
|
||||
break
|
||||
|
||||
case 'conflict':
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import Settings from '@overleaf/settings'
|
||||
import Modules from '../../app/src/infrastructure/Modules.mjs'
|
||||
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 = {}
|
||||
if (process.env.GITHUB_SYNC_ENABLED?.toLowerCase() === 'true') {
|
||||
logger.debug({}, 'Enabling GitHub Sync module')
|
||||
/*
|
||||
|
||||
const [{ default: GitHubSyncRouter },
|
||||
{ default: SyncStateManager },
|
||||
{ default: TokenManager }
|
||||
@@ -18,17 +15,15 @@ if (process.env.GITHUB_SYNC_ENABLED?.toLowerCase() === 'true') {
|
||||
import('./app/src/SyncStateManager.mjs'),
|
||||
import('./app/src/TokenManager.mjs'),
|
||||
])
|
||||
*/
|
||||
|
||||
// Delete project sync state from mongo (hook 'projectExpired')
|
||||
|
||||
const siteUrl = process.env.OVERLEAF_SITE_URL?.replace(/\/+$/, '') || 'http://localhost'
|
||||
const siteUrl = Settings.siteUrl.replace(/\/+$/, '') || 'http://localhost'
|
||||
Settings.githubSync = {
|
||||
clientID: process.env.GITHUB_SYNC_CLIENT_ID,
|
||||
clientSecret: process.env.GITHUB_SYNC_CLIENT_SECRET,
|
||||
callbackURL: `${siteUrl}/user/github-sync/oauth2/callback`,
|
||||
},
|
||||
|
||||
// Delete project sync state from mongo (hook 'projectExpired')
|
||||
Modules.hooks.attach('projectExpired', async projectId => {
|
||||
try {
|
||||
await SyncStateManager.removeProjectState(projectId)
|
||||
|
||||
Reference in New Issue
Block a user