first commit

This commit is contained in:
2024-06-25 14:25:29 +09:00
parent f50b7c0a0b
commit fa1b55d68b
39 changed files with 3848 additions and 2 deletions

22
MYSQL/docker-compose.yml Executable file
View File

@@ -0,0 +1,22 @@
mysql:
image: mysql:latest
container_name: mysql
restart: always
environment:
TZ: Asia/Seoul
MYSQL_ROOT_PASSWORD: 1234
MYSQL_DATABASE: medical_metaverse
MYSQL_USER: medical_metaverse
MYSQL_PASSWORD: 1234
# healthcheck:
# test: ["CMD", "echo 'SELECT version();'| mysql"]
# timeout: 5s
# retries: 5
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
volumes:
- /MYSQL/data/:/var/lib/mysql
- /MYSQL/backup:/backupfiles
ports:
- 3306:3306

View File

@@ -1,3 +1,3 @@
# A2TEC_METAVERSE_HOSPITAL_REST_SERVER
# A2TEC_METAVERSE_MEDICAL_REST_SERVER
A2TEC_METAVERSE_HOSPITAL_REST_SERVER
A2TEC_METAVERSE_MEDICAL_REST_SERVER

42
docker-compose.yml Normal file
View File

@@ -0,0 +1,42 @@
version: '3.7'
services:
api:
depends_on:
mysql:
condition: service_healthy
container_name: metaverse_medical_rest
image: metaverse/medical_rest:latest
build:
context: ./fast_api/
environment:
- TZ=Asia/Seoul
volumes:
- ./fast_api:/FAST_API
ports:
- 50510-50532:50510-50532
command: ["uvicorn", "app.main:app", "--host", '0.0.0.0', "--port", "50510"] #local
# command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "50532"] #my
mysql:
image: mysql:latest
container_name: mysql
restart: always
environment:
TZ: Asia/Seoul
MYSQL_ROOT_PASSWORD: 1234
MYSQL_DATABASE: medical_metaverse
MYSQL_USER: medical_metaverse
MYSQL_PASSWORD: 1234
healthcheck:
test: "mysql -uroot -p$$MYSQL_ROOT_PASSWORD -e \"SHOW DATABASES;\""
timeout: 5s
retries: 5
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
volumes:
- ./MYSQL/data/:/var/lib/mysql
- ./MYSQL/backup:/backupfiles
ports:
- 3306:3306

9
fast_api/.dockerignore Executable file
View File

@@ -0,0 +1,9 @@
.git/
.gitignore
.idea/
README.md
Dockerfile
__pycache__
docker-compose.yml
venv/
tests/

49
fast_api/.travis.yml Executable file
View File

@@ -0,0 +1,49 @@
os: linux
dist: bionic
language: python
services:
- docker
- mysql
python:
- 3.8
before_install:
- pip install awscli
- export PATH=$PATH:$HOME/.local/bin
script:
- pytest -v
before_deploy:
- if [ $TRAVIS_BRANCH == "master" ]; then export EB_ENV=notification-prd-api; fi
- if [ $TRAVIS_BRANCH == "develop" ]; then export EB_ENV=notification-dev-api; fi
- export REPO_NAME=$(echo $TRAVIS_REPO_SLUG | sed "s_^.*/__")
- export ELASTIC_BEANSTALK_LABEL=${REPO_NAME}-${TRAVIS_COMMIT::7}-$(date +%y%m%d%H%M%S)
deploy:
skip_cleanup: true
provider: elasticbeanstalk
access_key_id: $AWS_ACCESS
secret_access_key: $AWS_SECRET
region: ap-northeast-2
bucket: elasticbeanstalk-ap-northeast-2-884465654078
bucket_path: notification-api
app: notification-api
env: $EB_ENV
on:
all_branches: true
condition: $TRAVIS_BRANCH =~ ^develop|master
#notifications:
# slack:
# - rooms:
# - secure: ***********
# if: branch = master
# template:
# - "Repo `%{repository_slug}` *%{result}* build (<%{build_url}|#%{build_number}>) for commit (<%{compare_url}|%{commit}>) on branch `%{branch}`."
# - rooms:
# - secure: ***********
# if: branch = staging
# template:
# - "Repo `%{repository_slug}` *%{result}* build (<%{build_url}|#%{build_number}>) for commit (<%{compare_url}|%{commit}>) on branch `%{branch}`."
# - rooms:
# - secure: ***********
# if: branch = develop
# template:
# - "Repo `%{repository_slug}` *%{result}* build (<%{build_url}|#%{build_number}>) for commit (<%{compare_url}|%{commit}>) on branch `%{branch}`."

32
fast_api/Dockerfile Executable file
View File

@@ -0,0 +1,32 @@
# ------------------------------------------------------------------------------
# Base image
# ------------------------------------------------------------------------------
FROM python:3.9.7-slim
# ------------------------------------------------------------------------------
# Informations
# ------------------------------------------------------------------------------
LABEL maintainer="hsj100 <hsj100@a2tec.co.kr>"
LABEL title="A2TEC_METAVERSER_MEDICAL_REST"
LABEL description="Rest API Server with Fast API"
# ------------------------------------------------------------------------------
# Source
# ------------------------------------------------------------------------------
COPY ./app /FAST_API/app
# ------------------------------------------------------------------------------
# Install dependencies
# ------------------------------------------------------------------------------
COPY ./requirements.txt /FAST_API/requirements.txt
WORKDIR /FAST_API
RUN apt update > /dev/null && \
apt install -y build-essential && \
pip install --no-cache-dir --upgrade -r /FAST_API/requirements.txt
# ------------------------------------------------------------------------------
# Binary
# ------------------------------------------------------------------------------
# CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "50210"]

43
fast_api/README.md Normal file
View File

@@ -0,0 +1,43 @@
# METAVERSE_MEDICAL_REST_SERVER
RESTful API Server
### System Requirements
- Ubuntu 20.04
- docker 20.10.9
- docker-compose 1.25.0
- python 3.9
- python packages: requirements.txt
### Docker
- Base Image
* python:3.9.7-slim
- Target Image
* medical_metaverse_restapi:latest
- Environments variable
- Build
```
docker-build.sh
```
- Run
* docker-compose.yml 파일의 환경변수(environment) 수정 후 아래 명령으로 도커 실행
```
docker-compose up -d
```
### LOG
- 서버 로그 파일
```
TBD
```

View File

@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
"""
@File: api_request_sample.py
@Date: 2020-09-14
@author: A2TEC
@section MODIFYINFO 수정정보
- 수정자/수정일 : 수정내역
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
@brief: test api key
"""
import base64
import hmac
from datetime import datetime, timedelta
import requests
def parse_params_to_str(params):
url = "?"
for key, value in params.items():
url = url + str(key) + '=' + str(value) + '&'
return url[1:-1]
def hash_string(qs, secret_key):
mac = hmac.new(bytes(secret_key, encoding='utf8'), bytes(qs, encoding='utf-8'), digestmod='sha256')
d = mac.digest()
validating_secret = str(base64.b64encode(d).decode('utf-8'))
return validating_secret
def sample_request():
access_key = 'c0883231-4aa9-4a1f-a77b-3ef250af-e449-42e9-856a-b3ada17c426b'
secret_key = 'QhOaeXTAAkW6yWt31jWDeERkBsZ3X4UmPds656YD'
cur_time = datetime.utcnow()+timedelta(hours=9)
cur_timestamp = int(cur_time.timestamp())
qs = dict(key=access_key, timestamp=cur_timestamp)
header_secret = hash_string(parse_params_to_str(qs), secret_key)
url = f'http://127.0.0.1:8080/api/services?{parse_params_to_str(qs)}'
res = requests.get(url, headers=dict(secret=header_secret))
return res
print(sample_request().json())

110
fast_api/app/common/config.py Executable file
View File

@@ -0,0 +1,110 @@
# -*- coding: utf-8 -*-
"""
@File: config.py
@Date: 2020-09-14
@author: A2TEC
@section MODIFYINFO 수정정보
- 수정자/수정일 : 수정내역
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
@brief: Configurations
"""
from dataclasses import dataclass
from os import path, environ
from app.common import consts
from app.models import UserInfo
base_dir = path.dirname(path.dirname(path.dirname(path.abspath(__file__))))
@dataclass
class Config:
"""
기본 Configuration
"""
BASE_DIR: str = base_dir
DB_POOL_RECYCLE: int = 900
DB_ECHO: bool = True
DEBUG: bool = False
TEST_MODE: bool = False
DEV_TEST_CONNECT_ACCOUNT: str = None
# NOTE(hsj100): SERVICE_AUTH_API_KEY (token 방식으로 진행)
SERVICE_AUTH_API_KEY: bool = False
DB_URL: str = environ.get('DB_URL', f'mysql+pymysql://{consts.DB_USER_ID}:{consts.DB_USER_PW}@{consts.DB_ADDRESS}/{consts.DB_NAME}?charset={consts.DB_CHARSET}')
REST_SERVER_PORT = consts.REST_SERVER_PORT
SW_TITLE = consts.SW_TITLE
SW_VERSION = consts.SW_VERSION
SW_DESCRIPTION = consts.SW_DESCRIPTION
TERMS_OF_SERVICE = consts.TERMS_OF_SERVICE
CONTEACT = consts.CONTEACT
LICENSE_INFO = consts.LICENSE_INFO
# GLOBAL_TOKEN = consts.ADMIN_ACCOUNT_TOKEN
GLOBAL_TOKEN = None
@dataclass
class LocalConfig(Config):
TRUSTED_HOSTS = ['*']
ALLOW_SITE = ['*']
DEBUG: bool = True
@dataclass
class ProdConfig(Config):
TRUSTED_HOSTS = ['*']
ALLOW_SITE = ['*']
@dataclass
class TestConfig(Config):
DB_URL: str = environ.get('DB_URL', f'mysql+pymysql://{consts.DB_USER_ID}:{consts.DB_USER_PW}@{consts.DB_ADDRESS}/{consts.DB_NAME}_test?charset={consts.DB_CHARSET}')
TRUSTED_HOSTS = ['*']
ALLOW_SITE = ['*']
TEST_MODE: bool = True
@dataclass
class DevConfig(Config):
TRUSTED_HOSTS = ['*']
ALLOW_SITE = ['*']
DEBUG: bool = True
DB_URL: str = environ.get('DB_URL', f'mysql+pymysql://{consts.DB_USER_ID}:{consts.DB_USER_PW}@{consts.DB_ADDRESS}/{consts.DB_NAME}_dev?charset={consts.DB_CHARSET}')
REST_SERVER_PORT = consts.REST_SERVER_PORT + 1
SW_TITLE = '[Dev] ' + consts.SW_TITLE
@dataclass
class MyConfig(Config):
TRUSTED_HOSTS = ['*']
ALLOW_SITE = ['*']
DEBUG: bool = True
DB_URL: str = environ.get('DB_URL', f'mysql+pymysql://{consts.DB_USER_ID}:{consts.DB_USER_PW}@{consts.DB_ADDRESS}/{consts.DB_NAME}_my?charset={consts.DB_CHARSET}')
REST_SERVER_PORT = consts.REST_SERVER_PORT + 2
# NOTE(hsj100): DEV_TEST_CONNECT_ACCOUNT
DEV_TEST_CONNECT_ACCOUNT: UserInfo = UserInfo(**consts.ADMIN_INIT_ACCOUNT_INFO)
# DEV_TEST_CONNECT_ACCOUNT: str = None
# NOTE(hsj100): SERVICE_AUTH_API_KEY (token 방식으로 진행)
SERVICE_AUTH_API_KEY: bool = False
SW_TITLE = '[My] ' + consts.SW_TITLE
def conf():
"""
환경 불러오기
:return:
"""
config = dict(prod=ProdConfig, local=LocalConfig, test=TestConfig, dev=DevConfig, my=MyConfig)
return config[environ.get('API_ENV', 'local')]()
return config[environ.get('API_ENV', 'dev')]()
return config[environ.get('API_ENV', 'my')]()
return config[environ.get('API_ENV', 'test')]()

94
fast_api/app/common/consts.py Executable file
View File

@@ -0,0 +1,94 @@
# -*- coding: utf-8 -*-
"""
@File: consts.py
@Date: 2020-09-14
@author: A2TEC
@section MODIFYINFO 수정정보
- 수정자/수정일 : 수정내역
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
@brief: consts
"""
# SUPPORT PROJECT
SUPPORT_PROJECT_BASIC = 'PROJECT_BASIC'
SUPPORT_PROJECT_MEDICAL_METAVERSE = 'METAVERSE - MEDICAL'
# SUPPORT_PROJECT_SW_LICENSE_MANAGER = 'SW LICENSE MANAGER'
PROJECT_NAME = 'METAVERSE - MEDICAL'
SW_TITLE= f'{PROJECT_NAME} - REST API'
SW_VERSION = '0.9'
SW_DESCRIPTION = f'''
### METAVERSE MEDICAL REST API
## API 이용법
- **사용자 접속**시 토큰정보(***Authorization*** 항목) 획득 (이후 API 이용가능)
- 수신받은 토큰정보(***Authorization*** 항목)를 ***Request Header*** 에 포함해서 기타 API 사용
- 개별 API 설명과 Request/Response schema 참조
**시스템에서 사용하는 날짜형식**\n
YYYY-mm-ddTHH:MM:SS
YYYY-mm-dd HH:MM:SS
'''
TERMS_OF_SERVICE = 'http://www.a2tec.com'
CONTEACT={
'name': 'A2TEC (주)에이투텍',
'url': 'http://www.a2tec.co.kr',
'email': 'marketing@a2tec.co.kr'
}
LICENSE_INFO = {
'name': 'Copyright by A2TEC', 'url': 'http://www.a2tec.co.kr'
}
REST_SERVER_PORT = 50510
# ADMINISTRATOR ACCOUNT INFORMATION
ADMIN_INIT_ACCOUNT_INFO = dict(
id=1,
account='a2tecrest01@gmail.com',
pw='!dpdlxnxpr1',
name='administrator',
email='a2tecrest01@gmail.com',
email_pw='!dpdlxnxpr1',
)
ADMIN_ACCOUNT_TOKEN = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiYWNjb3VudCI6ImEydGVjcmVzdDAxQGdtYWlsLmNvbSIsIm5hbWUiOiJhZG1pbmlzdHJhdG9yIiwicGhvbmVfbnVtYmVyIjpudWxsLCJwaWN0dXJlIjpudWxsLCJhY2NvdW50X3R5cGUiOiJlbWFpbCJ9.QNiF4sc7BZYwzoir0oPqLCUIyglUgMjZuBc1mI2GRCE'
COOKIES_AUTH = 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MTQsImVtYWlsIjoia29hbGFAZGluZ3JyLmNvbSIsIm5hbWUiOm51bGwsInBob25lX251bWJlciI6bnVsbCwicHJvZmlsZV9pbWciOm51bGwsInNuc190eXBlIjpudWxsfQ.4vgrFvxgH8odoXMvV70BBqyqXOFa2NDQtzYkGywhV48'
JWT_SECRET = 'ABCD1234!'
JWT_ALGORITHM = 'HS256'
EXCEPT_PATH_LIST = ['/', '/openapi.json']
EXCEPT_PATH_REGEX = '^(/docs|/redoc|/api/auth' +\
'|/api/user/check_account_exist' +\
')'
MAX_API_KEY = 3
MAX_API_WHITELIST = 10
NUM_RETRY_UUID_GEN = 3
# DATABASE
# DB_ADDRESS = '52.78.56.148'
DB_ADDRESS = 'localhost'
DB_USER_ID = 'medical_metaverse'
DB_USER_PW = '1234'
DB_NAME = 'medical_metaverse'
DB_CHARSET = 'utf8mb4'
# Mail
MAIL_REG_TITLE = f'{PROJECT_NAME} - Registration'
MAIL_REG_CONTENTS = '''
안녕하세요.
[A2TEC] Metaverse medical Manager 입니다.
요청하신 라이선스가 정상 등록되었습니다.
라이선스 정보는 다음과 같습니다.
Account: {}
MAC: {}
감사합니다.
'''
# MEETING_ROOM
MEETING_ROOM_MAX_PERSON = 10

144
fast_api/app/database/conn.py Executable file
View File

@@ -0,0 +1,144 @@
# -*- coding: utf-8 -*-
"""
@File: conn.py
@Date: 2020-09-14
@author: A2TEC
@section MODIFYINFO 수정정보
- 수정자/수정일 : 수정내역
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
@brief: DB Connections
"""
from fastapi import FastAPI
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import logging
def _database_exist(engine, schema_name):
query = f'SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = "{schema_name}"'
with engine.connect() as conn:
result_proxy = conn.execute(query)
result = result_proxy.scalar()
return bool(result)
def _drop_database(engine, schema_name):
with engine.connect() as conn:
conn.execute(f'DROP DATABASE {schema_name};')
def _create_database(engine, schema_name):
with engine.connect() as conn:
conn.execute(f'CREATE DATABASE {schema_name} CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;')
class SQLAlchemy:
def __init__(self, app: FastAPI = None, **kwargs):
self._engine = None
self._session = None
if app is not None:
self.init_app(app=app, **kwargs)
def init_app(self, app: FastAPI, **kwargs):
"""
DB 초기화 함수
:param app: FastAPI 인스턴스
:param kwargs:
:return:
"""
database_url = kwargs.get('DB_URL')
pool_recycle = kwargs.setdefault('DB_POOL_RECYCLE', 900)
is_testing = kwargs.setdefault('TEST_MODE', False)
echo = kwargs.setdefault('DB_ECHO', True)
self._engine = create_engine(
database_url,
echo=echo,
pool_recycle=pool_recycle,
pool_pre_ping=True,
)
if is_testing: # create schema
db_url = self._engine.url
if db_url.host != 'localhost':
raise Exception('db host must be \'localhost\' in test environment')
except_schema_db_url = f'{db_url.drivername}://{db_url.username}:{db_url.password}@{db_url.host}'
schema_name = db_url.database
temp_engine = create_engine(except_schema_db_url, echo=echo, pool_recycle=pool_recycle, pool_pre_ping=True)
if _database_exist(temp_engine, schema_name):
_drop_database(temp_engine, schema_name)
_create_database(temp_engine, schema_name)
temp_engine.dispose()
else:
db_url = self._engine.url
except_schema_db_url = f'{db_url.drivername}://{db_url.username}:{db_url.password}@{db_url.host}'
schema_name = db_url.database
temp_engine = create_engine(except_schema_db_url, echo=echo, pool_recycle=pool_recycle, pool_pre_ping=True)
if not _database_exist(temp_engine, schema_name):
_create_database(temp_engine, schema_name)
Base.metadata.create_all(db.engine)
temp_engine.dispose()
self._session = sessionmaker(autocommit=False, autoflush=False, bind=self._engine)
# NOTE(hsj100): ADMINISTRATOR
create_admin(self._session)
@app.on_event('startup')
def startup():
self._engine.connect()
logging.info('DB connected.')
@app.on_event('shutdown')
def shutdown():
self._session.close_all()
self._engine.dispose()
logging.info('DB disconnected')
def get_db(self):
"""
요청마다 DB 세션 유지 함수
:return:
"""
if self._session is None:
raise Exception('must be called \'init_app\'')
db_session = None
try:
db_session = self._session()
yield db_session
finally:
db_session.close()
@property
def session(self):
return self.get_db
@property
def engine(self):
return self._engine
db = SQLAlchemy()
Base = declarative_base()
# NOTE(hsj100): ADMINISTRATOR
def create_admin(db_session):
import bcrypt
from app.database.schema import Users
from app.common.consts import ADMIN_INIT_ACCOUNT_INFO
session = db_session()
if not session:
raise Exception('cat`t create account of admin')
if Users.get(account=ADMIN_INIT_ACCOUNT_INFO['account']):
return
admin = {**ADMIN_INIT_ACCOUNT_INFO}
hash_pw = bcrypt.hashpw(str(admin['pw']).encode('utf-8'), bcrypt.gensalt())
admin['pw'] = hash_pw
Users.create(session=session, auto_commit=True, **admin)

149
fast_api/app/database/crud.py Executable file
View File

@@ -0,0 +1,149 @@
# -*- coding: utf-8 -*-
"""
@File: crud.py
@Date: 2020-09-14
@author: A2TEC
@section MODIFYINFO 수정정보
- 수정자/수정일 : 수정내역
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
@brief: CRUD
"""
async def table_read(accessor_info, target_table, request_body_info, response_model, response_model_data):
"""
table_read
"""
try:
# parameter
if not accessor_info:
raise Exception('invalid accessor')
if not target_table:
raise Exception(f'invalid table_name:{target_table}')
# if not request_body_info:
# raise Exception('invalid request_body_info')
if not response_model:
raise Exception('invalid response_model')
if not response_model_data:
raise Exception('invalid response_model_data')
# request
request_info = dict()
for key, val in request_body_info.dict().items():
if val:
request_info[key] = val
# search
search_info = target_table.filter(**request_info).all()
if not search_info:
raise Exception('not found data')
# result
result_info = list()
for value in search_info:
result_info.append(response_model_data.from_orm(value))
return response_model(data=result_info)
except Exception as e:
return response_model.set_error(str(e))
async def table_update(accessor_info, target_table, request_body_info, response_model, response_model_data=None):
search_info = None
try:
# parameter
if not accessor_info:
raise Exception('invalid accessor')
if not target_table:
raise Exception(f'invalid table_name:{target_table}')
if not request_body_info:
raise Exception('invalid request_body_info')
if not response_model:
raise Exception('invalid response_model')
# if not response_model_data:
# raise Exception('invalid response_model_data')
# request
if not request_body_info.search_info:
raise Exception('invalid request_body: search_info')
request_search_info = dict()
if request_body_info.search_info:
for key, val in request_body_info.search_info.dict().items():
if val:
request_search_info[key] = val
if not request_search_info:
raise Exception('invalid request_body: search_info')
if not request_body_info.update_info:
raise Exception('invalid request_body: update_info')
request_update_info = dict()
for key, val in request_body_info.update_info.dict().items():
if val:
request_update_info[key] = val
if not request_update_info:
raise Exception('invalid request_body: update_info')
# search
search_info = target_table.filter(**request_search_info)
# process
search_info.update(auto_commit=True, synchronize_session=False, **request_update_info)
# result
return response_model()
except Exception as e:
if search_info:
search_info.close()
return response_model.set_error(str(e))
async def table_delete(accessor_info, target_table, request_body_info, response_model, response_model_data=None):
search_info = None
try:
# request
if not accessor_info:
raise Exception('invalid accessor')
if not target_table:
raise Exception(f'invalid table_name:{target_table}')
if not request_body_info:
raise Exception('invalid request_body_info')
if not response_model:
raise Exception('invalid response_model')
# if not response_model_data:
# raise Exception('invalid response_model_data')
# request
request_search_info = dict()
if request_body_info:
for key, val in request_body_info.dict().items():
if val:
request_search_info[key] = val
if not request_search_info:
raise Exception('invalid request_body')
# search
search_info = target_table.filter(**request_search_info)
# process
search_info.delete(auto_commit=True, synchronize_session=False)
# result
return response_model()
except Exception as e:
if search_info:
search_info.close()
return response_model.set_error(str(e))

272
fast_api/app/database/schema.py Executable file
View File

@@ -0,0 +1,272 @@
# -*- coding: utf-8 -*-
"""
@File: schema.py
@Date: 2020-09-14
@author: A2TEC
@section MODIFYINFO 수정정보
- 수정자/수정일 : 수정내역
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
@brief: database schema
"""
from sqlalchemy import (
Column,
Integer,
String,
DateTime,
func,
Enum,
Boolean,
ForeignKey,
)
from sqlalchemy.orm import Session, relationship
from app.database.conn import Base, db
from app.utils.date_utils import D
from app.models import (
SexType,
UserType,
AccountType,
UserStatusType,
UserLoginType,
RoomType,
AppointmentStatusType,
RoomStatusType
)
from app.common.consts import MEETING_ROOM_MAX_PERSON
class BaseMixin:
id = Column(Integer, primary_key=True, index=True)
created_at = Column(DateTime, nullable=False, default=D.datetime())
updated_at = Column(DateTime, nullable=False, default=D.datetime(), onupdate=D.datetime())
def __init__(self):
self._q = None
self._session = None
self.served = None
def all_columns(self):
return [c for c in self.__table__.columns if c.primary_key is False and c.name != 'created_at']
def __hash__(self):
return hash(self.id)
@classmethod
def create(cls, session: Session, auto_commit=False, **kwargs):
"""
테이블 데이터 적재 전용 함수
:param session:
:param auto_commit: 자동 커밋 여부
:param kwargs: 적재 할 데이터
:return:
"""
obj = cls()
# NOTE(hsj100) : FIX_DATETIME
if 'created_at' not in kwargs:
obj.created_at = D.datetime()
if 'updated_at' not in kwargs:
obj.updated_at = D.datetime()
for col in obj.all_columns():
col_name = col.name
if col_name in kwargs:
setattr(obj, col_name, kwargs.get(col_name))
session.add(obj)
session.flush()
if auto_commit:
session.commit()
return obj
@classmethod
def get(cls, session: Session = None, **kwargs):
"""
Simply get a Row
:param session:
:param kwargs:
:return:
"""
sess = next(db.session()) if not session else session
query = sess.query(cls)
for key, val in kwargs.items():
col = getattr(cls, key)
query = query.filter(col == val)
if query.count() > 1:
raise Exception('Only one row is supposed to be returned, but got more than one.')
result = query.first()
if not session:
sess.close()
return result
@classmethod
def filter(cls, session: Session = None, **kwargs):
"""
Simply get a Row
:param session:
:param kwargs:
:return:
"""
cond = []
for key, val in kwargs.items():
key = key.split('__')
if len(key) > 2:
raise Exception('No 2 more dunders')
col = getattr(cls, key[0])
if len(key) == 1: cond.append((col == val))
elif len(key) == 2 and key[1] == 'gt': cond.append((col > val))
elif len(key) == 2 and key[1] == 'gte': cond.append((col >= val))
elif len(key) == 2 and key[1] == 'lt': cond.append((col < val))
elif len(key) == 2 and key[1] == 'lte': cond.append((col <= val))
elif len(key) == 2 and key[1] == 'in': cond.append((col.in_(val)))
elif len(key) == 2 and key[1] == 'like': cond.append((col.like(val)))
obj = cls()
if session:
obj._session = session
obj.served = True
else:
obj._session = next(db.session())
obj.served = False
query = obj._session.query(cls)
query = query.filter(*cond)
obj._q = query
return obj
@classmethod
def cls_attr(cls, col_name=None):
if col_name:
col = getattr(cls, col_name)
return col
else:
return cls
def order_by(self, *args: str):
for a in args:
if a.startswith('-'):
col_name = a[1:]
is_asc = False
else:
col_name = a
is_asc = True
col = self.cls_attr(col_name)
self._q = self._q.order_by(col.asc()) if is_asc else self._q.order_by(col.desc())
return self
def update(self, auto_commit: bool = False, synchronize_session='evaluate', **kwargs):
# NOTE(hsj100) : FIX_DATETIME
if 'updated_at' not in kwargs:
kwargs['updated_at'] = D.datetime()
qs = self._q.update(kwargs, synchronize_session=synchronize_session)
get_id = self.id
ret = None
self._session.flush()
if qs > 0 :
ret = self._q.first()
if auto_commit:
self._session.commit()
return ret
def first(self):
result = self._q.first()
self.close()
return result
def delete(self, auto_commit: bool = False, synchronize_session='evaluate'):
self._q.delete(synchronize_session=synchronize_session)
if auto_commit:
self._session.commit()
def all(self):
print(self.served)
result = self._q.all()
self.close()
return result
def count(self):
result = self._q.count()
self.close()
return result
def close(self):
if not self.served:
self._session.close()
else:
self._session.flush()
class ApiKeys(Base, BaseMixin):
__tablename__ = 'api_keys'
access_key = Column(String(length=64), nullable=False, index=True)
secret_key = Column(String(length=64), nullable=False)
user_memo = Column(String(length=40), nullable=True)
status = Column(Enum('active', 'stopped', 'deleted'), default='active')
is_whitelisted = Column(Boolean, default=False)
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
whitelist = relationship('ApiWhiteLists', backref='api_keys')
users = relationship('Users', back_populates='keys')
class ApiWhiteLists(Base, BaseMixin):
__tablename__ = 'api_whitelists'
ip_addr = Column(String(length=64), nullable=False)
api_key_id = Column(Integer, ForeignKey('api_keys.id'), nullable=False)
class Users(Base, BaseMixin):
__tablename__ = 'users'
status = Column(Enum(UserStatusType), nullable=False, default=UserStatusType.active)
# TODO(hsj100): LOGIN_STATUS
login = Column(Enum(UserLoginType), nullable=False, default=UserLoginType.logout)
account_type = Column(Enum(AccountType), nullable=False, default=AccountType.email)
account = Column(String(length=255), nullable=False, unique=True)
pw = Column(String(length=2000), nullable=True)
email = Column(String(length=255), nullable=True)
name = Column(String(length=255), nullable=False)
sex = Column(Enum(SexType), nullable=False, default=SexType.male)
rrn = Column(String(length=255), nullable=True)
address = Column(String(length=2000), nullable=True)
phone_number = Column(String(length=20), nullable=True)
picture = Column(String(length=1000), nullable=True)
marketing_agree = Column(Boolean, nullable=True, default=False)
keys = relationship('ApiKeys', back_populates='users')
user_type = Column(Enum(UserType), nullable=False, default=UserType.patient)
major = Column(String(length=255), nullable=True, default=None)
character = Column(String(length=255), nullable=True)
room_id = Column(Integer, ForeignKey('room.id'), nullable=True)
class Appointment(Base, BaseMixin):
__tablename__ = 'appointment'
create_account = Column(String(length=255), nullable=False)
doctor_account = Column(String(length=255), nullable=False)
patient_account = Column(String(length=255), nullable=False)
treatment_subject = Column(String(length=64), nullable=False)
date = Column(DateTime, nullable=False, default=func.timestamp())
status = Column(Enum(AppointmentStatusType), default=AppointmentStatusType.wait)
class TreatmentDivision(Base, BaseMixin):
__tablename__ = 'treat'
short_name = Column(String(length=64), nullable=False)
full_name = Column(String(length=255), nullable=False)
class Room(Base, BaseMixin):
__tablename__ = 'room'
type = Column(Enum(RoomType), nullable=False, default=RoomType.meeting)
auto_enter = Column(Boolean, nullable=False, default=True)
auto_delete = Column(Boolean, nullable=False, default=True)
name = Column(String(length=255), nullable=False)
pw = Column(String(length=2000), nullable=False)
create_account = Column(String(length=255), nullable=False)
status = Column(Enum(RoomStatusType), nullable=False, default=RoomStatusType.open)
cur_person = Column(Integer, nullable=False, default=0)
max_person = Column(Integer, nullable=False, default=MEETING_ROOM_MAX_PERSON)
desc = Column(String(length=2000), nullable=True)

View File

188
fast_api/app/errors/exceptions.py Executable file
View File

@@ -0,0 +1,188 @@
from app.common.consts import MAX_API_KEY, MAX_API_WHITELIST
class StatusCode:
HTTP_500 = 500
HTTP_400 = 400
HTTP_401 = 401
HTTP_403 = 403
HTTP_404 = 404
HTTP_405 = 405
class APIException(Exception):
status_code: int
code: str
msg: str
detail: str
ex: Exception
def __init__(
self,
*,
status_code: int = StatusCode.HTTP_500,
code: str = '000000',
msg: str = None,
detail: str = None,
ex: Exception = None,
):
self.status_code = status_code
self.code = code
self.msg = msg
self.detail = detail
self.ex = ex
super().__init__(ex)
class NotFoundUserEx(APIException):
def __init__(self, user_id: int = None, ex: Exception = None):
super().__init__(
status_code=StatusCode.HTTP_404,
msg=f'해당 유저를 찾을 수 없습니다.',
detail=f'Not Found User ID : {user_id}',
code=f'{StatusCode.HTTP_400}{"1".zfill(4)}',
ex=ex,
)
class NotAuthorized(APIException):
def __init__(self, ex: Exception = None):
super().__init__(
status_code=StatusCode.HTTP_401,
msg=f'로그인이 필요한 서비스 입니다.',
detail='Authorization Required',
code=f'{StatusCode.HTTP_401}{"1".zfill(4)}',
ex=ex,
)
class TokenExpiredEx(APIException):
def __init__(self, ex: Exception = None):
super().__init__(
status_code=StatusCode.HTTP_400,
msg=f'세션이 만료되어 로그아웃 되었습니다.',
detail='Token Expired',
code=f'{StatusCode.HTTP_400}{"1".zfill(4)}',
ex=ex,
)
class TokenDecodeEx(APIException):
def __init__(self, ex: Exception = None):
super().__init__(
status_code=StatusCode.HTTP_400,
msg=f'비정상적인 접근입니다.',
detail='Token has been compromised.',
code=f'{StatusCode.HTTP_400}{"2".zfill(4)}',
ex=ex,
)
class NoKeyMatchEx(APIException):
def __init__(self, ex: Exception = None):
super().__init__(
status_code=StatusCode.HTTP_404,
msg=f'해당 키에 대한 권한이 없거나 해당 키가 없습니다.',
detail='No Keys Matched',
code=f'{StatusCode.HTTP_404}{"3".zfill(4)}',
ex=ex,
)
class MaxKeyCountEx(APIException):
def __init__(self, ex: Exception = None):
super().__init__(
status_code=StatusCode.HTTP_400,
msg=f'API 키 생성은 {MAX_API_KEY}개 까지 가능합니다.',
detail='Max Key Count Reached',
code=f'{StatusCode.HTTP_400}{"4".zfill(4)}',
ex=ex,
)
class MaxWLCountEx(APIException):
def __init__(self, ex: Exception = None):
super().__init__(
status_code=StatusCode.HTTP_400,
msg=f'화이트리스트 생성은 {MAX_API_WHITELIST}개 까지 가능합니다.',
detail='Max Whitelist Count Reached',
code=f'{StatusCode.HTTP_400}{"5".zfill(4)}',
ex=ex,
)
class InvalidIpEx(APIException):
def __init__(self, ip: str, ex: Exception = None):
super().__init__(
status_code=StatusCode.HTTP_400,
msg=f'{ip}는 올바른 IP 가 아닙니다.',
detail=f'invalid IP : {ip}',
code=f'{StatusCode.HTTP_400}{"6".zfill(4)}',
ex=ex,
)
class SqlFailureEx(APIException):
def __init__(self, ex: Exception = None):
super().__init__(
status_code=StatusCode.HTTP_500,
msg=f'이 에러는 서버측 에러 입니다. 자동으로 리포팅 되며, 빠르게 수정하겠습니다.',
detail='Internal Server Error',
code=f'{StatusCode.HTTP_500}{"2".zfill(4)}',
ex=ex,
)
class APIQueryStringEx(APIException):
def __init__(self, ex: Exception = None):
super().__init__(
status_code=StatusCode.HTTP_400,
msg=f'쿼리스트링은 key, timestamp 2개만 허용되며, 2개 모두 요청시 제출되어야 합니다.',
detail='Query String Only Accept key and timestamp.',
code=f'{StatusCode.HTTP_400}{"7".zfill(4)}',
ex=ex,
)
class APIHeaderInvalidEx(APIException):
def __init__(self, ex: Exception = None):
super().__init__(
status_code=StatusCode.HTTP_400,
msg=f'헤더에 키 해싱된 Secret 이 없거나, 유효하지 않습니다.',
detail='Invalid HMAC secret in Header',
code=f'{StatusCode.HTTP_400}{"8".zfill(4)}',
ex=ex,
)
class APITimestampEx(APIException):
def __init__(self, ex: Exception = None):
super().__init__(
status_code=StatusCode.HTTP_400,
msg=f'쿼리스트링에 포함된 타임스탬프는 KST 이며, 현재 시간보다 작아야 하고, 현재시간 - 10초 보다는 커야 합니다.',
detail='timestamp in Query String must be KST, Timestamp must be less than now, and greater than now - 10.',
code=f'{StatusCode.HTTP_400}{"9".zfill(4)}',
ex=ex,
)
class NotFoundAccessKeyEx(APIException):
def __init__(self, api_key: str, ex: Exception = None):
super().__init__(
status_code=StatusCode.HTTP_404,
msg=f'API 키를 찾을 수 없습니다.',
detail=f'Not found such API Access Key : {api_key}',
code=f'{StatusCode.HTTP_404}{"10".zfill(4)}',
ex=ex,
)
class KakaoSendFailureEx(APIException):
def __init__(self, ex: Exception = None):
super().__init__(
status_code=StatusCode.HTTP_400,
msg=f'카카오톡 전송에 실패했습니다.',
detail=f'Failed to send KAKAO MSG.',
code=f'{StatusCode.HTTP_400}{"11".zfill(4)}',
ex=ex,
)

84
fast_api/app/main.py Executable file
View File

@@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
"""
@File: main.py
@Date: 2020-09-14
@author: A2TEC
@section MODIFYINFO 수정정보
- 수정자/수정일 : 수정내역
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
@brief: Main
"""
from dataclasses import asdict
import uvicorn
from fastapi import FastAPI, Depends
from fastapi.security import APIKeyHeader
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.middleware.cors import CORSMiddleware
from app.common import consts
from app.database.conn import db
from app.common.config import conf
from app.middlewares.token_validator import access_control
from app.middlewares.trusted_hosts import TrustedHostMiddleware
from app.routes import dev, index, auth, users, services
API_KEY_HEADER = APIKeyHeader(name='Authorization', auto_error=False)
def create_app():
"""
fast_api_app 생성
:return: fast_api_app
"""
configurations = conf()
fast_api_app = FastAPI(
title=configurations.SW_TITLE,
version=configurations.SW_VERSION,
description=configurations.SW_DESCRIPTION,
terms_of_service=configurations.TERMS_OF_SERVICE,
contact=configurations.CONTEACT,
license_info={'name': 'Copyright by A2TEC', 'url': 'http://www.a2tec.co.kr'}
)
# 데이터 베이스 이니셜라이즈
conf_dict = asdict(configurations)
db.init_app(fast_api_app, **conf_dict)
# 레디스 이니셜라이즈
# 미들웨어 정의
fast_api_app.add_middleware(middleware_class=BaseHTTPMiddleware, dispatch=access_control)
fast_api_app.add_middleware(
CORSMiddleware,
allow_origins=conf().ALLOW_SITE,
allow_credentials=True,
allow_methods=['*'],
allow_headers=['*'],
)
fast_api_app.add_middleware(TrustedHostMiddleware, allowed_hosts=conf().TRUSTED_HOSTS, except_path=['/health'])
# 라우터 정의
fast_api_app.include_router(index.router, tags=['Defaults'])
fast_api_app.include_router(auth.router, tags=['Authentication'], prefix='/api')
if conf().DEBUG:
fast_api_app.include_router(services.router, tags=['Services'], prefix='/api', dependencies=[Depends(API_KEY_HEADER)])
else:
fast_api_app.include_router(services.router, tags=['Services'], prefix='/api')
fast_api_app.include_router(users.router, tags=['Users'], prefix='/api', dependencies=[Depends(API_KEY_HEADER)])
if conf().DEBUG:
fast_api_app.include_router(dev.router, tags=['Developments'], prefix='/api', dependencies=[Depends(API_KEY_HEADER)])
return fast_api_app
app = create_app()
if __name__ == '__main__':
uvicorn.run('main:app', host='0.0.0.0', port=conf().REST_SERVER_PORT, reload=True)

View File

@@ -0,0 +1,166 @@
import base64
import hmac
import json
import time
import typing
import re
import jwt
import sqlalchemy.exc
from jwt.exceptions import ExpiredSignatureError, DecodeError
from starlette.requests import Request
from starlette.responses import JSONResponse
from app.common.consts import EXCEPT_PATH_LIST, EXCEPT_PATH_REGEX
from app.database.conn import db
from app.database.schema import Users, ApiKeys
from app.errors import exceptions as ex
from app.common import consts
from app.common.config import conf
from app.errors.exceptions import APIException, SqlFailureEx, APIQueryStringEx
from app.models import UserToken
from app.utils.date_utils import D
from app.utils.logger import api_logger
from app.utils.query_utils import to_dict
from dataclasses import asdict
async def access_control(request: Request, call_next):
request.state.req_time = D.datetime()
request.state.start = time.time()
request.state.inspect = None
request.state.user = None
request.state.service = None
ip = request.headers['x-forwarded-for'] if 'x-forwarded-for' in request.headers.keys() else request.client.host
request.state.ip = ip.split(',')[0] if ',' in ip else ip
headers = request.headers
cookies = request.cookies
url = request.url.path
if await url_pattern_check(url, EXCEPT_PATH_REGEX) or url in EXCEPT_PATH_LIST:
response = await call_next(request)
if url != '/':
await api_logger(request=request, response=response)
return response
try:
if url.startswith('/api'):
# api 인경우 헤더로 토큰 검사
# NOTE(hsj100): SERVICE_AUTH_API_KEY (token 방식으로 진행)
if url.startswith('/api/services') and conf().SERVICE_AUTH_API_KEY:
qs = str(request.query_params)
qs_list = qs.split('&')
session = next(db.session())
if not conf().DEBUG:
try:
qs_dict = {qs_split.split('=')[0]: qs_split.split('=')[1] for qs_split in qs_list}
except Exception:
raise ex.APIQueryStringEx()
qs_keys = qs_dict.keys()
if 'key' not in qs_keys or 'timestamp' not in qs_keys:
raise ex.APIQueryStringEx()
if 'secret' not in headers.keys():
raise ex.APIHeaderInvalidEx()
api_key = ApiKeys.get(session=session, access_key=qs_dict['key'])
if not api_key:
raise ex.NotFoundAccessKeyEx(api_key=qs_dict['key'])
mac = hmac.new(bytes(api_key.secret_key, encoding='utf8'), bytes(qs, encoding='utf-8'), digestmod='sha256')
d = mac.digest()
validating_secret = str(base64.b64encode(d).decode('utf-8'))
if headers['secret'] != validating_secret:
raise ex.APIHeaderInvalidEx()
now_timestamp = int(D.datetime(diff=9).timestamp())
if now_timestamp - 10 > int(qs_dict['timestamp']) or now_timestamp < int(qs_dict['timestamp']):
raise ex.APITimestampEx()
user_info = to_dict(api_key.users)
request.state.user = UserToken(**user_info)
else:
# Request User 가 필요함
if 'authorization' in headers.keys():
key = headers.get('Authorization')
api_key_obj = ApiKeys.get(session=session, access_key=key)
user_info = to_dict(Users.get(session=session, id=api_key_obj.user_id))
request.state.user = UserToken(**user_info)
# 토큰 없음
else:
if 'Authorization' not in headers.keys():
raise ex.NotAuthorized()
session.close()
response = await call_next(request)
return response
else:
if 'authorization' in headers.keys():
# 토큰 존재
token_info = await token_decode(access_token=headers.get('Authorization'))
request.state.user = UserToken(**token_info)
elif conf().DEV_TEST_CONNECT_ACCOUNT:
# NOTE(hsj100): DEV_TEST_CONNECT_ACCOUNT
request.state.user = UserToken.from_orm(conf().DEV_TEST_CONNECT_ACCOUNT)
else:
# 토큰 없음
if 'Authorization' not in headers.keys():
raise ex.NotAuthorized()
else:
# 템플릿 렌더링인 경우 쿠키에서 토큰 검사
cookies['Authorization'] = conf().COOKIES_AUTH
if 'Authorization' not in cookies.keys():
raise ex.NotAuthorized()
token_info = await token_decode(access_token=cookies.get('Authorization'))
request.state.user = UserToken(**token_info)
response = await call_next(request)
await api_logger(request=request, response=response)
except Exception as e:
error = await exception_handler(e)
error_dict = dict(status=error.status_code, msg=error.msg, detail=error.detail, code=error.code)
response = JSONResponse(status_code=error.status_code, content=error_dict)
await api_logger(request=request, error=error)
return response
async def url_pattern_check(path, pattern):
result = re.match(pattern, path)
if result:
return True
return False
async def token_decode(access_token):
"""
:param access_token:
:return:
"""
try:
access_token = access_token.replace('Bearer ', "")
payload = jwt.decode(access_token, key=consts.JWT_SECRET, algorithms=[consts.JWT_ALGORITHM])
except ExpiredSignatureError:
raise ex.TokenExpiredEx()
except DecodeError:
raise ex.TokenDecodeEx()
return payload
async def exception_handler(error: Exception):
print(error)
if isinstance(error, sqlalchemy.exc.OperationalError):
error = SqlFailureEx(ex=error)
if not isinstance(error, APIException):
error = APIException(ex=error, detail=str(error))
return error

View File

@@ -0,0 +1,63 @@
import typing
from starlette.datastructures import URL, Headers
from starlette.responses import PlainTextResponse, RedirectResponse, Response
from starlette.types import ASGIApp, Receive, Scope, Send
ENFORCE_DOMAIN_WILDCARD = 'Domain wildcard patterns must be lNo module named \'app\'ike \'*.example.com\'.'
class TrustedHostMiddleware:
def __init__(
self,
app: ASGIApp,
allowed_hosts: typing.Sequence[str] = None,
except_path: typing.Sequence[str] = None,
www_redirect: bool = True,
) -> None:
if allowed_hosts is None:
allowed_hosts = ['*']
if except_path is None:
except_path = []
for pattern in allowed_hosts:
assert '*' not in pattern[1:], ENFORCE_DOMAIN_WILDCARD
if pattern.startswith('*') and pattern != '*':
assert pattern.startswith('*.'), ENFORCE_DOMAIN_WILDCARD
self.app = app
self.allowed_hosts = list(allowed_hosts)
self.allow_any = '*' in allowed_hosts
self.www_redirect = www_redirect
self.except_path = list(except_path)
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if self.allow_any or scope['type'] not in ('http', 'websocket',): # pragma: no cover
await self.app(scope, receive, send)
return
headers = Headers(scope=scope)
host = headers.get('host', "").split(':')[0]
is_valid_host = False
found_www_redirect = False
for pattern in self.allowed_hosts:
if (
host == pattern
or (pattern.startswith('*') and host.endswith(pattern[1:]))
or URL(scope=scope).path in self.except_path
):
is_valid_host = True
break
elif 'www.' + host == pattern:
found_www_redirect = True
if is_valid_host:
await self.app(scope, receive, send)
else:
if found_www_redirect and self.www_redirect:
url = URL(scope=scope)
redirect_url = url.replace(netloc='www.' + url.netloc)
response = RedirectResponse(url=str(redirect_url)) # type: Response
else:
response = PlainTextResponse('Invalid host header', status_code=400)
await response(scope, receive, send)

590
fast_api/app/models.py Executable file
View File

@@ -0,0 +1,590 @@
# -*- coding: utf-8 -*-
"""
@File: models.py
@Date: 2020-09-14
@author: A2TEC
@section MODIFYINFO 수정정보
- 수정자/수정일 : 수정내역
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
@brief: data models
"""
from datetime import datetime
from enum import Enum
from typing import List
from pydantic import Field
from pydantic.main import BaseModel
from pydantic.networks import EmailStr, IPvAnyAddress
from typing import Optional
from app.common.consts import SW_TITLE, SW_VERSION, MEETING_ROOM_MAX_PERSON
from app.utils.date_utils import D
class SWInfo(BaseModel):
"""
### 서비스 정보
"""
name: str = Field(SW_TITLE, description='SW 이름', example=SW_VERSION)
version: str = Field(SW_VERSION, description='SW 버전', example=SW_VERSION)
date: str = Field(D.date_str(), description='현재날짜', example='%Y.%m.%dT%H:%M:%S')
class CustomEnum(Enum):
@classmethod
def get_elements_str(cls, is_key=True):
if is_key:
result_str = cls.__members__.keys()
else:
result_str = cls.__members__.values()
return '[' + ', '.join(result_str) + ']'
class SexType(str, CustomEnum):
male: str = 'male'
female: str = 'female'
class AccountType(str, CustomEnum):
email: str = 'email'
# facebook: str = 'facebook'
# google: str = 'google'
# kakao: str = 'kakao'
class UserStatusType(str, CustomEnum):
active: str = 'active'
deleted: str = 'deleted'
blocked: str = 'blocked'
class UserLoginType(str, CustomEnum):
login: str = 'login'
logout: str = 'logout'
class UserType(str, CustomEnum):
doctor: str = 'doctor'
patient: str = 'patient'
class RoomType(str, CustomEnum):
meeting: str = 'meeting'
lecture: str = 'lecture'
class AppointmentStatusType(str, CustomEnum):
wait: str = 'wait'
reserve: str = 'reserve'
cancel: str = 'cancel'
treatment: str = 'treatment'
class RoomStatusType(str, CustomEnum):
open: str = 'open'
close: str = 'close'
tclose: str = 'tclose'
full: str = 'full'
class Token(BaseModel):
Authorization: str = Field(None, description='인증키', example='Bearer [token]')
class EmailRecipients(BaseModel):
name: str
email: str
class SendEmail(BaseModel):
email_to: List[EmailRecipients] = None
class KakaoMsgBody(BaseModel):
msg: str = None
class MessageOk(BaseModel):
message: str = Field(default='OK')
class UserToken(BaseModel):
id: int
account: str = None
name: str = None
phone_number: str = None
picture: str = None
account_type: str = None
class Config:
orm_mode = True
class AddApiKey(BaseModel):
user_memo: str = None
class Config:
orm_mode = True
class GetApiKeyList(AddApiKey):
id: int = None
access_key: str = None
created_at: datetime = None
class GetApiKeys(GetApiKeyList):
secret_key: str = None
class CreateAPIWhiteLists(BaseModel):
ip_addr: str = None
class GetAPIWhiteLists(CreateAPIWhiteLists):
id: int
class Config:
orm_mode = True
class ResponseBase(BaseModel):
"""
### [Response] API End-Point
**정상처리**\n
- result: true\n
- error: null\n
**오류발생**\n
- result: false\n
- error: 오류내용\n
"""
result: bool = Field(True, description='처리상태(성공: true, 실패: false)', example=True)
error: str = Field(None, description='오류내용(성공: null, 실패: 오류내용)', example='invalid data')
@staticmethod
def set_error(error):
ResponseBase.result = False
ResponseBase.error = str(error)
return ResponseBase
class Config:
orm_mode = True
class TokenRes(ResponseBase):
"""
### [Response] 토큰 정보
"""
Authorization: str = Field(None, description='인증키', example='Bearer [token]')
class Config:
orm_mode = True
class UserInfo(BaseModel):
"""
### 유저 정보
"""
id: int = Field(None, description='Table Index', example='1')
created_at: datetime = Field(None, description='생성날짜', example='2022-01-01T12:34:56')
updated_at: datetime = Field(None, description='수정날짜', example='2022-01-01T12:34:56')
status: UserStatusType = Field(None, description='계정상태' + UserStatusType.get_elements_str(), example=UserStatusType.active)
login: UserLoginType = Field(None, description='로그인상태' + UserLoginType.get_elements_str(), example=UserLoginType.logout)
account_type: AccountType = Field(None, description='계정종류' + AccountType.get_elements_str(), example=AccountType.email)
account: str = Field(None, description='계정', example='user1@test.com')
pw: str = Field(None, description='비밀번호', example='1234')
email: str = Field(None, description='전자메일', example='user1@test.com')
name: str = Field(None, description='이름', example='user1')
sex: str = Field(None, description='성별' + SexType.get_elements_str(), example=SexType.male)
rrn: str = Field(None, description='주민등록번호', example='123456-1234567')
address: str = Field(None, description='주소', example='대구1')
phone_number: str = Field(None, description='연락처', example='010-1234-1234')
picture: str = Field(None, description='프로필사진', example='profile1.png')
marketing_agree: bool = Field(False, description='마케팅동의 여부', example=False)
# extra
user_type: str = Field(None, description='유저종류', example=UserType.patient)
major: str = Field(None, description='진료과목', example='정형외과')
character: str = Field(None, description='아바타 정보', example='Male_CH01')
room_id: int = Field(None, description='입장한 방번호', example='1')
class Config:
orm_mode = True
class UserInfoRes(ResponseBase):
"""
### [Response] 유저 정보
"""
data: UserInfo = None
class Config:
orm_mode = True
class UserLoginReq(BaseModel):
"""
### 유저 로그인 정보
"""
account: str = Field(None, description='계정', example='user1@test.com')
pw: str = Field(None, description='비밀번호', example='1234')
class UserRegisterReq(BaseModel):
"""
### [Request] 유저 등록
"""
# pip install 'pydantic'
# users
status: Optional[str] = Field(UserStatusType.blocked, description='계정상태' + UserStatusType.get_elements_str(), example=UserStatusType.active)
login: Optional[str] = Field(None, description='로그인상태' + UserLoginType.get_elements_str(), example=UserLoginType.logout)
account: str = Field(description='계정', example='test@test.com')
pw: str = Field(None, description='비밀번호', example='1234')
email: Optional[str] = Field(None, description='전자메일', example='test@test.com')
name: str = Field(description='이름', example='test')
sex: str = Field(SexType.male, description='성별' + SexType.get_elements_str(), example=SexType.male)
rrn: Optional[str] = Field(None, description='주민등록번호', example='19910101-1234567')
address: Optional[str] = Field(None, description='주소', example='대구광역시 동구 동촌로 351, 4층 (용계동, 에이스빌딩)')
phone_number: Optional[str] = Field(None, description='휴대전화', example='010-1234-1234')
picture: Optional[str] = Field(None, description='사진', example='profile1.png')
marketing_agree: Optional[bool] = Field(False, description='마케팅동의 여부', example=False)
# extra
user_type: str = Field(UserType.patient, description='유저종류', example=UserType.patient)
major: Optional[str] = Field(None, description='진료과목', example='정형외과')
character: Optional[str] = Field(default='Male_CH01', description='아바타 정보', example='Male_CH01')
# room_id: Optional[int] = Field(None, description='입장한 방번호', example='1')
class UserSearchReq(BaseModel):
"""
### [Request] 유저 검색
"""
id: Optional[int] = Field(None, description='등록번호', example='1')
created_at: Optional[datetime] = Field(None, description='생성날짜', example='2022-01-01T12:34:56')
created_at__gt: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 이후)', example='2022-02-10T15:00:00')
created_at__gte: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 포함 이후)', example='2022-02-10T15:00:00')
created_at__lt: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 이전)', example='2022-02-10T15:00:00')
created_at__lte: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 포함 이전)', example='2022-02-10T15:00:00')
updated_at: Optional[datetime] = Field(None, description='수정날짜', example='2022-01-01T12:34:56')
updated_at__gt: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 이후)', example='2022-02-10T15:00:00')
updated_at__gte: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 포함 이후)', example='2022-02-10T15:00:00')
updated_at__lt: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 이전)', example='2022-02-10T15:00:00')
updated_at__lte: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 포함 이전)', example='2022-02-10T15:00:00')
status: Optional[str] = Field(None, description='계정상태' + UserStatusType.get_elements_str(), example=UserStatusType.active)
login: Optional[str] = Field(None, description='로그인상태' + UserLoginType.get_elements_str(), example=UserLoginType.logout)
account_type: Optional[str] = Field(None, description='계정종류' + AccountType.get_elements_str(), example=AccountType.email)
account: Optional[str] = Field(None, description='계정', example='user1@test.com')
account__like: Optional[str] = Field(None, description='계정 부분검색', example='%user%')
email: Optional[str] = Field(None, description='전자메일', example='user1@test.com')
email__like: Optional[str] = Field(None, description='전자메일 부분검색', example='%user%')
name: Optional[str] = Field(None, description='이름', example='test')
name__like: Optional[str] = Field(None, description='이름 부분검색', example='%user%')
sex: Optional[str] = Field(None, description='성별' + SexType.get_elements_str(), example=SexType.male)
rrn: Optional[str] = Field(None, description='주민등록번호', example='123456-1234567')
address: Optional[str] = Field(None, description='주소', example='대구1')
address__like: Optional[str] = Field(None, description='주소 부분검색', example='%대구%')
phone_number: Optional[str] = Field(None, description='연락처', example='010-1234-1234')
picture: Optional[str] = Field(None, description='프로필사진', example='profile1.png')
marketing_agree: Optional[bool] = Field(False, description='마케팅동의 여부', example=False)
# extra
user_type: Optional[str] = Field(None, description='유저종류' + UserType.get_elements_str(), example=UserType.patient)
major: Optional[str] = Field(None, description='전공과목[patient 제외]', example='정형외과')
character: Optional[str] = Field(None, description='아바타 정보', example='Male_CH01')
room_id: Optional[int] = Field(None, description='입장한 방번호', example='1')
class Config:
orm_mode = True
class UserSearchRes(ResponseBase):
"""
### [Response] 유저 검색
"""
data: List[UserInfo] = []
class Config:
orm_mode = True
class UserUpdateReq(BaseModel):
"""
### 유저정보 변경
"""
status: Optional[str] = Field(None, description='계정상태' + UserStatusType.get_elements_str(), example=UserStatusType.active)
login: Optional[str] = Field(None, description='로그인상태' + UserLoginType.get_elements_str(), example=UserLoginType.logout)
account_type: Optional[str] = Field(None, description='계정종류' + AccountType.get_elements_str(), example=AccountType.email)
account: Optional[str] = Field(None, description='계정', example='user1@test.com')
# pw: Optional[str] = Field(None, description='비밀번호', example='1234')
name: Optional[str] = Field(None, description='이름', example='test')
sex: Optional[str] = Field(None, description='성별' + SexType.get_elements_str(), example=SexType.male)
rrn: Optional[str] = Field(None, description='주민등록번호', example='123456-1234567')
address: Optional[str] = Field(None, description='주소', example='대구1')
phone_number: Optional[str] = Field(None, description='연락처', example='010-1234-1234')
picture: Optional[str] = Field(None, description='프로필사진', example='profile1.png')
marketing_agree: Optional[bool] = Field(False, description='마케팅동의 여부', example=False)
# extra
user_type: Optional[str] = Field(None, description='유저종류', example='patient')
major: Optional[str] = Field(None, description='전공과목[patient 제외]', example='정형외과')
character: Optional[str] = Field(None, description='전공과목[patient 제외]', example='정형외과')
room_id: Optional[int] = Field(None, description='입장한 방번호', example='1')
class Config:
orm_mode = True
class UserUpdateMultiReq(BaseModel):
"""
### [Request] 유저 변경 (multi)
"""
search_info: UserSearchReq = None
update_info: UserUpdateReq = None
class Config:
orm_mode = True
class UserUpdatePWReq(BaseModel):
"""
### [Request] 유저 비밀번호 변경
"""
account: str = Field(None, description='계정', example='user1@test.com')
current_pw: str = Field(None, description='현재 비밀번호', example='1234')
new_pw: str = Field(None, description='신규 비밀번호', example='5678')
class AppointmentInfo(BaseModel):
"""
### 진료예약 정보
"""
id: int = Field(None, description='Table Index', example='1')
created_at: datetime = Field(None, description='생성날짜', example='2022-01-01T12:34:56')
updated_at: datetime = Field(None, description='수정날짜', example='2022-01-01T12:34:56')
create_account: str = Field(None, description='등록자 계정', example='doctor1@doctor.com')
doctor_account: str = Field(None, description='의사 계정', example='doctor1@doctor.com')
patient_account: str = Field(None, description='환자 계정', example='test@test.com')
treatment_subject: str = Field(None, description='진료과목', example='정형외과')
date: datetime = Field(None, description='예약날짜', example='2022-01-01 10:00:00')
status: str = Field(None, description='예약상태' + AppointmentStatusType.get_elements_str(), example=AppointmentStatusType.wait)
class Config:
orm_mode = True
class AppointmentInfoRes(ResponseBase):
"""
### [Response] 진료예약 정보
"""
data: AppointmentInfo = None
class Config:
orm_mode = True
class AppointmentRegisterReq(BaseModel):
"""
### [Request] 진료예약 등록
"""
create_account: str = Field(None, description='등록자 계정', example='doctor1@doctor.com')
doctor_account: str = Field(None, description='의사 계정', example='doctor1@doctor.com')
patient_account: str = Field(None, description='환자 계정', example='test@test.com')
treatment_subject: str = Field(None, description='진료과목', example='정형외과')
date: datetime = Field(None, description='예약날짜', example='2022-01-01 10:00:00')
status: Optional[str] = Field(AppointmentStatusType.wait, description='예약상태' + AppointmentStatusType.get_elements_str(), example=AppointmentStatusType.wait)
class Config:
orm_mode = True
class AppointmentSearchReq(BaseModel):
"""
### [Request] 진료예약 검색
"""
id: Optional[int] = Field(None, description='등록번호', example='1')
created_at: Optional[datetime] = Field(None, description='생성날짜', example='2022-01-01T12:34:56')
created_at__gt: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 이후)', example='2022-02-10T15:00:00')
created_at__gte: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 포함 이후)', example='2022-02-10T15:00:00')
created_at__lt: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 이전)', example='2022-02-10T15:00:00')
created_at__lte: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 포함 이전)', example='2022-02-10T15:00:00')
updated_at: Optional[datetime] = Field(None, description='수정날짜', example='2022-01-01T12:34:56')
updated_at__gt: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 이후)', example='2022-02-10T15:00:00')
updated_at__gte: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 포함 이후)', example='2022-02-10T15:00:00')
updated_at__lt: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 이전)', example='2022-02-10T15:00:00')
updated_at__lte: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 포함 이전)', example='2022-02-10T15:00:00')
create_account: Optional[str] = Field(None, description='등록자 계정', example='doctor1@doctor.com')
doctor_account: Optional[str] = Field(None, description='의사 계정', example='doctor1@doctor.com')
patient_account: Optional[str] = Field(None, description='환자 계정', example='test@test.com')
treatment_subject: Optional[str] = Field(None, description='진료과목', example='정형외과')
status: Optional[str] = Field(None, description='진료예약상태' + AppointmentStatusType.get_elements_str(), example=AppointmentStatusType.wait)
date: Optional[datetime] = Field(None, description='예약날짜', example='2022-01-01 10:00:00')
date__gt: Optional[datetime] = Field(None, description='예약날짜(주어진 날짜 이후)', example='2022-01-01T10:00:00')
date__gte: Optional[datetime] = Field(None, description='예약날짜(주어진 날짜 포함 이후)', example='2022-01-01T10:00:00')
date__lt: Optional[datetime] = Field(None, description='예약날짜(주어진 날짜 이전)', example='2022-01-01T10:00:00')
date__lte: Optional[datetime] = Field(None, description='예약날짜(주어진 날짜 포함 이전)', example='2022-01-01T10:00:00')
class Config:
orm_mode = True
class AppointmentSearchRes(ResponseBase):
"""
### [Response] 진료예약 검색
"""
data: List[AppointmentInfo] = []
class Config:
orm_mode = True
class AppointmentUpdateReq(BaseModel):
"""
### [Request] 진료예약 변경
"""
create_account: Optional[str] = Field(None, description='등록자 계정', example='doctor1@doctor.com')
doctor_account: Optional[str] = Field(None, description='의사 계정', example='doctor1@doctor.com')
patient_account: Optional[str] = Field(None, description='환자 계정', example='test@test.com')
treatment_subject: Optional[str] = Field(None, description='진료과목', example='정형외과')
date: Optional[datetime] = Field(None, description='예약날짜', example='2022-01-01 10:00:00')
status: Optional[str] = Field(None, description='진료예약상태' + AppointmentStatusType.get_elements_str(), example=AppointmentStatusType.wait)
class Config:
orm_mode = True
class AppointmentUpdateMultiReq(BaseModel):
"""
### [Request] 라이선스 변경 (multi)
"""
search_info: AppointmentSearchReq = None
update_info: AppointmentUpdateReq = None
class Config:
orm_mode = True
class RoomInfo(BaseModel):
"""
### 방 정보
"""
id: int = Field(None, description='Table Index', example='1')
created_at: datetime = Field(None, description='생성날짜', example='2022-01-01T12:34:56')
updated_at: datetime = Field(None, description='수정날짜', example='2022-01-01T12:34:56')
type: str = Field(None, description='방 종류' + RoomType.get_elements_str(), example=RoomType.meeting)
auto_enter: bool = Field(None, description='방 생성후, 자동 입장', example='true')
auto_delete: bool = Field(None, description='방 퇴장후, 방인원이 없을 경우 방 삭제', example='true')
name: str = Field(None, description='방제목', example='홍삼원외과')
pw: str = Field(None, description='비밀번호', example='1234')
create_account: str = Field(None, description='등록자 계정', example='doctor1@doctor.com')
status: str = Field(None, description='상태' + RoomStatusType.get_elements_str(), example=RoomStatusType.open)
cur_person: int = Field(None, description='현재 인원수', example='1')
max_person: int = Field(None, description=f'입장 최대 인원수(기본값: {MEETING_ROOM_MAX_PERSON})', example=MEETING_ROOM_MAX_PERSON)
desc: str = Field(None, description='부가정보', example='영업시간: 09:00 ~ 16:00')
class Config:
orm_mode = True
class RoomInfoRes(ResponseBase):
"""
### [Response] 방 정보
"""
data: RoomInfo = None
class Config:
orm_mode = True
class RoomRegisterReq(BaseModel):
"""
### [Request] 방 등록
"""
type: str = Field(RoomType.meeting, description='방 종류' + RoomType.get_elements_str(), example=RoomType.meeting)
auto_enter: Optional[bool] = Field(True, description='방 생성후, 자동 입장', example='true')
auto_delete: Optional[bool] = Field(True, description='방 퇴장후, 방인원이 없을 경우 방 삭제', example='true')
name: str = Field(None, description='방제목', example='홍삼원외과')
pw: str = Field(description='비밀번호', example='1234')
create_account: str = Field(None, description='등록자 계정', example='doctor1@doctor.com')
status: Optional[str] = Field(RoomStatusType.open, description='상태' + RoomStatusType.get_elements_str(), example=RoomStatusType.open)
max_person: Optional[int] = Field(MEETING_ROOM_MAX_PERSON, description=f'입장 최대 인원수(기본값: {MEETING_ROOM_MAX_PERSON})', example=MEETING_ROOM_MAX_PERSON)
desc: Optional[str] = Field(None, description='부가정보', example='영업시간: 09:00 ~ 16:00')
class Config:
orm_mode = True
class RoomSearchReq(BaseModel):
"""
### [Request] 방 검색
"""
id: Optional[int] = Field(None, description='등록번호', example='1')
created_at: Optional[datetime] = Field(None, description='생성날짜', example='2022-01-01T12:34:56')
created_at__gt: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 이후)', example='2022-02-10T15:00:00')
created_at__gte: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 포함 이후)', example='2022-02-10T15:00:00')
created_at__lt: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 이전)', example='2022-02-10T15:00:00')
created_at__lte: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 포함 이전)', example='2022-02-10T15:00:00')
updated_at: Optional[datetime] = Field(None, description='수정날짜', example='2022-01-01T12:34:56')
updated_at__gt: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 이후)', example='2022-02-10T15:00:00')
updated_at__gte: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 포함 이후)', example='2022-02-10T15:00:00')
updated_at__lt: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 이전)', example='2022-02-10T15:00:00')
updated_at__lte: Optional[datetime] = Field(None, description='생성날짜(주어진 날짜 포함 이전)', example='2022-02-10T15:00:00')
type: Optional[str] = Field(None, description='방 종류' + RoomType.get_elements_str(), example=RoomType.meeting)
auto_enter: Optional[bool] = Field(None, description='방 생성후, 자동 입장', example='true')
auto_delete: Optional[bool] = Field(None, description='방 퇴장후, 방인원이 없을 경우 방 삭제', example='true')
name: Optional[str] = Field(None, description='방제목', example='홍삼원외과')
name__like: Optional[str] = Field(None, description='방제목 부분 검색(sql문법)', example='%외과%')
create_account: Optional[str] = Field(None, description='등록자 계정', example='doctor1@doctor.com')
status: Optional[str] = Field(None, description='상태' + RoomStatusType.get_elements_str(), example=RoomStatusType.open)
cur_person: Optional[int] = Field(None, description='현재 인원수', example='1')
max_person: Optional[int] = Field(None, description=f'입장 최대 인원수(기본값: {MEETING_ROOM_MAX_PERSON})', example=MEETING_ROOM_MAX_PERSON)
desc: Optional[str] = Field(None, description='부가정보', example='영업시간: 09:00 ~ 16:00')
desc__like: Optional[str] = Field(None, description='부가정보 부분 검색(sql문법)', example='%영업%')
class Config:
orm_mode = True
class RoomSearchRes(ResponseBase):
"""
### [Response] 방 검색
"""
data: List[RoomInfo] = []
class Config:
orm_mode = True
class RoomUpdateReq(BaseModel):
"""
### [Request] 방 수정
"""
type: Optional[str] = Field(None, description='방 종류' + RoomType.get_elements_str(), example=RoomType.meeting)
auto_enter: Optional[bool] = Field(None, description='방 생성후, 자동 입장', example='true')
auto_delete: Optional[bool] = Field(None, description='방 퇴장후, 방인원이 없을 경우 방 삭제', example='true')
name: Optional[str] = Field(None, description='방제목', example='홍삼원외과')
pw: Optional[str] = Field(description='비밀번호', example='1234')
create_account: Optional[str] = Field(None, description='등록자 계정', example='doctor1@doctor.com')
status: Optional[str] = Field(None, description='상태' + RoomStatusType.get_elements_str(), example=RoomStatusType.open)
cur_person: Optional[int] = Field(None, description='현재 인원수', example='1')
max_person: Optional[int] = Field(None, description=f'입장 최대 인원수(기본값: {MEETING_ROOM_MAX_PERSON})', example=MEETING_ROOM_MAX_PERSON)
desc: Optional[str] = Field(None, description='부가정보', example='영업시간: 09:00 ~ 16:00')
class Config:
orm_mode = True
class RoomUpdateMultiReq(BaseModel):
"""
### [Request] 방 변경 (multi)
"""
search_info: RoomSearchReq = None
update_info: RoomUpdateReq = None
class Config:
orm_mode = True

195
fast_api/app/routes/auth.py Executable file
View File

@@ -0,0 +1,195 @@
# -*- coding: utf-8 -*-
"""
@File: auth.py
@Date: 2020-09-14
@author: A2TEC
@section MODIFYINFO 수정정보
- 수정자/수정일 : 수정내역
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
@brief: authentication api
"""
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
import bcrypt
import jwt
from datetime import datetime, timedelta
from app.common import consts
from app import models as M
from app.database.conn import db
from app.database.schema import Users, Room
router = APIRouter(prefix='/auth')
@router.get('/find-account/{account}', response_model=M.ResponseBase, summary='계정유무 검사')
async def find_account(account: str):
"""
## 계정유무 검사
주어진 계정이 존재하면 true, 없으면 false 처리
**결과**
- ResponseBase
"""
try:
search_info = Users.get(account=account)
if not search_info:
raise Exception(f'not found data: {account}')
return M.ResponseBase()
except Exception as e:
return M.ResponseBase.set_error(str(e))
@router.post('/register/{account_type}', status_code=201, response_model=M.TokenRes, summary='회원가입')
async def register(account_type: M.AccountType, request_body_info: M.UserRegisterReq, session: Session = Depends(db.session)):
"""
## 회원가입 (신규등록)
**결과**
- TokenRes
"""
new_user = None
try:
# request
if not request_body_info.account or not request_body_info.pw:
raise Exception('Account and PW must be provided')
register_user = Users.get(account=request_body_info.account)
if register_user:
raise Exception(f'exist email: {request_body_info.account}')
if account_type == M.AccountType.email:
hash_pw = None
if request_body_info.pw:
hash_pw = bcrypt.hashpw(request_body_info.pw.encode('utf-8'), bcrypt.gensalt())
# create users
new_user = Users.create(session, auto_commit=True,
status=request_body_info.status,
account_type=account_type,
account=request_body_info.account,
pw=hash_pw,
email=request_body_info.email,
name=request_body_info.name,
sex=request_body_info.sex,
rrn=request_body_info.rrn,
address=request_body_info.address,
phone_number=request_body_info.phone_number,
picture=request_body_info.picture,
# extra
user_type=request_body_info.user_type,
major=request_body_info.major,
character=request_body_info.character
)
token = dict(Authorization=f'Bearer {create_access_token(data=M.UserToken.from_orm(new_user).dict(exclude={"pw", "marketing_agree"}),)}')
return token
raise Exception('not supported')
except Exception as e:
if new_user:
new_user.close()
return M.ResponseBase.set_error(str(e))
@router.post('/login/{account_type}', status_code=200, response_model=M.TokenRes, summary='사용자 접속')
async def login(account_type: M.AccountType, request_body_info: M.UserLoginReq):
"""
## 사용자 접속
**결과**
- TokenRes
"""
login_user = None
update_user = None
try:
# request
if not request_body_info.account:
raise Exception('invalid request: account')
if account_type == M.AccountType.email:
# basic auth
login_user = Users.get(account=request_body_info.account)
if not login_user:
raise Exception('not found user')
# check pw
if not request_body_info.pw:
raise Exception('invalid request: pw')
is_verified = bcrypt.checkpw(request_body_info.pw.encode('utf-8'), login_user.pw.encode('utf-8'))
if not is_verified:
raise Exception('invalid password')
# TODO(hsj100): LOGIN_STATUS
update_user = Users.filter(account=request_body_info.account)
update_user.update(auto_commit=True, login='login')
token = dict(Authorization=f'Bearer {create_access_token(data=M.UserToken.from_orm(login_user).dict(exclude={"pw", "marketing_agree"}), )}')
return token
return M.ResponseBase.set_error('not supported')
except Exception as e:
if update_user:
update_user.close()
return M.ResponseBase.set_error(str(e))
@router.post('/login_out/{user_email}', status_code=200, response_model=M.TokenRes, summary='사용자 접속종료')
async def login_out(user_email: str):
"""
## 사용자 접속종료
** 방소유권을 가진 유저가 접속종료시에는 방삭제가 되며, 방에 속한 유저들의 방번호도 초기화 된다. **
현재 버전에서는 로그인/로그아웃의 상태를 유지하지 않고 상태값만을 서버에서 사용하기 때문에,\n
***로그상태는 실제상황과 다를 수 있다.***
정상처리시 Authorization(null) 반환
**결과**
- TokenRes
"""
user_info = None
try:
# TODO(hsj100): LOGIN_STATUS
user_info = Users.get(email=user_email)
if not user_info:
raise Exception('not found user')
# check room (creator)
room_info = Room.get(create_account=user_email)
if room_info:
# initialize the room index of users
search_info = Users.filter(room_id=room_info.id)
search_info.update(auto_commit=True, synchronize_session=False, room_id=None)
# delete room
# search
Room.filter(id=room_info.id).delete(auto_commit=True, synchronize_session=False)
Users.filter(email=user_email).update(auto_commit=True, login='logout')
return M.TokenRes()
except Exception as e:
if user_info:
user_info.close()
return M.ResponseBase.set_error(e)
async def is_account_exist(account: str):
get_account = Users.get(account=account)
return True if get_account else False
def create_access_token(*, data: dict = None, expires_delta: int = None):
to_encode = data.copy()
if expires_delta:
to_encode.update({'exp': datetime.utcnow() + timedelta(hours=expires_delta)})
encoded_jwt = jwt.encode(to_encode, consts.JWT_SECRET, algorithm=consts.JWT_ALGORITHM)
return encoded_jwt

235
fast_api/app/routes/dev.py Normal file
View File

@@ -0,0 +1,235 @@
# -*- coding: utf-8 -*-
"""
@File: dev.py
@Date: 2020-09-14
@author: A2TEC
@section MODIFYINFO 수정정보
- 수정자/수정일 : 수정내역
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
@brief: Developments Test
"""
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
import bcrypt
from starlette.responses import JSONResponse
from starlette.requests import Request
from inspect import currentframe as frame
import uuid
from app import models as M
from app.database.conn import db, Base
from app.database.schema import Users, Appointment, Room
# mail test
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
def send_mail():
"""
구글 계정사용시 : 보안 수준이 낮은 앱에서의 접근 활성화
:return:
"""
sender = 'jolimola@gmail.com'
sender_pw = '!ghkdtmdwns1'
# recipient = 'hsj100@a2tec.co.kr'
recipient = 'jwkim@daooldns.co.kr'
list_cc = ['cc1@gmail.com', 'cc2@naver.com']
str_cc = ','.join(list_cc)
title = 'Test mail'
contents = '''
This is test mail
using smtplib.
'''
smtp_server = smtplib.SMTP( # 1
host='smtp.gmail.com',
port=587
)
smtp_server.ehlo() # 2
smtp_server.starttls() # 2
smtp_server.ehlo() # 2
smtp_server.login(sender, sender_pw) # 3
msg = MIMEMultipart() # 4
msg['From'] = sender # 5
msg['To'] = recipient # 5
# msg['Cc'] = str_cc # 5
msg['Subject'] = contents # 5
msg.attach(MIMEText(contents, 'plain')) # 6
smtp_server.send_message(msg) # 7
smtp_server.quit() # 8
router = APIRouter(prefix='/dev')
@router.get('/test', summary='테스트', response_model=M.SWInfo)
async def test(request: Request):
"""
## ELB 상태 체크용 API
**결과**
- SWInfo
"""
# send_mail()
# print('state.user', request.state.user)
# try:
# a = 1/0
# except Exception as e:
# request.state.inspect = frame()
# raise e
result_info = dict(uuid=uuid.uuid1())
t = len(str(result_info['uuid']))
return result_info
return M.SWInfo()
@router.post('/db/add_test_data', summary='[DB] 테스트 데이터 생성', response_model=M.ResponseBase)
async def db_add_test_data(session: Session = Depends(db.session)):
"""
## 테스트 데이터 생성
프로젝트(DATABASE)에 테스트 데이터를 생성한다.
**결과**
- ResponseBase
"""
# user
hash_pw = bcrypt.hashpw(str('1234').encode('utf-8'), bcrypt.gensalt())
user_info = Users.create(session=session, auto_commit=True
, status=M.UserStatusType.active
, account='user1@test.com', pw=hash_pw
, email='user1@test.com'
, name='user1', sex=M.SexType.male
, rrn='111111-1000001', address='대구광역시 동구 1'
, phone_number='010-1111-1111', picture='Boy_1.png.png'
,user_type = 'patient', major = None, charactor = 'Male_ch01')
user_info = Users.create(session=session, auto_commit=True
, status=M.UserStatusType.active
, account='user2@test.com', pw=hash_pw
, email='user2@test.com'
, name='user2', sex=M.SexType.female
, rrn='111111-1000002', address='대구광역시 북구 2'
, phone_number='010-1111-2222', picture='Girl_1.png'
, user_type='patient', major=None, charactor='Female_ch01')
user_info = Users.create(session=session, auto_commit=True
, status=M.UserStatusType.active
, account='doctor1@doctor.com', pw=hash_pw
, email='doctor1@doctor.com'
, name='doctor1', sex=M.SexType.male
, rrn='111111-2000002', address='대구광역시 서구 3'
, phone_number='010-1111-3333', picture='Boy_2.png'
, user_type='doctor', major='정형외과', charactor='Male_ch02')
user_info = Users.create(session=session, auto_commit=True
, status=M.UserStatusType.active
, account='doctor2@doctor.com', pw=hash_pw
, email='doctor2@doctor.com'
, name='doctor2', sex=M.SexType.female
, rrn='111111-2000004', address='대구광역시 수성구 4'
, phone_number='010-1111-4444', picture='Girl_2.png'
, user_type='doctor', major='산부인과', charactor='Female_ch02')
Appointment.create(session=session, auto_commit=True,
create_account='doctor1@doctor.com',
doctor_account='doctor1@doctor.com',
patient_account='user1@test.com',
treatment_subject='정형외과',
date='2022-01-30 10:00.00')
Appointment.create(session=session, auto_commit=True,
create_account='doctor1@doctor.com',
doctor_account='doctor1@doctor.com',
patient_account='user2@test.com',
treatment_subject='정형외과',
date='2022-02-10 16:30.00')
Appointment.create(session=session, auto_commit=True,
create_account='doctor2@doctor.com',
doctor_account='doctor2@doctor.com',
patient_account='user1@test.com',
treatment_subject='산부인과',
date='2022-03-30 11:00.00')
Appointment.create(session=session, auto_commit=True,
create_account='doctor2@doctor.com',
doctor_account='doctor2@doctor.com',
patient_account='user2@test.com',
treatment_subject='산부인과',
date='2022-04-10 13:30.00')
return M.ResponseBase()
@router.post('/db/delete_all_data', summary='[DB] 데이터 삭제[전체]', response_model=M.ResponseBase)
async def db_delete_all_data():
"""
## DB 데이터 삭제
프로젝트(DATABASE)의 모든 테이블의 내용을 삭제한다. (테이블은 유지)
**결과**
- ResponseBase
"""
engine = db.engine
metadata = Base.metadata
foreign_key_turn_off = {
'mysql': 'SET FOREIGN_KEY_CHECKS=0;',
'postgresql': 'SET CONSTRAINTS ALL DEFERRED;',
'sqlite': 'PRAGMA foreign_keys = OFF;',
}
foreign_key_turn_on = {
'mysql': 'SET FOREIGN_KEY_CHECKS=1;',
'postgresql': 'SET CONSTRAINTS ALL IMMEDIATE;',
'sqlite': 'PRAGMA foreign_keys = ON;',
}
truncate_query = {
'mysql': 'TRUNCATE TABLE {};',
'postgresql': 'TRUNCATE TABLE {} RESTART IDENTITY CASCADE;',
'sqlite': 'DELETE FROM {};',
}
with engine.begin() as conn:
conn.execute(foreign_key_turn_off[engine.name])
for table in reversed(metadata.sorted_tables):
conn.execute(truncate_query[engine.name].format(table.name))
conn.execute(foreign_key_turn_on[engine.name])
return M.ResponseBase()
@router.post('/db/delete_all_tables', summary='[DB] 테이블 삭제[전체]', response_model=M.ResponseBase)
async def db_delete_all_tables():
"""
## DB 데이터 삭제
프로젝트(DATABASE)의 모든 테이블을 삭제한다.
**결과**
- ResponseBase
"""
engine = db.engine
metadata = Base.metadata
truncate_query = {
'mysql': 'DROP TABLE IF EXISTS {};',
'postgresql': 'DROP TABLE IF EXISTS {};',
'sqlite': 'DROP TABLE IF EXISTS {};',
}
with engine.begin() as conn:
for table in reversed(metadata.sorted_tables):
conn.execute(truncate_query[engine.name].format(table.name))
return M.ResponseBase()

32
fast_api/app/routes/index.py Executable file
View File

@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
"""
@File: index.py
@Date: 2020-09-14
@author: A2TEC
@section MODIFYINFO 수정정보
- 수정자/수정일 : 수정내역
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
@brief: basic & test api
"""
from fastapi import APIRouter
from app.utils.date_utils import D
from app.models import SWInfo
router = APIRouter()
@router.get('/', summary='서비스 정보', response_model=SWInfo)
async def index():
"""
## 서비스 정보
소프트웨어 이름, 버전정보, 현재시간
**결과**
- SWInfo
"""
sw_info = SWInfo()
sw_info.date = D.date_str()
return sw_info

384
fast_api/app/routes/services.py Executable file
View File

@@ -0,0 +1,384 @@
# -*- coding: utf-8 -*-
"""
@File: services.py
@Date: 2020-09-14
@author: A2TEC
@section MODIFYINFO 수정정보
- 수정자/수정일 : 수정내역
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
@brief: services api
"""
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from starlette.requests import Request
from app import models as M
from app.database.conn import db
from app.common.config import conf
from app.database.schema import Users, Appointment, Room
from app.database.crud import table_read, table_update, table_delete
router = APIRouter(prefix='/services')
@router.post('/appointment/search', response_model=M.AppointmentSearchRes, summary='진료예약 검색')
async def search_appointment(request: Request, request_body_info: M.AppointmentSearchReq):
"""
## 진료예약 검색
검색에 필요한 항목들을 Request body 에 사용한다.\n
검색에 사용된 각 항목들은 AND 조건으로 처리된다.
**전체검색**\n
empty object 사용 ( {} )\n
예) Request body: {}\n
**검색항목**\n
- 부분검색 항목\n
SQL 문법( %, _ )을 사용한다.\n
* __like: 시작포함(X%), 중간포함(%X%), 끝포함(%X)
- 구간검색 항목
* __lt: 주어진 값보다 작은값
* __lte: 주어진 값보다 같거나 작은 값
* __gt: 주어진 값보다 큰값
* __gte: 주어진 값보다 같거나 큰값
**결과**
- AppointmentSearchRes
"""
# result
return await table_read(request.state.user, Appointment, request_body_info, M.AppointmentSearchRes, M.AppointmentInfo)
@router.post('/appointment/add', response_model=M.AppointmentInfoRes, summary='진료예약 등록')
async def add_appointment(request: Request, request_body_info: M.AppointmentRegisterReq, session: Session = Depends(db.session)):
"""
## 진료예약 등록
현재 접속 계정으로 진료예약정보를 등록한다.\n
**진료 상태(status) 흐름**\n
- wait: 최초 진료예약 신청 상태
- reserve: 해당 병원 조직원(doctor)에 의해서 예약 상태로 변경
- cancel: 해당 병원 조직원(doctor)에 의해서 취소 상태로 변경
- treatment: 해당 병원 조직원(doctor)에 의해서 진료완료 상태로 변경
**결과**
- AppointmentInfoRes
"""
result_info = None
try:
# request
accessor_info = request.state.user
if not accessor_info:
raise Exception('invalid accessor')
request_info = dict()
for key, val in request_body_info.dict().items():
if val:
request_info[key] = val
if not request_info:
raise Exception('invalid request_body')
# check create_account
user_info = Users.get(account=request_body_info.create_account)
if not user_info:
raise Exception(f'not found user: {request_body_info.create_account}')
# check doctor_account
user_info = Users.get(account=request_body_info.doctor_account)
if not user_info:
raise Exception(f'not found user: {request_body_info.doctor_account}')
# check patient_account
user_info = Users.get(account=request_body_info.patient_account)
if not user_info:
raise Exception(f'not found user: {request_body_info.patient_account}')
# create
result_info = Appointment.create(session=session, auto_commit=True,
create_account=request_body_info.create_account,
doctor_account=request_body_info.doctor_account,
patient_account=request_body_info.patient_account,
treatment_subject=request_body_info.treatment_subject,
date=request_body_info.date,
status=request_body_info.status)
# result
return M.AppointmentInfoRes(data=result_info)
except Exception as e:
if result_info:
result_info.close()
return M.ResponseBase.set_error(str(e))
@router.put('/appointment/update', response_model=M.ResponseBase, summary='진료예약 변경')
async def update_appointment(request: Request, request_body_info: M.AppointmentUpdateMultiReq):
"""
## 진료예약 변경
**search_info**: 변경대상\n
**update_info**: 변경내용\n
- **비밀번호** 제외
**결과**
- ResponseBase
"""
# result
return await table_update(request.state.user, Appointment, request_body_info, M.ResponseBase)
@router.delete('/appointment/delete', response_model=M.ResponseBase, summary='진료예약 삭제')
async def delete_appointment_info(request: Request, request_body_info: M.AppointmentSearchReq):
"""
## 진료예약 삭제
조건에 해당하는 유저정보를 모두 삭제한다.\n
- **본 API는 DB에서 완적삭제를 하는 함수이며, 서버관리자가 사용하는 것을 권장한다.**
- **update API를 사용하여 상태 항목을 변경해서 사용하는 것을 권장.**
`유저삭제시 관계 테이블의 정보도 같이 삭제된다.`
**결과**
- ResponseBase
"""
# result
return await table_delete(request.state.user, Appointment, request_body_info, M.ResponseBase)
@router.post('/room/search', response_model=M.RoomSearchRes, summary='방 검색')
async def get_room(request: Request, request_body_info: M.RoomSearchReq):
"""
## 방 검색
검색에 필요한 항목들을 Request body 에 사용한다.\n
검색에 사용된 각 항목들은 AND 조건으로 처리된다.
**전체검색**\n
empty object 사용 ( {} )\n
예) Request body: {}\n
**검색항목**\n
- 부분검색 항목\n
SQL 문법( %, _ )을 사용한다.\n
* __like: 시작포함(X%), 중간포함(%X%), 끝포함(%X)
- 구간검색 항목
* __lt: 주어진 값보다 작은값
* __lte: 주어진 값보다 같거나 작은 값
* __gt: 주어진 값보다 큰값
* __gte: 주어진 값보다 같거나 큰값
**결과**
- RoomSearchRes
"""
# result
return await table_read(request.state.user, Room, request_body_info, M.RoomSearchRes, M.RoomInfo)
@router.post('/room/add', response_model=M.RoomInfoRes, summary='방 생성')
async def add_room(request: Request, request_body_info: M.RoomRegisterReq, session: Session = Depends(db.session)):
"""
## 방 등록
현재 접속 계정으로 방을 생성한다.\n
**방 상태(status) 흐름**\n
- open: 입장가능
- close: 입장불가
- tclose: 입장불가(일시적으로 입장불가 상태로 할 경우)
- full: 입장불가(만원)
**결과**
- RoomInfoRes
"""
result_info = None
try:
# request
accessor_info = request.state.user
if not accessor_info:
raise Exception('invalid accessor')
request_info = dict()
for key, val in request_body_info.dict().items():
if val:
request_info[key] = val
if not request_info:
raise Exception('invalid request_body')
# check creator
user_info = Users.get(account=request_body_info.create_account)
if not user_info:
raise Exception(f'not found user: {request_body_info.create_account}')
# check conditions
search_info = Room.filter(create_account=request_body_info.create_account).all()
if len(search_info) > 0:
raise Exception(f'already exists the rooms in user: {len(search_info)}')
# create
result_info = Room.create(session=session, auto_commit=True,
type=request_body_info.type,
name=request_body_info.name,
pw=request_body_info.pw,
create_account=request_body_info.create_account,
status=request_body_info.status,
cur_person=1 if request_body_info.auto_enter else 0,
max_person=request_body_info.max_person,
desc=request_body_info.desc)
# enter
if request_body_info.auto_enter:
Users.filter(account=request_body_info.create_account).update(auto_commit=True, synchronize_session=False, room_id=result_info.id)
# result
return M.RoomInfoRes(data=result_info)
except Exception as e:
if result_info:
result_info.close()
return M.ResponseBase.set_error(str(e))
@router.put('/room/update', response_model=M.ResponseBase, summary='방 변경')
async def update_room(request: Request, request_body_info: M.RoomUpdateMultiReq):
"""
## 방 변경
**search_info**: 변경대상\n
**update_info**: 변경내용\n
- **비밀번호** 제외
**결과**
- ResponseBase
"""
return await table_update(request.state.user, Room, request_body_info, M.ResponseBase)
@router.delete('/room/delete', response_model=M.ResponseBase, summary='방 삭제')
async def delete_room(request: Request, request_body_info: M.RoomSearchReq):
"""
## 방 삭제
조건에 해당하는 정보를 모두 삭제한다.\n
- **본 API는 DB에서 완적삭제를 하는 함수이며, 서버관리자가 사용하는 것을 권장한다.**
- **update API를 사용하여 상태 항목을 변경해서 사용하는 것을 권장.**
`삭제시, 이미 입장한 인원의 방번호는 전부 초기화 된다.`
**결과**
- ResponseBase
"""
search_info = None
try:
# request
accessor_info = request.state.user
if not accessor_info:
raise Exception('invalid accessor')
# initialize the room index of users
search_info = Users.filter(room_id=request_body_info.id)
search_info.update(auto_commit=True, synchronize_session=False, room_id=None)
# result
return await table_delete(request.state.user, Room, request_body_info, M.ResponseBase)
except Exception as e:
if search_info:
search_info.close()
return M.ResponseBase.set_error(str(e))
@router.post('/room/enter', response_model=M.RoomInfoRes, summary='방 입장')
async def enter_room(request: Request, room_id: int = Query(None, description='입장할 방 번호'), room_pw: str = Query(None, description='비밀번호')):
"""
## 방 입장
**결과**
- RoomInfoRes
"""
try:
# check - user
connect_user = request.state.user
user_info = Users.get(account=connect_user.account)
if not user_info:
raise Exception('invalid user(current)')
# check - room
room_info = Room.get(id=room_id)
if not room_info:
raise Exception('not found room')
if room_pw != room_info.pw:
raise Exception('invalid room_pw')
# check - number of person
cur_person = room_info.cur_person
if room_info.max_person <= room_info.cur_person:
raise Exception(f'full! (cur_person: {room_info.cur_person})')
# check - creator
is_creator = False
if room_info.create_account == user_info.account:
is_creator = True
if room_info.status != 'open' and not is_creator:
raise Exception('abort entrance')
# update - user
Users.filter(id=user_info.id).update(auto_commit=True, room_id=room_info.id)
# update - room
user_num = Users.filter(room_id=room_info.id).count()
result_room_info = Room.filter(id=room_info.id).update(auto_commit=True, cur_person=user_num)
# result
return M.RoomInfoRes(data=result_room_info)
except Exception as e:
return M.ResponseBase.set_error(str(e))
@router.post('/room/leave', response_model=M.RoomInfoRes, summary='방 퇴장')
async def leave_room(request: Request, room_id: int = Query(None, description='퇴장할 방 번호')):
"""
## 방 퇴장
**결과**
- RoomInfoRes
- 자동 방 삭제(**auto_delete**)가 될 경우에는 **data** 항목은 null로 반환
"""
try:
# check - user
connect_user = request.state.user
user_info = Users.get(account=connect_user.account)
if not user_info:
raise Exception(f'not found user: {connect_user.account}')
if user_info.room_id != room_id:
raise Exception(f'invalid room: {user_info.room_id}')
# check - room
room_info = Room.get(id=room_id)
if not room_info:
raise Exception('not found room')
# update - user
Users.filter(id=user_info.id).update(auto_commit=True, room_id=None)
# update - room
user_num = Users.filter(room_id=room_info.id).count()
result_room_info = Room.filter(id=room_info.id).update(auto_commit=True, cur_person=user_num)
# delete - room
if result_room_info.cur_person < 1:
if room_info.auto_delete:
Room.filter(id=room_info.id).delete(auto_commit=True)
# result
return M.ResponseBase()
# result
return M.RoomInfoRes(data=result_room_info)
except Exception as e:
return M.ResponseBase.set_error(str(e))

275
fast_api/app/routes/users.py Executable file
View File

@@ -0,0 +1,275 @@
# -*- coding: utf-8 -*-
"""
@File: users.py
@Date: 2020-09-14
@author: A2TEC
@section MODIFYINFO 수정정보
- 수정자/수정일 : 수정내역
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
@brief: users api
"""
from fastapi import APIRouter
from starlette.requests import Request
import bcrypt
from app import models as M
from app.common.config import conf
from app.database.schema import Users
from app.database.crud import table_read, table_update, table_delete
router = APIRouter(prefix='/user')
@router.get('/me', response_model=M.UserSearchRes, summary='접속자 정보')
async def get_me(request: Request):
"""
## 현재 접속된 자신정보 확인
**결과**
- UserSearchRes
"""
target_table = Users
search_info = None
try:
# request
if conf().GLOBAL_TOKEN:
raise Exception('not supported: use search api!')
accessor_info = request.state.user
if not accessor_info:
raise Exception('invalid accessor')
# search
search_info = target_table.get(account=accessor_info.account)
if not search_info:
raise Exception('not found data')
# result
result_info = list()
result_info.append(M.UserInfo.from_orm(search_info))
return M.UserSearchRes(data=result_info)
except Exception as e:
if search_info:
search_info.close()
return M.ResponseBase.set_error(str(e))
@router.post('/search', response_model=M.UserSearchRes, summary='유저정보 검색')
async def search_user(request: Request, request_body_info: M.UserSearchReq):
"""
## 유저정보 검색
검색에 필요한 항목들을 Request body 에 사용한다.\n
검색에 사용된 각 항목들은 AND 조건으로 처리된다.
**전체검색**\n
empty object 사용 ( {} )\n
예) Request body: {}\n
**검색항목**\n
- 부분검색 항목\n
SQL 문법( %, _ )을 사용한다.\n
* __like: 시작포함(X%), 중간포함(%X%), 끝포함(%X)
- 구간검색 항목
* __lt: 주어진 값보다 작은값
* __lte: 주어진 값보다 같거나 작은 값
* __gt: 주어진 값보다 큰값
* __gte: 주어진 값보다 같거나 큰값
**결과**
- UserSearchRes
"""
return await table_read(request.state.user, Users, request_body_info, M.UserSearchRes, M.UserInfo)
@router.put('/update', response_model=M.ResponseBase, summary='유저정보 변경')
async def update_user(request: Request, request_body_info: M.UserUpdateMultiReq):
"""
## 유저정보 변경
**search_info**: 변경대상\n
**update_info**: 변경내용\n
- **비밀번호** 제외
**결과**
- ResponseBase
"""
return await table_update(request.state.user, Users, request_body_info, M.ResponseBase)
@router.put('/update_pw', response_model=M.ResponseBase, summary='유저 비밀번호 변경')
async def update_user_pw(request: Request, request_body_info: M.UserUpdatePWReq):
"""
## 유저정보 비밀번호 변경
**account**의 **비밀번호**를 변경한다.
**결과**
- ResponseBase
"""
target_table = Users
search_info = None
try:
# request
accessor_info = request.state.user
if not accessor_info:
raise Exception('invalid accessor')
if not request_body_info.account:
raise Exception('invalid account')
# search
target_user = target_table.get(account=request_body_info.account)
is_verified = bcrypt.checkpw(request_body_info.current_pw.encode('utf-8'), target_user.pw.encode('utf-8'))
if not is_verified:
raise Exception('invalid password')
search_info = target_table.filter(id=target_user.id)
if not search_info.first():
raise Exception('not found data')
# process
hash_pw = bcrypt.hashpw(request_body_info.new_pw.encode('utf-8'), bcrypt.gensalt())
result_info = search_info.update(auto_commit=True, pw=hash_pw)
if not result_info or not result_info.id:
raise Exception('failed update')
# result
return M.ResponseBase()
except Exception as e:
if search_info:
search_info.close()
return M.ResponseBase.set_error(str(e))
@router.delete('/delete', response_model=M.ResponseBase, summary='유저정보 삭제')
async def delete_user(request: Request, request_body_info: M.UserSearchReq):
"""
## 유저정보 삭제
조건에 해당하는 정보를 모두 삭제한다.\n
- **본 API는 DB에서 완적삭제를 하는 함수이며, 서버관리자가 사용하는 것을 권장한다.**
- **update API를 사용하여 상태 항목을 변경해서 사용하는 것을 권장.**
`유저삭제시 관계 테이블의 정보도 같이 삭제된다.`
**결과**
- ResponseBase
"""
return await table_delete(request.state.user, Users, request_body_info, M.ResponseBase)
# NOTE(hsj100): apikey
"""
"""
# @router.get('/apikeys', response_model=List[M.GetApiKeyList])
# async def get_api_keys(request: Request):
# """
# API KEY 조회
# :param request:
# :return:
# """
# user = request.state.user
# api_keys = ApiKeys.filter(user_id=user.id).all()
# return api_keys
#
#
# @router.post('/apikeys', response_model=M.GetApiKeys)
# async def create_api_keys(request: Request, key_info: M.AddApiKey, session: Session = Depends(db.session)):
# """
# API KEY 생성
# :param request:
# :param key_info:
# :param session:
# :return:
# """
# user = request.state.user
#
# api_keys = ApiKeys.filter(session, user_id=user.id, status='active').count()
# if api_keys == MAX_API_KEY:
# raise ex.MaxKeyCountEx()
#
# alphabet = string.ascii_letters + string.digits
# s_key = ''.join(secrets.choice(alphabet) for _ in range(40))
# uid = None
# while not uid:
# uid_candidate = f'{str(uuid4())[:-12]}{str(uuid4())}'
# uid_check = ApiKeys.get(access_key=uid_candidate)
# if not uid_check:
# uid = uid_candidate
#
# key_info = key_info.dict()
# new_key = ApiKeys.create(session, auto_commit=True, secret_key=s_key, user_id=user.id, access_key=uid, **key_info)
# return new_key
#
#
# @router.put('/apikeys/{key_id}', response_model=M.GetApiKeyList)
# async def update_api_keys(request: Request, key_id: int, key_info: M.AddApiKey):
# """
# API KEY User Memo Update
# :param request:
# :param key_id:
# :param key_info:
# :return:
# """
# user = request.state.user
# key_data = ApiKeys.filter(id=key_id)
# if key_data and key_data.first().user_id == user.id:
# return key_data.update(auto_commit=True, **key_info.dict())
# raise ex.NoKeyMatchEx()
#
#
# @router.delete('/apikeys/{key_id}')
# async def delete_api_keys(request: Request, key_id: int, access_key: str):
# user = request.state.user
# await check_api_owner(user.id, key_id)
# search_by_key = ApiKeys.filter(access_key=access_key)
# if not search_by_key.first():
# raise ex.NoKeyMatchEx()
# search_by_key.delete(auto_commit=True)
# return MessageOk()
#
#
# @router.get('/apikeys/{key_id}/whitelists', response_model=List[M.GetAPIWhiteLists])
# async def get_api_keys(request: Request, key_id: int):
# user = request.state.user
# await check_api_owner(user.id, key_id)
# whitelists = ApiWhiteLists.filter(api_key_id=key_id).all()
# return whitelists
#
#
# @router.post('/apikeys/{key_id}/whitelists', response_model=M.GetAPIWhiteLists)
# async def create_api_keys(request: Request, key_id: int, ip: M.CreateAPIWhiteLists, session: Session = Depends(db.session)):
# user = request.state.user
# await check_api_owner(user.id, key_id)
# import ipaddress
# try:
# _ip = ipaddress.ip_address(ip.ip_addr)
# except Exception as e:
# raise ex.InvalidIpEx(ip.ip_addr, e)
# if ApiWhiteLists.filter(api_key_id=key_id).count() == MAX_API_WHITELIST:
# raise ex.MaxWLCountEx()
# ip_dup = ApiWhiteLists.get(api_key_id=key_id, ip_addr=ip.ip_addr)
# if ip_dup:
# return ip_dup
# ip_reg = ApiWhiteLists.create(session=session, auto_commit=True, api_key_id=key_id, ip_addr=ip.ip_addr)
# return ip_reg
#
#
# @router.delete('/apikeys/{key_id}/whitelists/{list_id}')
# async def delete_api_keys(request: Request, key_id: int, list_id: int):
# user = request.state.user
# await check_api_owner(user.id, key_id)
# ApiWhiteLists.filter(id=list_id, api_key_id=key_id).delete()
#
# return MessageOk()
#
#
# async def check_api_owner(user_id, key_id):
# api_keys = ApiKeys.get(id=key_id, user_id=user_id)
# if not api_keys:
# raise ex.NoKeyMatchEx()

View File

@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
"""
@File: date_utils.py
@Date: 2020-09-14
@author: A2TEC
@section MODIFYINFO 수정정보
- 수정자/수정일 : 수정내역
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
@brief: date-utility functions
"""
from datetime import datetime, date, timedelta
_TIMEDELTA = 9
class D:
def __init__(self, *args):
self.utc_now = datetime.utcnow()
# NOTE(hsj100): utc->kst
self.timedelta = _TIMEDELTA
@classmethod
def datetime(cls, diff: int=_TIMEDELTA) -> datetime:
return datetime.utcnow() + timedelta(hours=diff) if diff > 0 else datetime.now()
@classmethod
def date(cls, diff: int=_TIMEDELTA) -> date:
return cls.datetime(diff=diff).date()
@classmethod
def date_num(cls, diff: int=_TIMEDELTA) -> int:
return int(cls.date(diff=diff).strftime('%Y%m%d'))
@classmethod
def validate(cls, date_text):
try:
datetime.strptime(date_text, '%Y-%m-%d')
except ValueError:
raise ValueError('Incorrect data format, should be YYYY-MM-DD')
@classmethod
def date_str(cls, diff: int = _TIMEDELTA) -> str:
return cls.datetime(diff=diff).strftime('%Y-%m-%dT%H:%M:%S')
@classmethod
def check_expire_date(cls, expire_date: datetime):
td = expire_date - datetime.now()
timestamp = td.total_seconds()
return timestamp

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
"""
@File: extra.py
@Date: 2020-09-14
@author: A2TEC
@section MODIFYINFO 수정정보
- 수정자/수정일 : 수정내역
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
@brief: extra functions
"""
import uuid
from datetime import datetime, timedelta
from app.common.consts import NUM_RETRY_UUID_GEN
from app.database.schema import License
from app.utils.date_utils import D
from app import models as M

65
fast_api/app/utils/logger.py Executable file
View File

@@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
"""
@File: logger.py
@Date: 2020-09-14
@author: A2TEC
@section MODIFYINFO 수정정보
- 수정자/수정일 : 수정내역
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
@brief: logger
"""
import json
import logging
from datetime import timedelta, datetime
from time import time
from fastapi.requests import Request
from fastapi import Body
from fastapi.logger import logger
logger.setLevel(logging.INFO)
async def api_logger(request: Request, response=None, error=None):
time_format = '%Y/%m/%d %H:%M:%S'
t = time() - request.state.start
status_code = error.status_code if error else response.status_code
error_log = None
user = request.state.user
if error:
if request.state.inspect:
frame = request.state.inspect
error_file = frame.f_code.co_filename
error_func = frame.f_code.co_name
error_line = frame.f_lineno
else:
error_func = error_file = error_line = 'UNKNOWN'
error_log = dict(
errorFunc=error_func,
location='{} line in {}'.format(str(error_line), error_file),
raised=str(error.__class__.__name__),
msg=str(error.ex),
)
account = user.account.split('@') if user and user.account else None
user_log = dict(
client=request.state.ip,
user=user.id if user and user.id else None,
account='**' + account[0][2:-1] + '*@' + account[1] if user and user.account else None,
)
log_dict = dict(
url=request.url.hostname + request.url.path,
method=str(request.method),
statusCode=status_code,
errorDetail=error_log,
client=user_log,
processedTime=str(round(t * 1000, 5)) + 'ms',
datetimeUTC=datetime.utcnow().strftime(time_format),
datetimeKST=(datetime.utcnow() + timedelta(hours=9)).strftime(time_format),
)
if error and error.status_code >= 500:
logger.error(json.dumps(log_dict))
else:
logger.info(json.dumps(log_dict))

View File

@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
"""
@File: query_utils.py
@Date: 2020-09-14
@author: A2TEC
@section MODIFYINFO 수정정보
- 수정자/수정일 : 수정내역
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
@brief: query-utility functions
"""
from typing import List
def to_dict(model, *args, exclude: List = None):
q_dict = {}
for c in model.__table__.columns:
if not args or c.name in args:
if not exclude or c.name not in exclude:
q_dict[c.name] = getattr(model, c.name)
return q_dict

2
fast_api/docker-build.sh Normal file
View File

@@ -0,0 +1,2 @@
docker build -t metaverse/medical_rest:latest .

View File

@@ -0,0 +1,18 @@
version: '2.1'
services:
api:
# depends_on:
# mysql:
# condition: service_healthy
container_name: metaverse_medical_rest
image: metaverse/medical_rest:latest
build:
context: .
environment:
- TZ=Asia/Seoul
volumes:
- ./fast_api:/FAST_API
ports:
- 50510-50512:50510-50512
command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "50510"] #local
# command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "50532"] #my

10
fast_api/environment.yml Executable file
View File

@@ -0,0 +1,10 @@
name: medical_metaverse
channels:
- conda-forge
dependencies:
- python=3.9
- pip
- pip:
- -r requirements.txt

222
fast_api/gunicorn.conf.py Executable file
View File

@@ -0,0 +1,222 @@
# Gunicorn configuration file.
#
# Server socket
#
# bind - The socket to bind.
#
# A string of the form: 'HOST', 'HOST:PORT', 'unix:PATH'.
# An IP is a valid HOST.
#
# backlog - The number of pending connections. This refers
# to the number of clients that can be waiting to be
# served. Exceeding this number results in the client
# getting an error when attempting to connect. It should
# only affect servers under significant load.
#
# Must be a positive integer. Generally set in the 64-2048
# range.
#
bind = "0.0.0.0:5000"
backlog = 2048
#
# Worker processes
#
# workers - The number of worker processes that this server
# should keep alive for handling requests.
#
# A positive integer generally in the 2-4 x $(NUM_CORES)
# range. You'll want to vary this a bit to find the best
# for your particular application's work load.
#
# worker_class - The type of workers to use. The default
# sync class should handle most 'normal' types of work
# loads. You'll want to read
# http://docs.gunicorn.org/en/latest/design.html#choosing-a-worker-type
# for information on when you might want to choose one
# of the other worker classes.
#
# A string referring to a Python path to a subclass of
# gunicorn.workers.base.Worker. The default provided values
# can be seen at
# http://docs.gunicorn.org/en/latest/settings.html#worker-class
#
# worker_connections - For the eventlet and gevent worker classes
# this limits the maximum number of simultaneous clients that
# a single process can handle.
#
# A positive integer generally set to around 1000.
#
# timeout - If a worker does not notify the master process in this
# number of seconds it is killed and a new worker is spawned
# to replace it.
#
# Generally set to thirty seconds. Only set this noticeably
# higher if you're sure of the repercussions for sync workers.
# For the non sync workers it just means that the worker
# process is still communicating and is not tied to the length
# of time required to handle a single request.
#
# keepalive - The number of seconds to wait for the next request
# on a Keep-Alive HTTP connection.
#
# A positive integer. Generally set in the 1-5 seconds range.
#
# reload - Restart workers when code changes.
#
# This setting is intended for development. It will cause
# workers to be restarted whenever application code changes.
workers = 3
threads = 3
worker_class = "uvicorn.workers.UvicornWorker"
worker_connections = 1000
timeout = 60
keepalive = 2
reload = True
#
# spew - Install a trace function that spews every line of Python
# that is executed when running the server. This is the
# nuclear option.
#
# True or False
#
spew = False
#
# Server mechanics
#
# daemon - Detach the main Gunicorn process from the controlling
# terminal with a standard fork/fork sequence.
#
# True or False
#
# raw_env - Pass environment variables to the execution environment.
#
# pidfile - The path to a pid file to write
#
# A path string or None to not write a pid file.
#
# user - Switch worker processes to run as this user.
#
# A valid user id (as an integer) or the name of a user that
# can be retrieved with a call to pwd.getpwnam(value) or None
# to not change the worker process user.
#
# group - Switch worker process to run as this group.
#
# A valid group id (as an integer) or the name of a user that
# can be retrieved with a call to pwd.getgrnam(value) or None
# to change the worker processes group.
#
# umask - A mask for file permissions written by Gunicorn. Note that
# this affects unix socket permissions.
#
# A valid value for the os.umask(mode) call or a string
# compatible with int(value, 0) (0 means Python guesses
# the base, so values like "0", "0xFF", "0022" are valid
# for decimal, hex, and octal representations)
#
# tmp_upload_dir - A directory to store temporary request data when
# requests are read. This will most likely be disappearing soon.
#
# A path to a directory where the process owner can write. Or
# None to signal that Python should choose one on its own.
#
daemon = False
pidfile = None
umask = 0
user = None
group = None
tmp_upload_dir = None
#
# Logging
#
# logfile - The path to a log file to write to.
#
# A path string. "-" means log to stdout.
#
# loglevel - The granularity of log output
#
# A string of "debug", "info", "warning", "error", "critical"
#
errorlog = "-"
loglevel = "info"
accesslog = None
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
#
# Process naming
#
# proc_name - A base to use with setproctitle to change the way
# that Gunicorn processes are reported in the system process
# table. This affects things like 'ps' and 'top'. If you're
# going to be running more than one instance of Gunicorn you'll
# probably want to set a name to tell them apart. This requires
# that you install the setproctitle module.
#
# A string or None to choose a default of something like 'gunicorn'.
#
proc_name = "NotificationAPI"
#
# Server hooks
#
# post_fork - Called just after a worker has been forked.
#
# A callable that takes a server and worker instance
# as arguments.
#
# pre_fork - Called just prior to forking the worker subprocess.
#
# A callable that accepts the same arguments as after_fork
#
# pre_exec - Called just prior to forking off a secondary
# master process during things like config reloading.
#
# A callable that takes a server instance as the sole argument.
#
def post_fork(server, worker):
server.log.info("Worker spawned (pid: %s)", worker.pid)
def pre_fork(server, worker):
pass
def pre_exec(server):
server.log.info("Forked child, re-executing.")
def when_ready(server):
server.log.info("Server is ready. Spawning workers")
def worker_int(worker):
worker.log.info("worker received INT or QUIT signal")
# get traceback info
import threading, sys, traceback
id2name = {th.ident: th.name for th in threading.enumerate()}
code = []
for threadId, stack in sys._current_frames().items():
code.append("\n# Thread: %s(%d)" % (id2name.get(threadId, ""), threadId))
for filename, lineno, name, line in traceback.extract_stack(stack):
code.append('File: "%s", line %d, in %s' % (filename, lineno, name))
if line:
code.append(" %s" % (line.strip()))
worker.log.debug("\n".join(code))
def worker_abort(worker):
worker.log.info("worker received SIGABRT signal")

14
fast_api/requirements.txt Executable file
View File

@@ -0,0 +1,14 @@
fastapi==0.70.1
uvicorn==0.16.0
pymysql==1.0.2
sqlalchemy==1.4.29
bcrypt==3.2.0
pyjwt==2.3.0
yagmail==0.14.261
boto3==1.20.32
pytest==6.2.5
cryptography
#extra
pycryptodomex
pycryptodome

18
fast_api/test_main.py Executable file
View File

@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
"""
@File: test_main.py
@Date: 2020-09-14
@author: A2TEC
@section MODIFYINFO 수정정보
- 수정자/수정일 : 수정내역
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
@brief: 개발용 실행
"""
import uvicorn
from app.common.config import conf
if __name__ == '__main__':
print('test_main.py run')
uvicorn.run('app.main:app', host='0.0.0.0', port=conf().REST_SERVER_PORT, reload=True)

0
fast_api/tests/__init__.py Executable file
View File

79
fast_api/tests/conftest.py Executable file
View File

@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
"""
@File: conftest.py
@Date: 2020-09-14
@author: A2TEC
@section MODIFYINFO 수정정보
- 수정자/수정일 : 수정내역
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
@brief: test config
"""
import asyncio
import os
from os import path
from typing import List
import pytest
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from app.database.schema import Users
from app.main import create_app
from app.database.conn import db, Base
from app.models import UserToken
from app.routes.auth import create_access_token
"""
1. DB 생성
2. 테이블 생성
3. 테스트 코드 작동
4. 테이블 레코드 삭제
"""
@pytest.fixture(scope='session')
def app():
os.environ['API_ENV'] = 'test'
return create_app()
@pytest.fixture(scope='session')
def client(app):
# Create tables
Base.metadata.create_all(db.engine)
return TestClient(app=app)
@pytest.fixture(scope='function', autouse=True)
def session():
sess = next(db.session())
yield sess
clear_all_table_data(
session=sess,
metadata=Base.metadata,
except_tables=[]
)
sess.rollback()
@pytest.fixture(scope='function')
def login(session):
"""
테스트전 사용자 미리 등록
:param session:
:return:
"""
db_user = Users.create(session=session, email='ryan_test@dingrr.com', pw='123')
session.commit()
access_token = create_access_token(data=UserToken.from_orm(db_user).dict(exclude={'pw', 'marketing_agree'}),)
return dict(Authorization=f'Bearer {access_token}')
def clear_all_table_data(session: Session, metadata, except_tables: List[str] = None):
session.execute('SET FOREIGN_KEY_CHECKS = 0;')
for table in metadata.sorted_tables:
if table.name not in except_tables:
session.execute(table.delete())
session.execute('SET FOREIGN_KEY_CHECKS = 1;')
session.commit()

44
fast_api/tests/test_auth.py Executable file
View File

@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
"""
@File: test_auth.py
@Date: 2020-09-14
@author: A2TEC
@section MODIFYINFO 수정정보
- 수정자/수정일 : 수정내역
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
@brief: test auth.
"""
from app.database.conn import db
from app.database.schema import Users
def test_registration(client, session):
"""
레버 로그인
:param client:
:param session:
:return:
"""
user = dict(email='ryan@dingrr.com', pw='123', name='라이언', phone='01099999999')
res = client.post('api/auth/register/email', json=user)
res_body = res.json()
print(res.json())
assert res.status_code == 201
assert 'Authorization' in res_body.keys()
def test_registration_exist_email(client, session):
"""
레버 로그인
:param client:
:param session:
:return:
"""
user = dict(email='Hello@dingrr.com', pw='123', name='라이언', phone='01099999999')
db_user = Users.create(session=session, **user)
session.commit()
res = client.post('api/auth/register/email', json=user)
res_body = res.json()
assert res.status_code == 400
assert 'EMAIL_EXISTS' == res_body['msg']

33
fast_api/tests/test_user.py Executable file
View File

@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
"""
@File: test_user.py
@Date: 2020-09-14
@author: A2TEC
@section MODIFYINFO 수정정보
- 수정자/수정일 : 수정내역
- 2022-01-14/hsj100@a2tec.co.kr : refactoring
@brief: test user
"""
from app.database.conn import db
from app.database.schema import Users
def test_create_get_apikey(client, session, login):
"""
레버 로그인
:param client:
:param session:
:return:
"""
key = dict(user_memo='ryan__key')
res = client.post('api/user/apikeys', json=key, headers=login)
res_body = res.json()
assert res.status_code == 200
assert 'secret_key' in res_body
res = client.get('api/user/apikeys', headers=login)
res_body = res.json()
assert res.status_code == 200
assert 'ryan__key' in res_body[0]['user_memo']

27
readme.md Normal file
View File

@@ -0,0 +1,27 @@
# A2TEC_METAVERSE_MEDICAL_REST_SERVER
A2TEC_METAVERSE_MEDICAL_REST_SERVER
RESTful API Server
### System Requirements
- Ubuntu 20.04
- docker 20.10.17
- docker-compose 1.29.2
### Docker
- Base Image
* python:3.9.7-slim
* mysql:latest
- Target Image
* metaverse/medical_rest:latest
- Build & Run
* docker-compose.yml 파일의 환경변수(environment) 수정
* fast_api/app/common/consts.py 환경에 맞게 수정 후 실행
```
docker-compose up -d
```