Skip to content

이메일 인증 기능 추가 - #40

Merged
yoonjeonggg merged 2 commits into
mainfrom
feat/brevo-email-service
May 26, 2026
Merged

yoonjeonggg merged 2 commits into
mainfrom
feat/brevo-email-service

Conversation

@yoonjeonggg

@yoonjeonggg yoonjeonggg commented May 26, 2026 •

Copy link
Copy Markdown
Contributor

추가된 기능

  • 이메일 인증 발송
  • 이메일 인증 확인

brevo smtp서비스를 이용해 구현함
도메인으로는 산학 회사에서 받은 도메인 사용

Summary by CodeRabbit

New Features

  • Email verification: Added a complete email verification workflow. Users can request verification codes to be sent to their email address. Upon receiving the code, users can submit it to verify their email ownership. Verification codes automatically expire after a set time period.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 26, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

@yoonjeonggg, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 32 minutes and 43 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7c24c341-a8f6-468e-9de7-5227b5ca0965

📥 Commits

Reviewing files that changed from the base of the PR and between 5937534 and 7604238.

📒 Files selected for processing (2)
  • .github/workflows/test.yml
  • src/test/resources/application.properties
📝 Walkthrough

Walkthrough

This PR adds a complete email verification flow. It introduces request/response DTOs for email verification, a new EmailService that sends verification codes via Brevo SMTP, Redis-backed code storage in AuthService with 5-minute TTL, and HTTP POST endpoints in AuthController to initiate and verify email codes.

Changes

Email Verification Feature

Layer / File(s) Summary
Email Verification Request and Response DTOs
src/main/java/chainsawman/gesture/dto/user/request/EmailSendRequest.java, src/main/java/chainsawman/gesture/dto/user/request/EmailVerificationRequest.java, src/main/java/chainsawman/gesture/dto/user/response/EmailVerificationResponse.java
EmailSendRequest validates a non-blank email, EmailVerificationRequest validates email and 6-digit code, and EmailVerificationResponse includes verification flag, email, and ISO-formatted timestamp via Lombok and Jakarta validation annotations.
Email Service and Brevo Configuration
src/main/java/chainsawman/gesture/service/EmailService.java, src/main/resources/application-prod.properties, src/main/resources/application.properties
EmailService injects Brevo API credentials and sends verification codes via POST to Brevo's SMTP endpoint. Configuration files provide brevo.api.key and brevo.template.verification-id from environment variables.
AuthService Email Verification Methods
src/main/java/chainsawman/gesture/service/AuthService.java
sendEmailVerification generates a zero-padded 6-digit code, stores it in Redis with 5-minute TTL, and sends via EmailService. verifyEmail retrieves the code from Redis, throws ExpiredVerificationCodeException if absent or InvalidVerificationCodeException on mismatch, deletes the key on success, and returns EmailVerificationResponse with current timestamp.
Email Verification Controller Endpoints
src/main/java/chainsawman/gesture/controller/AuthController.java
Added /auth/email-send POST endpoint to send verification codes and /auth/email-verification POST endpoint to verify codes, both delegating to AuthService methods and returning standard ApiResponse wrappers.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 With Redis keys and Brevo's might,
Email codes sent left and right,
Six digits dance for verification's sake,
While timestamp blooms like lettuce break! 🥬✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '이메일 인증 기능 추가' (Add email verification feature) clearly and concisely summarizes the main change in the changeset, which implements email verification functionality including sending verification codes and verifying email codes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/brevo-email-service

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented May 26, 2026 •

Copy link
Copy Markdown

Test Results

106 tests   106 ✅  2s ⏱️
  9 suites    0 💤
  9 files      0 ❌

Results for commit 7604238.

♻️ This comment has been updated with latest results.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 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 `@src/main/java/chainsawman/gesture/service/AuthService.java`:
- Around line 232-237: The verification block in AuthService allows unlimited
OTP guesses; add attempt limiting by tracking and incrementing a per-code
attempt counter stored alongside the OTP (e.g., in the same Redis/kv store)
before validating. Define a MAX_ATTEMPTS constant (e.g., 5), increment the
counter key (e.g., verificationAttemptsKey for the same request identifier)
atomically, and if the count exceeds MAX_ATTEMPTS throw a new
TooManyAttemptsException and invalidate/delete the stored code; otherwise
proceed to compare stored.equals(request.getCode()) and reset/delete both the
code and attempts on successful verification or when expired. Ensure the new
counter respects the original TTL (set its expiry to the OTP TTL) and use the
AuthService method handling verification to locate and implement these changes.
- Around line 223-224: The code stores the verification code in Redis via
redisTemplate.opsForValue().set(key, code, VERIFICATION_TTL_MINUTES,
TimeUnit.MINUTES) before calling
emailService.sendVerificationCode(request.getEmail(), code), which can leave a
valid code in Redis if email delivery fails; change the flow so the email is
sent first and only on successful return persist the code, or if you must set
before send, wrap emailService.sendVerificationCode(...) in a try/catch and
remove the key (redisTemplate.delete(key)) on any failure (or propagate the
error) so a usable code is not left in Redis; update the logic around key, code,
VERIFICATION_TTL_MINUTES, and request.getEmail() accordingly.

In `@src/main/java/chainsawman/gesture/service/EmailService.java`:
- Line 22: The RestTemplate in EmailService is created without timeouts causing
potential thread hangs; replace the direct new RestTemplate() (field
restTemplate and any other plain RestTemplate uses around line 36) with a
RestTemplate configured via a ClientHttpRequestFactory that sets explicit
connect and read timeouts (e.g., using HttpComponentsClientHttpRequestFactory or
SimpleClientHttpRequestFactory), and initialize restTemplate with that factory
so Brevo HTTP calls have bounded connect/read timeouts (choose reasonable
timeout values like a few seconds).
- Around line 24-37: The sendVerificationCode method currently lets RestTemplate
transport and HTTP errors bubble up; wrap the call to restTemplate.postForEntity
inside a try/catch that catches HttpStatusCodeException and RestClientException
and rethrows a domain-specific unchecked exception (e.g., EmailSendException or
EmailServiceException) from EmailService with a clear message, the HTTP
status/body when available, and the original exception as the cause so API
clients receive a stable error contract; add the new exception class if missing
and update sendVerificationCode to throw it on failures.
🪄 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 Plus

Run ID: 66f8f4ba-bef0-4ff1-937d-83e1e5e2db1e

📥 Commits

Reviewing files that changed from the base of the PR and between 92e7aad and 5937534.

📒 Files selected for processing (8)
  • src/main/java/chainsawman/gesture/controller/AuthController.java
  • src/main/java/chainsawman/gesture/dto/user/request/EmailSendRequest.java
  • src/main/java/chainsawman/gesture/dto/user/request/EmailVerificationRequest.java
  • src/main/java/chainsawman/gesture/dto/user/response/EmailVerificationResponse.java
  • src/main/java/chainsawman/gesture/service/AuthService.java
  • src/main/java/chainsawman/gesture/service/EmailService.java
  • src/main/resources/application-prod.properties
  • src/main/resources/application.properties

Comment on lines +223 to +224
redisTemplate.opsForValue().set(key, code, VERIFICATION_TTL_MINUTES, TimeUnit.MINUTES);
emailService.sendVerificationCode(request.getEmail(), code);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid leaving verification codes in Redis when email delivery fails.

At Line 223-224, code is persisted before the external send. If send fails, a valid code remains without user delivery.

Proposed fix
     public void sendEmailVerification(EmailSendRequest request) {
         String code = String.format("%06d", SECURE_RANDOM.nextInt(1_000_000));
         String key = EMAIL_VERIFICATION_PREFIX + request.getEmail();
-        redisTemplate.opsForValue().set(key, code, VERIFICATION_TTL_MINUTES, TimeUnit.MINUTES);
-        emailService.sendVerificationCode(request.getEmail(), code);
+        redisTemplate.opsForValue().set(key, code, VERIFICATION_TTL_MINUTES, TimeUnit.MINUTES);
+        try {
+            emailService.sendVerificationCode(request.getEmail(), code);
+        } catch (RuntimeException e) {
+            redisTemplate.delete(key);
+            throw e;
+        }
     }
🤖 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 `@src/main/java/chainsawman/gesture/service/AuthService.java` around lines 223
- 224, The code stores the verification code in Redis via
redisTemplate.opsForValue().set(key, code, VERIFICATION_TTL_MINUTES,
TimeUnit.MINUTES) before calling
emailService.sendVerificationCode(request.getEmail(), code), which can leave a
valid code in Redis if email delivery fails; change the flow so the email is
sent first and only on successful return persist the code, or if you must set
before send, wrap emailService.sendVerificationCode(...) in a try/catch and
remove the key (redisTemplate.delete(key)) on any failure (or propagate the
error) so a usable code is not left in Redis; update the logic around key, code,
VERIFICATION_TTL_MINUTES, and request.getEmail() accordingly.

Comment on lines +232 to +237
if (stored == null) {
throw new ExpiredVerificationCodeException();
}
if (!stored.equals(request.getCode())) {
throw new InvalidVerificationCodeException();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Add attempt limiting to prevent OTP brute force.

Line 232-237 allows unlimited verification attempts for a 6-digit code during TTL. This is a security gap on an unauthenticated endpoint.

🤖 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 `@src/main/java/chainsawman/gesture/service/AuthService.java` around lines 232
- 237, The verification block in AuthService allows unlimited OTP guesses; add
attempt limiting by tracking and incrementing a per-code attempt counter stored
alongside the OTP (e.g., in the same Redis/kv store) before validating. Define a
MAX_ATTEMPTS constant (e.g., 5), increment the counter key (e.g.,
verificationAttemptsKey for the same request identifier) atomically, and if the
count exceeds MAX_ATTEMPTS throw a new TooManyAttemptsException and
invalidate/delete the stored code; otherwise proceed to compare
stored.equals(request.getCode()) and reset/delete both the code and attempts on
successful verification or when expired. Ensure the new counter respects the
original TTL (set its expiry to the OTP TTL) and use the AuthService method
handling verification to locate and implement these changes.

@Value("${brevo.template.verification-id}")
private int templateId;

private final RestTemplate restTemplate = new RestTemplate();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Configure HTTP timeouts for Brevo calls to avoid request-thread hangs.

Line 22 and Line 36 currently use a default RestTemplate without explicit connect/read timeouts. A slow/unresponsive Brevo call can block auth requests for too long.

Proposed fix
 `@Service`
 public class EmailService {
@@
-    private final RestTemplate restTemplate = new RestTemplate();
+    private final RestTemplate restTemplate;
+
+    public EmailService(RestTemplateBuilder restTemplateBuilder) {
+        this.restTemplate = restTemplateBuilder
+                .setConnectTimeout(Duration.ofSeconds(3))
+                .setReadTimeout(Duration.ofSeconds(5))
+                .build();
+    }
@@
-        restTemplate.postForEntity(BREVO_API_URL, new HttpEntity<>(body, headers), String.class);
+        restTemplate.postForEntity(BREVO_API_URL, new HttpEntity<>(body, headers), String.class);

Also applies to: 36-36

🤖 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 `@src/main/java/chainsawman/gesture/service/EmailService.java` at line 22, The
RestTemplate in EmailService is created without timeouts causing potential
thread hangs; replace the direct new RestTemplate() (field restTemplate and any
other plain RestTemplate uses around line 36) with a RestTemplate configured via
a ClientHttpRequestFactory that sets explicit connect and read timeouts (e.g.,
using HttpComponentsClientHttpRequestFactory or SimpleClientHttpRequestFactory),
and initialize restTemplate with that factory so Brevo HTTP calls have bounded
connect/read timeouts (choose reasonable timeout values like a few seconds).

Comment on lines +24 to +37
public void sendVerificationCode(String toEmail, String code) {
HttpHeaders headers = new HttpHeaders();
headers.set("api-key", apiKey);
headers.set("accept", "application/json");
headers.setContentType(MediaType.APPLICATION_JSON);

Map<String, Object> body = Map.of(
"to", List.of(Map.of("email", toEmail)),
"templateId", templateId,
"params", Map.of("code", code)
);

restTemplate.postForEntity(BREVO_API_URL, new HttpEntity<>(body, headers), String.class);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Wrap Brevo client failures into a domain exception.

Line 36 can throw transport/4xx/5xx exceptions directly. Convert them to a consistent domain error so API clients get stable error contracts.

Proposed fix
+import org.springframework.web.client.RestClientException;
@@
     public void sendVerificationCode(String toEmail, String code) {
@@
-        restTemplate.postForEntity(BREVO_API_URL, new HttpEntity<>(body, headers), String.class);
+        try {
+            restTemplate.postForEntity(BREVO_API_URL, new HttpEntity<>(body, headers), String.class);
+        } catch (RestClientException e) {
+            throw new RuntimeException("이메일 전송에 실패했습니다."); // replace with project DomainException
+        }
     }
🤖 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 `@src/main/java/chainsawman/gesture/service/EmailService.java` around lines 24
- 37, The sendVerificationCode method currently lets RestTemplate transport and
HTTP errors bubble up; wrap the call to restTemplate.postForEntity inside a
try/catch that catches HttpStatusCodeException and RestClientException and
rethrows a domain-specific unchecked exception (e.g., EmailSendException or
EmailServiceException) from EmailService with a clear message, the HTTP
status/body when available, and the original exception as the cause so API
clients receive a stable error contract; add the new exception class if missing
and update sendVerificationCode to throw it on failures.

@yoonjeonggg
yoonjeonggg merged commit f1f9f1c into main May 26, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant