앱이 기대하는 base-backend 계약(envelope·JWT·/chat/stream SSE)을 그대로 구현. Django 는 로그인·세션 미러(SQLite/PG)·OpenCode 이벤트 번역만 맡고, 답변은 전용 OpenCode 인스턴스(opencode/ workspace, codeassist 에이전트)가 만듦. - accounts: 이메일 로그인, access 60분 / refresh 14일, entra/config 는 501 - chat: 세션 목록·검색·메시지·취소 + POST /chat/stream 어댑터(part.delta→token, idle→usage/done, 클라 끊겨도 턴 감시 태스크가 DB 마무리, 첫 이벤트 60초 타임아웃) - OpenCode 1.18 멀티 프로젝트라 모든 요청에 ?directory= 부착 - docs-lib 에 OpenCode SDK 1.18.6 타입 원본, pytest 32 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
78 lines
2.9 KiB
Python
78 lines
2.9 KiB
Python
"""
|
|
인증 API — 프론트 `features/auth/api/auth.api.ts` 계약.
|
|
|
|
POST /api/v1/auth/login {email,password} → TokenResponse
|
|
POST /api/v1/auth/refresh {refreshToken} → TokenResponse (access 갱신)
|
|
POST /api/v1/auth/logout → null (클라가 토큰 버림. 서버는 회전·블랙리스트 안 함)
|
|
GET /api/v1/users/me → UserResponse
|
|
GET /api/v1/auth/entra/config → 501 (Entra 안 씀 — 프론트가 버튼 숨김)
|
|
"""
|
|
|
|
from rest_framework.permissions import AllowAny
|
|
from rest_framework.response import Response
|
|
from rest_framework.views import APIView
|
|
from rest_framework_simplejwt.exceptions import TokenError
|
|
from rest_framework_simplejwt.tokens import RefreshToken
|
|
|
|
from common.envelope import CodedError
|
|
|
|
from .authentication import issue_tokens
|
|
from .models import User
|
|
from .serializers import LoginSerializer, RefreshSerializer
|
|
|
|
|
|
class LoginView(APIView):
|
|
permission_classes = [AllowAny]
|
|
authentication_classes = [] # 옛 토큰이 붙어 있어도 로그인은 통과시킴
|
|
|
|
def post(self, request):
|
|
ser = LoginSerializer(data=request.data)
|
|
ser.is_valid(raise_exception=True)
|
|
user = User.objects.filter(email__iexact=ser.validated_data["email"]).first()
|
|
if user is None or not user.check_password(ser.validated_data["password"]):
|
|
raise CodedError(401, "INVALID_CREDENTIALS", "이메일 또는 비밀번호가 올바르지 않아")
|
|
if not user.is_active:
|
|
raise CodedError(403, "USER_INACTIVE", "비활성 계정이야")
|
|
return Response(issue_tokens(user))
|
|
|
|
|
|
class RefreshView(APIView):
|
|
permission_classes = [AllowAny]
|
|
authentication_classes = []
|
|
|
|
def post(self, request):
|
|
ser = RefreshSerializer(data=request.data or {})
|
|
ser.is_valid(raise_exception=True)
|
|
raw = ser.validated_data.get("refreshToken") or ""
|
|
if not raw:
|
|
raise CodedError(401, "REFRESH_TOKEN_MISSING", "refresh 토큰이 없어")
|
|
try:
|
|
refresh = RefreshToken(raw)
|
|
except TokenError:
|
|
raise CodedError(401, "REFRESH_TOKEN_INVALID", "refresh 토큰이 만료됐거나 잘못됐어")
|
|
user = User.objects.filter(id=refresh["uid"], is_active=True).first()
|
|
if user is None:
|
|
raise CodedError(401, "REFRESH_TOKEN_INVALID", "사용자를 찾을 수 없어")
|
|
return Response(issue_tokens(user, refresh))
|
|
|
|
|
|
class LogoutView(APIView):
|
|
permission_classes = [AllowAny]
|
|
authentication_classes = []
|
|
|
|
def post(self, _request):
|
|
return Response(None)
|
|
|
|
|
|
class MeView(APIView):
|
|
def get(self, request):
|
|
return Response(request.user.as_response())
|
|
|
|
|
|
class EntraConfigView(APIView):
|
|
permission_classes = [AllowAny]
|
|
authentication_classes = []
|
|
|
|
def get(self, _request):
|
|
raise CodedError(501, "NOT_IMPLEMENTED", "Entra 로그인은 지원하지 않아")
|