Skip to content

Commit

Permalink
Merge pull request #3 from f-lab-edu/dev
Browse files Browse the repository at this point in the history
[develop] branch 생성
  • Loading branch information
oortclwd committed Apr 11, 2022
2 parents 045d55f + b2b196b commit a37d525
Show file tree
Hide file tree
Showing 20 changed files with 363 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ venv/
ENV/
env.bak/
venv.bak/
.vscode/

# Spyder project settings
.spyderproject
Expand Down
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,30 @@
<h1 align="center">Cafe Search: CSB</h1>
<p align="center">1차 프로젝트: 개발자 모각코 분석 프로그램</p>

```
CAFE-SEARCH-CSB
├─.gitignore
├─ README.MD
├─ requirements.txt
└─backend/
├─apis/
│ ├─version1/
│ │ └─ route_users.py
│ └─ base.py
├─core/
│ └─ config.py
├─db/
│ ├─logics/
│ │ └─users.py
│ ├─models/
│ │ └─users.py
│ ├─base.py
│ ├─base_class.py
│ └─session.py
├─schemas/
│ └─users.py
└─tests/
│ └─conftest.py
└─test_routes/
└─test_users.py
```
8 changes: 8 additions & 0 deletions backend/apis/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from fastapi import APIRouter

from apis.version1 import route_users
from apis.version1 import route_login
api_router = APIRouter()

api_router.include_router(route_users.router, prefix="/users", tags=["users"])
api_router.include_router(route_login.router, prefix="/login", tags=["login"])
36 changes: 36 additions & 0 deletions backend/apis/version1/route_login.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from datetime import timedelta

from core.config import settings
from db.session import get_db
from db.logics.users import get_user
from db.logics.login import create_access_token
from fastapi import (
APIRouter,
Depends,
HTTPException,
status
)
from fastapi.security import OAuth2PasswordRequestForm

from schemas.tokens import Token
from sqlalchemy.orm import Session

router = APIRouter()

@router.post("/token", response_model=Token)
def login_for_access_token(
form_data: OAuth2PasswordRequestForm = Depends(),
db: Session = Depends(get_db)
):
user = get_user(form_data.username, form_data.password, db)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect Username or Password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"user_email":user.email}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
13 changes: 13 additions & 0 deletions backend/apis/version1/route_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from db.session import get_db
from db.logics.users import create_new_user
from fastapi import APIRouter, Depends, status
from schemas.users import UserCreate, ShowUser
from sqlalchemy.orm import Session

router = APIRouter()


@router.post("/", response_model=ShowUser, status_code=status.HTTP_201_CREATED)
def create_user(user: UserCreate, db: Session = Depends(get_db)):
user = create_new_user(user=user, db=db)
return user
45 changes: 45 additions & 0 deletions backend/core/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from pydantic import BaseSettings, SecretStr

from pathlib import Path

BASE_DIR = Path(__file__).resolve().parents[2]


class Settings(BaseSettings):
PROJECT_NAME: str = "Cafe Search"
PROJECT_VERSION: str = "1.0.0"

DB_USERNAME: str
DB_PASSWORD: SecretStr
DB_HOST: str
DB_PORT: int
DB_NAME: str

SECRET_KEY: str
SECRET_ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30


class Config:
env_file = str(BASE_DIR / ".env")
env_file_encoding = "utf-8"

class TestSettings(BaseSettings):
TEST_DB_USERNAME: str
TEST_DB_PASSWORD: SecretStr
TEST_DB_HOST: str
TEST_DB_PORT: int
TEST_DB_NAME: str

SECRET_KEY: str
SECRET_ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30


class Config:
env_file = str(BASE_DIR / ".env")
env_file_encoding = "utf-8"



settings = Settings()
12 changes: 12 additions & 0 deletions backend/core/hashing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from passlib.context import CryptContext

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

class Hasher:
@staticmethod
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)

@staticmethod
def get_password_hash(password):
return pwd_context.hash(password)
2 changes: 2 additions & 0 deletions backend/db/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from db.base_class import Base
from db.models.users import User
12 changes: 12 additions & 0 deletions backend/db/base_class.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from typing import Any
from sqlalchemy.ext.declarative import as_declarative, declared_attr


@as_declarative()
class Base:
id: Any
__name__: str

@declared_attr
def __tablename__(cls) -> str:
return cls.__name__.lower()
18 changes: 18 additions & 0 deletions backend/db/logics/login.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from core.config import settings
from datetime import datetime, timedelta
from jose import jwt
from typing import Optional

def create_access_token(data: dict, expires_delta: Optional[timedelta]=None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(
minutes = settings.ACCESS_TOKEN_EXPIRE_MINUTES
)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(
to_encode, settings.SECRET_KEY, algorithm=settings.SECRET_ALGORITHM
)
return encoded_jwt
25 changes: 25 additions & 0 deletions backend/db/logics/users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from core.hashing import Hasher
from db.models.users import User
from schemas.users import UserCreate
from sqlalchemy.orm import Session


def create_new_user(user: UserCreate, db: Session) -> User:
user = User(
username=user.username,
email=user.email,
hashed_password=Hasher.get_password_hash(user.password),
is_superuser=False,
)
db.add(user)
db.commit()
db.refresh(user)
return user

def get_user(username: str, password: str, db: Session) -> User:
user = db.query(User).filter(User.email == username).first()
if not user:
return False
if not Hasher.verify_password(password, user.hashed_password):
return False
return user
6 changes: 6 additions & 0 deletions backend/db/models/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from sqlalchemy import Column, Integer, DateTime, func

class BaseMixin:
id = Column(Integer, primary_key=True, index=True)
created_at = Column(DateTime, nullable=False, default=func.utc_timestamp())
updated_at = Column(DateTime, nullable=False, default=func.utc_timestamp(), onupdate=func.utc_timestamp())
11 changes: 11 additions & 0 deletions backend/db/models/users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from sqlalchemy import Column, String, Boolean

from db.base_class import Base
from db.models.base import BaseMixin


class User(Base, BaseMixin):
username = Column(String(40), unique=True, nullable=False)
email = Column(String(60), unique=True, nullable=False, index=True)
hashed_password = Column(String(100), nullable=False)
is_superuser = Column(Boolean(), default=False)
25 changes: 25 additions & 0 deletions backend/db/session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from typing import Generator
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

from core.config import settings

engine = create_engine(
"mysql+pymysql://{username}:{password}@{host}:{port}/{name}?charset=utf8mb4".format(
username=settings.DB_USERNAME,
password=settings.DB_PASSWORD.get_secret_value(),
host=settings.DB_HOST,
port=settings.DB_PORT,
name=settings.DB_NAME,
)
)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)


def get_db() -> Generator:
try:
db = SessionLocal()
yield db
finally:
db.close()
16 changes: 16 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from apis.base import api_router
from core.config import settings
from fastapi import FastAPI


def include_router(app: FastAPI):
app.include_router(api_router)


def start_application():
app = FastAPI(title=settings.PROJECT_NAME, version=settings.PROJECT_VERSION)
include_router(app)
return app


app = start_application()
13 changes: 13 additions & 0 deletions backend/schemas/tokens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from pydantic import BaseModel, EmailStr

class UserCreate(BaseModel):
username: str
email: EmailStr
password: str

class ShowUser(BaseModel):
username: str
email: EmailStr

class Config:
orm_mode = True
5 changes: 5 additions & 0 deletions backend/schemas/users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from pydantic import BaseModel

class Token(BaseModel):
access_token: str
token_type: str
65 changes: 65 additions & 0 deletions backend/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from fastapi import FastAPI
from fastapi.testclient import TestClient
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from typing import Any, Generator

import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from apis.base import api_router
from db.base import Base
from db.session import get_db
from core.config import TestSettings

def start_application() -> FastAPI:
app = FastAPI()
app.include_router(api_router)
return app


settings = TestSettings()
engine = create_engine(
"mysql+pymysql://{username}:{password}@{host}:{port}/{name}?charset=utf8mb4".format(
username=settings.TEST_DB_USERNAME,
password=settings.TEST_DB_PASSWORD.get_secret_value(),
host=settings.TEST_DB_HOST,
port=settings.TEST_DB_PORT,
name=settings.TEST_DB_NAME,
)
)

SessionTesting = sessionmaker(autocommit=False, autoflush=False, bind=engine)

@pytest.fixture(scope="module")
def app() -> Generator[FastAPI, Any, None]:
Base.metadata.create_all(engine)
_app = start_application()
yield _app
Base.metadata.drop_all(engine)

@pytest.fixture(scope="module")
def db_session(app: FastAPI) -> Generator[SessionTesting, Any, None]:
connection = engine.connect()
transaction = connection.begin()
session = SessionTesting(bind=connection)
yield session
session.close()
transaction.rollback()
connection.close()

@pytest.fixture(scope="module")
def client(
app: FastAPI, db_session: SessionTesting
) -> Generator[TestClient, Any, None]:
def _get_test_db():
try:
yield db_session
finally:
pass

app.dependency_overrides[get_db] = _get_test_db
with TestClient(app) as client:
yield client
12 changes: 12 additions & 0 deletions backend/tests/test_routes/test_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import json

def test_create_user(client):
data = {
"username": "test-sbjo",
"email": "testsb@jo.com",
"password" : "test-password"
}
response = client.post("/users/", json.dumps(data))
assert response.status_code == 201
assert response.json()["username"] == "test-sbjo"
assert response.json()["email"] == "testsb@jo.com"
10 changes: 10 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
pymysql
pydantic[dotenv]

sqlalchemy
fastapi
requests
pytest
uvicorn[standard]
python-jose[cryptography]
passlib[bcrypt]

0 comments on commit a37d525

Please sign in to comment.