feat(quiz-service): pipeline complet de gamification (RabbitMQ) - #27
Conversation
- generate_lessons.py : decoupe le vocabulaire deja importe (content-service)
en lecons de 8-10 mots, regroupees par niveau de difficulte deja calcule
(BEGINNER/INTERMEDIATE/ADVANCED, classification C4.5 automatique cote
recommendation-service). Idempotent par langue.
- generate_quizzes.py : genere une question QCM par mot de chaque lecon
('Comment dit-on X en {langue} ?'), distracteurs tires parmi les autres
mots de la meme lecon. Idempotent par lecon.
- Corrige l'absence totale de contenu pedagogique reel : Yemba (39 lecons/
344 mots), Duala (21/189), Bassa (81/727), avec 1257 questions generees
au total
- Question.wordId (nullable) : relie chaque question au mot content-service qu'elle teste -- necessaire pour que recommendation-service puisse suivre les mots uniques appris (gamification) - Migration V2 : ajoute la colonne + index - generate_quizzes.py : transmet desormais wordId a la creation - Les 1257 questions existantes ont ete
… session mobile - JwtAuthenticationFilter (api-gateway) : seul point de validation JWT du systeme. Laisse passer /api/auth/** sans verification, exige un Bearer token valide partout ailleurs, injecte X-User-Id (extrait du token) avant de router vers les services internes -- qui n'ont donc jamais besoin de decoder un JWT eux-memes - Reutilise la meme cle de signature HMAC que auth-service (jjwt) - mobile : AuthInterceptor attache automatiquement le token stocke a chaque requete Dio sortante - mobile : AuthController.restoreSession() reconstruit la session depuis flutter_secure_storage au demarrage -- corrige la perte de session a chaque rechargement de page (F5, navigation directe vers une URL, hot restart), la session n'etait auparavant gardee qu'en memoire - Necessaire avant de brancher quiz-service sur user_id fiable pour la gamification (aucune propagation d'identite inter-services n'existait avant ce commit)
- X-User-Id (injecte par api-gateway) desormais persiste sur chaque AnswerAttempt -- la table ne trackait auparavant aucun utilisateur - Les tentatives sont enfin reellement persistees (submitAnswer ne faisait que lire, jamais ecrire, malgre la table dediee) - Normalisation Unicode NFC avant comparaison des reponses -- corrige un bug reel ou des mots stockes en NFD (diacritiques combinants, frequent sur les tons Yemba) etaient injustement marques faux - LessonCompletionService : detecte la fin d'une lecon (toute question a au moins une tentative correcte -- les erreurs doivent etre corrigees avant completion, modele Duolingo) et publie lesson.completed + quiz.completed vers RabbitMQ (exchange afrilingua.events), consommes par le pipeline de gamification deja existant cote recommendation-service - is_perfect calcule honnetement (aucune tentative incorrecte enregistree) plutot que fige a true -- integrite du badge Perfectionniste - Table lesson_completions (contrainte unique user_id+lesson_id) : garde d'idempotence, empeche de republier/re-crediter l'XP a chaque reponse correcte soumise apres que la lecon soit deja terminee - Teste de bout en bout : login reel -> reponses avec erreur volontaire puis correction -> XP/niveau/lessons_completed/has_perfect_quiz corrects en base cote recommendation-service
📝 WalkthroughWalkthroughThe PR adds mobile session restoration and token injection, gateway JWT authentication, lesson and quiz generation scripts, and quiz-service answer-attempt persistence with lesson-completion events. ChangesMobile authentication
Gateway JWT authentication
Lesson and quiz generation tooling
Quiz completion tracking
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant MobileApp
participant ApiGateway
participant QuizService
participant RabbitMQ
MobileApp->>ApiGateway: request with Authorization bearer token
ApiGateway->>QuizService: request with trusted X-User-Id
QuizService->>QuizService: record answer and evaluate completion
QuizService->>RabbitMQ: publish quiz.completed and lesson.completed
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (8)
services/lesson-service/scripts/generate_lessons.py (1)
24-27: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winUse
requests.Session()for connection reuse across the script.The script makes hundreds of HTTP calls (one
create_lesson+ Nattach_wordper lesson). Eachrequests.get/postcreates a new TCP connection. A sharedrequests.Session()reuses connections via keep-alive, significantly reducing latency for batch generation scripts.♻️ Proposed refactor
import requests CONTENT_BASE_URL = "http://localhost:8080/api/content" LESSON_BASE_URL = "http://localhost:8080/api" +session = requests.Session() + DIFFICULTY_ORDER = ["BEGINNER", "INTERMEDIATE", "ADVANCED"]Then replace all
requests.get(...)/requests.post(...)calls withsession.get(...)/session.post(...).Also applies to: 42-71
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@services/lesson-service/scripts/generate_lessons.py` around lines 24 - 27, Introduce one shared requests.Session() for the lesson-generation script and reuse it across all HTTP operations. Update the request calls in the lesson creation and word-attachment flows, including the code around create_lesson and attach_word, to use the shared session’s get/post methods instead of module-level requests calls; preserve existing URLs, payloads, and response handling.services/quiz-service/scripts/generate_quizzes.py (1)
35-56: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winUse
requests.Session()and consider batching word fetches to reduce HTTP overhead.
fetch_wordis called once per word in the list comprehension at line 85, producing ~1,267 individual requests for the full dataset. Each also opens a new TCP connection. Arequests.Session()amortizes connection overhead; if the content-service supports a batch/bulk endpoint, fetching all words for a lesson in one call would eliminate the N+1 entirely.♻️ Proposed Session refactor
import requests CONTENT_BASE_URL = "http://localhost:8080/api/content" LESSON_BASE_URL = "http://localhost:8080/api" QUIZ_BASE_URL = "http://localhost:8080/api" +session = requests.Session() + MIN_WORDS_FOR_QUESTION = 4Then replace all
requests.get/postcalls withsession.get/post.Also applies to: 85-85
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@services/quiz-service/scripts/generate_quizzes.py` around lines 35 - 56, Refactor the quiz-generation request flow to create and reuse a requests.Session, replacing the direct requests.get calls in fetch_languages, fetch_lessons, fetch_word, and fetch_existing_question_count with session requests and passing the session through their callers. If the content service exposes a bulk word endpoint, update the word-fetching flow around fetch_word to retrieve lesson words in one request and eliminate the per-word N+1 calls.mobile/lib/features/auth/presentation/auth_providers.dart (2)
36-42: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winSequential un-batched writes risk partial-session state.
_persistSessionperforms 5 independentawaitwrites. If interrupted (app killed, storage exception mid-write) between writes, storage ends up with some but not all keys set. SincerestoreSession(lines 55-63) requires all five fields to be non-null to reconstruct a session, a partial write silently forces the user back to the login screen even though a valid access/refresh token pair was persisted.Consider persisting the session as a single JSON-encoded value under one key (single atomic write) instead of 5 separate keys.
♻️ Suggested consolidation
- Future<void> _persistSession(AuthResponse result) async { - await _storage.write(key: 'access_token', value: result.accessToken); - await _storage.write(key: 'refresh_token', value: result.refreshToken); - await _storage.write(key: 'account_id', value: result.id); - await _storage.write(key: 'email', value: result.email); - await _storage.write(key: 'role', value: result.role); - } + Future<void> _persistSession(AuthResponse result) async { + await _storage.write(key: 'session', value: jsonEncode({ + 'access_token': result.accessToken, + 'refresh_token': result.refreshToken, + 'account_id': result.id, + 'email': result.email, + 'role': result.role, + })); + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mobile/lib/features/auth/presentation/auth_providers.dart` around lines 36 - 42, Update _persistSession and the corresponding restoreSession logic to store the complete session as one JSON-encoded value under a single storage key, using one atomic write. Encode all five AuthResponse fields together, then decode that object during restoration and preserve the existing session reconstruction behavior.
79-85: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove the unused
sessionRestorationProvider.main.dartalready callsauthControllerProvider.notifier.restoreSession()beforerunApp, so this provider is dead code unless you plan to watch it somewhere else.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mobile/lib/features/auth/presentation/auth_providers.dart` around lines 79 - 85, Remove the unused sessionRestorationProvider declaration and its accompanying documentation comment; keep the existing authControllerProvider and restoreSession implementation unchanged because main.dart already invokes restoration before runApp.services/quiz-service/src/main/java/cm/afrilingua/quiz/config/EventProperties.java (1)
5-6: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider adding a default value for
exchange.If
application.events.exchangeis not set inapplication.yml,exchangewill benull, causingRabbitTemplate.convertAndSend(null, ...)to use the default AMQP exchange — which silently ignores routing keys and breaks event delivery to the recommendation service. Spring Boot 3.x supports@DefaultValueon record components.♻️ Suggested refactor
import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.bind.DefaultValue; `@ConfigurationProperties`(prefix = "application.events") -public record EventProperties(String exchange) {} +public record EventProperties(`@DefaultValue`("afrilingua.events") String exchange) {}Please verify that
application.events.exchangeis set inapplication.yml.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@services/quiz-service/src/main/java/cm/afrilingua/quiz/config/EventProperties.java` around lines 5 - 6, Ensure application.events.exchange is explicitly configured in application.yml and add a suitable `@DefaultValue` to the exchange component of EventProperties so it cannot be null when configuration is missing. Use the same exchange name in both places and preserve the existing configuration-properties binding.services/quiz-service/src/main/resources/application.yml (1)
21-25: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winExternalize RabbitMQ credentials for non-dev environments.
The RabbitMQ username and password are hardcoded, matching the datasource credentials. While acceptable for local development, ensure production deployments override these via environment variables (e.g.,
${RABBITMQ_USERNAME:afrilingua},${RABBITMQ_PASSWORD:afrilingua_dev_password}) or a secrets manager.🔒 Suggested externalization
rabbitmq: - host: localhost - port: 5672 - username: afrilingua - password: afrilingua_dev_password + host: ${RABBITMQ_HOST:localhost} + port: ${RABBITMQ_PORT:5672} + username: ${RABBITMQ_USERNAME:afrilingua} + password: ${RABBITMQ_PASSWORD:afrilingua_dev_password}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@services/quiz-service/src/main/resources/application.yml` around lines 21 - 25, Update the RabbitMQ credentials in the rabbitmq configuration to resolve username and password from environment variables with the current development credentials as defaults, allowing production deployments to override them without changing the application configuration.services/quiz-service/src/main/resources/db/migration/V2__add_word_id_to_questions.sql (1)
1-3: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueConsider concurrent index creation for production deployments.
Squawk flagged that
CREATE INDEXblocks writes. While this is negligible for a new/empty table in early development, consider usingCREATE INDEX CONCURRENTLYfor production-scale tables. Note thatCONCURRENTLYcannot run inside a transaction, so it would require a separate Flyway migration withexecuteInTransaction: false.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@services/quiz-service/src/main/resources/db/migration/V2__add_word_id_to_questions.sql` around lines 1 - 3, Update the questions index migration to create idx_questions_word concurrently for production-scale tables, and configure the Flyway migration with executeInTransaction: false because concurrent index creation cannot run in a transaction. Keep the word_id column addition unchanged and ensure the index creation is placed in a migration that runs outside a transaction.Source: Linters/SAST tools
services/quiz-service/src/main/resources/openapi/quiz-service.yaml (1)
178-197: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider adding
wordIdtoQuestionWithAnswerfor response consistency.The
QuestionWithAnswerschema (returned to the creator on question creation) omits the newwordIdfield, while bothCreateQuestionRequestand the learner-facingQuestionschema include it. Adding it would let creators verify the storedwordIdwithout a separate fetch.♻️ Suggested addition
QuestionWithAnswer: type: object description: Full view, returned only to the caller who just created the question properties: id: type: string format: uuid lessonId: type: string format: uuid + wordId: + type: string + format: uuid + nullable: true type: type: string enum: [MULTIPLE_CHOICE, MATCHING, FILL_IN_THE_BLANK]🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@services/quiz-service/src/main/resources/openapi/quiz-service.yaml` around lines 178 - 197, Add the wordId property to the QuestionWithAnswer schema, matching the existing definition and UUID format used by CreateQuestionRequest and Question, so question-creation responses expose the stored word association consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@mobile/lib/core/network/auth_interceptor.dart`:
- Around line 13-18: Update onRequest to catch failures from _storage.read,
allowing the request to continue without an Authorization header when token
retrieval fails. Ensure handler.next(options) is always called after the read
attempt, while preserving the existing header behavior for successfully
retrieved non-null tokens.
In `@mobile/lib/features/auth/presentation/auth_providers.dart`:
- Around line 44-63: Update Auth provider’s restoreSession() to catch
secure-storage read failures, including PlatformException, and reset or retain
the logged-out state instead of allowing the exception to propagate to startup.
Keep successful token reconstruction via AuthResponse unchanged, and use the
existing logging mechanism if available to record the failure.
In `@services/api-gateway/pom.xml`:
- Line 23: Update the jjwt.version property from 0.12.6 to 0.12.7 in the Maven
configuration, ensuring jjwt-api, jjwt-impl, and jjwt-jackson continue resolving
the same aligned version through this property.
In
`@services/api-gateway/src/main/java/cm/afrilingua/gateway/security/JwtAuthenticationFilter.java`:
- Around line 61-82: Validate the subject returned by claims.getSubject() in
JwtAuthenticationFilter before mutating the request: reject null or blank values
through the existing unauthorized flow instead of setting X-User-Id. Preserve
the current header replacement and valid-token behavior for nonblank subjects.
In `@services/api-gateway/src/main/resources/application.yml`:
- Around line 73-77: Remove the hardcoded fallback from the JWT secret-key
configuration and require JWT_SECRET_KEY to be explicitly provided, failing
application startup when it is absent. Apply the same required-secret
configuration to auth-service’s equivalent JwtProperties setting, preserving the
exact shared secret contract between both services.
In `@services/quiz-service/scripts/generate_quizzes.py`:
- Line 120: In the caller assigning the result of generate_for_lesson, rename
the ambiguous variable l to a descriptive name such as lesson data while
preserving the existing q value and downstream behavior.
In
`@services/quiz-service/src/main/java/cm/afrilingua/quiz/service/LessonCompletionService.java`:
- Around line 39-88: Update LessonCompletionService.checkAndPublishIfComplete to
catch DataIntegrityViolationException specifically around
lessonCompletionRepository.save and return when a concurrent completion already
won, preserving the answer transaction. Move both completion event publications
behind an AFTER_COMMIT transactional event listener: publish an internal
lesson-completed event after the save, and handle it in a listener that invokes
the existing eventPublisher methods only after the surrounding transaction
commits.
In
`@services/quiz-service/src/main/java/cm/afrilingua/quiz/service/QuestionService.java`:
- Around line 64-90: The submitAnswer transaction currently couples saving
AnswerAttempt with lesson completion processing, so completion failures can roll
back the recorded answer. Update submitAnswer and the completion flow so the
answer attempt is committed independently, then invoke checkAndPublishIfComplete
in a separate transaction or failure-isolated path while preserving the existing
correct-answer response behavior.
- Around line 92-99: Update the private normalize method in QuestionService to
handle a null value before calling trim or Normalizer.normalize, returning the
expected null-safe result so missing submitted answers do not cause an
exception. Preserve the existing trim and NFC normalization behavior for
non-null values.
---
Nitpick comments:
In `@mobile/lib/features/auth/presentation/auth_providers.dart`:
- Around line 36-42: Update _persistSession and the corresponding restoreSession
logic to store the complete session as one JSON-encoded value under a single
storage key, using one atomic write. Encode all five AuthResponse fields
together, then decode that object during restoration and preserve the existing
session reconstruction behavior.
- Around line 79-85: Remove the unused sessionRestorationProvider declaration
and its accompanying documentation comment; keep the existing
authControllerProvider and restoreSession implementation unchanged because
main.dart already invokes restoration before runApp.
In `@services/lesson-service/scripts/generate_lessons.py`:
- Around line 24-27: Introduce one shared requests.Session() for the
lesson-generation script and reuse it across all HTTP operations. Update the
request calls in the lesson creation and word-attachment flows, including the
code around create_lesson and attach_word, to use the shared session’s get/post
methods instead of module-level requests calls; preserve existing URLs,
payloads, and response handling.
In `@services/quiz-service/scripts/generate_quizzes.py`:
- Around line 35-56: Refactor the quiz-generation request flow to create and
reuse a requests.Session, replacing the direct requests.get calls in
fetch_languages, fetch_lessons, fetch_word, and fetch_existing_question_count
with session requests and passing the session through their callers. If the
content service exposes a bulk word endpoint, update the word-fetching flow
around fetch_word to retrieve lesson words in one request and eliminate the
per-word N+1 calls.
In
`@services/quiz-service/src/main/java/cm/afrilingua/quiz/config/EventProperties.java`:
- Around line 5-6: Ensure application.events.exchange is explicitly configured
in application.yml and add a suitable `@DefaultValue` to the exchange component of
EventProperties so it cannot be null when configuration is missing. Use the same
exchange name in both places and preserve the existing configuration-properties
binding.
In `@services/quiz-service/src/main/resources/application.yml`:
- Around line 21-25: Update the RabbitMQ credentials in the rabbitmq
configuration to resolve username and password from environment variables with
the current development credentials as defaults, allowing production deployments
to override them without changing the application configuration.
In
`@services/quiz-service/src/main/resources/db/migration/V2__add_word_id_to_questions.sql`:
- Around line 1-3: Update the questions index migration to create
idx_questions_word concurrently for production-scale tables, and configure the
Flyway migration with executeInTransaction: false because concurrent index
creation cannot run in a transaction. Keep the word_id column addition unchanged
and ensure the index creation is placed in a migration that runs outside a
transaction.
In `@services/quiz-service/src/main/resources/openapi/quiz-service.yaml`:
- Around line 178-197: Add the wordId property to the QuestionWithAnswer schema,
matching the existing definition and UUID format used by CreateQuestionRequest
and Question, so question-creation responses expose the stored word association
consistently.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2c3c81a0-9e5b-4212-95a6-d1ba6ee5d86e
📒 Files selected for processing (28)
mobile/lib/core/network/api_client.dartmobile/lib/core/network/auth_interceptor.dartmobile/lib/features/auth/presentation/auth_providers.dartmobile/lib/main.dartservices/api-gateway/pom.xmlservices/api-gateway/src/main/java/cm/afrilingua/gateway/ApiGatewayApplication.javaservices/api-gateway/src/main/java/cm/afrilingua/gateway/config/JwtProperties.javaservices/api-gateway/src/main/java/cm/afrilingua/gateway/security/JwtAuthenticationFilter.javaservices/api-gateway/src/main/resources/application.ymlservices/lesson-service/scripts/generate_lessons.pyservices/quiz-service/pom.xmlservices/quiz-service/scripts/generate_quizzes.pyservices/quiz-service/src/main/java/cm/afrilingua/quiz/QuizServiceApplication.javaservices/quiz-service/src/main/java/cm/afrilingua/quiz/config/EventProperties.javaservices/quiz-service/src/main/java/cm/afrilingua/quiz/config/RabbitMQConfig.javaservices/quiz-service/src/main/java/cm/afrilingua/quiz/entity/AnswerAttempt.javaservices/quiz-service/src/main/java/cm/afrilingua/quiz/entity/LessonCompletion.javaservices/quiz-service/src/main/java/cm/afrilingua/quiz/entity/Question.javaservices/quiz-service/src/main/java/cm/afrilingua/quiz/messaging/LessonCompletionEventPublisher.javaservices/quiz-service/src/main/java/cm/afrilingua/quiz/repository/AnswerAttemptRepository.javaservices/quiz-service/src/main/java/cm/afrilingua/quiz/repository/LessonCompletionRepository.javaservices/quiz-service/src/main/java/cm/afrilingua/quiz/service/LessonCompletionService.javaservices/quiz-service/src/main/java/cm/afrilingua/quiz/service/QuestionService.javaservices/quiz-service/src/main/resources/application.ymlservices/quiz-service/src/main/resources/db/migration/V2__add_word_id_to_questions.sqlservices/quiz-service/src/main/resources/db/migration/V3__add_user_id_to_answer_attempts.sqlservices/quiz-service/src/main/resources/db/migration/V4__create_lesson_completions_table.sqlservices/quiz-service/src/main/resources/openapi/quiz-service.yaml
| void onRequest(RequestOptions options, RequestInterceptorHandler handler) async { | ||
| final token = await _storage.read(key: 'access_token'); | ||
| if (token != null) { | ||
| options.headers['Authorization'] = 'Bearer $token'; | ||
| } | ||
| handler.next(options); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Unhandled storage read failure can hang requests indefinitely.
If _storage.read throws (platform exception, corrupted storage, permissions change), handler.next(options) is never called and the request stalls until the Dio timeout fires. Wrap the read in a try-catch so the request proceeds without a token — the gateway will return 401, which is the correct degraded behavior.
🛡️ Proposed fix: add error handling around storage read
`@override`
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
- final token = await _storage.read(key: 'access_token');
- if (token != null) {
- options.headers['Authorization'] = 'Bearer $token';
+ try {
+ final token = await _storage.read(key: 'access_token');
+ if (token != null) {
+ options.headers['Authorization'] = 'Bearer $token';
+ }
+ } catch (_) {
+ // If storage is unavailable, proceed without token; gateway will reject with 401.
}
handler.next(options);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| void onRequest(RequestOptions options, RequestInterceptorHandler handler) async { | |
| final token = await _storage.read(key: 'access_token'); | |
| if (token != null) { | |
| options.headers['Authorization'] = 'Bearer $token'; | |
| } | |
| handler.next(options); | |
| void onRequest(RequestOptions options, RequestInterceptorHandler handler) async { | |
| try { | |
| final token = await _storage.read(key: 'access_token'); | |
| if (token != null) { | |
| options.headers['Authorization'] = 'Bearer $token'; | |
| } | |
| } catch (_) { | |
| // If storage is unavailable, proceed without token; gateway will reject with 401. | |
| } | |
| handler.next(options); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@mobile/lib/core/network/auth_interceptor.dart` around lines 13 - 18, Update
onRequest to catch failures from _storage.read, allowing the request to continue
without an Authorization header when token retrieval fails. Ensure
handler.next(options) is always called after the read attempt, while preserving
the existing header behavior for successfully retrieved non-null tokens.
| /// Reconstructs the session from secure storage on app startup (page | ||
| /// reload, direct URL navigation, hot restart). Without this, the | ||
| /// in-memory-only state means every reload silently logs the user out | ||
| /// even though valid tokens are still on disk. | ||
| Future<void> restoreSession() async { | ||
| final accessToken = await _storage.read(key: 'access_token'); | ||
| final refreshToken = await _storage.read(key: 'refresh_token'); | ||
| final accountId = await _storage.read(key: 'account_id'); | ||
| final email = await _storage.read(key: 'email'); | ||
| final role = await _storage.read(key: 'role'); | ||
|
|
||
| if (accessToken != null && refreshToken != null && accountId != null && email != null && role != null) { | ||
| state = AuthResponse( | ||
| id: accountId, | ||
| accessToken: accessToken, | ||
| refreshToken: refreshToken, | ||
| email: email, | ||
| role: role, | ||
| ); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== files ==\n'
git ls-files 'mobile/lib/features/auth/presentation/auth_providers.dart' 'mobile/lib/main.dart'
printf '\n== outline auth_providers ==\n'
ast-grep outline mobile/lib/features/auth/presentation/auth_providers.dart --view expanded || true
printf '\n== outline main ==\n'
ast-grep outline mobile/lib/main.dart --view expanded || true
printf '\n== relevant lines auth_providers ==\n'
sed -n '1,220p' mobile/lib/features/auth/presentation/auth_providers.dart | cat -n
printf '\n== relevant lines main ==\n'
sed -n '1,160p' mobile/lib/main.dart | cat -nRepository: ASSONDJI/afrilingua
Length of output: 5898
Guard restoreSession() against secure-storage failures
mobile/lib/main.dart awaits restoreSession() before runApp(), so any PlatformException from _storage.read(...) will abort startup and leave the app blank. Catch the reads here and fall back to a logged-out state instead of crashing.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@mobile/lib/features/auth/presentation/auth_providers.dart` around lines 44 -
63, Update Auth provider’s restoreSession() to catch secure-storage read
failures, including PlatformException, and reset or retain the logged-out state
instead of allowing the exception to propagate to startup. Keep successful token
reconstruction via AuthResponse unchanged, and use the existing logging
mechanism if available to record the failure.
| <properties> | ||
| <java.version>21</java.version> | ||
| <spring-cloud.version>2023.0.3</spring-cloud.version> | ||
| <jjwt.version>0.12.6</jjwt.version> |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check Maven Central for the latest JJWT 0.12.x release
curl -s "https://repo1.maven.org/maven2/io/jsonwebtoken/jjwt-api/maven-metadata.xml" | grep -oP '<version>0\.12\.\d+</version>' | sort -V | tail -5
# Check GitHub advisories for JJWT
gh api graphql -f query='
{
securityVulnerabilities(first: 5, ecosystem: MAVEN, package: "io.jsonwebtoken:jjwt-api") {
nodes {
advisory { summary severity }
vulnerableVersionRange
firstPatchedVersion { identifier }
}
}
}'Repository: ASSONDJI/afrilingua
Length of output: 336
🏁 Script executed:
#!/bin/bash
set -euo pipefail
for artifact in jjwt-api jjwt-impl jjwt-jackson; do
echo "== $artifact =="
curl -fsS "https://repo1.maven.org/maven2/io/jsonwebtoken/${artifact}/maven-metadata.xml" \
| grep -oP '<version>0\.12\.\d+</version>' \
| sort -V \
| tail -5
doneRepository: ASSONDJI/afrilingua
Length of output: 597
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,120p' services/api-gateway/pom.xml | cat -nRepository: ASSONDJI/afrilingua
Length of output: 3634
Bump JJWT to 0.12.7 — 0.12.6 is behind the current 0.12.x patch; update the version property and keep jjwt-api, jjwt-impl, and jjwt-jackson aligned.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@services/api-gateway/pom.xml` at line 23, Update the jjwt.version property
from 0.12.6 to 0.12.7 in the Maven configuration, ensuring jjwt-api, jjwt-impl,
and jjwt-jackson continue resolving the same aligned version through this
property.
| try { | ||
| Claims claims = Jwts.parser() | ||
| .verifyWith(signingKey) | ||
| .build() | ||
| .parseSignedClaims(token) | ||
| .getPayload(); | ||
|
|
||
| String userId = claims.getSubject(); | ||
|
|
||
| ServerHttpRequest mutatedRequest = exchange.getRequest().mutate() | ||
| .headers(headers -> { | ||
| headers.remove("Authorization"); | ||
| headers.remove("X-User-Id"); // never trust a client-supplied value | ||
| headers.set("X-User-Id", userId); | ||
| }) | ||
| .build(); | ||
|
|
||
| return chain.filter(exchange.mutate().request(mutatedRequest).build()); | ||
| } catch (JwtException | IllegalArgumentException e) { | ||
| return unauthorized(exchange, "Invalid or expired token"); | ||
| } | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Unvalidated null subject propagates into X-User-Id.
claims.getSubject() (line 68) isn't checked for null/blank before being set as X-User-Id (line 74). If a token is ever parsed without a sub claim (malformed/legacy token, misconfigured issuer), downstream services would receive either a missing header or a header literally set to "null", and QuestionService.extractUserId() (which does UUID.fromString(userId)) would throw on a non-UUID value rather than the request being rejected here at the gateway boundary.
🛡️ Suggested defensive check
String userId = claims.getSubject();
+ if (userId == null || userId.isBlank()) {
+ return unauthorized(exchange, "Invalid or expired token");
+ }JWT parsing itself (Jwts.parser().verifyWith(signingKey).build().parseSignedClaims(token)) correctly follows the jjwt 0.12.x API.
Note: the static analysis hint about a "randomly-generated IV" on line 91 is a false positive — that line only builds a JSON error string, no cryptographic/CBC operation is involved.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| Claims claims = Jwts.parser() | |
| .verifyWith(signingKey) | |
| .build() | |
| .parseSignedClaims(token) | |
| .getPayload(); | |
| String userId = claims.getSubject(); | |
| ServerHttpRequest mutatedRequest = exchange.getRequest().mutate() | |
| .headers(headers -> { | |
| headers.remove("Authorization"); | |
| headers.remove("X-User-Id"); // never trust a client-supplied value | |
| headers.set("X-User-Id", userId); | |
| }) | |
| .build(); | |
| return chain.filter(exchange.mutate().request(mutatedRequest).build()); | |
| } catch (JwtException | IllegalArgumentException e) { | |
| return unauthorized(exchange, "Invalid or expired token"); | |
| } | |
| } | |
| try { | |
| Claims claims = Jwts.parser() | |
| .verifyWith(signingKey) | |
| .build() | |
| .parseSignedClaims(token) | |
| .getPayload(); | |
| String userId = claims.getSubject(); | |
| if (userId == null || userId.isBlank()) { | |
| return unauthorized(exchange, "Invalid or expired token"); | |
| } | |
| ServerHttpRequest mutatedRequest = exchange.getRequest().mutate() | |
| .headers(headers -> { | |
| headers.remove("Authorization"); | |
| headers.remove("X-User-Id"); // never trust a client-supplied value | |
| headers.set("X-User-Id", userId); | |
| }) | |
| .build(); | |
| return chain.filter(exchange.mutate().request(mutatedRequest).build()); | |
| } catch (JwtException | IllegalArgumentException e) { | |
| return unauthorized(exchange, "Invalid or expired token"); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@services/api-gateway/src/main/java/cm/afrilingua/gateway/security/JwtAuthenticationFilter.java`
around lines 61 - 82, Validate the subject returned by claims.getSubject() in
JwtAuthenticationFilter before mutating the request: reject null or blank values
through the existing unauthorized flow instead of setting X-User-Id. Preserve
the current header replacement and valid-token behavior for nonblank subjects.
| include: health,gateway | ||
| application: | ||
| security: | ||
| jwt: | ||
| secret-key: ${JWT_SECRET_KEY:404E635266556A586E3272357538782F413F4428472B4B6250645367566B59} |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win
Hardcoded JWT secret default checked into source control.
${JWT_SECRET_KEY:404E635266556A586E3272357538782F413F4428472B4B6250645367566B59} bakes a fixed fallback secret directly into the repo. Anyone with source access can read this value and forge valid JWTs for any user if JWT_SECRET_KEY isn't set in an environment (misconfigured deploy, forgotten env var, local/staging environment mistakenly treated as trusted). Given JwtAuthenticationFilter is documented as "the single point of authentication for the whole platform," a leaked/guessable static secret defeats the entire auth model.
Fail fast instead of silently falling back to a checked-in secret:
🔒 Suggested fix
application:
security:
jwt:
- secret-key: ${JWT_SECRET_KEY:404E635266556A586E3272357538782F413F4428472B4B6250645367566B59}
+ secret-key: ${JWT_SECRET_KEY}Note: this must be coordinated with auth-service's equivalent property, which per the JwtProperties comment must share the exact same secret.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| include: health,gateway | |
| application: | |
| security: | |
| jwt: | |
| secret-key: ${JWT_SECRET_KEY:404E635266556A586E3272357538782F413F4428472B4B6250645367566B59} | |
| include: health,gateway | |
| application: | |
| security: | |
| jwt: | |
| secret-key: ${JWT_SECRET_KEY} |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@services/api-gateway/src/main/resources/application.yml` around lines 73 -
77, Remove the hardcoded fallback from the JWT secret-key configuration and
require JWT_SECRET_KEY to be explicitly provided, failing application startup
when it is absent. Apply the same required-secret configuration to
auth-service’s equivalent JwtProperties setting, preserving the exact shared
secret contract between both services.
| lessons = fetch_lessons(language["id"]) | ||
| total_questions, total_lessons = 0, 0 | ||
| for lesson in lessons: | ||
| q, l = generate_for_lesson(language["name"], lesson, args.force) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Rename ambiguous variable l to satisfy Ruff E741.
Ruff flags l as an ambiguous variable name (E741). Use a descriptive name instead.
💚 Proposed fix
for lesson in lessons:
- q, l = generate_for_lesson(language["name"], lesson, args.force)
+ q, processed = generate_for_lesson(language["name"], lesson, args.force)
total_questions += q
- total_lessons += l
+ total_lessons += processed📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| q, l = generate_for_lesson(language["name"], lesson, args.force) | |
| for lesson in lessons: | |
| q, processed = generate_for_lesson(language["name"], lesson, args.force) | |
| total_questions += q | |
| total_lessons += processed |
🧰 Tools
🪛 Ruff (0.15.20)
[error] 120-120: Ambiguous variable name: l
(E741)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@services/quiz-service/scripts/generate_quizzes.py` at line 120, In the caller
assigning the result of generate_for_lesson, rename the ambiguous variable l to
a descriptive name such as lesson data while preserving the existing q value and
downstream behavior.
Source: Linters/SAST tools
| public void checkAndPublishIfComplete(UUID userId, UUID lessonId) { | ||
| if (userId == null) { | ||
| return; // no trusted identity (e.g. request bypassed the gateway) -- skip silently | ||
| } | ||
|
|
||
| // Idempotency guard: once this (user, lesson) pair is recorded, never | ||
| // re-publish -- without this, every correct answer submitted AFTER | ||
| // the lesson was already completed (e.g. re-answering a question | ||
| // that was already correct) would re-fire both events and | ||
| // re-award XP/badges. | ||
| if (lessonCompletionRepository.existsByUserIdAndLessonId(userId, lessonId)) { | ||
| return; | ||
| } | ||
|
|
||
| List<Question> lessonQuestions = questionRepository.findByLessonId(lessonId); | ||
| if (lessonQuestions.isEmpty()) { | ||
| return; | ||
| } | ||
|
|
||
| List<UUID> questionIds = lessonQuestions.stream().map(Question::getId).toList(); | ||
|
|
||
| Set<UUID> correctlyAnsweredQuestionIds = answerAttemptRepository | ||
| .findByUserIdAndQuestionIdInAndIsCorrectTrue(userId, questionIds) | ||
| .stream() | ||
| .map(cm.afrilingua.quiz.entity.AnswerAttempt::getQuestionId) | ||
| .collect(Collectors.toSet()); | ||
|
|
||
| boolean allAnswered = questionIds.stream().allMatch(correctlyAnsweredQuestionIds::contains); | ||
| if (!allAnswered) { | ||
| return; | ||
| } | ||
|
|
||
| List<UUID> correctWordIds = lessonQuestions.stream() | ||
| .map(Question::getWordId) | ||
| .filter(java.util.Objects::nonNull) | ||
| .distinct() | ||
| .toList(); | ||
|
|
||
| // Accurate perfection check: true only if NO wrong attempt exists for | ||
| // any question in this lesson, not just "eventually got it right". | ||
| boolean isPerfect = !answerAttemptRepository | ||
| .existsByUserIdAndQuestionIdInAndIsCorrectFalse(userId, questionIds); | ||
|
|
||
| lessonCompletionRepository.save( | ||
| LessonCompletion.builder().userId(userId).lessonId(lessonId).build() | ||
| ); | ||
|
|
||
| eventPublisher.publishLessonCompleted(userId); | ||
| eventPublisher.publishQuizCompleted(userId, correctWordIds, isPerfect); | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift
Events are published before the transaction commits — rollback causes duplicate XP/badges and silent answer loss.
checkAndPublishIfComplete is called within QuestionService.submitAnswer's @Transactional boundary. The eventPublisher.publish* calls (lines 86–87) send messages to RabbitMQ before the transaction commits. Two failure modes result:
-
Duplicate events on rollback: If the transaction fails to commit after events are published (e.g., unique constraint violation from a concurrent request, connection drop), the events are already in RabbitMQ. The
LessonCompletionrecord is not persisted, so the next correct answer passes the idempotency check (line 49) and re-publishes events — awarding duplicate XP/badges. -
Answer attempt loss on concurrent completion: Two concurrent requests for the last questions of a lesson can both pass the
existsByUserIdAndLessonIdcheck (lines 49–51). One succeeds; the other fails with a unique constraint violation onlessonCompletionRepository.save()(line 82). This exception rolls back the entiresubmitAnswertransaction, including theAnswerAttemptsave inQuestionService— the user's correct answer is silently lost.
Recommended fix: Decouple event publishing from the answer-submission transaction using @TransactionalEventListener(phase = AFTER_COMMIT) so events fire only after a successful commit. Additionally, catch DataIntegrityViolationException on the save call to handle the TOCTOU race gracefully — the completion already exists from the winning request, so simply return.
🔧 Proposed approach using TransactionalEventListener
// In LessonCompletionService: instead of calling eventPublisher directly,
// publish a Spring application event within the transaction.
+import org.springframework.context.ApplicationEventPublisher;
// ...
+private final ApplicationEventPublisher applicationEventPublisher;
// Replace lines 86-87:
-eventPublisher.publishLessonCompleted(userId);
-eventPublisher.publishQuizCompleted(userId, correctWordIds, isPerfect);
+applicationEventPublisher.publishEvent(
+ new LessonCompletedEvent(userId, correctWordIds, isPerfect));
// Catch constraint violations from concurrent requests:
try {
lessonCompletionRepository.save(
LessonCompletion.builder().userId(userId).lessonId(lessonId).build()
);
+} catch (org.springframework.dao.DataIntegrityViolationException e) {
+ return; // completion already recorded by a concurrent request
}
// New event class + listener in LessonCompletionEventPublisher:
+public record LessonCompletedEvent(UUID userId, List<UUID> correctWordIds, boolean isPerfect) {}
+@org.springframework.transaction.event.TransactionalEventListener(phase = org.springframework.transaction.event.TransactionPhase.AFTER_COMMIT)
+public void onLessonCompleted(LessonCompletedEvent event) {
+ publishLessonCompleted(event.userId());
+ publishQuizCompleted(event.userId(), event.correctWordIds(), event.isPerfect());
+}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@services/quiz-service/src/main/java/cm/afrilingua/quiz/service/LessonCompletionService.java`
around lines 39 - 88, Update LessonCompletionService.checkAndPublishIfComplete
to catch DataIntegrityViolationException specifically around
lessonCompletionRepository.save and return when a concurrent completion already
won, preserving the answer transaction. Move both completion event publications
behind an AFTER_COMMIT transactional event listener: publish an internal
lesson-completed event after the save, and handle it in a listener that invokes
the existing eventPublisher methods only after the surrounding transaction
commits.
| @Transactional | ||
| public AnswerResult submitAnswer(UUID questionId, SubmitAnswerRequest request) { | ||
| cm.afrilingua.quiz.entity.Question question = questionRepository.findById(questionId) | ||
| .orElseThrow(() -> new QuestionNotFoundException(questionId)); | ||
|
|
||
| boolean isCorrect = question.getCorrectAnswer() | ||
| .trim().equalsIgnoreCase(request.getSubmittedAnswer().trim()); | ||
| boolean isCorrect = normalize(question.getCorrectAnswer()) | ||
| .equalsIgnoreCase(normalize(request.getSubmittedAnswer())); | ||
|
|
||
| UUID userId = extractUserId(); | ||
|
|
||
| AnswerAttempt attempt = AnswerAttempt.builder() | ||
| .questionId(question.getId()) | ||
| .userId(userId) | ||
| .submittedAnswer(request.getSubmittedAnswer()) | ||
| .isCorrect(isCorrect) | ||
| .build(); | ||
| answerAttemptRepository.save(attempt); | ||
|
|
||
| if (isCorrect) { | ||
| lessonCompletionService.checkAndPublishIfComplete(userId, question.getLessonId()); | ||
| } | ||
|
|
||
| return new AnswerResult() | ||
| .questionId(question.getId()) | ||
| .correct(isCorrect) | ||
| .correctAnswer(question.getCorrectAnswer()); | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Answer attempt and completion check share a single transaction — failures in completion roll back the attempt.
submitAnswer is @Transactional and calls checkAndPublishIfComplete within the same transaction. If the completion check throws (e.g., unique constraint violation from a concurrent request, or RabbitMQ connectivity failure), the AnswerAttempt save at line 80 is also rolled back — the user's correct answer is silently lost. See the detailed analysis in the LessonCompletionService review comment.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@services/quiz-service/src/main/java/cm/afrilingua/quiz/service/QuestionService.java`
around lines 64 - 90, The submitAnswer transaction currently couples saving
AnswerAttempt with lesson completion processing, so completion failures can roll
back the recorded answer. Update submitAnswer and the completion flow so the
answer attempt is committed independently, then invoke checkAndPublishIfComplete
in a separate transaction or failure-isolated path while preserving the existing
correct-answer response behavior.
| /** Normalizes to NFC before comparison: the words dataset (Yemba tones in | ||
| * particular) can be stored with combining diacritics (NFD) while user | ||
| * input -- or even the reference data itself -- may use precomposed | ||
| * characters (NFC). Without this, visually identical strings compare | ||
| * as different, silently marking correct answers as wrong. */ | ||
| private String normalize(String value) { | ||
| return Normalizer.normalize(value.trim(), Normalizer.Form.NFC); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the request model and controller references
git ls-files | rg 'services/quiz-service/src/main/java/.*QuestionService\.java|SubmitAnswerRequest|Quiz.*Controller|Answer.*Controller|openapi|swagger|api'
# Search for submittedAnswer annotations and validation usage
rg -n --hidden -S 'submittedAnswer|`@NotNull`|`@Valid`|`@Validated`|`@RequestBody`' services/quiz-service/src/main/java services/quiz-service/src/main/resources
# Show the relevant request class if present
fd -a 'SubmitAnswerRequest.*' services/quiz-service/src/main/java services/quiz-service/src/main/resourcesRepository: ASSONDJI/afrilingua
Length of output: 2015
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map the quiz-service source tree
ast-grep outline services/quiz-service/src/main/java/cm/afrilingua/quiz/service/QuestionService.java --view expanded || true
ast-grep outline services/quiz-service/src/main/resources/openapi/quiz-service.yaml --view expanded || true
# Inspect the OpenAPI contract around submit answer
sed -n '120,170p' services/quiz-service/src/main/resources/openapi/quiz-service.yaml
# Find the request model and controller handling submit-answer
rg -n --hidden -S 'class SubmitAnswerRequest|submittedAnswer|`@NotNull`|`@Valid`|`@Validated`|`@RequestBody`|submitAnswer' services/quiz-service/src/main/java services/quiz-service/src/main/resources/openapi/quiz-service.yamlRepository: ASSONDJI/afrilingua
Length of output: 3217
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the controller and generated/request model definitions
sed -n '1,120p' services/quiz-service/src/main/java/cm/afrilingua/quiz/controller/QuestionController.java
# Find the request class if it exists in source
fd -a 'SubmitAnswerRequest.java' services/quiz-service/src/main/java services/quiz-service/target services/quiz-service/src/generated || true
# Search for bean-validation annotations around the request model and controller package
rg -n --hidden -S '`@Valid`|`@Validated`|`@NotNull`|`@RequestBody`|class SubmitAnswerRequest|interface SubmitAnswerRequest' services/quiz-service/src/main/java services/quiz-service/src/generatedRepository: ASSONDJI/afrilingua
Length of output: 1887
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the API interface that QuestionController implements
sed -n '1,240p' services/quiz-service/src/main/java/cm/afrilingua/quiz/api/QuizzesApi.java
# Look for global validation/configuration in quiz-service
rg -n --hidden -S 'MethodValidationPostProcessor|`@Validated`|`@ControllerAdvice`|`@ExceptionHandler`|ConstraintViolation|Validating|BeanValidation|Validation' services/quiz-service/src/main/java services/quiz-service/src/main/resources
# Find the request DTO definition
rg -n --hidden -S 'class SubmitAnswerRequest|record SubmitAnswerRequest|interface SubmitAnswerRequest' services/quiz-service/src/main/javaRepository: ASSONDJI/afrilingua
Length of output: 275
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the generated API and DTO sources
git ls-files | rg 'services/quiz-service/.*/QuizzesApi\.java|services/quiz-service/.*/SubmitAnswerRequest\.java|services/quiz-service/.*/CreateQuestionRequest\.java|services/quiz-service/.*/AnswerResult\.java'
# Inspect the quiz-service module build/codegen setup for validation generation
sed -n '1,260p' services/quiz-service/pom.xml
# If found, inspect the request DTO source
for f in $(git ls-files | rg 'services/quiz-service/.*/SubmitAnswerRequest\.java$'); do
echo "### $f"
sed -n '1,220p' "$f"
doneRepository: ASSONDJI/afrilingua
Length of output: 157
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect quiz-service build/codegen configuration for validation generation
sed -n '1,260p' services/quiz-service/pom.xml
# Search for OpenAPI generator settings in the service and parent build files
rg -n --hidden -S 'openapi|swagger|generator|useBeanValidation|useJakartaEe|useSpringBoot3|validation|NotNull|NotBlank' \
services/quiz-service/pom.xml pom.xml .github .mvn services/quiz-service/src/main/resourcesRepository: ASSONDJI/afrilingua
Length of output: 7473
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find any generated Java sources for quiz-service outside git-tracked files
fd -a 'SubmitAnswerRequest.java' .
fd -a 'QuizzesApi.java' .
fd -a 'QuestionController.java' services/quiz-service/src/main/java
# Inspect any matching request model if present
for f in $(fd -a 'SubmitAnswerRequest.java' .); do
echo "### $f"
sed -n '1,220p' "$f"
doneRepository: ASSONDJI/afrilingua
Length of output: 266
🌐 Web query:
openapi-generator spring generator useBeanValidation default interfaceOnly @Valid required field generated model
💡 Result:
To generate models with Bean Validation (such as @NotNull for required fields) and apply @Valid annotations when using the OpenAPI Generator Spring generator, you should configure the useBeanValidation option to true [1][2][3]. Key Configuration Details: 1. Enable Bean Validation: Set the useBeanValidation configuration option to true in your generator configuration (e.g., in your pom.xml or build.gradle) [2][3][4]. This ensures that the generator includes Jakarta/Javax validation annotations (like @NotNull, @Size, etc.) on model fields based on your OpenAPI specification [3]. 2. Handling interfaceOnly: The useBeanValidation option works in conjunction with interfaceOnly. Recent updates (as of early 2026) have improved support for this, particularly within the spring-http-interface library, allowing the generator to respect your Bean Validation settings rather than forcing them to false [2][3]. 3. Applying @Valid: When useBeanValidation is enabled, the generator typically applies @Valid annotations to request bodies and controller method parameters to trigger the validation process when the request is processed by the Spring framework [2][3]. If you are using the spring-http-interface library, ensure you are on a recent version of the generator (post-April/May 2026), as previous versions incorrectly forced these validation settings to false [2][3][4]. Enabling useBeanValidation will now correctly generate the required validation annotations and include necessary dependencies (e.g., spring-boot-starter-validation) [2][3]. Example Configuration (Maven): spring true true By setting useBeanValidation to true, the generator will inspect your OpenAPI schema for required fields and automatically apply the appropriate @NotNull annotations to the corresponding fields in the generated Java models [3].
Citations:
- 1: https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/spring.md
- 2: [JAVA-SPRING] - 22859 - spring http interface library should support 'useBeanValidation' OpenAPITools/openapi-generator#23803
- 3: [Java-Spring] Enable bean validation for
spring-http-interfacewhen configured OpenAPITools/openapi-generator#23609 - 4: [BUG][JavaSpring] Spring HTTP Interface library must support useBeanValidation OpenAPITools/openapi-generator#22859
🌐 Web query:
Spring MVC interface method parameter annotations @Valid on generated OpenAPI interfaceOnly controller validation
💡 Result:
When using the OpenAPI Generator for Spring with the interfaceOnly option, support for @Valid and validation annotations depends on specific configuration flags in your plugin settings. To enable validation in generated interfaces, you must set useBeanValidation to true in your configuration options [1][2]. This ensures the generator includes necessary Jakarta Validation annotations (e.g., @NotNull, @Size) on model fields and adds @Valid to method parameters in the generated interface [3][4]. Key configuration considerations: 1. Enable Bean Validation: Set true in your configOptions [1][2]. 2. Use Jakarta EE: Because modern Spring Boot (3.x+) relies on Jakarta Validation, ensure you set true to avoid namespace conflicts (using javax vs. jakarta packages) [5]. 3. Class-Level @Validated: By default, the generator may place @Validated at the class level [6][2]. For Spring Framework 6.x and later, this is often discouraged. You can use the option true to disable this class-level annotation if you prefer to rely on Spring's built-in validation mechanism [7][2]. 4. Dependency Management: Ensure your project includes the spring-boot-starter-validation dependency [3][4]. Note that even if useBeanValidation is set to false, some generated models may still contain validation imports, which can cause compilation errors if the validation dependency is missing [8]. If validation is not working as expected, verify that you are not overriding these behaviors with conflicting model templates or additional annotations (like Lombok, which can sometimes interfere with generation) [5].
Citations:
- 1: [BUG][JavaSpring] Spring HTTP Interface library must support useBeanValidation OpenAPITools/openapi-generator#22859
- 2: https://openapi-generator.tech/docs/generators/spring/
- 3: [JAVA-SPRING] - 22859 - spring http interface library should support 'useBeanValidation' OpenAPITools/openapi-generator#23803
- 4: [Java-Spring] Enable bean validation for
spring-http-interfacewhen configured OpenAPITools/openapi-generator#23609 - 5: https://stackoverflow.com/questions/75057781/openapi-generator-maven-plugin-field-validation-required-ignored-spring
- 6: [REQ][Java][Spring] Add option to disable @Validated annotation at class level for Spring generator with useBeanValidation OpenAPITools/openapi-generator#20899
- 7: [java][spring] add useSpringBuiltInValidation option to disable @Validated at class level (fix #20899) OpenAPITools/openapi-generator#20901
- 8: [BUG][Java][Spring] useBeanValidation=false still generates jakarta.validation.constraints.NotNull OpenAPITools/openapi-generator#23751
Guard normalize() against null. submittedAnswer is only marked required in the OpenAPI schema; this path still has no explicit @Valid/@NotNull enforcement, so a missing value can reach value.trim() and turn into a 500.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@services/quiz-service/src/main/java/cm/afrilingua/quiz/service/QuestionService.java`
around lines 92 - 99, Update the private normalize method in QuestionService to
handle a null value before calling trim or Normalizer.normalize, returning the
expected null-safe result so missing submitted answers do not cause an
exception. Preserve the existing trim and NFC normalization behavior for
non-null values.
feat(quiz-service): pipeline complet de gamification (RabbitMQ)
AnswerAttempt -- la table ne trackait auparavant aucun utilisateur
que lire, jamais ecrire, malgre la table dediee)
bug reel ou des mots stockes en NFD (diacritiques combinants, frequent
sur les tons Yemba) etaient injustement marques faux
au moins une tentative correcte -- les erreurs doivent etre corrigees
avant completion, modele Duolingo) et publie lesson.completed +
quiz.completed vers RabbitMQ (exchange afrilingua.events), consommes par
le pipeline de gamification deja existant cote recommendation-service
plutot que fige a true -- integrite du badge Perfectionniste
d'idempotence, empeche de republier/re-crediter l'XP a chaque reponse
correcte soumise apres que la lecon soit deja terminee
puis correction -> XP/niveau/lessons_completed/has_perfect_quiz corrects
en base cote recommendation-service
Summary by CodeRabbit
New Features
Bug Fixes