diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/database.py b/core/database.py new file mode 100644 index 0000000..7e7fa3e --- /dev/null +++ b/core/database.py @@ -0,0 +1,21 @@ +from sqlalchemy import create_engine, MetaData +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from starlette.config import Config + +config = Config('.env') +SQLALCHEMY_DATABASE_URL = config('SQLALCHEMY_DATABASE_URL') + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} +) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/core/models/__init__.py b/core/models/__init__.py new file mode 100644 index 0000000..e06e933 --- /dev/null +++ b/core/models/__init__.py @@ -0,0 +1 @@ +from core.models import models \ No newline at end of file diff --git a/core/models/models.py b/core/models/models.py new file mode 100644 index 0000000..6d311aa --- /dev/null +++ b/core/models/models.py @@ -0,0 +1,15 @@ +from sqlalchemy import Column, Integer, ForeignKey, String, DateTime +from sqlalchemy.orm import relationship + +from core.database import Base + + +class User(Base): + __tablename__ = "user" + + id = Column(Integer, primary_key=True, autoincrement=True) + stdId = Column(String, unique=True, nullable=False) + password = Column(String, nullable=False) + name = Column(String, nullable=False) + authority = Column(Integer, nullable=False) + hide = Column(Integer, nullable=False) diff --git a/core/schemas/__init__.py b/core/schemas/__init__.py new file mode 100644 index 0000000..cf94c10 --- /dev/null +++ b/core/schemas/__init__.py @@ -0,0 +1,4 @@ +from core.schemas import playlist_info +from core.schemas import song +from core.schemas import user + diff --git a/core/schemas/user.py b/core/schemas/user.py new file mode 100644 index 0000000..af3e909 --- /dev/null +++ b/core/schemas/user.py @@ -0,0 +1,53 @@ +from pydantic import BaseModel, field_validator + + +class User(BaseModel): + id: int + stdId: str + password: str + name: str + authority: int + hide: int = 0 + + class Token(BaseModel): + access_token: str + token_type: str + student_id: str + + +class UserInformation(BaseModel): + id: int + stdId: str + name: str + authority: int + + +class UserCreate(BaseModel): + stdId: str + password: str + name: str + + @field_validator('stdId', 'password', 'name') + def not_empty(cls, v): + if not v or not v.strip(): + raise ValueError('빈 값은 허용되지 않습니다.') + return v + + @field_validator('stdId') + def student_id_length(cls, v): + if len(v) != 8: + raise ValueError('학번은 8자리여야 합니다.') + return v + + +class UserList(BaseModel): + total: int = 0 + user_list: list[UserInformation] = [] + + +class UserUpdate(UserCreate): + user_id: int + + +class UserDelete(BaseModel): + user_id: int diff --git a/core/settings.py b/core/settings.py new file mode 100644 index 0000000..e69de29 diff --git a/dependencies.py b/dependencies.py new file mode 100644 index 0000000..37e219c --- /dev/null +++ b/dependencies.py @@ -0,0 +1,45 @@ +from enum import Enum + +from fastapi import Depends, HTTPException +from fastapi.security import OAuth2PasswordBearer +from jose import jwt, JWTError +from sqlalchemy.orm import Session +from starlette import status +from starlette.config import Config + +import routers.user.user_crud as user_crud +from core.database import get_db + +config = Config('.env') +SQLALCHEMY_DATABASE_URL = config('SQLALCHEMY_DATABASE_URL') + +ACCESS_TOKEN_EXPIRE_MINUTES = float(config('ACCESS_TOKEN_EXPIRE_MINUTES')) +SECRET_KEY = config('SECRET_KEY') +ALGORITHM = config('ALGORITHM') +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/user/login") + + +def get_current_user(token: str = Depends(oauth2_scheme), + db: Session = Depends(get_db)): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + student_id: str = payload.get("sub") + if student_id is None: + raise credentials_exception + except JWTError: + raise credentials_exception + else: + student_id = user_crud.get_user(db, student_id=student_id) + if student_id is None: + raise credentials_exception + return student_id + + +class Authority(Enum): + Admin = 0 + User = 1 diff --git a/internal/__init__.py b/internal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/internal/admin.py b/internal/admin.py new file mode 100644 index 0000000..ee34970 --- /dev/null +++ b/internal/admin.py @@ -0,0 +1,55 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from starlette import status + +import internal.admin_crud as admin_crud +from core.database import get_db +from core.models import models +from dependencies import get_current_user, Authority + +router = APIRouter( + prefix="/api/admin", + tags=["admin"], + dependencies=[Depends(get_current_user)] +) + + +# Authority 설정 +# Create +@router.post("/authority", status_code=status.HTTP_204_NO_CONTENT) +async def create_authority(student_id: str, current_user: models.User = Depends(get_current_user), db: Session = Depends(get_db)): + if current_user.authority != Authority.Admin.value: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, + detail="권한이 없습니다.") + + admin_crud.set_authority(student_id=student_id, db=db) + + +# Read +@router.get("/authority/{student_id}") +async def get_user_authority_level(student_id: str, current_user: models.User = Depends(get_current_user), db: Session = Depends(get_db)): + if current_user.authority != Authority.Admin.value: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, + detail="권한이 없습니다.") + + return {"authority": Authority(admin_crud.get_authority(student_id=student_id, db=db)).name} + + +# Update +@router.put("/authority") +async def toggle_authority(student_id: str, current_user: models.User = Depends(get_current_user), db: Session = Depends(get_db)): + if current_user.authority != Authority.Admin.value: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, + detail="권한이 없습니다.") + + return {"message": f"Update Authority {admin_crud.toggle_authority(student_id=student_id, db=db)}"} + + +# Delete +@router.delete("/authority", status_code=status.HTTP_204_NO_CONTENT) +async def delete_authority(student_id: str, current_user: models.User = Depends(get_current_user), db: Session = Depends(get_db)): + if current_user.authority != Authority.Admin.value: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, + detail="권한이 없습니다.") + admin_crud.unset_authority(student_id=student_id, db=db) +# !Authority 설정 diff --git a/internal/admin_crud.py b/internal/admin_crud.py new file mode 100644 index 0000000..33c3788 --- /dev/null +++ b/internal/admin_crud.py @@ -0,0 +1,29 @@ +from sqlalchemy.orm import Session + +from core.models.models import User + + +def set_authority(db: Session, student_id: str): + db.query(User).filter(User.stdId == student_id).update({"authority": 0}) + db.commit() + + +def unset_authority(db: Session, student_id: str): + db.query(User).filter(User.stdId == student_id).update({"authority": 1}) + db.commit() + + +def get_authority(db: Session, student_id: str): + return db.query(User).filter(User.stdId == student_id).first().authority + + +def toggle_authority(db: Session, student_id: str): + authority = get_authority(db, student_id) + if authority == 0: + unset_authority(db, student_id) + else: + set_authority(db, student_id) + db.commit() + + return get_authority(db, student_id) + diff --git a/main.py b/main.py new file mode 100644 index 0000000..4acfcd1 --- /dev/null +++ b/main.py @@ -0,0 +1,28 @@ +from typing import Annotated + +import uvicorn +from fastapi import ( + Cookie, + FastAPI, + Query, + WebSocket, + WebSocketException, + WebSocketDisconnect, + status, +) +from fastapi.responses import HTMLResponse + +from internal import admin +from routers.user import users_router as user + +app = FastAPI() + +# internal +app.include_router(admin.router) + +# routers +app.include_router(user.router) + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=80, reload=True) \ No newline at end of file diff --git a/routers/__init__.py b/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routers/user/__init__.py b/routers/user/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routers/user/playlist/__init__.py b/routers/user/playlist/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routers/user/playlist/playlist_crud.py b/routers/user/playlist/playlist_crud.py new file mode 100644 index 0000000..1a6a81b --- /dev/null +++ b/routers/user/playlist/playlist_crud.py @@ -0,0 +1,59 @@ +from datetime import datetime + +from sqlalchemy.orm import Session + +from core.models.models import PlayListInfo, User +from core.schemas.playlist_info import PlaylistInfoCreate +from routers.user.user_crud import get_user + + +def create_playlist(db: Session, playlist_create: PlaylistInfoCreate, current_user: User): + db_playlist = PlayListInfo( + owner=current_user.id, + title=playlist_create.title, + description=playlist_create.description, + create_time=datetime.now(), + hide=0, + ) + db.add(db_playlist) + db.commit() + + class Config: + orm_mode = True + + +def get_playlist_list(db: Session, skip: int = 0, limit: int = 10, keyword: str = ''): + _playlist_list = db.query(PlayListInfo).filter(PlayListInfo.hide == 0).order_by(PlayListInfo.id.desc()) + if keyword: + search = '%%{}%%'.format(keyword) + _playlist_list = _playlist_list.filter( + PlayListInfo.title.ilike(search) | + PlayListInfo.description.ilike(search) | + PlayListInfo.owner.ilike(search) + # | _playlist_list.join(PlayListInfo.playlist).any(PlayListInfo.title.ilike(search)) + ) + + total = _playlist_list.count() + playlist_list = _playlist_list.order_by(PlayListInfo.id.desc()).offset(skip).limit(limit).distinct().all() + + return total, playlist_list + + +def get_playlist(db: Session, playlist_id: int): + return db.query(PlayListInfo).filter(PlayListInfo.id == playlist_id, PlayListInfo.hide == 0).first() + + +def delete_playlist(db: Session, playlist_id: int): + db.query(PlayListInfo).filter(PlayListInfo.id == playlist_id).update({"hide": 1}) + db.commit() + + +def update_playlist(db: Session, db_playlist: PlayListInfo, playlist_update: PlaylistInfoCreate): + db_playlist.title = playlist_update.title + db_playlist.description = playlist_update.description + + db.add(db_playlist) + db.commit() + + class Config: + orm_mode = True diff --git a/routers/user/playlist/playlist_router.py b/routers/user/playlist/playlist_router.py new file mode 100644 index 0000000..66e89e7 --- /dev/null +++ b/routers/user/playlist/playlist_router.py @@ -0,0 +1,83 @@ +from starlette import status +from fastapi import APIRouter, Depends, HTTPException + +from core.database import get_db +from core.schemas import playlist_info +from routers.user.playlist import playlist_crud +from dependencies import get_current_user +from core.models import models + +router = APIRouter( + prefix="/api/playlist", + tags=["playlist"], +) + + +# Playlist_info +# Create +@router.post("/", status_code=status.HTTP_204_NO_CONTENT) +async def create_playlist(_playlist_create: playlist_info.PlaylistInfoCreate, + db=Depends(get_db), + current_user: models.User = Depends(get_current_user)): + playlist_crud.create_playlist(db, _playlist_create, current_user) + + +# Read +@router.get("/list", response_model=playlist_info.PlaylistList) +async def read_playlist_list(page: int = 0, + size: int = 10, + db=Depends(get_db), + search: str = ''): + total, _playlist_list = playlist_crud.get_playlist_list(db, skip=page * size, limit=size, keyword=search) + return {"total": total, "playlist_list": _playlist_list} + + +@router.get("/{playlist_id}", response_model=playlist_info.PlayListInfo) +async def read_playlist(playlist_id: str, db=Depends(get_db)): + playlist_data = playlist_crud.get_playlist(playlist_id=int(playlist_id), db=db) + + if not playlist_data: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, + detail="데이터를 찾을수 없습니다.") + + return { + "id": playlist_data.id, + "owner": playlist_data.owner, + "title": playlist_data.title, + "description": playlist_data.description, + "create_time": playlist_data.create_time, + } + + +# Update +@router.put("/{playlist_id}", status_code=status.HTTP_204_NO_CONTENT) +async def update_playlist(_update_playlist: playlist_info.PlaylistUpdate, + db=Depends(get_db), + current_user: models.User = Depends(get_current_user)): + db_playlist = playlist_crud.get_playlist(playlist_id=_update_playlist.playlist_id, db=db) + + if not db_playlist: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, + detail="데이터를 찾을수 없습니다.") + if db_playlist.owner != current_user.id and current_user.authority != 0: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, + detail="수정 권한이 없습니다.") + + playlist_crud.update_playlist(db, db_playlist, _update_playlist) + + +# Delete +@router.delete("/{playlist_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_playlist(playlist_id: str, current_user: models.User = Depends(get_current_user), db=Depends(get_db)): + db_playlist = playlist_crud.get_playlist(playlist_id=int(playlist_id), db=db) + + if not db_playlist: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, + detail="데이터를 찾을수 없습니다.") + if db_playlist.owner != current_user.id and current_user.authority != 0: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, + detail="삭제 권한이 없습니다.") + + playlist_crud.delete_playlist(db, playlist_id=int(playlist_id)) + +# !Playlist_info diff --git a/routers/user/user_crud.py b/routers/user/user_crud.py new file mode 100644 index 0000000..ac80734 --- /dev/null +++ b/routers/user/user_crud.py @@ -0,0 +1,59 @@ +from passlib.context import CryptContext +from sqlalchemy.orm import Session + +from core.models.models import User +from core.schemas.user import UserCreate, UserUpdate + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +def create_user(db: Session, user_create: UserCreate): + db_user = User( + stdId=user_create.stdId, + password=pwd_context.hash(user_create.password), + name=user_create.name, + authority=1, + hide=0, + ) + db.add(db_user) + db.commit() + + +def get_existing_user(db: Session, user_create: UserCreate): + _db_user = db.query(User).filter(User.stdId == user_create.stdId, User.hide == 0).first() + return + + +def get_user(db: Session, student_id: str): + return db.query(User).filter(User.stdId == student_id, User.hide == 0).first() + + +def get_user_list(db: Session, skip: int = 0, limit: int = 10, keyword: str = ''): + _user_list = db.query(User).filter(User.hide == 0).order_by(User.stdId.desc()) + if keyword: + search = '%%{}%%'.format(keyword) + _user_list = _user_list.filter(User.name.ilike(search) | User.stdId.ilike(search)) + + total = _user_list.count() + user_list = _user_list.order_by(User.id.desc()).offset(skip).limit(limit).distinct().all() + + return total, user_list + + +def delete_user(db: Session, student_id: str): + db.query(User).filter(User.stdId == student_id).update({"hide": 1}) + db.commit() + + +def update_user(db: Session, db_user: User, user_update: UserUpdate, authority: int): + #todo: 플레이리스 관련 수정 필요 + # 수정가능한 사용자 정보: 생성한 플레이리스트 + + db_user.password = pwd_context.hash(user_update.password) + + if authority == 0: + db_user.name = user_update.name + db_user.authority = user_update.authority + + db.add(db_user) + db.commit() diff --git a/routers/user/users_router.py b/routers/user/users_router.py new file mode 100644 index 0000000..0e359dc --- /dev/null +++ b/routers/user/users_router.py @@ -0,0 +1,142 @@ +from datetime import timedelta, datetime + +from fastapi import APIRouter, Depends, HTTPException +from fastapi.security import OAuth2PasswordRequestForm, OAuth2PasswordBearer +from jose import jwt +from sqlalchemy.orm import Session +from starlette import status +from starlette.config import Config + +import routers.user.user_crud as user_crud +from core.database import get_db +from core.models import models +from core.schemas import user +from dependencies import get_current_user +from routers.user.user_crud import pwd_context + +config = Config('.env') +SQLALCHEMY_DATABASE_URL = config('SQLALCHEMY_DATABASE_URL') + +ACCESS_TOKEN_EXPIRE_MINUTES = float(config('ACCESS_TOKEN_EXPIRE_MINUTES')) +SECRET_KEY = config('SECRET_KEY') +ALGORITHM = config('ALGORITHM') +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/user/login") + +router = APIRouter( + prefix="/api/user", + tags=["user"] +) + + +# User +# Create +@router.post("/register", status_code=status.HTTP_204_NO_CONTENT) +def create_users(_user_create: user.UserCreate, db: Session = Depends(get_db)): + user_data = user_crud.get_existing_user(db, user_create=_user_create) + if user_data: + raise HTTPException(status_code=status.HTTP_409_CONFLICT, + detail="이미 존재하는 사용자입니다.") + user_crud.create_user(db, _user_create) + + +# Read +@router.get("/list", response_model=user.UserList) +async def read_users(page: int = 0, size: int = 10, db: Session = Depends(get_db), search: str = ''): + + total, _user_list = user_crud.get_user_list(db, skip=page*size, limit=size, keyword=search) + + return { + "total": total, + "user_list": _user_list + } + + + +@router.get("/me", response_model=user.UserInformation) +def read_current_user(current_user: models.User = Depends(get_current_user), db=Depends(get_db)): + # Todo : 생성한 플레이리스트 정보 추가 + user_data = user_crud.get_user(db, student_id=current_user.stdId) + + return { + "id": user_data.id, + "stdId": user_data.stdId, + "name": user_data.name, + "authority": user_data.authority, + } + + +@router.get("/{student_id}", response_model=user.UserInformation) +async def read_user(student_id: str, db=Depends(get_db)): + # Todo : 생성한 플레이리스트 정보 추가 + user_data = user_crud.get_user(db, student_id=student_id) + + return { + "id": user_data.id, + "stdId": user_data.stdId, + "name": user_data.name, + "authority": user_data.authority, + } + + +# Update +@router.put("/{student_id}", status_code=status.HTTP_204_NO_CONTENT) +async def update_user(_user_update: user.UserUpdate, + db: Session = Depends(get_db), + current_user: models.User = Depends(get_current_user)): + # Todo : 특정 사용자의 정보를 수정하는 API + # 수정가능한 사용자 정보: 이름, 권한, 생성한 플레이리스트, 비밀번호 + # 비밀번호는 암호화된 상태로 저장되어야 함 + # 이름과 권한은 관리자만 수정 가능 + db_user = user_crud.get_user(db, student_id=_user_update.stdId) + + if not db_user: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, + detail="데이터를 찾을수 없습니다.") + if current_user.id != db_user.user.id and current_user.authority != 0: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, + detail="수정 권한이 없습니다.") + + user_crud.update_user(db=db, db_user=db_user, user_update=_user_update, authority=current_user.authority) + + +# Delete +@router.delete("/{student_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_user(student_id: str, current_user: models.User = Depends(get_current_user), db=Depends(get_db)): + db_user = user_crud.get_user(db, student_id=student_id) + + if current_user.id != student_id and current_user.authority != 0: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, + detail="삭제 권한이 없습니다.") + if not db_user: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, + detail="데이터를 찾을수 없습니다.") + + user_crud.delete_user(db=db, student_id=student_id) + + +# User Login +@router.post("/login", response_model=user.User.Token) +def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), + db: Session = Depends(get_db)): + + # check user and password + user_data = user_crud.get_user(db, form_data.username) + if not user_data or not pwd_context.verify(form_data.password, user_data.password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + + # make access token + data = { + "sub": user_data.stdId, + "exp": datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + } + access_token = jwt.encode(data, SECRET_KEY, algorithm=ALGORITHM) + + return { + "access_token": access_token, + "token_type": "bearer", + "student_id": user_data.stdId + } diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_main.http b/tests/test_main.http new file mode 100644 index 0000000..35b6fe3 --- /dev/null +++ b/tests/test_main.http @@ -0,0 +1,12 @@ +# Test your FastAPI endpoints + +GET http://127.0.0.1:8000/ +Accept: application/json + +### + +GET http://127.0.0.1:8000/hello/User +Accept: application/json + + +###