Skip to content

Commit a8f64eb

Browse files
feat(backend): add API endpoint to trigger account-driven permission sync (SOU-578)
Adds POST /api/trigger-account-permission-sync that creates and enqueues an AccountPermissionSyncJob for a given accountId, with entitlement and provider validation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7d2d4bb commit a8f64eb

File tree

3 files changed

+57
-2
lines changed

3 files changed

+57
-2
lines changed

packages/backend/src/api.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import { PrismaClient, RepoIndexingJobType } from '@sourcebot/db';
2-
import { createLogger } from '@sourcebot/shared';
2+
import { createLogger, env, hasEntitlement } from '@sourcebot/shared';
33
import express, { Request, Response } from 'express';
44
import 'express-async-errors';
55
import * as http from "http";
66
import z from 'zod';
77
import { ConnectionManager } from './connectionManager.js';
8+
import { AccountPermissionSyncer } from './ee/accountPermissionSyncer.js';
89
import { PromClient } from './promClient.js';
910
import { RepoIndexManager } from './repoIndexManager.js';
1011
import { createGitHubRepoRecord } from './repoCompileUtils.js';
1112
import { Octokit } from '@octokit/rest';
12-
import { SINGLE_TENANT_ORG_ID } from './constants.js';
13+
import { PERMISSION_SYNC_SUPPORTED_IDENTITY_PROVIDERS, SINGLE_TENANT_ORG_ID } from './constants.js';
1314

1415
const logger = createLogger('api');
1516
const PORT = 3060;
@@ -22,6 +23,7 @@ export class Api {
2223
private prisma: PrismaClient,
2324
private connectionManager: ConnectionManager,
2425
private repoIndexManager: RepoIndexManager,
26+
private accountPermissionSyncer: AccountPermissionSyncer,
2527
) {
2628
const app = express();
2729
app.use(express.json());
@@ -36,6 +38,7 @@ export class Api {
3638

3739
app.post('/api/sync-connection', this.syncConnection.bind(this));
3840
app.post('/api/index-repo', this.indexRepo.bind(this));
41+
app.post('/api/trigger-account-permission-sync', this.triggerAccountPermissionSync.bind(this));
3942
app.post(`/api/experimental/add-github-repo`, this.experimental_addGithubRepo.bind(this));
4043

4144
this.server = app.listen(PORT, () => {
@@ -96,6 +99,41 @@ export class Api {
9699
res.status(200).json({ jobId });
97100
}
98101

102+
private async triggerAccountPermissionSync(req: Request, res: Response) {
103+
if (env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED !== 'true' || !hasEntitlement('permission-syncing')) {
104+
res.status(403).json({ error: 'Permission syncing is not enabled.' });
105+
return;
106+
}
107+
108+
const schema = z.object({
109+
accountId: z.string(),
110+
}).strict();
111+
112+
const parsed = schema.safeParse(req.body);
113+
if (!parsed.success) {
114+
res.status(400).json({ error: parsed.error.message });
115+
return;
116+
}
117+
118+
const { accountId } = parsed.data;
119+
const account = await this.prisma.account.findUnique({
120+
where: { id: accountId },
121+
});
122+
123+
if (!account) {
124+
res.status(404).json({ error: 'Account not found' });
125+
return;
126+
}
127+
128+
if (!PERMISSION_SYNC_SUPPORTED_IDENTITY_PROVIDERS.includes(account.provider as typeof PERMISSION_SYNC_SUPPORTED_IDENTITY_PROVIDERS[number])) {
129+
res.status(400).json({ error: `Provider '${account.provider}' does not support permission syncing.` });
130+
return;
131+
}
132+
133+
const jobId = await this.accountPermissionSyncer.schedulePermissionSyncForAccount(account);
134+
res.status(200).json({ jobId });
135+
}
136+
99137
private async experimental_addGithubRepo(req: Request, res: Response) {
100138
const schema = z.object({
101139
owner: z.string(),

packages/backend/src/ee/accountPermissionSyncer.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,22 @@ export class AccountPermissionSyncer {
116116
await this.queue.close();
117117
}
118118

119+
public async schedulePermissionSyncForAccount(account: Account) {
120+
const [job] = await this.db.accountPermissionSyncJob.createManyAndReturn({
121+
data: [{ accountId: account.id }],
122+
});
123+
124+
await this.queue.add('accountPermissionSyncJob', {
125+
jobId: job.id,
126+
}, {
127+
removeOnComplete: env.REDIS_REMOVE_ON_COMPLETE,
128+
removeOnFail: env.REDIS_REMOVE_ON_FAIL,
129+
priority: 1,
130+
});
131+
132+
return job.id;
133+
}
134+
119135
private async schedulePermissionSync(accounts: Account[]) {
120136
// @note: we don't perform this in a transaction because
121137
// we want to avoid the situation where a job is created and run

packages/backend/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ const api = new Api(
8282
prisma,
8383
connectionManager,
8484
repoIndexManager,
85+
accountPermissionSyncer,
8586
);
8687

8788
logger.info('Worker started.');

0 commit comments

Comments
 (0)