aki-008 commited on
Commit
c9abf3f
Β·
1 Parent(s): aba3a06

chore: basic backend setup

Browse files
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 src.config import settings
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
  }