Merge pull request #231 from fporrmann/ext-ce-gitlab

GitLab Synchronization - Bugfix and cleanup
This commit is contained in:
Florian Porrmann
2026-08-11 16:11:31 +02:00
committed by GitHub
5 changed files with 68 additions and 76 deletions
@@ -190,7 +190,7 @@ async function fetchAllPages(url, options, operation) {
}
function getTokenRefreshTimestamp(token, safetyMarginInSec = 300) {
return token.created_at + token.expires_in - safetyMarginInSec;
return token.created_at + token.expires_in - safetyMarginInSec;
}
@@ -200,8 +200,8 @@ async function compareCommitsFull(token, repoFullName, from, to) {
url.searchParams.set('to', to)
return await fetchGitLabJson(url.toString(), {
headers: buildHeaders(token),
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
headers: buildHeaders(token),
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
}, 'compareCommitsFull')
}
@@ -224,8 +224,8 @@ function exchangeCodeForToken(code) {
headers: buildHeaders(),
json: {
client_id: Settings.gitlabSync.clientID,
client_secret: Settings.gitlabSync.clientSecret,
code: code,
client_secret: Settings.gitlabSync.clientSecret,
code: code,
grant_type: 'authorization_code',
redirect_uri: Settings.gitlabSync.callbackURL,
},
@@ -468,7 +468,7 @@ async function getBlobContent(token, repoFullName, sha) {
return await fetchGitLabJson(url, {
headers: buildHeaders(token),
signal: AbortSignal.timeout(REQUEST_LONG_TIMEOUT_MS)
}, 'getBlobContent').then(r => Buffer.from(r.content, 'base64'))
}, 'getBlobContent').then(r => r.content || '')
}
async function listNewCommitsWithStatus(token, fullName, branchName, fromCommit) {
@@ -118,14 +118,14 @@ async function oauth2Callback(req, res) {
let refresh_token
let refresh_timestamp
try {
[token, refresh_token, refresh_timestamp] = await api.exchangeCodeForToken(code)
[token, refresh_token, refresh_timestamp] = await api.exchangeCodeForToken(code)
if (!token || !refresh_token) {
HttpErrorHandler.badRequest(req, res, 'Failed to obtain access token from Git server')
return
}
} catch (err) {
const info = OError.getFullInfo(err)
const err_body = err.body
const err_body = err.body
logger.error(OError.getFullStack(err))
logger.error({ info, err_body, userId }, 'Failed to obtain access token from Git server')
HttpErrorHandler.badRequest(req, res, err.message || 'Bad request')
@@ -18,17 +18,16 @@ import { InvalidTokenError, NotFoundError } from './GitSyncErrors.mjs'
async function getGitConnState(userId) {
try {
const tokens = await TokenManager.getUserToken(userId)
if (!tokens) return false;
const { token } = tokens
const token = await TokenManager.getUserToken(userId)
if (!token) return false;
await api.getUser(token)
await api.getUser(token)
return true
} catch (err) {
if (err instanceof InvalidTokenError) {
logger.debug( { err, userId }, 'token invalid, treating as not connected')
return false
}
}
throw OError.tag(err, 'failed to validate token', { userId })
}
}
@@ -52,9 +51,9 @@ async function getProjectState(userId, projectId) {
}
let canPush
try {
const tokens = await TokenManager.getUserToken(userId)
if (!tokens) throw new InvalidTokenError('no user token', { userId, status: 400 });
const { token } = tokens
const token = await TokenManager.getUserToken(userId)
if (!token) throw new InvalidTokenError('no user token', { userId, status: 400 });
canPush = await api.getPushPermission(token, pss.repoFullName)
} catch (err) {
if ((err instanceof NotFoundError) ||
@@ -97,16 +96,14 @@ async function unlinkRepo(userId, projectId) {
async function listUserRepos(userId) {
const tokens = await TokenManager.getUserToken(userId)
if (!tokens) throw new InvalidTokenError('no user token', { userId, status: 400 });
const { token } = tokens
const token = await TokenManager.getUserToken(userId)
if (!token) throw new InvalidTokenError('no user token', { userId, status: 400 });
return await api.listUserRepos(token)
}
async function getUserAndOrgs(userId) {
const tokens = await TokenManager.getUserToken(userId)
if (!tokens) throw new InvalidTokenError('no user token', { userId, status: 400 });
const { token } = tokens
const token = await TokenManager.getUserToken(userId)
if (!token) throw new InvalidTokenError('no user token', { userId, status: 400 });
return await api.getUserAndOrgs(token)
}
@@ -117,9 +114,8 @@ async function getMergeOverview(userId, projectId) {
if (mergeStatus === 'conflict') return null
const tokens = await TokenManager.getUserToken(userId)
if (!tokens) throw new InvalidTokenError('no user token', { userId, status: 400 });
const { token } = tokens
const token = await TokenManager.getUserToken(userId)
if (!token) throw new InvalidTokenError('no user token', { userId, status: 400 });
const currentVersion = await HistoryManager.latestVersion(projectId.toString())
const isProjectUpdated = currentVersion !== lastSyncVersion
try {
@@ -148,9 +144,8 @@ async function getMergeOverview(userId, projectId) {
}
async function importRepo(userId, projectName, repoFullName, defaultBranchName) {
const tokens = await TokenManager.getUserToken(userId)
if (!tokens) throw new InvalidTokenError('no user token', { userId, status: 400 });
const { token } = tokens
const token = await TokenManager.getUserToken(userId)
if (!token) throw new InvalidTokenError('no user token', { userId, status: 400 });
const defaultBranchHead = await api.getBranchHead(token, repoFullName, defaultBranchName)
const fsPath = Path.join(Settings.path.dumpFolder, `gitlab_import_${crypto.randomUUID()}`)
@@ -203,9 +198,8 @@ async function exportProject(userId, projectId, repoOptions) {
throw new OError('Project is already linked to Git server', { projectId })
}
const tokens = await TokenManager.getUserToken(userId)
if (!tokens) throw new InvalidTokenError('no user token', { userId, status: 400 });
const { token } = tokens
const token = await TokenManager.getUserToken(userId)
if (!token) throw new InvalidTokenError('no user token', { userId, status: 400 });
const { path_with_namespace: repoFullName, default_branch: defaultBranchName } =
await api.createRepo(token, repoOptions)
@@ -40,9 +40,8 @@ async function doGitMergeWithoutLock(userId, projectId, message, claimConflictIs
claimConflictIsResolved && mergeStatus !== 'conflict'
) return { mergeStatus, repoFullName, unmergedBranchName }
const tokens = await TokenManager.getUserToken(userId)
if (!tokens) throw new InvalidTokenError('no user token', { userId, status: 400 });
const { token } = tokens
const token = await TokenManager.getUserToken(userId)
if (!token) throw new InvalidTokenError('no user token', { userId, status: 400 });
await DocumentUpdaterHandler.promises.flushProjectToMongo(projectId)
@@ -278,8 +277,7 @@ async function resolveConflictSyncState(
token,
repoFullName,
defaultBranchName,
newOlBranchHead,
tempBranchName,
tempBranchName
)
if (conflict) {
@@ -24,61 +24,61 @@ async function decryptAccessToken(tokenEncrypted) {
}
}
async function refreshToken(userId, tokens) {
logger.info({ userId }, "refreshing GitLab token")
async function refreshToken(userId, decData) {
logger.info({ userId }, "refreshing GitLab token")
if (!tokens.refresh_token) {
logger.error("refresh_token invalid")
return false
}
if (!decData.refresh_token) {
logger.error("refresh_token invalid")
return false
}
let token
let refresh_token
let refresh_timestamp
let token
let refresh_token
let refresh_timestamp
try {
[token, refresh_token, refresh_timestamp] = await api.refreshToken(tokens.refresh_token)
if (!token || !refresh_token) {
HttpErrorHandler.badRequest(req, res, 'Failed to refresh access token')
return false
}
} catch (err) {
const info = OError.getFullInfo(err)
logger.error(OError.getFullStack(err))
logger.error({ info, userId }, 'Failed to refresh access token')
HttpErrorHandler.badRequest(req, res, err.message || 'Bad request')
return false
}
try {
[token, refresh_token, refresh_timestamp] = await api.refreshToken(decData.refresh_token)
if (!token || !refresh_token) {
HttpErrorHandler.badRequest(req, res, 'Failed to refresh access token')
return false
}
} catch (err) {
const info = OError.getFullInfo(err)
logger.error(OError.getFullStack(err))
logger.error({ info, userId }, 'Failed to refresh access token')
HttpErrorHandler.badRequest(req, res, err.message || 'Bad request')
return false
}
try {
await saveUserToken(userId, { token, refresh_token, refresh_timestamp })
} catch (err) {
const info = OError.getFullInfo(err)
const errStatus = info?.status || 500
logger.error(OError.getFullStack(err))
logger.error({ info, userId }, 'Error saving user token')
HttpErrorHandler.handleErrorByStatusCode(req, res, err, errStatus)
return false
}
try {
await saveUserToken(userId, { token, refresh_token, refresh_timestamp })
} catch (err) {
const info = OError.getFullInfo(err)
const errStatus = info?.status || 500
logger.error(OError.getFullStack(err))
logger.error({ info, userId }, 'Error saving user token')
HttpErrorHandler.handleErrorByStatusCode(req, res, err, errStatus)
return false
}
return true
return true
}
// ------------------------- exports -------------------------- //
async function getUserToken(userId) {
const credentials = await GitLabSyncUserCredentials.findOne(normalizeQuery({ userId }))
if (!credentials) throw new InvalidTokenError('no user token', { userId, status: 400 })
let tokens = await decryptAccessToken(credentials.gitlab)
let decData = await decryptAccessToken(credentials.gitlab)
const now = Math.floor(Date.now() / 1000);
if (!tokens.refresh_timestamp || tokens.refresh_timestamp < now) {
const refreshed = await refreshToken(userId, tokens)
if (refreshed) {
return await getUserToken(userId)
}
if (!decData.refresh_timestamp || decData.refresh_timestamp < now) {
const refreshed = await refreshToken(userId, decData)
if (refreshed) {
return await getUserToken(userId)
}
}
return tokens
return decData.token
}
async function saveUserToken(userId, accessToken) {