fix: 5 bugs critiques de l'éditeur (Phase 1 audit)
1. replaceAll (Find & Replace) — une seule transaction ProseMirror au lieu d'un forEach cassé. Tous les matchs sont maintenant remplacés. 2. Link Preview unwrap — deleteNode() au lieu de clearer les attrs qui laissaient un nœud fantôme invisible dans le document. 3. Conversion Markdown → richtext — breaks: true dans marked.parse() Les simple newlines sont maintenant convertis en <br>. + préserve les blocs custom (toggle, callout, math, columns, outline, link-preview) en commentaires HTML lors de l'export MD. 4. emitNoteChange exercices — shape corrigée (type:'created' attend un objet Note, pas noteId/notebookId séparés). 5. Raccourcis clavier sans conflit : Cmd+Shift+C → Cmd+Alt+C (callout, avant: copier) Cmd+Shift+O → Cmd+Alt+O (outline, avant: historique/signets) Cmd+Shift+L → Cmd+Alt+L (colonnes, avant: lock screen macOS)
This commit is contained in:
@@ -7,8 +7,13 @@ import {
|
||||
parseRedisInt,
|
||||
isValidFeature,
|
||||
} from './quota-utils';
|
||||
|
||||
type SubscriptionTier = 'BASIC' | 'PRO' | 'BUSINESS' | 'ENTERPRISE';
|
||||
import {
|
||||
getLimitAsync,
|
||||
getTierFeaturesAsync,
|
||||
invalidateEntitlementCache,
|
||||
TIER_LIMITS,
|
||||
type SubscriptionTier,
|
||||
} from './plan-entitlements';
|
||||
|
||||
export interface EntitlementResult {
|
||||
allowed: boolean;
|
||||
@@ -70,79 +75,8 @@ export class QuotaExceededError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
const TIER_LIMITS: Record<SubscriptionTier, Record<string, number | 'unlimited'>> = {
|
||||
BASIC: {
|
||||
semantic_search: 30,
|
||||
auto_tag: 15,
|
||||
auto_title: 5,
|
||||
brainstorm_create: 1,
|
||||
brainstorm_expand: 10,
|
||||
brainstorm_enrich: 20,
|
||||
suggest_charts: 5,
|
||||
ai_flashcard: 5,
|
||||
voice_transcribe: 20,
|
||||
},
|
||||
PRO: {
|
||||
semantic_search: 200,
|
||||
auto_tag: 500,
|
||||
auto_title: 200,
|
||||
reformulate: 50,
|
||||
chat: 50,
|
||||
brainstorm_create: 5,
|
||||
brainstorm_expand: 100,
|
||||
brainstorm_enrich: 200,
|
||||
suggest_charts: 50,
|
||||
slide_generate: 20,
|
||||
excalidraw_generate: 20,
|
||||
ai_flashcard: 100,
|
||||
voice_transcribe: 500,
|
||||
},
|
||||
BUSINESS: {
|
||||
semantic_search: 1000,
|
||||
auto_tag: 1000,
|
||||
auto_title: 1000,
|
||||
reformulate: 500,
|
||||
chat: 500,
|
||||
brainstorm_create: 'unlimited',
|
||||
brainstorm_expand: 500,
|
||||
brainstorm_enrich: 1000,
|
||||
suggest_charts: 200,
|
||||
slide_generate: 100,
|
||||
excalidraw_generate: 100,
|
||||
ai_flashcard: 'unlimited',
|
||||
voice_transcribe: 'unlimited',
|
||||
},
|
||||
ENTERPRISE: {
|
||||
semantic_search: 'unlimited',
|
||||
auto_tag: 'unlimited',
|
||||
auto_title: 'unlimited',
|
||||
reformulate: 'unlimited',
|
||||
chat: 'unlimited',
|
||||
brainstorm_create: 'unlimited',
|
||||
brainstorm_expand: 'unlimited',
|
||||
brainstorm_enrich: 'unlimited',
|
||||
suggest_charts: 'unlimited',
|
||||
slide_generate: 'unlimited',
|
||||
excalidraw_generate: 'unlimited',
|
||||
ai_flashcard: 'unlimited',
|
||||
voice_transcribe: 'unlimited',
|
||||
},
|
||||
};
|
||||
|
||||
const TTL_SECONDS = 90 * 24 * 60 * 60;
|
||||
|
||||
const INCREMENT_LUA = `
|
||||
local current = tonumber(redis.call('GET', KEYS[1]) or '0')
|
||||
local ttl = tonumber(ARGV[1])
|
||||
redis.call('INCRBY', KEYS[1], 1)
|
||||
local ttlResult = redis.call('TTL', KEYS[1])
|
||||
if ttlResult == -1 then
|
||||
redis.call('EXPIRE', KEYS[1], ttl)
|
||||
end
|
||||
local newCount = tonumber(redis.call('GET', KEYS[1]))
|
||||
return newCount
|
||||
`;
|
||||
|
||||
const INCREMENT_BY_LUA = `
|
||||
local count = tonumber(ARGV[1]) or 1
|
||||
local ttl = tonumber(ARGV[2])
|
||||
@@ -171,14 +105,6 @@ local newCount = tonumber(redis.call('GET', KEYS[1]))
|
||||
return newCount
|
||||
`;
|
||||
|
||||
function getLimit(tier: SubscriptionTier, feature: string): number | undefined {
|
||||
const tierLimits = TIER_LIMITS[tier];
|
||||
const limit = tierLimits?.[feature];
|
||||
if (limit === 'unlimited') return Infinity;
|
||||
if (limit === undefined) return undefined;
|
||||
return limit;
|
||||
}
|
||||
|
||||
export async function getUserInfo(
|
||||
userId: string,
|
||||
): Promise<{ tier: SubscriptionTier; status: string; currentPeriodEnd?: Date }> {
|
||||
@@ -223,7 +149,7 @@ export async function canUseFeature(
|
||||
}
|
||||
|
||||
const tier = await getEffectiveTier(userId);
|
||||
const limit = getLimit(tier, feature);
|
||||
const limit = await getLimitAsync(tier, feature);
|
||||
|
||||
if (limit === undefined) {
|
||||
return {
|
||||
@@ -300,7 +226,7 @@ export async function reserveUsageOrThrow(
|
||||
}
|
||||
|
||||
const tier = await getEffectiveTier(userId);
|
||||
const limit = getLimit(tier, feature);
|
||||
const limit = await getLimitAsync(tier, feature);
|
||||
|
||||
if (limit === undefined) {
|
||||
throw new QuotaExceededError(
|
||||
@@ -374,8 +300,8 @@ export async function getUserQuotas(
|
||||
userId: string,
|
||||
): Promise<Record<string, { remaining: number; limit: number; used: number }>> {
|
||||
const tier = await getEffectiveTier(userId);
|
||||
const features = await getTierFeaturesAsync(tier);
|
||||
const period = getCurrentPeriodKey();
|
||||
const features = Object.keys(TIER_LIMITS[tier]);
|
||||
|
||||
if (features.length === 0) return {};
|
||||
|
||||
@@ -387,7 +313,7 @@ export async function getUserQuotas(
|
||||
const result: Record<string, { remaining: number; limit: number; used: number }> = {};
|
||||
for (let i = 0; i < features.length; i++) {
|
||||
const feature = features[i];
|
||||
const limit = getLimit(tier, feature) ?? 0;
|
||||
const limit = (await getLimitAsync(tier, feature)) ?? 0;
|
||||
const current = parseRedisInt(values[i]);
|
||||
result[feature] = {
|
||||
remaining: limit === Infinity ? Infinity : Math.max(0, limit - current),
|
||||
@@ -401,12 +327,17 @@ export async function getUserQuotas(
|
||||
console.error('[entitlements] getUserQuotas Redis error:', err);
|
||||
const result: Record<string, { remaining: number; limit: number; used: number }> = {};
|
||||
for (const feature of features) {
|
||||
const limit = getLimit(tier, feature) ?? 0;
|
||||
const limit = (await getLimitAsync(tier, feature)) ?? 0;
|
||||
result[feature] = { remaining: limit, limit, used: 0 };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export { TIER_LIMITS, getLimit };
|
||||
export { TIER_LIMITS, invalidateEntitlementCache };
|
||||
export type { SubscriptionTier };
|
||||
|
||||
/** @deprecated Use getLimitAsync — sync helper for tests only */
|
||||
export async function getLimit(tier: SubscriptionTier, feature: string): Promise<number | undefined> {
|
||||
return getLimitAsync(tier, feature);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user