Add user roles
This commit is contained in:
+11
-1
@@ -85,9 +85,19 @@ def get_current_user(authorization: Optional[str] = Header(None), db: Session =
|
|||||||
|
|
||||||
def get_current_admin_user(current_user: User = Depends(get_current_user)) -> User:
|
def get_current_admin_user(current_user: User = Depends(get_current_user)) -> User:
|
||||||
"""Get the current user and verify they are an admin"""
|
"""Get the current user and verify they are an admin"""
|
||||||
if not current_user.is_admin:
|
if current_user.role != "admin":
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Not enough permissions. Admin access required."
|
detail="Not enough permissions. Admin access required."
|
||||||
)
|
)
|
||||||
return current_user
|
return current_user
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_non_readonly_user(current_user: User = Depends(get_current_user)) -> User:
|
||||||
|
"""Get the current user and verify they are not read-only"""
|
||||||
|
if current_user.role == "readonly":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Read-only users cannot perform this action."
|
||||||
|
)
|
||||||
|
return current_user
|
||||||
|
|||||||
+27
-13
@@ -5,13 +5,20 @@ from typing import List, Optional, Dict, Any
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from .database import engine, get_db, Base
|
from .database import engine, get_db, Base
|
||||||
from .models import Drug, DrugVariant, Dispensing, User
|
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 .auth import hash_password, verify_password, create_access_token, get_current_user, get_current_admin_user, get_current_non_readonly_user, ACCESS_TOKEN_EXPIRE_MINUTES
|
||||||
from .mqtt_service import publish_label_print_with_response
|
from .mqtt_service import publish_label_print_with_response
|
||||||
|
from .migrate_to_roles import migrate_users_table
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
# Create tables
|
# Create tables
|
||||||
Base.metadata.create_all(bind=engine)
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
# Run migration to convert is_admin to role
|
||||||
|
try:
|
||||||
|
migrate_users_table()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Migration failed: {e}. Continuing anyway...")
|
||||||
|
|
||||||
app = FastAPI(title="Drug Inventory API")
|
app = FastAPI(title="Drug Inventory API")
|
||||||
|
|
||||||
# CORS middleware for frontend
|
# CORS middleware for frontend
|
||||||
@@ -30,6 +37,7 @@ router = APIRouter(prefix="/api")
|
|||||||
class UserCreate(BaseModel):
|
class UserCreate(BaseModel):
|
||||||
username: str
|
username: str
|
||||||
password: str
|
password: str
|
||||||
|
role: Optional[str] = "user" # admin, user, readonly
|
||||||
|
|
||||||
class PasswordChange(BaseModel):
|
class PasswordChange(BaseModel):
|
||||||
current_password: str
|
current_password: str
|
||||||
@@ -41,7 +49,7 @@ class AdminPasswordChange(BaseModel):
|
|||||||
class UserResponse(BaseModel):
|
class UserResponse(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
username: str
|
username: str
|
||||||
is_admin: bool
|
role: str
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
@@ -166,7 +174,7 @@ def register(user_data: UserCreate, db: Session = Depends(get_db)):
|
|||||||
db_user = User(
|
db_user = User(
|
||||||
username=user_data.username,
|
username=user_data.username,
|
||||||
hashed_password=hashed_password,
|
hashed_password=hashed_password,
|
||||||
is_admin=True
|
role="admin"
|
||||||
)
|
)
|
||||||
db.add(db_user)
|
db.add(db_user)
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -224,11 +232,17 @@ def create_user(user_data: UserCreate, db: Session = Depends(get_db), current_us
|
|||||||
if existing_user:
|
if existing_user:
|
||||||
raise HTTPException(status_code=400, detail="Username already exists")
|
raise HTTPException(status_code=400, detail="Username already exists")
|
||||||
|
|
||||||
|
# Validate role
|
||||||
|
valid_roles = ["admin", "user", "readonly"]
|
||||||
|
role = user_data.role or "user"
|
||||||
|
if role not in valid_roles:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Invalid role. Must be one of: {', '.join(valid_roles)}")
|
||||||
|
|
||||||
hashed_password = hash_password(user_data.password)
|
hashed_password = hash_password(user_data.password)
|
||||||
db_user = User(
|
db_user = User(
|
||||||
username=user_data.username,
|
username=user_data.username,
|
||||||
hashed_password=hashed_password,
|
hashed_password=hashed_password,
|
||||||
is_admin=False
|
role=role
|
||||||
)
|
)
|
||||||
db.add(db_user)
|
db.add(db_user)
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -334,7 +348,7 @@ def get_drug(drug_id: int, db: Session = Depends(get_db), current_user: User = D
|
|||||||
return drug_dict
|
return drug_dict
|
||||||
|
|
||||||
@router.post("/drugs", response_model=DrugWithVariantsResponse)
|
@router.post("/drugs", response_model=DrugWithVariantsResponse)
|
||||||
def create_drug(drug: DrugCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
|
def create_drug(drug: DrugCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_non_readonly_user)):
|
||||||
"""Create a new drug"""
|
"""Create a new drug"""
|
||||||
# Check if drug name already exists
|
# Check if drug name already exists
|
||||||
existing = db.query(Drug).filter(Drug.name == drug.name).first()
|
existing = db.query(Drug).filter(Drug.name == drug.name).first()
|
||||||
@@ -352,7 +366,7 @@ def create_drug(drug: DrugCreate, db: Session = Depends(get_db), current_user: U
|
|||||||
return drug_dict
|
return drug_dict
|
||||||
|
|
||||||
@router.put("/drugs/{drug_id}", response_model=DrugWithVariantsResponse)
|
@router.put("/drugs/{drug_id}", response_model=DrugWithVariantsResponse)
|
||||||
def update_drug(drug_id: int, drug_update: DrugUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
|
def update_drug(drug_id: int, drug_update: DrugUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_non_readonly_user)):
|
||||||
"""Update a drug"""
|
"""Update a drug"""
|
||||||
drug = db.query(Drug).filter(Drug.id == drug_id).first()
|
drug = db.query(Drug).filter(Drug.id == drug_id).first()
|
||||||
if not drug:
|
if not drug:
|
||||||
@@ -370,7 +384,7 @@ def update_drug(drug_id: int, drug_update: DrugUpdate, db: Session = Depends(get
|
|||||||
return drug_dict
|
return drug_dict
|
||||||
|
|
||||||
@router.delete("/drugs/{drug_id}")
|
@router.delete("/drugs/{drug_id}")
|
||||||
def delete_drug(drug_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
|
def delete_drug(drug_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_non_readonly_user)):
|
||||||
"""Delete a drug and all its variants"""
|
"""Delete a drug and all its variants"""
|
||||||
drug = db.query(Drug).filter(Drug.id == drug_id).first()
|
drug = db.query(Drug).filter(Drug.id == drug_id).first()
|
||||||
if not drug:
|
if not drug:
|
||||||
@@ -386,7 +400,7 @@ def delete_drug(drug_id: int, db: Session = Depends(get_db), current_user: User
|
|||||||
|
|
||||||
# Drug Variant endpoints
|
# Drug Variant endpoints
|
||||||
@router.post("/drugs/{drug_id}/variants", response_model=DrugVariantResponse)
|
@router.post("/drugs/{drug_id}/variants", response_model=DrugVariantResponse)
|
||||||
def create_drug_variant(drug_id: int, variant: DrugVariantCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
|
def create_drug_variant(drug_id: int, variant: DrugVariantCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_non_readonly_user)):
|
||||||
"""Create a new variant for a drug"""
|
"""Create a new variant for a drug"""
|
||||||
# Check if drug exists
|
# Check if drug exists
|
||||||
drug = db.query(Drug).filter(Drug.id == drug_id).first()
|
drug = db.query(Drug).filter(Drug.id == drug_id).first()
|
||||||
@@ -422,7 +436,7 @@ def get_drug_variant(variant_id: int, db: Session = Depends(get_db), current_use
|
|||||||
return variant
|
return variant
|
||||||
|
|
||||||
@router.put("/variants/{variant_id}", response_model=DrugVariantResponse)
|
@router.put("/variants/{variant_id}", response_model=DrugVariantResponse)
|
||||||
def update_drug_variant(variant_id: int, variant_update: DrugVariantUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
|
def update_drug_variant(variant_id: int, variant_update: DrugVariantUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_non_readonly_user)):
|
||||||
"""Update a drug variant"""
|
"""Update a drug variant"""
|
||||||
variant = db.query(DrugVariant).filter(DrugVariant.id == variant_id).first()
|
variant = db.query(DrugVariant).filter(DrugVariant.id == variant_id).first()
|
||||||
if not variant:
|
if not variant:
|
||||||
@@ -436,7 +450,7 @@ def update_drug_variant(variant_id: int, variant_update: DrugVariantUpdate, db:
|
|||||||
return variant
|
return variant
|
||||||
|
|
||||||
@router.delete("/variants/{variant_id}")
|
@router.delete("/variants/{variant_id}")
|
||||||
def delete_drug_variant(variant_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
|
def delete_drug_variant(variant_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_non_readonly_user)):
|
||||||
"""Delete a drug variant"""
|
"""Delete a drug variant"""
|
||||||
variant = db.query(DrugVariant).filter(DrugVariant.id == variant_id).first()
|
variant = db.query(DrugVariant).filter(DrugVariant.id == variant_id).first()
|
||||||
if not variant:
|
if not variant:
|
||||||
@@ -449,7 +463,7 @@ def delete_drug_variant(variant_id: int, db: Session = Depends(get_db), current_
|
|||||||
|
|
||||||
# Dispensing endpoints
|
# Dispensing endpoints
|
||||||
@router.post("/dispense", response_model=DispensingResponse)
|
@router.post("/dispense", response_model=DispensingResponse)
|
||||||
def dispense_drug(dispensing: DispensingCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
|
def dispense_drug(dispensing: DispensingCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_non_readonly_user)):
|
||||||
"""Record a drug dispensing and reduce inventory"""
|
"""Record a drug dispensing and reduce inventory"""
|
||||||
# Check if drug variant exists
|
# Check if drug variant exists
|
||||||
variant = db.query(DrugVariant).filter(DrugVariant.id == dispensing.drug_variant_id).first()
|
variant = db.query(DrugVariant).filter(DrugVariant.id == dispensing.drug_variant_id).first()
|
||||||
@@ -523,7 +537,7 @@ def capitalize_label_text(text: str) -> str:
|
|||||||
|
|
||||||
# Label printing endpoint
|
# Label printing endpoint
|
||||||
@router.post("/labels/print", response_model=LabelPrintResponse)
|
@router.post("/labels/print", response_model=LabelPrintResponse)
|
||||||
def print_label(label_request: LabelPrintRequest, current_user: User = Depends(get_current_user)):
|
def print_label(label_request: LabelPrintRequest, current_user: User = Depends(get_current_non_readonly_user)):
|
||||||
"""
|
"""
|
||||||
Print a drug label by publishing an MQTT message
|
Print a drug label by publishing an MQTT message
|
||||||
|
|
||||||
@@ -588,7 +602,7 @@ def print_label(label_request: LabelPrintRequest, current_user: User = Depends(g
|
|||||||
|
|
||||||
# Notes printing endpoint
|
# Notes printing endpoint
|
||||||
@router.post("/notes/print", response_model=NotesPrintResponse)
|
@router.post("/notes/print", response_model=NotesPrintResponse)
|
||||||
def print_notes(notes_request: NotesPrintRequest, current_user: User = Depends(get_current_user)):
|
def print_notes(notes_request: NotesPrintRequest, current_user: User = Depends(get_current_non_readonly_user)):
|
||||||
"""
|
"""
|
||||||
Print notes by publishing an MQTT message
|
Print notes by publishing an MQTT message
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
"""
|
||||||
|
Migration script to convert is_admin boolean field to role string field
|
||||||
|
"""
|
||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from sqlalchemy.engine.url import make_url
|
||||||
|
|
||||||
|
def migrate_users_table():
|
||||||
|
"""Add role column to users table and migrate data from is_admin"""
|
||||||
|
|
||||||
|
# Get database path from environment or use default
|
||||||
|
db_url = os.getenv("DATABASE_URL", "sqlite:///./data/drugs.db")
|
||||||
|
|
||||||
|
# Parse SQLite URL to get the file path
|
||||||
|
if db_url.startswith("sqlite:///"):
|
||||||
|
db_path = db_url.replace("sqlite:///", "")
|
||||||
|
# Handle relative paths
|
||||||
|
if not db_path.startswith("/"):
|
||||||
|
db_path = Path("/app/data") / "drugs.db"
|
||||||
|
else:
|
||||||
|
db_path = Path(db_path)
|
||||||
|
else:
|
||||||
|
print(f"Unsupported database URL: {db_url}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not db_path.exists():
|
||||||
|
print(f"Database does not exist at {db_path}, skipping migration")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Connecting to database at {db_path}")
|
||||||
|
conn = sqlite3.connect(str(db_path))
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check if role column already exists
|
||||||
|
cursor.execute("PRAGMA table_info(users)")
|
||||||
|
columns = [col[1] for col in cursor.fetchall()]
|
||||||
|
|
||||||
|
if "role" in columns:
|
||||||
|
print("Role column already exists, skipping migration")
|
||||||
|
conn.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
if not columns:
|
||||||
|
print("Users table does not exist yet, skipping migration")
|
||||||
|
conn.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
print("Migrating users table: adding role column...")
|
||||||
|
|
||||||
|
# Add role column with default value
|
||||||
|
cursor.execute("ALTER TABLE users ADD COLUMN role VARCHAR DEFAULT 'user'")
|
||||||
|
|
||||||
|
# Migrate data from is_admin to role
|
||||||
|
if "is_admin" in columns:
|
||||||
|
print("Migrating data from is_admin to role...")
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE users
|
||||||
|
SET role = CASE
|
||||||
|
WHEN is_admin = 1 THEN 'admin'
|
||||||
|
ELSE 'user'
|
||||||
|
END
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Drop the old is_admin column
|
||||||
|
# SQLite doesn't support DROP COLUMN directly in older versions,
|
||||||
|
# so we use a workaround
|
||||||
|
cursor.execute("ALTER TABLE users RENAME TO users_old")
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE users (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
username VARCHAR UNIQUE NOT NULL,
|
||||||
|
hashed_password VARCHAR NOT NULL,
|
||||||
|
role VARCHAR NOT NULL DEFAULT 'user',
|
||||||
|
created_at DATETIME
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO users (id, username, hashed_password, role, created_at)
|
||||||
|
SELECT id, username, hashed_password, role, created_at FROM users_old
|
||||||
|
""")
|
||||||
|
cursor.execute("DROP TABLE users_old")
|
||||||
|
print("Successfully migrated is_admin to role and cleaned up old column")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print("Migration completed successfully!")
|
||||||
|
|
||||||
|
except sqlite3.OperationalError as e:
|
||||||
|
print(f"Migration error: {e}")
|
||||||
|
conn.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
migrate_users_table()
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ class User(Base):
|
|||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
username = Column(String, unique=True, index=True, nullable=False)
|
username = Column(String, unique=True, index=True, nullable=False)
|
||||||
hashed_password = Column(String, nullable=False)
|
hashed_password = Column(String, nullable=False)
|
||||||
is_admin = Column(Boolean, default=False)
|
role = Column(String, default="user", nullable=False) # admin, user, readonly
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+46
-6
@@ -71,16 +71,36 @@ function showMainApp() {
|
|||||||
document.getElementById('loginPage').style.display = 'none';
|
document.getElementById('loginPage').style.display = 'none';
|
||||||
document.getElementById('mainApp').style.display = 'block';
|
document.getElementById('mainApp').style.display = 'block';
|
||||||
|
|
||||||
|
// Handle backward compatibility: convert old is_admin format to role
|
||||||
|
if (!currentUser.role && currentUser.is_admin !== undefined) {
|
||||||
|
currentUser.role = currentUser.is_admin ? 'admin' : 'user';
|
||||||
|
}
|
||||||
|
// Default to 'user' if role is still undefined
|
||||||
|
if (!currentUser.role) {
|
||||||
|
currentUser.role = 'user';
|
||||||
|
}
|
||||||
|
|
||||||
const userDisplay = document.getElementById('currentUser');
|
const userDisplay = document.getElementById('currentUser');
|
||||||
if (userDisplay) {
|
if (userDisplay) {
|
||||||
userDisplay.textContent = `👤 ${currentUser.username}`;
|
const roleLabel = currentUser.role.charAt(0).toUpperCase() + currentUser.role.slice(1);
|
||||||
|
userDisplay.textContent = `👤 ${currentUser.username} [${roleLabel}]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const adminBtn = document.getElementById('adminBtn');
|
const adminBtn = document.getElementById('adminBtn');
|
||||||
if (adminBtn) {
|
if (adminBtn) {
|
||||||
adminBtn.style.display = currentUser.is_admin ? 'block' : 'none';
|
adminBtn.style.display = currentUser.role === 'admin' ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hide action buttons for read-only users
|
||||||
|
const isReadOnly = currentUser.role === 'readonly';
|
||||||
|
const addDrugBtn = document.getElementById('addDrugBtn');
|
||||||
|
const dispenseBtn = document.getElementById('dispenseBtn');
|
||||||
|
const printNotesBtn = document.getElementById('printNotesBtn');
|
||||||
|
|
||||||
|
if (addDrugBtn) addDrugBtn.style.display = isReadOnly ? 'none' : 'block';
|
||||||
|
if (dispenseBtn) dispenseBtn.style.display = isReadOnly ? 'none' : 'block';
|
||||||
|
if (printNotesBtn) printNotesBtn.style.display = isReadOnly ? 'none' : 'block';
|
||||||
|
|
||||||
setupEventListeners();
|
setupEventListeners();
|
||||||
loadDrugs();
|
loadDrugs();
|
||||||
}
|
}
|
||||||
@@ -384,6 +404,7 @@ function renderDrugs() {
|
|||||||
const totalQuantity = drug.variants.reduce((sum, v) => sum + v.quantity, 0);
|
const totalQuantity = drug.variants.reduce((sum, v) => sum + v.quantity, 0);
|
||||||
const isLowStock = lowStockVariants > 0;
|
const isLowStock = lowStockVariants > 0;
|
||||||
const isExpanded = expandedDrugs.has(drug.id);
|
const isExpanded = expandedDrugs.has(drug.id);
|
||||||
|
const isReadOnly = currentUser.role === 'readonly';
|
||||||
|
|
||||||
const variantsHtml = isExpanded ? `
|
const variantsHtml = isExpanded ? `
|
||||||
${drug.variants.map(variant => {
|
${drug.variants.map(variant => {
|
||||||
@@ -402,10 +423,12 @@ function renderDrugs() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="variant-actions">
|
<div class="variant-actions">
|
||||||
|
${!isReadOnly ? `
|
||||||
<button class="btn btn-primary btn-small" onclick="prescribeVariant(${variant.id}, '${drug.name.replace(/'/g, "\\'")}', '${variant.strength.replace(/'/g, "\\'")}', '${variant.unit.replace(/'/g, "\\'")}')">🏷️ Prescribe & Print</button>
|
<button class="btn btn-primary btn-small" onclick="prescribeVariant(${variant.id}, '${drug.name.replace(/'/g, "\\'")}', '${variant.strength.replace(/'/g, "\\'")}', '${variant.unit.replace(/'/g, "\\'")}')">🏷️ Prescribe & Print</button>
|
||||||
<button class="btn btn-success btn-small" onclick="dispenseVariant(${variant.id})">💊 Dispense</button>
|
<button class="btn btn-success btn-small" onclick="dispenseVariant(${variant.id})">💊 Dispense</button>
|
||||||
<button class="btn btn-warning btn-small" onclick="openEditVariantModal(${variant.id})">Edit</button>
|
<button class="btn btn-warning btn-small" onclick="openEditVariantModal(${variant.id})">Edit</button>
|
||||||
<button class="btn btn-danger btn-small" onclick="deleteVariant(${variant.id})">Delete</button>
|
<button class="btn btn-danger btn-small" onclick="deleteVariant(${variant.id})">Delete</button>
|
||||||
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -424,10 +447,14 @@ function renderDrugs() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="drug-actions">
|
<div class="drug-actions">
|
||||||
|
${!isReadOnly ? `
|
||||||
<button class="btn btn-primary btn-small" onclick="event.stopPropagation(); openAddVariantModal(${drug.id})">➕ Add</button>
|
<button class="btn btn-primary btn-small" onclick="event.stopPropagation(); openAddVariantModal(${drug.id})">➕ Add</button>
|
||||||
|
` : ''}
|
||||||
<button class="btn btn-info btn-small" onclick="event.stopPropagation(); showDrugHistory(${drug.id})">📋 History</button>
|
<button class="btn btn-info btn-small" onclick="event.stopPropagation(); showDrugHistory(${drug.id})">📋 History</button>
|
||||||
|
${!isReadOnly ? `
|
||||||
<button class="btn btn-warning btn-small" onclick="event.stopPropagation(); openEditModal(${drug.id})">Edit Drug</button>
|
<button class="btn btn-warning btn-small" onclick="event.stopPropagation(); openEditModal(${drug.id})">Edit Drug</button>
|
||||||
<button class="btn btn-danger btn-small" onclick="event.stopPropagation(); deleteDrug(${drug.id})">Delete</button>
|
<button class="btn btn-danger btn-small" onclick="event.stopPropagation(); deleteDrug(${drug.id})">Delete</button>
|
||||||
|
` : ''}
|
||||||
<span class="expand-icon">${isExpanded ? '▼' : '▶'}</span>
|
<span class="expand-icon">${isExpanded ? '▼' : '▶'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1089,6 +1116,7 @@ async function openUserManagement() {
|
|||||||
const modal = document.getElementById('userManagementModal');
|
const modal = document.getElementById('userManagementModal');
|
||||||
document.getElementById('newUsername').value = '';
|
document.getElementById('newUsername').value = '';
|
||||||
document.getElementById('newUserPassword').value = '';
|
document.getElementById('newUserPassword').value = '';
|
||||||
|
document.getElementById('newUserRole').value = '';
|
||||||
|
|
||||||
const usersList = document.getElementById('usersList');
|
const usersList = document.getElementById('usersList');
|
||||||
usersList.innerHTML = '<h3>Users</h3><p class="loading">Loading users...</p>';
|
usersList.innerHTML = '<h3>Users</h3><p class="loading">Loading users...</p>';
|
||||||
@@ -1102,16 +1130,21 @@ async function openUserManagement() {
|
|||||||
const usersHtml = `
|
const usersHtml = `
|
||||||
<h3>Users</h3>
|
<h3>Users</h3>
|
||||||
<div class="users-table">
|
<div class="users-table">
|
||||||
${users.map(user => `
|
${users.map(user => {
|
||||||
|
const roleLabel = user.role.charAt(0).toUpperCase() + user.role.slice(1);
|
||||||
|
const roleBadge = user.role === 'admin' ? '👑 Admin' :
|
||||||
|
user.role === 'readonly' ? '👁️ Read-Only' : '👤 Regular';
|
||||||
|
return `
|
||||||
<div class="user-item">
|
<div class="user-item">
|
||||||
<span>${user.username}</span>
|
<span>${user.username}</span>
|
||||||
<span class="admin-badge">${user.is_admin ? '👑 Admin' : 'User'}</span>
|
<span class="admin-badge">${roleBadge}</span>
|
||||||
<button class="btn btn-secondary btn-small" onclick="openAdminChangePasswordModal(${user.id}, '${escapeHtml(user.username)}')">🔑 Password</button>
|
<button class="btn btn-secondary btn-small" onclick="openAdminChangePasswordModal(${user.id}, '${escapeHtml(user.username)}')">🔑 Password</button>
|
||||||
${user.id !== currentUser.id ? `
|
${user.id !== currentUser.id ? `
|
||||||
<button class="btn btn-danger btn-small" onclick="deleteUser(${user.id})">Delete</button>
|
<button class="btn btn-danger btn-small" onclick="deleteUser(${user.id})">Delete</button>
|
||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
`).join('')}
|
`;
|
||||||
|
}).join('')}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -1135,11 +1168,17 @@ async function createUser(e) {
|
|||||||
|
|
||||||
const username = document.getElementById('newUsername').value;
|
const username = document.getElementById('newUsername').value;
|
||||||
const password = document.getElementById('newUserPassword').value;
|
const password = document.getElementById('newUserPassword').value;
|
||||||
|
const role = document.getElementById('newUserRole').value;
|
||||||
|
|
||||||
|
if (!role) {
|
||||||
|
showToast('Please select a role', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiCall('/users', {
|
const response = await apiCall('/users', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ username, password })
|
body: JSON.stringify({ username, password, role })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -1149,6 +1188,7 @@ async function createUser(e) {
|
|||||||
|
|
||||||
document.getElementById('newUsername').value = '';
|
document.getElementById('newUsername').value = '';
|
||||||
document.getElementById('newUserPassword').value = '';
|
document.getElementById('newUserPassword').value = '';
|
||||||
|
document.getElementById('newUserRole').value = '';
|
||||||
showToast('User created successfully!', 'success');
|
showToast('User created successfully!', 'success');
|
||||||
openUserManagement();
|
openUserManagement();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -416,6 +416,12 @@
|
|||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<input type="text" id="newUsername" placeholder="Username" required>
|
<input type="text" id="newUsername" placeholder="Username" required>
|
||||||
<input type="password" id="newUserPassword" placeholder="Password" required>
|
<input type="password" id="newUserPassword" placeholder="Password" required>
|
||||||
|
<select id="newUserRole" required>
|
||||||
|
<option value="">-- Select Role --</option>
|
||||||
|
<option value="admin">Admin</option>
|
||||||
|
<option value="user">Regular User</option>
|
||||||
|
<option value="readonly">Read-Only</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary btn-small">Create User</button>
|
<button type="submit" class="btn btn-primary btn-small">Create User</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
+4
-1
@@ -868,10 +868,13 @@ footer {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-management-content input {
|
.user-management-content input,
|
||||||
|
.user-management-content select {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 150px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|||||||
Reference in New Issue
Block a user