Files
conai/backend/app/api/auth.py
sinmb79 2a4950d8a0 feat: CONAI Phase 1 MVP 초기 구현
소형 건설업체(100억 미만)를 위한 AI 기반 토목공사 통합관리 플랫폼

Backend (FastAPI):
- SQLAlchemy 모델 13개 (users, projects, wbs, tasks, daily_reports, reports, inspections, quality, weather, permits, rag, settings)
- API 라우터 11개 (auth, projects, tasks, daily_reports, reports, inspections, weather, rag, kakao, permits, settings)
- Services: Claude AI 래퍼, CPM Gantt 계산, 기상청 API, RAG(pgvector), 카카오 Skill API
- Alembic 마이그레이션 (pgvector 포함)
- pytest 테스트 (CPM, 날씨 경보)

Frontend (Next.js 15):
- 11개 페이지 (대시보드, 프로젝트, Gantt, 일보, 검측, 품질, 날씨, 인허가, RAG, 설정)
- TanStack Query + Zustand + Tailwind CSS

인프라:
- Docker Compose (PostgreSQL pgvector + backend + frontend)
- 한국어 README 및 설치 가이드

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 20:06:36 +09:00

75 lines
2.9 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from typing import Annotated
from app.core.database import get_db
from app.core.security import verify_password, get_password_hash, create_access_token, create_refresh_token, decode_token
from app.models.user import User
from app.schemas.user import UserCreate, UserResponse, TokenResponse
from app.deps import CurrentUser
router = APIRouter(prefix="/auth", tags=["인증"])
@router.post("/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def register(data: UserCreate, db: Annotated[AsyncSession, Depends(get_db)]):
result = await db.execute(select(User).where(User.email == data.email))
if result.scalar_one_or_none():
raise HTTPException(status_code=400, detail="이미 등록된 이메일입니다")
user = User(
email=data.email,
hashed_password=get_password_hash(data.password),
name=data.name,
role=data.role,
phone=data.phone,
)
db.add(user)
await db.commit()
await db.refresh(user)
return user
@router.post("/login", response_model=TokenResponse)
async def login(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
db: Annotated[AsyncSession, Depends(get_db)],
):
result = await db.execute(select(User).where(User.email == form_data.username, User.is_active == True))
user = result.scalar_one_or_none()
if not user or not verify_password(form_data.password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="이메일 또는 비밀번호가 올바르지 않습니다",
headers={"WWW-Authenticate": "Bearer"},
)
return TokenResponse(
access_token=create_access_token(str(user.id)),
refresh_token=create_refresh_token(str(user.id)),
user=UserResponse.model_validate(user),
)
@router.post("/refresh", response_model=TokenResponse)
async def refresh_token(refresh_token_str: str, db: Annotated[AsyncSession, Depends(get_db)]):
payload = decode_token(refresh_token_str)
if not payload or payload.get("type") != "refresh":
raise HTTPException(status_code=401, detail="유효하지 않은 리프레시 토큰입니다")
result = await db.execute(select(User).where(User.id == payload["sub"], User.is_active == True))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(status_code=401, detail="사용자를 찾을 수 없습니다")
return TokenResponse(
access_token=create_access_token(str(user.id)),
refresh_token=create_refresh_token(str(user.id)),
user=UserResponse.model_validate(user),
)
@router.get("/me", response_model=UserResponse)
async def get_me(current_user: CurrentUser):
return current_user