sourcebot/packages/web/src/lib/authUtils.ts

192 lines
7 KiB
TypeScript
Raw Normal View History

import type { User as AuthJsUser } from "next-auth";
import { env } from "@/env.mjs";
import { prisma } from "@/prisma";
import { OrgRole } from "@sourcebot/db";
import { SINGLE_TENANT_ORG_DOMAIN, SINGLE_TENANT_ORG_ID } from "@/lib/constants";
fix(search-contexts): Fix issue where a repository would not appear in a search context if it was created after the search context was created (#354) ## Problem If a repository is added **after** a search context (e.g., a new repository is synced from the code host), then it will never be added to the context even if it should be included. The workaround is to restart the instance. ## Solution This PR adds a call to re-sync all search contexts whenever a connection is successfully synced. This PR adds the `@sourcebot/shared` package that contains `syncSearchContexts.ts` (previously in web) and it's dependencies (namely the entitlements system). ## Why another package? Because the `syncSearchContexts` call is now called from: 1. `initialize.ts` in **web** - handles syncing search contexts on startup and whenever the config is modified in watch mode. This is the same as before. 2. `connectionManager.ts` in **backend** - syncs the search contexts whenever a connection is successfully synced. ## Follow-up devex work Two things: 1. We have several very thin shared packages (i.e., `crypto`, `error`, and `logger`) that we can probably fold into this "general" shared package. `schemas` and `db` _feels_ like they should remain separate (mostly because they are "code-gen" packages). 2. When running `yarn dev`, any changes made to the shared package will only get picked if you `ctrl+c` and restart the instance. Would be nice if we have watch mode work across package dependencies in the monorepo.
2025-06-17 21:04:25 +00:00
import { hasEntitlement } from "@sourcebot/shared";
import { isServiceError } from "@/lib/utils";
import { ServiceErrorException } from "@/lib/serviceError";
import { createAccountRequest } from "@/actions";
import { handleJITProvisioning } from "@/ee/features/sso/sso";
import { createLogger } from "@sourcebot/logger";
import { getAuditService } from "@/ee/features/audit/factory";
const logger = createLogger('web-auth-utils');
const auditService = getAuditService();
export const onCreateUser = async ({ user }: { user: AuthJsUser }) => {
if (!user.id) {
logger.error("User ID is undefined on user creation");
await auditService.createAudit({
action: "user.creation_failed",
actor: {
id: "undefined",
type: "user"
},
target: {
id: "undefined",
type: "user"
},
orgId: SINGLE_TENANT_ORG_ID, // TODO(mt)
metadata: {
message: "User ID is undefined on user creation"
}
});
throw new Error("User ID is undefined on user creation");
}
// In single-tenant mode, we assign the first user to sign
// up as the owner of the default org.
if (env.SOURCEBOT_TENANCY_MODE === 'single') {
const defaultOrg = await prisma.org.findUnique({
where: {
id: SINGLE_TENANT_ORG_ID,
},
include: {
members: {
where: {
role: {
not: OrgRole.GUEST,
}
}
},
}
});
if (defaultOrg === null) {
await auditService.createAudit({
action: "user.creation_failed",
actor: {
id: user.id,
type: "user"
},
target: {
id: user.id,
type: "user"
},
orgId: SINGLE_TENANT_ORG_ID,
metadata: {
message: "Default org not found on single tenant user creation"
}
});
throw new Error("Default org not found on single tenant user creation");
}
// Only the first user to sign up will be an owner of the default org.
const isFirstUser = defaultOrg.members.length === 0;
if (isFirstUser) {
await prisma.$transaction(async (tx) => {
await tx.org.update({
where: {
id: SINGLE_TENANT_ORG_ID,
},
data: {
members: {
create: {
role: OrgRole.OWNER,
user: {
connect: {
id: user.id,
}
}
}
}
}
});
await tx.user.update({
where: {
id: user.id,
},
data: {
pendingApproval: false,
}
});
});
await auditService.createAudit({
action: "user.owner_created",
actor: {
id: user.id,
type: "user"
},
orgId: SINGLE_TENANT_ORG_ID,
target: {
id: SINGLE_TENANT_ORG_ID.toString(),
type: "org"
}
});
} else {
// TODO(auth): handle multi tenant case
if (env.AUTH_EE_ENABLE_JIT_PROVISIONING === 'true' && hasEntitlement("sso")) {
const res = await handleJITProvisioning(user.id, SINGLE_TENANT_ORG_DOMAIN);
if (isServiceError(res)) {
logger.error(`Failed to provision user ${user.id} for org ${SINGLE_TENANT_ORG_DOMAIN}: ${res.message}`);
await auditService.createAudit({
action: "user.jit_provisioning_failed",
actor: {
id: user.id,
type: "user"
},
target: {
id: SINGLE_TENANT_ORG_ID.toString(),
type: "org"
},
orgId: SINGLE_TENANT_ORG_ID,
metadata: {
message: `Failed to provision user ${user.id} for org ${SINGLE_TENANT_ORG_DOMAIN}: ${res.message}`
}
});
throw new ServiceErrorException(res);
}
await auditService.createAudit({
action: "user.jit_provisioned",
actor: {
id: user.id,
type: "user"
},
target: {
id: SINGLE_TENANT_ORG_ID.toString(),
type: "org"
},
orgId: SINGLE_TENANT_ORG_ID,
});
} else {
const res = await createAccountRequest(user.id, SINGLE_TENANT_ORG_DOMAIN);
if (isServiceError(res)) {
logger.error(`Failed to provision user ${user.id} for org ${SINGLE_TENANT_ORG_DOMAIN}: ${res.message}`);
await auditService.createAudit({
action: "user.join_request_creation_failed",
actor: {
id: user.id,
type: "user"
},
target: {
id: SINGLE_TENANT_ORG_ID.toString(),
type: "org"
},
orgId: SINGLE_TENANT_ORG_ID,
metadata: {
message: res.message
}
});
throw new ServiceErrorException(res);
}
await auditService.createAudit({
action: "user.join_requested",
actor: {
id: user.id,
type: "user"
},
orgId: SINGLE_TENANT_ORG_ID,
target: {
id: SINGLE_TENANT_ORG_ID.toString(),
type: "org"
},
});
}
}
}
};