Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
62 changes: 52 additions & 10 deletions packages/auth/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
organization,
} from "better-auth/plugins";
import { jwt } from "better-auth/plugins/jwt";
import { and, count, desc, eq } from "drizzle-orm";
import { and, count, desc, eq, sql } from "drizzle-orm";
import type Stripe from "stripe";
import { env } from "./env";
import { acceptInvitationEndpoint } from "./lib/accept-invitation-endpoint";
Expand Down Expand Up @@ -100,18 +100,60 @@ export const auth = betterAuth({
user: {
create: {
after: async (user) => {
const org = await auth.api.createOrganization({
body: {
name: `${user.name}'s Team`,
slug: `${user.id.slice(0, 8)}-team`,
userId: user.id,
},
});
const domain = user.email.split("@")[1]?.toLowerCase();
let enrolledOrgId: string | null = null;

if (domain) {
const matchingOrgs = await db.query.organizations.findMany({
where: sql`${authSchema.organizations.allowedDomains} @> ARRAY[${domain}]::text[]`,
});

for (const org of matchingOrgs) {
try {
await auth.api.addMember({
body: {
organizationId: org.id,
userId: user.id,
role: "member",
},
});
if (!enrolledOrgId) {
enrolledOrgId = org.id;
}
} catch (error) {
console.error(
`[auto-enroll] Failed to add user ${user.id} to org ${org.id}:`,
error,
);
// addMember may have created the DB record before a downstream error (e.g. Stripe) — check
const memberExists = await db.query.members.findFirst({
where: and(
eq(authSchema.members.organizationId, org.id),
eq(authSchema.members.userId, user.id),
),
});
if (memberExists && !enrolledOrgId) {
enrolledOrgId = org.id;
}
}
}
}

if (!enrolledOrgId) {
const personalOrg = await auth.api.createOrganization({
body: {
name: `${user.name}'s Team`,
slug: `${user.id.slice(0, 8)}-team`,
userId: user.id,
},
});
enrolledOrgId = personalOrg?.id ?? null;
}

if (org?.id) {
if (enrolledOrgId) {
await db
.update(authSchema.sessions)
.set({ activeOrganizationId: org.id })
.set({ activeOrganizationId: enrolledOrgId })
.where(eq(authSchema.sessions.userId, user.id));
Comment on lines +103 to 157
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Managed-domain users can still get a personal org on pre-insert enrollment failures.

At Line 142, fallback personal-org creation runs whenever no membership was confirmed. That includes failures that happen before member creation (e.g., business-rule rejections), which weakens the managed-domain restriction.

Targeted fix
+ let hadManagedDomainMatch = false;

if (domain) {
  const matchingOrgs = await db.query.organizations.findMany({
    where: sql`${authSchema.organizations.allowedDomains} @> ARRAY[${domain}]::text[]`,
  });
+ hadManagedDomainMatch = matchingOrgs.length > 0;

  // ... enrollment loop ...
}

if (!enrolledOrgId) {
+ if (hadManagedDomainMatch) {
+   throw new Error(
+     "Your account is managed by your organization. Contact your admin."
+   );
+ }
  const personalOrg = await auth.api.createOrganization({
    body: {
      name: `${user.name}'s Team`,
      slug: `${user.id.slice(0, 8)}-team`,
      userId: user.id,
    },
  });
  enrolledOrgId = personalOrg?.id ?? null;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/auth/src/server.ts` around lines 103 - 157, The code currently
creates a personal organization whenever no membership was confirmed, which lets
managed-domain users get a personal org if addMember fails pre-insert; fix by
only creating the personal org when there were no matching managed-domain
organizations to begin with. Change the logic around matchingOrgs/enrolledOrgId:
after fetching matchingOrgs (variable matchingOrgs), set a flag (e.g.,
foundManagedDomains = matchingOrgs.length > 0) and only call
auth.api.createOrganization when !foundManagedDomains (i.e., no allowed domains
matched). Keep the existing memberExists fallback for recovering enrollments but
do not treat a failed enrollment attempt (matchingOrgs present but no confirmed
member) as permission to create a personal org.

}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE "auth"."organizations" ADD COLUMN "allowed_domains" text[] DEFAULT '{}' NOT NULL;--> statement-breakpoint
CREATE INDEX "organizations_allowed_domains_idx" ON "auth"."organizations" USING gin ("allowed_domains");
Loading