Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 48 additions & 29 deletions src/main/resources/static/css/test-mypage.css
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,12 @@ body {
align-items: flex-start; /* 위쪽 정렬 (선택) */
gap: 0px; /* 두 영역 사이 간격 */
border-radius: 20px;
border: 2px solid #6c6c6c;
padding: 0px;
background-color: #0B1C17;
}

/* profile-section에 테두리와 배경, 패딩 등 원하는 스타일 추가 */
.profile-section {
background: #000;
padding: 40px 60px;
min-width: 350px; /* 적당한 최소 너비 (조절 가능) */
display: flex;
Expand Down Expand Up @@ -129,10 +128,14 @@ body {
}

/* info-row-2는 좌측정렬, 필요시 각 요소에 margin-right 부여 */
.info-row-2 {
justify-content: flex-start;
.info-row-2 { display: flex;
flex-direction: column;
align-items: flex-start;
gap: 36px;
}
.info-row-2-inner {
display: flex;align-items: center;
}

/* info-label, info-value 등 기존 스타일 유지 */
.info-label {
Expand All @@ -148,58 +151,74 @@ body {
font-weight: 700;
min-width: 80px;
}
.info-label.email-verify {
margin-left: 20px;
color: #0a1d15;
background: #00a141;
border-radius: 3px;
padding: 2px 10px;
font-size: 10px;
font-weight: 700;
}


.github-btns {
display: flex;
gap: 10px;
align-items: center;
margin-left: 40px;
}
.github-btn, .blog-btn {
background: #222;
color: #fff;
background: transparent;
border: none;
border-radius: 10px;
padding: 8px 20px;
font-size: 12px;
font-weight: 800;
cursor: pointer;
transition: background 0.2s;
flex-direction: column;
align-items: center;
justify-content: center;
display: flex;
color: white;
}
.github-btn:hover, .blog-btn:hover {
background: #00a141;
color: #0a1d15;

.github-btn-img{
width: 40px;
height: 40px;
}

.github-heatmap-section {
background: #fff;
border-radius: 10px;
min-height: 200px;
padding: 20px;
margin-top: 30px;
}
.heatmap-placeholder {
color: #6c6c6c;
text-align: center;
font-size: 18px;
padding: 60px 0;
}
.email-verify .checkmark {
display: inline-block;
width: 18px;
height: 18px;
vertical-align: middle;
/* 이메일 인증 전 텍스트 버튼처럼 보이게 */
.email-verify .email-verify {
height: 25px;
border-radius: 6px;
padding: 2px;
box-sizing: border-box;
margin-left: 10px;
/* 마우스 올릴 때 */
cursor: pointer;
transition: box-shadow 0.3s ease;
}
.email-verify .checkmark svg {
width: 100%;
height: 100%;
display: block;

.email-verify .email-verify:hover {
box-shadow: 0 0 8px rgba(0, 123, 255, 0.7); /* 파란빛 그림자 예시 */
}

.email-verify .check-image {
width: 24px;
height: 24px;
border-radius: 6px;
padding: 2px;
box-sizing: border-box;
margin-left: 10px;
/* 아무 액션 안함 */
cursor: default;
pointer-events: none; /* 클릭, 호버 무시 */
}

.social-row {
display: flex;
}
Binary file added src/main/resources/static/images/check.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/main/resources/static/images/email-verify.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
80 changes: 75 additions & 5 deletions src/main/resources/static/js/test-mypage.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,30 @@ function fetchUserData() {
const accessToken = sessionStorage.getItem('accessToken');
if (!accessToken) return;

// 1. 리뷰 토큰 조회
fetch('/api/users/review-token', {
method: 'GET',
headers: {
'Accept': 'application/json',
'Authorization': accessToken
},
credentials: 'include'
})
.then(response => {
if (!response.ok) throw new Error('유저 정보를 불러오지 못했습니다.');
return response.json();
})
.then((res) => {
// 응답 구조에 따라 아래 코드 수정
const user = res.result || res;
const aiReviewElem = document.getElementById('aiReview');
if (aiReviewElem) aiReviewElem.textContent = user.reviewToken || '-';
})
.catch(err => {
console.error(err);
});


// 1. 내 정보 조회 API 호출 (토큰을 Authorization 헤더에 넣기)
fetch('/api/users', {
method: 'GET',
Expand All @@ -37,8 +61,7 @@ function fetchUserData() {
document.getElementById('email').textContent = user.email || '';
document.getElementById('tier').textContent = user.tier || '';
document.getElementById('ranking').textContent = user.ranking || '-';
document.getElementById('solvedCount').textContent = user.solvedCount || '-';
document.getElementById('aiReview').textContent = user.aiReview || '-';
document.getElementById('solvedCount').textContent = user.totalSolvedCount || '-';
setEmailVerify(!!user.verified);

// 블로그, 깃허브 버튼 링크 처리
Expand All @@ -61,14 +84,61 @@ function fetchUserData() {
console.error(err);
// 필요시 에러 안내
});

}

// 이메일 인증 상태 표시 함수
function setEmailVerify(isVerified) {
const emailVerify = document.getElementById('emailVerify');
if (isVerified) {
emailVerify.innerHTML = `<span class="checkmark"><svg viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="10" fill="#00a141"/><path d="M6 10.5L9 13.5L14 8.5" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span>`;
emailVerify.innerHTML = `
<img src="/images/check.png" alt="인증 완료" class="check-image" />
`;
} else {
emailVerify.textContent = '-';
emailVerify.innerHTML = `
<img src="/images/email-verify.png"
alt="이메일 인증하기"
class="email-verify clickable"
id="emailVerifyButton" />
`;

// 이미지 삽입 후 클릭 이벤트 바인딩
const emailVerifyButton = document.getElementById('emailVerifyButton');
emailVerifyButton.addEventListener('click', sendEmailVerification);
}
}

//이메일 전송
async function sendEmailVerification() {
const accessToken = sessionStorage.getItem('accessToken');

if (!accessToken) {
alert('로그인이 필요합니다.');
return;
}

try {
const response = await fetch('/api/email/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': accessToken
},
body: JSON.stringify({
redirectUrl: 'http://localhost:8080'
})
});

if (response.ok) {
const result = await response.json();
alert('인증 메일이 발송되었습니다.');
console.log(result);
} else {
const error = await response.json();
alert(`오류: ${error.message || '인증 요청 실패'}`);
}
} catch (err) {
console.error('이메일 인증 오류:', err);
alert('서버 오류가 발생했습니다.');
}
}
Comment on lines +111 to 143
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

이메일 인증 전송 기능이 잘 구현되었으나 보안 및 설정 개선이 필요합니다.

API 호출 로직과 에러 처리가 적절하게 구현되어 있습니다. 다만 하드코딩된 URL과 보안 관련 개선이 필요합니다.

다음 개선사항들을 제안합니다:

+// 환경별 설정을 위한 상수
+const CONFIG = {
+    redirectUrl: window.location.origin
+};
+
 async function sendEmailVerification() {
     const accessToken = sessionStorage.getItem('accessToken');
 
     if (!accessToken) {
         alert('로그인이 필요합니다.');
         return;
     }
 
     try {
         const response = await fetch('/api/email/send', {
             method: 'POST',
             headers: {
                 'Content-Type': 'application/json',
                 'Authorization': accessToken
             },
             body: JSON.stringify({
-                redirectUrl: 'http://localhost:8080'
+                redirectUrl: CONFIG.redirectUrl
             })
         });

또한 CSRF 보안을 위한 토큰 검증을 고려해보세요.

CSRF 보안 설정이 적절히 되어 있는지 확인하기 위해 다음 스크립트를 실행해보세요:


🏁 Script executed:

#!/bin/bash
# CSRF 보안 설정 확인
rg -A 10 -B 5 "csrf" --type java

Length of output: 1925


CSRF 전역 비활성화 확인 및 redirectUrl 하드코딩 제거 필요

이메일 인증 전송 기능은 정상 동작하나, 현 코드에서는 하드코딩된 redirectUrl과 전역 CSRF 비활성화로 인한 보안 취약점이 발견되었습니다.

  • 적용 위치

    • src/main/resources/static/js/test-mypage.js (약 111–143행)
    • src/main/java/org/ezcode/codetest/common/security/config/SecurityConfig.java
  • 개선 사항

    1. redirectUrl 환경 설정화
      하드코딩된 URL 대신 런타임 호스트를 참조하도록 변경합니다.
      +// 환경별 설정을 위한 상수
      +const CONFIG = {
      +    redirectUrl: window.location.origin
      +};
       
       async function sendEmailVerification() {
           …
           const response = await fetch('/api/email/send', {
               …
               body: JSON.stringify({
      -            redirectUrl: 'http://localhost:8080'
      +            redirectUrl: CONFIG.redirectUrl
               })
           });
           …
       }
    2. CSRF 설정 재검토
      현재 SecurityConfig.java에서 .csrf(AbstractHttpConfigurer::disable())로 전역 비활성화되어 있습니다.
      • 특정 엔드포인트에만 CSRF를 비활성화하거나
      • CSRF 토큰 검증을 유지한 채 JWT 인증을 도입하거나
      • SameSite 쿠키 설정 등 추가 방어책을 적용하십시오.

위 두 가지를 반영해 보안 및 설정 유연성을 강화해주세요.

📝 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.

Suggested change
async function sendEmailVerification() {
const accessToken = sessionStorage.getItem('accessToken');
if (!accessToken) {
alert('로그인이 필요합니다.');
return;
}
try {
const response = await fetch('/api/email/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': accessToken
},
body: JSON.stringify({
redirectUrl: 'http://localhost:8080'
})
});
if (response.ok) {
const result = await response.json();
alert('인증 메일이 발송되었습니다.');
console.log(result);
} else {
const error = await response.json();
alert(`오류: ${error.message || '인증 요청 실패'}`);
}
} catch (err) {
console.error('이메일 인증 오류:', err);
alert('서버 오류가 발생했습니다.');
}
}
// 환경별 설정을 위한 상수
const CONFIG = {
redirectUrl: window.location.origin
};
async function sendEmailVerification() {
const accessToken = sessionStorage.getItem('accessToken');
if (!accessToken) {
alert('로그인이 필요합니다.');
return;
}
try {
const response = await fetch('/api/email/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': accessToken
},
body: JSON.stringify({
redirectUrl: CONFIG.redirectUrl
})
});
if (response.ok) {
const result = await response.json();
alert('인증 메일이 발송되었습니다.');
console.log(result);
} else {
const error = await response.json();
alert(`오류: ${error.message || '인증 요청 실패'}`);
}
} catch (err) {
console.error('이메일 인증 오류:', err);
alert('서버 오류가 발생했습니다.');
}
}
🤖 Prompt for AI Agents
In src/main/resources/static/js/test-mypage.js around lines 111 to 143, replace
the hardcoded redirectUrl with a dynamic value derived from the current runtime
host, such as using window.location.origin, to avoid fixed URLs. Additionally,
in src/main/java/org/ezcode/codetest/common/security/config/SecurityConfig.java,
revise the global CSRF disablement by either enabling CSRF protection globally
and selectively disabling it only for specific endpoints like the email API, or
implement JWT authentication with CSRF tokens maintained, or apply other
mitigations like SameSite cookie attributes to enhance security while preserving
necessary CSRF defenses.


75 changes: 43 additions & 32 deletions src/main/resources/templates/test-mypage.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,40 +22,51 @@
</aside>
<main class="main-content">
<section class="user-infos">
<section class="profile-section">
<div class="profile-img">
<!-- 프로필 이미지: DB에 없으면 기본 이미지로 대체 -->
<img src="/images/logo.png" alt="프로필 이미지" id="profileImage">
</div>
<div class="nickname" id="nickname">ezcode</div>
</section>
<section class="info-section">
<div class="info-row info-row-1">
<div class="info-label">랭킹</div>
<div class="info-value" id="ranking">7th</div>
<div class="info-label">푼 문제 수</div>
<div class="info-value" id="solvedCount">134,452,565 문제</div>
<div class="info-label">남은 AI 리뷰 횟수</div>
<div class="info-value" id="aiReview">5번</div>
</div>
<div class="info-row info-row-2">
<div class="info-label">티어</div>
<div class="info-value" id="tier">-</div>
<div class="info-label">아이디</div>
<div class="info-value" id="userId">-</div>
<div class="info-label">이메일</div>
<div class="info-value" id="email">-</div>
<div class="info-label email-verify">
<span id="emailVerify">이메일 인증</span>
<section class="profile-section">
<div class="profile-img">
<!-- 프로필 이미지: DB에 없으면 기본 이미지로 대체 -->
<img src="/images/logo.png" alt="프로필 이미지" id="profileImage">
</div>
</div>
<div class="info-row github-row">
<div class="github-btns">
<button class="github-btn">github</button>
<button class="blog-btn">blog</button>
<div class="info-value" id="nickname">닉네임</div>
</section>
<section class="info-section">
<div class="info-row info-row-1">
<div class="info-label">랭킹</div>
<div class="info-value" id="ranking">7th</div>
<div class="info-label" >푼 문제 수</div>
<div class="info-value" id="solvedCount">문제</div>
<div class="info-label">남은 AI 리뷰 횟수</div>
<div class="info-value" id="aiReview">-</div>
</div>
</div>
</section>
<div class="info-row info-row-2">
<div class="info-row-2-inner">
<div class="info-label">티어</div>
<div class="info-value" id="tier">-</div>
</div>
<div class="info-row-2-inner">
<div class="info-label">아이디</div>
<div class="info-value" id="userId">-</div>
</div>
<div class="info-row-2-inner">
<div class="info-label">이메일</div>
<div class="info-value" id="email">-</div>
<div class="info-label email-verify">
<span id="emailVerify" class="verify-text">이메일 인증</span>
</div>
</div>


</div>
<div class="social-row">
<div class="github-btns">
<button class="github-btn">
<img src="/images/githubIcon.png" class = "github-btn-img" alt="github 이미지"/>
github
</button>
<button class="blog-btn">blog</button>
</div>
</div>
</section>
</section>
<section class="github-heatmap-section">
<!-- 깃허브 히트맵: DB 연동시 색상 하이라이트 처리 -->
Expand Down