Skip to content
Open
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
65 changes: 65 additions & 0 deletions apps/studio.giselles.ai/lib/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { isInternalUserEmail } from "./utils";

describe("isInternalUserEmail", () => {
const originalEnv = process.env.INTERNAL_USER_EMAIL_DOMAIN;

afterEach(() => {
if (originalEnv === undefined) {
delete process.env.INTERNAL_USER_EMAIL_DOMAIN;
} else {
process.env.INTERNAL_USER_EMAIL_DOMAIN = originalEnv;
}
});

describe("when INTERNAL_USER_EMAIL_DOMAIN is set", () => {
beforeEach(() => {
process.env.INTERNAL_USER_EMAIL_DOMAIN = "example.com";
});

test("returns true for email matching the domain exactly", () => {
expect(isInternalUserEmail("[email protected]")).toBe(true);
});

test("returns false for email with subdomain", () => {
expect(isInternalUserEmail("[email protected]")).toBe(false);
});

test("returns false for email with different domain", () => {
expect(isInternalUserEmail("[email protected]")).toBe(false);
});

test("returns false for email with domain that contains configured domain as prefix", () => {
expect(isInternalUserEmail("[email protected]")).toBe(false);
});

test("returns false for email without @ symbol", () => {
expect(isInternalUserEmail("invalid-email")).toBe(false);
});

test("returns false for empty string", () => {
expect(isInternalUserEmail("")).toBe(false);
});
});

describe("when INTERNAL_USER_EMAIL_DOMAIN is not set", () => {
beforeEach(() => {
delete process.env.INTERNAL_USER_EMAIL_DOMAIN;
});

test("returns false for any email", () => {
expect(isInternalUserEmail("[email protected]")).toBe(false);
expect(isInternalUserEmail("[email protected]")).toBe(false);
});
});

describe("when INTERNAL_USER_EMAIL_DOMAIN is empty string", () => {
beforeEach(() => {
process.env.INTERNAL_USER_EMAIL_DOMAIN = "";
});

test("returns false for any email", () => {
expect(isInternalUserEmail("[email protected]")).toBe(false);
});
});
});
15 changes: 13 additions & 2 deletions apps/studio.giselles.ai/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,20 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

export const isEmailFromRoute06 = (email: string): boolean => {
/**
* Checks if the email belongs to an internal user based on the configured domain.
* Internal users are treated as having team.type === "internal".
*
* @param email - The email address to check
* @returns true if the email domain exactly matches INTERNAL_USER_EMAIL_DOMAIN env var
*/
export const isInternalUserEmail = (email: string): boolean => {
const internalDomain = process.env.INTERNAL_USER_EMAIL_DOMAIN;
if (!internalDomain) {
return false;
}
const domain = email.split("@")[1];
return domain ? domain.endsWith("route06.co.jp") : false;
return domain === internalDomain;
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
users,
workspaces,
} from "@/db";
import { isEmailFromRoute06 } from "@/lib/utils";
import { isInternalUserEmail } from "@/lib/utils";
import { createTeamId } from "@/services/teams/utils";

export const initializeAccount = async (
Expand All @@ -36,7 +36,7 @@ export const initializeAccount = async (
userDbId: user.dbId,
supabaseUserId,
});
const internalAccount = isEmailFromRoute06(supabaseUserEmail ?? "");
const internalAccount = isInternalUserEmail(supabaseUserEmail ?? "");
const [team] = await tx
.insert(teams)
.values({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { db, supabaseUserMappings, teamMemberships, teams, users } from "@/db";
import { stripeV2Flag } from "@/flags";
import { updateGiselleSession } from "@/lib/giselle-session";
import { getUser } from "@/lib/supabase";
import { isEmailFromRoute06 } from "@/lib/utils";
import { isInternalUserEmail } from "@/lib/utils";
import {
DRAFT_TEAM_NAME_METADATA_KEY,
DRAFT_TEAM_USER_DB_ID_METADATA_KEY,
Expand All @@ -30,7 +30,7 @@ export async function createTeam(formData: FormData) {
}

const isInternalUser =
supabaseUser.email != null && isEmailFromRoute06(supabaseUser.email);
supabaseUser.email != null && isInternalUserEmail(supabaseUser.email);
if (isInternalUser) {
const teamId = await createInternalTeam(supabaseUser, teamName);
await setCurrentTeam(teamId);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import invariant from "tiny-invariant";
import { getUser } from "@/lib/supabase";
import { isEmailFromRoute06 } from "@/lib/utils";
import { formatStripePrice, getCachedPrice } from "@/services/external/stripe";
import { fetchUserTeams } from "../fetch-user-teams";
import { canCreateFreeTeam } from "../plan-features/free-team-creation";
import { TeamCreationForm } from "./team-creation-form";

export default async function TeamCreation({
Expand All @@ -14,17 +14,18 @@ export default async function TeamCreation({
if (!user) {
throw new Error("User not found");
}
const isInternalUser = user.email != null && isEmailFromRoute06(user.email);
const teams = await fetchUserTeams();
const hasExistingFreeTeam = teams.some((team) => team.plan === "free");
const proPlanPriceId = process.env.STRIPE_PRO_PLAN_PRICE_ID;
invariant(proPlanPriceId, "STRIPE_PRO_PLAN_PRICE_ID is not set");
const proPlan = await getCachedPrice(proPlanPriceId);
const proPlanPrice = formatStripePrice(proPlan);

return (
<TeamCreationForm
canCreateFreeTeam={!isInternalUser && !hasExistingFreeTeam}
canCreateFreeTeam={canCreateFreeTeam(
user.email,
teams.map((t) => t.plan),
)}
proPlanPrice={proPlanPrice}
>
{children}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
import { describe, expect, test } from "vitest";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import type { TeamPlan } from "@/db/schema";
import { canCreateFreeTeam } from "./free-team-creation";

describe("canCreateFreeTeam", () => {
const originalEnv = process.env.INTERNAL_USER_EMAIL_DOMAIN;

beforeEach(() => {
process.env.INTERNAL_USER_EMAIL_DOMAIN = "internal.example.com";
});

afterEach(() => {
if (originalEnv === undefined) {
delete process.env.INTERNAL_USER_EMAIL_DOMAIN;
} else {
process.env.INTERNAL_USER_EMAIL_DOMAIN = originalEnv;
}
});

test("returns true for non-internal user without existing free team", () => {
expect(canCreateFreeTeam("[email protected]", ["pro", "team"])).toBe(true);
});

test("returns false for internal user (route06.co.jp)", () => {
expect(canCreateFreeTeam("user@route06.co.jp", [])).toBe(false);
test("returns false for internal user", () => {
expect(canCreateFreeTeam("user@internal.example.com", [])).toBe(false);
});

test("returns false for user with existing free team", () => {
Expand All @@ -32,4 +46,9 @@ describe("canCreateFreeTeam", () => {
test("returns true for user with no teams", () => {
expect(canCreateFreeTeam("[email protected]", [])).toBe(true);
});

test("returns true for any user when INTERNAL_USER_EMAIL_DOMAIN is not set", () => {
delete process.env.INTERNAL_USER_EMAIL_DOMAIN;
expect(canCreateFreeTeam("[email protected]", [])).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import type { TeamPlan } from "@/db/schema";
import { isEmailFromRoute06 } from "@/lib/utils";
import { isInternalUserEmail } from "@/lib/utils";

/**
* Determines if a user can create a free team.
* - Internal users (route06.co.jp) cannot create free teams
* - Internal users cannot create free teams
* - Users who already have a free team cannot create another
*/
export function canCreateFreeTeam(
email: string | null | undefined,
existingTeamPlans: TeamPlan[],
): boolean {
const isInternalUser = email != null && isEmailFromRoute06(email);
const isInternalUser = email != null && isInternalUserEmail(email);
if (isInternalUser) {
return false;
}
Expand Down
Loading