fix(quotas): unifier le décompte IA (BYOK, rollback) et combler les fuites
Centralise la réserve via ai-quota, corrige admin unavailable (-1), brancher les routes sans quota et le host-pays brainstorm, avec usage-meter élargi, noms de clusters, MCP et ajustements dashboard/insights. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import { VALID_FEATURES, getCurrentPeriodKey } from '@/lib/quota-utils'
|
||||
import {
|
||||
getAllEntitlementsForAdmin,
|
||||
invalidateEntitlementCache,
|
||||
ENTITLEMENT_UNAVAILABLE,
|
||||
type SubscriptionTier as TierType,
|
||||
} from '@/lib/plan-entitlements'
|
||||
import { logAuditEventAsync } from '@/lib/audit-log'
|
||||
@@ -73,9 +74,20 @@ export async function updatePlanEntitlement(
|
||||
}
|
||||
|
||||
if (mode === 'unavailable') {
|
||||
await prisma.planEntitlement.deleteMany({
|
||||
where: { tier: tier as SubscriptionTier, feature },
|
||||
})
|
||||
await prisma.planEntitlement.upsert({
|
||||
where: {
|
||||
tier_feature: {
|
||||
tier: tier as SubscriptionTier,
|
||||
feature,
|
||||
},
|
||||
},
|
||||
update: { limitValue: ENTITLEMENT_UNAVAILABLE },
|
||||
create: {
|
||||
tier: tier as SubscriptionTier,
|
||||
feature,
|
||||
limitValue: ENTITLEMENT_UNAVAILABLE,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await prisma.planEntitlement.upsert({
|
||||
where: {
|
||||
@@ -158,7 +170,7 @@ export async function updateBillingConfig(data: Record<string, string>) {
|
||||
|
||||
async function getUsageOverviewInternal() {
|
||||
const period = getCurrentPeriodKey()
|
||||
const periodStart = new Date(`${period}-01`)
|
||||
const periodStart = new Date(`${period}-01T00:00:00.000Z`)
|
||||
|
||||
const aggregated = await prisma.usageLog.groupBy({
|
||||
by: ['feature'],
|
||||
|
||||
@@ -4,6 +4,8 @@ import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import { createHash, randomBytes } from 'crypto'
|
||||
import { assertMcpAccess, isMcpTierAllowed } from '@/lib/mcp-access'
|
||||
import { getEffectiveTier } from '@/lib/entitlements'
|
||||
|
||||
const KEY_PREFIX = 'mcp_key_'
|
||||
|
||||
@@ -62,6 +64,8 @@ export async function generateMcpKey(name: string): Promise<{ rawKey: string; in
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) throw new Error('Unauthorized')
|
||||
|
||||
await assertMcpAccess(session.user.id)
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
select: { id: true, name: true, email: true },
|
||||
@@ -155,6 +159,13 @@ export type McpServerStatus = {
|
||||
url: string | null
|
||||
}
|
||||
|
||||
export async function getMcpAccessStatus(): Promise<{ allowed: boolean; tier: string }> {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) return { allowed: false, tier: 'BASIC' }
|
||||
const tier = await getEffectiveTier(session.user.id)
|
||||
return { allowed: isMcpTierAllowed(tier), tier }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get MCP server status — mode and URL.
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,8 @@ import prisma from '@/lib/prisma'
|
||||
import { getChatProvider } from '@/lib/ai/factory'
|
||||
import { getSystemConfig } from '@/lib/config'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import { withAiQuota } from '@/lib/ai-quota'
|
||||
import { QuotaExceededError } from '@/lib/entitlements'
|
||||
|
||||
export interface OrganizationNote {
|
||||
id: string
|
||||
@@ -134,8 +136,16 @@ Format de réponse JSON attendu:
|
||||
|
||||
let rawResponse: string
|
||||
try {
|
||||
rawResponse = await provider.generateText(prompt)
|
||||
rawResponse = await withAiQuota(
|
||||
session.user.id,
|
||||
'reformulate',
|
||||
() => provider.generateText(prompt),
|
||||
{ lane: 'chat' },
|
||||
)
|
||||
} catch (aiErr) {
|
||||
if (aiErr instanceof QuotaExceededError) {
|
||||
return { success: false, error: 'Quota IA épuisé pour cette fonctionnalité.' }
|
||||
}
|
||||
console.error('[organize-notebook] AI generateText failed:', aiErr)
|
||||
return { success: false, error: `L'IA n'a pas pu répondre : ${(aiErr as Error).message}` }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user