개발 중 로그인마다 인증 코드를 기다리던 것을 끄는 스위치. AUTH_OTP_DISABLED 가 true 이고 ENVIRONMENT 가 development 일 때만 듣는다 — 운영에서는 켜져 있어도 무시된다. - 로그인: 새 브라우저·주기 재인증 둘 다 코드 없이 통과, 생략마다 경고 로그 - 회원가입: 흐름은 그대로 두고 인증 코드를 서버 로그에 기록 - .env 에 AUTH_OTP_DISABLED=True (배포 전 False 로 되돌릴 것) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJC56e4osweKJ4vafm9ReM
102 lines
4.0 KiB
Python
102 lines
4.0 KiB
Python
"""회원가입, 이메일 인증 및 회사 검색 API."""
|
|
|
|
import logging
|
|
from datetime import datetime
|
|
|
|
from fastapi import APIRouter, HTTPException, Query, Request
|
|
from fastapi.responses import JSONResponse
|
|
|
|
from common_util.common_util_auth import (
|
|
generate_device_token,
|
|
generate_otp,
|
|
hash_device_token,
|
|
hash_password,
|
|
hash_user_agent,
|
|
set_device_token_cookie,
|
|
verify_password,
|
|
)
|
|
from common_util.common_util_auth_repository import (
|
|
complete_registration,
|
|
consume_otp,
|
|
create_registration,
|
|
get_active_otp,
|
|
get_user_by_email,
|
|
refresh_pending_registration,
|
|
replace_otp,
|
|
search_companies,
|
|
trust_device,
|
|
)
|
|
from common_util.common_util_email import send_email_background
|
|
from common_util.common_util_email_templates import otp_email
|
|
from config.config_system import AUTH_OTP_DISABLED
|
|
|
|
from .A07_Register_Schema import RegisterRequest, RegisterVerifyRequest
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/api/auth", tags=["Authentication"])
|
|
|
|
|
|
@router.get("/companies")
|
|
async def find_companies(q: str = Query(min_length=1, max_length=100)):
|
|
return {"status": "success", "companies": await search_companies(q.strip())}
|
|
|
|
|
|
@router.post("/register/request", status_code=202)
|
|
async def request_registration(payload: RegisterRequest):
|
|
email = str(payload.email).lower()
|
|
data = payload.model_dump(exclude={"password_confirm"})
|
|
data["email"] = email
|
|
password_hash = hash_password(payload.password)
|
|
|
|
existing = await get_user_by_email(email)
|
|
if existing:
|
|
# 이미 이메일 인증까지 마친 계정이면 재가입 불가
|
|
if existing["status"] != "PENDING_EMAIL":
|
|
raise HTTPException(status_code=409, detail="이미 등록된 이메일입니다.")
|
|
# 미인증 계정이면 최신 요청으로 갱신하고 새 인증 코드를 발급
|
|
user_id = existing["id"]
|
|
await refresh_pending_registration(user_id, data, password_hash)
|
|
else:
|
|
user_id = await create_registration(data, password_hash)
|
|
|
|
code = generate_otp()
|
|
await replace_otp(user_id, "REGISTER", hash_password(code))
|
|
subject, html = otp_email(code, "회원가입")
|
|
send_email_background(email, subject, html)
|
|
# 개발 단계에서는 메일을 못 받아도 가입을 이어갈 수 있게 코드를 로그에 찍는다
|
|
# (2026-09-13 사용자 지시). ENVIRONMENT=development + AUTH_OTP_DISABLED=true 에서만.
|
|
if AUTH_OTP_DISABLED:
|
|
logger.warning("[auth] 개발용 가입 인증 코드 — %s : %s", email, code)
|
|
return {"status": "success", "message": "인증 코드를 발송했습니다."}
|
|
|
|
|
|
@router.post("/register/verify")
|
|
async def verify_registration(payload: RegisterVerifyRequest, request: Request):
|
|
user = await get_user_by_email(str(payload.email).lower())
|
|
if not user or user["status"] != "PENDING_EMAIL":
|
|
raise HTTPException(status_code=400, detail="인증 대기 중인 가입 요청이 없습니다.")
|
|
otp = await get_active_otp(user["id"], "REGISTER")
|
|
if (
|
|
not otp
|
|
or otp["expires_at"] < datetime.utcnow()
|
|
or not verify_password(payload.otp_code, otp["otp_hash"])
|
|
):
|
|
raise HTTPException(status_code=400, detail="인증 코드가 유효하지 않습니다.")
|
|
await consume_otp(otp["id"])
|
|
await complete_registration(user["id"], bool(user["is_master"]))
|
|
# 가입 인증한 브라우저를 신뢰 등록하여 첫 로그인 시 OTP를 생략한다.
|
|
agent = request.headers.get("user-agent", "unknown")[:1000]
|
|
device_token = generate_device_token()
|
|
await trust_device(user["id"], hash_device_token(device_token), hash_user_agent(agent))
|
|
# 회원가입 시점에는 회사가 없으므로(NO_COMPANY) 마스터 알림은 발송하지 않는다.
|
|
# 회사 생성/연결은 로그인 후 B01_Dashboard에서 진행한다.
|
|
response = JSONResponse(
|
|
{
|
|
"status": "success",
|
|
"account_status": "NO_COMPANY",
|
|
}
|
|
)
|
|
set_device_token_cookie(response, device_token)
|
|
return response
|