서승완
Builds for 2 pipelines canceled in 15 minutes 31 seconds

Merge branch 'feature/upload' into 'master'

Feature/upload



See merge request !8
......@@ -23,7 +23,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SECRET_KEY = 'wfs076redt^-5_*dw6!_dqz*2z!a_#()33y@r_q7&+4r_40h9$'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
DEBUG = False
ALLOWED_HOSTS = ['127.0.0.1', 'khubox-api.khunet.net']
......@@ -49,6 +49,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'khubox.middlewares.auth.AuthMiddleware',
]
ROOT_URLCONF = 'config.urls'
......@@ -128,9 +129,6 @@ STATIC_URL = '/static/'
# Custom Settings
S3_BUCKET = 'khubox-files'
CDN_PATH = 'https://khubox-files.khunet.net'
CLOUDFRONT_KEY_ID = 'APKAJ3FOBWI34OZJTXJQ'
CLOUDFRONT_KEY_PRIVATE = '-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA18VtzURs+fQev5L00LRwRbJaObQI4kfJCIsOE7eWSOqq4Akh\nA7fI6vs3z8orXBvgc+k6GgAHrIdNwckxoQuTsCxrTDm1104qy1T4JkVxkIBYHJgH\nGzKUloK5IqdmcYbOK7IQeHJ2gR9Mv/3oKUytJSsrbM9k4oLrsxGpyEuJeHIg28aP\nwhoVWmBGcPu48l4aYAZEVY7LZRJSOQ9y7Lf8FS1u7Xtw1P91gEaqrqVXqRWY02C8\nsixpJJuiAPnM3rpcpVNlAaPdDkWmaWYJoJDOlce7Dmx1a9Ckr24krM//vpEljurC\nGml0AsHpL8LE9msM5VA+miCxCz/K+wDgm2xvvQIDAQABAoIBAFmP3pLceyuJVBYK\n5smWjB+x91eKTkG2sFB2f8JZau0bUxApWeXULHa1DiaW8UaLX7BdN7vBFW5cvz7X\nx1zklEoFNghuz/btwD+kJlikbI4hZ/F+fTyh0yFiY3xp5dDrtrpWcBW+1UeleVMc\nDnjOFfSepajFsUeANlue0k2MZSRz34s2T2scV5ZkooqdXddYUF/wDhefYm6uvCgI\nPyvY/mbJTyhte/xagY/m6yzk5gxgad0qP2ZZrHhLLMlJ/GEZToWDxD2xUei61NQT\nFFc5ZutkAE6fVb3I4SJUBSX5fl0tTMz4Aak1GP2phMhjZyjYnQMq8kvL4BNFb7gp\nary8W/UCgYEA+eKkfjjlPsEx6yHMhD7pAwy/MpUqJmF5LMxIG+qfd+GZMQ2oucn5\npUAwBHP7BD6E9H7/7jdjnCiO+iPrzM9vNLfqsdCtPWzoFYJp/6Fv002uX8seNYvJ\nQyQqrM85LYIghhnkcmJMA8GR/Iu5ZEeE2BkAl9T2EKclzmB62d/ki6MCgYEA3Q0V\nz08IEwSJW+jEsOM+XGg2YkNqCVKGQD9n4CPx0TFVJxfqFl2nVwlN2hfrlJLUQ9+l\nfXnS5AW3tE88t9we+ea0saJZEqqlm/rGsfTV/twS9cWSgvG5fTzhUbu9/ElMU29L\nmydQfWTvCup7zCuQtgwM5ZRtPwuKsI8urUg6zR8CgYAt0coZvvMCI8i0dbkbkrGF\nNqQkcUeOTBc9CKQ8QjRFdh9x6DBFCOz2ySNE3cNsTs5wSo1BL/Ta4HD/GvEU2ABr\nKUImor3xYnPX5dbr4b0wgLD1rbf3V49q+Um98C1q086E6GCEPNP1aFwNc81lvtt0\nCHmcXZdVDGEZS4WbR7uPgwKBgBO/moY12lPQoPDsH75p3uVkjg9DVJLWo5XT1FTr\nASyeSqw+b7Rl05BsDV+BqZNRdtNFhMRsANJMTHg4aAVJDh9nZBdGmMyZIEiKI/w8\nEm49fRgl+YvnSpoMuViS/EswxTfjBo8q+P7q6IxCHKNF9Ry+gNx14TizsEVL1XC3\ntkEjAoGBAMyp7wdPobJMXcclRVq6rqHs9OMcnZAveVKyxNgDbZu4OB5X4xTxGEYT\nNZQ0MFf/HcwlnH7797gVQeqF9dlqUJYe+Fc8lc/Rcwta/4R5uMgri9t8RKN91YKF\nUUFBsDEkWlkoAmfPkcrrq9cLJlmSNt3ehQj4p5iAJwoVBXXa++PO\n-----END RSA PRIVATE KEY-----'
# Cors
......
import boto3
import datetime
from botocore.signers import CloudFrontSigner
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from django.conf import settings
def rsa_signer(message):
private_key = serialization.load_pem_private_key(
settings.CLOUDFRONT_KEY_PRIVATE.encode('ascii'),
password=None,
backend=default_backend()
def sign_download(file_id):
s3 = boto3.client('s3')
signed_url = s3.generate_presigned_url(
'get_object',
Params={'Bucket': settings.S3_BUCKET, 'Key': file_id},
ExpiresIn=3600,
HttpMethod='GET'
)
return private_key.sign(message, padding.PKCS1v15(), hashes.SHA1())
def sign_download(url):
expire_date = datetime.datetime.utcnow() + datetime.timedelta(hours=1)
cloudfront_signer = CloudFrontSigner(settings.CLOUDFRONT_KEY_ID, rsa_signer)
signed_url = cloudfront_signer.generate_presigned_url(url, date_less_than=expire_date)
return signed_url
......@@ -52,3 +42,13 @@ def s3_copy(file_id, new_file_id):
}
obj = bucket.Object(str(new_file_id))
obj.copy(copy_source)
def s3_update_and_return_size(file_id, name):
s3 = boto3.resource('s3')
s3_object = s3.Object(settings.S3_BUCKET, file_id)
s3_object.copy_from(CopySource={'Bucket': settings.S3_BUCKET, 'Key': file_id},
ContentType='application/octet-stream',
ContentDisposition='attachment; filename=%s' % name,
MetadataDirective='REPLACE')
return s3_object.content_length
......
import os
import django
from .aws import s3_delete, s3_update_and_return_size
from .models import File
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
django.setup()
def process_upload(event, context):
file_id = event['Records'][0]['s3']['object']['key']
file = File.objects.filter(id=file_id, deleted_at__isnull=True)
# File Gone
if len(file) == 0:
s3_delete([file_id])
return
# Update
file[0].size = s3_update_and_return_size(file_id, file[0].name)
file[0].save()
import jwt
from django.conf import settings
from django.http import JsonResponse
class AuthMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if 'HTTP_AUTHORIZATION' in request.META:
token = str(request.META['HTTP_AUTHORIZATION'])[7:]
try:
decoded = jwt.decode(token, settings.SECRET_KEY, algorithms=['HS256'])
request.user_id = decoded['id']
except jwt.exceptions.DecodeError:
return JsonResponse({'result': False, 'error': '토큰이 잘못되었습니다.'})
except jwt.exceptions.ExpiredSignatureError:
return JsonResponse({'result': False, 'error': '토큰이 만료되었습니다.'})
else:
request.user_id = None
response = self.get_response(request)
return response
import json
import uuid
from django.conf import settings
from django.utils import timezone
from ..aws import sign_upload, sign_download, s3_copy, s3_delete
from pathvalidate import sanitize_filename
from ..aws import sign_upload, sign_download, s3_copy, s3_delete, s3_update_and_return_size
from ..models import File, GroupUser
# 폴더/파일 목록
def list_item(request):
# TODO: Auth
request.user_id = 1
# Check Login
if request.user_id is None:
return {'result': False, 'error': '로그인을 해주세요.'}
# Validate
if request.GET.get('is_public') != 'true' \
and request.GET.get('is_starred') != 'true' \
and request.GET.get('is_trashed') != 'true':
return {'result': False, 'error': '입력이 누락되었습니다.'}
return {'result': False, 'error': '잘못된 요청입니다.'}
# Query Files
files = None
......@@ -26,7 +27,7 @@ def list_item(request):
elif request.GET.get('is_trashed') == 'true':
files = File.objects.filter(owner_user_id=request.user_id, is_trashed=1, deleted_at__isnull=True)
# Structure
# Serialize
data = []
for file in files:
data.append({
......@@ -45,23 +46,24 @@ def list_item(request):
# 폴더 생성, 파일 업로드
def create(request):
# TODO: Auth
request.user_id = 1
# Check Login
if request.user_id is None:
return {'result': False, 'error': '로그인을 해주세요.'}
# Load
try:
received = json.loads(request.body.decode('utf-8'))
except json.decoder.JSONDecodeError:
return {'result': False, 'error': '입력이 잘못되었습니다.'}
return {'result': False, 'error': '잘못된 요청입니다.'}
# Validate
if 'parent_id' not in received \
or 'type' not in received \
or 'name' not in received:
return {'result': False, 'error': '입력이 누락되었습니다.'}
return {'result': False, 'error': '잘못된 요청입니다.'}
if (received['type'] != 'folder' and received['type'] != 'file') \
or received['name'] == '':
return {'result': False, 'error': '입력이 잘못되었습니다.'}
return {'result': False, 'error': '잘못된 요청입니다.'}
# Get Parent
parent = File.objects.filter(id=received['parent_id'], is_trashed=0, deleted_at__isnull=True)
......@@ -89,7 +91,7 @@ def create(request):
owner_group_id=parent[0].owner_group_id,
uploader_id=request.user_id,
type=received['type'],
name=received['name'],
name=sanitize_filename(received['name']),
size=0,
created_at=timezone.now()
)
......@@ -105,8 +107,9 @@ def create(request):
# 휴지통 비우기
def empty_trash(request):
# TODO: Auth
request.user_id = 1
# Check Login
if request.user_id is None:
return {'result': False, 'error': '로그인을 해주세요.'}
# Query Files
files = File.objects.filter(owner_user_id=request.user_id, is_trashed=1, deleted_at__isnull=True)
......@@ -138,8 +141,9 @@ def empty_trash(request):
# 폴더/파일 조회
def find_item(request, file_id):
# TODO: Auth
request.user_id = 1
# Check Login
if request.user_id is None:
return {'result': False, 'error': '로그인을 해주세요.'}
# Query
file = File.objects.filter(id=file_id, deleted_at__isnull=True)
......@@ -174,12 +178,10 @@ def find_item(request, file_id):
# Return File
if file[0].type == 'file':
download_url = '%s/%s' % (settings.CDN_PATH, file[0].id)
download_url = sign_download(download_url)
download_url = sign_download(file[0].id)
data = {
'id': file[0].id,
'parent_id': file[0].parent_id,
'uploader_id': file[0].uploader_id,
'name': file[0].name,
'size': file[0].size,
'is_public': file[0].is_public,
......@@ -213,14 +215,15 @@ def find_item(request, file_id):
# 폴더/파일 수정
def update_item(request, file_id):
# TODO: Auth
request.user_id = 1
# Check Login
if request.user_id is None:
return {'result': False, 'error': '로그인을 해주세요.'}
# Load
try:
received = json.loads(request.body.decode('utf-8'))
except json.decoder.JSONDecodeError:
return {'result': False, 'error': '입력이 잘못되었습니다.'}
return {'result': False, 'error': '잘못된 요청입니다.'}
# Validate
if 'name' not in received \
......@@ -228,7 +231,7 @@ def update_item(request, file_id):
and 'is_public' not in received \
and 'is_starred' not in received \
and 'is_trashed' not in received:
return {'result': False, 'error': '입력이 누락되었습니다.'}
return {'result': False, 'error': '잘못된 요청입니다.'}
# Query
file = File.objects.filter(id=file_id, deleted_at__isnull=True)
......@@ -267,7 +270,10 @@ def update_item(request, file_id):
# Update
if 'name' in received:
file[0].name = received['name']
if received['name'] == '':
return {'result': False, 'error': '이름을 제대로 입력해주세요.'}
file[0].name = sanitize_filename(received['name'])
s3_update_and_return_size(file_id, file[0].name)
if 'parent_id' in received:
file[0].parent_id = received['parent_id']
if 'is_public' in received:
......@@ -285,8 +291,9 @@ def update_item(request, file_id):
# 파일 복제
def copy(request, file_id):
# TODO: Auth
request.user_id = 1
# Check Login
if request.user_id is None:
return {'result': False, 'error': '로그인을 해주세요.'}
# Get File
file = File.objects.filter(id=file_id, type='file', is_trashed=0, deleted_at__isnull=True)
......
......@@ -7,18 +7,19 @@ from ..models import File, Group, GroupUser, User
# 그룹 생성
def create(request):
# TODO: Auth
request.user_id = 1
# Check Login
if request.user_id is None:
return {'result': False, 'error': '로그인을 해주세요.'}
# Load
try:
received = json.loads(request.body.decode('utf-8'))
except json.decoder.JSONDecodeError:
return {'result': False, 'error': '입력이 잘못되었습니다.'}
return {'result': False, 'error': '잘못된 요청입니다.'}
# Validate
if 'name' not in received or received['name'] == '':
return {'result': False, 'error': '입력이 누락되었습니다.'}
return {'result': False, 'error': '잘못된 요청입니다.'}
# Create
root_folder = uuid.uuid4()
......@@ -44,24 +45,24 @@ def create(request):
created_at=timezone.now()
)
return {'result': True, 'group_id': group.id}
return {'result': True}
# 그룹 초대장 조회
def find_invite(request, invite_code):
# TODO: Auth
request.user_id = 1
# Check Login
if request.user_id is None:
return {'result': False, 'error': '로그인을 해주세요.'}
# Query
group = Group.objects.filter(invite_code=invite_code)
# Check Exists
if len(group) == 0:
return {'result': False, 'error': '존재하지 않는 초대장입니다.'}
return {'result': False, 'error': '잘못된 초대코드입니다.'}
# Structure
# Serialize
data = {
'id': group[0].id,
'name': group[0].name
}
......@@ -77,15 +78,16 @@ def find_invite(request, invite_code):
# 그룹 초대장 사용
def use_invite(request, invite_code):
# TODO: Auth
request.user_id = 1
# Check Login
if request.user_id is None:
return {'result': False, 'error': '로그인을 해주세요.'}
# Query
group = Group.objects.filter(invite_code=invite_code)
# Check Exists
if len(group) == 0:
return {'result': False, 'error': '존재하지 않는 초대장입니다.'}
return {'result': False, 'error': '잘못된 초대코드입니다.'}
# Check Joined
joined = GroupUser.objects.filter(group_id=group[0].id, user_id=request.user_id)
......@@ -104,18 +106,18 @@ def use_invite(request, invite_code):
# 그룹 목록
def list_me(request):
# TODO: Auth
request.user_id = 1
# Check Login
if request.user_id is None:
return {'result': False, 'error': '로그인을 해주세요.'}
# Query
joined = GroupUser.objects.filter(user_id=request.user_id).values_list('group_id', flat=True)
groups = Group.objects.filter(id__in=joined)
# Structure
# Serialize
data = []
for group in groups:
data.append({
'id': group.id,
'name': group.name,
'root_folder': group.root_folder,
})
......@@ -125,24 +127,24 @@ def list_me(request):
# 그룹 조회
def find_item(request, group_id):
# TODO: Auth
request.user_id = 1
# Check Login
if request.user_id is None:
return {'result': False, 'error': '로그인을 해주세요.'}
# Check Joined
joined = GroupUser.objects.filter(group_id=group_id, user_id=request.user_id)
if len(joined) == 0:
return {'result': False, 'error': '입력이 잘못되었습니다.'}
return {'result': False, 'error': '잘못된 요청입니다.'}
# Query
group = Group.objects.filter(id=group_id)
# Check Exists
if len(group) == 0:
return {'result': False, 'error': '존재하지 않는 그룹입니다.'}
return {'result': False, 'error': '잘못된 요청입니다.'}
# Structure
# Serialize
data = {
'id': group[0].id,
'name': group[0].name,
'root_folder': group[0].root_folder,
}
......@@ -157,9 +159,9 @@ def find_item(request, group_id):
'id': user.id,
'name': user.name,
})
data['user'] = user_data
data['id'] = group[0].id
data['users'] = user_data
data['invite_code'] = group[0].invite_code
data['created_at'] = group[0].created_at
data['is_owner'] = True
return {'result': True, 'data': data}
......@@ -167,29 +169,30 @@ def find_item(request, group_id):
# 그룹 수정
def update_item(request, group_id):
# TODO: Auth
request.user_id = 1
# Check Login
if request.user_id is None:
return {'result': False, 'error': '로그인을 해주세요.'}
# Load
try:
received = json.loads(request.body.decode('utf-8'))
except json.decoder.JSONDecodeError:
return {'result': False, 'error': '입력이 잘못되었습니다.'}
return {'result': False, 'error': '잘못된 요청입니다.'}
# Validate
if 'name' not in received or received['name'] == '':
return {'result': False, 'error': '입력이 누락되었습니다.'}
return {'result': False, 'error': '잘못된 요청입니다.'}
# Query
group = Group.objects.filter(id=group_id)
# Check Exists
if len(group) == 0:
return {'result': False, 'error': '존재하지 않는 그룹입니다.'}
return {'result': False, 'error': '잘못된 요청입니다.'}
# Check Owner
if group[0].owner_id != request.user_id:
return {'result': False, 'error': '권한이 없습니다.'}
return {'result': False, 'error': '잘못된 요청입니다.'}
# Update
group[0].name = received['name']
......@@ -200,19 +203,20 @@ def update_item(request, group_id):
# 그룹 삭제
def delete_item(request, group_id):
# TODO: Auth
request.user_id = 1
# Check Login
if request.user_id is None:
return {'result': False, 'error': '로그인을 해주세요.'}
# Query
group = Group.objects.filter(id=group_id)
# Check Exists
if len(group) == 0:
return {'result': False, 'error': '존재하지 않는 그룹입니다.'}
return {'result': False, 'error': '잘못된 요청입니다.'}
# Check Owner
if group[0].owner_id != request.user_id:
return {'result': False, 'error': '권한이 없습니다.'}
return {'result': False, 'error': '잘못된 요청입니다.'}
# S3 Delete
del_list = File.objects.filter(owner_group_id=group_id).values_list('id', flat=True)
......@@ -228,15 +232,16 @@ def delete_item(request, group_id):
# 그룹 사용자 삭제
def remove_user(request, group_id, user_id):
# TODO: Auth
request.user_id = 1
# Check Login
if request.user_id is None:
return {'result': False, 'error': '로그인을 해주세요.'}
# Query
group = Group.objects.filter(id=group_id)
# Check Owner
if group[0].owner_id != request.user_id:
return {'result': False, 'error': '권한이 없습니다.'}
return {'result': False, 'error': '잘못된 요청입니다.'}
# Check Me
if int(user_id) == request.user_id:
......
......@@ -16,32 +16,34 @@ def create(request):
try:
received = json.loads(request.body.decode('utf-8'))
except json.decoder.JSONDecodeError:
return {'result': False, 'error': '입력이 잘못되었습니다.'}
return {'result': False, 'error': '잘못된 요청입니다.'}
# Validate
if 'email' not in received \
or 'password' not in received \
or 'name' not in received:
return {'result': False, 'error': '입력이 누락되었습니다.'}
return {'result': False, 'error': '잘못된 요청입니다.'}
# Validate Email
try:
validate_email(received['email'])
except ValidationError:
return {'result': False, 'error': '이메일 형식이 잘못되었습니다.'}
return {'result': False, 'error': '이메일을 제대로 입력해주세요.'}
# Validate Password
if len(received['password']) < 8:
return {'result': False, 'error': '비밀번호는 최소 8글자 입니다.'}
return {'result': False, 'error': '비밀번호를 8자리 이상으로 입력해주세요.'}
# Validate Name
if len(received['name']) > 50:
return {'result': False, 'error': '이름은 최대 50글자 입니다.'}
elif len(received['name']) == 0:
return {'result': False, 'error': '이름을 제대로 입력해주세요.'}
# Check Duplicates
is_exists = User.objects.filter(email=received['email'])
if len(is_exists) > 0:
return {'result': False, 'error': '이미 사용중인 이메일 주소 입니다.'}
return {'result': False, 'error': '이미 사용 중인 이메일입니다.'}
# Insert
root_folder = uuid.uuid4()
......@@ -70,21 +72,21 @@ def login(request):
try:
received = json.loads(request.body.decode('utf-8'))
except json.decoder.JSONDecodeError:
return {'result': False, 'error': '입력이 잘못되었습니다.'}
return {'result': False, 'error': '잘못된 요청입니다.'}
# Validate
if 'email' not in received \
or 'password' not in received:
return {'result': False, 'error': '입력이 누락되었습니다.'}
return {'result': False, 'error': '잘못된 요청입니다.'}
# Select
# Query
user = User.objects.filter(email=received['email'])
# Not Exists
# Check Exists
if len(user) != 1:
return {'result': False, 'error': '로그인에 실패하였습니다.'}
# Check
# Check Password
if check_password(received['password'], user[0].password) is False:
return {'result': False, 'error': '로그인에 실패하였습니다.'}
......@@ -97,8 +99,9 @@ def login(request):
# 회원정보 조회
def find_me(request):
# TODO: Auth
request.user_id = 1
# Check Login
if request.user_id is None:
return {'result': False, 'error': '로그인을 해주세요.'}
# Query
user = User.objects.filter(id=request.user_id)
......@@ -121,19 +124,20 @@ def find_me(request):
# 회원정보 수정
def update_me(request):
# TODO: Auth
request.user_id = 1
# Check Login
if request.user_id is None:
return {'result': False, 'error': '로그인을 해주세요.'}
# Load
try:
received = json.loads(request.body.decode('utf-8'))
except json.decoder.JSONDecodeError:
return {'result': False, 'error': '입력이 잘못되었습니다.'}
return {'result': False, 'error': '잘못된 요청입니다.'}
# Validate
if 'name' not in received \
and ('old_password' not in received and 'password' not in received):
return {'result': False, 'error': '입력이 누락되었습니다.'}
return {'result': False, 'error': '잘못된 요청입니다.'}
# Query
user = User.objects.filter(id=request.user_id)
......@@ -144,14 +148,16 @@ def update_me(request):
# Change Name
if 'name' in received:
if len(received['name']) == 0:
return {'result': False, 'error': '이름을 제대로 입력해주세요.'}
user[0].name = received['name']
# Change Password
if 'old_password' in received and 'password' in received:
if check_password(received['old_password'], user[0].password) is False:
return {'result': False, 'error': '이전 비밀번호가 잘못되었습니다.'}
return {'result': False, 'error': '이전 비밀번호를 제대로 입력해주세요.'}
if len(received['password']) < 8:
return {'result': False, 'error': '비밀번호는 최소 8글자 입니다.'}
return {'result': False, 'error': '비밀번호를 8자리 이상으로 입력해주세요.'}
user[0].password = make_password(received['password'])
# Save
......
......@@ -17,6 +17,7 @@ idna==2.9
importlib-metadata==1.6.0
jmespath==0.10.0
-e git+http://khuhub.khu.ac.kr/2016104129/kappa.git@1b0e17bb6da7460d9d494828c682a5ef66736aa3#egg=kappa
pathvalidate==2.3.0
pip-tools==5.1.2
placebo==0.9.0
pycparser==2.20
......
......@@ -10,6 +10,17 @@
"vpc_config": {
"SubnetIds": ["subnet-02d51e69"],
"SecurityGroupIds": ["sg-09c30d37d6a504f5c"]
}
},
"events": [
{
"function": "khubox.lambda.process_upload",
"event_source": {
"arn": "arn:aws:s3:::khubox-files",
"events": [
"s3:ObjectCreated:Put"
]
}
}
]
}
}
......