diff --git a/backend/app/auth.py b/backend/app/auth.py
new file mode 100644
index 0000000..e3d4112
--- /dev/null
+++ b/backend/app/auth.py
@@ -0,0 +1,93 @@
+from datetime import datetime, timedelta
+from typing import Optional
+from jose import JWTError, jwt
+from passlib.context import CryptContext
+from fastapi import Depends, HTTPException, status, Header
+from sqlalchemy.orm import Session
+from .database import get_db
+from .models import User
+
+# Configuration
+SECRET_KEY = "your-secret-key-change-in-production" # Change this in production!
+ALGORITHM = "HS256"
+ACCESS_TOKEN_EXPIRE_MINUTES = 720
+
+pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
+
+
+def hash_password(password: str) -> str:
+ """Hash a password using bcrypt"""
+ return pwd_context.hash(password)
+
+
+def verify_password(plain_password: str, hashed_password: str) -> bool:
+ """Verify a password against its hash"""
+ return pwd_context.verify(plain_password, hashed_password)
+
+
+def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
+ """Create a JWT access token"""
+ to_encode = data.copy()
+ if expires_delta:
+ expire = datetime.utcnow() + expires_delta
+ else:
+ expire = datetime.utcnow() + timedelta(minutes=15)
+ to_encode.update({"exp": expire})
+ encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
+ return encoded_jwt
+
+
+def get_current_user(authorization: Optional[str] = Header(None), db: Session = Depends(get_db)) -> User:
+ """Get the current authenticated user from JWT token"""
+ if not authorization:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Missing authorization header",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
+ try:
+ scheme, credentials = authorization.split()
+ if scheme.lower() != "bearer":
+ raise ValueError("Invalid auth scheme")
+ except (ValueError, AttributeError):
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid authorization header format",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
+ try:
+ payload = jwt.decode(credentials, SECRET_KEY, algorithms=[ALGORITHM])
+ username: str = payload.get("sub")
+ if username is None:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid authentication credentials",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+ except JWTError:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid authentication credentials",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
+ user = db.query(User).filter(User.username == username).first()
+ if user is None:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid authentication credentials",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+ return user
+
+
+def get_current_admin_user(current_user: User = Depends(get_current_user)) -> User:
+ """Get the current user and verify they are an admin"""
+ if not current_user.is_admin:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Not enough permissions. Admin access required."
+ )
+ return current_user
diff --git a/backend/app/main.py b/backend/app/main.py
index 95bbc2b..03aee8a 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -1,10 +1,11 @@
-from fastapi import FastAPI, Depends, HTTPException, APIRouter
+from fastapi import FastAPI, Depends, HTTPException, APIRouter, status
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
from typing import List, Optional
-from datetime import datetime
+from datetime import datetime, timedelta
from .database import engine, get_db, Base
-from .models import Drug, DrugVariant, Dispensing
+from .models import Drug, DrugVariant, Dispensing, User
+from .auth import hash_password, verify_password, create_access_token, get_current_user, get_current_admin_user, ACCESS_TOKEN_EXPIRE_MINUTES
from pydantic import BaseModel
# Create tables
@@ -25,18 +26,42 @@ app.add_middleware(
router = APIRouter(prefix="/api")
# Pydantic schemas
+class UserCreate(BaseModel):
+ username: str
+ password: str
+
+class PasswordChange(BaseModel):
+ current_password: str
+ new_password: str
+
+class AdminPasswordChange(BaseModel):
+ new_password: str
+
+class UserResponse(BaseModel):
+ id: int
+ username: str
+ is_admin: bool
+
+ class Config:
+ from_attributes = True
+
+class TokenResponse(BaseModel):
+ access_token: str
+ token_type: str
+ user: UserResponse
+
class DrugCreate(BaseModel):
name: str
- description: str = None
+ description: Optional[str] = None
class DrugUpdate(BaseModel):
- name: str = None
- description: str = None
+ name: Optional[str] = None
+ description: Optional[str] = None
class DrugResponse(BaseModel):
id: int
name: str
- description: str = None
+ description: Optional[str] = None
class Config:
from_attributes = True
@@ -67,7 +92,7 @@ class DrugVariantResponse(BaseModel):
class DrugWithVariantsResponse(BaseModel):
id: int
name: str
- description: str = None
+ description: Optional[str] = None
variants: List[DrugVariantResponse] = []
class Config:
@@ -92,13 +117,153 @@ class DispensingResponse(BaseModel):
class Config:
from_attributes = True
+# Authentication Routes
+@router.post("/auth/register", response_model=TokenResponse)
+def register(user_data: UserCreate, db: Session = Depends(get_db)):
+ """Register the first admin user (only allowed if no users exist)"""
+ # Check if users already exist
+ user_count = db.query(User).count()
+ if user_count > 0:
+ raise HTTPException(
+ status_code=403,
+ detail="Registration is disabled. Contact an administrator to create an account."
+ )
+
+ # Check if user already exists
+ existing_user = db.query(User).filter(User.username == user_data.username).first()
+ if existing_user:
+ raise HTTPException(status_code=400, detail="Username already registered")
+
+ # First (and only allowed) user is admin
+ hashed_password = hash_password(user_data.password)
+ db_user = User(
+ username=user_data.username,
+ hashed_password=hashed_password,
+ is_admin=True
+ )
+ db.add(db_user)
+ db.commit()
+ db.refresh(db_user)
+
+ # Create access token
+ access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
+ access_token = create_access_token(
+ data={"sub": db_user.username},
+ expires_delta=access_token_expires
+ )
+
+ return {
+ "access_token": access_token,
+ "token_type": "bearer",
+ "user": db_user
+ }
+
+@router.post("/auth/login", response_model=TokenResponse)
+def login(user_data: UserCreate, db: Session = Depends(get_db)):
+ """Login with username and password"""
+ user = db.query(User).filter(User.username == user_data.username).first()
+ if not user or not verify_password(user_data.password, user.hashed_password):
+ raise HTTPException(status_code=401, detail="Invalid credentials")
+
+ # Create access token
+ access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
+ access_token = create_access_token(
+ data={"sub": user.username},
+ expires_delta=access_token_expires
+ )
+
+ return {
+ "access_token": access_token,
+ "token_type": "bearer",
+ "user": user
+ }
+
+@router.get("/auth/me", response_model=UserResponse)
+def get_current_user_info(current_user: User = Depends(get_current_user)):
+ """Get current user info"""
+ return current_user
+
+# User Management Routes (Admin only)
+@router.get("/users", response_model=List[UserResponse])
+def list_users(db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_user)):
+ """List all users (admin only)"""
+ return db.query(User).all()
+
+@router.post("/users", response_model=UserResponse)
+def create_user(user_data: UserCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_user)):
+ """Create a new user (admin only)"""
+ # Check if user already exists
+ existing_user = db.query(User).filter(User.username == user_data.username).first()
+ if existing_user:
+ raise HTTPException(status_code=400, detail="Username already exists")
+
+ hashed_password = hash_password(user_data.password)
+ db_user = User(
+ username=user_data.username,
+ hashed_password=hashed_password,
+ is_admin=False
+ )
+ db.add(db_user)
+ db.commit()
+ db.refresh(db_user)
+ return db_user
+
+@router.delete("/users/{user_id}")
+def delete_user(user_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_user)):
+ """Delete a user (admin only)"""
+ # Don't allow deleting yourself
+ if current_user.id == user_id:
+ raise HTTPException(status_code=400, detail="Cannot delete your own user account")
+
+ user = db.query(User).filter(User.id == user_id).first()
+ if not user:
+ raise HTTPException(status_code=404, detail="User not found")
+
+ db.delete(user)
+ db.commit()
+ return {"message": "User deleted successfully"}
+
+@router.post("/auth/change-password")
+def change_own_password(password_data: PasswordChange, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
+ """Change current user's password"""
+ user = db.query(User).filter(User.id == current_user.id).first()
+ if not user:
+ raise HTTPException(status_code=404, detail="User not found")
+
+ # Verify current password
+ if not verify_password(password_data.current_password, user.hashed_password):
+ raise HTTPException(status_code=401, detail="Current password is incorrect")
+
+ # Update password
+ user.hashed_password = hash_password(password_data.new_password)
+ db.commit()
+
+ return {"message": "Password changed successfully"}
+
+@router.post("/users/{user_id}/change-password")
+def admin_change_password(user_id: int, password_data: AdminPasswordChange, db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_user)):
+ """Change a user's password (admin only)"""
+ user = db.query(User).filter(User.id == user_id).first()
+ if not user:
+ raise HTTPException(status_code=404, detail="User not found")
+
+ # Don't allow changing yourself via this endpoint
+ if current_user.id == user_id:
+ raise HTTPException(status_code=400, detail="Use /auth/change-password to change your own password")
+
+ # Update password
+ user.hashed_password = hash_password(password_data.new_password)
+ db.commit()
+
+ return {"message": "Password changed successfully"}
+
# Routes
@router.get("/")
def read_root():
return {"message": "Drug Inventory API"}
@router.get("/drugs", response_model=List[DrugWithVariantsResponse])
-def list_drugs(db: Session = Depends(get_db)):
+def list_drugs(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Get all drugs with their variants"""
drugs = db.query(Drug).all()
result = []
@@ -110,7 +275,7 @@ def list_drugs(db: Session = Depends(get_db)):
return result
@router.get("/drugs/low-stock", response_model=List[DrugWithVariantsResponse])
-def low_stock_drugs(db: Session = Depends(get_db)):
+def low_stock_drugs(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Get drugs with low stock variants"""
# Get variants that are low on stock
low_stock_variants = db.query(DrugVariant).filter(
@@ -130,7 +295,7 @@ def low_stock_drugs(db: Session = Depends(get_db)):
return result
@router.get("/drugs/{drug_id}", response_model=DrugWithVariantsResponse)
-def get_drug(drug_id: int, db: Session = Depends(get_db)):
+def get_drug(drug_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Get a specific drug with its variants"""
drug = db.query(Drug).filter(Drug.id == drug_id).first()
if not drug:
@@ -142,7 +307,7 @@ def get_drug(drug_id: int, db: Session = Depends(get_db)):
return drug_dict
@router.post("/drugs", response_model=DrugWithVariantsResponse)
-def create_drug(drug: DrugCreate, db: Session = Depends(get_db)):
+def create_drug(drug: DrugCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Create a new drug"""
# Check if drug name already exists
existing = db.query(Drug).filter(Drug.name == drug.name).first()
@@ -160,7 +325,7 @@ def create_drug(drug: DrugCreate, db: Session = Depends(get_db)):
return drug_dict
@router.put("/drugs/{drug_id}", response_model=DrugWithVariantsResponse)
-def update_drug(drug_id: int, drug_update: DrugUpdate, db: Session = Depends(get_db)):
+def update_drug(drug_id: int, drug_update: DrugUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Update a drug"""
drug = db.query(Drug).filter(Drug.id == drug_id).first()
if not drug:
@@ -178,7 +343,7 @@ def update_drug(drug_id: int, drug_update: DrugUpdate, db: Session = Depends(get
return drug_dict
@router.delete("/drugs/{drug_id}")
-def delete_drug(drug_id: int, db: Session = Depends(get_db)):
+def delete_drug(drug_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Delete a drug and all its variants"""
drug = db.query(Drug).filter(Drug.id == drug_id).first()
if not drug:
@@ -194,7 +359,7 @@ def delete_drug(drug_id: int, db: Session = Depends(get_db)):
# Drug Variant endpoints
@router.post("/drugs/{drug_id}/variants", response_model=DrugVariantResponse)
-def create_drug_variant(drug_id: int, variant: DrugVariantCreate, db: Session = Depends(get_db)):
+def create_drug_variant(drug_id: int, variant: DrugVariantCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Create a new variant for a drug"""
# Check if drug exists
drug = db.query(Drug).filter(Drug.id == drug_id).first()
@@ -222,7 +387,7 @@ def create_drug_variant(drug_id: int, variant: DrugVariantCreate, db: Session =
return db_variant
@router.get("/variants/{variant_id}", response_model=DrugVariantResponse)
-def get_drug_variant(variant_id: int, db: Session = Depends(get_db)):
+def get_drug_variant(variant_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Get a specific drug variant"""
variant = db.query(DrugVariant).filter(DrugVariant.id == variant_id).first()
if not variant:
@@ -230,7 +395,7 @@ def get_drug_variant(variant_id: int, db: Session = Depends(get_db)):
return variant
@router.put("/variants/{variant_id}", response_model=DrugVariantResponse)
-def update_drug_variant(variant_id: int, variant_update: DrugVariantUpdate, db: Session = Depends(get_db)):
+def update_drug_variant(variant_id: int, variant_update: DrugVariantUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Update a drug variant"""
variant = db.query(DrugVariant).filter(DrugVariant.id == variant_id).first()
if not variant:
@@ -244,7 +409,7 @@ def update_drug_variant(variant_id: int, variant_update: DrugVariantUpdate, db:
return variant
@router.delete("/variants/{variant_id}")
-def delete_drug_variant(variant_id: int, db: Session = Depends(get_db)):
+def delete_drug_variant(variant_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Delete a drug variant"""
variant = db.query(DrugVariant).filter(DrugVariant.id == variant_id).first()
if not variant:
@@ -257,7 +422,7 @@ def delete_drug_variant(variant_id: int, db: Session = Depends(get_db)):
# Dispensing endpoints
@router.post("/dispense", response_model=DispensingResponse)
-def dispense_drug(dispensing: DispensingCreate, db: Session = Depends(get_db)):
+def dispense_drug(dispensing: DispensingCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Record a drug dispensing and reduce inventory"""
# Check if drug variant exists
variant = db.query(DrugVariant).filter(DrugVariant.id == dispensing.drug_variant_id).first()
@@ -283,12 +448,12 @@ def dispense_drug(dispensing: DispensingCreate, db: Session = Depends(get_db)):
return db_dispensing
@router.get("/dispense/history", response_model=List[DispensingResponse])
-def list_dispensings(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
+def list_dispensings(skip: int = 0, limit: int = 100, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Get dispensing records (audit log)"""
return db.query(Dispensing).order_by(Dispensing.dispensed_at.desc()).offset(skip).limit(limit).all()
@router.get("/drugs/{drug_id}/dispense/history", response_model=List[DispensingResponse])
-def get_drug_dispensings(drug_id: int, db: Session = Depends(get_db)):
+def get_drug_dispensings(drug_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Get dispensing history for a specific drug (all variants)"""
# Verify drug exists
drug = db.query(Drug).filter(Drug.id == drug_id).first()
@@ -301,7 +466,7 @@ def get_drug_dispensings(drug_id: int, db: Session = Depends(get_db)):
return db.query(Dispensing).filter(Dispensing.drug_variant_id.in_(variant_ids)).order_by(Dispensing.dispensed_at.desc()).all()
@router.get("/variants/{variant_id}/dispense/history", response_model=List[DispensingResponse])
-def get_variant_dispensings(variant_id: int, db: Session = Depends(get_db)):
+def get_variant_dispensings(variant_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Get dispensing history for a specific drug variant"""
# Verify variant exists
variant = db.query(DrugVariant).filter(DrugVariant.id == variant_id).first()
diff --git a/backend/app/models.py b/backend/app/models.py
index 6de779a..efcc519 100644
--- a/backend/app/models.py
+++ b/backend/app/models.py
@@ -1,7 +1,17 @@
-from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey
+from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey, Boolean
from sqlalchemy.sql import func
from .database import Base
+class User(Base):
+ __tablename__ = "users"
+
+ id = Column(Integer, primary_key=True, index=True)
+ username = Column(String, unique=True, index=True, nullable=False)
+ hashed_password = Column(String, nullable=False)
+ is_admin = Column(Boolean, default=False)
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
+
+
class Drug(Base):
__tablename__ = "drugs"
@@ -31,7 +41,7 @@ class Dispensing(Base):
id = Column(Integer, primary_key=True, index=True)
drug_variant_id = Column(Integer, ForeignKey("drug_variants.id"), nullable=False)
quantity = Column(Float, nullable=False)
- animal_name = Column(String, nullable=False) # Name/ID of the animal
+ animal_name = Column(String, nullable=True) # Name/ID of the animal (optional)
user_name = Column(String, nullable=False) # User who dispensed
dispensed_at = Column(DateTime(timezone=True), server_default=func.now(), index=True)
notes = Column(String, nullable=True)
diff --git a/backend/requirements.txt b/backend/requirements.txt
index 55acc1c..8c18216 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -3,3 +3,5 @@ uvicorn==0.24.0
sqlalchemy==2.0.23
pydantic==2.5.0
python-multipart==0.0.6
+python-jose[cryptography]==3.3.0
+passlib[argon2]==1.7.4
diff --git a/frontend/app.js b/frontend/app.js
index b50773b..c68b3a6 100644
--- a/frontend/app.js
+++ b/frontend/app.js
@@ -4,9 +4,137 @@ let currentDrug = null;
let showLowStockOnly = false;
let searchTerm = '';
let expandedDrugs = new Set();
+let currentUser = null;
+let accessToken = null;
-// Initialize
+// Initialize on page load
document.addEventListener('DOMContentLoaded', () => {
+ checkAuth();
+});
+
+// Check if user is already logged in
+function checkAuth() {
+ const token = localStorage.getItem('accessToken');
+ const user = localStorage.getItem('currentUser');
+
+ if (token && user) {
+ accessToken = token;
+ currentUser = JSON.parse(user);
+ showMainApp();
+ } else {
+ showLoginPage();
+ }
+}
+
+// Show login page
+function showLoginPage() {
+ document.getElementById('loginPage').style.display = 'flex';
+ document.getElementById('mainApp').style.display = 'none';
+
+ const loginForm = document.getElementById('loginForm');
+ if (loginForm) loginForm.addEventListener('submit', handleLogin);
+}
+
+// Show main app
+function showMainApp() {
+ document.getElementById('loginPage').style.display = 'none';
+ document.getElementById('mainApp').style.display = 'block';
+
+ const userDisplay = document.getElementById('currentUser');
+ if (userDisplay) {
+ userDisplay.textContent = `👤 ${currentUser.username}`;
+ }
+
+ const adminBtn = document.getElementById('adminBtn');
+ if (adminBtn) {
+ adminBtn.style.display = currentUser.is_admin ? 'block' : 'none';
+ }
+
+ setupEventListeners();
+ loadDrugs();
+}
+
+// Handle login
+async function handleLogin(e) {
+ e.preventDefault();
+ const username = document.getElementById('loginUsername').value;
+ const password = document.getElementById('loginPassword').value;
+
+ try {
+ const response = await fetch(`${API_URL}/auth/login`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ username, password })
+ });
+
+ if (!response.ok) {
+ throw new Error('Invalid credentials');
+ }
+
+ const data = await response.json();
+ accessToken = data.access_token;
+ currentUser = data.user;
+
+ localStorage.setItem('accessToken', accessToken);
+ localStorage.setItem('currentUser', JSON.stringify(currentUser));
+
+ document.getElementById('loginForm').reset();
+ const errorDiv = document.getElementById('loginError');
+ if (errorDiv) errorDiv.style.display = 'none';
+ showMainApp();
+ } catch (error) {
+ const errorDiv = document.getElementById('loginError');
+ if (errorDiv) {
+ errorDiv.textContent = error.message;
+ errorDiv.style.display = 'block';
+ }
+ }
+}
+
+// Handle register
+// Logout
+function handleLogout() {
+ localStorage.removeItem('accessToken');
+ localStorage.removeItem('currentUser');
+ accessToken = null;
+ currentUser = null;
+ const loginForm = document.getElementById('loginForm');
+ if (loginForm) loginForm.reset();
+ const registerForm = document.getElementById('registerForm');
+ if (registerForm) {
+ registerForm.style.display = 'none';
+ }
+ const form = document.getElementById('loginForm');
+ if (form) form.style.display = 'block';
+ showLoginPage();
+}
+
+// API helper with authentication
+async function apiCall(endpoint, options = {}) {
+ const headers = {
+ 'Content-Type': 'application/json',
+ ...options.headers
+ };
+
+ if (accessToken) {
+ headers['Authorization'] = `Bearer ${accessToken}`;
+ }
+
+ const response = await fetch(`${API_URL}${endpoint}`, {
+ ...options,
+ headers
+ });
+
+ if (response.status === 401) {
+ handleLogout();
+ throw new Error('Authentication expired');
+ }
+
+ return response;
+}
+
+// Setup event listeners
+function setupEventListeners() {
const drugForm = document.getElementById('drugForm');
const variantForm = document.getElementById('variantForm');
const editVariantForm = document.getElementById('editVariantForm');
@@ -18,6 +146,7 @@ document.addEventListener('DOMContentLoaded', () => {
const dispenseModal = document.getElementById('dispenseModal');
const editModal = document.getElementById('editModal');
const addDrugBtn = document.getElementById('addDrugBtn');
+ const dispenseBtn = document.getElementById('dispenseBtn');
const cancelAddBtn = document.getElementById('cancelAddBtn');
const cancelVariantBtn = document.getElementById('cancelVariantBtn');
const cancelEditVariantBtn = document.getElementById('cancelEditVariantBtn');
@@ -25,41 +154,75 @@ document.addEventListener('DOMContentLoaded', () => {
const cancelEditBtn = document.getElementById('cancelEditBtn');
const showAllBtn = document.getElementById('showAllBtn');
const showLowStockBtn = document.getElementById('showLowStockBtn');
+ const userMenuBtn = document.getElementById('userMenuBtn');
+ const adminBtn = document.getElementById('adminBtn');
+ const logoutBtn = document.getElementById('logoutBtn');
+ const changePasswordBtn = document.getElementById('changePasswordBtn');
// Modal close buttons
const closeButtons = document.querySelectorAll('.close');
- drugForm.addEventListener('submit', handleAddDrug);
- variantForm.addEventListener('submit', handleAddVariant);
- editVariantForm.addEventListener('submit', handleEditVariant);
- dispenseForm.addEventListener('submit', handleDispenseDrug);
- editForm.addEventListener('submit', handleEditDrug);
+ if (drugForm) drugForm.addEventListener('submit', handleAddDrug);
+ if (variantForm) variantForm.addEventListener('submit', handleAddVariant);
+ if (editVariantForm) editVariantForm.addEventListener('submit', handleEditVariant);
+ if (dispenseForm) dispenseForm.addEventListener('submit', handleDispenseDrug);
+ if (editForm) editForm.addEventListener('submit', handleEditDrug);
- addDrugBtn.addEventListener('click', () => openModal(addModal));
+ if (addDrugBtn) addDrugBtn.addEventListener('click', () => openModal(addModal));
+ if (dispenseBtn) dispenseBtn.addEventListener('click', () => {
+ updateDispenseDrugSelect();
+ openModal(dispenseModal);
+ });
+
+ if (cancelAddBtn) cancelAddBtn.addEventListener('click', () => closeModal(addModal));
+ if (cancelVariantBtn) cancelVariantBtn.addEventListener('click', () => closeModal(addVariantModal));
+ if (cancelEditVariantBtn) cancelEditVariantBtn.addEventListener('click', () => closeModal(editVariantModal));
+ if (cancelDispenseBtn) cancelDispenseBtn.addEventListener('click', () => closeModal(dispenseModal));
+ if (cancelEditBtn) cancelEditBtn.addEventListener('click', closeEditModal);
- cancelAddBtn.addEventListener('click', () => closeModal(addModal));
- cancelVariantBtn.addEventListener('click', () => closeModal(addVariantModal));
- cancelEditVariantBtn.addEventListener('click', () => closeModal(editVariantModal));
- cancelDispenseBtn.addEventListener('click', () => closeModal(dispenseModal));
- cancelEditBtn.addEventListener('click', closeEditModal);
const closeHistoryBtn = document.getElementById('closeHistoryBtn');
- closeHistoryBtn.addEventListener('click', () => closeModal(document.getElementById('historyModal')));
+ if (closeHistoryBtn) closeHistoryBtn.addEventListener('click', () => closeModal(document.getElementById('historyModal')));
+
+ const closeUserManagementBtn = document.getElementById('closeUserManagementBtn');
+ if (closeUserManagementBtn) closeUserManagementBtn.addEventListener('click', () => closeModal(document.getElementById('userManagementModal')));
+
+ const changePasswordForm = document.getElementById('changePasswordForm');
+ if (changePasswordForm) changePasswordForm.addEventListener('submit', handleChangePassword);
+
+ const cancelChangePasswordBtn = document.getElementById('cancelChangePasswordBtn');
+ if (cancelChangePasswordBtn) cancelChangePasswordBtn.addEventListener('click', () => closeModal(document.getElementById('changePasswordModal')));
+
+ const adminChangePasswordForm = document.getElementById('adminChangePasswordForm');
+ if (adminChangePasswordForm) adminChangePasswordForm.addEventListener('submit', handleAdminChangePassword);
+
+ const cancelAdminChangePasswordBtn = document.getElementById('cancelAdminChangePasswordBtn');
+ if (cancelAdminChangePasswordBtn) cancelAdminChangePasswordBtn.addEventListener('click', () => closeModal(document.getElementById('adminChangePasswordModal')));
closeButtons.forEach(btn => btn.addEventListener('click', (e) => {
const modal = e.target.closest('.modal');
closeModal(modal);
}));
- showAllBtn.addEventListener('click', () => {
+ if (showAllBtn) showAllBtn.addEventListener('click', () => {
showLowStockOnly = false;
updateFilterButtons();
renderDrugs();
});
- showLowStockBtn.addEventListener('click', () => {
+ if (showLowStockBtn) showLowStockBtn.addEventListener('click', () => {
showLowStockOnly = true;
updateFilterButtons();
renderDrugs();
});
+
+ // User menu
+ if (userMenuBtn) userMenuBtn.addEventListener('click', () => {
+ const dropdown = document.getElementById('userDropdown');
+ if (dropdown) dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none';
+ });
+
+ if (changePasswordBtn) changePasswordBtn.addEventListener('click', openChangePasswordModal);
+ if (adminBtn) adminBtn.addEventListener('click', openUserManagement);
+ if (logoutBtn) logoutBtn.addEventListener('click', handleLogout);
// Search functionality
const drugSearch = document.getElementById('drugSearch');
@@ -80,14 +243,12 @@ document.addEventListener('DOMContentLoaded', () => {
closeModal(e.target);
}
});
-
- loadDrugs();
-});
+}
// Load drugs from API
async function loadDrugs() {
try {
- const response = await fetch(`${API_URL}/drugs`);
+ const response = await apiCall('/drugs');
if (!response.ok) throw new Error('Failed to load drugs');
allDrugs = await response.json();
@@ -96,18 +257,32 @@ async function loadDrugs() {
} catch (error) {
console.error('Error loading drugs:', error);
document.getElementById('drugsList').innerHTML =
- '
Error loading drugs. Make sure the backend is running on http://localhost:8000
';
+ 'Error loading drugs. Make sure the backend is running.
';
}
}
// Modal utility functions
function openModal(modal) {
+ // Find the highest z-index among currently visible modals
+ const visibleModals = document.querySelectorAll('.modal.show');
+ let maxZIndex = 1000;
+
+ visibleModals.forEach(m => {
+ const zIndex = parseInt(window.getComputedStyle(m).zIndex, 10) || 1000;
+ if (zIndex > maxZIndex) {
+ maxZIndex = zIndex;
+ }
+ });
+
+ // Set the new modal's z-index higher than any existing modal
+ modal.style.zIndex = (maxZIndex + 100).toString();
modal.classList.add('show');
document.body.style.overflow = 'hidden';
}
function closeModal(modal) {
modal.classList.remove('show');
+ modal.style.zIndex = '1000';
document.body.style.overflow = 'auto';
}
@@ -224,9 +399,8 @@ async function handleAddDrug(e) {
try {
// Create the drug first
- const drugResponse = await fetch(`${API_URL}/drugs`, {
+ const drugResponse = await apiCall('/drugs', {
method: 'POST',
- headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(drugData)
});
@@ -243,9 +417,8 @@ async function handleAddDrug(e) {
low_stock_threshold: parseFloat(document.getElementById('initialVariantThreshold').value) || 10
};
- const variantResponse = await fetch(`${API_URL}/drugs/${createdDrug.id}/variants`, {
+ const variantResponse = await apiCall(`/drugs/${createdDrug.id}/variants`, {
method: 'POST',
- headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(variantData)
});
@@ -282,15 +455,14 @@ async function handleDispenseDrug(e) {
const dispensingData = {
drug_variant_id: variantId,
quantity: quantity,
- animal_name: animalName,
+ animal_name: animalName || null,
user_name: userName,
notes: notes || null
};
try {
- const response = await fetch(`${API_URL}/dispense`, {
+ const response = await apiCall('/dispense', {
method: 'POST',
- headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dispensingData)
});
@@ -360,9 +532,8 @@ async function handleAddVariant(e) {
};
try {
- const response = await fetch(`${API_URL}/drugs/${drugId}/variants`, {
+ const response = await apiCall(`/drugs/${drugId}/variants`, {
method: 'POST',
- headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(variantData)
});
@@ -412,9 +583,8 @@ async function handleEditVariant(e) {
};
try {
- const response = await fetch(`${API_URL}/variants/${variantId}`, {
+ const response = await apiCall(`/variants/${variantId}`, {
method: 'PUT',
- headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(variantData)
});
@@ -448,7 +618,7 @@ async function deleteVariant(variantId) {
if (!confirm('Are you sure you want to delete this variant?')) return;
try {
- const response = await fetch(`${API_URL}/variants/${variantId}`, {
+ const response = await apiCall(`/variants/${variantId}`, {
method: 'DELETE'
});
@@ -476,7 +646,7 @@ async function showDrugHistory(drugId) {
openModal(historyModal);
try {
- const response = await fetch(`${API_URL}/dispense/history`);
+ const response = await apiCall(`/dispense/history`);
if (!response.ok) throw new Error('Failed to fetch history');
const allHistory = await response.json();
@@ -546,9 +716,8 @@ async function handleEditDrug(e) {
};
try {
- const response = await fetch(`${API_URL}/drugs/${drugId}`, {
+ const response = await apiCall(`/drugs/${drugId}`, {
method: 'PUT',
- headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(drugData)
});
@@ -568,7 +737,7 @@ async function deleteDrug(drugId) {
if (!confirm('Are you sure you want to delete this drug?')) return;
try {
- const response = await fetch(`${API_URL}/drugs/${drugId}`, {
+ const response = await apiCall(`/drugs/${drugId}`, {
method: 'DELETE'
});
@@ -582,6 +751,104 @@ async function deleteDrug(drugId) {
}
}
+// Password Management
+function openChangePasswordModal() {
+ const modal = document.getElementById('changePasswordModal');
+ document.getElementById('changePasswordForm').reset();
+
+ // Close dropdown
+ const dropdown = document.getElementById('userDropdown');
+ if (dropdown) dropdown.style.display = 'none';
+
+ openModal(modal);
+}
+
+async function handleChangePassword(e) {
+ e.preventDefault();
+
+ const currentPassword = document.getElementById('currentPassword').value;
+ const newPassword = document.getElementById('newPassword').value;
+ const confirmPassword = document.getElementById('confirmNewPassword').value;
+
+ if (newPassword !== confirmPassword) {
+ alert('New passwords do not match!');
+ return;
+ }
+
+ if (newPassword.length < 1) {
+ alert('New password cannot be empty!');
+ return;
+ }
+
+ try {
+ const response = await apiCall('/auth/change-password', {
+ method: 'POST',
+ body: JSON.stringify({
+ current_password: currentPassword,
+ new_password: newPassword
+ })
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.detail || 'Failed to change password');
+ }
+
+ alert('Password changed successfully!');
+ closeModal(document.getElementById('changePasswordModal'));
+ } catch (error) {
+ console.error('Error changing password:', error);
+ alert('Failed to change password: ' + error.message);
+ }
+}
+
+async function openAdminChangePasswordModal(userId, username) {
+ const modal = document.getElementById('adminChangePasswordModal');
+ document.getElementById('adminChangePasswordForm').reset();
+ document.getElementById('adminChangePasswordUserId').value = userId;
+ document.getElementById('adminChangePasswordUsername').value = username;
+ openModal(modal);
+}
+
+async function handleAdminChangePassword(e) {
+ e.preventDefault();
+
+ const userId = document.getElementById('adminChangePasswordUserId').value;
+ const newPassword = document.getElementById('adminChangePasswordNewPassword').value;
+ const confirmPassword = document.getElementById('adminChangePasswordConfirm').value;
+
+ if (newPassword !== confirmPassword) {
+ alert('Passwords do not match!');
+ return;
+ }
+
+ if (newPassword.length < 1) {
+ alert('Password cannot be empty!');
+ return;
+ }
+
+ try {
+ const response = await apiCall(`/users/${userId}/change-password`, {
+ method: 'POST',
+ body: JSON.stringify({
+ new_password: newPassword
+ })
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.detail || 'Failed to change password');
+ }
+
+ alert('Password changed successfully!');
+ closeModal(document.getElementById('adminChangePasswordModal'));
+ openUserManagement();
+ } catch (error) {
+ console.error('Error changing password:', error);
+ alert('Failed to change password: ' + error.message);
+ }
+}
+
// Update filter button states
function updateFilterButtons() {
document.getElementById('showAllBtn').classList.toggle('active', !showLowStockOnly);
@@ -594,3 +861,93 @@ function escapeHtml(text) {
div.textContent = text;
return div.innerHTML;
}
+
+// User Management
+async function openUserManagement() {
+ const modal = document.getElementById('userManagementModal');
+ document.getElementById('newUsername').value = '';
+ document.getElementById('newUserPassword').value = '';
+
+ const usersList = document.getElementById('usersList');
+ usersList.innerHTML = 'Users
Loading users...
';
+
+ try {
+ const response = await apiCall('/users');
+ if (!response.ok) throw new Error('Failed to load users');
+
+ const users = await response.json();
+
+ const usersHtml = `
+ Users
+
+ ${users.map(user => `
+
+ ${user.username}
+ ${user.is_admin ? '👑 Admin' : 'User'}
+
+ ${user.id !== currentUser.id ? `
+
+ ` : ''}
+
+ `).join('')}
+
+ `;
+
+ usersList.innerHTML = usersHtml;
+ } catch (error) {
+ console.error('Error loading users:', error);
+ usersList.innerHTML = 'Users
Error loading users
';
+ }
+
+ const createUserForm = document.getElementById('createUserForm');
+ if (createUserForm) {
+ createUserForm.onsubmit = createUser;
+ }
+
+ openModal(modal);
+}
+
+// Create user
+async function createUser(e) {
+ e.preventDefault();
+
+ const username = document.getElementById('newUsername').value;
+ const password = document.getElementById('newUserPassword').value;
+
+ try {
+ const response = await apiCall('/users', {
+ method: 'POST',
+ body: JSON.stringify({ username, password })
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.detail || 'Failed to create user');
+ }
+
+ document.getElementById('newUsername').value = '';
+ document.getElementById('newUserPassword').value = '';
+ alert('User created successfully!');
+ openUserManagement();
+ } catch (error) {
+ console.error('Error creating user:', error);
+ alert('Failed to create user: ' + error.message);
+ }
+}
+
+// Delete user
+async function deleteUser(userId) {
+ if (!confirm('Are you sure you want to delete this user?')) return;
+
+ try {
+ const response = await apiCall(`/users/${userId}`, { method: 'DELETE' });
+
+ if (!response.ok) throw new Error('Failed to delete user');
+
+ alert('User deleted successfully!');
+ openUserManagement();
+ } catch (error) {
+ console.error('Error deleting user:', error);
+ alert('Failed to delete user: ' + error.message);
+ }
+}
diff --git a/frontend/index.html b/frontend/index.html
index 9b88c98..32da6c1 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -7,19 +7,53 @@
-
-
+
+
+
🐶 MTAR Drug Inventory System 🐶
-
+
+
+
+
-
-
-