diff --git a/.gitignore b/.gitignore index babaa70..6f347c3 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ venv/ # IDE .vscode/ -.idea/ \ No newline at end of file +.idea/cookies.txt +cookies.txt diff --git a/accounts/urls.py b/accounts/urls.py new file mode 100644 index 0000000..70ff0e5 --- /dev/null +++ b/accounts/urls.py @@ -0,0 +1,14 @@ +# accounts/urls.py +from django.urls import path +from . import views + +urlpatterns = [ + path("signup/", views.signup, name="signup"), + + path("login/", views.login, name="login"), + + path("logout/", views.logout, name="logout"), + + path("session/", views.session, name="session"), # GET /auth/session + +] \ No newline at end of file diff --git a/accounts/views.py b/accounts/views.py index 91ea44a..6a9fcdb 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,3 +1,98 @@ -from django.shortcuts import render +# accounts/views.py +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.response import Response +from rest_framework import status +from django.contrib.auth import authenticate, login as auth_login, logout as auth_logout +from django.contrib.auth.models import User +from django.views.decorators.csrf import ensure_csrf_cookie +from django.utils.decorators import method_decorator # Create your views here. + +def _has_profile(user) -> bool: + # TODO: 프로필 모델 확정 후 구현 + return False + +# 회원가입 +@api_view(["POST"]) +@permission_classes([AllowAny]) # 공개 엔드포인트 +def signup(request): + username = request.data.get("username") + password = request.data.get("password") + + # TODO: 검증 — 필수값 누락, username 중복, 비밀번호 정책 등 + + user = User.objects.create_user(username=username, password=password) + auth_login(request, user) # 가입직후 세션 생성 + + return Response( + {"user_id": user.id, "username": user.username, + "has_profile": False, "next": "/signup/profile"}, + status=status.HTTP_201_CREATED, + ) + +# 로그인 +@api_view(["POST"]) +@permission_classes([AllowAny]) # 공개 엔드포인트 +def login(request): + username = request.data.get("username") + password = request.data.get("password") + + # 400: 필수값 누락 + if not username or not password: + return Response( + {"code": "INVALID_INPUT", + "message": "아이디와 비밀번호를 모두 입력해주세요." + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + user = authenticate(request, username=username, password=password) + + if user is None: # 401: 인증 실패 + return Response( + {"code": "INVALID_CREDENTIALS", + "message": "아이디 또는 비밀번호가 일치하지 않습니다." + }, + status=status.HTTP_401_UNAUTHORIZED, + ) + + auth_login(request, user) # 로그인 성공 시 세션 생성 + has_profile = _has_profile(user) # 프로필 존재 여부 확인 + return Response( + {"user_id": user.id, "username": user.username, + "has_profile": has_profile, "next": "/" if has_profile else "/signup/profile"}, + status=status.HTTP_200_OK, + ) + +# 로그아웃 +@api_view(["POST"]) +@permission_classes([IsAuthenticated]) +def logout(request): + auth_logout(request) # 세션 삭제 + return Response( + {"success": True, "night_session_active": True, "next": "/login"}, + status=status.HTTP_200_OK + ) + +# 세션 확인 +@api_view(["GET"]) +@permission_classes([AllowAny]) +@ensure_csrf_cookie +def session(request): + # 미인증 + if not request.user.is_authenticated: + return Response( + {"is_authenticated": False, "user_id": None, + "has_profile": False, "next": "/login"}, + status=status.HTTP_200_OK, + ) + + # 인증됨 + has_profile = _has_profile(request.user) # TODO: 프로필 이슈에서 구현 + return Response( + {"is_authenticated": True, "user_id": request.user.id, + "has_profile": has_profile, "next": "/" if has_profile else "/signup/profile"}, + status=status.HTTP_200_OK, + ) diff --git a/config/exception_handlers.py b/config/exception_handlers.py new file mode 100644 index 0000000..0aef76b --- /dev/null +++ b/config/exception_handlers.py @@ -0,0 +1,24 @@ +# config/exception_handlers.py +from rest_framework.views import exception_handler +from rest_framework.exceptions import NotAuthenticated, AuthenticationFailed, PermissionDenied +from rest_framework import status + + +def custom_exception_handler(exc, context): + response = exception_handler(exc, context) + + if response is None: + return response + + # 미인증: DRF 기본 403 -> 401로 변환 + if isinstance(exc, (NotAuthenticated, AuthenticationFailed)): + response.status_code = status.HTTP_401_UNAUTHORIZED + response.data = {"code": "NOT_AUTHENTICATED", + "message": "로그인 상태가 아닙니다."} + + # CSRF 실패: 403 + elif isinstance(exc, PermissionDenied): + response.data = {"code": "CSRF_FAILED", + "message": "요청이 유효하지 않습니다. 새로고침 후 다시 시도해주세요."} + + return response \ No newline at end of file diff --git a/config/settings.py b/config/settings.py index f802de6..a59b54c 100644 --- a/config/settings.py +++ b/config/settings.py @@ -37,8 +37,18 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + + 'rest_framework', + 'accounts', ] +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": [ + "rest_framework.authentication.SessionAuthentication", + ], + "EXCEPTION_HANDLER": "config.exception_handlers.custom_exception_handler", +} + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', diff --git a/config/urls.py b/config/urls.py index c74036a..50375ae 100644 --- a/config/urls.py +++ b/config/urls.py @@ -15,8 +15,9 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path +from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), -] + path('auth/', include('accounts.urls')), +] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index a8c5930..d32c7a0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ asgiref==3.12.1 -Django==6.0.7 -djangorestframework==3.17.1 +Django==6.0.8 +djangorestframework==3.17.2 sqlparse==0.5.5 -tzdata==2026.3