Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
wip on single tenancy mode
  • Loading branch information
brendan-kellam committed Mar 20, 2025
commit a37925906ff37c3b960842d0d2ccd59b0f2239c3
3 changes: 2 additions & 1 deletion .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,5 @@ SOURCEBOT_TELEMETRY_DISABLED=true # Disables telemetry collection

# CONFIG_MAX_REPOS_NO_TOKEN=
# SOURCEBOT_ROOT_DOMAIN=
# NODE_ENV=
# NODE_ENV=
# SOURCEBOT_TENANCY_MODE=mutli
70 changes: 53 additions & 17 deletions packages/web/src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { decrypt, encrypt } from "@sourcebot/crypto"
import { getConnection } from "./data/connection";
import { ConnectionSyncStatus, Prisma, OrgRole, RepoIndexingStatus, StripeSubscriptionStatus } from "@sourcebot/db";
import { cookies, headers } from "next/headers"
import { getUser } from "@/data/user";
import { Session } from "next-auth";
import { env } from "@/env.mjs";
import Stripe from "stripe";
Expand All @@ -25,7 +24,7 @@ import InviteUserEmail from "./emails/inviteUserEmail";
import { createTransport } from "nodemailer";
import { orgDomainSchema, orgNameSchema, repositoryQuerySchema } from "./lib/schemas";
import { RepositoryQuery } from "./lib/types";
import { MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME } from "./lib/constants";
import { MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, SINGLE_TENANT_USER_EMAIL, SINGLE_TENANT_USER_ID } from "./lib/constants";
import { stripeClient } from "./lib/stripe";
import { IS_BILLING_ENABLED } from "./lib/stripe";

Expand All @@ -34,6 +33,16 @@ const ajv = new Ajv({
});

export const withAuth = async <T>(fn: (session: Session) => Promise<T>) => {
if (env.SOURCEBOT_TENANCY_MODE === 'single') {
return fn({
user: {
id: SINGLE_TENANT_USER_ID,
email: SINGLE_TENANT_USER_EMAIL,
},
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30).toISOString(),
});
}

const session = await auth();
if (!session) {
return notAuthenticated();
Expand Down Expand Up @@ -89,11 +98,6 @@ export const withOrgMembership = async <T>(session: Session, domain: string, fn:
});
}

export const isAuthed = async () => {
const session = await auth();
return session != null;
}

export const createOrg = (name: string, domain: string): Promise<{ id: number } | ServiceError> =>
withAuth(async (session) => {
const org = await prisma.org.create({
Expand Down Expand Up @@ -695,6 +699,38 @@ export const cancelInvite = async (inviteId: string, domain: string): Promise<{
}, /* minRequiredRole = */ OrgRole.OWNER)
);

export const getMe = async () =>
withAuth(async (session) => {
const user = await prisma.user.findUnique({
where: {
id: session.user.id,
},
include: {
orgs: {
include: {
org: true,
}
},
}
});

if (!user) {
return notFound();
}

return {
id: user.id,
email: user.email,
name: user.name,
memberships: user.orgs.map((org) => ({
id: org.orgId,
role: org.role,
domain: org.org.domain,
name: org.org.name,
}))
}
});

export const redeemInvite = async (inviteId: string): Promise<{ success: boolean } | ServiceError> =>
withAuth(async (session) => {
const invite = await prisma.invite.findUnique({
Expand All @@ -710,9 +746,9 @@ export const redeemInvite = async (inviteId: string): Promise<{ success: boolean
return notFound();
}

const user = await getUser(session.user.id);
if (!user) {
return notFound();
const user = await getMe();
if (isServiceError(user)) {
return user;
}

// Check if the user is the recipient of the invite
Expand Down Expand Up @@ -765,10 +801,10 @@ export const redeemInvite = async (inviteId: string): Promise<{ success: boolean
});

export const getInviteInfo = async (inviteId: string) =>
withAuth(async (session) => {
const user = await getUser(session.user.id);
if (!user) {
return notFound();
withAuth(async () => {
const user = await getMe();
if (isServiceError(user)) {
return user;
}

const invite = await prisma.invite.findUnique({
Expand Down Expand Up @@ -880,9 +916,9 @@ export const createOnboardingSubscription = async (domain: string) =>
return notFound();
}

const user = await getUser(session.user.id);
if (!user) {
return notFound();
const user = await getMe();
if (isServiceError(user)) {
return user;
}

if (!stripeClient) {
Expand Down
17 changes: 8 additions & 9 deletions packages/web/src/app/[domain]/components/orgSelector/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { auth } from "@/auth";
import { getUserOrgs } from "../../../../data/user";
import { OrgSelectorDropdown } from "./orgSelectorDropdown";
import { prisma } from "@/prisma";
import { getMe } from "@/actions";
import { isServiceError } from "@/lib/utils";

interface OrgSelectorProps {
domain: string;
Expand All @@ -10,12 +10,11 @@ interface OrgSelectorProps {
export const OrgSelector = async ({
domain,
}: OrgSelectorProps) => {
const session = await auth();
if (!session) {
const user = await getMe();
if (isServiceError(user)) {
return null;
}

const orgs = await getUserOrgs(session.user.id);
const activeOrg = await prisma.org.findUnique({
where: {
domain,
Expand All @@ -28,10 +27,10 @@ export const OrgSelector = async ({

return (
<OrgSelectorDropdown
orgs={orgs.map((org) => ({
name: org.name,
id: org.id,
domain: org.domain,
orgs={user.memberships.map(({ name, domain, id }) => ({
name,
domain,
id,
}))}
activeOrgId={activeOrg.id}
/>
Expand Down
7 changes: 0 additions & 7 deletions packages/web/src/app/[domain]/connections/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { ConfigSetting } from "./components/configSetting"
import { DeleteConnectionSetting } from "./components/deleteConnectionSetting"
import { DisplayNameSetting } from "./components/displayNameSetting"
import { RepoList } from "./components/repoList"
import { auth } from "@/auth"
import { getConnectionByDomain } from "@/data/connection"
import { Overview } from "./components/overview"

Expand All @@ -30,19 +29,13 @@ interface ConnectionManagementPageProps {
}

export default async function ConnectionManagementPage({ params, searchParams }: ConnectionManagementPageProps) {
const session = await auth();
if (!session) {
return null;
}

const connection = await getConnectionByDomain(Number(params.id), params.domain);
if (!connection) {
return <NotFound className="flex w-full h-full items-center justify-center" message="Connection not found" />
}

const currentTab = searchParams.tab || "overview";


return (
<Tabs value={currentTab} className="w-full">
<Header className="mb-6" withTopMargin={false}>
Expand Down
35 changes: 19 additions & 16 deletions packages/web/src/app/[domain]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME } from "@/lib/co
import { SyntaxReferenceGuide } from "./components/syntaxReferenceGuide";
import { SyntaxGuideProvider } from "./components/syntaxGuideProvider";
import { IS_BILLING_ENABLED } from "@/lib/stripe";
import { env } from "@/env.mjs";

interface LayoutProps {
children: React.ReactNode,
Expand All @@ -29,24 +30,26 @@ export default async function Layout({
return <PageNotFound />
}

if (env.SOURCEBOT_TENANCY_MODE === 'multi') {
const session = await auth();
if (!session) {
return <PageNotFound />
}

const session = await auth();
if (!session) {
return <PageNotFound />
}


const membership = await prisma.userToOrg.findUnique({
where: {
orgId_userId: {
orgId: org.id,
userId: session.user.id
const membership = await prisma.userToOrg.findUnique({
where: {
orgId_userId: {
orgId: org.id,
userId: session.user.id
}
}
}
});
});

if (!membership) {
return <PageNotFound />
if (!membership) {
return <PageNotFound />
}
} else {
// no-op
}

if (!org.isOnboarded) {
Expand All @@ -57,7 +60,7 @@ export default async function Layout({
)
}

if (IS_BILLING_ENABLED) {
if (IS_BILLING_ENABLED && env.SOURCEBOT_TENANCY_MODE === 'multi') {
const subscription = await fetchSubscription(domain);
if (
subscription &&
Expand Down
6 changes: 0 additions & 6 deletions packages/web/src/app/[domain]/settings/(general)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { auth } from "@/auth";
import { ChangeOrgNameCard } from "./components/changeOrgNameCard";
import { isServiceError } from "@/lib/utils";
import { getCurrentUserRole } from "@/actions";
Expand All @@ -13,11 +12,6 @@ interface GeneralSettingsPageProps {
}

export default async function GeneralSettingsPage({ params: { domain } }: GeneralSettingsPageProps) {
const session = await auth();
if (!session) {
return null;
}

const currentUserRole = await getCurrentUserRole(domain)
if (isServiceError(currentUserRole)) {
return <div>Failed to fetch user role. Please contact us at team@sourcebot.dev if this issue persists.</div>
Expand Down
17 changes: 5 additions & 12 deletions packages/web/src/app/[domain]/settings/members/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { MembersList } from "./components/membersList";
import { getOrgMembers } from "@/actions";
import { isServiceError } from "@/lib/utils";
import { auth } from "@/auth";
import { getUser, getUserRoleInOrg } from "@/data/user";
import { getOrgFromDomain } from "@/data/org";
import { InviteMemberCard } from "./components/inviteMemberCard";
import { Tabs, TabsContent } from "@/components/ui/tabs";
import { TabSwitcher } from "@/components/ui/tab-switcher";
import { InvitesList } from "./components/invitesList";
import { getOrgInvites } from "@/actions";
import { getOrgInvites, getMe } from "@/actions";
import { IS_BILLING_ENABLED } from "@/lib/stripe";
interface MembersSettingsPageProps {
params: {
Expand All @@ -20,23 +18,18 @@ interface MembersSettingsPageProps {
}

export default async function MembersSettingsPage({ params: { domain }, searchParams: { tab } }: MembersSettingsPageProps) {
const session = await auth();
if (!session) {
return null;
}

const members = await getOrgMembers(domain);
const org = await getOrgFromDomain(domain);
if (!org) {
return null;
}

const user = await getUser(session.user.id);
if (!user) {
const me = await getMe();
if (isServiceError(me)) {
return null;
}

const userRoleInOrg = await getUserRoleInOrg(user.id, org.id);
const userRoleInOrg = me.memberships.find((membership) => membership.id === org.id)?.role;
if (!userRoleInOrg) {
return null;
}
Expand Down Expand Up @@ -78,7 +71,7 @@ export default async function MembersSettingsPage({ params: { domain }, searchPa
<TabsContent value="members">
<MembersList
members={members}
currentUserId={session.user.id}
currentUserId={me.id}
currentUserRole={userRoleInOrg}
orgName={org.name}
/>
Expand Down
39 changes: 0 additions & 39 deletions packages/web/src/data/user.ts

This file was deleted.

2 changes: 2 additions & 0 deletions packages/web/src/env.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export const env = createEnv({
NODE_ENV: z.enum(["development", "test", "production"]),
SOURCEBOT_TELEMETRY_DISABLED: booleanSchema.default('false'),
DATABASE_URL: z.string().url(),

SOURCEBOT_TENANCY_MODE: z.enum(["multi", "single"]).default("multi"),
},
// @NOTE: Make sure you destructure all client variables in the
// `experimental__runtimeEnv` block below.
Expand Down
Loading