feat(integrations): add connectionScope (workspace|user) to OAuth flow

This commit is contained in:
Philipinho
2026-05-23 04:35:59 +01:00
parent 31043a9d98
commit 0ab6ae86e3
3 changed files with 80 additions and 9 deletions
@@ -149,15 +149,29 @@ export class OAuthService {
? new Date(Date.now() + tokenResponse.expires_in * 1000)
: null;
const connection = await this.connectionRepo.upsert({
integrationId,
userId,
workspaceId,
accessToken: encryptedAccessToken,
refreshToken: encryptedRefreshToken,
tokenExpiresAt,
scopes: tokenResponse.scope ?? null,
});
const connectionScope =
provider.definition.oauth?.connectionScope ?? 'user';
const connection =
connectionScope === 'workspace'
? await this.connectionRepo.upsertWorkspaceConnection({
integrationId,
userId,
workspaceId,
accessToken: encryptedAccessToken,
refreshToken: encryptedRefreshToken,
tokenExpiresAt,
scopes: tokenResponse.scope ?? null,
})
: await this.connectionRepo.upsert({
integrationId,
userId,
workspaceId,
accessToken: encryptedAccessToken,
refreshToken: encryptedRefreshToken,
tokenExpiresAt,
scopes: tokenResponse.scope ?? null,
});
if (provider.onConnected) {
await provider.onConnected({
@@ -4,6 +4,9 @@ export type OAuthConfig = {
authUrl: string;
tokenUrl: string;
scopes: string[];
// 'workspace' = one shared bot/app connection per integration (Slack model);
// 'user' (default) = each Docmost user OAuths separately and gets their own token (Linear, Jira, GitHub model)
connectionScope?: 'workspace' | 'user';
};
export type UnfurlPattern = {
@@ -95,6 +95,60 @@ export class IntegrationConnectionRepo {
.executeTakeFirstOrThrow();
}
async upsertWorkspaceConnection(
input: {
integrationId: string;
userId: string;
workspaceId: string;
accessToken: string;
refreshToken?: string | null;
tokenExpiresAt?: Date | null;
scopes?: string | null;
},
trx?: KyselyTransaction,
): Promise<IntegrationConnection> {
const db = dbOrTx(this.db, trx);
const existing = await this.findWorkspaceConnection(input.integrationId, trx);
if (existing) {
return this.update(
existing.id,
{
accessToken: input.accessToken,
refreshToken: input.refreshToken ?? null,
tokenExpiresAt: input.tokenExpiresAt ?? null,
scopes: input.scopes ?? null,
userId: input.userId,
},
trx,
);
}
// Clear any stale non-workspace row for the same (integration, user) to
// avoid the uq(integration_id, user_id) constraint blocking the insert.
await db
.deleteFrom('integrationConnections')
.where('integrationId', '=', input.integrationId)
.where('userId', '=', input.userId)
.where('kind', '!=', 'workspace')
.execute();
return db
.insertInto('integrationConnections')
.values({
integrationId: input.integrationId,
userId: input.userId,
workspaceId: input.workspaceId,
accessToken: input.accessToken,
refreshToken: input.refreshToken ?? null,
tokenExpiresAt: input.tokenExpiresAt ?? null,
scopes: input.scopes ?? null,
kind: 'workspace',
})
.returningAll()
.executeTakeFirstOrThrow();
}
async update(
connectionId: string,
data: UpdatableIntegrationConnection,