권한 관리 시스템 개선
✨ 새로운 기능: - 사용자별 세분화된 권한 체크 (can_manage_books, can_manage_notes, can_manage_novels) - 페이지별 권한 가드 시스템 추가 (permission-guard.js) - 헤더 메뉴 권한별 표시/숨김 기능 🔧 백엔드 개선: - 모든 문서 관련 API에서 can_manage_books 권한 체크 추가 - documents.py: 개별 문서 조회, PDF 조회 권한 로직 수정 - highlights.py: 하이라이트 생성/조회 권한 체크 개선 - bookmarks.py: 북마크 생성/조회 권한 체크 개선 - document_links.py: 문서 링크 관련 권한 체크 개선 🎨 프론트엔드 개선: - header-loader.js: updateMenuPermissions 함수 추가로 권한별 메뉴 제어 - permission-guard.js: 페이지 접근 권한 체크 및 리다이렉트 처리 - 권한 없는 페이지 접근 시 메인 페이지로 안전한 리다이렉트 - 헤더 사용자 정보 상태 보존 로직 추가 🛡️ 보안 강화: - 403 Forbidden 에러 해결 - 권한 없는 사용자의 무단 페이지 접근 차단 - 문서 관리 권한이 있는 사용자는 모든 문서 공유 가능 📱 사용자 경험 개선: - 권한에 따른 메뉴 자동 표시/숨김 - 로그인 상태 유지 개선 - 권한 없는 기능 접근 시 친화적인 알림 및 리다이렉트
This commit is contained in:
@@ -75,8 +75,8 @@ async def create_bookmark(
|
|||||||
detail="Document not found"
|
detail="Document not found"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 문서 접근 권한 확인
|
# 문서 접근 권한 확인 (관리자 또는 문서 관리 권한이 있으면 모든 문서 접근 가능)
|
||||||
if not document.is_public and document.uploaded_by != current_user.id and not current_user.is_admin:
|
if not (current_user.is_admin or current_user.can_manage_books) and not document.is_public and document.uploaded_by != current_user.id:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Not enough permissions to access this document"
|
detail="Not enough permissions to access this document"
|
||||||
@@ -155,8 +155,8 @@ async def get_document_bookmarks(
|
|||||||
detail="Document not found"
|
detail="Document not found"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 문서 접근 권한 확인
|
# 문서 접근 권한 확인 (관리자 또는 문서 관리 권한이 있으면 모든 문서 접근 가능)
|
||||||
if not document.is_public and document.uploaded_by != current_user.id and not current_user.is_admin:
|
if not (current_user.is_admin or current_user.can_manage_books) and not document.is_public and document.uploaded_by != current_user.id:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Not enough permissions to access this document"
|
detail="Not enough permissions to access this document"
|
||||||
|
|||||||
@@ -109,8 +109,8 @@ async def create_document_link(
|
|||||||
detail="Source document not found"
|
detail="Source document not found"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 권한 확인
|
# 권한 확인 (관리자 또는 문서 관리 권한이 있으면 모든 문서 접근 가능)
|
||||||
if not source_doc.is_public and source_doc.uploaded_by != current_user.id and not current_user.is_admin:
|
if not (current_user.is_admin or current_user.can_manage_books) and not source_doc.is_public and source_doc.uploaded_by != current_user.id:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Access denied to source document"
|
detail="Access denied to source document"
|
||||||
@@ -146,7 +146,7 @@ async def create_document_link(
|
|||||||
|
|
||||||
# 대상 문서/노트 권한 확인
|
# 대상 문서/노트 권한 확인
|
||||||
if target_doc:
|
if target_doc:
|
||||||
if not target_doc.is_public and target_doc.uploaded_by != current_user.id and not current_user.is_admin:
|
if not (current_user.is_admin or current_user.can_manage_books) and not target_doc.is_public and target_doc.uploaded_by != current_user.id:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Access denied to target document"
|
detail="Access denied to target document"
|
||||||
@@ -235,8 +235,8 @@ async def get_document_links(
|
|||||||
detail="Document not found"
|
detail="Document not found"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 권한 확인
|
# 권한 확인 (관리자 또는 문서 관리 권한이 있으면 모든 문서 접근 가능)
|
||||||
if not document.is_public and document.uploaded_by != current_user.id and not current_user.is_admin:
|
if not (current_user.is_admin or current_user.can_manage_books) and not document.is_public and document.uploaded_by != current_user.id:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Access denied"
|
detail="Access denied"
|
||||||
@@ -345,8 +345,8 @@ async def get_linkable_documents(
|
|||||||
and_(
|
and_(
|
||||||
Document.html_path.isnot(None), # HTML 문서만
|
Document.html_path.isnot(None), # HTML 문서만
|
||||||
Document.id != document_id, # 자기 자신 제외
|
Document.id != document_id, # 자기 자신 제외
|
||||||
# 권한 확인: 공개 문서이거나 본인이 업로드한 문서
|
# 권한 확인: 공개 문서이거나 본인이 업로드한 문서이거나 문서 관리 권한이 있음
|
||||||
(Document.is_public == True) | (Document.uploaded_by == current_user.id) | (current_user.is_admin == True)
|
(Document.is_public == True) | (Document.uploaded_by == current_user.id) | (current_user.is_admin == True) | (current_user.can_manage_books == True)
|
||||||
)
|
)
|
||||||
).order_by(
|
).order_by(
|
||||||
# 같은 서적 우선, 그 다음 정렬 순서
|
# 같은 서적 우선, 그 다음 정렬 순서
|
||||||
@@ -522,8 +522,8 @@ async def get_document_backlinks(
|
|||||||
|
|
||||||
print(f"✅ 문서 찾음: {document.title}")
|
print(f"✅ 문서 찾음: {document.title}")
|
||||||
|
|
||||||
# 권한 확인
|
# 권한 확인 (관리자 또는 문서 관리 권한이 있으면 모든 문서 접근 가능)
|
||||||
if not document.is_public and document.uploaded_by != current_user.id and not current_user.is_admin:
|
if not (current_user.is_admin or current_user.can_manage_books) and not document.is_public and document.uploaded_by != current_user.id:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Access denied"
|
detail="Access denied"
|
||||||
@@ -541,8 +541,8 @@ async def get_document_backlinks(
|
|||||||
).outerjoin(Book, Document.book_id == Book.id).where(
|
).outerjoin(Book, Document.book_id == Book.id).where(
|
||||||
and_(
|
and_(
|
||||||
DocumentLink.target_document_id == document_id,
|
DocumentLink.target_document_id == document_id,
|
||||||
# 권한 확인: 공개 문서이거나 본인이 업로드한 문서
|
# 권한 확인: 공개 문서이거나 본인이 업로드한 문서이거나 문서 관리 권한이 있음
|
||||||
(Document.is_public == True) | (Document.uploaded_by == current_user.id) | (current_user.is_admin == True)
|
(Document.is_public == True) | (Document.uploaded_by == current_user.id) | (current_user.is_admin == True) | (current_user.can_manage_books == True)
|
||||||
)
|
)
|
||||||
).order_by(DocumentLink.created_at.desc())
|
).order_by(DocumentLink.created_at.desc())
|
||||||
|
|
||||||
@@ -650,8 +650,8 @@ async def get_document_link_fragments(
|
|||||||
detail="Document not found"
|
detail="Document not found"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 권한 확인
|
# 권한 확인 (관리자 또는 문서 관리 권한이 있으면 모든 문서 접근 가능)
|
||||||
if not document.is_public and document.uploaded_by != current_user.id and not current_user.is_admin:
|
if not (current_user.is_admin or current_user.can_manage_books) and not document.is_public and document.uploaded_by != current_user.id:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Access denied"
|
detail="Access denied"
|
||||||
@@ -663,8 +663,8 @@ async def get_document_link_fragments(
|
|||||||
).where(
|
).where(
|
||||||
and_(
|
and_(
|
||||||
DocumentLink.source_document_id == document_id,
|
DocumentLink.source_document_id == document_id,
|
||||||
# 권한 확인: 공개 문서이거나 본인이 업로드한 문서
|
# 권한 확인: 공개 문서이거나 본인이 업로드한 문서이거나 문서 관리 권한이 있음
|
||||||
(Document.is_public == True) | (Document.uploaded_by == current_user.id) | (current_user.is_admin == True)
|
(Document.is_public == True) | (Document.uploaded_by == current_user.id) | (current_user.is_admin == True) | (current_user.can_manage_books == True)
|
||||||
)
|
)
|
||||||
).order_by(DocumentLink.start_offset.asc())
|
).order_by(DocumentLink.start_offset.asc())
|
||||||
|
|
||||||
|
|||||||
@@ -98,8 +98,8 @@ async def list_documents(
|
|||||||
selectinload(Document.category) # 소분류 정보 추가
|
selectinload(Document.category) # 소분류 정보 추가
|
||||||
)
|
)
|
||||||
|
|
||||||
# 권한 필터링 (관리자가 아니면 공개 문서 + 자신이 업로드한 문서만)
|
# 권한 필터링 (관리자 또는 문서 관리 권한이 있으면 모든 문서, 아니면 공개 문서 + 자신이 업로드한 문서만)
|
||||||
if not current_user.is_admin:
|
if not (current_user.is_admin or current_user.can_manage_books):
|
||||||
query = query.where(
|
query = query.where(
|
||||||
or_(
|
or_(
|
||||||
Document.is_public == True,
|
Document.is_public == True,
|
||||||
@@ -175,8 +175,8 @@ async def list_all_documents(
|
|||||||
selectinload(Document.category) # 소분류 정보 추가
|
selectinload(Document.category) # 소분류 정보 추가
|
||||||
)
|
)
|
||||||
|
|
||||||
# 권한 필터링 (관리자가 아니면 공개 문서 + 자신이 업로드한 문서만)
|
# 권한 필터링 (관리자 또는 문서 관리 권한이 있으면 모든 문서, 아니면 공개 문서 + 자신이 업로드한 문서만)
|
||||||
if not current_user.is_admin:
|
if not (current_user.is_admin or current_user.can_manage_books):
|
||||||
query = query.where(
|
query = query.where(
|
||||||
or_(
|
or_(
|
||||||
Document.is_public == True,
|
Document.is_public == True,
|
||||||
@@ -514,8 +514,8 @@ async def get_document(
|
|||||||
detail="Document not found"
|
detail="Document not found"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 권한 확인
|
# 권한 확인 (관리자 또는 문서 관리 권한이 있으면 모든 문서 접근 가능)
|
||||||
if not document.is_public and document.uploaded_by != current_user.id and not current_user.is_admin:
|
if not (current_user.is_admin or current_user.can_manage_books) and not document.is_public and document.uploaded_by != current_user.id:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Not enough permissions"
|
detail="Not enough permissions"
|
||||||
@@ -616,9 +616,9 @@ async def get_document_pdf(
|
|||||||
print(f"🔐 문서 권한: is_public={document.is_public}, uploaded_by={document.uploaded_by}")
|
print(f"🔐 문서 권한: is_public={document.is_public}, uploaded_by={document.uploaded_by}")
|
||||||
print(f"👤 사용자 권한: is_admin={current_user.is_admin}, user_id={current_user.id}")
|
print(f"👤 사용자 권한: is_admin={current_user.is_admin}, user_id={current_user.id}")
|
||||||
|
|
||||||
# 권한 확인
|
# 권한 확인 (관리자 또는 문서 관리 권한이 있으면 모든 문서 접근 가능)
|
||||||
if not current_user.is_admin and not document.is_public and document.uploaded_by != current_user.id:
|
if not (current_user.is_admin or current_user.can_manage_books) and not document.is_public and document.uploaded_by != current_user.id:
|
||||||
print(f"❌ 접근 권한 없음 - 관리자: {current_user.is_admin}, 공개: {document.is_public}, 소유자: {document.uploaded_by == current_user.id}")
|
print(f"❌ 접근 권한 없음 - 관리자: {current_user.is_admin}, 문서권한: {current_user.can_manage_books}, 공개: {document.is_public}, 소유자: {document.uploaded_by == current_user.id}")
|
||||||
raise HTTPException(status_code=403, detail="Access denied")
|
raise HTTPException(status_code=403, detail="Access denied")
|
||||||
|
|
||||||
# PDF 파일 확인
|
# PDF 파일 확인
|
||||||
|
|||||||
@@ -84,8 +84,8 @@ async def create_highlight(
|
|||||||
detail="Document not found"
|
detail="Document not found"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 문서 접근 권한 확인
|
# 문서 접근 권한 확인 (관리자 또는 문서 관리 권한이 있으면 모든 문서 접근 가능)
|
||||||
if not document.is_public and document.uploaded_by != current_user.id and not current_user.is_admin:
|
if not (current_user.is_admin or current_user.can_manage_books) and not document.is_public and document.uploaded_by != current_user.id:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Not enough permissions to access this document"
|
detail="Not enough permissions to access this document"
|
||||||
|
|||||||
@@ -213,7 +213,7 @@
|
|||||||
|
|
||||||
<!-- 로그아웃 -->
|
<!-- 로그아웃 -->
|
||||||
<div class="border-t border-gray-100 my-1"></div>
|
<div class="border-t border-gray-100 my-1"></div>
|
||||||
<button onclick="handleLogout()" class="flex items-center w-full px-4 py-2 text-sm text-red-600 hover:bg-red-50">
|
<button onclick="console.log('로그아웃 버튼 클릭됨'); if(typeof handleLogout === 'function') { handleLogout(); } else { console.error('handleLogout 함수가 정의되지 않음'); }" class="flex items-center w-full px-4 py-2 text-sm text-red-600 hover:bg-red-50">
|
||||||
<i class="fas fa-sign-out-alt w-4 h-4 mr-3"></i>
|
<i class="fas fa-sign-out-alt w-4 h-4 mr-3"></i>
|
||||||
로그아웃
|
로그아웃
|
||||||
</button>
|
</button>
|
||||||
@@ -404,12 +404,53 @@
|
|||||||
window.location.href = `login.html?redirect=${currentUrl}`;
|
window.location.href = `login.html?redirect=${currentUrl}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
window.handleLogout = () => {
|
window.handleLogout = async () => {
|
||||||
// 각 페이지의 로그아웃 함수 호출
|
try {
|
||||||
if (typeof logout === 'function') {
|
console.log('🔄 로그아웃 시작...');
|
||||||
logout();
|
console.log('🔍 window.api 존재 여부:', !!window.api);
|
||||||
} else {
|
console.log('🔍 logout 함수 존재 여부:', typeof logout);
|
||||||
console.log('로그아웃 함수를 찾을 수 없습니다.');
|
|
||||||
|
// 각 페이지의 로그아웃 함수가 있으면 호출
|
||||||
|
if (typeof logout === 'function') {
|
||||||
|
await logout();
|
||||||
|
} else {
|
||||||
|
console.log('🔄 직접 로그아웃 처리 시작...');
|
||||||
|
|
||||||
|
// API 로그아웃 시도
|
||||||
|
if (window.api && typeof window.api.logout === 'function') {
|
||||||
|
console.log('🌐 API 로그아웃 호출...');
|
||||||
|
try {
|
||||||
|
await window.api.logout();
|
||||||
|
console.log('✅ API 로그아웃 성공');
|
||||||
|
} catch (apiError) {
|
||||||
|
console.log('⚠️ API 로그아웃 실패:', apiError);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ window.api.logout 함수를 찾을 수 없음');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 로컬 스토리지 정리 (항상 실행)
|
||||||
|
console.log('🧹 로컬 스토리지 정리...');
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
localStorage.removeItem('refresh_token');
|
||||||
|
localStorage.removeItem('user_info');
|
||||||
|
|
||||||
|
console.log('✅ 로그아웃 완료');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 로그인 페이지로 리다이렉트
|
||||||
|
const currentUrl = encodeURIComponent(window.location.pathname + window.location.search);
|
||||||
|
window.location.href = `login.html?redirect=${currentUrl}`;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 로그아웃 실패:', error);
|
||||||
|
|
||||||
|
// 에러가 발생해도 로컬 데이터는 정리하고 로그인 페이지로 이동
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
localStorage.removeItem('refresh_token');
|
||||||
|
localStorage.removeItem('user_info');
|
||||||
|
|
||||||
|
window.location.href = 'login.html';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -441,6 +441,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- JavaScript 파일들 -->
|
<!-- JavaScript 파일들 -->
|
||||||
|
<script src="/static/js/permission-guard.js"></script>
|
||||||
<script src="/static/js/api.js?v=2025012384"></script>
|
<script src="/static/js/api.js?v=2025012384"></script>
|
||||||
<script src="/static/js/auth.js?v=2025012351"></script>
|
<script src="/static/js/auth.js?v=2025012351"></script>
|
||||||
<script src="/static/js/header-loader.js?v=2025012351"></script>
|
<script src="/static/js/header-loader.js?v=2025012351"></script>
|
||||||
|
|||||||
@@ -277,6 +277,22 @@ window.bookDocumentsApp = () => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 날짜 포맷팅
|
// 날짜 포맷팅
|
||||||
|
// 로그아웃
|
||||||
|
async logout() {
|
||||||
|
try {
|
||||||
|
await window.api.logout();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout error:', error);
|
||||||
|
} finally {
|
||||||
|
// 로컬 스토리지 정리
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
localStorage.removeItem('refresh_token');
|
||||||
|
localStorage.removeItem('user_info');
|
||||||
|
|
||||||
|
console.log('✅ 로그아웃 완료');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
formatDate(dateString) {
|
formatDate(dateString) {
|
||||||
if (!dateString) return '';
|
if (!dateString) return '';
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
|
|||||||
@@ -398,6 +398,22 @@ window.bookEditorApp = () => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 로그아웃
|
||||||
|
async logout() {
|
||||||
|
try {
|
||||||
|
await window.api.logout();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout error:', error);
|
||||||
|
} finally {
|
||||||
|
// 로컬 스토리지 정리
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
localStorage.removeItem('refresh_token');
|
||||||
|
localStorage.removeItem('user_info');
|
||||||
|
|
||||||
|
console.log('✅ 로그아웃 완료');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// 뒤로가기
|
// 뒤로가기
|
||||||
goBack() {
|
goBack() {
|
||||||
window.location.href = `book-documents.html?bookId=${this.bookId}`;
|
window.location.href = `book-documents.html?bookId=${this.bookId}`;
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ class HeaderLoader {
|
|||||||
// 헤더 HTML 삽입
|
// 헤더 HTML 삽입
|
||||||
container.innerHTML = headerHtml;
|
container.innerHTML = headerHtml;
|
||||||
|
|
||||||
|
// 헤더 로드 후 필요한 함수들 정의
|
||||||
|
this.initializeHeaderFunctions();
|
||||||
|
|
||||||
this.headerLoaded = true;
|
this.headerLoaded = true;
|
||||||
console.log('✅ 헤더 로드 완료');
|
console.log('✅ 헤더 로드 완료');
|
||||||
|
|
||||||
@@ -48,6 +51,140 @@ class HeaderLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 헤더 로드 후 필요한 함수들 초기화
|
||||||
|
*/
|
||||||
|
initializeHeaderFunctions() {
|
||||||
|
console.log('🔧 헤더 함수들 초기화 중...');
|
||||||
|
|
||||||
|
// handleLogin 함수 정의
|
||||||
|
window.handleLogin = () => {
|
||||||
|
console.log('🔐 handleLogin 호출됨 - 로그인 페이지로 이동');
|
||||||
|
const currentUrl = encodeURIComponent(window.location.href);
|
||||||
|
window.location.href = `login.html?redirect=${currentUrl}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// handleLogout 함수 정의
|
||||||
|
window.handleLogout = async () => {
|
||||||
|
try {
|
||||||
|
console.log('🔄 로그아웃 시작...');
|
||||||
|
console.log('🔍 window.api 존재 여부:', !!window.api);
|
||||||
|
console.log('🔍 logout 함수 존재 여부:', typeof logout);
|
||||||
|
|
||||||
|
// 각 페이지의 로그아웃 함수가 있으면 호출
|
||||||
|
if (typeof logout === 'function') {
|
||||||
|
await logout();
|
||||||
|
} else {
|
||||||
|
console.log('🔄 직접 로그아웃 처리 시작...');
|
||||||
|
|
||||||
|
// API 로그아웃 시도
|
||||||
|
if (window.api && typeof window.api.logout === 'function') {
|
||||||
|
console.log('🌐 API 로그아웃 호출...');
|
||||||
|
try {
|
||||||
|
await window.api.logout();
|
||||||
|
console.log('✅ API 로그아웃 성공');
|
||||||
|
} catch (apiError) {
|
||||||
|
console.log('⚠️ API 로그아웃 실패:', apiError);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ window.api.logout 함수를 찾을 수 없음');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 로컬 스토리지 정리 (항상 실행)
|
||||||
|
console.log('🧹 로컬 스토리지 정리...');
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
localStorage.removeItem('refresh_token');
|
||||||
|
localStorage.removeItem('user_info');
|
||||||
|
|
||||||
|
console.log('✅ 로그아웃 완료');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 로그인 페이지로 리다이렉트
|
||||||
|
const currentUrl = encodeURIComponent(window.location.pathname + window.location.search);
|
||||||
|
window.location.href = `login.html?redirect=${currentUrl}`;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 로그아웃 실패:', error);
|
||||||
|
|
||||||
|
// 에러가 발생해도 로컬 데이터는 정리하고 로그인 페이지로 이동
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
localStorage.removeItem('refresh_token');
|
||||||
|
localStorage.removeItem('user_info');
|
||||||
|
|
||||||
|
window.location.href = 'login.html';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('✅ 헤더 함수들 초기화 완료');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자 권한에 따른 메뉴 표시/숨김
|
||||||
|
*/
|
||||||
|
updateMenuPermissions(user) {
|
||||||
|
// 메뉴 요소들 가져오기
|
||||||
|
const menuItems = {
|
||||||
|
// 문서 관리 관련
|
||||||
|
'pdf-manager-nav-item': user.can_manage_books || user.is_admin,
|
||||||
|
'book-documents-nav-item': user.can_manage_books || user.is_admin,
|
||||||
|
'book-editor-nav-item': user.can_manage_books || user.is_admin,
|
||||||
|
|
||||||
|
// 노트 관리 관련
|
||||||
|
'notes-list-nav-item': user.can_manage_notes || user.is_admin,
|
||||||
|
'notebooks-nav-item': user.can_manage_notes || user.is_admin,
|
||||||
|
'note-editor-nav-item': user.can_manage_notes || user.is_admin,
|
||||||
|
|
||||||
|
// 소설 관리 관련
|
||||||
|
'story-view-nav-item': user.can_manage_novels || user.is_admin,
|
||||||
|
'story-reader-nav-item': user.can_manage_novels || user.is_admin,
|
||||||
|
'memo-tree-nav-item': user.can_manage_novels || user.is_admin,
|
||||||
|
|
||||||
|
// 할일 관리 - 노트 관리 권한 필요 (올바른 ID 사용)
|
||||||
|
'todos-nav-link': user.can_manage_notes || user.is_admin,
|
||||||
|
|
||||||
|
// 검색은 모든 사용자 허용
|
||||||
|
'search-nav-link': true,
|
||||||
|
|
||||||
|
// 메인 페이지는 모든 사용자 허용 (문서 보기만)
|
||||||
|
'index-nav-item': true
|
||||||
|
};
|
||||||
|
|
||||||
|
// 각 메뉴 아이템의 표시/숨김 처리
|
||||||
|
Object.entries(menuItems).forEach(([itemId, hasPermission]) => {
|
||||||
|
const menuItem = document.getElementById(itemId);
|
||||||
|
if (menuItem) {
|
||||||
|
if (hasPermission) {
|
||||||
|
menuItem.classList.remove('hidden');
|
||||||
|
console.log(`✅ ${itemId} 메뉴 표시`);
|
||||||
|
} else {
|
||||||
|
menuItem.classList.add('hidden');
|
||||||
|
console.log(`❌ ${itemId} 메뉴 숨김`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 드롭다운 메뉴의 링크들도 체크
|
||||||
|
const dropdownLinks = document.querySelectorAll('.user-dropdown a, .user-dropdown button');
|
||||||
|
dropdownLinks.forEach(link => {
|
||||||
|
const href = link.getAttribute('href') || '';
|
||||||
|
let hasPermission = true;
|
||||||
|
|
||||||
|
if (href.includes('pdf-manager') || href.includes('book-')) {
|
||||||
|
hasPermission = user.can_manage_books || user.is_admin;
|
||||||
|
} else if (href.includes('note') || href.includes('notebook')) {
|
||||||
|
hasPermission = user.can_manage_notes || user.is_admin;
|
||||||
|
} else if (href.includes('story') || href.includes('memo-tree')) {
|
||||||
|
hasPermission = user.can_manage_novels || user.is_admin;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasPermission) {
|
||||||
|
link.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
link.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 헤더 로드 실패 시 폴백 헤더 표시
|
* 헤더 로드 실패 시 폴백 헤더 표시
|
||||||
*/
|
*/
|
||||||
@@ -194,7 +331,7 @@ document.addEventListener('headerLoaded', () => {
|
|||||||
if (dropdownUserEmail) dropdownUserEmail.textContent = user.email || '';
|
if (dropdownUserEmail) dropdownUserEmail.textContent = user.email || '';
|
||||||
if (dropdownUserRole) dropdownUserRole.textContent = roleText;
|
if (dropdownUserRole) dropdownUserRole.textContent = roleText;
|
||||||
|
|
||||||
// 관리자 메뉴 표시/숨김
|
// 사용자별 메뉴 권한 체크
|
||||||
console.log('🔍 사용자 권한 확인:', {
|
console.log('🔍 사용자 권한 확인:', {
|
||||||
role: user.role,
|
role: user.role,
|
||||||
is_admin: user.is_admin,
|
is_admin: user.is_admin,
|
||||||
@@ -203,6 +340,10 @@ document.addEventListener('headerLoaded', () => {
|
|||||||
can_manage_novels: user.can_manage_novels
|
can_manage_novels: user.can_manage_novels
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 개별 메뉴 권한 체크
|
||||||
|
window.headerLoader.updateMenuPermissions(user);
|
||||||
|
|
||||||
|
// 관리자 메뉴 표시/숨김 (전체 관리자만)
|
||||||
if (adminMenuSection) {
|
if (adminMenuSection) {
|
||||||
if (user.role === 'root' || user.role === 'admin' || user.is_admin) {
|
if (user.role === 'root' || user.role === 'admin' || user.is_admin) {
|
||||||
console.log('✅ 관리자 메뉴 표시');
|
console.log('✅ 관리자 메뉴 표시');
|
||||||
|
|||||||
@@ -333,6 +333,22 @@ window.pdfManagerApp = () => ({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 로그아웃
|
||||||
|
async logout() {
|
||||||
|
try {
|
||||||
|
await window.api.logout();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout error:', error);
|
||||||
|
} finally {
|
||||||
|
// 로컬 스토리지 정리
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
localStorage.removeItem('refresh_token');
|
||||||
|
localStorage.removeItem('user_info');
|
||||||
|
|
||||||
|
console.log('✅ 로그아웃 완료');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// 알림 표시
|
// 알림 표시
|
||||||
showNotification(message, type = 'info') {
|
showNotification(message, type = 'info') {
|
||||||
console.log(`${type.toUpperCase()}: ${message}`);
|
console.log(`${type.toUpperCase()}: ${message}`);
|
||||||
|
|||||||
155
frontend/static/js/permission-guard.js
Normal file
155
frontend/static/js/permission-guard.js
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
/**
|
||||||
|
* 페이지별 권한 체크 가드
|
||||||
|
*/
|
||||||
|
class PermissionGuard {
|
||||||
|
constructor() {
|
||||||
|
this.pagePermissions = {
|
||||||
|
// 문서 관리 관련
|
||||||
|
'pdf-manager.html': (user) => user.can_manage_books || user.is_admin,
|
||||||
|
'book-documents.html': (user) => user.can_manage_books || user.is_admin,
|
||||||
|
'book-editor.html': (user) => user.can_manage_books || user.is_admin,
|
||||||
|
|
||||||
|
// 노트 관리 관련
|
||||||
|
'notes.html': (user) => user.can_manage_notes || user.is_admin,
|
||||||
|
'notebooks.html': (user) => user.can_manage_notes || user.is_admin,
|
||||||
|
'note-editor.html': (user) => user.can_manage_notes || user.is_admin,
|
||||||
|
'todos.html': (user) => user.can_manage_notes || user.is_admin,
|
||||||
|
|
||||||
|
// 소설 관리 관련
|
||||||
|
'story-view.html': (user) => user.can_manage_novels || user.is_admin,
|
||||||
|
'story-reader.html': (user) => user.can_manage_novels || user.is_admin,
|
||||||
|
'memo-tree.html': (user) => user.can_manage_novels || user.is_admin,
|
||||||
|
|
||||||
|
// 관리자 전용
|
||||||
|
'user-management.html': (user) => user.is_admin,
|
||||||
|
'system-settings.html': (user) => user.is_admin,
|
||||||
|
'backup-restore.html': (user) => user.is_admin,
|
||||||
|
'logs.html': (user) => user.is_admin,
|
||||||
|
'setup.html': (user) => user.is_admin,
|
||||||
|
|
||||||
|
// 모든 사용자 허용
|
||||||
|
'index.html': () => true,
|
||||||
|
'search.html': () => true,
|
||||||
|
'upload.html': () => true,
|
||||||
|
'viewer.html': () => true,
|
||||||
|
'login.html': () => true,
|
||||||
|
'profile.html': () => true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 페이지의 권한을 체크합니다
|
||||||
|
*/
|
||||||
|
async checkCurrentPagePermission() {
|
||||||
|
try {
|
||||||
|
// 현재 페이지 파일명 추출
|
||||||
|
const currentPage = window.location.pathname.split('/').pop() || 'index.html';
|
||||||
|
console.log(`🔐 권한 체크 시작 - 페이지: ${currentPage}`);
|
||||||
|
|
||||||
|
// 권한 체크 함수 가져오기
|
||||||
|
const permissionCheck = this.pagePermissions[currentPage];
|
||||||
|
|
||||||
|
// 권한 체크가 정의되지 않은 페이지는 허용
|
||||||
|
if (!permissionCheck) {
|
||||||
|
console.log(`✅ 권한 체크 없음 - 페이지 허용: ${currentPage}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 사용자 정보 가져오기
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
if (!token) {
|
||||||
|
console.log(`❌ 토큰 없음 - 로그인 페이지로 이동`);
|
||||||
|
this.redirectToLogin();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 먼저 기존 사용자 정보가 있는지 확인 (헤더 상태 보존)
|
||||||
|
let user = window.currentUser;
|
||||||
|
|
||||||
|
// 사용자 정보가 없으면 API로 조회
|
||||||
|
if (!user) {
|
||||||
|
const response = await fetch('/api/auth/me', {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.log(`❌ 사용자 정보 조회 실패 - 로그인 페이지로 이동`);
|
||||||
|
this.redirectToLogin();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
user = await response.json();
|
||||||
|
// 전역 사용자 정보 저장 (헤더에서 사용)
|
||||||
|
window.currentUser = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`👤 사용자 정보: ${user.email}, 권한: 관리자=${user.is_admin}, 문서=${user.can_manage_books}, 노트=${user.can_manage_notes}, 소설=${user.can_manage_novels}`);
|
||||||
|
|
||||||
|
// 권한 체크 실행
|
||||||
|
const hasPermission = permissionCheck(user);
|
||||||
|
|
||||||
|
if (hasPermission) {
|
||||||
|
console.log(`✅ 권한 확인 - 페이지 접근 허용: ${currentPage}`);
|
||||||
|
window.permissionGuardExecuted = true;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.log(`❌ 권한 없음 - 메인 페이지로 이동: ${currentPage}`);
|
||||||
|
window.permissionGuardExecuted = true;
|
||||||
|
this.redirectToMain();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`🚫 권한 체크 오류:`, error);
|
||||||
|
this.redirectToLogin();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 로그인 페이지로 리다이렉트
|
||||||
|
*/
|
||||||
|
redirectToLogin() {
|
||||||
|
const currentUrl = encodeURIComponent(window.location.pathname + window.location.search);
|
||||||
|
window.location.href = `login.html?redirect=${currentUrl}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메인 페이지로 리다이렉트
|
||||||
|
*/
|
||||||
|
redirectToMain() {
|
||||||
|
// 사용자에게 알림 표시
|
||||||
|
if (typeof alert !== 'undefined') {
|
||||||
|
alert('이 페이지에 접근할 권한이 없습니다. 메인 페이지로 이동합니다.');
|
||||||
|
}
|
||||||
|
window.location.href = 'index.html';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 전역 인스턴스 생성
|
||||||
|
window.permissionGuard = new PermissionGuard();
|
||||||
|
|
||||||
|
// 헤더 로드 완료 후 권한 체크 실행 (헤더 상태 보존)
|
||||||
|
document.addEventListener('headerLoaded', () => {
|
||||||
|
console.log('🔐 헤더 로드 완료 후 권한 체크 시작');
|
||||||
|
// 로그인 페이지는 권한 체크 제외
|
||||||
|
const currentPage = window.location.pathname.split('/').pop() || 'index.html';
|
||||||
|
if (currentPage !== 'login.html') {
|
||||||
|
// 약간의 지연을 두어 헤더 초기화 완료 대기
|
||||||
|
setTimeout(() => {
|
||||||
|
window.permissionGuard.checkCurrentPagePermission();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 백업: DOM 로드 완료 시 권한 체크 실행 (헤더 이벤트가 없는 경우)
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// 헤더 로드 이벤트를 기다리되, 3초 후에는 강제 실행
|
||||||
|
setTimeout(() => {
|
||||||
|
const currentPage = window.location.pathname.split('/').pop() || 'index.html';
|
||||||
|
if (currentPage !== 'login.html' && !window.permissionGuardExecuted) {
|
||||||
|
console.log('🔐 백업 권한 체크 실행');
|
||||||
|
window.permissionGuard.checkCurrentPagePermission();
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
@@ -664,6 +664,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 권한 가드 -->
|
||||||
|
<script src="static/js/permission-guard.js"></script>
|
||||||
<!-- 헤더 로더 -->
|
<!-- 헤더 로더 -->
|
||||||
<script src="static/js/header-loader.js"></script>
|
<script src="static/js/header-loader.js"></script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user