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
|
// GH repo was force pushed, lastSybc commit possibly anavailable
|
||||||
async function resolveDetachedSyncState(
|
async function resolveDetachedSyncState(
|
||||||
token,
|
token,
|
||||||
@@ -574,7 +574,7 @@ async function buildDetachedSyncPlan({
|
|||||||
buffer = await HistoryManager.getProjectFileBuffer(projectId, currentVersion, path)
|
buffer = await HistoryManager.getProjectFileBuffer(projectId, currentVersion, path)
|
||||||
}
|
}
|
||||||
const sha = await api.uploadBlob(token, repoFullName, buffer)
|
const sha = await api.uploadBlob(token, repoFullName, buffer)
|
||||||
// assert sha !== localHash
|
// assert sha !== localHash
|
||||||
}
|
}
|
||||||
|
|
||||||
// OL == GH, do nothing
|
// OL == GH, do nothing
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-2
@@ -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':
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user