add new ui
This commit is contained in:
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
# Тип оборудования по умолчанию
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
63
backend/routers/auth.py
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
22
backend/routers/owners.py
Normal 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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
79
backend/security.py
Normal 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
|
||||
Reference in New Issue
Block a user