feat(api): add is_client check to middleware and billing
This commit is contained in:
parent
9e75f9b78d
commit
ccb66f99d8
4 changed files with 38 additions and 5 deletions
|
|
@ -28,12 +28,14 @@ describe("billing helpers", () => {
|
|||
id: "owner-user",
|
||||
created_at: "2026-01-01T10:00:00.000Z",
|
||||
is_temporary: false,
|
||||
is_client: false,
|
||||
plan: "annual",
|
||||
},
|
||||
{
|
||||
id: "late-user",
|
||||
created_at: "2026-01-02T10:00:00.000Z",
|
||||
is_temporary: false,
|
||||
is_client: false,
|
||||
plan: "solo",
|
||||
},
|
||||
]);
|
||||
|
|
@ -47,18 +49,21 @@ describe("billing helpers", () => {
|
|||
id: "user-1",
|
||||
created_at: "2026-01-01T10:00:00.000Z",
|
||||
is_temporary: false,
|
||||
is_client: false,
|
||||
plan: "solo",
|
||||
},
|
||||
{
|
||||
id: "temp-1",
|
||||
created_at: "2026-01-02T10:00:00.000Z",
|
||||
is_temporary: true,
|
||||
is_client: false,
|
||||
plan: "solo",
|
||||
},
|
||||
{
|
||||
id: "user-2",
|
||||
created_at: "2026-01-03T10:00:00.000Z",
|
||||
is_temporary: null,
|
||||
is_client: false,
|
||||
plan: "team",
|
||||
},
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ describe("Middleware Tests", () => {
|
|||
const middlewareManager = MiddlewareManager.getInstance();
|
||||
|
||||
const createProfilesSupabaseMock = (result: {
|
||||
data: { is_temporary: boolean } | null;
|
||||
data: { is_temporary?: boolean; is_client?: boolean } | null;
|
||||
error: { message: string } | null;
|
||||
}) => ({
|
||||
from: vi.fn().mockReturnValue({
|
||||
|
|
@ -342,6 +342,33 @@ describe("Middleware Tests", () => {
|
|||
expect(res.status).toBe(401);
|
||||
expect(data.error).toBe("User is read only");
|
||||
});
|
||||
|
||||
it("should return 401 for client users", async () => {
|
||||
const app = new Hono();
|
||||
app.use(async (c, next) => {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Test-only context injection
|
||||
(c as any).set(
|
||||
"supabase",
|
||||
createProfilesSupabaseMock({
|
||||
data: { is_temporary: false, is_client: true },
|
||||
error: null,
|
||||
}) as any
|
||||
);
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Test-only context injection
|
||||
(c as any).set("user", { id: "client-user" } as any);
|
||||
await next();
|
||||
});
|
||||
app.use(middlewareManager.regularUserCheck);
|
||||
app.get("/test", (c) => c.json({ success: true }));
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: testClient requires any for dynamic route access
|
||||
const client = testClient(app) as any;
|
||||
const res = await client.test.$get();
|
||||
const data = await res.json();
|
||||
|
||||
expect(res.status).toBe(401);
|
||||
expect(data.error).toBe("User is read only");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Billing Checkout Access Middleware", () => {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ type BillingProfileRow = {
|
|||
id: string;
|
||||
created_at: string | null;
|
||||
is_temporary: boolean | null;
|
||||
is_client: boolean | null;
|
||||
plan: string | null;
|
||||
};
|
||||
|
||||
|
|
@ -87,7 +88,7 @@ export const parseTrialRolloutDate = (
|
|||
export const getOrganizationOwner = (profiles: BillingProfileRow[]) => profiles[0] ?? null;
|
||||
|
||||
export const getBillableMemberCount = (profiles: BillingProfileRow[]) =>
|
||||
profiles.filter((profile) => profile.is_temporary !== true).length;
|
||||
profiles.filter((profile) => profile.is_temporary !== true && profile.is_client !== true).length;
|
||||
|
||||
export const getTrialWindow = (input: {
|
||||
ownerCreatedAt: Date;
|
||||
|
|
@ -179,7 +180,7 @@ const getPlanHint = (price: StripePriceRow | undefined, product: StripeProductRo
|
|||
const getOrganizationProfiles = async (supabase: SupabaseClient, organizationId: number) => {
|
||||
const { data, error } = await supabase
|
||||
.from("profiles")
|
||||
.select("id, created_at, is_temporary, plan")
|
||||
.select("id, created_at, is_temporary, is_client, plan")
|
||||
.eq("organization_id", organizationId)
|
||||
.order("created_at", { ascending: true });
|
||||
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ export class MiddlewareManager {
|
|||
|
||||
const { data: profile, error } = await supabase
|
||||
.from("profiles")
|
||||
.select("is_temporary")
|
||||
.select("is_temporary, is_client")
|
||||
.eq("id", user.id)
|
||||
.single();
|
||||
|
||||
|
|
@ -92,7 +92,7 @@ export class MiddlewareManager {
|
|||
return c.json({ error: error?.message ?? "Profile not found" }, 500);
|
||||
}
|
||||
|
||||
if (!allowTemporaryUsers && profile.is_temporary) {
|
||||
if ((!allowTemporaryUsers && profile.is_temporary) || profile.is_client) {
|
||||
return c.json({ error: "User is read only" }, 401);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue