🎉 Initial commit: Document Server MVP

 Features implemented:
- FastAPI backend with JWT authentication
- PostgreSQL database with async SQLAlchemy
- HTML document viewer with smart highlighting
- Note system connected to highlights (1:1 relationship)
- Bookmark system for quick navigation
- Integrated search (documents + notes)
- Tag system for document organization
- Docker containerization with Nginx

🔧 Technical stack:
- Backend: FastAPI + PostgreSQL + Redis
- Frontend: Alpine.js + Tailwind CSS
- Authentication: JWT tokens
- File handling: HTML + PDF support
- Search: Full-text search with relevance scoring

📋 Core functionality:
- Text selection → Highlight creation
- Highlight → Note attachment
- Note management with search/filtering
- Bookmark creation at scroll positions
- Document upload with metadata
- User management (admin creates accounts)
This commit is contained in:
Hyungi Ahn
2025-08-21 16:09:17 +09:00
commit 3036b8f0fb
40 changed files with 6303 additions and 0 deletions

View File

@@ -0,0 +1,176 @@
"""
사용자 관리 API 라우터
"""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update, delete
from typing import List
from src.core.database import get_db
from src.models.user import User
from src.schemas.auth import UserInfo
from src.api.dependencies import get_current_active_user, get_current_admin_user
from pydantic import BaseModel
class UpdateProfileRequest(BaseModel):
"""프로필 업데이트 요청"""
full_name: str = None
theme: str = None
language: str = None
timezone: str = None
class UpdateUserRequest(BaseModel):
"""사용자 정보 업데이트 요청 (관리자용)"""
full_name: str = None
is_active: bool = None
is_admin: bool = None
router = APIRouter()
@router.get("/profile", response_model=UserInfo)
async def get_profile(
current_user: User = Depends(get_current_active_user)
):
"""현재 사용자 프로필 조회"""
return UserInfo.from_orm(current_user)
@router.put("/profile", response_model=UserInfo)
async def update_profile(
profile_data: UpdateProfileRequest,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db)
):
"""프로필 업데이트"""
update_data = {}
if profile_data.full_name is not None:
update_data["full_name"] = profile_data.full_name
if profile_data.theme is not None:
update_data["theme"] = profile_data.theme
if profile_data.language is not None:
update_data["language"] = profile_data.language
if profile_data.timezone is not None:
update_data["timezone"] = profile_data.timezone
if update_data:
await db.execute(
update(User)
.where(User.id == current_user.id)
.values(**update_data)
)
await db.commit()
await db.refresh(current_user)
return UserInfo.from_orm(current_user)
@router.get("/", response_model=List[UserInfo])
async def list_users(
admin_user: User = Depends(get_current_admin_user),
db: AsyncSession = Depends(get_db)
):
"""사용자 목록 조회 (관리자 전용)"""
result = await db.execute(select(User).order_by(User.created_at.desc()))
users = result.scalars().all()
return [UserInfo.from_orm(user) for user in users]
@router.get("/{user_id}", response_model=UserInfo)
async def get_user(
user_id: str,
admin_user: User = Depends(get_current_admin_user),
db: AsyncSession = Depends(get_db)
):
"""특정 사용자 조회 (관리자 전용)"""
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return UserInfo.from_orm(user)
@router.put("/{user_id}", response_model=UserInfo)
async def update_user(
user_id: str,
user_data: UpdateUserRequest,
admin_user: User = Depends(get_current_admin_user),
db: AsyncSession = Depends(get_db)
):
"""사용자 정보 업데이트 (관리자 전용)"""
# 사용자 존재 확인
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
# 자기 자신의 관리자 권한은 제거할 수 없음
if user.id == admin_user.id and user_data.is_admin is False:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Cannot remove admin privileges from yourself"
)
# 업데이트할 데이터 준비
update_data = {}
if user_data.full_name is not None:
update_data["full_name"] = user_data.full_name
if user_data.is_active is not None:
update_data["is_active"] = user_data.is_active
if user_data.is_admin is not None:
update_data["is_admin"] = user_data.is_admin
if update_data:
await db.execute(
update(User)
.where(User.id == user_id)
.values(**update_data)
)
await db.commit()
await db.refresh(user)
return UserInfo.from_orm(user)
@router.delete("/{user_id}")
async def delete_user(
user_id: str,
admin_user: User = Depends(get_current_admin_user),
db: AsyncSession = Depends(get_db)
):
"""사용자 삭제 (관리자 전용)"""
# 사용자 존재 확인
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
# 자기 자신은 삭제할 수 없음
if user.id == admin_user.id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Cannot delete yourself"
)
# 사용자 삭제
await db.execute(delete(User).where(User.id == user_id))
await db.commit()
return {"message": "User deleted successfully"}