From 3c600681540cdd3160bb01220bd887acd14c39a0 Mon Sep 17 00:00:00 2001 From: Tuguldur Gantumur Date: Mon, 24 Jun 2024 12:23:13 +1000 Subject: [PATCH 1/2] Created Models, Schemas and User controller --- server/controller/user.py | 64 +++++++ server/database.py | 34 ++-- server/models.py | 118 +++++++++++++ server/models/account_verification_token.py | 10 ++ server/models/audio_file.py | 10 ++ server/models/box_score.py | 18 ++ server/models/models.py | 39 ----- server/models/password_reset_token.py | 10 ++ server/models/player.py | 12 ++ server/models/player_stat.py | 19 +++ server/models/team.py | 12 ++ server/models/team_stat.py | 22 +++ server/models/text_file.py | 12 ++ server/models/token.py | 11 ++ server/models/user.py | 14 ++ .../routes/controllers/databasecontroller.py | 0 server/schemas/generic.py | 7 + server/schemas/user.py | 15 ++ server/server.py | 161 +++++++++++++++++- server/services/user.py | 67 ++++++++ 20 files changed, 592 insertions(+), 63 deletions(-) create mode 100644 server/controller/user.py create mode 100644 server/models.py create mode 100644 server/models/account_verification_token.py create mode 100644 server/models/audio_file.py create mode 100644 server/models/box_score.py delete mode 100644 server/models/models.py create mode 100644 server/models/password_reset_token.py create mode 100644 server/models/player.py create mode 100644 server/models/player_stat.py create mode 100644 server/models/team.py create mode 100644 server/models/team_stat.py create mode 100644 server/models/text_file.py create mode 100644 server/models/token.py create mode 100644 server/models/user.py delete mode 100644 server/routes/controllers/databasecontroller.py create mode 100644 server/schemas/generic.py create mode 100644 server/schemas/user.py create mode 100644 server/services/user.py diff --git a/server/controller/user.py b/server/controller/user.py new file mode 100644 index 0000000..6fe6cb0 --- /dev/null +++ b/server/controller/user.py @@ -0,0 +1,64 @@ +import http +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from pydantic import BaseModel, EmailStr + +from database import get_db +from models.user import User as UserModel +from services.user_service import UserService +from schemas.user import UserCreate, UserResponse, TokenResponse +from schemas.generic import GenericResponseModel + +user_router = APIRouter( + prefix="/v1/users", + tags=["users"] +) + +@user_router.post( + "/register", + status_code=http.HTTPStatus.CREATED, + response_model=GenericResponseModel +) +async def register_user(user: UserCreate, db: Session = Depends(get_db)): + service = UserService(db) + try: + new_user = service.create_user(user) + return GenericResponseModel( + status_code=http.HTTPStatus.CREATED, + message="User successfully registered", + error="", + data={"user": new_user} + ) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + +@user_router.post( + "/login", + status_code=http.HTTPStatus.OK, + response_model=GenericResponseModel +) +async def login_user(email: EmailStr, password: str, db: Session = Depends(get_db)): + service = UserService(db) + try: + token = service.authenticate_user(email, password) + return GenericResponseModel( + status_code=http.HTTPStatus.OK, + message="User successfully logged in", + error="", + data={"token": token} + ) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + +@user_router.get( + "/me", + status_code=http.HTTPStatus.OK, + response_model=GenericResponseModel +) +async def get_user_details(current_user: UserModel = Depends(UserService.get_current_user)): + return GenericResponseModel( + status_code=http.HTTPStatus.OK, + message="User details retrieved successfully", + error="", + data={"user": current_user} + ) diff --git a/server/database.py b/server/database.py index 41f523c..139f579 100644 --- a/server/database.py +++ b/server/database.py @@ -27,29 +27,21 @@ print(f"Error creating connection pool: {e}") exit(1) -# # Use getconn() to get a connection from the connection pool -conn = postgreSQL_pool.getconn() +def get_db_connection(): + conn = postgreSQL_pool.getconn() + try: + yield conn + finally: + postgreSQL_pool.putconn(conn) -cursor = conn.cursor() +# # # Use getconn() to get a connection from the connection pool +# conn = postgreSQL_pool.getconn() -# Drop previous table of same name if one exists -cursor.execute("DROP TABLE IF EXISTS pharmacy;") -print("Finished dropping table (if existed)") +# cursor = conn.cursor() -# Create a table -cursor.execute("CREATE TABLE pharmacy (pharmacy_id integer, pharmacy_name text, city text, state text, zip_code integer);") -print("Finished creating table") -# Create a index -cursor.execute("CREATE INDEX idx_pharmacy_id ON pharmacy(pharmacy_id);") -print("Finished creating index") -# Insert some data into the table -cursor.execute("INSERT INTO pharmacy (pharmacy_id,pharmacy_name,city,state,zip_code) VALUES (%s, %s, %s, %s,%s);", (1,"Target","Sunnyvale","California",94001)) -cursor.execute("INSERT INTO pharmacy (pharmacy_id,pharmacy_name,city,state,zip_code) VALUES (%s, %s, %s, %s,%s);", (2,"CVS","San Francisco","California",94002)) -print("Inserted 2 rows of data") - -# Clean up -conn.commit() -cursor.close() -conn.close() \ No newline at end of file +# # Clean up +# conn.commit() +# cursor.close() +# conn.close() \ No newline at end of file diff --git a/server/models.py b/server/models.py new file mode 100644 index 0000000..b6887e0 --- /dev/null +++ b/server/models.py @@ -0,0 +1,118 @@ +from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean, Float, Text +from sqlalchemy.sql.functions import current_timestamp +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() + +class User(Base): + __tablename__ = 'users' + id = Column(Integer, primary_key=True, autoincrement=True) + username = Column(String(32), unique=True, nullable=False) + email = Column(String(256), unique=True, nullable=False) + passwordHash = Column(String(256), nullable=False) + updated_at = Column(DateTime, default=current_timestamp(), onupdate=current_timestamp()) + created_at = Column(DateTime, default=current_timestamp()) + +class Team(Base): + __tablename__ = 'teams' + id = Column(Integer, primary_key=True, autoincrement=True) + name = Column(String(32), unique=True, nullable=False) + updated_at = Column(DateTime, default=current_timestamp(), onupdate=current_timestamp()) + created_at = Column(DateTime, default=current_timestamp()) + +class TeamStat(Base): + __tablename__ = 'teamStats' + id = Column(Integer, primary_key=True, autoincrement=True) + teamId = Column(Integer, ForeignKey('teams.id'), nullable=False) + boxscoreId = Column(Integer, ForeignKey('boxscores.id'), nullable=False) + createdAt = Column(DateTime, default=current_timestamp()) + startAt = Column(DateTime, nullable=False) + endAt = Column(DateTime, nullable=False) + points = Column(Integer, nullable=False) + rebounds = Column(Integer, nullable=False) + assists = Column(Integer, nullable=False) + steal = Column(Integer, nullable=False) + block = Column(Integer, nullable=False) + turnover = Column(Integer, nullable=False) + fgPercent = Column(Float, nullable=False) + fgAttempt = Column(Integer, nullable=False) + fgMade = Column(Integer, nullable=False) + fouls = Column(Integer, nullable=False) + +class Boxscore(Base): + __tablename__ = 'boxscores' + id = Column(Integer, primary_key=True, autoincrement=True) + userId = Column(Integer, ForeignKey('users.id'), nullable=False) + homeTeamId = Column(Integer, ForeignKey('teams.id'), nullable=False) + awayTeamId = Column(Integer, ForeignKey('teams.id'), nullable=False) + textFileId = Column(Integer, ForeignKey('textFiles.id'), nullable=False) + score = Column(String, nullable=False) + homeScore = Column(Integer, nullable=False) + awayScore = Column(Integer, nullable=False) + startAt = Column(DateTime, nullable=False) + endAt = Column(DateTime, nullable=False) + updatedAt = Column(DateTime, default=current_timestamp(), onupdate=current_timestamp()) + createdAt = Column(DateTime, default=current_timestamp()) + +class Player(Base): + __tablename__ = 'players' + id = Column(Integer, primary_key=True, autoincrement=True) + userId = Column(Integer, ForeignKey('users.id'), nullable=False) + teamId = Column(Integer, ForeignKey('teams.id'), nullable=False) + updatedAt = Column(DateTime, default=current_timestamp(), onupdate=current_timestamp()) + createdAt = Column(DateTime, default=current_timestamp()) + matches = Column(String, nullable=True) + +class PlayerStat(Base): + __tablename__ = 'playerStats' + id = Column(Integer, primary_key=True, autoincrement=True) + playerId = Column(Integer, ForeignKey('players.id'), nullable=False) + boxscoreId = Column(Integer, ForeignKey('boxscores.id'), nullable=False) + points = Column(Integer, nullable=False) + rebounds = Column(Integer, nullable=False) + assists = Column(Integer, nullable=False) + steal = Column(Integer, nullable=False) + block = Column(Integer, nullable=False) + turnover = Column(Integer, nullable=False) + fgPercent = Column(Float, nullable=False) + fgAttempt = Column(Integer, nullable=False) + fgMade = Column(Integer, nullable=False) + fouls = Column(Integer, nullable=False) + +class Token(Base): + __tablename__ = 'tokens' + id = Column(Integer, primary_key=True, autoincrement=True) + userId = Column(Integer, ForeignKey('users.id'), nullable=False) + token = Column(String(256), unique=True, nullable=False) + expiresAt = Column(DateTime, nullable=False) + used = Column(Boolean, default=False) + +class PasswordResetToken(Base): + __tablename__ = 'passwordResetTokens' + id = Column(Integer, primary_key=True, autoincrement=True) + userId = Column(Integer, ForeignKey('users.id'), nullable=False) + token = Column(String(256), unique=True, nullable=False) + expiresAt = Column(DateTime, nullable=False) + +class AccountVerificationToken(Base): + __tablename__ = 'accountVerificationTokens' + id = Column(Integer, primary_key=True, autoincrement=True) + userId = Column(Integer, ForeignKey('users.id'), nullable=False) + token = Column(String(256), unique=True, nullable=False) + expiresAt = Column(DateTime, nullable=False) + +class AudioFile(Base): + __tablename__ = 'audioFiles' + id = Column(Integer, primary_key=True, autoincrement=True) + userId = Column(Integer, ForeignKey('users.id'), nullable=False) + filePath = Column(String, nullable=False) + createdAt = Column(DateTime, default=current_timestamp()) + +class TextFile(Base): + __tablename__ = 'textFiles' + id = Column(Integer, primary_key=True, autoincrement=True) + userId = Column(Integer, ForeignKey('users.id'), nullable=False) + audioId = Column(Integer, ForeignKey('audioFiles.id'), nullable=False) + contents = Column(Text, nullable=False) + createdAt = Column(DateTime, default=current_timestamp()) + filepath = Column(String, nullable=False) diff --git a/server/models/account_verification_token.py b/server/models/account_verification_token.py new file mode 100644 index 0000000..6095b0d --- /dev/null +++ b/server/models/account_verification_token.py @@ -0,0 +1,10 @@ +from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean, Float, Text +from sqlalchemy.sql.functions import current_timestamp +from sqlalchemy.ext.declarative import declarative_base + +class AccountVerificationToken(Base): + __tablename__ = 'accountVerificationTokens' + id = Column(Integer, primary_key=True, autoincrement=True) + userId = Column(Integer, ForeignKey('users.id'), nullable=False) + token = Column(String(256), unique=True, nullable=False) + expiresAt = Column(DateTime, nullable=False) \ No newline at end of file diff --git a/server/models/audio_file.py b/server/models/audio_file.py new file mode 100644 index 0000000..030367c --- /dev/null +++ b/server/models/audio_file.py @@ -0,0 +1,10 @@ +from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean, Float, Text +from sqlalchemy.sql.functions import current_timestamp +from sqlalchemy.ext.declarative import declarative_base + +class AudioFile(Base): + __tablename__ = 'audioFiles' + id = Column(Integer, primary_key=True, autoincrement=True) + userId = Column(Integer, ForeignKey('users.id'), nullable=False) + filePath = Column(String, nullable=False) + createdAt = Column(DateTime, default=current_timestamp()) \ No newline at end of file diff --git a/server/models/box_score.py b/server/models/box_score.py new file mode 100644 index 0000000..f54e90b --- /dev/null +++ b/server/models/box_score.py @@ -0,0 +1,18 @@ +from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean, Float, Text +from sqlalchemy.sql.functions import current_timestamp +from sqlalchemy.ext.declarative import declarative_base + +class Boxscore(Base): + __tablename__ = 'boxscores' + id = Column(Integer, primary_key=True, autoincrement=True) + userId = Column(Integer, ForeignKey('users.id'), nullable=False) + homeTeamId = Column(Integer, ForeignKey('teams.id'), nullable=False) + awayTeamId = Column(Integer, ForeignKey('teams.id'), nullable=False) + textFileId = Column(Integer, ForeignKey('textFiles.id'), nullable=False) + score = Column(String, nullable=False) + homeScore = Column(Integer, nullable=False) + awayScore = Column(Integer, nullable=False) + startAt = Column(DateTime, nullable=False) + endAt = Column(DateTime, nullable=False) + updatedAt = Column(DateTime, default=current_timestamp(), onupdate=current_timestamp()) + createdAt = Column(DateTime, default=current_timestamp()) \ No newline at end of file diff --git a/server/models/models.py b/server/models/models.py deleted file mode 100644 index e8b108a..0000000 --- a/server/models/models.py +++ /dev/null @@ -1,39 +0,0 @@ -# from pydantic import BaseModel -# from typing import Optional -# EXAMPLE -from enum import StrEnum - -from sqlalchemy import Column -from sqlalchemy import Integer -from sqlalchemy import String -from sqlalchemy import DateTime -from sqlalchemy import ForeignKey -from sqlalchemy.sql.functions import current_timestamp - -from sqlalchemy.ext.declarative import declarative_base - - -Base = declarative_base() - -class User(Base): - __tablename__ = 'users' - id = Column("id", Integer(), primary_key=True, autoincrement=True) - name = Column("name", String(32)) - role = Column("role", String(32)) - email = Column("email", String(256), unique=True) - password = Column("password", String(256)) - updated_at = Column("updated_at", DateTime(), default=current_timestamp()) - created_at = Column("created_at", DateTime(), default=current_timestamp()) - - class Role(StrEnum): - ADMIN = "admin" - USER = "user" - GUEST = "guest" - -class Token(Base): - __tablename__ = 'tokens' - id = Column("id", Integer(), primary_key=True, autoincrement=True) - user_id = Column("user_id", Integer(), ForeignKey('users.id')) - hash = Column("hash", String(256), unique=True) - expired_at = Column("expired_at", DateTime()) - created_at = Column("created_at", DateTime(), default=current_timestamp()) \ No newline at end of file diff --git a/server/models/password_reset_token.py b/server/models/password_reset_token.py new file mode 100644 index 0000000..1bfc9fd --- /dev/null +++ b/server/models/password_reset_token.py @@ -0,0 +1,10 @@ +from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean, Float, Text +from sqlalchemy.sql.functions import current_timestamp +from sqlalchemy.ext.declarative import declarative_base + +class PasswordResetToken(Base): + __tablename__ = 'passwordResetTokens' + id = Column(Integer, primary_key=True, autoincrement=True) + userId = Column(Integer, ForeignKey('users.id'), nullable=False) + token = Column(String(256), unique=True, nullable=False) + expiresAt = Column(DateTime, nullable=False) \ No newline at end of file diff --git a/server/models/player.py b/server/models/player.py new file mode 100644 index 0000000..a97180e --- /dev/null +++ b/server/models/player.py @@ -0,0 +1,12 @@ +from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean, Float, Text +from sqlalchemy.sql.functions import current_timestamp +from sqlalchemy.ext.declarative import declarative_base + +class Player(Base): + __tablename__ = 'players' + id = Column(Integer, primary_key=True, autoincrement=True) + userId = Column(Integer, ForeignKey('users.id'), nullable=False) + teamId = Column(Integer, ForeignKey('teams.id'), nullable=False) + updatedAt = Column(DateTime, default=current_timestamp(), onupdate=current_timestamp()) + createdAt = Column(DateTime, default=current_timestamp()) + matches = Column(String, nullable=True) \ No newline at end of file diff --git a/server/models/player_stat.py b/server/models/player_stat.py new file mode 100644 index 0000000..17647f4 --- /dev/null +++ b/server/models/player_stat.py @@ -0,0 +1,19 @@ +from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean, Float, Text +from sqlalchemy.sql.functions import current_timestamp +from sqlalchemy.ext.declarative import declarative_base + +class PlayerStat(Base): + __tablename__ = 'playerStats' + id = Column(Integer, primary_key=True, autoincrement=True) + playerId = Column(Integer, ForeignKey('players.id'), nullable=False) + boxscoreId = Column(Integer, ForeignKey('boxscores.id'), nullable=False) + points = Column(Integer, nullable=False) + rebounds = Column(Integer, nullable=False) + assists = Column(Integer, nullable=False) + steal = Column(Integer, nullable=False) + block = Column(Integer, nullable=False) + turnover = Column(Integer, nullable=False) + fgPercent = Column(Float, nullable=False) + fgAttempt = Column(Integer, nullable=False) + fgMade = Column(Integer, nullable=False) + fouls = Column(Integer, nullable=False) \ No newline at end of file diff --git a/server/models/team.py b/server/models/team.py new file mode 100644 index 0000000..f1b079e --- /dev/null +++ b/server/models/team.py @@ -0,0 +1,12 @@ +from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean, Float, Text +from sqlalchemy.sql.functions import current_timestamp +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() + +class Team(Base): + __tablename__ = 'teams' + id = Column(Integer, primary_key=True, autoincrement=True) + name = Column(String(32), unique=True, nullable=False) + updated_at = Column(DateTime, default=current_timestamp(), onupdate=current_timestamp()) + created_at = Column(DateTime, default=current_timestamp()) \ No newline at end of file diff --git a/server/models/team_stat.py b/server/models/team_stat.py new file mode 100644 index 0000000..7af4146 --- /dev/null +++ b/server/models/team_stat.py @@ -0,0 +1,22 @@ +from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean, Float, Text +from sqlalchemy.sql.functions import current_timestamp +from sqlalchemy.ext.declarative import declarative_base + +class TeamStat(Base): + __tablename__ = 'teamStats' + id = Column(Integer, primary_key=True, autoincrement=True) + teamId = Column(Integer, ForeignKey('teams.id'), nullable=False) + boxscoreId = Column(Integer, ForeignKey('boxscores.id'), nullable=False) + createdAt = Column(DateTime, default=current_timestamp()) + startAt = Column(DateTime, nullable=False) + endAt = Column(DateTime, nullable=False) + points = Column(Integer, nullable=False) + rebounds = Column(Integer, nullable=False) + assists = Column(Integer, nullable=False) + steal = Column(Integer, nullable=False) + block = Column(Integer, nullable=False) + turnover = Column(Integer, nullable=False) + fgPercent = Column(Float, nullable=False) + fgAttempt = Column(Integer, nullable=False) + fgMade = Column(Integer, nullable=False) + fouls = Column(Integer, nullable=False) \ No newline at end of file diff --git a/server/models/text_file.py b/server/models/text_file.py new file mode 100644 index 0000000..040957d --- /dev/null +++ b/server/models/text_file.py @@ -0,0 +1,12 @@ +from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean, Float, Text +from sqlalchemy.sql.functions import current_timestamp +from sqlalchemy.ext.declarative import declarative_base + +class TextFile(Base): + __tablename__ = 'textFiles' + id = Column(Integer, primary_key=True, autoincrement=True) + userId = Column(Integer, ForeignKey('users.id'), nullable=False) + audioId = Column(Integer, ForeignKey('audioFiles.id'), nullable=False) + contents = Column(Text, nullable=False) + createdAt = Column(DateTime, default=current_timestamp()) + filepath = Column(String, nullable=False) \ No newline at end of file diff --git a/server/models/token.py b/server/models/token.py new file mode 100644 index 0000000..62e645a --- /dev/null +++ b/server/models/token.py @@ -0,0 +1,11 @@ +from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean, Float, Text +from sqlalchemy.sql.functions import current_timestamp +from sqlalchemy.ext.declarative import declarative_base + +class Token(Base): + __tablename__ = 'tokens' + id = Column(Integer, primary_key=True, autoincrement=True) + userId = Column(Integer, ForeignKey('users.id'), nullable=False) + token = Column(String(256), unique=True, nullable=False) + expiresAt = Column(DateTime, nullable=False) + used = Column(Boolean, default=False) \ No newline at end of file diff --git a/server/models/user.py b/server/models/user.py new file mode 100644 index 0000000..2e7c64d --- /dev/null +++ b/server/models/user.py @@ -0,0 +1,14 @@ +from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean, Float, Text +from sqlalchemy.sql.functions import current_timestamp +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() + +class User(Base): + __tablename__ = 'users' + id = Column(Integer, primary_key=True, autoincrement=True) + username = Column(String(32), unique=True, nullable=False) + email = Column(String(256), unique=True, nullable=False) + passwordHash = Column(String(256), nullable=False) + updated_at = Column(DateTime, default=current_timestamp(), onupdate=current_timestamp()) + created_at = Column(DateTime, default=current_timestamp()) \ No newline at end of file diff --git a/server/routes/controllers/databasecontroller.py b/server/routes/controllers/databasecontroller.py deleted file mode 100644 index e69de29..0000000 diff --git a/server/schemas/generic.py b/server/schemas/generic.py new file mode 100644 index 0000000..5e95a60 --- /dev/null +++ b/server/schemas/generic.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel, EmailStr + +class GenericResponseModel(BaseModel): + status_code: int + message: str + error: str + data: dict = None \ No newline at end of file diff --git a/server/schemas/user.py b/server/schemas/user.py new file mode 100644 index 0000000..8852d5b --- /dev/null +++ b/server/schemas/user.py @@ -0,0 +1,15 @@ +from pydantic import BaseModel, EmailStr + +class UserCreate(BaseModel): + username: str + email: EmailStr + password: str + +class UserResponse(BaseModel): + id: int + username: str + email: EmailStr + +class TokenResponse(BaseModel): + access_token: str + token_type: str diff --git a/server/server.py b/server/server.py index 9b9160c..52e9b9e 100644 --- a/server/server.py +++ b/server/server.py @@ -1,7 +1,162 @@ -from fastapi import FastAPI +from fastapi import FastAPI, Depends, HTTPException, status, File, UploadFile +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from pydantic import BaseModel +from typing import List +from passlib.context import CryptContext +from sqlalchemy.orm import sessionmaker, Session +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy import create_engine, Column, Integer, String, DateTime, ForeignKey +from sqlalchemy.sql.functions import current_timestamp +from enum import Enum +import os +from dotenv import load_dotenv +import shutil +from datetime import datetime, timedelta +import jwt + +# Import database connection and models +from database import get_db_connection +from models import User, Token +# pip install fastapi uvicorn psycopg2-binary passlib python-dotenv jwt +load_dotenv() app = FastAPI() +# Password hashing configuration +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +# OAuth2 configuration +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +# Pydantic models +class UserCreate(BaseModel): + name: str + email: str + password: str + +class UserResponse(BaseModel): + id: int + name: str + email: str + + +SECRET_KEY = os.getenv('SECRET_KEY') +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + +# Database models using SQLAlchemy ORM +Base = declarative_base() + +class UserRole(str, Enum): + ADMIN = "admin" + USER = "user" + GUEST = "guest" + +# Dependency +def get_db(): + conn = next(get_db_connection()) + return conn + +def get_user_by_email(db, email: str): + cursor = db.cursor() + cursor.execute("SELECT * FROM users WHERE email = %s", (email,)) + user = cursor.fetchone() + cursor.close() + return user + +def create_user(db, user: UserCreate): + hashed_password = pwd_context.hash(user.password) + cursor = db.cursor() + cursor.execute( + "INSERT INTO users (name, email, password, created_at, updated_at) VALUES (%s, %s, %s, %s, %s) RETURNING id, name, email", + (user.name, user.email, hashed_password, datetime.utcnow(), datetime.utcnow()) + ) + new_user = cursor.fetchone() + db.commit() + cursor.close() + return new_user + +def authenticate_user(db, email: str, password: str): + user = get_user_by_email(db, email) + if not user or not pwd_context.verify(password, user[4]): + return False + return user + +def create_access_token(data: dict, expires_delta: timedelta = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + +def get_current_user(db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)): + 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]) + email: str = payload.get("sub") + if email is None: + raise credentials_exception + except jwt.PyJWTError: + raise credentials_exception + user = get_user_by_email(db, email) + if user is None: + raise credentials_exception + return user + +# Routes @app.get("/") -def index(): - return "Hello World" +def example(): + return "Hello World DDD" + +@app.post("/register", response_model=UserResponse) +def register(user: UserCreate, db: Session = Depends(get_db)): + db_user = get_user_by_email(db, user.email) + if db_user: + raise HTTPException(status_code=400, detail="Email already registered") + new_user = create_user(db, user) + return {"id": new_user[0], "name": new_user[1], "email": new_user[2]} + +@app.post("/token") +def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): + user = authenticate_user(db, form_data.username, form_data.password) + if not user: + raise HTTPException(status_code=400, detail="Incorrect email or password") + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token(data={"sub": user[3]}, expires_delta=access_token_expires) + return {"access_token": access_token, "token_type": "bearer"} + +# @app.post("/upload", response_model=FileUploadResponse) +# def upload_file(file: UploadFile = File(...), current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): +# user_directory = f"files/{current_user[3]}" +# os.makedirs(user_directory, exist_ok=True) +# file_path = os.path.join(user_directory, file.filename) +# with open(file_path, "wb") as buffer: +# shutil.copyfileobj(file.file, buffer) +# cursor = db.cursor() +# cursor.execute( +# "INSERT INTO files (filename, user_id) VALUES (%s, %s) RETURNING id, filename", +# (file.filename, current_user[0]) +# ) +# new_file = cursor.fetchone() +# db.commit() +# cursor.close() +# return {"filename": new_file[1]} + +# @app.get("/myfiles", response_model=List[FileUploadResponse]) +# def list_files(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): +# cursor = db.cursor() +# cursor.execute("SELECT filename FROM files WHERE user_id = %s", (current_user[0],)) +# files = cursor.fetchall() +# cursor.close() +# return [{"filename": file[0]} for file in files] + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="127.0.0.1", port=8000, log_level="info") diff --git a/server/services/user.py b/server/services/user.py new file mode 100644 index 0000000..4946ce5 --- /dev/null +++ b/server/services/user.py @@ -0,0 +1,67 @@ +from sqlalchemy.orm import Session +from passlib.context import CryptContext +from datetime import datetime, timedelta +import jwt +from models import User as UserModel +from schemas.user import UserCreate, UserResponse +from fastapi import Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer + +SECRET_KEY = "your_secret_key" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/v1/users/login") + +class UserService: + def __init__(self, db: Session): + self.db = db + + def create_user(self, user: UserCreate): + hashed_password = pwd_context.hash(user.password) + db_user = UserModel( + username=user.username, + email=user.email, + passwordHash=hashed_password + ) + self.db.add(db_user) + self.db.commit() + self.db.refresh(db_user) + return db_user + + def authenticate_user(self, email: str, password: str): + user = self.db.query(UserModel).filter(UserModel.email == email).first() + if not user or not pwd_context.verify(password, user.passwordHash): + raise HTTPException(status_code=400, detail="Incorrect email or password") + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = self.create_access_token(data={"sub": user.email}, expires_delta=access_token_expires) + return {"access_token": access_token, "token_type": "bearer"} + + def create_access_token(self, data: dict, expires_delta: timedelta = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + @staticmethod + def get_current_user(db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)): + 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]) + email: str = payload.get("sub") + if email is None: + raise credentials_exception + except jwt.PyJWTError: + raise credentials_exception + user = db.query(UserModel).filter(UserModel.email == email).first() + if user is None: + raise credentials_exception + return user From 1929bcdff13c61ab814b0634706de21817064110 Mon Sep 17 00:00:00 2001 From: Tuguldur Gantumur Date: Mon, 24 Jun 2024 15:27:49 +1000 Subject: [PATCH 2/2] Added user, transcript service and controller --- server/controller/transcript.py | 64 +++++++++ server/models.py | 1 + server/models/account_verification_token.py | 3 +- server/models/user.py | 3 +- server/server.py | 130 +++++++++---------- server/services/transcript_service.py | 35 +++++ server/services/{user.py => user_service.py} | 6 +- 7 files changed, 167 insertions(+), 75 deletions(-) create mode 100644 server/controller/transcript.py create mode 100644 server/services/transcript_service.py rename server/services/{user.py => user_service.py} (96%) diff --git a/server/controller/transcript.py b/server/controller/transcript.py new file mode 100644 index 0000000..d872e64 --- /dev/null +++ b/server/controller/transcript.py @@ -0,0 +1,64 @@ +import http + +from fastapi import APIRouter +from botocore.exceptions import ClientError + +from src.utils.settings import AWS_BUCKET_NAME +from src.schemas.base import GenericResponseModel +from src.schemas.transcription import ( + TranscribeAudioRequestSchema, + PollTranscriptionRequestSchema +) +from src.services.transcription import TranscriptionService + +transcription_router = APIRouter( + prefix="/v1/transcription", + tags=[TranscriptionRouterTags.transcribe] +) + +# Define the path operation decorator +@transcription_router.post( + "/create", + status_code=http.HTTPStatus.CREATED, + response_model=GenericResponseModel +) + +# Define an async function to create the transcribed audio with help from Service layer +async def transcribe_audio(req: TranscribeAudioRequestSchema): + transcribe_client = AWSTranscribeClient().get_client() + + service = TranscriptionService() + + filename, file_format = req.s3_filename.split(".") + job_name = req.job_name + language_code = req.language_code + + file_uri = service.generate_file_uri( + bucket_name=AWS_BUCKET_NAME, + filename=filename, + extension=file_format + ) + + try: + response = await service.transcribe_file( + transcribe_client=transcribe_client, + job_name=job_name, + file_uri=file_uri, + file_format=file_format, + language_code=language_code + ) + + return GenericResponseModel( + status_code=http.HTTPStatus.CREATED, + message="Successfully created audio transcription", + error="", + data=response, + ) + except TimeoutError: + return GenericResponseModel( + ... + ) + except ClientError: + return GenericResponseModel( + ... + ) \ No newline at end of file diff --git a/server/models.py b/server/models.py index b6887e0..764ccd2 100644 --- a/server/models.py +++ b/server/models.py @@ -12,6 +12,7 @@ class User(Base): passwordHash = Column(String(256), nullable=False) updated_at = Column(DateTime, default=current_timestamp(), onupdate=current_timestamp()) created_at = Column(DateTime, default=current_timestamp()) + verified = Column(Boolean, default=False) class Team(Base): __tablename__ = 'teams' diff --git a/server/models/account_verification_token.py b/server/models/account_verification_token.py index 6095b0d..fee9081 100644 --- a/server/models/account_verification_token.py +++ b/server/models/account_verification_token.py @@ -7,4 +7,5 @@ class AccountVerificationToken(Base): id = Column(Integer, primary_key=True, autoincrement=True) userId = Column(Integer, ForeignKey('users.id'), nullable=False) token = Column(String(256), unique=True, nullable=False) - expiresAt = Column(DateTime, nullable=False) \ No newline at end of file + expiresAt = Column(DateTime, nullable=False) + createdAt = Column(DateTime, default=current_timestamp()) \ No newline at end of file diff --git a/server/models/user.py b/server/models/user.py index 2e7c64d..c6fd52d 100644 --- a/server/models/user.py +++ b/server/models/user.py @@ -11,4 +11,5 @@ class User(Base): email = Column(String(256), unique=True, nullable=False) passwordHash = Column(String(256), nullable=False) updated_at = Column(DateTime, default=current_timestamp(), onupdate=current_timestamp()) - created_at = Column(DateTime, default=current_timestamp()) \ No newline at end of file + created_at = Column(DateTime, default=current_timestamp()) + verified = Column(Boolean, default=False) \ No newline at end of file diff --git a/server/server.py b/server/server.py index 52e9b9e..aced68c 100644 --- a/server/server.py +++ b/server/server.py @@ -21,6 +21,8 @@ load_dotenv() app = FastAPI() +app.include_router(user_router) +app.include_router(transcript_router) # Password hashing configuration pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") @@ -28,18 +30,6 @@ # OAuth2 configuration oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") -# Pydantic models -class UserCreate(BaseModel): - name: str - email: str - password: str - -class UserResponse(BaseModel): - id: int - name: str - email: str - - SECRET_KEY = os.getenv('SECRET_KEY') ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 30 @@ -47,68 +37,64 @@ class UserResponse(BaseModel): # Database models using SQLAlchemy ORM Base = declarative_base() -class UserRole(str, Enum): - ADMIN = "admin" - USER = "user" - GUEST = "guest" # Dependency -def get_db(): - conn = next(get_db_connection()) - return conn - -def get_user_by_email(db, email: str): - cursor = db.cursor() - cursor.execute("SELECT * FROM users WHERE email = %s", (email,)) - user = cursor.fetchone() - cursor.close() - return user - -def create_user(db, user: UserCreate): - hashed_password = pwd_context.hash(user.password) - cursor = db.cursor() - cursor.execute( - "INSERT INTO users (name, email, password, created_at, updated_at) VALUES (%s, %s, %s, %s, %s) RETURNING id, name, email", - (user.name, user.email, hashed_password, datetime.utcnow(), datetime.utcnow()) - ) - new_user = cursor.fetchone() - db.commit() - cursor.close() - return new_user - -def authenticate_user(db, email: str, password: str): - user = get_user_by_email(db, email) - if not user or not pwd_context.verify(password, user[4]): - return False - return user - -def create_access_token(data: dict, expires_delta: timedelta = None): - to_encode = data.copy() - if expires_delta: - expire = datetime.utcnow() + expires_delta - else: - expire = datetime.utcnow() + timedelta(minutes=15) - to_encode.update({"exp": expire}) - encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) - return encoded_jwt - -def get_current_user(db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)): - 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]) - email: str = payload.get("sub") - if email is None: - raise credentials_exception - except jwt.PyJWTError: - raise credentials_exception - user = get_user_by_email(db, email) - if user is None: - raise credentials_exception - return user +# def get_db(): +# conn = next(get_db_connection()) +# return conn + +# def get_user_by_email(db, email: str): +# cursor = db.cursor() +# cursor.execute("SELECT * FROM users WHERE email = %s", (email,)) +# user = cursor.fetchone() +# cursor.close() +# return user + +# def create_user(db, user: UserCreate): +# hashed_password = pwd_context.hash(user.password) +# cursor = db.cursor() +# cursor.execute( +# "INSERT INTO users (name, email, password, created_at, updated_at) VALUES (%s, %s, %s, %s, %s) RETURNING id, name, email", +# (user.name, user.email, hashed_password, datetime.utcnow(), datetime.utcnow()) +# ) +# new_user = cursor.fetchone() +# db.commit() +# cursor.close() +# return new_user + +# def authenticate_user(db, email: str, password: str): +# user = get_user_by_email(db, email) +# if not user or not pwd_context.verify(password, user[4]): +# return False +# return user + +# def create_access_token(data: dict, expires_delta: timedelta = None): +# to_encode = data.copy() +# if expires_delta: +# expire = datetime.utcnow() + expires_delta +# else: +# expire = datetime.utcnow() + timedelta(minutes=15) +# to_encode.update({"exp": expire}) +# encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) +# return encoded_jwt + +# def get_current_user(db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)): +# 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]) +# email: str = payload.get("sub") +# if email is None: +# raise credentials_exception +# except jwt.PyJWTError: +# raise credentials_exception +# user = get_user_by_email(db, email) +# if user is None: +# raise credentials_exception +# return user # Routes @app.get("/") diff --git a/server/services/transcript_service.py b/server/services/transcript_service.py new file mode 100644 index 0000000..462c446 --- /dev/null +++ b/server/services/transcript_service.py @@ -0,0 +1,35 @@ +from fastapi import FastAPI +from src.controllers import transcription + +# Create Transcription Service +class TranscriptionService: + async def transcribe_file(self, transcribe_client: any, + job_name: str, file_uri: str, + file_format: str, language_code = "id-ID"): + try: + transcribe_client.start_transcription_job( + TranscriptionJobName=job_name, + Media={ + "MediaFileUri": file_uri + }, + MediaFormat=file_format, + LanguageCode=language_code, + ) + + job_result = await self.poll_transcription_job( + transcribe_client=transcribe_client, + job_name=job_name + ) + + # Store to database + await self.store_transcription(item=job_result) + ... + + return job_result + + # return res + except TimeoutError: + return TimeoutError("Timeout when polling the transcription results") + except ClientError: + return ClientError("Transcription Job failed.", operation_name="start_transcription_job") + \ No newline at end of file diff --git a/server/services/user.py b/server/services/user_service.py similarity index 96% rename from server/services/user.py rename to server/services/user_service.py index 4946ce5..78bbbec 100644 --- a/server/services/user.py +++ b/server/services/user_service.py @@ -6,8 +6,12 @@ from schemas.user import UserCreate, UserResponse from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer +import os +from dotenv import load_dotenv -SECRET_KEY = "your_secret_key" +load_dotenv() + +SECRET_KEY = os.getenv('SECRET_KEY') ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 30 pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")