fix: audit fixes — tier-quota bypass, webhook SSRF, auth hardening, pagination, a11y
Full audit (bugs/UI-UX/backend/feature-gap) turned up a money-leak AI quota bypass, webhook SSRF, and a long tail of missing pagination/auth/a11y work. Fixes land together since HANDOFF.md tracked them as one backlog. - AI routes charge tier quota before generating; nutrition POST is author-only - Webhook dispatch re-validates URL per delivery (SSRF/DNS-rebinding), treats redirects as failures; recipe.published now actually dispatches - New indexes/unique constraints on recipes, meal-planning, comments FK cascade - Recipe PUT/restore snapshot only inside the transaction, after validation - Recipe DELETE cleans up S3 objects (recipe + review photos) - Optimistic UI (favorite/star/follow/shopping-list) rolls back on failure - Upload presign enforces file size cap + per-tier storage quota - Route-level loading/error/not-found states across (app), admin, and root - middleware.ts guards (app)/admin; requireAdmin checks DB role, not cached session; rate limiting applied to both session and API-key branches, bucketed per key; Stripe webhook dedupes by event id - Pagination added to recipes, feed, profile, comments, pantry, admin tables - Nav shows real avatar + profile link + dark-mode toggle; destructive actions standardized on AlertDialog - Unsaved-changes guard + real ingredient/step validation on recipe form; canonical /recipes/[id] used in-app; next/image migration; aria-labels and alt text across icon buttons, avatars, recipe photos - packages/api-types removed (zero callers, too drifted to safely rewire); openapi.ts and ai-keys error shape drift fixed; BYOK decrypt failures now surface instead of silently falling back to the platform key Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,10 @@
|
||||
"apiKeys": "Clés API",
|
||||
"webhooks": "Webhooks",
|
||||
"language": "Langue",
|
||||
"notifications": "Notifications"
|
||||
"notifications": "Notifications",
|
||||
"viewProfile": "Voir le profil",
|
||||
"lightMode": "Mode clair",
|
||||
"darkMode": "Mode sombre"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notifications",
|
||||
@@ -151,7 +154,8 @@
|
||||
"bulkVisibility": "{count, plural, one {1 recette définie sur {visibility}} other {{count} recettes définies sur {visibility}}}",
|
||||
"bulkDeleteFailed": "Échec de la suppression",
|
||||
"bulkUpdateFailed": "Échec de la mise à jour",
|
||||
"bulkDeleteConfirm": "{count, plural, one {Supprimer 1 recette ?} other {Supprimer {count} recettes ?}} Cette action est irréversible.",
|
||||
"bulkDeleteConfirmTitle": "{count, plural, one {Supprimer 1 recette ?} other {Supprimer {count} recettes ?}}",
|
||||
"bulkDeleteConfirmDescription": "Cette action est irréversible.",
|
||||
"deselectAll": "Tout désélectionner",
|
||||
"selectAll": "Tout sélectionner",
|
||||
"clickToSelect": "Cliquez sur les cartes pour sélectionner",
|
||||
@@ -323,7 +327,8 @@
|
||||
"ariaLabel": "Demander à l'IA à propos de cette recette",
|
||||
"title": "Poser une question sur cette recette",
|
||||
"intro": "Demandez n'importe quoi sur cette recette — ingrédients, techniques, substitutions, minutage…",
|
||||
"inputPlaceholder": "Posez une question…"
|
||||
"inputPlaceholder": "Posez une question…",
|
||||
"send": "Envoyer le message"
|
||||
},
|
||||
"servingScaler": {
|
||||
"servings": "Portions",
|
||||
@@ -351,7 +356,10 @@
|
||||
"selectConversation": "Sélectionnez une conversation",
|
||||
"messageButton": "Message",
|
||||
"startConversationFailed": "Échec du démarrage de la conversation",
|
||||
"title": "Messages"
|
||||
"title": "Messages",
|
||||
"loadOlder": "Charger les messages précédents",
|
||||
"loadingOlder": "Chargement…",
|
||||
"send": "Envoyer le message"
|
||||
},
|
||||
"nutritionPanel": {
|
||||
"estimateButton": "Estimer les valeurs nutritionnelles",
|
||||
@@ -414,7 +422,9 @@
|
||||
"anyDifficulty": "Toutes",
|
||||
"editManually": "Modifier manuellement",
|
||||
"surpriseMe": "Surprends-moi",
|
||||
"generatingContent": "Génération du contenu de la recette…"
|
||||
"generatingContent": "Génération du contenu de la recette…",
|
||||
"decrease": "Diminuer",
|
||||
"increase": "Augmenter"
|
||||
},
|
||||
"auth": {
|
||||
"signIn": "Se connecter",
|
||||
@@ -520,7 +530,13 @@
|
||||
"createRecipe": "Créer la recette",
|
||||
"updateSuccess": "Recette mise à jour",
|
||||
"createSuccess": "Recette créée",
|
||||
"saveError": "Échec de l'enregistrement de la recette"
|
||||
"saveError": "Échec de l'enregistrement de la recette",
|
||||
"ingredientsRequired": "Ajoutez au moins un ingrédient",
|
||||
"stepsRequired": "Ajoutez au moins une étape",
|
||||
"discardChangesTitle": "Abandonner les modifications ?",
|
||||
"discardChangesDescription": "Vous avez des modifications non enregistrées. Si vous quittez maintenant, elles seront perdues.",
|
||||
"keepEditing": "Continuer l'édition",
|
||||
"discardChanges": "Abandonner"
|
||||
},
|
||||
"canCook": {
|
||||
"title": "Que puis-je cuisiner ?",
|
||||
@@ -604,7 +620,8 @@
|
||||
"listCreating": "Création…",
|
||||
"listCreate": "Créer",
|
||||
"pickRecipe": "Choisir une recette",
|
||||
"addEntry": "+ Ajouter"
|
||||
"addEntry": "+ Ajouter",
|
||||
"removeEntry": "Retirer le repas"
|
||||
},
|
||||
"pantry": {
|
||||
"title": "Garde-manger",
|
||||
@@ -618,6 +635,8 @@
|
||||
"add": "Ajouter",
|
||||
"addFailed": "Échec de l'ajout",
|
||||
"removeFailed": "Échec de la suppression",
|
||||
"removeConfirmTitle": "Retirer cet article ?",
|
||||
"removeConfirmDescription": "Retirer « {name} » du garde-manger ?",
|
||||
"empty": "Aucun article dans le garde-manger.",
|
||||
"expired": "Expiré",
|
||||
"expiresInDays": "Expire dans {days}j",
|
||||
@@ -639,7 +658,10 @@
|
||||
"trendingEmpty": "Aucune recette tendance cette semaine.",
|
||||
"forYou": "Pour vous",
|
||||
"forYouEmpty": "Ajoutez des favoris ou notez des recettes pour des suggestions personnalisées.",
|
||||
"loading": "Chargement…"
|
||||
"loading": "Chargement…",
|
||||
"loadMore": "Charger plus",
|
||||
"loadFailed": "Échec du chargement des recettes. Vérifiez votre connexion et réessayez.",
|
||||
"retry": "Réessayer"
|
||||
},
|
||||
"shoppingLists": {
|
||||
"title": "Listes de courses",
|
||||
@@ -736,6 +758,13 @@
|
||||
"commentPlaceholder": "Partagez vos impressions…",
|
||||
"commentsTitle": "Commentaires",
|
||||
"noCommentsYet": "Aucun commentaire pour l'instant. Soyez le premier !",
|
||||
"deleteCommentTitle": "Supprimer le commentaire ?",
|
||||
"deleteCommentDescription": "Cette action est irréversible.",
|
||||
"blockConfirmTitle": "Bloquer @{username} ?",
|
||||
"blockConfirmDescription": "Cette personne ne pourra plus vous suivre ni commenter vos recettes.",
|
||||
"blockConfirmAction": "Bloquer",
|
||||
"loadMoreComments": "Charger plus de commentaires",
|
||||
"loadingMoreComments": "Chargement…",
|
||||
"postButton": "Publier",
|
||||
"postingButton": "Publication…",
|
||||
"replyButton": "Répondre",
|
||||
@@ -756,7 +785,9 @@
|
||||
"signInToReact": "Connectez-vous pour réagir aux commentaires",
|
||||
"reactionFailed": "Échec de la mise à jour de la réaction",
|
||||
"addReaction": "Ajouter la réaction {type}",
|
||||
"removeReaction": "Retirer la réaction {type}"
|
||||
"removeReaction": "Retirer la réaction {type}",
|
||||
"favoriteAdd": "Enregistrer la recette",
|
||||
"favoriteRemove": "Retirer la recette des enregistrements"
|
||||
},
|
||||
"cookingMode": {
|
||||
"cooking": "En cuisine",
|
||||
|
||||
Reference in New Issue
Block a user