add new ui

This commit is contained in:
Danamir
2025-11-10 11:28:49 +03:00
parent 3f91dc91ec
commit e428e7f762
19 changed files with 755 additions and 31 deletions

View File

@@ -1,5 +1,26 @@
from backend.models import Base
from backend.database import engine
from sqlalchemy.orm import sessionmaker
from backend.models import Base, User
from backend.database import engine
from backend.security import get_password_hash
def recreate_db_with_admin():
# Drop and recreate all tables
Base.metadata.drop_all(bind=engine)
Base.metadata.create_all(bind=engine)
# Seed default admin user (username: admin, password: admin)
Session = sessionmaker(bind=engine)
db = Session()
try:
if not db.query(User).filter(User.username == 'admin').first():
db.add(User(username='admin', password_hash=get_password_hash('admin'), role='admin'))
db.commit()
finally:
db.close()
if __name__ == "__main__":
recreate_db_with_admin()
Base.metadata.drop_all(bind=engine) # Опционально, если вдруг есть
Base.metadata.create_all(bind=engine)

View File

@@ -10,6 +10,8 @@ from backend.routers.oboruds import oboruds
from backend.routers.components import components
from backend.routers.rashodniki import consumables
from backend.routers.zametki import zametki
from backend.routers.auth import auth
from backend.routers.owners import owners
@@ -38,6 +40,10 @@ app.mount("/app", StaticFiles(directory="frontend", html=True), name="frontend")
def root():
return RedirectResponse(url="/app/")
@app.get("/login")
def login_page():
return RedirectResponse(url="/app/login.html")
# Подключение роутов
app.include_router(equipment_types)
@@ -46,3 +52,5 @@ app.include_router(oboruds)
app.include_router(components)
app.include_router(consumables)
app.include_router(zametki)
app.include_router(auth)
app.include_router(owners)

View File

@@ -1,10 +1,12 @@
import sys
from sqlalchemy import create_engine, text
from sqlalchemy import create_engine, text, inspect
from sqlalchemy.orm import sessionmaker
from backend.database import SessionLocal as NewSession
from backend.database import SessionLocal as NewSession, engine as new_engine
from backend import models
from pathlib import Path
OLD_DB_URL = "sqlite:///./backend/old_app.db"
OLD_DB_URL = "sqlite:///./instance/project.-10-11-25.db"
old_engine = create_engine(OLD_DB_URL, connect_args={"check_same_thread": False})
OldSession = sessionmaker(bind=old_engine)
old_db = OldSession()
@@ -14,7 +16,25 @@ new_db = NewSession()
def log(msg: str):
print(f"[INFO] {msg}", file=sys.stderr)
def ensure_schema():
"""Ensure new DB has required tables/columns (owners table and oboruds.owner_id)."""
# Create any missing tables defined in models (e.g., owners)
models.Base.metadata.create_all(bind=new_engine)
# If oboruds.owner_id is missing (older DB), add the column (SQLite allows simple ALTER)
try:
inspector = inspect(new_engine)
cols = [c["name"] for c in inspector.get_columns("oboruds")]
if "owner_id" not in cols:
with new_engine.begin() as conn:
conn.execute(text("ALTER TABLE oboruds ADD COLUMN owner_id INTEGER"))
log("Добавлен столбец oboruds.owner_id")
except Exception as e:
log(f"Предупреждение: проверка/добавление owner_id не выполнена: {e}")
def migrate():
ensure_schema()
log("Запуск переноса данных из old_app.db → app.db")
# Тип оборудования по умолчанию

View File

@@ -7,6 +7,15 @@ import datetime
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
username = Column(String(150), unique=True, nullable=False)
password_hash = Column(String(255), nullable=False)
role = Column(String(50), nullable=False, default='viewer') # 'admin' | 'editor' | 'viewer'
@@ -28,6 +37,14 @@ class EquipmentType(Base):
oboruds = relationship("Oboruds", back_populates="type")
class Owner(Base):
__tablename__ = 'owners'
id = Column(Integer, primary_key=True)
name = Column(String, unique=True, nullable=False)
oboruds = relationship("Oboruds", back_populates="owner")
class Oboruds(Base):
__tablename__ = 'oboruds'
@@ -45,6 +62,9 @@ class Oboruds(Base):
type_id = Column(Integer, ForeignKey("equipment_types.id"))
type = relationship("EquipmentType", back_populates="oboruds")
owner_id = Column(Integer, ForeignKey("owners.id"))
owner = relationship("Owner", back_populates="oboruds")
components = relationship("Component", back_populates="oborud")
consumables = relationship("Consumable", back_populates="oborud")

View File

@@ -1,10 +1,11 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from .. import models, schemas, database
from ..security import require_roles
auditories = APIRouter(prefix="/auditories", tags=["auditories"])
@auditories.post("/", response_model=schemas.AuditoryRead)
@auditories.post("/", response_model=schemas.AuditoryRead, dependencies=[Depends(require_roles(["admin", "editor"]))])
def create_auditory(item: schemas.AuditoryCreate, db: Session = Depends(database.get_db)):
obj = models.Auditory(**item.dict())
db.add(obj)

63
backend/routers/auth.py Normal file
View File

@@ -0,0 +1,63 @@
from datetime import timedelta
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from .. import schemas, database
from ..security import authenticate_user, create_access_token, get_password_hash, require_roles
from ..models import User
auth = APIRouter(prefix="/auth", tags=["auth"])
@auth.post("/token", response_model=schemas.Token)
def login_for_access_token(
form_data: OAuth2PasswordRequestForm = Depends(),
db: Session = Depends(database.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 username or password")
access_token_expires = timedelta(minutes=60)
access_token = create_access_token(data={"sub": user.username, "role": user.role}, expires_delta=access_token_expires)
return {"access_token": access_token, "token_type": "bearer"}
@auth.post("/users", response_model=schemas.UserRead, dependencies=[Depends(require_roles(["admin"]))])
def create_user(item: schemas.UserCreate, db: Session = Depends(database.get_db)):
if db.query(User).filter(User.username == item.username).first():
raise HTTPException(status_code=400, detail="Username already exists")
obj = User(username=item.username, password_hash=get_password_hash(item.password), role=item.role)
db.add(obj)
db.commit()
db.refresh(obj)
return obj
@auth.post("/users/admin", response_model=schemas.UserRead, dependencies=[Depends(require_roles(["admin"]))])
def create_admin_user(item: schemas.UserCreate, db: Session = Depends(database.get_db)):
if db.query(User).filter(User.username == item.username).first():
raise HTTPException(status_code=400, detail="Username already exists")
obj = User(username=item.username, password_hash=get_password_hash(item.password), role="admin")
db.add(obj)
db.commit()
db.refresh(obj)
return obj
@auth.get("/users", response_model=list[schemas.UserRead], dependencies=[Depends(require_roles(["admin"]))])
def list_users(db: Session = Depends(database.get_db)):
return db.query(User).all()
@auth.patch("/users/{user_id}/role", response_model=schemas.UserRead, dependencies=[Depends(require_roles(["admin"]))])
def update_user_role(user_id: int, payload: schemas.UserRoleUpdate, db: Session = Depends(database.get_db)):
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
user.role = payload.role
db.commit()
db.refresh(user)
return user

View File

@@ -1,11 +1,12 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from .. import models, schemas, database
from ..security import require_roles
components = APIRouter(prefix="/components", tags=["components"])
@components.post("/", response_model=schemas.ComponentRead)
@components.post("/", response_model=schemas.ComponentRead, dependencies=[Depends(require_roles(["admin", "editor"]))])
def create_component(item: schemas.ComponentCreate, db: Session = Depends(database.get_db)):
obj = models.Component(**item.dict())
db.add(obj)

View File

@@ -1,10 +1,11 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from .. import models, schemas, database
from ..security import require_roles
equipment_types = APIRouter(prefix="/equipment-types", tags=["equipment_types"])
@equipment_types.post("/", response_model=schemas.EquipmentTypeRead)
@equipment_types.post("/", response_model=schemas.EquipmentTypeRead, dependencies=[Depends(require_roles(["admin", "editor"]))])
def create_equipment_type(item: schemas.EquipmentTypeCreate, db: Session = Depends(database.get_db)):
obj = models.EquipmentType(**item.dict())
db.add(obj)

View File

@@ -2,10 +2,11 @@ from fastapi import APIRouter, Depends, HTTPException
from typing import Optional
from sqlalchemy.orm import Session
from .. import models, schemas, database
from ..security import require_roles
oboruds = APIRouter(prefix="/oboruds", tags=["oboruds"])
@oboruds.post("/", response_model=schemas.OborudRead)
@oboruds.post("/", response_model=schemas.OborudRead, dependencies=[Depends(require_roles(["admin", "editor"]))])
def create_oborud(item: schemas.OborudCreate, db: Session = Depends(database.get_db)):
obj = models.Oboruds(**item.dict())
db.add(obj)
@@ -26,3 +27,16 @@ def get_oborud(oborud_id: int, db: Session = Depends(database.get_db)):
if not obj:
raise HTTPException(status_code=404, detail="Oborud not found")
return obj
@oboruds.patch("/{oborud_id}", response_model=schemas.OborudRead, dependencies=[Depends(require_roles(["admin", "editor"]))])
def update_oborud(oborud_id: int, payload: schemas.OborudUpdate, db: Session = Depends(database.get_db)):
obj = db.query(models.Oboruds).filter(models.Oboruds.id == oborud_id).first()
if not obj:
raise HTTPException(status_code=404, detail="Oborud not found")
data = payload.dict(exclude_unset=True)
for k, v in data.items():
setattr(obj, k, v)
db.commit()
db.refresh(obj)
return obj

22
backend/routers/owners.py Normal file
View File

@@ -0,0 +1,22 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from .. import models, schemas, database
from ..security import require_roles
owners = APIRouter(prefix="/owners", tags=["owners"])
@owners.post("/", response_model=schemas.OwnerRead, dependencies=[Depends(require_roles(["admin", "editor"]))])
def create_owner(item: schemas.OwnerCreate, db: Session = Depends(database.get_db)):
obj = models.Owner(**item.dict())
db.add(obj)
db.commit()
db.refresh(obj)
return obj
@owners.get("/", response_model=list[schemas.OwnerRead])
def list_owners(db: Session = Depends(database.get_db)):
return db.query(models.Owner).all()

View File

@@ -1,10 +1,11 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from .. import models, schemas, database
from ..security import require_roles
consumables = APIRouter(prefix="/consumables", tags=["consumables"])
@consumables.post("/", response_model=schemas.ConsumableRead)
@consumables.post("/", response_model=schemas.ConsumableRead, dependencies=[Depends(require_roles(["admin", "editor"]))])
def create_consumable(item: schemas.ConsumableCreate, db: Session = Depends(database.get_db)):
obj = models.Consumable(**item.dict())
db.add(obj)

View File

@@ -1,10 +1,11 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from .. import models, schemas, database
from ..security import require_roles
zametki = APIRouter(prefix="/zametki", tags=["zametki"])
@zametki.post("/", response_model=schemas.ZametkaRead)
@zametki.post("/", response_model=schemas.ZametkaRead, dependencies=[Depends(require_roles(["admin", "editor"]))])
def create_zametka(item: schemas.ZametkaCreate, db: Session = Depends(database.get_db)):
obj = models.Zametki(**item.dict())
db.add(obj)

View File

@@ -1,7 +1,7 @@
# backend/schemas.py
from pydantic import BaseModel
from typing import Optional, List
from typing import Optional, List, Literal
from datetime import datetime
@@ -49,6 +49,20 @@ class ConsumableRead(ConsumableBase):
orm_mode = True
# === Owner ===
class OwnerBase(BaseModel):
name: str
class OwnerCreate(OwnerBase):
pass
class OwnerRead(OwnerBase):
id: int
class Config:
orm_mode = True
# === Oborud ===
class OborudBase(BaseModel):
invNumber: Optional[int]
@@ -58,13 +72,25 @@ class OborudBase(BaseModel):
kolichestvo: Optional[int] = None
aud_id: int
type_id: int
owner_id: Optional[int] = None
class OborudCreate(OborudBase):
pass
class OborudUpdate(BaseModel):
invNumber: Optional[int] = None
nazvanie: Optional[str] = None
raspologenie: Optional[str] = None
numberppasu: Optional[str] = None
kolichestvo: Optional[int] = None
aud_id: Optional[int] = None
type_id: Optional[int] = None
owner_id: Optional[int] = None
class OborudRead(OborudBase):
id: int
type: EquipmentTypeRead
owner: Optional[OwnerRead] = None
components: List[ComponentRead] = []
consumables: List[ConsumableRead] = []
@@ -100,3 +126,33 @@ class ZametkaRead(ZametkaBase):
class Config:
orm_mode = True
# === Auth/User ===
class Token(BaseModel):
access_token: str
token_type: str = "bearer"
class TokenData(BaseModel):
username: Optional[str] = None
role: Optional[str] = None
Role = Literal["admin", "editor", "viewer"]
class UserBase(BaseModel):
username: str
role: Role = "viewer"
class UserCreate(UserBase):
password: str
class UserRead(UserBase):
id: int
class Config:
orm_mode = True
class UserRoleUpdate(BaseModel):
role: Role

79
backend/security.py Normal file
View File

@@ -0,0 +1,79 @@
import os
from datetime import datetime, timedelta, timezone
from typing import Optional, Callable, Iterable
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from passlib.context import CryptContext
from sqlalchemy.orm import Session
from . import models
from .database import get_db
SECRET_KEY = os.getenv("SECRET_KEY", "dev-secret-change-me")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "60"))
# Use pbkdf2_sha256 to avoid external bcrypt backend issues
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")
def verify_password(plain_password: str, password_hash: str) -> bool:
return pwd_context.verify(plain_password, password_hash)
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
to_encode = data.copy()
expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
def get_user_by_username(db: Session, username: str) -> Optional[models.User]:
return db.query(models.User).filter(models.User.username == username).first()
def authenticate_user(db: Session, username: str, password: str) -> Optional[models.User]:
user = get_user_by_username(db, username)
if not user:
return None
if not verify_password(password, user.password_hash):
return None
return user
async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)) -> models.User:
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])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = get_user_by_username(db, username=username)
if user is None:
raise credentials_exception
return user
def require_roles(allowed_roles: Iterable[str]) -> Callable[[models.User], models.User]:
allowed = set(allowed_roles)
def _dependency(user: models.User = Depends(get_current_user)) -> models.User:
if user.role not in allowed:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions")
return user
return _dependency