diff --git a/requirements.txt b/requirements.txt index f6d44d8..2edddb5 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/wtnt/admin/serializers.py b/wtnt/admin/serializers.py index 7b34b57..717d666 100644 --- a/wtnt/admin/serializers.py +++ b/wtnt/admin/serializers.py @@ -21,4 +21,4 @@ class ApproveTeamSerializer(serializers.ModelSerializer): class Meta: model = Team - fields = ["id", "name", "created_at", "is_approved", "genre"] + fields = ["id", "title", "created_at", "is_approved", "genre"] diff --git a/wtnt/admin/team/service.py b/wtnt/admin/team/service.py index af89402..99b49ed 100644 --- a/wtnt/admin/team/service.py +++ b/wtnt/admin/team/service.py @@ -1,7 +1,8 @@ from django.contrib.auth import get_user_model +import core.exception.notfound as notfound_exception +import core.exception.team as team_exception from admin.serializers import ApproveTeamSerializer -from core.exceptions import NotFoundError, KeywordNotMatchError from core.pagenations import ListPagenationSize10 from core.service import BaseService from core.utils.s3 import S3Utils @@ -18,7 +19,7 @@ def get_not_approved_team(self): return serializer.data except Team.DoesNotExist: - raise NotFoundError() + raise notfound_exception.TeamNotFoundError() def approve_teams(self): team_ids = [int(id) for id in self.request.data.get("ids").split(",")] @@ -26,7 +27,7 @@ def approve_teams(self): if cnt: return {"detail": "Success to update teams"} else: - raise NotFoundError() + raise notfound_exception.TeamNotFoundError() def reject_teams(self, status): team_ids = [int(id) for id in self.request.data.get("ids").split(",")] @@ -38,7 +39,7 @@ def reject_teams(self, status): if cnt: return {"detail": "Success to reject teams"} else: - raise NotFoundError() + raise notfound_exception.TeamNotFoundError() def get_approved_teams(self): queryset = Team.objects.filter(is_approved=True).order_by("id") @@ -59,10 +60,10 @@ def serach_teams(self): leader_ids = User.objects.search_by_name(name=keyword).values_list("id", flat=True) queryset = Team.objects.search_by_leader_ids(leader_ids=leader_ids) else: - raise KeywordNotMatchError + raise team_exception.TeamKeywordNotMatchError() if not queryset: - raise NotFoundError() + raise notfound_exception.TeamNotFoundError() paginated = self.paginate_queryset(queryset, self.request, view=self) serializer = ApproveTeamSerializer(paginated, many=True) diff --git a/wtnt/admin/team/urls.py b/wtnt/admin/team/urls.py index ad42dc4..06f529a 100644 --- a/wtnt/admin/team/urls.py +++ b/wtnt/admin/team/urls.py @@ -6,7 +6,7 @@ ) urlpatterns = [ - path("manage", TeamManageView.as_view()), - path("list", TeamDeleteView.as_view()), - path("search", TeamSearchView.as_view()), + path("manage", TeamManageView.as_view(), name="admin-team-manage"), + path("list", TeamDeleteView.as_view(), name="admin-team-list"), + path("search", TeamSearchView.as_view(), name="admin-team-search"), ] diff --git a/wtnt/admin/team/views.py b/wtnt/admin/team/views.py index 544fe31..774fa1e 100644 --- a/wtnt/admin/team/views.py +++ b/wtnt/admin/team/views.py @@ -1,9 +1,10 @@ from rest_framework import status from rest_framework.response import Response from rest_framework.views import APIView -from rest_framework.permissions import IsAdminUser from django.contrib.auth import get_user_model +import core.exception.request as exception +from core.permissions import IsAdminUser from .service import AdminTeamService User = get_user_model() @@ -19,12 +20,26 @@ def get(self, request): return Response(data, status=status.HTTP_200_OK) def patch(self, request, *args, **kwargs): + required_field = ["ids"] + if len(request.data) != len(required_field): + raise exception.InvalidRequestError() + for field in required_field: + if field not in request.data: + raise exception.InvalidRequestError() + admin_service = AdminTeamService(request) data = admin_service.approve_teams() return Response(data, status=status.HTTP_202_ACCEPTED) def delete(self, request, *args, **kwargs): + required_field = ["ids"] + if len(request.data) != len(required_field): + raise exception.InvalidRequestError() + for field in required_field: + if field not in request.data: + raise exception.InvalidRequestError() + admin_service = AdminTeamService(request) data = admin_service.reject_teams(status=False) @@ -39,6 +54,13 @@ def get(self, request): return admin_service.get_approved_teams() def delete(self, request, *args, **kwargs): + required_field = ["ids"] + if len(request.data) != len(required_field): + raise exception.InvalidRequestError() + for field in required_field: + if field not in request.data: + raise exception.InvalidRequestError() + admin_service = AdminTeamService(request) data = admin_service.reject_teams(status=True) diff --git a/wtnt/admin/tests.py b/wtnt/admin/tests.py deleted file mode 100644 index a79ca8b..0000000 --- a/wtnt/admin/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -# from django.test import TestCase - -# Create your tests here. diff --git a/wtnt/admin/user/service.py b/wtnt/admin/user/service.py index 63ab3d8..a83ca58 100644 --- a/wtnt/admin/user/service.py +++ b/wtnt/admin/user/service.py @@ -1,6 +1,7 @@ from django.contrib.auth import get_user_model -from core.exceptions import NotFoundError, KeywordNotMatchError +import core.exception.notfound as notfound_exception +import core.exception.team as team_exception from core.pagenations import ListPagenationSize10 from core.service import BaseService from core.utils.s3 import S3Utils @@ -11,13 +12,12 @@ class AdminUserService(BaseService, ListPagenationSize10): def get_not_approved_users(self): - try: - queryset = User.objects.filter(is_approved=False, is_superuser=False) + queryset = User.objects.filter(is_approved=False, is_superuser=False) + if queryset: serializer = ApproveUserSerializer(queryset, many=True) return serializer.data - except User.DoesNotExist: - raise NotFoundError() + raise notfound_exception.UserNotFoundError() def approve_users(self): user_ids = [int(id) for id in self.request.data.get("ids").split(",")] @@ -25,7 +25,7 @@ def approve_users(self): if cnt: return {"detail": "Success to update users"} else: - raise NotFoundError() + raise notfound_exception.UserNotFoundError() def reject_users(self, status): user_ids = [int(id) for id in self.request.data.get("ids").split(",")] @@ -36,7 +36,7 @@ def reject_users(self, status): if cnt: return {"detail": "Success to reject users"} else: - raise NotFoundError() + raise notfound_exception.UserNotFoundError() def get_approved_users(self): queryset = User.objects.filter(is_approved=True, is_superuser=False).order_by("student_num") @@ -56,10 +56,10 @@ def search_users(self): elif search_filter == "position": queryset = User.objects.search_by_position(position=keyword) else: - raise KeywordNotMatchError() + raise team_exception.TeamKeywordNotMatchError() if not queryset: - raise NotFoundError() + raise notfound_exception.UserNotFoundError() paginated = self.paginate_queryset(queryset, self.request, view=self) serializer = ApproveUserSerializer(paginated, many=True) diff --git a/wtnt/admin/user/urls.py b/wtnt/admin/user/urls.py index eada609..c32ad86 100644 --- a/wtnt/admin/user/urls.py +++ b/wtnt/admin/user/urls.py @@ -6,7 +6,7 @@ ) urlpatterns = [ - path("manage", UserManageView.as_view()), - path("list", UserDeleteView.as_view()), - path("search", UserSearchView.as_view()), + path("manage", UserManageView.as_view(), name="admin-user-manage"), + path("list", UserDeleteView.as_view(), name="admin-user-list"), + path("search", UserSearchView.as_view(), name="admin-user-search"), ] diff --git a/wtnt/admin/user/views.py b/wtnt/admin/user/views.py index a6bfeea..506fcc9 100644 --- a/wtnt/admin/user/views.py +++ b/wtnt/admin/user/views.py @@ -1,9 +1,10 @@ from rest_framework import status from rest_framework.response import Response from rest_framework.views import APIView -from rest_framework.permissions import IsAdminUser from django.contrib.auth import get_user_model +import core.exception.request as exception +from core.permissions import IsAdminUser from .service import AdminUserService User = get_user_model() @@ -19,12 +20,26 @@ def get(self, request): return Response(data, status=status.HTTP_200_OK) def patch(self, request, *args, **kwargs): + required_field = ["ids"] + if len(request.data) != len(required_field): + raise exception.InvalidRequestError() + for field in required_field: + if field not in request.data: + raise exception.InvalidRequestError() + admin_service = AdminUserService(request) data = admin_service.approve_users() return Response(data, status=status.HTTP_202_ACCEPTED) def delete(self, request, *args, **kwargs): + required_field = ["ids"] + if len(request.data) != len(required_field): + raise exception.InvalidRequestError() + for field in required_field: + if field not in request.data: + raise exception.InvalidRequestError() + admin_service = AdminUserService(request) data = admin_service.reject_users(status=False) @@ -39,6 +54,13 @@ def get(self, request): return admin_service.get_approved_users() def delete(self, request, *args, **kwargs): + required_field = ["ids"] + if len(request.data) != len(required_field): + raise exception.InvalidRequestError() + for field in required_field: + if field not in request.data: + raise exception.InvalidRequestError() + admin_service = AdminUserService(request) data = admin_service.reject_users(status=True) diff --git a/wtnt/core/authenticator.py b/wtnt/core/authenticator.py new file mode 100644 index 0000000..5e34ac7 --- /dev/null +++ b/wtnt/core/authenticator.py @@ -0,0 +1,60 @@ +from typing import Optional, Set, TypeVar + +from django.contrib.auth.models import AbstractBaseUser +from rest_framework import HTTP_HEADER_ENCODING +from rest_framework_simplejwt.authentication import JWTAuthentication +from rest_framework_simplejwt.tokens import Token +import core.exception.token as exception +from rest_framework_simplejwt.settings import api_settings +from rest_framework_simplejwt.exceptions import TokenError +from rest_framework_simplejwt.models import TokenUser + +AUTH_HEADER_TYPES = api_settings.AUTH_HEADER_TYPES + +AUTH_HEADER_TYPE_BYTES: Set[bytes] = {h.encode(HTTP_HEADER_ENCODING) for h in AUTH_HEADER_TYPES} + +AuthUser = TypeVar("AuthUser", AbstractBaseUser, TokenUser) + + +class CustomJWTAuthentication(JWTAuthentication): + def get_raw_token(self, header: bytes) -> Optional[bytes]: + parts = header.split() + + if len(parts) == 0: + # Empty AUTHORIZATION header sent + raise exception.NoTokenInAuthorizationHeaderError() + + if parts[0] not in AUTH_HEADER_TYPE_BYTES: + # Assume the header does not contain a JSON web token + raise exception.NoTokenInAuthorizationHeaderError() + + if len(parts) != 2: + raise exception.AuthenticationHeaderSpaceError() + + return parts[1] + + def get_validated_token(self, raw_token: bytes) -> Token: + for AuthToken in api_settings.AUTH_TOKEN_CLASSES: + try: + return AuthToken(raw_token) + except TokenError: + raise exception.AccessTokenExpiredError() + + def get_user(self, validated_token: Token) -> AuthUser: + """ + Attempts to find and return a user using the given validated token. + """ + try: + user_id = validated_token[api_settings.USER_ID_CLAIM] + except KeyError: + raise exception.TokenWithNoUserDataError() + + try: + user = self.user_model.objects.get(**{api_settings.USER_ID_FIELD: user_id}) + except self.user_model.DoesNotExist: + raise exception.UserNotFoundInTokenDataError() + + if not user.is_active: + raise exception.UserInactiveInTokenDataError() + + return user diff --git a/wtnt/core/exception/apply.py b/wtnt/core/exception/apply.py new file mode 100644 index 0000000..3abdc83 --- /dev/null +++ b/wtnt/core/exception/apply.py @@ -0,0 +1,11 @@ +from rest_framework.exceptions import APIException + + +class ClosedApplyError(APIException): + status_code = 400 + default_detail = {"message": "마감된 지원입니다.", "code": "0500"} + + +class DuplicatedApplyError(APIException): + status_code = 400 + default_detail = {"message": "중복된 지원입니다.", "code": "0501"} diff --git a/wtnt/core/exception/login.py b/wtnt/core/exception/login.py new file mode 100644 index 0000000..32a7157 --- /dev/null +++ b/wtnt/core/exception/login.py @@ -0,0 +1,51 @@ +from rest_framework.exceptions import APIException + + +class UserNameNotValidError(APIException): + status_code = 400 + default_detail = {"message": "이름에 공백, 특수문자 또는 숫자가 포함되어 있습니다.", "code": "0100"} + + +class UserNameTooLongError(APIException): + status_code = 400 + default_detail = {"message": "이름이 너무 짧거나 깁니다.", "code": "0101"} + + +class StudentNumNotValidError(APIException): + status_code = 400 + default_detail = {"message": "학번에 공백,특수문자 또는 문자가 포함되어 있습니다.", "code": "0110"} + + +class StudentNumTooLongError(APIException): + status_code = 400 + default_detail = {"message": "학번이 너무 짧거나 깁니다.", "code": "0111"} + + +class StudentNumDuplicatedError(APIException): + status_code = 400 + default_detail = {"message": "중복된 학번입니다.", "code": "0112"} + + +class PositionNotValidError(APIException): + status_code = 400 + default_detail = {"message": "포지션 기입이 올바르지 않습니다.", "code": "0120"} + + +class EmailCodeNotMatchError(APIException): + status_code = 400 + default_detail = {"message": "이메일 인증 코드 또는 첨부된 이메일이 일치하지 않습니다.", "code": "0130"} + + +class EmailTimeoutError(APIException): + status_code = 400 + default_detail = {"message": "이메일 인증 시간이 초과되었습니다.", "code": "0131"} + + +class EmailCodeNotMatchAfterAuthError(APIException): + status_code = 400 + default_detail = {"message": "인증 시 사용된 코드(이메일)와 Body의 코드(이메일)가 일치하지 않습니다.", "code": "0132"} + + +class EmailCeleryError(APIException): + status_code = 400 + default_detail = {"message": "이메일 발송 도중 문제가 발생했습니다.", "code": "0133"} diff --git a/wtnt/core/exception/notfound.py b/wtnt/core/exception/notfound.py new file mode 100644 index 0000000..1260107 --- /dev/null +++ b/wtnt/core/exception/notfound.py @@ -0,0 +1,26 @@ +from rest_framework.exceptions import APIException + + +class UserNotFoundError(APIException): + status_code = 404 + default_detail = {"message": "해당하는 유저가 존재하지 않습니다.", "code": "0000"} + + +class TeamNotFoundError(APIException): + status_code = 404 + default_detail = {"message": "해당하는 팀이 존재하지 않습니다.", "code": "0001"} + + +class TechNotFoundError(APIException): + status_code = 404 + default_detail = {"message": "해당하는 기술스택이 존재하지 않습니다.", "code": "0002"} + + +class ApplyNotFoundError(APIException): + status_code = 404 + default_detail = {"message": "해당하는 지원이 존재하지 않습니다.", "code": "0003"} + + +class TeamUserNotFoundError(APIException): + status_code = 404 + default_detail = {"message": "해당하는 팀원이 존재하지 않습니다.", "code": "0004"} diff --git a/wtnt/core/exception/permissions.py b/wtnt/core/exception/permissions.py new file mode 100644 index 0000000..e27df6c --- /dev/null +++ b/wtnt/core/exception/permissions.py @@ -0,0 +1,17 @@ +from rest_framework.exceptions import PermissionDenied + + +class IsNotApprovedUserError(PermissionDenied): + default_detail = {"message": "인증된 회원의 요청이 아닙니다.", "code": "0300"} + + +class IsNotAdminUSerError(PermissionDenied): + default_detail = {"message": "관리자의 요청이 아닙니다.", "code": "0301"} + + +class IsNotLeaderError(PermissionDenied): + default_detail = {"message": "팀장의 요청이 아닙니다.", "code": "0302"} + + +class IsNotOwnerError(PermissionDenied): + default_detail = {"message": "프로필 주인의 요청이 아닙니다.", "code": "0303"} diff --git a/wtnt/core/exception/request.py b/wtnt/core/exception/request.py new file mode 100644 index 0000000..d47e657 --- /dev/null +++ b/wtnt/core/exception/request.py @@ -0,0 +1,6 @@ +from rest_framework.exceptions import APIException + + +class InvalidRequestError(APIException): + status_code = 400 + default_detail = {"message": "요청의 형식이 잘못되었습니다.", "code": "0010"} diff --git a/wtnt/core/exception/team.py b/wtnt/core/exception/team.py new file mode 100644 index 0000000..ce2c4a8 --- /dev/null +++ b/wtnt/core/exception/team.py @@ -0,0 +1,55 @@ +from rest_framework.exceptions import APIException + + +class TeamNameLengthError(APIException): + status_code = 400 + default_detail = {"message": "팀 이름이 공백이거나 길이가 맞지 않습니다.", "code": "0200"} + + +class TeamNameDuplicateError(APIException): + status_code = 400 + default_detail = {"message": "팀 이름이 중복됩니다.", "code": "0201"} + + +class TeamGenreNotValidError(APIException): + status_code = 400 + default_detail = {"message": "프로젝트의 유형이 올바르지 않습니다.", "code": "0210"} + + +class TeamCategoryNotValidError(APIException): + status_code = 400 + default_detail = {"message": "정해진 범주 내의 서브 카테고리가 아닙니다.", "code": "0220"} + + +class TeamMemberCountLengthError(APIException): + status_code = 400 + default_detail = {"message": "모집인원의 숫자가 적절하지 못합니다.", "code": "0221"} + + +class TeamExplainLengthError(APIException): + status_code = 400 + default_detail = {"message": "프로젝트 설명이 공백이거나 길이를 넘어섰습니다.", "code": "0230"} + + +class TeamUrlLengthError(APIException): + status_code = 400 + default_detail = {"message": "프로젝트 관련 URL이 공백입니다.", "code": "0240"} + + +class TeamImageTypeError(APIException): + status_code = 400 + default_detail = {"message": "파일이 이미지가 아니거나 지원하지 않는 형식입니다", "code": "0250"} + + +class TeamKeywordNotMatchError(APIException): + status_code = 400 + default_detail = {"message": "검색에 필요한 키워드가 아닙니다.", "code": "0260"} + + +class TeamLikeVersionError(APIException): + status_code = 400 + default_detail = {"message": "좋아요에 필요한 버전이 일치하지 않습니다", "code": "0270"} + + def __init__(self, current_version): + self.default_detail = {"detail": self.default_detail, "version": current_version} + super().__init__(detail=self.default_detail, code=self.default_code) diff --git a/wtnt/core/exception/token.py b/wtnt/core/exception/token.py new file mode 100644 index 0000000..2d38382 --- /dev/null +++ b/wtnt/core/exception/token.py @@ -0,0 +1,33 @@ +from rest_framework.exceptions import AuthenticationFailed + + +class AccessTokenExpiredError(AuthenticationFailed): + default_detail = {"message": "만료된 액세스 토큰입니다.", "code": "0400"} + + +class InvalidTokenError(AuthenticationFailed): + default_detail = {"message": "유효한 토큰의 타입이 아닙니다.", "code": "0401"} + + +class AuthenticationHeaderSpaceError(AuthenticationFailed): + default_detail = {"message": "인증 헤더는 반드시 두 덩이로 이루어져야합니다.", "code": "0402"} + + +class TokenWithNoUserDataError(AuthenticationFailed): + default_detail = {"message": "토큰에 유저의 식별정보가 들어있지 않습니다.", "code": "0403"} + + +class UserNotFoundInTokenDataError(AuthenticationFailed): + default_detail = {"message": "토큰의 유저 식별정보가 유효하지 않습니다.", "code": "0404"} + + +class UserInactiveInTokenDataError(AuthenticationFailed): + default_detail = {"message": "토큰 속 유저가 Inactive 상태입니다.", "code": "0405"} + + +class NoTokenInAuthorizationHeaderError(AuthenticationFailed): + default_detail = {"message": "자격 증명이 주어지지 않았습니다.", "code": "0406"} + + +class RefreshTokenExpiredError(AuthenticationFailed): + default_detail = {"message": "리프레쉬 토큰이 만료되었습니다.", "code": "0410"} diff --git a/wtnt/core/exceptions.py b/wtnt/core/exceptions.py deleted file mode 100644 index ddb4ce1..0000000 --- a/wtnt/core/exceptions.py +++ /dev/null @@ -1,69 +0,0 @@ -from rest_framework.exceptions import APIException - - -class IsNotLeaderError(APIException): - status_code = 403 - default_detail = "It's not leader of team" - - -class IsNotOwnerError(APIException): - status_code = 403 - default_detail = "It's not an owner" - - -class CodeNotMatchError(APIException): - status_code = 400 - default_detail = "Code Not Matched" - - -class RefreshTokenExpiredError(APIException): - status_code = 401 - default_detail = "Expired Refresh Token" - - -class CeleryTaskError(APIException): - status_code = 400 - default_detail = "" - - -class NotFoundError(APIException): - status_code = 404 - default_detail = "No Content" - - -class SerializerNotValidError(APIException): - status_code = 400 - default_detail = "" - - @staticmethod - def get_detail(error_dict): - error_messages = "" - for field, errors in error_dict.items(): - for error in errors: - error_messages += f"{field}: {error}\n" - - return error_messages - - -class KeywordNotMatchError(APIException): - status_code = 400 - default_datail = "Keyword Not Matched" - - -class ClosedApplyError(APIException): - status_code = 400 - default_detail = "It's a closed apply" - - -class DuplicatedApplyError(APIException): - status_code = 400 - default_detail = "It's a duplicated apply" - - -class VersionError(APIException): - status_code = 400 - default_detail = "Version Not Matched" - - def __init__(self, current_version): - self.default_detail = {"detail": self.default_detail, "version": current_version} - super().__init__(detail=self.default_detail, code=self.default_code) diff --git a/wtnt/core/middleware.py b/wtnt/core/middleware.py index de65653..fb77246 100644 --- a/wtnt/core/middleware.py +++ b/wtnt/core/middleware.py @@ -7,11 +7,14 @@ def __init__(self, get_response): super().__init__(get_response) self.API = ["github/login", "callback/github"] self.REFRESH = "token/refresh" + self.LOGOUT = "logout" def process_response(self, request, response): path = request.path_info is_valid = any(api in path for api in self.API) is_refresh = True if self.REFRESH in path else False + is_logout = True if self.LOGOUT in path else False + if ( is_valid or is_refresh @@ -19,13 +22,35 @@ def process_response(self, request, response): ): if request.META.get("HTTP_X_FROM", None) == "web": response.set_cookie( - "access", response.headers.get("access", None), httponly=True, samesite="none", secure=True + "access", + response.headers.get("access", None), + httponly=True, + samesite="none", + secure=True, + domain="whatmeow.shop", ) if is_valid: del response.headers["access"] response.content = response.render().rendered_content + elif is_logout and response.status_code == status.HTTP_204_NO_CONTENT: + for cookie_name in request.COOKIES: + response.set_cookie( + cookie_name, + value="", + httponly=True, + samesite="none", + secure=True, + max_age=0, + domain="whatmeow.shop", + ) + + if request.META.get("HTTP_X_FROM", None) == "web": + del response.headers["access"] + + response.content = response.render().rendered_content + return response @@ -40,4 +65,5 @@ def process_request(self, request): if not is_valid: if request.META.get("HTTP_X_FROM", None) == "web": - request.META["HTTP_AUTHORIZATION"] = f"Bearer {request.COOKIES.get('access', None)}" + if request.COOKIES.get("access"): + request.META["HTTP_AUTHORIZATION"] = f"Bearer {request.COOKIES.get('access', None)}" diff --git a/wtnt/core/pagenations.py b/wtnt/core/pagenations.py index f45ba51..9a20129 100644 --- a/wtnt/core/pagenations.py +++ b/wtnt/core/pagenations.py @@ -1,10 +1,43 @@ from rest_framework.pagination import PageNumberPagination, CursorPagination +import wtnt.settings as api_settings class ListPagenationSize10(PageNumberPagination): page_size = 10 + def get_next_link(self): + if not self.page.has_next(): + return None + url = self.request.build_absolute_uri(self.page.next_page_number()) + if api_settings.DEBUG: + return url + return url.replace("http://localhost:8000", api_settings.MY_DOMAIN) + + def get_previous_link(self): + if not self.page.has_previous(): + return None + url = self.request.build_absolute_uri(self.page.previous_page_number()) + if api_settings.DEBUG: + return url + return url.replace("http://localhost:8000", api_settings.MY_DOMAIN) + class TeamPagination(CursorPagination): page_size = 2 ordering = "-created_at" + + def get_next_link(self): + url = super().get_next_link() + if url: + if api_settings.DEBUG: + return url + return url.replace("http://localhost:8000", api_settings.MY_DOMAIN) + return None + + def get_previous_link(self): + url = super().get_previous_link() + if url: + if api_settings.DEBUG: + return url + return url.replace("http://localhost:8000", api_settings.MY_DOMAIN) + return None diff --git a/wtnt/core/permissions.py b/wtnt/core/permissions.py index 41b13f5..600ad67 100644 --- a/wtnt/core/permissions.py +++ b/wtnt/core/permissions.py @@ -1,6 +1,18 @@ from rest_framework.permissions import BasePermission +import core.exception.permissions as exception class IsApprovedUser(BasePermission): def has_permission(self, request, view): - return bool(request.user.is_authenticated and request.user.is_approved) + if not (request.user.is_authenticated and request.user.is_approved): + raise exception.IsNotApprovedUserError() + + return True + + +class IsAdminUser(BasePermission): + def has_permission(self, request, view): + if not (request.user and request.user.is_staff): + raise exception.IsNotAdminUSerError() + + return True diff --git a/wtnt/core/service.py b/wtnt/core/service.py index f04c609..17ef6a6 100644 --- a/wtnt/core/service.py +++ b/wtnt/core/service.py @@ -1,4 +1,4 @@ -from .exceptions import IsNotOwnerError, IsNotLeaderError +import core.exception.permissions as exception class BaseService: @@ -13,10 +13,10 @@ def check_ownership(self): user_id = self.request.user.id if owner_id != user_id: - raise IsNotOwnerError() + raise exception.IsNotOwnerError() class BaseServiceWithCheckLeader(BaseService): def check_leader(self, user_id, leader_id): if not (user_id == leader_id): - raise IsNotLeaderError() + raise exception.IsNotLeaderError() diff --git a/wtnt/core/utils/auth.py b/wtnt/core/utils/auth.py deleted file mode 100644 index 7aee9a8..0000000 --- a/wtnt/core/utils/auth.py +++ /dev/null @@ -1,7 +0,0 @@ -def get_user_info(user): - return { - "name": user.name, - "student_num": user.student_num, - "id": user.id, - "image": user.image, - } diff --git a/wtnt/core/utils/redis.py b/wtnt/core/utils/redis.py index 1fc1bd2..268a3ff 100644 --- a/wtnt/core/utils/redis.py +++ b/wtnt/core/utils/redis.py @@ -1,33 +1,47 @@ -from datetime import datetime, timedelta - -from django.core.cache import cache +from datetime import timedelta from django_redis import get_redis_connection -client = get_redis_connection() - class RedisUtils: - @staticmethod - def sadd_view_client(team_id, user_id, adress): - return client.sadd(f"views:{team_id}", f"{user_id}_{adress}") - - @staticmethod - def set_refresh_token_in_cache(user_id, refresh_token): - cache.set(user_id, refresh_token) - cache.expire_at(user_id, datetime.now() + timedelta(days=7)) - - @staticmethod - def get_refresh_token_in_cache(user_id): - return cache.get(user_id) - - @staticmethod - def set_code_in_redis_from_email(email, code): - client.set(email, code) - - @staticmethod - def get_code_in_redis_from_email(email): - return client.get(email).decode() - - @staticmethod - def delete_code_in_redis_from_email(email): - client.delete(email) + client = None + + @classmethod + def init(cls): + if not cls.client: + cls.client = get_redis_connection() + + @classmethod + def sadd_view_client(cls, team_id, user_id, adress): + RedisUtils.init() + return cls.client.sadd(f"views:{team_id}", f"{user_id}_{adress}") + + @classmethod + def set_refresh_token(cls, user_id, refresh_token): + RedisUtils.init() + ttl_seconds = timedelta(days=7).total_seconds() + cls.client.setex(user_id, int(ttl_seconds), refresh_token) + + @classmethod + def delete_refresh_token(cls, user_id): + RedisUtils.init() + cls.client.delete(user_id) + + @classmethod + def get_refresh_token(cls, user_id): + RedisUtils.init() + return cls.client.get(user_id) + + @classmethod + def set_code_in_redis_from_email(cls, email, code): + RedisUtils.init() + cls.client.set(email, code) + + @classmethod + def get_code_in_redis_from_email(cls, email): + RedisUtils.init() + return cls.client.get(email) + + @classmethod + def delete_code_in_redis_from_email(cls, email): + RedisUtils.init() + cls.client.delete(email) diff --git a/wtnt/core/utils/s3.py b/wtnt/core/utils/s3.py index c4e0988..e59be5a 100644 --- a/wtnt/core/utils/s3.py +++ b/wtnt/core/utils/s3.py @@ -1,8 +1,9 @@ -from PIL import Image +from PIL import Image, UnidentifiedImageError import boto3 import io import uuid +import core.exception.team as exception import wtnt.settings as settings @@ -18,6 +19,7 @@ class S3Utils: ) bucket = settings.BUCKET_NAME region = settings.AWS_REGION + extra_args = {"ContentType": "image/jpeg", "ContentDisposition": "inline"} @classmethod def create_thumnail(cls, image, category): @@ -46,12 +48,19 @@ def upload_team_image_on_s3(cls, image, id=None): _uuid = uuid.uuid4() else: _uuid = id + + if image is None: + return f"https://{cls.bucket}.s3.{cls.region}.amazonaws.com/default/", _uuid + root = cls.get_team_image_name(_uuid) - thumnail = cls.create_thumnail(image, "team") - s3_client.upload_fileobj(thumnail, cls.bucket, root + "thumnail.jpg") + try: + thumnail = cls.create_thumnail(image, "team") + s3_client.upload_fileobj(thumnail, cls.bucket, root + "thumnail.jpg", ExtraArgs=cls.extra_args) - image.seek(0) - s3_client.upload_fileobj(image, cls.bucket, root + "image.jpg") + image.seek(0) + s3_client.upload_fileobj(image, cls.bucket, root + "image.jpg", ExtraArgs=cls.extra_args) + except UnidentifiedImageError: + raise exception.TeamImageTypeError() return f"https://{cls.bucket}.s3.{cls.region}.amazonaws.com/{root}", _uuid @@ -66,11 +75,14 @@ def delete_team_image_on_s3(cls, id): def upload_user_image_on_s3(cls, id, image): s3_client = cls.client root = cls.get_user_image_name(id) - thumnail = cls.create_thumnail(image, "user") - s3_client.upload_fileobj(thumnail, cls.bucket, root + "thumnail.jpg") - - image.seek(0) - s3_client.upload_fileobj(image, cls.bucket, root + "image.jpg") + try: + thumnail = cls.create_thumnail(image, "user") + s3_client.upload_fileobj(thumnail, cls.bucket, root + "thumnail.jpg", ExtraArgs=cls.extra_args) + + image.seek(0) + s3_client.upload_fileobj(image, cls.bucket, root + "image.jpg", ExtraArgs=cls.extra_args) + except UnidentifiedImageError: + raise exception.TeamImageTypeError() return f"https://{cls.bucket}.s3.{cls.region}.amazonaws.com/{root}" diff --git a/wtnt/core/utils/team.py b/wtnt/core/utils/team.py index bcb24ee..e93a704 100644 --- a/wtnt/core/utils/team.py +++ b/wtnt/core/utils/team.py @@ -52,11 +52,13 @@ def make_data(leader, strs, image, categories, counts, uuid): "explain": strs.get("explain"), "genre": strs.get("genre"), "image": image, - "url": strs.get("urls", []), "category": TeamResponse.make_tech_data(categories, counts), "uuid": uuid, } + if "urls" in strs: + _dict["url"] = strs["urls"] + return _dict @staticmethod diff --git a/wtnt/pytest.ini b/wtnt/pytest.ini new file mode 100644 index 0000000..5f19121 --- /dev/null +++ b/wtnt/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +DJANGO_SETTINGS_MODULE=wtnt.settings +python_files=tests.py test_*.py *_tests.py \ No newline at end of file diff --git a/wtnt/team/apply/service.py b/wtnt/team/apply/service.py index 92412f3..787fb68 100644 --- a/wtnt/team/apply/service.py +++ b/wtnt/team/apply/service.py @@ -1,6 +1,8 @@ -from core.exceptions import NotFoundError, ClosedApplyError, DuplicatedApplyError, SerializerNotValidError +import core.exception.notfound as notfound_exception +import core.exception.apply as apply_exception from core.service import BaseServiceWithCheckLeader from core.utils.team import ApplyResponse +from django.db import IntegrityError from team.models import TeamApply, Team, TeamTech, TeamUser from team.serializers import TeamApplySerializer @@ -12,7 +14,7 @@ def get_applies(self): try: team = Team.objects.get(id=team_id) except Team.DoesNotExist: - raise NotFoundError() + raise notfound_exception.TeamNotFoundError() self.check_leader(user_id, team.leader.id) queryset = TeamApply.objects.filter(is_approved=False, team_id=team_id) @@ -21,31 +23,31 @@ def get_applies(self): serializer = TeamApplySerializer(queryset, many=True) return serializer.data else: - raise NotFoundError() + raise notfound_exception.ApplyNotFoundError() def post_apply(self): - bio = self.request.data.get("bio", None) - tech = self.request.data.get("subCategory") + bio = self.request.data.get("bio", "열심히 하겠습니다!") user_id = self.request.user.id - team_id = self.kwargs.get("team_id") + tech_id = self.kwargs.get("team_id") + try: + teamTech = TeamTech.objects.get(id=tech_id) + except TeamTech.DoesNotExist(): + raise notfound_exception.TechNotFoundError() - teamTech = TeamTech.objects.get(team_id=team_id, tech=tech) if teamTech.need_num <= teamTech.current_num: - raise ClosedApplyError() + raise apply_exception.ClosedApplyError() - apply_data = ApplyResponse.make_data(user_id, team_id, bio, tech) + apply_data = ApplyResponse.make_data(user_id, teamTech.team.id, bio, teamTech.tech) serializer = TeamApplySerializer(data=apply_data) if serializer.is_valid(): try: serializer.save() - except Exception: - raise DuplicatedApplyError() + except IntegrityError: + raise apply_exception.DuplicatedApplyError() return serializer.data - raise SerializerNotValidError(detail=SerializerNotValidError.get_detail(serializer.errors)) - def approve_apply(self): apply_id = self.kwargs.get("team_id") user_id = self.request.user.id @@ -54,14 +56,14 @@ def approve_apply(self): team = Team.objects.get(id=apply.team_id) team_tech = TeamTech.objects.get(team_id=team.id, tech=apply.tech) except TeamApply.DoesNotExist: - raise NotFoundError() + raise notfound_exception.ApplyNotFoundError() except Team.DoesNotExist: - raise NotFoundError() + raise notfound_exception.TeamNotFoundError() except TeamTech.DoesNotExist: - raise NotFoundError() + raise notfound_exception.TechNotFoundError() if team_tech.current_num >= team_tech.need_num: - raise ClosedApplyError() + raise apply_exception.ClosedApplyError() self.check_leader(user_id, team.leader.id) @@ -76,8 +78,6 @@ def approve_apply(self): return {"detail": "Success to update apply"} - raise SerializerNotValidError(detail=SerializerNotValidError.get_detail(serializer.errors)) - def reject_apply(self): apply_id = self.kwargs.get("team_id") user_id = self.request.user.id @@ -85,9 +85,9 @@ def reject_apply(self): apply = TeamApply.objects.get(id=apply_id) team = Team.objects.get(id=apply.team_id) except TeamApply.DoesNotExist: - raise NotFoundError() + raise notfound_exception.ApplyNotFoundError() except Team.DoesNotExist: - raise NotFoundError() + raise notfound_exception.TeamNotFoundError() self.check_leader(user_id, team.leader.id) apply.delete() diff --git a/wtnt/team/apply/urls.py b/wtnt/team/apply/urls.py index cc98ff0..dcbeaad 100644 --- a/wtnt/team/apply/urls.py +++ b/wtnt/team/apply/urls.py @@ -2,5 +2,5 @@ from .views import TeamApplyView urlpatterns = [ - path("", TeamApplyView.as_view()), + path("", TeamApplyView.as_view(), name="team-apply"), ] diff --git a/wtnt/team/apply/views.py b/wtnt/team/apply/views.py index 20de123..ad7f315 100644 --- a/wtnt/team/apply/views.py +++ b/wtnt/team/apply/views.py @@ -2,14 +2,13 @@ from rest_framework.views import APIView from rest_framework.response import Response -from team.serializers import TeamApplySerializer +import core.exception.request as exception from core.permissions import IsApprovedUser from .service import ApplyService class TeamApplyView(APIView): permission_classes = [IsApprovedUser] - serializer_class = TeamApplySerializer def get(self, request, *args, **kwargs): apply_service = ApplyService(request, **kwargs) @@ -18,6 +17,13 @@ def get(self, request, *args, **kwargs): return Response(data, status=status.HTTP_200_OK) def post(self, request, *args, **kwargs): + required_field = ["bio"] + if len(request.data) != len(required_field): + raise exception.InvalidRequestError() + for field in required_field: + if field not in request.data: + raise exception.InvalidRequestError() + apply_service = ApplyService(request, **kwargs) data = apply_service.post_apply() diff --git a/wtnt/team/like/service.py b/wtnt/team/like/service.py index cd05ca8..fa77f2f 100644 --- a/wtnt/team/like/service.py +++ b/wtnt/team/like/service.py @@ -1,4 +1,5 @@ -from core.exceptions import NotFoundError, VersionError, SerializerNotValidError +import core.exception.notfound as notfound_exception +import core.exception.team as team_exception from core.service import BaseService from core.utils.team import LikeResponse from team.models import Likes, Team @@ -20,12 +21,12 @@ def like(self): team.version += 1 team.save() - return LikeResponse.make_data(team.like, False, version) + return LikeResponse.make_data(team.like, False, team.version) - raise VersionError(current_version=team.version) + raise team_exception.TeamLikeVersionError(current_version=team.version) except Team.DoesNotExist: - raise NotFoundError() + raise notfound_exception.TeamNotFoundError() except Likes.DoesNotExist: if version == team.version: @@ -36,8 +37,6 @@ def like(self): team.version += 1 team.save() - return LikeResponse.make_data(team.like, True, version) + return LikeResponse.make_data(team.like, True, team.version) - raise SerializerNotValidError(detail=SerializerNotValidError.get_detail(serializer.errors)) - - raise VersionError(current_version=team.version) + raise team_exception.TeamLikeVersionError(current_version=team.version) diff --git a/wtnt/team/like/urls.py b/wtnt/team/like/urls.py index 9c8b214..a0d3687 100644 --- a/wtnt/team/like/urls.py +++ b/wtnt/team/like/urls.py @@ -2,5 +2,5 @@ from .views import TeamLikeView urlpatterns = [ - path("", TeamLikeView.as_view()), + path("", TeamLikeView.as_view(), name="like-team"), ] diff --git a/wtnt/team/like/views.py b/wtnt/team/like/views.py index 8d078fb..6fffce7 100644 --- a/wtnt/team/like/views.py +++ b/wtnt/team/like/views.py @@ -2,6 +2,7 @@ from rest_framework.response import Response from rest_framework.views import APIView +import core.exception.request as exception from core.permissions import IsApprovedUser from .service import LikeService @@ -10,6 +11,13 @@ class TeamLikeView(APIView): permission_classes = [IsApprovedUser] def post(self, request, *args, **kwargs): + required_field = ["version"] + if len(request.data) != len(required_field): + raise exception.InvalidRequestError() + for field in required_field: + if field not in request.data: + raise exception.InvalidRequestError() + like_service = LikeService(request, **kwargs) data = like_service.like() diff --git a/wtnt/team/models.py b/wtnt/team/models.py index 98846e3..864f2c9 100644 --- a/wtnt/team/models.py +++ b/wtnt/team/models.py @@ -2,6 +2,7 @@ from django.db import models from core.models import TimestampedModel +import core.exception.team as exception from user.models import CustomUser from .manager import LikesManager, TeamManager @@ -23,6 +24,21 @@ class Team(TimestampedModel): is_accomplished = models.BooleanField(default=False) objects = TeamManager() + def clean(self): + if Team.objects.filter(title=self.title).exclude(pk=self.pk).exists(): + raise exception.TeamNameDuplicateError() + + if not (0 < len(self.title) <= 30): + raise exception.TeamNameLengthError() + + valid_genres = ["웹", "앱", "게임"] + if self.genre not in valid_genres: + raise exception.TeamGenreNotValidError() + + def save(self, *args, **kwargs): + self.clean() + super().save(*args, **kwargs) + class TeamApply(TimestampedModel): user = models.ForeignKey(CustomUser, on_delete=models.CASCADE) @@ -41,6 +57,37 @@ class TeamTech(TimestampedModel): current_num = models.IntegerField(default=0) tech = models.CharField(max_length=15) + def clean(self): + valid_categories = [ + "웹", + "IOS", + "안드로이드", + "크로스플랫폼", + "자바", + "파이썬", + "노드", + "UI/UX 기획", + "게임 기획", + "컨텐츠 기획", + "프로젝트 매니저", + "유니티", + "언리얼", + "딥러닝", + "머신러닝", + "데이터 엔지니어", + "게임 그래픽 디자인", + "UI/UX 디자인", + ] + if self.tech not in valid_categories: + raise exception.TeamCategoryNotValidError() + + if self.need_num and not (0 < self.need_num <= 10): + raise exception.TeamMemberCountLengthError() + + def save(self, *args, **kwargs): + self.clean() + super().save(*args, **kwargs) + class Likes(TimestampedModel): team = models.ForeignKey(Team, on_delete=models.CASCADE) diff --git a/wtnt/team/serializers.py b/wtnt/team/serializers.py index 4ee443b..4d3c3fc 100644 --- a/wtnt/team/serializers.py +++ b/wtnt/team/serializers.py @@ -1,6 +1,18 @@ from rest_framework import serializers from .models import Team, TeamTech, TeamApply, Likes from core.fields import BinaryField +import core.exception.team as exception + + +class LeaderInfoIncludedSerializer(serializers.ModelSerializer): + leader_info = serializers.SerializerMethodField(read_only=True) + + def get_leader_info(self, obj): + return { + "name": obj.leader.name, + "id": obj.leader.id, + "image_url": obj.leader.image if "github" in obj.leader.image else obj.leader.image + "thumnail.jpg", + } class TeamTechCreateSerializer(serializers.ModelSerializer): @@ -9,16 +21,15 @@ class Meta: fields = ["id", "tech", "need_num", "current_num"] -class TeamCreateSerializer(serializers.ModelSerializer): +class TeamCreateSerializer(LeaderInfoIncludedSerializer): category = TeamTechCreateSerializer(many=True) view = serializers.IntegerField(read_only=True) image = serializers.CharField(write_only=True) uuid = serializers.UUIDField(write_only=True) leader_id = serializers.IntegerField(write_only=True) image_url = serializers.SerializerMethodField() - leader_info = serializers.SerializerMethodField(read_only=True) explain = BinaryField() - url = BinaryField() + url = BinaryField(required=False, allow_null=True) class Meta: model = Team @@ -39,12 +50,38 @@ class Meta: "uuid", ] - def get_leader_info(self, obj): - return {"name": obj.leader.name, "id": obj.leader.id, "image_url": obj.leader.image + "thumnail.jpg"} - def get_image_url(self, obj): return obj.image + "image.jpg" + from rest_framework.fields import empty + + def run_validation(self, data=empty): + (is_empty_value, data) = self.validate_empty_values(data) + if is_empty_value: + return data + + team_data = {key: value for key, value in data.items() if key != "category"} + instance = Team(**team_data) + instance.clean() + + value = self.to_internal_value(data) + self.run_validators(value) + value = self.validate(value) + + return value + + def validate(self, data): + url = data.get("url", None) + explain = data.get("explain", None) + + if explain is not None and not (0 < len(explain.decode()) <= 2000): + raise exception.TeamExplainLengthError() + + if url is not None and len(url.decode()) == 0: + raise exception.TeamUrlLengthError() + + return data + def create(self, validated_data): techs = validated_data.pop("category") team = Team.objects.create(**validated_data) @@ -89,9 +126,8 @@ class Meta: fields = ["id", "team_id", "user_id", "is_approved", "created_at", "bio", "tech"] -class TeamListSerializer(serializers.ModelSerializer): +class TeamListSerializer(LeaderInfoIncludedSerializer): category = TeamTechCreateSerializer(many=True, read_only=True) - leader_info = serializers.SerializerMethodField(read_only=True) image = serializers.CharField(write_only=True) image_url = serializers.SerializerMethodField() @@ -99,9 +135,6 @@ class Meta: model = Team fields = ["id", "title", "image", "image_url", "category", "leader_info", "like", "version", "view", "genre"] - def get_leader_info(self, obj): - return {"id": obj.leader.id, "name": obj.leader.name, "image_url": obj.leader.image + "thumnail.jpg"} - def get_image_url(self, obj): return obj.image + "thumnail.jpg" @@ -115,12 +148,7 @@ class Meta: fields = ["user_id", "team_id"] -class TeamManageActivitySerializer(serializers.ModelSerializer): - leader_info = serializers.SerializerMethodField(read_only=True) - +class TeamManageActivitySerializer(LeaderInfoIncludedSerializer): class Meta: model = Team fields = ["id", "title", "leader_info"] - - def get_leader_info(self, obj): - return {"id": obj.leader.id, "name": obj.leader.name, "image_url": obj.leader.image + "thumnail.jpg"} diff --git a/wtnt/team/team/service.py b/wtnt/team/team/service.py index 52e09bd..696552a 100644 --- a/wtnt/team/team/service.py +++ b/wtnt/team/team/service.py @@ -1,4 +1,5 @@ -from core.exceptions import SerializerNotValidError, NotFoundError, KeywordNotMatchError +import core.exception.team as team_exception +import core.exception.notfound as notfound_exception from core.pagenations import TeamPagination from core.service import BaseServiceWithCheckLeader from core.utils.team import TeamResponse @@ -11,7 +12,7 @@ class TeamService(BaseServiceWithCheckLeader, TeamPagination): def create_team(self): user_id = self.request.user.id - url, uuid = S3Utils.upload_team_image_on_s3(self.request.FILES.get("image")) + url, uuid = S3Utils.upload_team_image_on_s3(self.request.FILES.get("image", None)) team_data = TeamResponse.make_data( user_id, @@ -30,8 +31,7 @@ def create_team(self): teamUser.save() return TeamResponse.get_detail_response(serializer.data, user_id) - - raise SerializerNotValidError(detail=SerializerNotValidError.get_detail(serializer.errors)) + print("말도안돼 왜 validation 통과 못함?") def update_team(self): user_id = self.request.user.id @@ -40,10 +40,10 @@ def update_team(self): try: team = Team.objects.get(id=team_id) except Team.DoesNotExist: - raise NotFoundError() + raise notfound_exception.TeamNotFoundError() self.check_leader(user_id, team.leader.id) - if self.request.FILES.get("image"): + if self.request.FILES.get("image", None): url, _ = S3Utils.upload_team_image_on_s3(self.request.FILES.get("image"), id=team.uuid) else: url = team.image @@ -62,8 +62,6 @@ def update_team(self): serializer.save() return TeamResponse.get_detail_response(serializer.data, user_id) - raise SerializerNotValidError(detail=SerializerNotValidError.get_detail(serializer.errors)) - def get_team_detail(self): team_id = self.kwargs.get("team_id") user_id = self.request.user.id if self.request.user.id else None @@ -79,18 +77,18 @@ def get_team_detail(self): return TeamResponse.get_detail_response(serializer.data, user_id) except Team.DoesNotExist: - raise NotFoundError() + raise notfound_exception.TeamNotFoundError() def get_paginated_team_list(self): user_id = self.request.user.id keyword = self.request.query_params.get("keyword") if keyword == "inprogress": - queryset = Team.objects.filter(is_accomplished=False).all() + queryset = Team.objects.filter(is_accomplished=False, is_approved=True).all() elif keyword == "accomplished": - queryset = Team.objects.filter(is_accomplished=True).all() + queryset = Team.objects.filter(is_accomplished=True, is_approved=True).all() else: - raise KeywordNotMatchError() + raise team_exception.TeamKeywordNotMatchError() if queryset: paginated = self.paginate_queryset(queryset, self.request, view=self) @@ -98,4 +96,4 @@ def get_paginated_team_list(self): data = TeamResponse.get_team_list_response(serializer.data, user_id) return self.get_paginated_response(data) else: - raise NotFoundError() + raise notfound_exception.TeamNotFoundError() diff --git a/wtnt/team/team/urls.py b/wtnt/team/team/urls.py index 96e8353..21ae164 100644 --- a/wtnt/team/team/urls.py +++ b/wtnt/team/team/urls.py @@ -2,7 +2,7 @@ from .views import TeamView, TeamListView, TeamDetailView urlpatterns = [ - path("create", TeamView.as_view()), - path("list", TeamListView.as_view()), - path("detail/", TeamDetailView.as_view()), + path("create", TeamView.as_view(), name="create-team"), + path("list", TeamListView.as_view(), name="pagenated-team-main"), + path("detail/", TeamDetailView.as_view(), name="detail-team"), ] diff --git a/wtnt/team/team/views.py b/wtnt/team/team/views.py index ad60075..a5e9544 100644 --- a/wtnt/team/team/views.py +++ b/wtnt/team/team/views.py @@ -5,6 +5,7 @@ from django.contrib.auth import get_user_model from django_redis import get_redis_connection +import core.exception.request as exception from core.permissions import IsApprovedUser from .service import TeamService @@ -17,6 +18,13 @@ class TeamView(APIView): permission_classes = [IsApprovedUser] def post(self, request, *args, **kwargs): + required_field = ["title", "genre", "explain", "subCategory", "memberCount"] + if not (5 <= len(request.data) <= 7): + raise exception.InvalidRequestError() + for field in required_field: + if field not in request.data: + raise exception.InvalidRequestError() + team_service = TeamService(request) data = team_service.create_team() return Response(data, status=status.HTTP_201_CREATED) @@ -32,6 +40,13 @@ def get(self, request, *args, **kwargs): return Response(data, status=status.HTTP_200_OK) def put(self, request, *args, **kwargs): + required_field = ["title", "genre", "explain", "subCategory", "memberCount"] + if not (5 <= len(request.data) <= 7): + raise exception.InvalidRequestError() + for field in required_field: + if field not in request.data: + raise exception.InvalidRequestError() + team_service = TeamService(request, **kwargs) data = team_service.update_team() diff --git a/wtnt/team/tests.py b/wtnt/team/tests.py deleted file mode 100644 index a79ca8b..0000000 --- a/wtnt/team/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -# from django.test import TestCase - -# Create your tests here. diff --git a/wtnt/tests/__init__.py b/wtnt/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wtnt/tests/auth/__init__.py b/wtnt/tests/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wtnt/tests/auth/conftest.py b/wtnt/tests/auth/conftest.py new file mode 100644 index 0000000..b7432ea --- /dev/null +++ b/wtnt/tests/auth/conftest.py @@ -0,0 +1,82 @@ +import pytest +import requests_mock + +from django.contrib.auth import get_user_model +from allauth.socialaccount.models import SocialAccount +from rest_framework_simplejwt.tokens import AccessToken, RefreshToken + +user = get_user_model() + + +@pytest.fixture +def github_mock(): + with requests_mock.Mocker() as m: + m.post( + "https://github.com/login/oauth/access_token", + json={ + "access_token": "fake-access-token", + "scope": "", + "token_type": "bearer", + }, + ) + m.get( + "https://api.github.com/user", + json={ + "id": 123456, + "login": "testuser", + "name": "test", + "avatar_url": "https://avatars.githubusercontent.com/u/123456?v=4", + }, + ) + yield m + + +@pytest.fixture +def initial_user(): + User = user.objects.create(id=1, name="test", email="testuser@sample.com", image="testimage", password="testpw") + + return User + + +@pytest.fixture +def registered_user(): + User = user.objects.create(id=1, name="test", email="testuser@gmail.com", image="testimage/", password="testpw") + SocialAccount.objects.create( + id=1, + provider="github", + uid=123456, + last_login=None, + date_joined=None, + extra_data={"id": 123456, "login": "testuser", "avatar_url": "testimage/"}, + user_id=1, + ) + + return User + + +@pytest.fixture +def initial_socialaccount(initial_user): + SocialAccount.objects.create( + id=1, + provider="github", + last_login=None, + date_joined=None, + extra_data={"id": 123456, "login": "testuser", "avatar_url": "testimage/"}, + user_id=1, + ) + + +@pytest.fixture +def setup_email_code(mock_redis): + mock_redis.set("testuser@gmail.com", "test") + yield mock_redis + + +@pytest.fixture +def access_token(registered_user): + return AccessToken.for_user(registered_user) + + +@pytest.fixture +def refresh_token(registered_user): + return RefreshToken.for_user(registered_user) diff --git a/wtnt/tests/auth/test_jwt.py b/wtnt/tests/auth/test_jwt.py new file mode 100644 index 0000000..d6a4637 --- /dev/null +++ b/wtnt/tests/auth/test_jwt.py @@ -0,0 +1,50 @@ +import pytest +from django.urls import reverse +from rest_framework import status + + +@pytest.mark.django_db +class TestJWT: + @pytest.fixture(autouse=True) + def setup(self, api_client, mock_redis): + self.api_client = api_client + self.mock_redis = mock_redis + + def test_token_refresh(self, access_token, refresh_token): + url = reverse("token-refresh") + + self.mock_redis.set("1", str(refresh_token)) + + headers = {"Authorization": f"Bearer {str(access_token)}", "Content-Type": "application/json"} + response = self.api_client.post(url, {}, format="json", headers=headers) + + assert response.status_code == status.HTTP_200_OK + assert "access" in response.headers + + def test_token_refresh_failed(self, access_token): + url = reverse("token-refresh") + + headers = {"Authorization": f"Bearer {str(access_token)}", "Content-Type": "application/json"} + + response = self.api_client.post(url, {}, format="json", headers=headers) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + + def test_authencation_failed(self, registered_user): + user_id = registered_user.id + + url = reverse("profile-team-manage", args=[user_id]) # Random View Need Authentication + + response = self.api_client.get(url) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + + def test_authentication_failed_by_permission(self, registered_user, access_token): + user_id = registered_user.id + + headers = {"Authorization": f"Bearer {str(access_token)}", "Content-Type": "application/json"} + url = reverse("profile-team-manage", args=[user_id]) + + response = self.api_client.get(url, headers=headers) + + assert response.status_code == status.HTTP_403_FORBIDDEN diff --git a/wtnt/tests/auth/test_social_login.py b/wtnt/tests/auth/test_social_login.py new file mode 100644 index 0000000..fd40bf2 --- /dev/null +++ b/wtnt/tests/auth/test_social_login.py @@ -0,0 +1,108 @@ +import pytest +from django.urls import reverse +from rest_framework import status + + +@pytest.mark.django_db +class TestSocialLogin: + @pytest.fixture(autouse=True) + def setup(self, api_client, social_app, github_mock, mock_redis): + self.api_client = api_client + self.social_app = social_app + self.github_mock = github_mock + self.mock_redis = mock_redis + + def test_github_initial_register(self): + url = reverse("github-login") + data = {"access_token": "fake-access-token"} + response = self.api_client.post(url, data, format="json") + assert response.status_code == status.HTTP_200_OK + + response_data = response.data + + assert "registered" in response_data + assert not response_data["registered"] + assert "user" not in response_data + + def test_github_login(self, registered_user): + url = reverse("github-login") + data = {"access_token": "fake-access-token"} + response = self.api_client.post(url, data, foramt="json") + assert response.status_code == status.HTTP_200_OK + + response_data = response.data + + assert response_data["registered"] + assert response_data["user"]["name"] == "test" + + def test_github_finish(self, initial_user, initial_socialaccount, setup_email_code): + url = reverse("github-finish") + data = { + "email": "testuser@gmail.com", + "student_num": 111111111, + "name": "test", + "position": "test", + "code": "test", + } + assert "test" == self.mock_redis.get("testuser@gmail.com").decode() + self.api_client.force_authenticate(user=initial_user) + response = self.api_client.post(url, data, format="json") + assert response.status_code == status.HTTP_201_CREATED + + responsed_user_data = response.data["user"] + + assert "test" == responsed_user_data["name"] + assert 111111111 == responsed_user_data["student_num"] + assert 1 == responsed_user_data["id"] + assert self.mock_redis.get("testuser@gmail.com") is None + + def test_github_finish_failed_by_email_code(self, initial_user, initial_socialaccount, setup_email_code): + url = reverse("github-finish") + data = { + "email": "testuser@gmail.com", + "student_num": 111111111, + "name": "test", + "position": "test", + "code": "wrong", + } + self.api_client.force_authenticate(user=initial_user) + response = self.api_client.post(url, data, format="json") + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_github_finish_failed_by_wrong_data(self, initial_user, initial_socialaccount, setup_email_code): + url = reverse("github-finish") + data = { + "email": "testuser@gmail.com", + "student_num": 111111111, + "name": "verylongtestname", + "position": "test", + "code": "test", + } + self.api_client.force_authenticate(user=initial_user) + response = self.api_client.post(url, data, format="json") + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_github_finish_failed_by_missing_data(self, initial_user, initial_socialaccount, setup_email_code): + url = reverse("github-finish") + data = { + "email": "testuser@gmail.com", + "student_num": 111111111, + "position": "test", + "code": "test", + } + self.api_client.force_authenticate(user=initial_user) + response = self.api_client.post(url, data, format="json") + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_github_finish_failed_by_wrong_type_data(self, initial_user, initial_socialaccount, setup_email_code): + url = reverse("github-finish") + data = { + "email": "testuser@gmail.com", + "student_num": "test", + "name": "test", + "position": "test", + "code": "test", + } + self.api_client.force_authenticate(user=initial_user) + response = self.api_client.post(url, data, format="json") + assert response.status_code == status.HTTP_400_BAD_REQUEST diff --git a/wtnt/tests/conftest.py b/wtnt/tests/conftest.py new file mode 100644 index 0000000..8ae9508 --- /dev/null +++ b/wtnt/tests/conftest.py @@ -0,0 +1,29 @@ +import pytest +import fakeredis + +from django.contrib.sites.models import Site +from allauth.socialaccount.models import SocialApp +from rest_framework.test import APIClient +from unittest.mock import patch + + +@pytest.fixture +def api_client(): + return APIClient() + + +@pytest.fixture +def social_app(db): + site = Site.objects.get_current() + social_app = SocialApp.objects.create( + provider="github", name="GitHub", client_id="fake-client-id", secret="fake-client-secret" + ) + social_app.sites.add(site) + return social_app + + +@pytest.fixture +def mock_redis(): + mock_redis_client = fakeredis.FakeStrictRedis() + with patch("core.utils.redis.RedisUtils.client", mock_redis_client): + yield mock_redis_client diff --git a/wtnt/user/auth/service.py b/wtnt/user/auth/service.py index dde1420..584b812 100644 --- a/wtnt/user/auth/service.py +++ b/wtnt/user/auth/service.py @@ -3,11 +3,12 @@ from rest_framework_simplejwt.tokens import AccessToken from rest_framework_simplejwt.exceptions import TokenError, InvalidToken -from core.utils.auth import get_user_info from core.utils.redis import RedisUtils from core.service import BaseService -from core.exceptions import CodeNotMatchError, RefreshTokenExpiredError, CeleryTaskError +import core.exception.login as login_exception +import core.exception.token as token_exception from user.tasks import send_email +from user.serializers import UserSerializer User = get_user_model() @@ -28,7 +29,7 @@ def process_response_data(self, response_data): user_id = response_data["user"]["pk"] if refresh_token: - RedisUtils.set_refresh_token_in_cache(user_id, refresh_token) + RedisUtils.set_refresh_token(user_id, refresh_token) response_data.pop("refresh") _, email = response_data["user"]["email"].split("@") @@ -39,18 +40,28 @@ def process_response_data(self, response_data): else: response_data["registered"] = True user = User.objects.get(id=user_id) - response_data["user"] = get_user_info(user) + response_data["user"] = UserSerializer(user).data response_data.pop("access") return response_data, access_token + def logout(self): + _, access_token = self.request.META.get("HTTP_AUTHORIZATION").split(" ") + try: + user_id = AccessToken(access_token, verify=False).payload.get("user_id") + except TokenError: + raise token_exception.InvalidTokenError() + RedisUtils.delete_refresh_token(user_id) + class RegisterService(BaseService): def finish_register_by_user_input(self): code = self.request.data.get("code") email = self.request.data.get("email") - if code != RedisUtils.get_code_in_redis_from_email(): - raise CodeNotMatchError() + code_from_redis = RedisUtils.get_code_in_redis_from_email(email) + + if code_from_redis is None or code != code_from_redis.decode(): + raise login_exception.EmailCodeNotMatchAfterAuthError() extra_data = SocialAccount.objects.get(user_id=self.request.user.id).extra_data user = User.objects.get(id=self.request.user.id) @@ -58,19 +69,22 @@ def finish_register_by_user_input(self): RedisUtils.delete_code_in_redis_from_email(email) - return user + return UserSerializer(user).data class RefreshService(BaseService): def extract_refresh_token(self): _, access_token = self.request.META.get("HTTP_AUTHORIZATION").split(" ") - user_id = AccessToken(access_token, verify=False).payload.get("user_id") + try: + user_id = AccessToken(access_token, verify=False).payload.get("user_id") + except TokenError: + raise token_exception.InvalidTokenError() - refresh_token = RedisUtils.get_refresh_token_in_cache(user_id) + refresh_token = RedisUtils.get_refresh_token(user_id) if not refresh_token: - raise RefreshTokenExpiredError() + raise token_exception.RefreshTokenExpiredError() - return refresh_token + return refresh_token.decode() def get_new_access_token_from_serializer(self, serializer): try: @@ -86,18 +100,22 @@ def get_new_access_token_from_serializer(self, serializer): class EmailVerifyService(BaseService): def send_email(self): email = self.request.data.get("email") - try: send_email.delay(email) except Exception as e: - raise CeleryTaskError(detail=str(e)) + print(f"Exception: {e}") + raise login_exception.EmailCeleryError() def check_code(self): code = self.request.data.get("code") email = self.request.data.get("email") + code_from_redis = RedisUtils.get_code_in_redis_from_email(email) + + if code_from_redis is None: + raise login_exception.EmailTimeoutError() - if code == RedisUtils.get_code_in_redis_from_email(email): + if code == code_from_redis.decode(): RedisUtils.set_code_in_redis_from_email(email, code) return code - raise CodeNotMatchError() + raise login_exception.EmailCodeNotMatchError() diff --git a/wtnt/user/auth/urls.py b/wtnt/user/auth/urls.py index 0f7b301..a2120cf 100644 --- a/wtnt/user/auth/urls.py +++ b/wtnt/user/auth/urls.py @@ -1,6 +1,7 @@ from django.urls import path from .views import ( GithubLoginView, + LogoutView, GithubOAuthCallBackView, FinishGithubLoginView, WtntTokenRefreshView, @@ -9,8 +10,9 @@ urlpatterns = [ path("github/callback", GithubOAuthCallBackView.as_view()), - path("github/login", GithubLoginView.as_view()), - path("github/finish", FinishGithubLoginView.as_view()), - path("token/refresh", WtntTokenRefreshView.as_view()), - path("email", EmailVerifyView.as_view()), + path("github/login", GithubLoginView.as_view(), name="github-login"), + path("github/finish", FinishGithubLoginView.as_view(), name="github-finish"), + path("logout", LogoutView.as_view(), name="logout"), + path("token/refresh", WtntTokenRefreshView.as_view(), name="token-refresh"), + path("email", EmailVerifyView.as_view(), name="verify-email"), ] diff --git a/wtnt/user/auth/views.py b/wtnt/user/auth/views.py index bef0de1..4cb5b7a 100644 --- a/wtnt/user/auth/views.py +++ b/wtnt/user/auth/views.py @@ -3,13 +3,14 @@ from rest_framework.views import APIView from rest_framework.request import Request from rest_framework.response import Response -from rest_framework.permissions import AllowAny +from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework import status from rest_framework_simplejwt.views import TokenRefreshView from dj_rest_auth.registration.views import SocialLoginView from django.contrib.auth import get_user_model from django_redis import get_redis_connection +import core.exception.request as exception import requests from .service import AuthService, RegisterService, RefreshService, EmailVerifyService from user.serializers import UserSerializer @@ -25,6 +26,13 @@ class GithubLoginView(SocialLoginView): client_class = OAuth2Client def post(self, request, *args, **kwargs): + required_field = ["code"] + if len(request.data) != len(required_field): + raise exception.InvalidRequestError() + for field in required_field: + if field not in request.data: + raise exception.InvalidRequestError() + auth_service = AuthService(request) self.callback_url = auth_service.determine_callback_url() @@ -37,6 +45,19 @@ def post(self, request, *args, **kwargs): return response +class LogoutView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request, *args, **kwargs): + auth_service = AuthService(request) + auth_service.logout() + + response = Response("", status=status.HTTP_204_NO_CONTENT) + response.headers["access"] = "" + + return response + + class GithubOAuthCallBackView(APIView): permission_classes = [AllowAny] @@ -63,11 +84,17 @@ class FinishGithubLoginView(APIView): serializer_class = UserSerializer def post(self, request): + required_field = ["student_num", "code", "email", "name", "position"] + if len(request.data) != len(required_field): + raise exception.InvalidRequestError() + for field in required_field: + if field not in request.data: + raise exception.InvalidRequestError() + register_service = RegisterService(request) - user = register_service.finish_register_by_user_input() - serializer = self.serializer_class(user) + data = register_service.finish_register_by_user_input() - return Response({"user": serializer.data}, status=status.HTTP_201_CREATED) + return Response({"user": data}, status=status.HTTP_201_CREATED) class WtntTokenRefreshView(TokenRefreshView): @@ -88,12 +115,26 @@ class EmailVerifyView(APIView): permission_classes = [AllowAny] def patch(self, request, *args, **kwargs): + required_field = ["code", "email"] + if len(request.data) != len(required_field): + raise exception.InvalidRequestError() + for field in required_field: + if field not in request.data: + raise exception.InvalidRequestError() + email_verify_service = EmailVerifyService(request) code = email_verify_service.check_code() return Response({"code": code}, status=status.HTTP_200_OK) def post(self, request, *args, **kwargs): + required_field = ["email"] + if len(request.data) != len(required_field): + raise exception.InvalidRequestError() + for field in required_field: + if field not in request.data: + raise exception.InvalidRequestError() + email_verify_service = EmailVerifyService(request) email_verify_service.send_email() diff --git a/wtnt/user/models.py b/wtnt/user/models.py index 1abf086..e7679c1 100644 --- a/wtnt/user/models.py +++ b/wtnt/user/models.py @@ -1,6 +1,8 @@ from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin from django.db import models + from .managers import UserManager +import core.exception.login as exception from core.models import TimestampedModel @@ -10,10 +12,10 @@ class CustomUser(AbstractBaseUser, PermissionsMixin, TimestampedModel): email = models.EmailField(unique=True) social_id = models.IntegerField(null=True) - name = models.CharField(max_length=5) + name = models.CharField(max_length=12) university = models.CharField(max_length=8, default="부경대학교") club = models.CharField(max_length=10, default="WAP") - student_num = models.CharField(max_length=20, null=True) + student_num = models.CharField(max_length=12, null=True) position = models.CharField(max_length=15, null=True) explain = models.CharField(max_length=500, default="열심히 하겠습니다!") image = models.CharField(max_length=200, null=True) @@ -41,9 +43,29 @@ def finish_register(self, extra_data, request_data, *args, **kwargs): self.email = extra_data.get("login") + "@github.com" self.image = extra_data.get("avatar_url") self.position = request_data.get("position") - + self.clean() super().save(*args, **kwargs) + def clean(self): + if any(char.isdigit() or not char.isalnum() for char in self.name): + raise exception.UserNameNotValidError() + + if not (2 <= len(self.name) <= 12): + raise exception.UserNameTooLongError() + + if not str(self.student_num).isdigit(): + raise exception.StudentNumNotValidError() + + if not (7 <= len(str(self.student_num)) <= 10): + raise exception.StudentNumTooLongError() + + if CustomUser.objects.filter(student_num=self.student_num).exclude(pk=self.pk).exists(): + raise exception.StudentNumDuplicatedError() + + valid_positions = ["백엔드", "프론트엔드", "AI", "디자이너"] + if self.position not in valid_positions: + raise exception.PositionNotValidError() + class UserUrls(models.Model): user = models.OneToOneField(CustomUser, primary_key=True, on_delete=models.CASCADE) diff --git a/wtnt/user/profile/service.py b/wtnt/user/profile/service.py index 680a698..1fcfb62 100644 --- a/wtnt/user/profile/service.py +++ b/wtnt/user/profile/service.py @@ -1,7 +1,8 @@ from django.contrib.auth import get_user_model +import core.exception.notfound as notfound_exception +import core.exception.team as team_exception from core.service import BaseServiceWithCheckOwnership -from core.exceptions import NotFoundError, SerializerNotValidError, KeywordNotMatchError from user.models import UserUrls, UserTech from team.models import Team, TeamApply, TeamUser, Likes, TeamTech from user.serializers import UserUrlSerializer, UserTechSerializer, UserProfileSerializer @@ -26,7 +27,7 @@ def process_response_data(self): return ProfileResponse.make_data(user_serializer.data, url, tech, owner_id) except User.DoesNotExist: - raise NotFoundError() + raise notfound_exception.UserNotFoundError() def get_url_data(self, user_id): try: @@ -62,8 +63,6 @@ def update_user_info(self): serializer.save() return {"explain": explain, "position": position, "image_url": url + "image.jpg"} - raise SerializerNotValidError(detail=SerializerNotValidError.get_detail(serializer.errors)) - def update_user_url_info(self): owner_id = self.kwargs.get("user_id") user_id = self.request.user.id @@ -81,8 +80,6 @@ def update_user_url_info(self): data = ProfileResponse.make_url_data(serializer.data) return data - raise SerializerNotValidError(detail=SerializerNotValidError.get_detail(serializer.errors)) - def update_tech_info(self): owner_id = self.kwargs.get("user_id") user_id = self.request.user.id @@ -100,8 +97,6 @@ def update_tech_info(self): data = ProfileResponse.make_tech_data(serializer.data) return data - raise SerializerNotValidError(detail=SerializerNotValidError.get_detail(serializer.errors)) - class MyActivityServcie(BaseServiceWithCheckOwnership): def get_my_activity(self): @@ -119,7 +114,7 @@ def get_my_activity(self): elif keyword == "inprogress": team_data = Team.objects.filter(id__in=team_ids, is_accomplished=False, is_approved=True) else: - raise KeywordNotMatchError() + raise team_exception.TeamKeywordNotMatchError() serializer = TeamListSerializer(team_data, many=True) teams = TeamResponse.get_team_list_response(serializer.data, user_id) @@ -157,7 +152,7 @@ def delete_or_leave_team(self): try: team = Team.objects.get(id=team_id) except Team.DoesNotExist: - raise NotFoundError() + raise notfound_exception.TeamNotFoundError() if team.leader.id == user_id: S3Utils.delete_team_image_on_s3(team.uuid) @@ -169,9 +164,9 @@ def delete_or_leave_team(self): team_user = TeamUser.objects.get(team_id=team_id, user_id=user_id) team_tech = TeamTech.objects.get(tech=team_apply.tech, team_id=team_id, user_id=user_id) except TeamApply.DoesNotExist: - raise NotFoundError() + raise notfound_exception.ApplyNotFoundError() except TeamUser.DoesNotExist: - raise NotFoundError() + raise notfound_exception.TeamUserNotFoundError() team_apply.delete() team_user.delete() diff --git a/wtnt/user/profile/urls.py b/wtnt/user/profile/urls.py index d2091be..aeb77e7 100644 --- a/wtnt/user/profile/urls.py +++ b/wtnt/user/profile/urls.py @@ -9,10 +9,10 @@ ) urlpatterns = [ - path("", UserProfileView.as_view()), - path("tech/", UserTechView.as_view()), - path("url/", UserUrlView.as_view()), - path("activity/", UserMyActivityView.as_view()), - path("team-manage/", UserManageActivityView.as_view()), - path("like/", UserLikeTeamView.as_view()), + path("", UserProfileView.as_view(), name="user-profile"), + path("tech/", UserTechView.as_view(), name="update-user-tech"), + path("url/", UserUrlView.as_view(), name="update-user-url"), + path("activity/", UserMyActivityView.as_view(), name="user-my-activity"), + path("team-manage/", UserManageActivityView.as_view(), name="profile-team-manage"), + path("like/", UserLikeTeamView.as_view(), name="like-team-list"), ] diff --git a/wtnt/user/profile/views.py b/wtnt/user/profile/views.py index bfca3c8..f92ca99 100644 --- a/wtnt/user/profile/views.py +++ b/wtnt/user/profile/views.py @@ -4,6 +4,7 @@ from rest_framework import status from rest_framework.permissions import AllowAny +import core.exception.request as exception from core.permissions import IsApprovedUser from .service import ProfileService, MyActivityServcie, MyTeamManageService @@ -20,6 +21,13 @@ def get(self, request, *args, **kwargs): return Response(data, status=status.HTTP_200_OK) def patch(self, request, *args, **kwargs): + required_field = ["explain", "position"] + if not (2 <= len(request.data) <= 3): + raise exception.InvalidRequestError() + for field in required_field: + if field not in request.data: + raise exception.InvalidRequestError() + profile_service = ProfileService(request, **kwargs) profile_service.check_ownership() data = profile_service.update_user_info() @@ -31,6 +39,13 @@ class UserTechView(APIView): permission_classes = [AllowAny] def post(self, request, *args, **kwargs): + required_field = ["tech"] + if len(request.data) != len(required_field): + raise exception.InvalidRequestError() + for field in required_field: + if field not in request.data: + raise exception.InvalidRequestError() + profile_service = ProfileService(request, **kwargs) profile_service.check_ownership() data = profile_service.update_tech_info() @@ -42,6 +57,13 @@ class UserUrlView(APIView): permission_classes = [AllowAny] def post(self, request, *args, **kwargs): + required_field = ["url"] + if len(request.data) != len(required_field): + raise exception.InvalidRequestError() + for field in required_field: + if field not in request.data: + raise exception.InvalidRequestError() + profile_service = ProfileService(request, **kwargs) profile_service.check_ownership() data = profile_service.update_user_url_info() diff --git a/wtnt/user/serializers.py b/wtnt/user/serializers.py index 457f50b..b140e3d 100644 --- a/wtnt/user/serializers.py +++ b/wtnt/user/serializers.py @@ -21,7 +21,10 @@ class Meta: fields = BaseUserSerializer.Meta.fields + ["image", "image_url"] def get_image_url(self, obj): - return obj.image + "image.jpg" + if "github" not in obj.image: + return obj.image + "image.jpg" + else: + return obj.image class UserProfileSerializer(UserSerializer): diff --git a/wtnt/user/tasks.py b/wtnt/user/tasks.py index 2a76695..66282b0 100644 --- a/wtnt/user/tasks.py +++ b/wtnt/user/tasks.py @@ -9,12 +9,12 @@ client = get_redis_connection("default") -def make_random_code_for_register(self): +def make_random_code_for_register(): digit_and_alpha = string.ascii_letters + string.digits return "".join(secrets.choice(digit_and_alpha) for _ in range(6)) -def get_template(self, code): +def get_template(code): return """ diff --git a/wtnt/user/tests.py b/wtnt/user/tests.py deleted file mode 100644 index a79ca8b..0000000 --- a/wtnt/user/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -# from django.test import TestCase - -# Create your tests here. diff --git a/wtnt/wtnt/settings.py b/wtnt/wtnt/settings.py index 047c9a1..528dfbd 100644 --- a/wtnt/wtnt/settings.py +++ b/wtnt/wtnt/settings.py @@ -28,7 +28,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = env("DEBUG") - +MY_DOMAIN = "https://api.whatmeow.shop" SITE_ID = 1 ALLOWED_HOSTS = ["*"] @@ -77,7 +77,7 @@ REST_FRAMEWORK = { - "DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework_simplejwt.authentication.JWTAuthentication",), + "DEFAULT_AUTHENTICATION_CLASSES": ("core.authenticator.CustomJWTAuthentication",), }