feat(web): Next.js 15 app shell, auth, i18n, base UI
App Router setup with next-intl (en/fr), Better Auth wiring, shadcn/ui components, Tailwind, AES-256-GCM encrypt util, Redis rate limiter, tier limit checker, site_settings helper for runtime .env overrides.
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
import { db } from "@epicure/db";
|
||||
import { tierDefinitions, userUsage } from "@epicure/db";
|
||||
import { eq, and, sql } from "@epicure/db";
|
||||
|
||||
function currentMonth() {
|
||||
const now = new Date();
|
||||
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
export type LimitKey = "recipe" | "aiCall" | "storage";
|
||||
|
||||
export class TierLimitError extends Error {
|
||||
constructor(public readonly limit: LimitKey, public readonly tier: string) {
|
||||
super(`Tier limit reached: ${limit} (tier: ${tier})`);
|
||||
this.name = "TierLimitError";
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkTierLimit(
|
||||
userId: string,
|
||||
userTier: "free" | "pro",
|
||||
key: LimitKey
|
||||
): Promise<void> {
|
||||
const [tierDef] = await db
|
||||
.select()
|
||||
.from(tierDefinitions)
|
||||
.where(eq(tierDefinitions.tier, userTier));
|
||||
|
||||
if (!tierDef) return;
|
||||
|
||||
const month = currentMonth();
|
||||
const [usage] = await db
|
||||
.select()
|
||||
.from(userUsage)
|
||||
.where(and(eq(userUsage.userId, userId), eq(userUsage.month, month)));
|
||||
|
||||
const current = usage ?? {
|
||||
aiCallsUsed: 0,
|
||||
recipeCount: 0,
|
||||
storageUsedMb: 0,
|
||||
};
|
||||
|
||||
if (key === "recipe" && current.recipeCount >= tierDef.maxRecipes) {
|
||||
throw new TierLimitError("recipe", userTier);
|
||||
}
|
||||
if (key === "aiCall" && current.aiCallsUsed >= tierDef.aiCallsPerMonth) {
|
||||
throw new TierLimitError("aiCall", userTier);
|
||||
}
|
||||
}
|
||||
|
||||
export async function incrementUsage(
|
||||
userId: string,
|
||||
key: LimitKey,
|
||||
amount = 1
|
||||
): Promise<void> {
|
||||
const month = currentMonth();
|
||||
const id = `${userId}-${month}`;
|
||||
|
||||
const initialValues = {
|
||||
id,
|
||||
userId,
|
||||
month,
|
||||
aiCallsUsed: key === "aiCall" ? amount : 0,
|
||||
recipeCount: key === "recipe" ? amount : 0,
|
||||
storageUsedMb: key === "storage" ? amount : 0,
|
||||
};
|
||||
|
||||
const incrementSet =
|
||||
key === "aiCall"
|
||||
? { aiCallsUsed: sql`${userUsage.aiCallsUsed} + ${amount}` }
|
||||
: key === "recipe"
|
||||
? { recipeCount: sql`${userUsage.recipeCount} + ${amount}` }
|
||||
: { storageUsedMb: sql`${userUsage.storageUsedMb} + ${amount}` };
|
||||
|
||||
await db
|
||||
.insert(userUsage)
|
||||
.values(initialValues)
|
||||
.onConflictDoUpdate({
|
||||
target: [userUsage.userId, userUsage.month],
|
||||
set: incrementSet,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user