fixed admin page

parent c9a3b5c5
Showing with 173 additions and 1268 deletions
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = {}; const nextConfig = {
output : 'export',
basePath: process.env.NEXT_PUBLIC_BASE_PATH || '',
distDir : 'build',
trailingSlash : true,
};
export default nextConfig; export default nextConfig;
...@@ -27,11 +27,16 @@ def get_password_hash(password): ...@@ -27,11 +27,16 @@ def get_password_hash(password):
# User authentication # User authentication
def authenticate_user(db: Session, email: str, password: str): def authenticate_user(db: Session, email: str, password: str):
print(f"Attempting to authenticate user: {email}") # Add this
user = db.query(models.User).filter(models.User.email == email).first() user = db.query(models.User).filter(models.User.email == email).first()
if not user: if not user:
print(f"User not found: {email}") # Add this
return False return False
print(f"User found: {email}. Verifying password...") # Add this
if not verify_password(password, user.password): if not verify_password(password, user.password):
print(f"Password verification failed for user: {email}") # Add this
return False return False
print(f"Password verified successfully for user: {email}") # Add this
return user return user
# Token functions # Token functions
...@@ -65,20 +70,42 @@ async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = De ...@@ -65,20 +70,42 @@ async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = De
raise credentials_exception raise credentials_exception
return user return user
# In auth.py
async def get_current_active_user(current_user: schemas.User = Depends(get_current_user)): async def get_current_active_user(current_user: schemas.User = Depends(get_current_user)):
if current_user.status != schemas.UserStatus.approved: # --- Add these lines ---
print(f"DEBUG: Checking status for user {current_user.email}.")
print(f"DEBUG: DB Status Value: '{current_user.status}', Type: {type(current_user.status)}")
print(f"DEBUG: Comparing against: '{schemas.UserStatus.approved}', Type: {type(schemas.UserStatus.approved)}")
# --- End of added lines ---
if current_user.status.value != schemas.UserStatus.approved:
print(f"DEBUG: Status check FAILED: '{current_user.status.value}' != '{schemas.UserStatus.approved.value}'") # Add this
raise HTTPException(status_code=400, detail="Inactive user") raise HTTPException(status_code=400, detail="Inactive user")
print(f"DEBUG: Status check PASSED for user {current_user.email}") # Add this
return current_user return current_user
# Role-based authorization # Role-based authorization
# In auth.py
def get_admin_user(current_user: schemas.User = Depends(get_current_active_user)): def get_admin_user(current_user: schemas.User = Depends(get_current_active_user)):
if current_user.role != schemas.UserRole.admin: # --- Add these lines ---
print(f"DEBUG: Checking role for user {current_user.email}.")
print(f"DEBUG: DB Role Value: '{current_user.role}', Type: {type(current_user.role)}")
print(f"DEBUG: Comparing against: '{schemas.UserRole.admin}', Type: {type(schemas.UserRole.admin)}")
# --- End of added lines ---
if current_user.role.value != schemas.UserRole.admin:
print(f"DEBUG: Role check FAILED: '{current_user.role}' != '{schemas.UserRole.admin}'") # Add this
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized" detail="Not authorized"
) )
print(f"DEBUG: Role check PASSED for user {current_user.email}") # Add this
return current_user return current_user
def get_professor_user(current_user: schemas.User = Depends(get_current_active_user)): def get_professor_user(current_user: schemas.User = Depends(get_current_active_user)):
if current_user.role != schemas.UserRole.professor and current_user.role != schemas.UserRole.admin: if current_user.role != schemas.UserRole.professor and current_user.role != schemas.UserRole.admin:
raise HTTPException( raise HTTPException(
...@@ -93,4 +120,4 @@ def get_student_user(current_user: schemas.User = Depends(get_current_active_use ...@@ -93,4 +120,4 @@ def get_student_user(current_user: schemas.User = Depends(get_current_active_use
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized" detail="Not authorized"
) )
return current_user return current_user
\ No newline at end of file
ke
\ No newline at end of file
from auth import (
get_password_hash,
authenticate_user,
create_access_token,
get_current_user,
get_current_active_user,
get_admin_user,
get_professor_user,
get_student_user,
ACCESS_TOKEN_EXPIRE_MINUTES
)
new_hash= get_password_hash("admin123")
print(new_hash)
\ No newline at end of file
...@@ -77,7 +77,7 @@ CREATE TABLE IF NOT EXISTS user_requests ( ...@@ -77,7 +77,7 @@ CREATE TABLE IF NOT EXISTS user_requests (
-- Insert sample admin user -- Insert sample admin user
INSERT INTO users (email, password, role, status) INSERT INTO users (email, password, role, status)
VALUES ('admin@ujaen.es', '$2b$12$1tJXSR/YKBEsvbRvuQC1qOQkgBCzpWzXaAkXSWCSEkM9GpzNBGjVK', 'admin', 'approved'); VALUES ('admin@ujaen.es', '$2b$12$ESnGdZ.c/GPYfYcsCbe5eeqdHCBpJq.Q7qE4vmJnJDwDgIY7PHDamPS', 'admin', 'approved');
-- Password is 'admin123' (hashed) -- Password is 'admin123' (hashed)
-- Insert sample professor users -- Insert sample professor users
......
# UJA Exams Database Schema # UJA Exams Database Schema
## Overview
This document outlines the database schema for the UJA Exams web application. The schema is designed to support the application's requirements for user management, exam management, and grade tracking.
## Tables ## Tables
### 1. Users ### 1. Users
......
#!/bin/bash
# Initialize the database
echo "Creating UJA Exams database..."
sudo mysql -e "CREATE DATABASE IF NOT EXISTS uja_exams;"
sudo mysql -e "CREATE USER IF NOT EXISTS 'uja_user'@'localhost' IDENTIFIED BY 'uja_password';"
sudo mysql -e "GRANT ALL PRIVILEGES ON uja_exams.* TO 'uja_user'@'localhost';"
sudo mysql -e "FLUSH PRIVILEGES;"
# Import the database schema
echo "Importing database schema..."
sudo mysql uja_exams < /home/ubuntu/uja_exams_app/database/init_db.sql
echo "Database setup completed successfully!"
# UJA Exams Web Application
This is a web application for managing exams at UJA.
## Deployment Instructions
### Prerequisites
- Node.js (v14 or higher)
- Python 3.8 or higher
- MariaDB/MySQL
### Backend Setup
1. Navigate to the backend directory: `cd backend`
2. Create a virtual environment: `python3 -m venv venv`
3. Activate the virtual environment: `source venv/bin/activate`
4. Install dependencies: `pip install -r requirements.txt`
5. Start the backend server: `./start_backend.sh`
### Frontend Setup
1. Navigate to the frontend directory: `cd frontend`
2. Install dependencies: `npm install`
3. Start the frontend server: `./start_frontend.sh`
### Running the Application
You can run both backend and frontend servers simultaneously using the main deployment script:
```
./deploy.sh
```
### Accessing the Application
- Backend API: http://localhost:8000
- Frontend: http://localhost:3000
## User Roles
- Student: Can view and download exams, see grades
- Professor: Can upload exams, manage grades
- Admin: Can approve user registrations, manage all content
## Features
- User authentication with role-based access control
- Exam upload and management
- Grade tracking
- Admin approval workflow
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
from datetime import datetime, timedelta
import jwt
from passlib.context import CryptContext
from typing import Optional
import models
import schemas
from database import get_db
# Security configuration
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
# Password functions
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password):
return pwd_context.hash(password)
# User authentication
def authenticate_user(db: Session, email: str, password: str):
user = db.query(models.User).filter(models.User.email == email).first()
if not user:
return False
if not verify_password(password, user.password):
return False
return user
# Token functions
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
# User dependency functions
async 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])
email: str = payload.get("sub")
if email is None:
raise credentials_exception
token_data = schemas.TokenData(email=email)
except jwt.PyJWTError:
raise credentials_exception
user = db.query(models.User).filter(models.User.email == token_data.email).first()
if user is None:
raise credentials_exception
return user
async def get_current_active_user(current_user: schemas.User = Depends(get_current_user)):
if current_user.status != schemas.UserStatus.approved:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
# Role-based authorization
def get_admin_user(current_user: schemas.User = Depends(get_current_active_user)):
if current_user.role != schemas.UserRole.admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized"
)
return current_user
def get_professor_user(current_user: schemas.User = Depends(get_current_active_user)):
if current_user.role != schemas.UserRole.professor and current_user.role != schemas.UserRole.admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized"
)
return current_user
def get_student_user(current_user: schemas.User = Depends(get_current_active_user)):
if current_user.role != schemas.UserRole.student:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized"
)
return current_user
from sqlalchemy.orm import Session
from typing import List, Optional
from datetime import datetime
import models
import schemas
# User CRUD operations
def get_user(db: Session, user_id: int):
return db.query(models.User).filter(models.User.id == user_id).first()
def get_user_by_email(db: Session, email: str):
return db.query(models.User).filter(models.User.email == email).first()
def get_users(db: Session, skip: int = 0, limit: int = 100):
return db.query(models.User).offset(skip).limit(limit).all()
def create_user(db: Session, user: schemas.UserCreate, hashed_password: str):
db_user = models.User(
email=user.email,
password=hashed_password,
role=user.role,
status=models.UserStatus.pending
)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
def update_user(db: Session, user_id: int, user: schemas.UserUpdate):
db_user = db.query(models.User).filter(models.User.id == user_id).first()
if db_user:
if user.email:
db_user.email = user.email
if user.password:
db_user.password = user.password
if user.status:
db_user.status = user.status
db.commit()
db.refresh(db_user)
return db_user
# Exam CRUD operations
def get_exam(db: Session, exam_id: int):
return db.query(models.Exam).filter(models.Exam.id == exam_id).first()
def get_exams(db: Session, skip: int = 0, limit: int = 100):
return db.query(models.Exam).offset(skip).limit(limit).all()
def get_exams_by_professor(db: Session, professor_id: int, skip: int = 0, limit: int = 100):
return db.query(models.Exam).filter(models.Exam.professor_id == professor_id).offset(skip).limit(limit).all()
def create_exam(db: Session, exam: schemas.ExamCreate):
db_exam = models.Exam(**exam.dict())
db.add(db_exam)
db.commit()
db.refresh(db_exam)
return db_exam
def update_exam(db: Session, exam_id: int, exam: schemas.ExamUpdate):
db_exam = db.query(models.Exam).filter(models.Exam.id == exam_id).first()
if db_exam:
if exam.title:
db_exam.title = exam.title
if exam.description is not None:
db_exam.description = exam.description
db.commit()
db.refresh(db_exam)
return db_exam
def delete_exam(db: Session, exam_id: int):
db_exam = db.query(models.Exam).filter(models.Exam.id == exam_id).first()
if db_exam:
db.delete(db_exam)
db.commit()
return True
return False
# Grade CRUD operations
def get_grade(db: Session, grade_id: int):
return db.query(models.Grade).filter(models.Grade.id == grade_id).first()
def get_grades(db: Session, skip: int = 0, limit: int = 100):
return db.query(models.Grade).offset(skip).limit(limit).all()
def get_grades_by_student(db: Session, student_id: int, skip: int = 0, limit: int = 100):
return db.query(models.Grade).filter(models.Grade.student_id == student_id).offset(skip).limit(limit).all()
def get_grades_by_exam(db: Session, exam_id: int, skip: int = 0, limit: int = 100):
return db.query(models.Grade).filter(models.Grade.exam_id == exam_id).offset(skip).limit(limit).all()
def create_grade(db: Session, grade: schemas.GradeCreate):
db_grade = models.Grade(**grade.dict())
db.add(db_grade)
db.commit()
db.refresh(db_grade)
return db_grade
def update_grade(db: Session, grade_id: int, grade: schemas.GradeUpdate):
db_grade = db.query(models.Grade).filter(models.Grade.id == grade_id).first()
if db_grade:
if grade.grade is not None:
db_grade.grade = grade.grade
if grade.result_date:
db_grade.result_date = grade.result_date
db.commit()
db.refresh(db_grade)
return db_grade
# Rating CRUD operations
def get_rating(db: Session, rating_id: int):
return db.query(models.ExamRating).filter(models.ExamRating.id == rating_id).first()
def get_rating_by_student_exam(db: Session, student_id: int, exam_id: int):
return db.query(models.ExamRating).filter(
models.ExamRating.student_id == student_id,
models.ExamRating.exam_id == exam_id
).first()
def get_ratings_by_exam(db: Session, exam_id: int, skip: int = 0, limit: int = 100):
return db.query(models.ExamRating).filter(models.ExamRating.exam_id == exam_id).offset(skip).limit(limit).all()
def create_rating(db: Session, rating: schemas.RatingCreate):
db_rating = models.ExamRating(**rating.dict())
db.add(db_rating)
db.commit()
db.refresh(db_rating)
return db_rating
def update_rating(db: Session, student_id: int, exam_id: int, rating_value: float):
db_rating = get_rating_by_student_exam(db, student_id, exam_id)
if db_rating:
db_rating.rating = rating_value
db.commit()
db.refresh(db_rating)
return db_rating
return None
# Exchange CRUD operations
def get_exchange(db: Session, exchange_id: int):
return db.query(models.ExamExchange).filter(models.ExamExchange.id == exchange_id).first()
def get_exchanges_by_sender(db: Session, sender_id: int, skip: int = 0, limit: int = 100):
return db.query(models.ExamExchange).filter(models.ExamExchange.sender_id == sender_id).offset(skip).limit(limit).all()
def get_exchanges_by_receiver(db: Session, receiver_id: int, skip: int = 0, limit: int = 100):
return db.query(models.ExamExchange).filter(models.ExamExchange.receiver_id == receiver_id).offset(skip).limit(limit).all()
def create_exchange(db: Session, exchange: schemas.ExchangeCreate):
db_exchange = models.ExamExchange(**exchange.dict())
db.add(db_exchange)
db.commit()
db.refresh(db_exchange)
return db_exchange
def update_exchange_status(db: Session, exchange_id: int, status: schemas.RequestStatus):
db_exchange = db.query(models.ExamExchange).filter(models.ExamExchange.id == exchange_id).first()
if db_exchange:
db_exchange.status = status
db.commit()
db.refresh(db_exchange)
return db_exchange
# User Request CRUD operations
def get_request(db: Session, request_id: int):
return db.query(models.UserRequest).filter(models.UserRequest.id == request_id).first()
def get_request_by_user(db: Session, user_id: int):
return db.query(models.UserRequest).filter(models.UserRequest.user_id == user_id).first()
def get_requests(db: Session, skip: int = 0, limit: int = 100):
return db.query(models.UserRequest).offset(skip).limit(limit).all()
def get_pending_requests(db: Session, skip: int = 0, limit: int = 100):
return db.query(models.UserRequest).filter(
models.UserRequest.status == models.RequestStatus.pending
).offset(skip).limit(limit).all()
def create_request(db: Session, user_id: int):
db_request = models.UserRequest(user_id=user_id)
db.add(db_request)
db.commit()
db.refresh(db_request)
return db_request
def update_request_status(db: Session, request_id: int, status: schemas.RequestStatus):
db_request = db.query(models.UserRequest).filter(models.UserRequest.id == request_id).first()
if db_request:
db_request.status = status
db_request.response_date = datetime.now()
# Update user status based on request status
user = db.query(models.User).filter(models.User.id == db_request.user_id).first()
if status == schemas.RequestStatus.accepted:
user.status = models.UserStatus.approved
elif status == schemas.RequestStatus.rejected:
user.status = models.UserStatus.rejected
db.commit()
db.refresh(db_request)
return db_request
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
# Database connection URL
SQLALCHEMY_DATABASE_URL = "mysql+pymysql://uja_user:uja_password@localhost/uja_exams"
# Create SQLAlchemy engine
engine = create_engine(SQLALCHEMY_DATABASE_URL)
# Create SessionLocal class
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Create Base class
Base = declarative_base()
# Dependency to get DB session
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
from fastapi import FastAPI, Depends, HTTPException, status, UploadFile, File, Form
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
from typing import List, Optional
from datetime import datetime, timedelta
import os
import shutil
# Import local modules
from database import get_db, engine
import models
import schemas
import crud
from auth import (
get_password_hash,
authenticate_user,
create_access_token,
get_current_user,
get_current_active_user,
get_admin_user,
get_professor_user,
get_student_user,
ACCESS_TOKEN_EXPIRE_MINUTES
)
# Create database tables
models.Base.metadata.create_all(bind=engine)
# Initialize FastAPI app
app = FastAPI(title="UJA Exams API")
# Configure CORS
origins = [
"http://localhost",
"http://localhost:3000",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Create uploads directory
UPLOAD_DIR = "uploads/exams"
os.makedirs(UPLOAD_DIR, exist_ok=True)
# Authentication endpoints
@app.post("/token", response_model=schemas.Token)
async def login_for_access_token(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=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.email, "role": user.role.value}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
# User endpoints
@app.post("/users/", response_model=schemas.User)
async def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
db_user = crud.get_user_by_email(db, email=user.email)
if db_user:
raise HTTPException(status_code=400, detail="Email already registered")
hashed_password = get_password_hash(user.password)
db_user = crud.create_user(db, user=user, hashed_password=hashed_password)
# Create user request for admin approval
crud.create_request(db, user_id=db_user.id)
return db_user
@app.get("/users/me/", response_model=schemas.User)
async def read_users_me(current_user: schemas.User = Depends(get_current_active_user)):
return current_user
@app.get("/users/", response_model=List[schemas.User])
async def read_users(
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db),
current_user: schemas.User = Depends(get_admin_user)
):
users = crud.get_users(db, skip=skip, limit=limit)
return users
# Exam endpoints
@app.post("/exams/", response_model=schemas.Exam)
async def create_exam(
title: str = Form(...),
description: str = Form(None),
file: UploadFile = File(...),
db: Session = Depends(get_db),
current_user: schemas.User = Depends(get_professor_user)
):
# Save the uploaded file
file_location = f"{UPLOAD_DIR}/{file.filename}"
with open(file_location, "wb+") as file_object:
shutil.copyfileobj(file.file, file_object)
# Create exam record
exam_data = schemas.ExamCreate(
title=title,
description=description,
file_path=file_location,
professor_id=current_user.id
)
db_exam = crud.create_exam(db, exam=exam_data)
return db_exam
@app.get("/exams/", response_model=List[schemas.Exam])
async def read_exams(
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db),
current_user: schemas.User = Depends(get_current_active_user)
):
exams = crud.get_exams(db, skip=skip, limit=limit)
# Calculate average rating for each exam
for exam in exams:
ratings = crud.get_ratings_by_exam(db, exam_id=exam.id)
if ratings:
exam.average_rating = sum(r.rating for r in ratings) / len(ratings)
else:
exam.average_rating = 0
return exams
@app.get("/exams/{exam_id}", response_model=schemas.Exam)
async def read_exam(
exam_id: int,
db: Session = Depends(get_db),
current_user: schemas.User = Depends(get_current_active_user)
):
exam = crud.get_exam(db, exam_id=exam_id)
if exam is None:
raise HTTPException(status_code=404, detail="Exam not found")
# Calculate average rating
ratings = crud.get_ratings_by_exam(db, exam_id=exam.id)
if ratings:
exam.average_rating = sum(r.rating for r in ratings) / len(ratings)
else:
exam.average_rating = 0
return exam
# Grade endpoints
@app.post("/grades/", response_model=schemas.Grade)
async def create_grade(
grade: schemas.GradeCreate,
db: Session = Depends(get_db),
current_user: schemas.User = Depends(get_professor_user)
):
# Check if exam exists
exam = crud.get_exam(db, exam_id=grade.exam_id)
if exam is None:
raise HTTPException(status_code=404, detail="Exam not found")
# Check if student exists
student = crud.get_user(db, user_id=grade.student_id)
if student is None or student.role != schemas.UserRole.student:
raise HTTPException(status_code=404, detail="Student not found")
# Check if grade already exists
existing_grades = crud.get_grades_by_student(db, student_id=grade.student_id)
for existing_grade in existing_grades:
if existing_grade.exam_id == grade.exam_id:
raise HTTPException(status_code=400, detail="Grade already exists for this student and exam")
db_grade = crud.create_grade(db, grade=grade)
return db_grade
@app.get("/grades/", response_model=List[schemas.Grade])
async def read_grades(
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db),
current_user: schemas.User = Depends(get_current_active_user)
):
if current_user.role == schemas.UserRole.student:
# Students can only see their own grades
grades = crud.get_grades_by_student(db, student_id=current_user.id, skip=skip, limit=limit)
else:
# Professors and admins can see all grades
grades = crud.get_grades(db, skip=skip, limit=limit)
return grades
# Rating endpoints
@app.post("/ratings/", response_model=schemas.Rating)
async def create_rating(
rating: schemas.RatingCreate,
db: Session = Depends(get_db),
current_user: schemas.User = Depends(get_student_user)
):
# Check if exam exists
exam = crud.get_exam(db, exam_id=rating.exam_id)
if exam is None:
raise HTTPException(status_code=404, detail="Exam not found")
# Check if rating already exists
existing_rating = crud.get_rating_by_student_exam(db, student_id=current_user.id, exam_id=rating.exam_id)
if existing_rating:
# Update existing rating
updated_rating = crud.update_rating(db, student_id=current_user.id, exam_id=rating.exam_id, rating_value=rating.rating)
return updated_rating
else:
# Create new rating
rating_data = schemas.RatingCreate(
exam_id=rating.exam_id,
student_id=current_user.id,
rating=rating.rating
)
db_rating = crud.create_rating(db, rating=rating_data)
return db_rating
# Exchange endpoints
@app.post("/exchanges/", response_model=schemas.Exchange)
async def create_exchange(
exchange: schemas.ExchangeCreate,
db: Session = Depends(get_db),
current_user: schemas.User = Depends(get_student_user)
):
if current_user.id != exchange.sender_id:
raise HTTPException(status_code=403, detail="You can only create exchanges for yourself")
# Check if exam exists
exam = crud.get_exam(db, exam_id=exchange.exam_id)
if exam is None:
raise HTTPException(status_code=404, detail="Exam not found")
# Check if receiver exists and is a student
receiver = crud.get_user(db, user_id=exchange.receiver_id)
if receiver is None or receiver.role != schemas.UserRole.student:
raise HTTPException(status_code=404, detail="Receiver not found or not a student")
db_exchange = crud.create_exchange(db, exchange=exchange)
return db_exchange
@app.get("/exchanges/", response_model=List[schemas.Exchange])
async def read_exchanges(
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db),
current_user: schemas.User = Depends(get_current_active_user)
):
if current_user.role == schemas.UserRole.student:
# Students can only see exchanges they're involved in
sent_exchanges = crud.get_exchanges_by_sender(db, sender_id=current_user.id, skip=skip, limit=limit)
received_exchanges = crud.get_exchanges_by_receiver(db, receiver_id=current_user.id, skip=skip, limit=limit)
exchanges = sent_exchanges + received_exchanges
else:
# Professors and admins can see all exchanges
exchanges = db.query(models.ExamExchange).offset(skip).limit(limit).all()
return exchanges
@app.put("/exchanges/{exchange_id}", response_model=schemas.Exchange)
async def update_exchange(
exchange_id: int,
exchange_update: schemas.ExchangeUpdate,
db: Session = Depends(get_db),
current_user: schemas.User = Depends(get_current_active_user)
):
db_exchange = crud.get_exchange(db, exchange_id=exchange_id)
if db_exchange is None:
raise HTTPException(status_code=404, detail="Exchange not found")
if current_user.role == schemas.UserRole.student and current_user.id != db_exchange.receiver_id:
raise HTTPException(status_code=403, detail="You can only update exchanges where you are the receiver")
updated_exchange = crud.update_exchange_status(db, exchange_id=exchange_id, status=exchange_update.status)
return updated_exchange
# User request endpoints
@app.get("/requests/", response_model=List[schemas.UserRequest])
async def read_requests(
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db),
current_user: schemas.User = Depends(get_admin_user)
):
requests = crud.get_requests(db, skip=skip, limit=limit)
return requests
@app.put("/requests/{request_id}", response_model=schemas.UserRequest)
async def update_request(
request_id: int,
request_update: schemas.UserRequestUpdate,
db: Session = Depends(get_db),
current_user: schemas.User = Depends(get_admin_user)
):
db_request = crud.get_request(db, request_id=request_id)
if db_request is None:
raise HTTPException(status_code=404, detail="Request not found")
updated_request = crud.update_request_status(db, request_id=request_id, status=request_update.status)
return updated_request
# Root endpoint
@app.get("/")
async def root():
return {"message": "Welcome to UJA Exams API"}
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, Text, Float, Date, DateTime, Enum, UniqueConstraint
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
import enum
from database import Base
# Enum definitions
class UserRole(enum.Enum):
student = "student"
professor = "professor"
admin = "admin"
class UserStatus(enum.Enum):
pending = "pending"
approved = "approved"
rejected = "rejected"
class RequestStatus(enum.Enum):
pending = "pending"
accepted = "accepted"
rejected = "rejected"
# Model definitions
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String(255), unique=True, index=True, nullable=False)
password = Column(String(255), nullable=False)
role = Column(Enum(UserRole), nullable=False)
status = Column(Enum(UserStatus), default=UserStatus.pending, nullable=False)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
# Relationships
exams = relationship("Exam", back_populates="professor")
grades = relationship("Grade", back_populates="student")
ratings = relationship("ExamRating", back_populates="student")
sent_exchanges = relationship("ExamExchange", foreign_keys="ExamExchange.sender_id", back_populates="sender")
received_exchanges = relationship("ExamExchange", foreign_keys="ExamExchange.receiver_id", back_populates="receiver")
request = relationship("UserRequest", back_populates="user", uselist=False)
class Exam(Base):
__tablename__ = "exams"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(255), nullable=False)
description = Column(Text)
file_path = Column(String(255), nullable=False)
professor_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
# Relationships
professor = relationship("User", back_populates="exams")
grades = relationship("Grade", back_populates="exam")
ratings = relationship("ExamRating", back_populates="exam")
exchanges = relationship("ExamExchange", back_populates="exam")
class Grade(Base):
__tablename__ = "grades"
id = Column(Integer, primary_key=True, index=True)
student_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
exam_id = Column(Integer, ForeignKey("exams.id", ondelete="CASCADE"), nullable=False)
grade = Column(Float(precision=5, decimal_return_scale=2), nullable=False)
result_date = Column(Date, nullable=False)
# Unique constraint
__table_args__ = (UniqueConstraint('student_id', 'exam_id', name='unique_student_exam'),)
# Relationships
student = relationship("User", back_populates="grades")
exam = relationship("Exam", back_populates="grades")
class ExamRating(Base):
__tablename__ = "exam_ratings"
id = Column(Integer, primary_key=True, index=True)
exam_id = Column(Integer, ForeignKey("exams.id", ondelete="CASCADE"), nullable=False)
student_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
rating = Column(Float(precision=2, decimal_return_scale=1), nullable=False)
created_at = Column(DateTime, server_default=func.now())
# Unique constraint
__table_args__ = (UniqueConstraint('student_id', 'exam_id', name='unique_student_exam_rating'),)
# Relationships
exam = relationship("Exam", back_populates="ratings")
student = relationship("User", back_populates="ratings")
class ExamExchange(Base):
__tablename__ = "exam_exchanges"
id = Column(Integer, primary_key=True, index=True)
sender_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
receiver_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
exam_id = Column(Integer, ForeignKey("exams.id", ondelete="CASCADE"), nullable=False)
status = Column(Enum(RequestStatus), default=RequestStatus.pending, nullable=False)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
# Relationships
sender = relationship("User", foreign_keys=[sender_id], back_populates="sent_exchanges")
receiver = relationship("User", foreign_keys=[receiver_id], back_populates="received_exchanges")
exam = relationship("Exam", back_populates="exchanges")
class UserRequest(Base):
__tablename__ = "user_requests"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
status = Column(Enum(RequestStatus), default=RequestStatus.pending, nullable=False)
request_date = Column(DateTime, server_default=func.now())
response_date = Column(DateTime, nullable=True)
# Relationships
user = relationship("User", back_populates="request")
annotated-types==0.7.0
anyio==4.9.0
click==8.1.8
exceptiongroup==1.2.2
fastapi==0.115.11
greenlet==3.1.1
h11==0.14.0
idna==3.10
pydantic==2.10.6
pydantic_core==2.27.2
PyMySQL==1.1.1
python-multipart==0.0.20
sniffio==1.3.1
SQLAlchemy==2.0.39
starlette==0.46.1
typing_extensions==4.12.2
uvicorn==0.34.0
from pydantic import BaseModel, EmailStr, Field, validator
from typing import Optional, List
from datetime import datetime, date
from enum import Enum
# Enum definitions
class UserRole(str, Enum):
student = "student"
professor = "professor"
admin = "admin"
class UserStatus(str, Enum):
pending = "pending"
approved = "approved"
rejected = "rejected"
class RequestStatus(str, Enum):
pending = "pending"
accepted = "accepted"
rejected = "rejected"
# User schemas
class UserBase(BaseModel):
email: EmailStr
class UserCreate(UserBase):
password: str
role: UserRole
class UserUpdate(BaseModel):
email: Optional[EmailStr] = None
password: Optional[str] = None
status: Optional[UserStatus] = None
class UserInDB(UserBase):
id: int
role: UserRole
status: UserStatus
created_at: datetime
updated_at: datetime
class Config:
orm_mode = True
class User(UserInDB):
pass
# Exam schemas
class ExamBase(BaseModel):
title: str
description: Optional[str] = None
class ExamCreate(ExamBase):
file_path: str
professor_id: int
class ExamUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
class ExamInDB(ExamBase):
id: int
file_path: str
professor_id: int
created_at: datetime
updated_at: datetime
class Config:
orm_mode = True
class Exam(ExamInDB):
average_rating: Optional[float] = None
# Grade schemas
class GradeBase(BaseModel):
student_id: int
exam_id: int
grade: float = Field(..., ge=0, le=100)
result_date: date
class GradeCreate(GradeBase):
pass
class GradeUpdate(BaseModel):
grade: Optional[float] = Field(None, ge=0, le=100)
result_date: Optional[date] = None
class GradeInDB(GradeBase):
id: int
class Config:
orm_mode = True
class Grade(GradeInDB):
exam: Optional[ExamBase] = None
# Rating schemas
class RatingBase(BaseModel):
exam_id: int
student_id: int
rating: float = Field(..., ge=0, le=5)
class RatingCreate(RatingBase):
pass
class RatingInDB(RatingBase):
id: int
created_at: datetime
class Config:
orm_mode = True
class Rating(RatingInDB):
pass
# Exchange schemas
class ExchangeBase(BaseModel):
sender_id: int
receiver_id: int
exam_id: int
class ExchangeCreate(ExchangeBase):
pass
class ExchangeUpdate(BaseModel):
status: RequestStatus
class ExchangeInDB(ExchangeBase):
id: int
status: RequestStatus
created_at: datetime
updated_at: datetime
class Config:
orm_mode = True
class Exchange(ExchangeInDB):
exam: Optional[ExamBase] = None
# User Request schemas
class UserRequestBase(BaseModel):
user_id: int
class UserRequestCreate(UserRequestBase):
pass
class UserRequestUpdate(BaseModel):
status: RequestStatus
class UserRequestInDB(UserRequestBase):
id: int
status: RequestStatus
request_date: datetime
response_date: Optional[datetime] = None
class Config:
orm_mode = True
class UserRequest(UserRequestInDB):
user: Optional[UserBase] = None
# Token schemas
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
email: Optional[str] = None
role: Optional[UserRole] = None
#!/bin/bash
# Backend startup script for production
# Create virtual environment if it doesn't exist
if [ ! -d "venv" ]; then
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
else
source venv/bin/activate
fi
# Start the backend server
uvicorn main:app --host 0.0.0.0 --port 8000
#!/bin/bash
# Main deployment script for UJA Exams web application
# Start the backend server
cd backend
./start_backend.sh &
BACKEND_PID=$!
# Wait for backend to start
echo "Waiting for backend to start..."
sleep 5
# Start the frontend server
cd ../frontend
./start_frontend.sh &
FRONTEND_PID=$!
echo "Both servers are running!"
echo "Backend PID: $BACKEND_PID"
echo "Frontend PID: $FRONTEND_PID"
echo "Press Ctrl+C to stop both servers"
# Wait for user to press Ctrl+C
trap "kill $BACKEND_PID $FRONTEND_PID; exit" INT
wait
{
"files": {
"main.css": "/static/css/main.1ca9c762.css",
"main.js": "/static/js/main.d32f103b.js",
"static/js/453.8ab44547.chunk.js": "/static/js/453.8ab44547.chunk.js",
"index.html": "/index.html",
"main.1ca9c762.css.map": "/static/css/main.1ca9c762.css.map",
"main.d32f103b.js.map": "/static/js/main.d32f103b.js.map",
"453.8ab44547.chunk.js.map": "/static/js/453.8ab44547.chunk.js.map"
},
"entrypoints": [
"static/css/main.1ca9c762.css",
"static/js/main.d32f103b.js"
]
}
\ No newline at end of file
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>React App</title><script defer="defer" src="/static/js/main.d32f103b.js"></script><link href="/static/css/main.1ca9c762.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
\ No newline at end of file
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
{
"name": "uja-exams-frontend",
"version": "1.0.0",
"description": "UJA Exams Frontend Server",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.17.1"
}
}
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
const express = require('express');
const path = require('path');
const app = express();
// Serve static files
app.use(express.static(__dirname));
// Handle React routing, return all requests to React app
app.get('*', function(req, res) {
res.sendFile(path.join(__dirname, 'index.html'));
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Frontend server is running on port ${PORT}`);
});
#!/bin/bash
# Frontend startup script for production
# Install dependencies if needed
if [ ! -d "node_modules" ]; then
npm install
fi
# Start the frontend server
npm start
body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;margin:0}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}.auth-container{background-color:#f5f5f5;display:flex;flex-direction:column;min-height:100vh}.auth-header{align-items:center;background-color:#4b7a3e;color:#fff;display:flex;padding:1rem}.auth-form-container{align-items:center;display:flex;flex:1 1;justify-content:center;padding:2rem}.auth-form{background-color:#e8f5e4;border-radius:8px;box-shadow:0 4px 6px #0000001a;max-width:400px;padding:2rem;width:100%}.auth-title{color:#333;font-size:1.5rem;margin-bottom:.5rem;text-align:center}.auth-subtitle{color:#666;margin-bottom:1.5rem;text-align:center}.input-with-icon{align-items:center;display:flex;position:relative}.input-icon{color:#666;left:10px;position:absolute}.form-group input,.form-group select{border:1px solid #ddd;border-radius:4px;font-size:1rem;padding:.75rem .75rem .75rem 2.5rem;width:100%}.password-toggle{background:none;border:none;color:#666;cursor:pointer;position:absolute;right:10px}.text-right{text-align:right}.forgot-password{color:#4b7a3e;font-size:.9rem;text-decoration:none}.auth-button{background-color:#4b7a3e;border:none;border-radius:4px;color:#fff;cursor:pointer;font-size:1rem;padding:.75rem;transition:background-color .3s;width:100%}.auth-button:hover{background-color:#3c6132}.auth-button:disabled{background-color:#a0c49d;cursor:not-allowed}.auth-footer{color:#666;margin-top:1.5rem;text-align:center}.auth-footer a{color:#4b7a3e;text-decoration:none}.auth-error{background-color:#f8d7da;border-radius:4px;color:#721c24;margin-bottom:1.5rem;padding:.75rem;text-align:center}.pending-container{background-color:#e8f5e4;border-radius:8px;box-shadow:0 4px 6px #0000001a;margin:2rem auto;max-width:600px;padding:2rem;text-align:center;width:100%}.pending-title{color:#d35400;margin-bottom:1.5rem}.pending-message{color:#333;line-height:1.6;margin-bottom:2rem}.pending-actions{display:flex;justify-content:center}.navigation-container{display:flex;flex-direction:column;width:100%}.navigation-header{background-color:#4b7a3e;color:#fff;justify-content:space-between;padding:1rem}.logo,.navigation-header{align-items:center;display:flex}.logo{font-size:1.5rem;font-weight:700}.at-symbol{margin-right:.5rem}.search-bar{align-items:center;background-color:#fff;border-radius:20px;display:flex;padding:.5rem;width:40%}.search-bar input{border:none;flex-grow:1;outline:none;padding:.25rem}.search-button{background:none;border:none;cursor:pointer;font-size:1.2rem}.user-profile{align-items:center;display:flex}.user-name{margin-right:1rem;text-transform:capitalize}.profile-icon{cursor:pointer;position:relative}.profile-icon img{background-color:#ddd;border-radius:50%;height:40px;width:40px}.profile-dropdown{background-color:#fff;border-radius:4px;box-shadow:0 2px 10px #0000001a;display:none;position:absolute;right:0;top:100%;width:150px;z-index:10}.profile-icon:hover .profile-dropdown{display:block}.dropdown-item{color:#333;cursor:pointer;padding:.75rem 1rem}.dropdown-item:hover{background-color:#f5f5f5}.navigation-menu{background-color:#e8f5e4;border-bottom:1px solid #ddd;display:flex}.nav-link{border-bottom:3px solid #0000;color:#333;padding:1rem 1.5rem;text-decoration:none;transition:all .3s}.nav-link:hover{background-color:#d4ebd0}.nav-link.active{border-bottom-color:#4b7a3e;font-weight:700}.dashboard-container{background-color:#f5f5f5;display:flex;flex-direction:column;min-height:100vh}.dashboard-content{padding:2rem}.dashboard-sections{display:flex;gap:1rem;justify-content:space-between;margin-bottom:2rem}.dashboard-section{background-color:#fff;border-radius:8px;box-shadow:0 2px 4px #0000001a;flex:1 1;padding:1.5rem}.dashboard-section h2{color:#333;font-size:1.2rem;margin-bottom:1rem;margin-top:0}.section-card,.section-content{display:flex;justify-content:center}.section-card{align-items:center;background-color:#f9f9f9;border-radius:8px;cursor:pointer;flex-direction:column;padding:2rem;transition:transform .3s,box-shadow .3s;width:100%}.section-card:hover{box-shadow:0 5px 15px #0000001a;transform:translateY(-5px)}.card-icon{font-size:2.5rem;margin-bottom:1rem}.card-title{color:#333;font-weight:700}.exams-section{background-color:#fff;border-radius:8px;box-shadow:0 2px 4px #0000001a;margin-bottom:2rem;padding:1.5rem}.exams-header{align-items:center;display:flex;justify-content:space-between;margin-bottom:1.5rem}.exams-header h2{color:#333;margin:0}.browse-all-btn{background:none;border:none;color:#4b7a3e;cursor:pointer;font-size:.9rem;font-weight:700}.exams-grid{grid-gap:1.5rem;display:grid;gap:1.5rem;grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}.exam-card{border-radius:8px;box-shadow:0 2px 4px #0000001a;cursor:pointer;overflow:hidden;position:relative;transition:transform .3s,box-shadow .3s}.exam-card:hover{box-shadow:0 5px 15px #0000001a;transform:translateY(-5px)}.new-badge{background-color:#4b7a3e;border-radius:4px;color:#fff;font-size:.8rem;font-weight:700;left:10px;padding:.25rem .5rem;position:absolute;top:10px}.exam-image{align-items:center;background-color:#eee;display:flex;height:150px;justify-content:center}.placeholder-image{background-color:#ddd;border-radius:50%;height:50px;width:50px}.exam-info{padding:1rem}.exam-name{color:#4b7a3e;font-size:1rem;margin:0 0 .5rem}.exam-course,.exam-date,.exam-description{color:#666;font-size:.9rem;margin:0 0 .25rem}.exam-rating{color:#f39c12;font-weight:700;margin-top:.5rem}.exchange-section{background-color:#fff;border-radius:8px;box-shadow:0 2px 4px #0000001a;padding:1.5rem}.exchange-section h2{color:#333;margin-bottom:1rem;margin-top:0}.exchange-form{display:flex;gap:1rem}.exchange-form input{border:1px solid #ddd;border-radius:4px;flex-grow:1;padding:.75rem}.send-btn{background-color:#4b7a3e;border:none;border-radius:4px;color:#fff;cursor:pointer;padding:0 1.5rem;transition:background-color .3s}.send-btn:hover{background-color:#3c6132}.grades-container{background-color:#f5f5f5;display:flex;flex-direction:column;min-height:100vh}.grades-content{padding:2rem}.grades-title{color:#333;margin-bottom:2rem}.grades-controls{display:flex;justify-content:space-between;margin-bottom:1.5rem}.grades-table-container{background-color:#fff;border-radius:8px;box-shadow:0 2px 4px #0000001a;overflow:hidden}.grades-table{border-collapse:collapse;width:100%}.grades-table td,.grades-table th{border-bottom:1px solid #eee;padding:1rem;text-align:left}.grades-table th{background-color:#f9f9f9;color:#333;font-weight:700}.grades-table tr:hover,.upload-container{background-color:#f5f5f5}.upload-container{display:flex;flex-direction:column;min-height:100vh}.upload-content{margin:0 auto;max-width:800px;padding:2rem}.upload-title{color:#333;margin-bottom:2rem;text-align:center}.upload-form{background-color:#fff;border-radius:8px;box-shadow:0 2px 4px #0000001a;padding:2rem}.form-group{margin-bottom:1.5rem}.form-group label{color:#333;display:block;font-weight:700;margin-bottom:.5rem}.form-group textarea{border:1px solid #ddd;border-radius:4px;font-family:inherit;font-size:1rem;padding:.75rem;resize:vertical;width:100%}.file-upload-area{display:flex;flex-direction:column;gap:1rem}.file-upload-box{align-items:center;border:2px dashed #ddd;border-radius:8px;cursor:pointer;display:flex;justify-content:center;min-height:150px;padding:2rem;position:relative;text-align:center;transition:border-color .3s}.file-upload-box:hover{border-color:#4b7a3e}.upload-placeholder{align-items:center;color:#666;display:flex;flex-direction:column}.upload-icon{font-size:3rem;margin-bottom:1rem}.upload-text{font-size:1rem}.file-input{cursor:pointer;height:100%;left:0;opacity:0;position:absolute;top:0;width:100%}.file-info{align-items:center;display:flex;flex-direction:column;width:100%}.file-icon{font-size:3rem;margin-bottom:.5rem}.file-name{font-weight:700;margin-bottom:.25rem;word-break:break-all}.file-size{color:#666;font-size:.9rem}.choose-file-btn{align-self:center;background-color:#4b7a3e;border:none;border-radius:4px;color:#fff;cursor:pointer;padding:.75rem 1.5rem;transition:background-color .3s}.choose-file-btn:hover{background-color:#3c6132}.submit-btn{background-color:#4b7a3e;border:none;border-radius:4px;color:#fff;cursor:pointer;font-size:1rem;margin-top:1rem;padding:.75rem 1.5rem;transition:background-color .3s;width:100%}.submit-btn:hover{background-color:#3c6132}.submit-btn:disabled{background-color:#a0c49d;cursor:not-allowed}.upload-error{background-color:#f8d7da;color:#721c24}.upload-error,.upload-success{border-radius:4px;margin-bottom:1.5rem;padding:.75rem;text-align:center}.upload-success{background-color:#d4edda;color:#155724}.requests-container{background-color:#f5f5f5;display:flex;flex-direction:column;min-height:100vh}.requests-content{padding:2rem}.requests-title{color:#333;margin-bottom:2rem}.requests-controls{display:flex;justify-content:space-between;margin-bottom:1.5rem}.search-box{position:relative;width:300px}.search-box input{border:1px solid #ddd;border-radius:4px;font-size:1rem;padding:.75rem 40px .75rem .75rem;width:100%}.search-icon{background:none;border:none;color:#666;cursor:pointer;font-size:1.2rem;position:absolute;right:10px;top:50%;transform:translateY(-50%)}.filters-dropdown{position:relative}.filters-btn{align-items:center;background-color:#fff;border:1px solid #ddd;border-radius:4px;cursor:pointer;display:flex;font-size:1rem;gap:.5rem;padding:.75rem 1.5rem}.dropdown-icon{font-size:.8rem}.requests-table-container{background-color:#fff;border-radius:8px;box-shadow:0 2px 4px #0000001a;overflow:hidden}.requests-table{border-collapse:collapse;width:100%}.requests-table td,.requests-table th{border-bottom:1px solid #eee;padding:1rem;text-align:left}.requests-table th{background-color:#f9f9f9;color:#333;font-weight:700}.requests-table tr:hover{background-color:#f5f5f5}.status-badge{border-radius:4px;display:inline-block;font-size:.8rem;font-weight:700;padding:.25rem .5rem}.status-badge.accepted{background-color:#d4edda;color:#155724}.status-badge.rejected{background-color:#f8d7da;color:#721c24}.status-badge.pending{background-color:#fff3cd;color:#856404}.actions-cell{display:flex;gap:.5rem}.accept-btn,.reject-btn{border:none;border-radius:4px;cursor:pointer;font-size:.8rem;padding:.5rem 1rem;transition:background-color .3s}.accept-btn{background-color:#4b7a3e;color:#fff}.accept-btn:hover{background-color:#3c6132}.reject-btn{background-color:#dc3545;color:#fff}.reject-btn:hover{background-color:#c82333}.accept-btn:disabled,.reject-btn:disabled{background-color:#ccc;cursor:not-allowed}.pagination-controls{align-items:center;background-color:#f9f9f9;border-top:1px solid #eee;display:flex;justify-content:space-between;padding:1rem}.rows-per-page{align-items:center;color:#666;display:flex;gap:.5rem}.rows-per-page select{border:1px solid #ddd;border-radius:4px;padding:.25rem}.page-info{color:#666}.page-navigation{display:flex;gap:.25rem}.page-nav-btn{background:none;border:none;color:#4b7a3e;cursor:pointer;font-size:1rem;padding:.5rem}.page-nav-btn:disabled{color:#ccc;cursor:not-allowed}.loading{color:#666;padding:2rem;text-align:center}
/*# sourceMappingURL=main.1ca9c762.css.map*/
\ No newline at end of file
{"version":3,"file":"static/css/main.1ca9c762.css","mappings":"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","sources":["index.css","styles/Auth.css","styles/Navigation.css","styles/Dashboard.css","styles/Grades.css","styles/UploadExam.css","styles/AdminRequests.css"],"sourcesContent":["body {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\n monospace;\n}\n",".auth-container {\n display: flex;\n flex-direction: column;\n min-height: 100vh;\n background-color: #f5f5f5;\n}\n\n.auth-header {\n background-color: #4b7a3e;\n color: white;\n padding: 1rem;\n display: flex;\n align-items: center;\n}\n\n.logo {\n font-size: 1.5rem;\n font-weight: bold;\n display: flex;\n align-items: center;\n}\n\n.at-symbol {\n margin-right: 0.5rem;\n}\n\n.auth-form-container {\n display: flex;\n justify-content: center;\n align-items: center;\n flex: 1;\n padding: 2rem;\n}\n\n.auth-form {\n background-color: #e8f5e4;\n padding: 2rem;\n border-radius: 8px;\n width: 100%;\n max-width: 400px;\n box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n}\n\n.auth-title {\n font-size: 1.5rem;\n margin-bottom: 0.5rem;\n color: #333;\n text-align: center;\n}\n\n.auth-subtitle {\n color: #666;\n margin-bottom: 1.5rem;\n text-align: center;\n}\n\n.form-group {\n margin-bottom: 1.5rem;\n}\n\n.form-group label {\n display: block;\n margin-bottom: 0.5rem;\n color: #333;\n}\n\n.input-with-icon {\n position: relative;\n display: flex;\n align-items: center;\n}\n\n.input-icon {\n position: absolute;\n left: 10px;\n color: #666;\n}\n\n.form-group input,\n.form-group select {\n width: 100%;\n padding: 0.75rem;\n padding-left: 2.5rem;\n border: 1px solid #ddd;\n border-radius: 4px;\n font-size: 1rem;\n}\n\n.password-toggle {\n position: absolute;\n right: 10px;\n background: none;\n border: none;\n cursor: pointer;\n color: #666;\n}\n\n.text-right {\n text-align: right;\n}\n\n.forgot-password {\n color: #4b7a3e;\n text-decoration: none;\n font-size: 0.9rem;\n}\n\n.auth-button {\n width: 100%;\n padding: 0.75rem;\n background-color: #4b7a3e;\n color: white;\n border: none;\n border-radius: 4px;\n font-size: 1rem;\n cursor: pointer;\n transition: background-color 0.3s;\n}\n\n.auth-button:hover {\n background-color: #3c6132;\n}\n\n.auth-button:disabled {\n background-color: #a0c49d;\n cursor: not-allowed;\n}\n\n.auth-footer {\n margin-top: 1.5rem;\n text-align: center;\n color: #666;\n}\n\n.auth-footer a {\n color: #4b7a3e;\n text-decoration: none;\n}\n\n.auth-error {\n background-color: #f8d7da;\n color: #721c24;\n padding: 0.75rem;\n margin-bottom: 1.5rem;\n border-radius: 4px;\n text-align: center;\n}\n\n/* Pending Approval Page */\n.pending-container {\n background-color: #e8f5e4;\n padding: 2rem;\n border-radius: 8px;\n width: 100%;\n max-width: 600px;\n box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n margin: 2rem auto;\n text-align: center;\n}\n\n.pending-title {\n color: #d35400;\n margin-bottom: 1.5rem;\n}\n\n.pending-message {\n color: #333;\n margin-bottom: 2rem;\n line-height: 1.6;\n}\n\n.pending-actions {\n display: flex;\n justify-content: center;\n}\n",".navigation-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n}\n\n.navigation-header {\n background-color: #4b7a3e;\n color: white;\n padding: 1rem;\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n\n.logo {\n font-size: 1.5rem;\n font-weight: bold;\n display: flex;\n align-items: center;\n}\n\n.at-symbol {\n margin-right: 0.5rem;\n}\n\n.search-bar {\n display: flex;\n align-items: center;\n background-color: white;\n border-radius: 20px;\n padding: 0.5rem;\n width: 40%;\n}\n\n.search-bar input {\n border: none;\n flex-grow: 1;\n padding: 0.25rem;\n outline: none;\n}\n\n.search-button {\n background: none;\n border: none;\n cursor: pointer;\n font-size: 1.2rem;\n}\n\n.user-profile {\n display: flex;\n align-items: center;\n}\n\n.user-name {\n margin-right: 1rem;\n text-transform: capitalize;\n}\n\n.profile-icon {\n position: relative;\n cursor: pointer;\n}\n\n.profile-icon img {\n width: 40px;\n height: 40px;\n border-radius: 50%;\n background-color: #ddd;\n}\n\n.profile-dropdown {\n position: absolute;\n top: 100%;\n right: 0;\n background-color: white;\n border-radius: 4px;\n box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n width: 150px;\n display: none;\n z-index: 10;\n}\n\n.profile-icon:hover .profile-dropdown {\n display: block;\n}\n\n.dropdown-item {\n padding: 0.75rem 1rem;\n color: #333;\n cursor: pointer;\n}\n\n.dropdown-item:hover {\n background-color: #f5f5f5;\n}\n\n.navigation-menu {\n display: flex;\n background-color: #e8f5e4;\n border-bottom: 1px solid #ddd;\n}\n\n.nav-link {\n padding: 1rem 1.5rem;\n color: #333;\n text-decoration: none;\n border-bottom: 3px solid transparent;\n transition: all 0.3s;\n}\n\n.nav-link:hover {\n background-color: #d4ebd0;\n}\n\n.nav-link.active {\n border-bottom-color: #4b7a3e;\n font-weight: bold;\n}\n",".dashboard-container {\n display: flex;\n flex-direction: column;\n min-height: 100vh;\n background-color: #f5f5f5;\n}\n\n.dashboard-content {\n padding: 2rem;\n}\n\n.dashboard-sections {\n display: flex;\n justify-content: space-between;\n margin-bottom: 2rem;\n gap: 1rem;\n}\n\n.dashboard-section {\n background-color: #fff;\n border-radius: 8px;\n padding: 1.5rem;\n flex: 1;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n}\n\n.dashboard-section h2 {\n margin-top: 0;\n margin-bottom: 1rem;\n color: #333;\n font-size: 1.2rem;\n}\n\n.section-content {\n display: flex;\n justify-content: center;\n}\n\n.section-card {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n background-color: #f9f9f9;\n border-radius: 8px;\n padding: 2rem;\n width: 100%;\n cursor: pointer;\n transition: transform 0.3s, box-shadow 0.3s;\n}\n\n.section-card:hover {\n transform: translateY(-5px);\n box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);\n}\n\n.card-icon {\n font-size: 2.5rem;\n margin-bottom: 1rem;\n}\n\n.card-title {\n font-weight: bold;\n color: #333;\n}\n\n.exams-section {\n background-color: #fff;\n border-radius: 8px;\n padding: 1.5rem;\n margin-bottom: 2rem;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n}\n\n.exams-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 1.5rem;\n}\n\n.exams-header h2 {\n margin: 0;\n color: #333;\n}\n\n.browse-all-btn {\n background: none;\n border: none;\n color: #4b7a3e;\n font-weight: bold;\n cursor: pointer;\n font-size: 0.9rem;\n}\n\n.exams-grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));\n gap: 1.5rem;\n}\n\n.exam-card {\n border-radius: 8px;\n overflow: hidden;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n cursor: pointer;\n transition: transform 0.3s, box-shadow 0.3s;\n position: relative;\n}\n\n.exam-card:hover {\n transform: translateY(-5px);\n box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);\n}\n\n.new-badge {\n position: absolute;\n top: 10px;\n left: 10px;\n background-color: #4b7a3e;\n color: white;\n padding: 0.25rem 0.5rem;\n border-radius: 4px;\n font-size: 0.8rem;\n font-weight: bold;\n}\n\n.exam-image {\n height: 150px;\n background-color: #eee;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.placeholder-image {\n width: 50px;\n height: 50px;\n border-radius: 50%;\n background-color: #ddd;\n}\n\n.exam-info {\n padding: 1rem;\n}\n\n.exam-name {\n margin: 0 0 0.5rem 0;\n font-size: 1rem;\n color: #4b7a3e;\n}\n\n.exam-description,\n.exam-date,\n.exam-course {\n margin: 0 0 0.25rem 0;\n font-size: 0.9rem;\n color: #666;\n}\n\n.exam-rating {\n margin-top: 0.5rem;\n font-weight: bold;\n color: #f39c12;\n}\n\n.exchange-section {\n background-color: #fff;\n border-radius: 8px;\n padding: 1.5rem;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n}\n\n.exchange-section h2 {\n margin-top: 0;\n margin-bottom: 1rem;\n color: #333;\n}\n\n.exchange-form {\n display: flex;\n gap: 1rem;\n}\n\n.exchange-form input {\n flex-grow: 1;\n padding: 0.75rem;\n border: 1px solid #ddd;\n border-radius: 4px;\n}\n\n.send-btn {\n background-color: #4b7a3e;\n color: white;\n border: none;\n border-radius: 4px;\n padding: 0 1.5rem;\n cursor: pointer;\n transition: background-color 0.3s;\n}\n\n.send-btn:hover {\n background-color: #3c6132;\n}\n\n.loading {\n text-align: center;\n padding: 2rem;\n color: #666;\n}\n",".grades-container {\n display: flex;\n flex-direction: column;\n min-height: 100vh;\n background-color: #f5f5f5;\n}\n\n.grades-content {\n padding: 2rem;\n}\n\n.grades-title {\n margin-bottom: 2rem;\n color: #333;\n}\n\n.grades-controls {\n display: flex;\n justify-content: space-between;\n margin-bottom: 1.5rem;\n}\n\n.search-box {\n position: relative;\n width: 300px;\n}\n\n.search-box input {\n width: 100%;\n padding: 0.75rem;\n padding-right: 40px;\n border: 1px solid #ddd;\n border-radius: 4px;\n font-size: 1rem;\n}\n\n.search-icon {\n position: absolute;\n right: 10px;\n top: 50%;\n transform: translateY(-50%);\n background: none;\n border: none;\n cursor: pointer;\n font-size: 1.2rem;\n color: #666;\n}\n\n.filters-dropdown {\n position: relative;\n}\n\n.filters-btn {\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 4px;\n padding: 0.75rem 1.5rem;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n font-size: 1rem;\n}\n\n.dropdown-icon {\n font-size: 0.8rem;\n}\n\n.grades-table-container {\n background-color: #fff;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n overflow: hidden;\n}\n\n.grades-table {\n width: 100%;\n border-collapse: collapse;\n}\n\n.grades-table th,\n.grades-table td {\n padding: 1rem;\n text-align: left;\n border-bottom: 1px solid #eee;\n}\n\n.grades-table th {\n background-color: #f9f9f9;\n font-weight: bold;\n color: #333;\n}\n\n.grades-table tr:hover {\n background-color: #f5f5f5;\n}\n\n.pagination-controls {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 1rem;\n background-color: #f9f9f9;\n border-top: 1px solid #eee;\n}\n\n.rows-per-page {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n color: #666;\n}\n\n.rows-per-page select {\n padding: 0.25rem;\n border: 1px solid #ddd;\n border-radius: 4px;\n}\n\n.page-info {\n color: #666;\n}\n\n.page-navigation {\n display: flex;\n gap: 0.25rem;\n}\n\n.page-nav-btn {\n background: none;\n border: none;\n cursor: pointer;\n padding: 0.5rem;\n font-size: 1rem;\n color: #4b7a3e;\n}\n\n.page-nav-btn:disabled {\n color: #ccc;\n cursor: not-allowed;\n}\n\n.loading {\n text-align: center;\n padding: 2rem;\n color: #666;\n}\n",".upload-container {\n display: flex;\n flex-direction: column;\n min-height: 100vh;\n background-color: #f5f5f5;\n}\n\n.upload-content {\n padding: 2rem;\n max-width: 800px;\n margin: 0 auto;\n}\n\n.upload-title {\n margin-bottom: 2rem;\n color: #333;\n text-align: center;\n}\n\n.upload-form {\n background-color: #fff;\n border-radius: 8px;\n padding: 2rem;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n}\n\n.form-group {\n margin-bottom: 1.5rem;\n}\n\n.form-group label {\n display: block;\n margin-bottom: 0.5rem;\n color: #333;\n font-weight: bold;\n}\n\n.form-group textarea {\n width: 100%;\n padding: 0.75rem;\n border: 1px solid #ddd;\n border-radius: 4px;\n resize: vertical;\n font-family: inherit;\n font-size: 1rem;\n}\n\n.file-upload-area {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n.file-upload-box {\n border: 2px dashed #ddd;\n border-radius: 8px;\n padding: 2rem;\n text-align: center;\n position: relative;\n cursor: pointer;\n transition: border-color 0.3s;\n min-height: 150px;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.file-upload-box:hover {\n border-color: #4b7a3e;\n}\n\n.upload-placeholder {\n display: flex;\n flex-direction: column;\n align-items: center;\n color: #666;\n}\n\n.upload-icon {\n font-size: 3rem;\n margin-bottom: 1rem;\n}\n\n.upload-text {\n font-size: 1rem;\n}\n\n.file-input {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n opacity: 0;\n cursor: pointer;\n}\n\n.file-info {\n display: flex;\n flex-direction: column;\n align-items: center;\n width: 100%;\n}\n\n.file-icon {\n font-size: 3rem;\n margin-bottom: 0.5rem;\n}\n\n.file-name {\n font-weight: bold;\n margin-bottom: 0.25rem;\n word-break: break-all;\n}\n\n.file-size {\n color: #666;\n font-size: 0.9rem;\n}\n\n.choose-file-btn {\n background-color: #4b7a3e;\n color: white;\n border: none;\n border-radius: 4px;\n padding: 0.75rem 1.5rem;\n cursor: pointer;\n transition: background-color 0.3s;\n align-self: center;\n}\n\n.choose-file-btn:hover {\n background-color: #3c6132;\n}\n\n.submit-btn {\n background-color: #4b7a3e;\n color: white;\n border: none;\n border-radius: 4px;\n padding: 0.75rem 1.5rem;\n cursor: pointer;\n transition: background-color 0.3s;\n font-size: 1rem;\n width: 100%;\n margin-top: 1rem;\n}\n\n.submit-btn:hover {\n background-color: #3c6132;\n}\n\n.submit-btn:disabled {\n background-color: #a0c49d;\n cursor: not-allowed;\n}\n\n.upload-error {\n background-color: #f8d7da;\n color: #721c24;\n padding: 0.75rem;\n margin-bottom: 1.5rem;\n border-radius: 4px;\n text-align: center;\n}\n\n.upload-success {\n background-color: #d4edda;\n color: #155724;\n padding: 0.75rem;\n margin-bottom: 1.5rem;\n border-radius: 4px;\n text-align: center;\n}\n",".requests-container {\n display: flex;\n flex-direction: column;\n min-height: 100vh;\n background-color: #f5f5f5;\n}\n\n.requests-content {\n padding: 2rem;\n}\n\n.requests-title {\n margin-bottom: 2rem;\n color: #333;\n}\n\n.requests-controls {\n display: flex;\n justify-content: space-between;\n margin-bottom: 1.5rem;\n}\n\n.search-box {\n position: relative;\n width: 300px;\n}\n\n.search-box input {\n width: 100%;\n padding: 0.75rem;\n padding-right: 40px;\n border: 1px solid #ddd;\n border-radius: 4px;\n font-size: 1rem;\n}\n\n.search-icon {\n position: absolute;\n right: 10px;\n top: 50%;\n transform: translateY(-50%);\n background: none;\n border: none;\n cursor: pointer;\n font-size: 1.2rem;\n color: #666;\n}\n\n.filters-dropdown {\n position: relative;\n}\n\n.filters-btn {\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 4px;\n padding: 0.75rem 1.5rem;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n font-size: 1rem;\n}\n\n.dropdown-icon {\n font-size: 0.8rem;\n}\n\n.requests-table-container {\n background-color: #fff;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n overflow: hidden;\n}\n\n.requests-table {\n width: 100%;\n border-collapse: collapse;\n}\n\n.requests-table th,\n.requests-table td {\n padding: 1rem;\n text-align: left;\n border-bottom: 1px solid #eee;\n}\n\n.requests-table th {\n background-color: #f9f9f9;\n font-weight: bold;\n color: #333;\n}\n\n.requests-table tr:hover {\n background-color: #f5f5f5;\n}\n\n.status-badge {\n display: inline-block;\n padding: 0.25rem 0.5rem;\n border-radius: 4px;\n font-size: 0.8rem;\n font-weight: bold;\n}\n\n.status-badge.accepted {\n background-color: #d4edda;\n color: #155724;\n}\n\n.status-badge.rejected {\n background-color: #f8d7da;\n color: #721c24;\n}\n\n.status-badge.pending {\n background-color: #fff3cd;\n color: #856404;\n}\n\n.actions-cell {\n display: flex;\n gap: 0.5rem;\n}\n\n.accept-btn,\n.reject-btn {\n padding: 0.5rem 1rem;\n border-radius: 4px;\n font-size: 0.8rem;\n cursor: pointer;\n border: none;\n transition: background-color 0.3s;\n}\n\n.accept-btn {\n background-color: #4b7a3e;\n color: white;\n}\n\n.accept-btn:hover {\n background-color: #3c6132;\n}\n\n.reject-btn {\n background-color: #dc3545;\n color: white;\n}\n\n.reject-btn:hover {\n background-color: #c82333;\n}\n\n.accept-btn:disabled,\n.reject-btn:disabled {\n background-color: #ccc;\n cursor: not-allowed;\n}\n\n.pagination-controls {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 1rem;\n background-color: #f9f9f9;\n border-top: 1px solid #eee;\n}\n\n.rows-per-page {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n color: #666;\n}\n\n.rows-per-page select {\n padding: 0.25rem;\n border: 1px solid #ddd;\n border-radius: 4px;\n}\n\n.page-info {\n color: #666;\n}\n\n.page-navigation {\n display: flex;\n gap: 0.25rem;\n}\n\n.page-nav-btn {\n background: none;\n border: none;\n cursor: pointer;\n padding: 0.5rem;\n font-size: 1rem;\n color: #4b7a3e;\n}\n\n.page-nav-btn:disabled {\n color: #ccc;\n cursor: not-allowed;\n}\n\n.loading {\n text-align: center;\n padding: 2rem;\n color: #666;\n}\n"],"names":[],"sourceRoot":""}
\ No newline at end of file
"use strict";(self.webpackChunkfrontend=self.webpackChunkfrontend||[]).push([[453],{453:(e,t,n)=>{n.r(t),n.d(t,{getCLS:()=>y,getFCP:()=>g,getFID:()=>C,getLCP:()=>P,getTTFB:()=>D});var i,r,a,o,u=function(e,t){return{name:e,value:void 0===t?-1:t,delta:0,entries:[],id:"v2-".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12)}},c=function(e,t){try{if(PerformanceObserver.supportedEntryTypes.includes(e)){if("first-input"===e&&!("PerformanceEventTiming"in self))return;var n=new PerformanceObserver((function(e){return e.getEntries().map(t)}));return n.observe({type:e,buffered:!0}),n}}catch(e){}},f=function(e,t){var n=function n(i){"pagehide"!==i.type&&"hidden"!==document.visibilityState||(e(i),t&&(removeEventListener("visibilitychange",n,!0),removeEventListener("pagehide",n,!0)))};addEventListener("visibilitychange",n,!0),addEventListener("pagehide",n,!0)},s=function(e){addEventListener("pageshow",(function(t){t.persisted&&e(t)}),!0)},m=function(e,t,n){var i;return function(r){t.value>=0&&(r||n)&&(t.delta=t.value-(i||0),(t.delta||void 0===i)&&(i=t.value,e(t)))}},v=-1,d=function(){return"hidden"===document.visibilityState?0:1/0},p=function(){f((function(e){var t=e.timeStamp;v=t}),!0)},l=function(){return v<0&&(v=d(),p(),s((function(){setTimeout((function(){v=d(),p()}),0)}))),{get firstHiddenTime(){return v}}},g=function(e,t){var n,i=l(),r=u("FCP"),a=function(e){"first-contentful-paint"===e.name&&(f&&f.disconnect(),e.startTime<i.firstHiddenTime&&(r.value=e.startTime,r.entries.push(e),n(!0)))},o=window.performance&&performance.getEntriesByName&&performance.getEntriesByName("first-contentful-paint")[0],f=o?null:c("paint",a);(o||f)&&(n=m(e,r,t),o&&a(o),s((function(i){r=u("FCP"),n=m(e,r,t),requestAnimationFrame((function(){requestAnimationFrame((function(){r.value=performance.now()-i.timeStamp,n(!0)}))}))})))},h=!1,T=-1,y=function(e,t){h||(g((function(e){T=e.value})),h=!0);var n,i=function(t){T>-1&&e(t)},r=u("CLS",0),a=0,o=[],v=function(e){if(!e.hadRecentInput){var t=o[0],i=o[o.length-1];a&&e.startTime-i.startTime<1e3&&e.startTime-t.startTime<5e3?(a+=e.value,o.push(e)):(a=e.value,o=[e]),a>r.value&&(r.value=a,r.entries=o,n())}},d=c("layout-shift",v);d&&(n=m(i,r,t),f((function(){d.takeRecords().map(v),n(!0)})),s((function(){a=0,T=-1,r=u("CLS",0),n=m(i,r,t)})))},E={passive:!0,capture:!0},w=new Date,L=function(e,t){i||(i=t,r=e,a=new Date,F(removeEventListener),S())},S=function(){if(r>=0&&r<a-w){var e={entryType:"first-input",name:i.type,target:i.target,cancelable:i.cancelable,startTime:i.timeStamp,processingStart:i.timeStamp+r};o.forEach((function(t){t(e)})),o=[]}},b=function(e){if(e.cancelable){var t=(e.timeStamp>1e12?new Date:performance.now())-e.timeStamp;"pointerdown"==e.type?function(e,t){var n=function(){L(e,t),r()},i=function(){r()},r=function(){removeEventListener("pointerup",n,E),removeEventListener("pointercancel",i,E)};addEventListener("pointerup",n,E),addEventListener("pointercancel",i,E)}(t,e):L(t,e)}},F=function(e){["mousedown","keydown","touchstart","pointerdown"].forEach((function(t){return e(t,b,E)}))},C=function(e,t){var n,a=l(),v=u("FID"),d=function(e){e.startTime<a.firstHiddenTime&&(v.value=e.processingStart-e.startTime,v.entries.push(e),n(!0))},p=c("first-input",d);n=m(e,v,t),p&&f((function(){p.takeRecords().map(d),p.disconnect()}),!0),p&&s((function(){var a;v=u("FID"),n=m(e,v,t),o=[],r=-1,i=null,F(addEventListener),a=d,o.push(a),S()}))},k={},P=function(e,t){var n,i=l(),r=u("LCP"),a=function(e){var t=e.startTime;t<i.firstHiddenTime&&(r.value=t,r.entries.push(e),n())},o=c("largest-contentful-paint",a);if(o){n=m(e,r,t);var v=function(){k[r.id]||(o.takeRecords().map(a),o.disconnect(),k[r.id]=!0,n(!0))};["keydown","click"].forEach((function(e){addEventListener(e,v,{once:!0,capture:!0})})),f(v,!0),s((function(i){r=u("LCP"),n=m(e,r,t),requestAnimationFrame((function(){requestAnimationFrame((function(){r.value=performance.now()-i.timeStamp,k[r.id]=!0,n(!0)}))}))}))}},D=function(e){var t,n=u("TTFB");t=function(){try{var t=performance.getEntriesByType("navigation")[0]||function(){var e=performance.timing,t={entryType:"navigation",startTime:0};for(var n in e)"navigationStart"!==n&&"toJSON"!==n&&(t[n]=Math.max(e[n]-e.navigationStart,0));return t}();if(n.value=n.delta=t.responseStart,n.value<0||n.value>performance.now())return;n.entries=[t],e(n)}catch(e){}},"complete"===document.readyState?setTimeout(t,0):addEventListener("load",(function(){return setTimeout(t,0)}))}}}]);
//# sourceMappingURL=453.8ab44547.chunk.js.map
\ No newline at end of file
{"version":3,"file":"static/js/453.8ab44547.chunk.js","mappings":"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","sources":["../node_modules/web-vitals/dist/web-vitals.js"],"sourcesContent":["var e,t,n,i,r=function(e,t){return{name:e,value:void 0===t?-1:t,delta:0,entries:[],id:\"v2-\".concat(Date.now(),\"-\").concat(Math.floor(8999999999999*Math.random())+1e12)}},a=function(e,t){try{if(PerformanceObserver.supportedEntryTypes.includes(e)){if(\"first-input\"===e&&!(\"PerformanceEventTiming\"in self))return;var n=new PerformanceObserver((function(e){return e.getEntries().map(t)}));return n.observe({type:e,buffered:!0}),n}}catch(e){}},o=function(e,t){var n=function n(i){\"pagehide\"!==i.type&&\"hidden\"!==document.visibilityState||(e(i),t&&(removeEventListener(\"visibilitychange\",n,!0),removeEventListener(\"pagehide\",n,!0)))};addEventListener(\"visibilitychange\",n,!0),addEventListener(\"pagehide\",n,!0)},u=function(e){addEventListener(\"pageshow\",(function(t){t.persisted&&e(t)}),!0)},c=function(e,t,n){var i;return function(r){t.value>=0&&(r||n)&&(t.delta=t.value-(i||0),(t.delta||void 0===i)&&(i=t.value,e(t)))}},f=-1,s=function(){return\"hidden\"===document.visibilityState?0:1/0},m=function(){o((function(e){var t=e.timeStamp;f=t}),!0)},v=function(){return f<0&&(f=s(),m(),u((function(){setTimeout((function(){f=s(),m()}),0)}))),{get firstHiddenTime(){return f}}},d=function(e,t){var n,i=v(),o=r(\"FCP\"),f=function(e){\"first-contentful-paint\"===e.name&&(m&&m.disconnect(),e.startTime<i.firstHiddenTime&&(o.value=e.startTime,o.entries.push(e),n(!0)))},s=window.performance&&performance.getEntriesByName&&performance.getEntriesByName(\"first-contentful-paint\")[0],m=s?null:a(\"paint\",f);(s||m)&&(n=c(e,o,t),s&&f(s),u((function(i){o=r(\"FCP\"),n=c(e,o,t),requestAnimationFrame((function(){requestAnimationFrame((function(){o.value=performance.now()-i.timeStamp,n(!0)}))}))})))},p=!1,l=-1,h=function(e,t){p||(d((function(e){l=e.value})),p=!0);var n,i=function(t){l>-1&&e(t)},f=r(\"CLS\",0),s=0,m=[],v=function(e){if(!e.hadRecentInput){var t=m[0],i=m[m.length-1];s&&e.startTime-i.startTime<1e3&&e.startTime-t.startTime<5e3?(s+=e.value,m.push(e)):(s=e.value,m=[e]),s>f.value&&(f.value=s,f.entries=m,n())}},h=a(\"layout-shift\",v);h&&(n=c(i,f,t),o((function(){h.takeRecords().map(v),n(!0)})),u((function(){s=0,l=-1,f=r(\"CLS\",0),n=c(i,f,t)})))},T={passive:!0,capture:!0},y=new Date,g=function(i,r){e||(e=r,t=i,n=new Date,w(removeEventListener),E())},E=function(){if(t>=0&&t<n-y){var r={entryType:\"first-input\",name:e.type,target:e.target,cancelable:e.cancelable,startTime:e.timeStamp,processingStart:e.timeStamp+t};i.forEach((function(e){e(r)})),i=[]}},S=function(e){if(e.cancelable){var t=(e.timeStamp>1e12?new Date:performance.now())-e.timeStamp;\"pointerdown\"==e.type?function(e,t){var n=function(){g(e,t),r()},i=function(){r()},r=function(){removeEventListener(\"pointerup\",n,T),removeEventListener(\"pointercancel\",i,T)};addEventListener(\"pointerup\",n,T),addEventListener(\"pointercancel\",i,T)}(t,e):g(t,e)}},w=function(e){[\"mousedown\",\"keydown\",\"touchstart\",\"pointerdown\"].forEach((function(t){return e(t,S,T)}))},L=function(n,f){var s,m=v(),d=r(\"FID\"),p=function(e){e.startTime<m.firstHiddenTime&&(d.value=e.processingStart-e.startTime,d.entries.push(e),s(!0))},l=a(\"first-input\",p);s=c(n,d,f),l&&o((function(){l.takeRecords().map(p),l.disconnect()}),!0),l&&u((function(){var a;d=r(\"FID\"),s=c(n,d,f),i=[],t=-1,e=null,w(addEventListener),a=p,i.push(a),E()}))},b={},F=function(e,t){var n,i=v(),f=r(\"LCP\"),s=function(e){var t=e.startTime;t<i.firstHiddenTime&&(f.value=t,f.entries.push(e),n())},m=a(\"largest-contentful-paint\",s);if(m){n=c(e,f,t);var d=function(){b[f.id]||(m.takeRecords().map(s),m.disconnect(),b[f.id]=!0,n(!0))};[\"keydown\",\"click\"].forEach((function(e){addEventListener(e,d,{once:!0,capture:!0})})),o(d,!0),u((function(i){f=r(\"LCP\"),n=c(e,f,t),requestAnimationFrame((function(){requestAnimationFrame((function(){f.value=performance.now()-i.timeStamp,b[f.id]=!0,n(!0)}))}))}))}},P=function(e){var t,n=r(\"TTFB\");t=function(){try{var t=performance.getEntriesByType(\"navigation\")[0]||function(){var e=performance.timing,t={entryType:\"navigation\",startTime:0};for(var n in e)\"navigationStart\"!==n&&\"toJSON\"!==n&&(t[n]=Math.max(e[n]-e.navigationStart,0));return t}();if(n.value=n.delta=t.responseStart,n.value<0||n.value>performance.now())return;n.entries=[t],e(n)}catch(e){}},\"complete\"===document.readyState?setTimeout(t,0):addEventListener(\"load\",(function(){return setTimeout(t,0)}))};export{h as getCLS,d as getFCP,L as getFID,F as getLCP,P as getTTFB};\n"],"names":["e","t","n","i","r","name","value","delta","entries","id","concat","Date","now","Math","floor","random","a","PerformanceObserver","supportedEntryTypes","includes","self","getEntries","map","observe","type","buffered","o","document","visibilityState","removeEventListener","addEventListener","u","persisted","c","f","s","m","timeStamp","v","setTimeout","firstHiddenTime","d","disconnect","startTime","push","window","performance","getEntriesByName","requestAnimationFrame","p","l","h","hadRecentInput","length","takeRecords","T","passive","capture","y","g","w","E","entryType","target","cancelable","processingStart","forEach","S","L","b","F","once","P","getEntriesByType","timing","max","navigationStart","responseStart","readyState"],"sourceRoot":""}
\ No newline at end of file
This diff could not be displayed because it is too large.
/**
* @license React
* react-dom-client.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-dom.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-jsx-runtime.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
This diff could not be displayed because it is too large.
...@@ -12,13 +12,10 @@ const AdminHome = () => { ...@@ -12,13 +12,10 @@ const AdminHome = () => {
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => { useEffect(() => {
// Fetch exams data and pending requests
const fetchData = async () => { const fetchData = async () => {
try { try {
setIsLoading(true); setIsLoading(true);
// This would be replaced with actual API calls
// const examsResponse = await axios.get('http://localhost:8000/exams/');
// const requestsResponse = await axios.get('http://localhost:8000/admin/requests/pending/count');
// Mock data for now // Mock data for now
const mockExams = [ const mockExams = [
...@@ -70,7 +67,7 @@ const AdminHome = () => { ...@@ -70,7 +67,7 @@ const AdminHome = () => {
]; ];
setExams(mockExams); setExams(mockExams);
setPendingRequests(2); // Mock pending requests count setPendingRequests(2);
} catch (error) { } catch (error) {
console.error('Error fetching data:', error); console.error('Error fetching data:', error);
} finally { } finally {
......
...@@ -10,21 +10,23 @@ const AdminRequests = () => { ...@@ -10,21 +10,23 @@ const AdminRequests = () => {
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [rowsPerPage, setRowsPerPage] = useState(10); const [rowsPerPage, setRowsPerPage] = useState(10);
const [totalRows, setTotalRows] = useState(0);
const [error, setError] = useState(''); const [error, setError] = useState('');
const { user } = useAuth();
useEffect(() => { useEffect(() => {
// Fetch requests data
const fetchRequests = async () => { const fetchRequests = async () => {
try { try {
setIsLoading(true); setIsLoading(true);
setError(''); setError('');
const requestsData = await adminService.getAllRequests(); const requestsData = await adminService.getAllRequests();
setRequests(requestsData); if (Array.isArray(requestsData)) {
setTotalRows(requestsData.length); setRequests(requestsData);
} else {
setRequests([]);
console.error('Error fetching requests: Data is not an array', requestsData);
setError('Failed to load requests. Unexpected data format.');
}
} catch (error) { } catch (error) {
console.error('Error fetching requests:', error); console.error('Error fetching requests:', error);
setError('Failed to load requests. Please try again later.'); setError('Failed to load requests. Please try again later.');
...@@ -42,7 +44,7 @@ const AdminRequests = () => { ...@@ -42,7 +44,7 @@ const AdminRequests = () => {
}; };
const handleRowsPerPageChange = (e) => { const handleRowsPerPageChange = (e) => {
setRowsPerPage(parseInt(e.target.value)); setRowsPerPage(parseInt(e.target.value, 10));
setCurrentPage(1); setCurrentPage(1);
}; };
...@@ -54,13 +56,13 @@ const AdminRequests = () => { ...@@ -54,13 +56,13 @@ const AdminRequests = () => {
try { try {
setError(''); setError('');
await adminService.approveRequest(requestId); await adminService.approveRequest(requestId);
setRequests(prevRequests =>
// Update local state prevRequests.map(request =>
setRequests(requests.map(request => request.id === requestId
request.request_id === requestId ? { ...request, status: 'accepted' }
? { ...request, status: 'Accepted' } : request
: request )
)); );
} catch (error) { } catch (error) {
console.error('Error accepting request:', error); console.error('Error accepting request:', error);
setError('Failed to approve request. Please try again.'); setError('Failed to approve request. Please try again.');
...@@ -71,96 +73,112 @@ const AdminRequests = () => { ...@@ -71,96 +73,112 @@ const AdminRequests = () => {
try { try {
setError(''); setError('');
await adminService.rejectRequest(requestId); await adminService.rejectRequest(requestId);
setRequests(prevRequests =>
// Update local state prevRequests.map(request =>
setRequests(requests.map(request => request.id === requestId
request.request_id === requestId ? { ...request, status: 'rejected' }
? { ...request, status: 'Rejected' } : request
: request )
)); );
} catch (error) { } catch (error) {
console.error('Error rejecting request:', error); console.error('Error rejecting request:', error);
setError('Failed to reject request. Please try again.'); setError('Failed to reject request. Please try again.');
} }
}; };
// Filter requests based on search term const filteredRequests = requests.filter(request => {
const filteredRequests = requests.filter(request => const searchLower = searchTerm.toLowerCase();
request.student.toLowerCase().includes(searchTerm.toLowerCase()) || // Ensure request and its properties are defined before calling toLowerCase or String()
request.email.toLowerCase().includes(searchTerm.toLowerCase()) || const requestIdStr = String(request.id || '').toLowerCase();
request.request_id.toLowerCase().includes(searchTerm.toLowerCase()) const userIdStr = String(request.user_id || '').toLowerCase();
); const statusStr = String(request.status || '').toLowerCase();
const requestDateStr = request.request_date ? new Date(request.request_date).toLocaleDateString().toLowerCase() : '';
return (
requestIdStr.includes(searchLower) ||
userIdStr.includes(searchLower) ||
statusStr.includes(searchLower) ||
requestDateStr.includes(searchLower)
);
});
// Calculate pagination
const totalPages = Math.ceil(filteredRequests.length / rowsPerPage); const totalPages = Math.ceil(filteredRequests.length / rowsPerPage);
const startIndex = (currentPage - 1) * rowsPerPage; const startIndex = (currentPage - 1) * rowsPerPage;
const paginatedRequests = filteredRequests.slice(startIndex, startIndex + rowsPerPage); const paginatedRequests = filteredRequests.slice(startIndex, startIndex + rowsPerPage);
if (error) {
return (
<div className="requests-container">
<Navigation activePage="requests" />
<div className="requests-content">
<h1 className="requests-title">Requests</h1>
<div className="error-message">{error}</div>
</div>
</div>
);
}
return ( return (
<div className="requests-container"> <div className="requests-container">
<Navigation activePage="requests" /> <Navigation activePage="requests" />
<div className="requests-content"> <div className="requests-content">
<h1 className="requests-title">Requests</h1> <h1 className="requests-title">Requests</h1>
<div className="requests-controls"> <div className="requests-controls">
<div className="search-box"> <div className="search-box">
<input <input
type="text" type="text"
placeholder="Search..." placeholder="Search by Request ID, User ID, Status, or Date"
value={searchTerm} value={searchTerm}
onChange={handleSearch} onChange={handleSearch}
/> />
<button className="search-icon">🔍</button> <button type="button" className="search-icon">🔍</button>
</div>
<div className="filters-dropdown">
<button className="filters-btn">
Filters <span className="dropdown-icon"></span>
</button>
</div> </div>
</div> </div>
<div className="requests-table-container"> <div className="requests-table-container">
{isLoading ? ( {isLoading ? (
<div className="loading">Loading requests...</div> <div className="loading">Loading requests...</div>
) : paginatedRequests.length === 0 && !searchTerm ? (
<div className="no-requests">No pending requests found.</div>
) : paginatedRequests.length === 0 && searchTerm ? (
<div className="no-requests">No requests match your search.</div>
) : ( ) : (
<> <>
<table className="requests-table"> <table className="requests-table">
<thead> <thead>
<tr> <tr>
<th>Request ID</th> <th>Request ID</th>
<th>Student</th> <th>User ID</th>
<th>Email</th>
<th>Request Date</th> <th>Request Date</th>
<th>Status</th> <th>Status</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{paginatedRequests.map((request, index) => ( {paginatedRequests.map((request) => (
<tr key={index}> <tr key={request.id}>
<td>{request.request_id}</td> <td>{request.id}</td>
<td>{request.student}</td> <td>{request.user_id}</td>
<td>{request.email}</td> <td>{request.request_date ? new Date(request.request_date).toLocaleDateString() : 'N/A'}</td>
<td>{request.request_date}</td>
<td> <td>
<span className={`status-badge ${request.status.toLowerCase()}`}> <span className={`status-badge ${String(request.status || '').toLowerCase()}`}>
{request.status} {request.status || 'N/A'}
</span> </span>
</td> </td>
<td className="actions-cell"> <td className="actions-cell">
<button <button
type="button"
className="accept-btn" className="accept-btn"
onClick={() => handleAccept(request.request_id)} onClick={() => handleAccept(request.id)}
disabled={request.status !== 'Pending'} disabled={String(request.status || '').toLowerCase() !== 'pending'}
> >
Accept Accept
</button> </button>
<button <button
type="button"
className="reject-btn" className="reject-btn"
onClick={() => handleReject(request.request_id)} onClick={() => handleReject(request.id)}
disabled={request.status !== 'Pending'} disabled={String(request.status || '').toLowerCase() !== 'pending'}
> >
Reject Reject
</button> </button>
...@@ -169,7 +187,7 @@ const AdminRequests = () => { ...@@ -169,7 +187,7 @@ const AdminRequests = () => {
))} ))}
</tbody> </tbody>
</table> </table>
<div className="pagination-controls"> <div className="pagination-controls">
<div className="rows-per-page"> <div className="rows-per-page">
Rows per page: Rows per page:
...@@ -180,36 +198,38 @@ const AdminRequests = () => { ...@@ -180,36 +198,38 @@ const AdminRequests = () => {
<option value={100}>100</option> <option value={100}>100</option>
</select> </select>
</div> </div>
<div className="page-info"> <div className="page-info">
{startIndex + 1}-{Math.min(startIndex + rowsPerPage, filteredRequests.length)} of {filteredRequests.length} {filteredRequests.length > 0 ? `${startIndex + 1}-${Math.min(startIndex + rowsPerPage, filteredRequests.length)} of ${filteredRequests.length}` : '0 of 0'}
</div> </div>
<div className="page-navigation"> <div className="page-navigation">
<button <button
type="button"
className="page-nav-btn" className="page-nav-btn"
disabled={currentPage === 1} disabled={currentPage === 1 || totalPages === 0}
onClick={() => handlePageChange(1)} onClick={() => handlePageChange(1)}
> >
</button> </button>
<button <button
type="button"
className="page-nav-btn" className="page-nav-btn"
disabled={currentPage === 1} disabled={currentPage === 1 || totalPages === 0}
onClick={() => handlePageChange(currentPage - 1)} onClick={() => handlePageChange(currentPage - 1)}
> >
</button> </button>
<button <button
type="button"
className="page-nav-btn" className="page-nav-btn"
disabled={currentPage === totalPages} disabled={currentPage === totalPages || totalPages === 0}
onClick={() => handlePageChange(currentPage + 1)} onClick={() => handlePageChange(currentPage + 1)}
> >
</button> </button>
<button <button
type="button"
className="page-nav-btn" className="page-nav-btn"
disabled={currentPage === totalPages} disabled={currentPage === totalPages || totalPages === 0}
onClick={() => handlePageChange(totalPages)} onClick={() => handlePageChange(totalPages)}
> >
...@@ -225,3 +245,4 @@ const AdminRequests = () => { ...@@ -225,3 +245,4 @@ const AdminRequests = () => {
}; };
export default AdminRequests; export default AdminRequests;
...@@ -13,6 +13,23 @@ const Login = () => { ...@@ -13,6 +13,23 @@ const Login = () => {
const { login, user } = useAuth(); const { login, user } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
React.useEffect(() => {
if (user) { // Check if the user state in context is updated
const userRole = user.role || '';
console.log("DEBUG: User context updated, role:", userRole);
if (userRole === 'student') {
navigate('/student');
} else if (userRole === 'professor') {
navigate('/professor');
} else if (userRole === 'admin') {
navigate('/admin');
} else {
console.error("DEBUG: Unknown user role for navigation:", userRole);
}
}
}, [user, navigate]);
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
setError(''); setError('');
......
...@@ -15,7 +15,7 @@ const PendingApproval = () => { ...@@ -15,7 +15,7 @@ const PendingApproval = () => {
<h1 className="pending-title">Account Pending Approval</h1> <h1 className="pending-title">Account Pending Approval</h1>
<p className="pending-message"> <p className="pending-message">
Your account registration has been submitted and is waiting for administrator approval. Your account registration has been submitted and is waiting for administrator approval.
You will be notified once your account has been approved. check later.
</p> </p>
<div className="pending-actions"> <div className="pending-actions">
<Link to="/login" className="auth-button">Back to Login</Link> <Link to="/login" className="auth-button">Back to Login</Link>
......
...@@ -2,7 +2,7 @@ import axios from 'axios'; ...@@ -2,7 +2,7 @@ import axios from 'axios';
// Create axios instance with base URL // Create axios instance with base URL
const api = axios.create({ const api = axios.create({
baseURL: 'http://localhost:8000', baseURL: 'http://10.82.52.184:8000',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
......
...@@ -101,29 +101,32 @@ export const gradeService = { ...@@ -101,29 +101,32 @@ export const gradeService = {
export const adminService = { export const adminService = {
// Get all user registration requests // Get all user registration requests
getAllRequests: async (params = {}) => { getAllRequests: async (params = {}) => {
const response = await api.get('/admin/requests', { params }); // Change path from '/admin/requests' to '/requests/'
const response = await api.get('/requests/', { params });
return response.data; return response.data;
}, },
// Approve user registration request // Approve user registration request
approveRequest: async (requestId) => { approveRequest: async (requestId) => {
const response = await api.post(`/admin/requests/${requestId}/approve`); // Change path, method (POST to PUT), and send status in body
const response = await api.put(`/requests/${requestId}`, { status: 'accepted' });
return response.data; return response.data;
}, },
// Reject user registration request // Reject user registration request
rejectRequest: async (requestId) => { rejectRequest: async (requestId) => {
const response = await api.post(`/admin/requests/${requestId}/reject`); // Change path, method (POST to PUT), and send status in body
const response = await api.put(`/requests/${requestId}`, { status: 'rejected' });
return response.data; return response.data;
}, },
// Get all users // Get all users (Assuming this matches backend, verify if needed)
getAllUsers: async (params = {}) => { getAllUsers: async (params = {}) => {
const response = await api.get('/admin/users', { params }); const response = await api.get('/admin/users', { params });
return response.data; return response.data;
}, },
// Update user role // Update user role (Assuming this matches backend, verify if needed)
updateUserRole: async (userId, role) => { updateUserRole: async (userId, role) => {
const response = await api.put(`/admin/users/${userId}/role`, { role }); const response = await api.put(`/admin/users/${userId}/role`, { role });
return response.data; return response.data;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment