chore: basic backend setup
Browse files- Backend/{src β app}/__init__.py +0 -0
- Backend/{src/db β app/api}/__init__.py +0 -0
- Backend/app/api/deps.py +50 -0
- Backend/app/api/v1/__init__.py +3 -0
- Backend/app/api/v1/api.py +18 -0
- Backend/app/api/v1/endpoints/__init__.py +0 -0
- Backend/app/api/v1/endpoints/auth.py +66 -0
- Backend/app/api/v1/endpoints/students.py +178 -0
- Backend/{src β app}/config.py +0 -0
- Backend/app/core/__init__.py +3 -0
- Backend/app/core/security.py +24 -0
- Backend/{src/db/client.py β app/database.py} +1 -1
- Backend/app/main.py +53 -0
- Backend/app/models/__init__.py +4 -0
- Backend/app/models/tables.py +21 -0
- Backend/app/schema/__init__.py +3 -0
- Backend/app/schema/models.py +43 -0
- Backend/run.py +11 -0
- Backend/src/db/models.py +0 -5
- Backend/src/main.py +0 -31
- Frontend/package-lock.json +1 -0
- Frontend/package.json +6 -1
Backend/{src β app}/__init__.py
RENAMED
|
File without changes
|
Backend/{src/db β app/api}/__init__.py
RENAMED
|
File without changes
|
Backend/app/api/deps.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import Depends, HTTPException, status
|
| 2 |
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
| 3 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
| 4 |
+
from sqlalchemy import select
|
| 5 |
+
from jose import JWTError, jwt
|
| 6 |
+
from app.database import async_session_maker
|
| 7 |
+
from app.models import User
|
| 8 |
+
from app.config import settings
|
| 9 |
+
|
| 10 |
+
security = HTTPBearer()
|
| 11 |
+
|
| 12 |
+
async def get_db():
|
| 13 |
+
async with async_session_maker() as session:
|
| 14 |
+
try:
|
| 15 |
+
yield session
|
| 16 |
+
await session.commit()
|
| 17 |
+
except Exception:
|
| 18 |
+
await session.rollback()
|
| 19 |
+
raise
|
| 20 |
+
finally:
|
| 21 |
+
await session.close()
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
async def get_current_user(
|
| 25 |
+
credentials: HTTPAuthorizationCredentials = Depends(security),
|
| 26 |
+
db: AsyncSession = Depends(get_db)
|
| 27 |
+
) -> User:
|
| 28 |
+
credentials_exception = HTTPException(
|
| 29 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 30 |
+
detail="could not validate credentials",
|
| 31 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
try:
|
| 35 |
+
token = credentials.credentials
|
| 36 |
+
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
| 37 |
+
username: str = payload.get("sub")
|
| 38 |
+
if username is None:
|
| 39 |
+
raise credentials_exception
|
| 40 |
+
except JWTError:
|
| 41 |
+
raise credentials_exception
|
| 42 |
+
|
| 43 |
+
result = await db.execute(select(User).filter(User.username == username))
|
| 44 |
+
user = result.scalar_one_or_none()
|
| 45 |
+
|
| 46 |
+
if user is None:
|
| 47 |
+
raise credentials_exception
|
| 48 |
+
|
| 49 |
+
return user
|
| 50 |
+
|
Backend/app/api/v1/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.api.v1.api import api_router
|
| 2 |
+
|
| 3 |
+
__all__ = ["api_router"]
|
Backend/app/api/v1/api.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter
|
| 2 |
+
from app.api.v1.endpoints import auth, students
|
| 3 |
+
|
| 4 |
+
api_router = APIRouter()
|
| 5 |
+
|
| 6 |
+
# Include authentication routes
|
| 7 |
+
api_router.include_router(
|
| 8 |
+
auth.router,
|
| 9 |
+
prefix="/auth",
|
| 10 |
+
tags=["Authentication"]
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
# Include student routes
|
| 14 |
+
api_router.include_router(
|
| 15 |
+
students.router,
|
| 16 |
+
prefix="/students",
|
| 17 |
+
tags=["Students"]
|
| 18 |
+
)
|
Backend/app/api/v1/endpoints/__init__.py
ADDED
|
File without changes
|
Backend/app/api/v1/endpoints/auth.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, HTTPException, Depends, status
|
| 2 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
| 3 |
+
from sqlalchemy import select
|
| 4 |
+
from datetime import timedelta
|
| 5 |
+
from app.schema import UserCreate, Token
|
| 6 |
+
from app.models import User
|
| 7 |
+
from app.core import verify_password, get_password_hash, create_access_token
|
| 8 |
+
from app.api.deps import get_db
|
| 9 |
+
from app.config import settings
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
router = APIRouter()
|
| 13 |
+
|
| 14 |
+
@router.post("/register", response_model=dict)
|
| 15 |
+
async def register(user: UserCreate, db: AsyncSession = Depends(get_db)):
|
| 16 |
+
try:
|
| 17 |
+
result = await db.execute(select(User).filter(User.username == user.username))
|
| 18 |
+
existing_user = result.scalar_one_or_none()
|
| 19 |
+
|
| 20 |
+
if existing_user:
|
| 21 |
+
raise HTTPException(
|
| 22 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 23 |
+
detail='Username already registered'
|
| 24 |
+
)
|
| 25 |
+
new_user = User(
|
| 26 |
+
username=user.username,
|
| 27 |
+
hashed_password=get_password_hash(user.password)
|
| 28 |
+
)
|
| 29 |
+
db.add(new_user)
|
| 30 |
+
await db.commit()
|
| 31 |
+
|
| 32 |
+
return {"message": "User registered sucessfully", "username": user.username}
|
| 33 |
+
except HTTPException:
|
| 34 |
+
raise
|
| 35 |
+
except Exception as e:
|
| 36 |
+
raise HTTPException(
|
| 37 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 38 |
+
detail=f'registered failed: {str(e)}'
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
@router.post("/login", response_model=Token)
|
| 42 |
+
async def login(username: str, password: str, db: AsyncSession = Depends(get_db)):
|
| 43 |
+
try:
|
| 44 |
+
result = await db.execute(select(User).filter(User.username == username))
|
| 45 |
+
user = result.scalar_one_or_none()
|
| 46 |
+
|
| 47 |
+
if not user or not verify_password(password, user.hashed_password):
|
| 48 |
+
raise HTTPException(
|
| 49 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 50 |
+
detail="Incorrect username or password",
|
| 51 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 52 |
+
)
|
| 53 |
+
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
| 54 |
+
access_token = create_access_token(
|
| 55 |
+
data={'sub':user.username},
|
| 56 |
+
expires_deltas=access_token_expires
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
return {"access_token":access_token, "token_type":"bearer"}
|
| 60 |
+
except HTTPException:
|
| 61 |
+
raise
|
| 62 |
+
except Exception as e:
|
| 63 |
+
raise HTTPException(
|
| 64 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 65 |
+
detail=f"Login failed: {str(e)}"
|
| 66 |
+
)
|
Backend/app/api/v1/endpoints/students.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, HTTPException, Depends, status
|
| 2 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
| 3 |
+
from sqlalchemy import select
|
| 4 |
+
from typing import List
|
| 5 |
+
from app.schema import StudentCreate, StudentUpdate, StudentResponse
|
| 6 |
+
from app.models import Student, User
|
| 7 |
+
from app.api.deps import get_db, get_current_user
|
| 8 |
+
|
| 9 |
+
router = APIRouter()
|
| 10 |
+
|
| 11 |
+
@router.post("/", response_model=StudentResponse, status_code=status.HTTP_201_CREATED)
|
| 12 |
+
async def create_student(
|
| 13 |
+
student: StudentCreate,
|
| 14 |
+
db:AsyncSession = Depends(get_db),
|
| 15 |
+
current_user: User = Depends(get_current_user)
|
| 16 |
+
):
|
| 17 |
+
try:
|
| 18 |
+
result = await db.execute(select(Student).filter(Student.email == student.email))
|
| 19 |
+
existing_user = result.scalar_one_or_none()
|
| 20 |
+
|
| 21 |
+
if existing_user:
|
| 22 |
+
raise HTTPException(
|
| 23 |
+
status_code= status.HTTP_400_BAD_REQUEST,
|
| 24 |
+
detail=f'student with email {student.email} already exists'
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
new_student = Student(**student.model_dump())
|
| 28 |
+
db.add(new_student)
|
| 29 |
+
await db.commit()
|
| 30 |
+
await db.refresh(new_student)
|
| 31 |
+
|
| 32 |
+
return new_student
|
| 33 |
+
except HTTPException:
|
| 34 |
+
raise
|
| 35 |
+
except Exception as e :
|
| 36 |
+
raise HTTPException(
|
| 37 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 38 |
+
detail=f'failed to create student: {str(e)}'
|
| 39 |
+
)
|
| 40 |
+
@router.get("/", response_model=List[StudentResponse])
|
| 41 |
+
async def get_all_students(
|
| 42 |
+
skip: int = 0,
|
| 43 |
+
limit: int = 100,
|
| 44 |
+
db: AsyncSession = Depends(get_db),
|
| 45 |
+
current_user: User = Depends(get_current_user)
|
| 46 |
+
):
|
| 47 |
+
"""Get all students with pagination (Protected)"""
|
| 48 |
+
try:
|
| 49 |
+
result = await db.execute(
|
| 50 |
+
select(Student).offset(skip).limit(limit)
|
| 51 |
+
)
|
| 52 |
+
students = result.scalars().all()
|
| 53 |
+
return students
|
| 54 |
+
except Exception as e:
|
| 55 |
+
raise HTTPException(
|
| 56 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 57 |
+
detail=f"Failed to fetch students: {str(e)}"
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
@router.get("/{student_id}", response_model=StudentResponse)
|
| 62 |
+
async def get_student(
|
| 63 |
+
student_id: int,
|
| 64 |
+
db: AsyncSession = Depends(get_db),
|
| 65 |
+
current_user: User = Depends(get_current_user)
|
| 66 |
+
):
|
| 67 |
+
"""Get a specific student by ID (Protected)"""
|
| 68 |
+
try:
|
| 69 |
+
result = await db.execute(select(Student).filter(Student.id == student_id))
|
| 70 |
+
student = result.scalar_one_or_none()
|
| 71 |
+
|
| 72 |
+
if not student:
|
| 73 |
+
raise HTTPException(
|
| 74 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 75 |
+
detail=f"Student with ID {student_id} not found"
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
return student
|
| 79 |
+
except HTTPException:
|
| 80 |
+
raise
|
| 81 |
+
except Exception as e:
|
| 82 |
+
raise HTTPException(
|
| 83 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 84 |
+
detail=f"Failed to fetch student: {str(e)}"
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
@router.put("/{student_id}", response_model=StudentResponse)
|
| 89 |
+
async def update_student(
|
| 90 |
+
student_id: int,
|
| 91 |
+
student_update: StudentUpdate,
|
| 92 |
+
db: AsyncSession = Depends(get_db),
|
| 93 |
+
current_user: User = Depends(get_current_user)
|
| 94 |
+
):
|
| 95 |
+
"""Update a student's information (Protected)"""
|
| 96 |
+
try:
|
| 97 |
+
result = await db.execute(select(Student).filter(Student.id == student_id))
|
| 98 |
+
student = result.scalar_one_or_none()
|
| 99 |
+
|
| 100 |
+
if not student:
|
| 101 |
+
raise HTTPException(
|
| 102 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 103 |
+
detail=f"Student with ID {student_id} not found"
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
# Update only provided fields
|
| 107 |
+
update_data = student_update.model_dump(exclude_unset=True)
|
| 108 |
+
|
| 109 |
+
# Check email uniqueness if email is being updated
|
| 110 |
+
if "email" in update_data:
|
| 111 |
+
result = await db.execute(
|
| 112 |
+
select(Student).filter(
|
| 113 |
+
Student.email == update_data["email"],
|
| 114 |
+
Student.id != student_id
|
| 115 |
+
)
|
| 116 |
+
)
|
| 117 |
+
existing = result.scalar_one_or_none()
|
| 118 |
+
if existing:
|
| 119 |
+
raise HTTPException(
|
| 120 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 121 |
+
detail=f"Email {update_data['email']} is already in use"
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
for key, value in update_data.items():
|
| 125 |
+
setattr(student, key, value)
|
| 126 |
+
|
| 127 |
+
await db.commit()
|
| 128 |
+
await db.refresh(student)
|
| 129 |
+
|
| 130 |
+
return student
|
| 131 |
+
except HTTPException:
|
| 132 |
+
raise
|
| 133 |
+
except Exception as e:
|
| 134 |
+
raise HTTPException(
|
| 135 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 136 |
+
detail=f"Failed to update student: {str(e)}"
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
@router.patch("/{student_id}", response_model=StudentResponse)
|
| 141 |
+
async def partial_update_student(
|
| 142 |
+
student_id: int,
|
| 143 |
+
student_update: StudentUpdate,
|
| 144 |
+
db: AsyncSession = Depends(get_db),
|
| 145 |
+
current_user: User = Depends(get_current_user)
|
| 146 |
+
):
|
| 147 |
+
"""Partially update a student (same as PUT for this implementation) (Protected)"""
|
| 148 |
+
return await update_student(student_id, student_update, db, current_user)
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
@router.delete("/{student_id}", status_code=status.HTTP_204_NO_CONTENT)
|
| 152 |
+
async def delete_student(
|
| 153 |
+
student_id: int,
|
| 154 |
+
db: AsyncSession = Depends(get_db),
|
| 155 |
+
current_user: User = Depends(get_current_user)
|
| 156 |
+
):
|
| 157 |
+
"""Delete a student (Protected)"""
|
| 158 |
+
try:
|
| 159 |
+
result = await db.execute(select(Student).filter(Student.id == student_id))
|
| 160 |
+
student = result.scalar_one_or_none()
|
| 161 |
+
|
| 162 |
+
if not student:
|
| 163 |
+
raise HTTPException(
|
| 164 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 165 |
+
detail=f"Student with ID {student_id} not found"
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
await db.delete(student)
|
| 169 |
+
await db.commit()
|
| 170 |
+
|
| 171 |
+
return None
|
| 172 |
+
except HTTPException:
|
| 173 |
+
raise
|
| 174 |
+
except Exception as e:
|
| 175 |
+
raise HTTPException(
|
| 176 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 177 |
+
detail=f"Failed to delete student: {str(e)}"
|
| 178 |
+
)
|
Backend/{src β app}/config.py
RENAMED
|
File without changes
|
Backend/app/core/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.core.security import verify_password, get_password_hash, create_access_token
|
| 2 |
+
|
| 3 |
+
__all__ = ["verify_password", "get_password_hash", "create_access_token"]
|
Backend/app/core/security.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime, timedelta
|
| 2 |
+
from typing import Optional
|
| 3 |
+
from passlib.context import CryptContext
|
| 4 |
+
from jose import jwt
|
| 5 |
+
from app.config import settings
|
| 6 |
+
|
| 7 |
+
pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
|
| 8 |
+
|
| 9 |
+
def verify_password(plain_password: str, hashed_password:str) -> bool:
|
| 10 |
+
password_bytes = plain_password.encode("utf-8")[:72]
|
| 11 |
+
plain_password_truncated = password_bytes.decode("utf-8", errors="ignore")
|
| 12 |
+
return pwd_context.verify(plain_password, hashed_password)
|
| 13 |
+
|
| 14 |
+
def get_password_hash(password: str) -> str:
|
| 15 |
+
password_bytes = password.encode('utf-8')[:72]
|
| 16 |
+
password_truncated = password_bytes.decode("utf-8", errors='ignore')
|
| 17 |
+
return pwd_context.hash(password_truncated)
|
| 18 |
+
|
| 19 |
+
def create_access_token(data: dict, expires_deltas: Optional[timedelta] = None) -> str:
|
| 20 |
+
to_encode = data.copy()
|
| 21 |
+
expire = datetime.now() + (expires_deltas or timedelta(minutes=15))
|
| 22 |
+
to_encode.update({"exp": expire})
|
| 23 |
+
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
| 24 |
+
return encoded_jwt
|
Backend/{src/db/client.py β app/database.py}
RENAMED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
| 2 |
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
| 3 |
from datetime import datetime
|
| 4 |
-
from
|
| 5 |
|
| 6 |
|
| 7 |
engine = create_async_engine(settings.DATABASE_URL, echo=True)
|
|
|
|
| 1 |
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
| 2 |
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
| 3 |
from datetime import datetime
|
| 4 |
+
from app.config import settings
|
| 5 |
|
| 6 |
|
| 7 |
engine = create_async_engine(settings.DATABASE_URL, echo=True)
|
Backend/app/main.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI
|
| 2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
+
from contextlib import asynccontextmanager
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from app.config import settings
|
| 6 |
+
from app.database import engine, Base
|
| 7 |
+
from app.api.v1.api import api_router
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@asynccontextmanager
|
| 11 |
+
async def lifespan(app: FastAPI):
|
| 12 |
+
"""Application lifespan manager"""
|
| 13 |
+
print("ποΈ Server starting:", datetime.now())
|
| 14 |
+
print("π§ Creating tables if they don't exist...")
|
| 15 |
+
|
| 16 |
+
async with engine.begin() as conn:
|
| 17 |
+
await conn.run_sync(Base.metadata.create_all)
|
| 18 |
+
|
| 19 |
+
print("β
Tables ready!")
|
| 20 |
+
yield
|
| 21 |
+
print("π§Ή Server shutting down:", datetime.now())
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# Create FastAPI application
|
| 25 |
+
app = FastAPI(
|
| 26 |
+
title=settings.APP_NAME,
|
| 27 |
+
description=settings.APP_DESCRIPTION,
|
| 28 |
+
version=settings.APP_VERSION,
|
| 29 |
+
lifespan=lifespan
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
# CORS Configuration
|
| 33 |
+
app.add_middleware(
|
| 34 |
+
CORSMiddleware,
|
| 35 |
+
allow_origins=settings.CORS_ORIGINS,
|
| 36 |
+
allow_credentials=True,
|
| 37 |
+
allow_methods=["*"],
|
| 38 |
+
allow_headers=["*"],
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
# Include API router
|
| 42 |
+
app.include_router(api_router, prefix="/api/v1")
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# Health check endpoint
|
| 46 |
+
@app.get("/", tags=["Health"])
|
| 47 |
+
async def root():
|
| 48 |
+
"""Health check endpoint"""
|
| 49 |
+
return {
|
| 50 |
+
"status": "healthy",
|
| 51 |
+
"message": f"{settings.APP_NAME} is running",
|
| 52 |
+
"version": settings.APP_VERSION
|
| 53 |
+
}
|
Backend/app/models/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.models.tables import Student, User
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
__all__ = ["Student", "User"]
|
Backend/app/models/tables.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import String
|
| 2 |
+
from sqlalchemy.orm import Mapped, mapped_column
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
from app.database import Base
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class Student(Base):
|
| 8 |
+
__tablename__ = "students"
|
| 9 |
+
|
| 10 |
+
id: Mapped[int] = mapped_column(primary_key=True, index= True)
|
| 11 |
+
name: Mapped[str] = mapped_column(String(100))
|
| 12 |
+
email: Mapped[str] = mapped_column(String(100), unique=True, index=True)
|
| 13 |
+
created_at: Mapped[datetime] = mapped_column(default=datetime.now())
|
| 14 |
+
|
| 15 |
+
class User(Base):
|
| 16 |
+
__tablename__ = "users"
|
| 17 |
+
|
| 18 |
+
id: Mapped[int] = mapped_column(primary_key=True, index=True)
|
| 19 |
+
username: Mapped[str] = mapped_column(String(50), unique=True, index=True)
|
| 20 |
+
# email: Mapped[str] = mapped_column(String(100), unique=True, index=True)
|
| 21 |
+
hashed_password: Mapped[str] = mapped_column(String(255))
|
Backend/app/schema/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.schema.models import StudentCreate, StudentUpdate, StudentResponse, UserCreate, Token
|
| 2 |
+
|
| 3 |
+
__all__ = ["StudentCreate", "StudentUpdate", "StudentResponse", "UserCreate", "Token"]
|
Backend/app/schema/models.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, EmailStr, Field, field_validator, ConfigDict
|
| 2 |
+
from typing import Optional
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
|
| 5 |
+
class StudentBase(BaseModel):
|
| 6 |
+
name: str = Field(..., min_length=2, max_length=100)
|
| 7 |
+
email: EmailStr = Field(...)
|
| 8 |
+
|
| 9 |
+
@field_validator("name")
|
| 10 |
+
def validate_name(cls, v):
|
| 11 |
+
if not v.strip():
|
| 12 |
+
raise ValueError('Name cannot be empty or just whitespace')
|
| 13 |
+
return v.strip()
|
| 14 |
+
|
| 15 |
+
class StudentCreate(StudentBase):
|
| 16 |
+
pass
|
| 17 |
+
|
| 18 |
+
class StudentUpdate(BaseModel):
|
| 19 |
+
name: Optional[str] = Field(None, min_length=2, max_length=100)
|
| 20 |
+
email: Optional[EmailStr] = None
|
| 21 |
+
|
| 22 |
+
class StudentResponse(StudentBase):
|
| 23 |
+
id: int
|
| 24 |
+
created_at: datetime
|
| 25 |
+
|
| 26 |
+
model_config = ConfigDict(from_attributes=True)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class UserCreate(BaseModel):
|
| 30 |
+
username: str = Field(..., min_length=3, max_length=50)
|
| 31 |
+
password: str = Field(..., min_length=6, max_length=72)
|
| 32 |
+
|
| 33 |
+
@field_validator('password')
|
| 34 |
+
def validate_password(cls, v):
|
| 35 |
+
if len(v.encode("utf-8")) > 72:
|
| 36 |
+
raise ValueError('Password cannot exceed 72 bytes')
|
| 37 |
+
return v
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class Token(BaseModel):
|
| 41 |
+
access_token: str
|
| 42 |
+
token_type: str
|
| 43 |
+
|
Backend/run.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uvicorn
|
| 2 |
+
from app.main import app
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
uvicorn.run(
|
| 7 |
+
"app.main:app",
|
| 8 |
+
host="0.0.0.0",
|
| 9 |
+
port=8000,
|
| 10 |
+
reload=True
|
| 11 |
+
)
|
Backend/src/db/models.py
DELETED
|
@@ -1,5 +0,0 @@
|
|
| 1 |
-
from sqlalchemy import String
|
| 2 |
-
from sqlalchemy.orm import Mapped, mapped_column
|
| 3 |
-
from datetime import datetime
|
| 4 |
-
from db.client import Base
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Backend/src/main.py
DELETED
|
@@ -1,31 +0,0 @@
|
|
| 1 |
-
from fastapi import FastAPI
|
| 2 |
-
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
-
from contextlib import asynccontextmanager
|
| 4 |
-
from datetime import datetime
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
# @asynccontextmanager
|
| 10 |
-
# async def lifespan(app: FastAPI):
|
| 11 |
-
# print("server starting", datetime.now())
|
| 12 |
-
# print("creating tables if they dont exist....")
|
| 13 |
-
# async with engine
|
| 14 |
-
|
| 15 |
-
# app = FastAPI(lifespan=lifespan)
|
| 16 |
-
# app.include_router(api_router)
|
| 17 |
-
app.add_middleware(
|
| 18 |
-
CORSMiddleware,
|
| 19 |
-
allow_origins=["*"],
|
| 20 |
-
allow_credentials=True,
|
| 21 |
-
allow_methods=["*"],
|
| 22 |
-
allow_headers=["*"],
|
| 23 |
-
)
|
| 24 |
-
@app.get("/", tags=["Health"])
|
| 25 |
-
async def root():
|
| 26 |
-
"""Health check endpoint"""
|
| 27 |
-
return {
|
| 28 |
-
"status": "healthy",
|
| 29 |
-
"message": "Student Management API is running",
|
| 30 |
-
"version": "1.0.0"
|
| 31 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Frontend/package-lock.json
CHANGED
|
@@ -7,6 +7,7 @@
|
|
| 7 |
"": {
|
| 8 |
"name": "package",
|
| 9 |
"version": "0.0.0",
|
|
|
|
| 10 |
"dependencies": {
|
| 11 |
"@splinetool/react-spline": "^4.1.0",
|
| 12 |
"@splinetool/runtime": "^1.11.2",
|
|
|
|
| 7 |
"": {
|
| 8 |
"name": "package",
|
| 9 |
"version": "0.0.0",
|
| 10 |
+
"license": "ISC",
|
| 11 |
"dependencies": {
|
| 12 |
"@splinetool/react-spline": "^4.1.0",
|
| 13 |
"@splinetool/runtime": "^1.11.2",
|
Frontend/package.json
CHANGED
|
@@ -37,5 +37,10 @@
|
|
| 37 |
"typescript": "~5.9.3",
|
| 38 |
"typescript-eslint": "^8.46.3",
|
| 39 |
"vite": "^7.2.2"
|
| 40 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
}
|
|
|
|
| 37 |
"typescript": "~5.9.3",
|
| 38 |
"typescript-eslint": "^8.46.3",
|
| 39 |
"vite": "^7.2.2"
|
| 40 |
+
},
|
| 41 |
+
"description": "This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.",
|
| 42 |
+
"main": "eslint.config.js",
|
| 43 |
+
"keywords": [],
|
| 44 |
+
"author": "",
|
| 45 |
+
"license": "ISC"
|
| 46 |
}
|