chore(worker): Refactor permission syncing join table to be between Account <> Repo (#600)

This commit is contained in:
Brendan Kellam 2025-11-04 20:12:07 -08:00 committed by GitHub
parent 449c76fdcc
commit 5fde901356
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 260 additions and 187 deletions

View file

@ -19,9 +19,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed issue with an unbounded `Promise.allSettled(...)` when retrieving details from the GitHub API about a large number of repositories (or orgs or users). [#591](https://github.com/sourcebot-dev/sourcebot/pull/591) - Fixed issue with an unbounded `Promise.allSettled(...)` when retrieving details from the GitHub API about a large number of repositories (or orgs or users). [#591](https://github.com/sourcebot-dev/sourcebot/pull/591)
- Fixed resource exhaustion (EAGAIN errors) when syncing generic-git-host connections with thousands of repositories. [#593](https://github.com/sourcebot-dev/sourcebot/pull/593) - Fixed resource exhaustion (EAGAIN errors) when syncing generic-git-host connections with thousands of repositories. [#593](https://github.com/sourcebot-dev/sourcebot/pull/593)
## Removed ### Removed
- Removed built-in secret manager. [#592](https://github.com/sourcebot-dev/sourcebot/pull/592) - Removed built-in secret manager. [#592](https://github.com/sourcebot-dev/sourcebot/pull/592)
### Changed
- Changed internal representation of how repo permissions are represented in the database. [#600](https://github.com/sourcebot-dev/sourcebot/pull/600)
## [4.8.1] - 2025-10-29 ## [4.8.1] - 2025-10-29
### Fixed ### Fixed

View file

@ -1,5 +1,5 @@
import * as Sentry from "@sentry/node"; import * as Sentry from "@sentry/node";
import { PrismaClient, User, UserPermissionSyncJobStatus } from "@sourcebot/db"; import { PrismaClient, AccountPermissionSyncJobStatus, Account } from "@sourcebot/db";
import { createLogger } from "@sourcebot/logger"; import { createLogger } from "@sourcebot/logger";
import { Job, Queue, Worker } from "bullmq"; import { Job, Queue, Worker } from "bullmq";
import { Redis } from "ioredis"; import { Redis } from "ioredis";
@ -14,16 +14,15 @@ const LOG_TAG = 'user-permission-syncer';
const logger = createLogger(LOG_TAG); const logger = createLogger(LOG_TAG);
const createJobLogger = (jobId: string) => createLogger(`${LOG_TAG}:job:${jobId}`); const createJobLogger = (jobId: string) => createLogger(`${LOG_TAG}:job:${jobId}`);
const QUEUE_NAME = 'userPermissionSyncQueue'; const QUEUE_NAME = 'accountPermissionSyncQueue';
type UserPermissionSyncJob = { type AccountPermissionSyncJob = {
jobId: string; jobId: string;
} }
export class AccountPermissionSyncer {
export class UserPermissionSyncer { private queue: Queue<AccountPermissionSyncJob>;
private queue: Queue<UserPermissionSyncJob>; private worker: Worker<AccountPermissionSyncJob>;
private worker: Worker<UserPermissionSyncJob>;
private interval?: NodeJS.Timeout; private interval?: NodeJS.Timeout;
constructor( constructor(
@ -31,10 +30,10 @@ export class UserPermissionSyncer {
private settings: Settings, private settings: Settings,
redis: Redis, redis: Redis,
) { ) {
this.queue = new Queue<UserPermissionSyncJob>(QUEUE_NAME, { this.queue = new Queue<AccountPermissionSyncJob>(QUEUE_NAME, {
connection: redis, connection: redis,
}); });
this.worker = new Worker<UserPermissionSyncJob>(QUEUE_NAME, this.runJob.bind(this), { this.worker = new Worker<AccountPermissionSyncJob>(QUEUE_NAME, this.runJob.bind(this), {
connection: redis, connection: redis,
concurrency: 1, concurrency: 1,
}); });
@ -52,16 +51,12 @@ export class UserPermissionSyncer {
this.interval = setInterval(async () => { this.interval = setInterval(async () => {
const thresholdDate = new Date(Date.now() - this.settings.experiment_userDrivenPermissionSyncIntervalMs); const thresholdDate = new Date(Date.now() - this.settings.experiment_userDrivenPermissionSyncIntervalMs);
const users = await this.db.user.findMany({ const accounts = await this.db.account.findMany({
where: { where: {
AND: [ AND: [
{ {
accounts: { provider: {
some: { in: PERMISSION_SYNC_SUPPORTED_CODE_HOST_TYPES
provider: {
in: PERMISSION_SYNC_SUPPORTED_CODE_HOST_TYPES
}
}
} }
}, },
{ {
@ -79,15 +74,15 @@ export class UserPermissionSyncer {
{ {
status: { status: {
in: [ in: [
UserPermissionSyncJobStatus.PENDING, AccountPermissionSyncJobStatus.PENDING,
UserPermissionSyncJobStatus.IN_PROGRESS, AccountPermissionSyncJobStatus.IN_PROGRESS,
], ],
} }
}, },
// Don't schedule if there are recent failed jobs (within the threshold date). Note `gt` is used here since this is a inverse condition. // Don't schedule if there are recent failed jobs (within the threshold date). Note `gt` is used here since this is a inverse condition.
{ {
AND: [ AND: [
{ status: UserPermissionSyncJobStatus.FAILED }, { status: AccountPermissionSyncJobStatus.FAILED },
{ completedAt: { gt: thresholdDate } }, { completedAt: { gt: thresholdDate } },
] ]
} }
@ -100,7 +95,7 @@ export class UserPermissionSyncer {
} }
}); });
await this.schedulePermissionSync(users); await this.schedulePermissionSync(accounts);
}, 1000 * 5); }, 1000 * 5);
} }
@ -112,18 +107,18 @@ export class UserPermissionSyncer {
await this.queue.close(); await this.queue.close();
} }
private async schedulePermissionSync(users: User[]) { private async schedulePermissionSync(accounts: Account[]) {
// @note: we don't perform this in a transaction because // @note: we don't perform this in a transaction because
// we want to avoid the situation where a job is created and run // we want to avoid the situation where a job is created and run
// prior to the transaction being committed. // prior to the transaction being committed.
const jobs = await this.db.userPermissionSyncJob.createManyAndReturn({ const jobs = await this.db.accountPermissionSyncJob.createManyAndReturn({
data: users.map(user => ({ data: accounts.map(account => ({
userId: user.id, accountId: account.id,
})), })),
}); });
await this.queue.addBulk(jobs.map((job) => ({ await this.queue.addBulk(jobs.map((job) => ({
name: 'userPermissionSyncJob', name: 'accountPermissionSyncJob',
data: { data: {
jobId: job.id, jobId: job.id,
}, },
@ -134,103 +129,97 @@ export class UserPermissionSyncer {
}))) })))
} }
private async runJob(job: Job<UserPermissionSyncJob>) { private async runJob(job: Job<AccountPermissionSyncJob>) {
const id = job.data.jobId; const id = job.data.jobId;
const logger = createJobLogger(id); const logger = createJobLogger(id);
const { user } = await this.db.userPermissionSyncJob.update({ const { account } = await this.db.accountPermissionSyncJob.update({
where: { where: {
id, id,
}, },
data: { data: {
status: UserPermissionSyncJobStatus.IN_PROGRESS, status: AccountPermissionSyncJobStatus.IN_PROGRESS,
}, },
select: { select: {
user: { account: {
include: { include: {
accounts: true, user: true,
} }
} }
} }
}); });
if (!user) { logger.info(`Syncing permissions for ${account.provider} account (id: ${account.id}) for user ${account.user.email}...`);
throw new Error(`User ${id} not found`);
}
logger.info(`Syncing permissions for user ${user.email}...`);
// Get a list of all repos that the user has access to from all connected accounts. // Get a list of all repos that the user has access to from all connected accounts.
const repoIds = await (async () => { const repoIds = await (async () => {
const aggregatedRepoIds: Set<number> = new Set(); const aggregatedRepoIds: Set<number> = new Set();
for (const account of user.accounts) { if (account.provider === 'github') {
if (account.provider === 'github') { if (!account.access_token) {
if (!account.access_token) { throw new Error(`User '${account.user.email}' does not have an GitHub OAuth access token associated with their GitHub account.`);
throw new Error(`User '${user.email}' does not have an GitHub OAuth access token associated with their GitHub account.`);
}
const { octokit } = await createOctokitFromToken({
token: account.access_token,
url: env.AUTH_EE_GITHUB_BASE_URL,
});
// @note: we only care about the private repos since we don't need to build a mapping
// for public repos.
// @see: packages/web/src/prisma.ts
const githubRepos = await getReposForAuthenticatedUser(/* visibility = */ 'private', octokit);
const gitHubRepoIds = githubRepos.map(repo => repo.id.toString());
const repos = await this.db.repo.findMany({
where: {
external_codeHostType: 'github',
external_id: {
in: gitHubRepoIds,
}
}
});
repos.forEach(repo => aggregatedRepoIds.add(repo.id));
} else if (account.provider === 'gitlab') {
if (!account.access_token) {
throw new Error(`User '${user.email}' does not have a GitLab OAuth access token associated with their GitLab account.`);
}
const api = await createGitLabFromOAuthToken({
oauthToken: account.access_token,
url: env.AUTH_EE_GITLAB_BASE_URL,
});
// @note: we only care about the private and internal repos since we don't need to build a mapping
// for public repos.
// @see: packages/web/src/prisma.ts
const privateGitLabProjects = await getProjectsForAuthenticatedUser('private', api);
const internalGitLabProjects = await getProjectsForAuthenticatedUser('internal', api);
const gitLabProjectIds = [
...privateGitLabProjects,
...internalGitLabProjects,
].map(project => project.id.toString());
const repos = await this.db.repo.findMany({
where: {
external_codeHostType: 'gitlab',
external_id: {
in: gitLabProjectIds,
}
}
});
repos.forEach(repo => aggregatedRepoIds.add(repo.id));
} }
const { octokit } = await createOctokitFromToken({
token: account.access_token,
url: env.AUTH_EE_GITHUB_BASE_URL,
});
// @note: we only care about the private repos since we don't need to build a mapping
// for public repos.
// @see: packages/web/src/prisma.ts
const githubRepos = await getReposForAuthenticatedUser(/* visibility = */ 'private', octokit);
const gitHubRepoIds = githubRepos.map(repo => repo.id.toString());
const repos = await this.db.repo.findMany({
where: {
external_codeHostType: 'github',
external_id: {
in: gitHubRepoIds,
}
}
});
repos.forEach(repo => aggregatedRepoIds.add(repo.id));
} else if (account.provider === 'gitlab') {
if (!account.access_token) {
throw new Error(`User '${account.user.email}' does not have a GitLab OAuth access token associated with their GitLab account.`);
}
const api = await createGitLabFromOAuthToken({
oauthToken: account.access_token,
url: env.AUTH_EE_GITLAB_BASE_URL,
});
// @note: we only care about the private and internal repos since we don't need to build a mapping
// for public repos.
// @see: packages/web/src/prisma.ts
const privateGitLabProjects = await getProjectsForAuthenticatedUser('private', api);
const internalGitLabProjects = await getProjectsForAuthenticatedUser('internal', api);
const gitLabProjectIds = [
...privateGitLabProjects,
...internalGitLabProjects,
].map(project => project.id.toString());
const repos = await this.db.repo.findMany({
where: {
external_codeHostType: 'gitlab',
external_id: {
in: gitLabProjectIds,
}
}
});
repos.forEach(repo => aggregatedRepoIds.add(repo.id));
} }
return Array.from(aggregatedRepoIds); return Array.from(aggregatedRepoIds);
})(); })();
await this.db.$transaction([ await this.db.$transaction([
this.db.user.update({ this.db.account.update({
where: { where: {
id: user.id, id: account.id,
}, },
data: { data: {
accessibleRepos: { accessibleRepos: {
@ -238,9 +227,9 @@ export class UserPermissionSyncer {
} }
} }
}), }),
this.db.userToRepoPermission.createMany({ this.db.accountToRepoPermission.createMany({
data: repoIds.map(repoId => ({ data: repoIds.map(repoId => ({
userId: user.id, accountId: account.id,
repoId, repoId,
})), })),
skipDuplicates: true, skipDuplicates: true,
@ -248,31 +237,35 @@ export class UserPermissionSyncer {
]); ]);
} }
private async onJobCompleted(job: Job<UserPermissionSyncJob>) { private async onJobCompleted(job: Job<AccountPermissionSyncJob>) {
const logger = createJobLogger(job.data.jobId); const logger = createJobLogger(job.data.jobId);
const { user } = await this.db.userPermissionSyncJob.update({ const { account } = await this.db.accountPermissionSyncJob.update({
where: { where: {
id: job.data.jobId, id: job.data.jobId,
}, },
data: { data: {
status: UserPermissionSyncJobStatus.COMPLETED, status: AccountPermissionSyncJobStatus.COMPLETED,
user: { account: {
update: { update: {
permissionSyncedAt: new Date(), permissionSyncedAt: new Date(),
} },
}, },
completedAt: new Date(), completedAt: new Date(),
}, },
select: { select: {
user: true account: {
include: {
user: true,
}
}
} }
}); });
logger.info(`Permissions synced for user ${user.email}`); logger.info(`Permissions synced for ${account.provider} account (id: ${account.id}) for user ${account.user.email}`);
} }
private async onJobFailed(job: Job<UserPermissionSyncJob> | undefined, err: Error) { private async onJobFailed(job: Job<AccountPermissionSyncJob> | undefined, err: Error) {
const logger = createJobLogger(job?.data.jobId ?? 'unknown'); const logger = createJobLogger(job?.data.jobId ?? 'unknown');
Sentry.captureException(err, { Sentry.captureException(err, {
@ -282,26 +275,30 @@ export class UserPermissionSyncer {
} }
}); });
const errorMessage = (email: string) => `User permission sync job failed for user ${email}: ${err.message}`; const errorMessage = (accountId: string, email: string) => `Account permission sync job failed for account (id: ${accountId}) for user ${email}: ${err.message}`;
if (job) { if (job) {
const { user } = await this.db.userPermissionSyncJob.update({ const { account } = await this.db.accountPermissionSyncJob.update({
where: { where: {
id: job.data.jobId, id: job.data.jobId,
}, },
data: { data: {
status: UserPermissionSyncJobStatus.FAILED, status: AccountPermissionSyncJobStatus.FAILED,
completedAt: new Date(), completedAt: new Date(),
errorMessage: err.message, errorMessage: err.message,
}, },
select: { select: {
user: true, account: {
include: {
user: true,
}
}
} }
}); });
logger.error(errorMessage(user.email ?? user.id)); logger.error(errorMessage(account.id, account.user.email ?? 'unknown user (email not found)'));
} else { } else {
logger.error(errorMessage('unknown job (id not found)')); logger.error(errorMessage('unknown account (id not found)', 'unknown user (id not found)'));
} }
} }
} }

View file

@ -168,7 +168,7 @@ export class RepoPermissionSyncer {
throw new Error(`No credentials found for repo ${id}`); throw new Error(`No credentials found for repo ${id}`);
} }
const userIds = await (async () => { const accountIds = await (async () => {
if (repo.external_codeHostType === 'github') { if (repo.external_codeHostType === 'github') {
const isGitHubCloud = credentials.hostUrl ? new URL(credentials.hostUrl).hostname === GITHUB_CLOUD_HOSTNAME : false; const isGitHubCloud = credentials.hostUrl ? new URL(credentials.hostUrl).hostname === GITHUB_CLOUD_HOSTNAME : false;
const { octokit } = await createOctokitFromToken({ const { octokit } = await createOctokitFromToken({
@ -195,12 +195,9 @@ export class RepoPermissionSyncer {
in: githubUserIds, in: githubUserIds,
} }
}, },
select: {
userId: true,
},
}); });
return accounts.map(account => account.userId); return accounts.map(account => account.id);
} else if (repo.external_codeHostType === 'gitlab') { } else if (repo.external_codeHostType === 'gitlab') {
const api = await createGitLabFromPersonalAccessToken({ const api = await createGitLabFromPersonalAccessToken({
token: credentials.token, token: credentials.token,
@ -222,12 +219,9 @@ export class RepoPermissionSyncer {
in: gitlabUserIds, in: gitlabUserIds,
} }
}, },
select: {
userId: true,
},
}); });
return accounts.map(account => account.userId); return accounts.map(account => account.id);
} }
return []; return [];
@ -239,14 +233,14 @@ export class RepoPermissionSyncer {
id: repo.id, id: repo.id,
}, },
data: { data: {
permittedUsers: { permittedAccounts: {
deleteMany: {}, deleteMany: {},
} }
} }
}), }),
this.db.userToRepoPermission.createMany({ this.db.accountToRepoPermission.createMany({
data: userIds.map(userId => ({ data: accountIds.map(accountId => ({
userId, accountId,
repoId: repo.id, repoId: repo.id,
})), })),
}) })

View file

@ -11,7 +11,7 @@ import { ConnectionManager } from './connectionManager.js';
import { INDEX_CACHE_DIR, REPOS_CACHE_DIR } from './constants.js'; import { INDEX_CACHE_DIR, REPOS_CACHE_DIR } from './constants.js';
import { GithubAppManager } from "./ee/githubAppManager.js"; import { GithubAppManager } from "./ee/githubAppManager.js";
import { RepoPermissionSyncer } from './ee/repoPermissionSyncer.js'; import { RepoPermissionSyncer } from './ee/repoPermissionSyncer.js';
import { UserPermissionSyncer } from "./ee/userPermissionSyncer.js"; import { AccountPermissionSyncer } from "./ee/accountPermissionSyncer.js";
import { env } from "./env.js"; import { env } from "./env.js";
import { PromClient } from './promClient.js'; import { PromClient } from './promClient.js';
import { RepoIndexManager } from "./repoIndexManager.js"; import { RepoIndexManager } from "./repoIndexManager.js";
@ -52,7 +52,7 @@ if (hasEntitlement('github-app')) {
const connectionManager = new ConnectionManager(prisma, settings, redis, promClient); const connectionManager = new ConnectionManager(prisma, settings, redis, promClient);
const repoPermissionSyncer = new RepoPermissionSyncer(prisma, settings, redis); const repoPermissionSyncer = new RepoPermissionSyncer(prisma, settings, redis);
const userPermissionSyncer = new UserPermissionSyncer(prisma, settings, redis); const accountPermissionSyncer = new AccountPermissionSyncer(prisma, settings, redis);
const repoIndexManager = new RepoIndexManager(prisma, settings, redis, promClient); const repoIndexManager = new RepoIndexManager(prisma, settings, redis, promClient);
const configManager = new ConfigManager(prisma, connectionManager, env.CONFIG_PATH); const configManager = new ConfigManager(prisma, connectionManager, env.CONFIG_PATH);
@ -65,7 +65,7 @@ if (env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true' && !hasEntitlement('per
} }
else if (env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true' && hasEntitlement('permission-syncing')) { else if (env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true' && hasEntitlement('permission-syncing')) {
repoPermissionSyncer.startScheduler(); repoPermissionSyncer.startScheduler();
userPermissionSyncer.startScheduler(); accountPermissionSyncer.startScheduler();
} }
logger.info('Worker started.'); logger.info('Worker started.');
@ -81,7 +81,7 @@ const cleanup = async (signal: string) => {
repoIndexManager.dispose(), repoIndexManager.dispose(),
connectionManager.dispose(), connectionManager.dispose(),
repoPermissionSyncer.dispose(), repoPermissionSyncer.dispose(),
userPermissionSyncer.dispose(), accountPermissionSyncer.dispose(),
promClient.dispose(), promClient.dispose(),
configManager.dispose(), configManager.dispose(),
]), ]),

View file

@ -0,0 +1,65 @@
/*
Warnings:
- You are about to drop the column `permissionSyncedAt` on the `User` table. All the data in the column will be lost.
- You are about to drop the `UserPermissionSyncJob` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `UserToRepoPermission` table. If the table is not empty, all the data it contains will be lost.
*/
-- CreateEnum
CREATE TYPE "AccountPermissionSyncJobStatus" AS ENUM ('PENDING', 'IN_PROGRESS', 'COMPLETED', 'FAILED');
-- DropForeignKey
ALTER TABLE "UserPermissionSyncJob" DROP CONSTRAINT "UserPermissionSyncJob_userId_fkey";
-- DropForeignKey
ALTER TABLE "UserToRepoPermission" DROP CONSTRAINT "UserToRepoPermission_repoId_fkey";
-- DropForeignKey
ALTER TABLE "UserToRepoPermission" DROP CONSTRAINT "UserToRepoPermission_userId_fkey";
-- AlterTable
ALTER TABLE "Account" ADD COLUMN "permissionSyncedAt" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "User" DROP COLUMN "permissionSyncedAt";
-- DropTable
DROP TABLE "UserPermissionSyncJob";
-- DropTable
DROP TABLE "UserToRepoPermission";
-- DropEnum
DROP TYPE "UserPermissionSyncJobStatus";
-- CreateTable
CREATE TABLE "AccountPermissionSyncJob" (
"id" TEXT NOT NULL,
"status" "AccountPermissionSyncJobStatus" NOT NULL DEFAULT 'PENDING',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"completedAt" TIMESTAMP(3),
"errorMessage" TEXT,
"accountId" TEXT NOT NULL,
CONSTRAINT "AccountPermissionSyncJob_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AccountToRepoPermission" (
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"repoId" INTEGER NOT NULL,
"accountId" TEXT NOT NULL,
CONSTRAINT "AccountToRepoPermission_pkey" PRIMARY KEY ("repoId","accountId")
);
-- AddForeignKey
ALTER TABLE "AccountPermissionSyncJob" ADD CONSTRAINT "AccountPermissionSyncJob_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AccountToRepoPermission" ADD CONSTRAINT "AccountToRepoPermission_repoId_fkey" FOREIGN KEY ("repoId") REFERENCES "Repo"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AccountToRepoPermission" ADD CONSTRAINT "AccountToRepoPermission_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View file

@ -59,7 +59,7 @@ model Repo {
connections RepoToConnection[] connections RepoToConnection[]
imageUrl String? imageUrl String?
permittedUsers UserToRepoPermission[] permittedAccounts AccountToRepoPermission[]
permissionSyncJobs RepoPermissionSyncJob[] permissionSyncJobs RepoPermissionSyncJob[]
permissionSyncedAt DateTime? /// When the permissions were last synced successfully. permissionSyncedAt DateTime? /// When the permissions were last synced successfully.
@ -349,7 +349,6 @@ model User {
accounts Account[] accounts Account[]
orgs UserToOrg[] orgs UserToOrg[]
accountRequest AccountRequest? accountRequest AccountRequest?
accessibleRepos UserToRepoPermission[]
/// List of pending invites that the user has created /// List of pending invites that the user has created
invites Invite[] invites Invite[]
@ -361,40 +360,38 @@ model User {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
permissionSyncJobs UserPermissionSyncJob[]
permissionSyncedAt DateTime?
} }
enum UserPermissionSyncJobStatus { enum AccountPermissionSyncJobStatus {
PENDING PENDING
IN_PROGRESS IN_PROGRESS
COMPLETED COMPLETED
FAILED FAILED
} }
model UserPermissionSyncJob { model AccountPermissionSyncJob {
id String @id @default(cuid()) id String @id @default(cuid())
status UserPermissionSyncJobStatus @default(PENDING) status AccountPermissionSyncJobStatus @default(PENDING)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
completedAt DateTime? completedAt DateTime?
errorMessage String? errorMessage String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade) account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
userId String accountId String
} }
model UserToRepoPermission { model AccountToRepoPermission {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
repo Repo @relation(fields: [repoId], references: [id], onDelete: Cascade) repo Repo @relation(fields: [repoId], references: [id], onDelete: Cascade)
repoId Int repoId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade) account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
userId String accountId String
@@id([repoId, userId]) @@id([repoId, accountId])
} }
// @see : https://authjs.dev/concepts/database-models#account // @see : https://authjs.dev/concepts/database-models#account
@ -412,6 +409,12 @@ model Account {
id_token String? id_token String?
session_state String? session_state String?
/// List of repos that this account has access to.
accessibleRepos AccountToRepoPermission[]
permissionSyncJobs AccountPermissionSyncJob[]
permissionSyncedAt DateTime?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt

View file

@ -1,5 +1,5 @@
import { SINGLE_TENANT_ORG_DOMAIN, SINGLE_TENANT_ORG_ID, SINGLE_TENANT_ORG_NAME } from '@/lib/constants'; import { SINGLE_TENANT_ORG_DOMAIN, SINGLE_TENANT_ORG_ID, SINGLE_TENANT_ORG_NAME } from '@/lib/constants';
import { ApiKey, Org, PrismaClient, User } from '@prisma/client'; import { Account, ApiKey, Org, PrismaClient, User } from '@prisma/client';
import { beforeEach, vi } from 'vitest'; import { beforeEach, vi } from 'vitest';
import { mockDeep, mockReset } from 'vitest-mock-extended'; import { mockDeep, mockReset } from 'vitest-mock-extended';
@ -35,7 +35,7 @@ export const MOCK_API_KEY: ApiKey = {
createdById: '1', createdById: '1',
} }
export const MOCK_USER: User = { export const MOCK_USER_WITH_ACCOUNTS: User & { accounts: Account[] } = {
id: '1', id: '1',
name: 'Test User', name: 'Test User',
email: 'test@test.com', email: 'test@test.com',
@ -44,7 +44,7 @@ export const MOCK_USER: User = {
hashedPassword: null, hashedPassword: null,
emailVerified: null, emailVerified: null,
image: null, image: null,
permissionSyncedAt: null accounts: [],
} }
export const userScopedPrismaClientExtension = vi.fn(); export const userScopedPrismaClientExtension = vi.fn();

View file

@ -20,7 +20,7 @@ if (env.NODE_ENV !== "production") globalForPrisma.prisma = prisma
* Creates a prisma client extension that scopes queries to striclty information * Creates a prisma client extension that scopes queries to striclty information
* a given user should be able to access. * a given user should be able to access.
*/ */
export const userScopedPrismaClientExtension = (userId?: string) => { export const userScopedPrismaClientExtension = (accountIds?: string[]) => {
return Prisma.defineExtension( return Prisma.defineExtension(
(prisma) => { (prisma) => {
return prisma.$extends({ return prisma.$extends({
@ -28,17 +28,21 @@ export const userScopedPrismaClientExtension = (userId?: string) => {
...(env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true' && hasEntitlement('permission-syncing') ? { ...(env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true' && hasEntitlement('permission-syncing') ? {
repo: { repo: {
async $allOperations({ args, query }) { async $allOperations({ args, query }) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any const argsWithWhere = args as Record<string, unknown> & {
const argsWithWhere = args as any; where?: Prisma.RepoWhereInput;
}
argsWithWhere.where = { argsWithWhere.where = {
...(argsWithWhere.where || {}), ...(argsWithWhere.where || {}),
OR: [ OR: [
// Only include repos that are permitted to the user // Only include repos that are permitted to the user
...(userId ? [ ...(accountIds ? [
{ {
permittedUsers: { permittedAccounts: {
some: { some: {
userId, accountId: {
in: accountIds,
}
} }
} }
}, },
@ -48,7 +52,7 @@ export const userScopedPrismaClientExtension = (userId?: string) => {
isPublic: true, isPublic: true,
} }
] ]
} };
return query(args); return query(args);
} }

View file

@ -2,7 +2,7 @@ import { expect, test, vi, beforeEach, describe } from 'vitest';
import { Session } from 'next-auth'; import { Session } from 'next-auth';
import { notAuthenticated } from './lib/serviceError'; import { notAuthenticated } from './lib/serviceError';
import { getAuthContext, getAuthenticatedUser, withAuthV2, withOptionalAuthV2 } from './withAuthV2'; import { getAuthContext, getAuthenticatedUser, withAuthV2, withOptionalAuthV2 } from './withAuthV2';
import { MOCK_API_KEY, MOCK_ORG, MOCK_USER, prisma } from './__mocks__/prisma'; import { MOCK_API_KEY, MOCK_ORG, MOCK_USER_WITH_ACCOUNTS, prisma } from './__mocks__/prisma';
import { OrgRole } from '@sourcebot/db'; import { OrgRole } from '@sourcebot/db';
const mocks = vi.hoisted(() => { const mocks = vi.hoisted(() => {
@ -83,7 +83,7 @@ describe('getAuthenticatedUser', () => {
test('should return a user object if a valid session is present', async () => { test('should return a user object if a valid session is present', async () => {
const userId = 'test-user-id'; const userId = 'test-user-id';
prisma.user.findUnique.mockResolvedValue({ prisma.user.findUnique.mockResolvedValue({
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}); });
setMockSession(createMockSession({ user: { id: 'test-user-id' } })); setMockSession(createMockSession({ user: { id: 'test-user-id' } }));
@ -95,7 +95,7 @@ describe('getAuthenticatedUser', () => {
test('should return a user object if a valid api key is present', async () => { test('should return a user object if a valid api key is present', async () => {
const userId = 'test-user-id'; const userId = 'test-user-id';
prisma.user.findUnique.mockResolvedValue({ prisma.user.findUnique.mockResolvedValue({
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}); });
prisma.apiKey.findUnique.mockResolvedValue({ prisma.apiKey.findUnique.mockResolvedValue({
@ -165,7 +165,7 @@ describe('getAuthContext', () => {
test('should return a auth context object if a valid session is present and the user is a member of the organization', async () => { test('should return a auth context object if a valid session is present and the user is a member of the organization', async () => {
const userId = 'test-user-id'; const userId = 'test-user-id';
prisma.user.findUnique.mockResolvedValue({ prisma.user.findUnique.mockResolvedValue({
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}); });
prisma.org.findUnique.mockResolvedValue({ prisma.org.findUnique.mockResolvedValue({
@ -183,7 +183,7 @@ describe('getAuthContext', () => {
expect(authContext).not.toBeUndefined(); expect(authContext).not.toBeUndefined();
expect(authContext).toStrictEqual({ expect(authContext).toStrictEqual({
user: { user: {
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}, },
org: MOCK_ORG, org: MOCK_ORG,
@ -195,7 +195,7 @@ describe('getAuthContext', () => {
test('should return a auth context object if a valid session is present and the user is a member of the organization with OWNER role', async () => { test('should return a auth context object if a valid session is present and the user is a member of the organization with OWNER role', async () => {
const userId = 'test-user-id'; const userId = 'test-user-id';
prisma.user.findUnique.mockResolvedValue({ prisma.user.findUnique.mockResolvedValue({
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}); });
prisma.org.findUnique.mockResolvedValue({ prisma.org.findUnique.mockResolvedValue({
@ -213,7 +213,7 @@ describe('getAuthContext', () => {
expect(authContext).not.toBeUndefined(); expect(authContext).not.toBeUndefined();
expect(authContext).toStrictEqual({ expect(authContext).toStrictEqual({
user: { user: {
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}, },
org: MOCK_ORG, org: MOCK_ORG,
@ -225,7 +225,7 @@ describe('getAuthContext', () => {
test('should return a auth context object if a valid session is present and the user is not a member of the organization. The role should be GUEST.', async () => { test('should return a auth context object if a valid session is present and the user is not a member of the organization. The role should be GUEST.', async () => {
const userId = 'test-user-id'; const userId = 'test-user-id';
prisma.user.findUnique.mockResolvedValue({ prisma.user.findUnique.mockResolvedValue({
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}); });
prisma.org.findUnique.mockResolvedValue({ prisma.org.findUnique.mockResolvedValue({
@ -238,7 +238,7 @@ describe('getAuthContext', () => {
expect(authContext).not.toBeUndefined(); expect(authContext).not.toBeUndefined();
expect(authContext).toStrictEqual({ expect(authContext).toStrictEqual({
user: { user: {
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}, },
org: MOCK_ORG, org: MOCK_ORG,
@ -268,7 +268,7 @@ describe('withAuthV2', () => {
test('should call the callback with the auth context object if a valid session is present and the user is a member of the organization', async () => { test('should call the callback with the auth context object if a valid session is present and the user is a member of the organization', async () => {
const userId = 'test-user-id'; const userId = 'test-user-id';
prisma.user.findUnique.mockResolvedValue({ prisma.user.findUnique.mockResolvedValue({
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}); });
prisma.org.findUnique.mockResolvedValue({ prisma.org.findUnique.mockResolvedValue({
@ -286,7 +286,7 @@ describe('withAuthV2', () => {
const result = await withAuthV2(cb); const result = await withAuthV2(cb);
expect(cb).toHaveBeenCalledWith({ expect(cb).toHaveBeenCalledWith({
user: { user: {
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}, },
org: MOCK_ORG, org: MOCK_ORG,
@ -298,7 +298,7 @@ describe('withAuthV2', () => {
test('should call the callback with the auth context object if a valid session is present and the user is a member of the organization with OWNER role', async () => { test('should call the callback with the auth context object if a valid session is present and the user is a member of the organization with OWNER role', async () => {
const userId = 'test-user-id'; const userId = 'test-user-id';
prisma.user.findUnique.mockResolvedValue({ prisma.user.findUnique.mockResolvedValue({
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}); });
prisma.org.findUnique.mockResolvedValue({ prisma.org.findUnique.mockResolvedValue({
@ -316,7 +316,7 @@ describe('withAuthV2', () => {
const result = await withAuthV2(cb); const result = await withAuthV2(cb);
expect(cb).toHaveBeenCalledWith({ expect(cb).toHaveBeenCalledWith({
user: { user: {
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}, },
org: MOCK_ORG, org: MOCK_ORG,
@ -328,7 +328,7 @@ describe('withAuthV2', () => {
test('should call the callback with the auth context object if a valid session is present and the user is a member of the organization (api key)', async () => { test('should call the callback with the auth context object if a valid session is present and the user is a member of the organization (api key)', async () => {
const userId = 'test-user-id'; const userId = 'test-user-id';
prisma.user.findUnique.mockResolvedValue({ prisma.user.findUnique.mockResolvedValue({
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}); });
prisma.org.findUnique.mockResolvedValue({ prisma.org.findUnique.mockResolvedValue({
@ -351,7 +351,7 @@ describe('withAuthV2', () => {
const result = await withAuthV2(cb); const result = await withAuthV2(cb);
expect(cb).toHaveBeenCalledWith({ expect(cb).toHaveBeenCalledWith({
user: { user: {
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}, },
org: MOCK_ORG, org: MOCK_ORG,
@ -363,7 +363,7 @@ describe('withAuthV2', () => {
test('should call the callback with the auth context object if a valid session is present and the user is a member of the organization with OWNER role (api key)', async () => { test('should call the callback with the auth context object if a valid session is present and the user is a member of the organization with OWNER role (api key)', async () => {
const userId = 'test-user-id'; const userId = 'test-user-id';
prisma.user.findUnique.mockResolvedValue({ prisma.user.findUnique.mockResolvedValue({
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}); });
prisma.org.findUnique.mockResolvedValue({ prisma.org.findUnique.mockResolvedValue({
@ -386,7 +386,7 @@ describe('withAuthV2', () => {
const result = await withAuthV2(cb); const result = await withAuthV2(cb);
expect(cb).toHaveBeenCalledWith({ expect(cb).toHaveBeenCalledWith({
user: { user: {
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}, },
org: MOCK_ORG, org: MOCK_ORG,
@ -398,7 +398,7 @@ describe('withAuthV2', () => {
test('should return a service error if the user is a member of the organization but does not have a valid session', async () => { test('should return a service error if the user is a member of the organization but does not have a valid session', async () => {
const userId = 'test-user-id'; const userId = 'test-user-id';
prisma.user.findUnique.mockResolvedValue({ prisma.user.findUnique.mockResolvedValue({
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}); });
prisma.org.findUnique.mockResolvedValue({ prisma.org.findUnique.mockResolvedValue({
@ -421,7 +421,7 @@ describe('withAuthV2', () => {
test('should return a service error if the user is a guest of the organization', async () => { test('should return a service error if the user is a guest of the organization', async () => {
const userId = 'test-user-id'; const userId = 'test-user-id';
prisma.user.findUnique.mockResolvedValue({ prisma.user.findUnique.mockResolvedValue({
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}); });
prisma.org.findUnique.mockResolvedValue({ prisma.org.findUnique.mockResolvedValue({
@ -445,7 +445,7 @@ describe('withAuthV2', () => {
test('should return a service error if the user is not a member of the organization (guest role)', async () => { test('should return a service error if the user is not a member of the organization (guest role)', async () => {
const userId = 'test-user-id'; const userId = 'test-user-id';
prisma.user.findUnique.mockResolvedValue({ prisma.user.findUnique.mockResolvedValue({
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}); });
prisma.org.findUnique.mockResolvedValue({ prisma.org.findUnique.mockResolvedValue({
@ -465,7 +465,7 @@ describe('withOptionalAuthV2', () => {
test('should call the callback with the auth context object if a valid session is present and the user is a member of the organization', async () => { test('should call the callback with the auth context object if a valid session is present and the user is a member of the organization', async () => {
const userId = 'test-user-id'; const userId = 'test-user-id';
prisma.user.findUnique.mockResolvedValue({ prisma.user.findUnique.mockResolvedValue({
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}); });
prisma.org.findUnique.mockResolvedValue({ prisma.org.findUnique.mockResolvedValue({
@ -483,7 +483,7 @@ describe('withOptionalAuthV2', () => {
const result = await withOptionalAuthV2(cb); const result = await withOptionalAuthV2(cb);
expect(cb).toHaveBeenCalledWith({ expect(cb).toHaveBeenCalledWith({
user: { user: {
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}, },
org: MOCK_ORG, org: MOCK_ORG,
@ -495,7 +495,7 @@ describe('withOptionalAuthV2', () => {
test('should call the callback with the auth context object if a valid session is present and the user is a member of the organization with OWNER role', async () => { test('should call the callback with the auth context object if a valid session is present and the user is a member of the organization with OWNER role', async () => {
const userId = 'test-user-id'; const userId = 'test-user-id';
prisma.user.findUnique.mockResolvedValue({ prisma.user.findUnique.mockResolvedValue({
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}); });
prisma.org.findUnique.mockResolvedValue({ prisma.org.findUnique.mockResolvedValue({
@ -513,7 +513,7 @@ describe('withOptionalAuthV2', () => {
const result = await withOptionalAuthV2(cb); const result = await withOptionalAuthV2(cb);
expect(cb).toHaveBeenCalledWith({ expect(cb).toHaveBeenCalledWith({
user: { user: {
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}, },
org: MOCK_ORG, org: MOCK_ORG,
@ -525,7 +525,7 @@ describe('withOptionalAuthV2', () => {
test('should call the callback with the auth context object if a valid session is present and the user is a member of the organization (api key)', async () => { test('should call the callback with the auth context object if a valid session is present and the user is a member of the organization (api key)', async () => {
const userId = 'test-user-id'; const userId = 'test-user-id';
prisma.user.findUnique.mockResolvedValue({ prisma.user.findUnique.mockResolvedValue({
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}); });
prisma.org.findUnique.mockResolvedValue({ prisma.org.findUnique.mockResolvedValue({
@ -548,7 +548,7 @@ describe('withOptionalAuthV2', () => {
const result = await withOptionalAuthV2(cb); const result = await withOptionalAuthV2(cb);
expect(cb).toHaveBeenCalledWith({ expect(cb).toHaveBeenCalledWith({
user: { user: {
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}, },
org: MOCK_ORG, org: MOCK_ORG,
@ -560,7 +560,7 @@ describe('withOptionalAuthV2', () => {
test('should call the callback with the auth context object if a valid session is present and the user is a member of the organization with OWNER role (api key)', async () => { test('should call the callback with the auth context object if a valid session is present and the user is a member of the organization with OWNER role (api key)', async () => {
const userId = 'test-user-id'; const userId = 'test-user-id';
prisma.user.findUnique.mockResolvedValue({ prisma.user.findUnique.mockResolvedValue({
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}); });
prisma.org.findUnique.mockResolvedValue({ prisma.org.findUnique.mockResolvedValue({
@ -583,7 +583,7 @@ describe('withOptionalAuthV2', () => {
const result = await withOptionalAuthV2(cb); const result = await withOptionalAuthV2(cb);
expect(cb).toHaveBeenCalledWith({ expect(cb).toHaveBeenCalledWith({
user: { user: {
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}, },
org: MOCK_ORG, org: MOCK_ORG,
@ -595,7 +595,7 @@ describe('withOptionalAuthV2', () => {
test('should return a service error if the user is a member of the organization but does not have a valid session', async () => { test('should return a service error if the user is a member of the organization but does not have a valid session', async () => {
const userId = 'test-user-id'; const userId = 'test-user-id';
prisma.user.findUnique.mockResolvedValue({ prisma.user.findUnique.mockResolvedValue({
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}); });
prisma.org.findUnique.mockResolvedValue({ prisma.org.findUnique.mockResolvedValue({
@ -618,7 +618,7 @@ describe('withOptionalAuthV2', () => {
test('should return a service error if the user is a guest of the organization', async () => { test('should return a service error if the user is a guest of the organization', async () => {
const userId = 'test-user-id'; const userId = 'test-user-id';
prisma.user.findUnique.mockResolvedValue({ prisma.user.findUnique.mockResolvedValue({
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}); });
prisma.org.findUnique.mockResolvedValue({ prisma.org.findUnique.mockResolvedValue({
@ -642,7 +642,7 @@ describe('withOptionalAuthV2', () => {
test('should return a service error if the user is not a member of the organization (guest role)', async () => { test('should return a service error if the user is not a member of the organization (guest role)', async () => {
const userId = 'test-user-id'; const userId = 'test-user-id';
prisma.user.findUnique.mockResolvedValue({ prisma.user.findUnique.mockResolvedValue({
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}); });
prisma.org.findUnique.mockResolvedValue({ prisma.org.findUnique.mockResolvedValue({
@ -662,7 +662,7 @@ describe('withOptionalAuthV2', () => {
const userId = 'test-user-id'; const userId = 'test-user-id';
prisma.user.findUnique.mockResolvedValue({ prisma.user.findUnique.mockResolvedValue({
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}); });
prisma.org.findUnique.mockResolvedValue({ prisma.org.findUnique.mockResolvedValue({
@ -677,7 +677,7 @@ describe('withOptionalAuthV2', () => {
const result = await withOptionalAuthV2(cb); const result = await withOptionalAuthV2(cb);
expect(cb).toHaveBeenCalledWith({ expect(cb).toHaveBeenCalledWith({
user: { user: {
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}, },
org: { org: {
@ -696,7 +696,7 @@ describe('withOptionalAuthV2', () => {
const userId = 'test-user-id'; const userId = 'test-user-id';
prisma.user.findUnique.mockResolvedValue({ prisma.user.findUnique.mockResolvedValue({
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}); });
prisma.org.findUnique.mockResolvedValue({ prisma.org.findUnique.mockResolvedValue({
@ -718,7 +718,7 @@ describe('withOptionalAuthV2', () => {
const userId = 'test-user-id'; const userId = 'test-user-id';
prisma.user.findUnique.mockResolvedValue({ prisma.user.findUnique.mockResolvedValue({
...MOCK_USER, ...MOCK_USER_WITH_ACCOUNTS,
id: userId, id: userId,
}); });
prisma.org.findUnique.mockResolvedValue({ prisma.org.findUnique.mockResolvedValue({

View file

@ -88,7 +88,8 @@ export const getAuthContext = async (): Promise<OptionalAuthContext | ServiceErr
}, },
}) : null; }) : null;
const prisma = __unsafePrisma.$extends(userScopedPrismaClientExtension(user?.id)) as PrismaClient; const accountIds = user?.accounts.map(account => account.id);
const prisma = __unsafePrisma.$extends(userScopedPrismaClientExtension(accountIds)) as PrismaClient;
return { return {
user: user ?? undefined, user: user ?? undefined,
@ -106,6 +107,9 @@ export const getAuthenticatedUser = async () => {
const user = await __unsafePrisma.user.findUnique({ const user = await __unsafePrisma.user.findUnique({
where: { where: {
id: userId, id: userId,
},
include: {
accounts: true,
} }
}); });
@ -125,6 +129,9 @@ export const getAuthenticatedUser = async () => {
where: { where: {
id: apiKey.createdById, id: apiKey.createdById,
}, },
include: {
accounts: true,
}
}); });
if (!user) { if (!user) {