- 회원가입 : POST/users/signup
- 로그인 : POST/users/signin
- 게시물 목록 : GET/bulletin-board/post
- 게시물 작성 : POST/bulletin-board/post
- 게시물 수정 : PATCH/bulletin-board/post/int:post_id
- 게시물 삭세 : DELETE/bulletin-board/post/int:post_id
- 게시물 상세 : GET/bulletin-board/post-detail/int:post_id
🥕 github:
https://github.com/chrisYang256/wnated-wecode-FreeOnboarding
회원가입 및 로그인한 유저만 게시글 작성, 수정, 삭제가 가능하도록 설계하여
회원가입 및 로그인 데코레이터를 통한 인증, 인가 기능을 추가하였습니다.
- models.py
from django.db import models
class User(models.Model):
name = models.CharField(max_length = 20)
password = models.CharField(max_length = 200)
email = models.EmailField(max_length = 50, unique = True)
created_at = models.DateTimeField(auto_now_add = True)
updated_at = models.DateTimeField(auto_now = True)
class Meta:
db_table = 'users'
- views.py
import json
import re
import bcrypt
import jwt
from django.http import JsonResponse
from django.views import View
from my_settings import SECRET_KEY, ALGORITHMS
from users.models import User
class SignUp(View):
def post(self, request):
data = json.loads(request.body)
REGX_EMAIL = '^[a-zA-Z0-9+-_.]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$'
REGX_PASSWORD = '^(?=.*\d)(?=.*[a-zA-Z])[0-9a-zA-Z!@#$%^&*]{8,20}$'
try:
if User.objects.filter(email=data['email']).exists():
return JsonResponse({'message': 'EXIST_EMAIL'}, status=400)
if not re.match(REGX_EMAIL, data['email']):
return JsonResponse({'message': 'INVALID_EMAIL_FORM'}, status=400)
if not re.match(REGX_PASSWORD, data['password']):
return JsonResponse({'message': 'INVALID_PASSWORD_FORM'}, status=400)
password = bcrypt.hashpw(data['password'].encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
User.objects.create(
name = data['name'],
email = data['email'],
password = password,
)
return JsonResponse({'message': 'SUCCESS'}, status=201)
except KeyError:
return JsonResponse({'message': 'KEY_ERROR'}, status=400)
class SignIn(View):
def post(self, request):
try:
data = json.loads(request.body)
user = User.objects.get(email = data['email'])
if not (user and bcrypt.checkpw(data['password'].encode('utf-8'), user.password.encode('utf-8'))):
return JsonResponse({"message" : "INVALID_USER"}, status=401)
except KeyError:
return JsonResponse({"message" : "KEY_ERROR"}, status=400)
access_token = jwt.encode({'user_id' : user.id}, SECRET_KEY, ALGORITHMS)
return JsonResponse({'access_token' : access_token}, status=201)
- utils.py
import jwt
from django.http import JsonResponse
from my_settings import SECRET_KEY, ALGORITHMS
from users.models import User
def login_decorator(func):
def wrapper(self, request, *args, **kwargs):
if 'Authorization' not in request.headers:
return JsonResponse ({'message' : 'UNAUTHORIZED'}, status=401)
access_token = request.headers.get('Authorization')
try:
payload = jwt.decode(access_token, SECRET_KEY, algorithms=ALGORITHMS)
user = User.objects.get(id=payload['user_id'])
request.user = user
except jwt.exceptions.DecodeError:
return JsonResponse({'MESSAGE': 'INVALID_TOKEN'}, status=401)
except User.DoesNotExist:
return JsonResponse({'MESSAGE': 'INVALID_USER'}, status=401)
return func(self, request, *args, **kwargs)
return wrapper
- urls.py
from django.urls import path
from users.views import SignIn, SignUp
urlpatterns = [
path('/signup', SignUp.as_view()),
path('/signin', SignIn.as_view()),
]
- SignUp API
python의 re.match()
를 이용하여 정규표현식과 입력값을 대조하여
예외처리를 하였습니다.
- utils.py
login_decorator함수가 데코레이터로 붙은 API들은 토큰 검사 후 토큰을 decode하여
user_id를 request.user에 담아 넘겨줌으로써 유효성 검사 및 HTTP stateless를
해결하였습니다.
RESTful API를 준수하였습니다.
게시물 보기 외에는 모두 인가 관련 예외처리를 하였습니다.
- models.py
from django.db import models
class BulletinBoard(models.Model):
author = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, related_name='bulletin_board')
title = models.CharField(max_length=100)
description = models.CharField(max_length=5000)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'bulletin_boards'
- views.py
import json
import time
import datetime
from django.http import JsonResponse
from django.views import View
from users.utils import login_decorator
from .models import BulletinBoard
from users.models import User
class Post(View):
def get(self, request):
limit = int(request.GET.get('limit', 5))
offset = int(request.GET.get('offset', 0))
limit = limit + offset
post_list = BulletinBoard.objects.all()[offset:limit]
results = [
{
'author' : post.author.name if post.author else "사라진 회원입니다.",
'title' : post.title,
'created_at' : post.created_at
} for post in post_list]
return JsonResponse({'results' : results}, status=200)
@login_decorator
def post(self, request):
try:
data = json.loads(request.body)
if not User.objects.filter(id=request.user.id):
return JsonResponse({'message' : 'INVALID_USER'}, status=404)
BulletinBoard.objects.create(
author_id = request.user.id,
title = data['title'],
description = data['description'],
created_at = data['created_at']
)
time.sleep(1)
return JsonResponse({'message' : 'SUCCESS'}, status=201)
except KeyError:
return JsonResponse({'message': 'KEY_ERROR'}, status=400)
@login_decorator
def patch(self, request, post_id):
try:
data = json.loads(request.body)
post = BulletinBoard.objects.get(id=post_id)
if not post.author_id == request.user.id:
return JsonResponse({'message' : 'INVALID_USER'}, status=404)
post.title = data['title']
post.description = data['description']
post.updated_at = datetime.datetime.now()
post.save()
return JsonResponse({'message' : 'SUCCESS'}, status=200)
except KeyError:
return JsonResponse({'message': 'KEY_ERROR'}, status=400)
except BulletinBoard.DoesNotExist:
return JsonResponse({'message' : 'INVALID_POST'}, status=404)
@login_decorator
def delete(self, request, post_id):
try:
post = BulletinBoard.objects.get(id = post_id)
if not post.author_id == request.user.id:
return JsonResponse({'message' : 'INVALID_USER'}, status=404)
BulletinBoard.objects.get(id=post_id).delete()
return JsonResponse({'message' : 'SUCCESS'}, status=200)
except BulletinBoard.DoesNotExist:
return JsonResponse({'message' : 'INVALID_POST'}, status=404)
class PostDetail(View):
def get(self, request, post_id):
try:
post = BulletinBoard.objects.get(id=post_id)
results = {
'author' : post.author.name if post.author else "사라진 회원입니다.",
'title' : post.title,
'description' : post.description,
'created_at' : post.created_at,
'updated_at' : post.updated_at
}
return JsonResponse({'results' : results}, status=200)
except BulletinBoard.DoesNotExist:
return JsonResponse({'message' : 'INVALID_POST'}, status=404)
- urls.py
from django.urls import path
from .views import Post, PostDetail
urlpatterns = [
path('/post', Post.as_view()),
path('/post/<int:post_id>', Post.as_view()),
path('/post-detail/<int:post_id>', PostDetail.as_view()),
]
- models.py의 BulletinBoar 테이블
author colum의 옵션으로 on_delete=models.SET_NULL
을 설정하여 해당 글을
작성한 회원이 삭제되어도 글이 삭제되지 않도록 설계하였습니다.
- POST API의 get
BD에서 삭제된 회원의 경우 3항연산자를 이용하여 게시물 정보를 return할 때
"사라진 회원입니다."라는 텍스트를 제공하도록 하였습니다.
- POST API의 post
python의 time.sleep()
을 사용하여 게시물 등록 시간을 임의로 지연시켜
DDos공격을 차단하는 개념을 아주 간단하게 표현해 보았습니다.
- POST API의 patch
게시물의 일부 데이터를 수정하기 때문에 PUT이 아닌 PATCH 메소드를 사용했습니다.
오랜만에 써보는 작은 회고인 것 같습니다.
얼마 전부터 글을 반말에서 존대로 바꿨습니다.
글자 수를 줄이는게 효율적이고 보기에 더 좋을 것 같아 반말컨셉을 잡은건데
건방져보이는 감이 있어서 살짝 불편했었기에 바꿔봤는데 마음이 평화로워집니다.
파이썬과 장고를 배우며 처음으로 하나의 프로젝트에 온전한 CRUD를 전부
구현해 본 것 같습니다.
아니 정확히는 코딩을 배우면서 처음인 것 같습니다.
그러면서 RESTful API도 처음으로 제대로 적용해 본 것 같습니다.
URI를 POST/post-write와 같이 각각 설정하고 API를 모두 분리해 놓았었는데
제대로 REST를 파괴한 흑역사임과 동시에 잊혀지지 않을 배움이었습니다.
전체적으로 미숙한데 code convetion도 지키려고 몇번을 다시 검토하느라
시간이 좀 걸린것도 기억에 남는데 깔끔한 코드를 보니 행복해져 옵니다.
작지만 큰 한발짝을 내딛은 것 같은 이 뿌듯함을 또 경험할 수 있기를..! ❤️🔥