Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ CAP_AWS_ENDPOINT=${NEXT_PUBLIC_CAP_AWS_ENDPOINT}
# Necessary for authentication, genearte by running `openssl rand -base64 32`
NEXTAUTH_SECRET=

# Restrict signup to specific email domains (comma-separated)
# If empty or not set, signup is open to all email addresses
# Example: CAP_ALLOWED_SIGNUP_DOMAINS=company.com,partner.org
# CAP_ALLOWED_SIGNUP_DOMAINS=

# Provide if you want to use Google authentication
# GOOGLE_CLIENT_ID=
# GOOGLE_CLIENT_SECRET=
Expand Down
21 changes: 21 additions & 0 deletions packages/database/auth/auth-options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { users, organizations, organizationMembers } from "../schema";
import { nanoId } from "../helpers";
import { sendEmail } from "../emails/config";
import { LoginLink } from "../emails/login-link";
import { isEmailAllowedForSignup } from "./domain-utils";

export const config = {
maxDuration: 120,
Expand Down Expand Up @@ -169,6 +170,26 @@ export const authOptions = (): NextAuthOptions => {
},
},
callbacks: {
async signIn({ user, account, profile }) {
// Only apply domain restrictions for new users, existing ones can always sign in
if (user.email) {
const [existingUser] = await db()
.select()
.from(users)
.where(eq(users.email, user.email))
.limit(1);

if (!existingUser) {
const allowedDomains = serverEnv().CAP_ALLOWED_SIGNUP_DOMAINS;
if (!isEmailAllowedForSignup(user.email, allowedDomains)) {
console.log(`Signup blocked for email domain: ${user.email}`);
return false;
}
}
}

return true;
},
async session({ token, session }) {
if (!session.user) return session;

Expand Down
41 changes: 41 additions & 0 deletions packages/database/auth/domain-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { z } from "zod";

export function isEmailAllowedForSignup(
email: string,
allowedDomainsConfig?: string
): boolean {
// If no domain restrictions are configured, allow all signups
if (!allowedDomainsConfig || allowedDomainsConfig.trim() === "") {
return true;
}

const emailDomain = extractDomainFromEmail(email);
if (!emailDomain) {
return false;
}

const allowedDomains = parseAllowedDomains(allowedDomainsConfig);
return allowedDomains.includes(emailDomain.toLowerCase());
}

function extractDomainFromEmail(email: string): string | null {
const emailValidation = z.email().safeParse(email);
if (!emailValidation.success) {
return null;
}

// Extract domain from validated email
const atIndex = email.lastIndexOf("@");
return atIndex !== -1 ? email.substring(atIndex + 1) : null;
}

function parseAllowedDomains(allowedDomainsConfig: string): string[] {
return allowedDomainsConfig
.split(",")
.map(domain => domain.trim().toLowerCase())
.filter(domain => domain.length > 0 && isValidDomain(domain));
}

function isValidDomain(domain: string): boolean {
return z.hostname().safeParse(domain).success;
}
3 changes: 2 additions & 1 deletion packages/database/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"next": "14.2.9",
"next-auth": "^4.24.5",
"react-email": "^4.0.16",
"resend": "4.6.0"
"resend": "4.6.0",
"zod": "^4.0.15"
},
"devDependencies": {
"@cap/ui": "workspace:*",
Expand Down
1 change: 1 addition & 0 deletions packages/env/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ function createServerEnv() {
GROQ_API_KEY: z.string().optional(),
INTERCOM_SECRET: z.string().optional(),
CAP_VIDEOS_DEFAULT_PUBLIC: boolString(true),
CAP_ALLOWED_SIGNUP_DOMAINS: z.string().optional(),
VERCEL_ENV: z
.union([
z.literal("production"),
Expand Down
66 changes: 39 additions & 27 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading