Devices have replaced Clients for authentication as well as location using sections.

Printing of reports done.
Main section is now a fixture

User and Devices list gives last login details.
This commit is contained in:
2020-10-27 16:59:24 +05:30
parent cbc2f29e29
commit f8683cf080
32 changed files with 335 additions and 291 deletions
@@ -0,0 +1,91 @@
"""devices
Revision ID: 00878740057e
Revises: 8c06ac60d125
Create Date: 2020-10-27 14:02:39.859733
"""
import sqlalchemy as sa
from alembic import op
from sqlalchemy import column, table
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = "00878740057e"
down_revision = "8c06ac60d125"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"login_history",
sa.Column("login_history_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("device_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("date", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["device_id"], ["devices.id"], name=op.f("fk_login_history_device_id_devices")),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], name=op.f("fk_login_history_user_id_users")),
sa.PrimaryKeyConstraint("login_history_id", name=op.f("pk_login_history")),
sa.UniqueConstraint("user_id", "device_id", "date", name=op.f("uq_login_history_user_id")),
)
op.drop_table("clients")
op.add_column("devices", sa.Column("enabled", sa.Boolean(), nullable=False))
op.add_column("devices", sa.Column("creation_date", sa.DateTime(timezone=True), nullable=False))
op.alter_column("food_tables", "seats", existing_type=sa.NUMERIC(), type_=sa.Integer(), existing_nullable=False)
op.alter_column(
"food_tables", "sort_order", existing_type=sa.NUMERIC(), type_=sa.Integer(), existing_nullable=False
)
op.alter_column(
"menu_categories", "sort_order", existing_type=sa.NUMERIC(), type_=sa.Integer(), existing_nullable=False
)
op.alter_column(
"modifier_categories", "sort_order", existing_type=sa.NUMERIC(), type_=sa.Integer(), existing_nullable=False
)
op.alter_column("products", "sort_order", existing_type=sa.NUMERIC(), type_=sa.Integer(), existing_nullable=False)
op.add_column("sections", sa.Column("is_fixture", sa.Boolean(), nullable=True))
section = table("sections", column("id", postgresql.UUID(as_uuid=True)), column("is_fixture", sa.Boolean()))
op.execute(
section.update()
.where(section.c.id == op.inline_literal("3f13f6e7-dc76-4fca-8fdb-b2bbf29b35df"))
.values({"is_fixture": op.inline_literal(True)})
)
op.execute(
section.update()
.where(section.c.id != op.inline_literal("3f13f6e7-dc76-4fca-8fdb-b2bbf29b35df"))
.values({"is_fixture": op.inline_literal(False)})
)
op.alter_column("sections", "is_fixture", nullable=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("sections", "is_fixture")
op.alter_column("products", "sort_order", existing_type=sa.Integer(), type_=sa.NUMERIC(), existing_nullable=False)
op.alter_column(
"modifier_categories", "sort_order", existing_type=sa.Integer(), type_=sa.NUMERIC(), existing_nullable=False
)
op.alter_column(
"menu_categories", "sort_order", existing_type=sa.Integer(), type_=sa.NUMERIC(), existing_nullable=False
)
op.alter_column(
"food_tables", "sort_order", existing_type=sa.Integer(), type_=sa.NUMERIC(), existing_nullable=False
)
op.alter_column("food_tables", "seats", existing_type=sa.Integer(), type_=sa.NUMERIC(), existing_nullable=False)
op.drop_column("devices", "enabled")
op.create_table(
"clients",
sa.Column("id", sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column("name", sa.VARCHAR(length=255), autoincrement=False, nullable=False),
sa.Column("enabled", sa.BOOLEAN(), autoincrement=False, nullable=False),
sa.Column("otp", sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column("creation_date", postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=False),
sa.PrimaryKeyConstraint("id", name="pk_clients"),
sa.UniqueConstraint("name", name="uq_clients_name"),
)
op.drop_table("login_history")
# ### end Alembic commands ###
@@ -466,6 +466,8 @@ def upgrade():
op.execute(Permission.__table__.insert().values(id="5b66c6f6-003a-4ef8-ba28-49b8ff1ac33c", name="Printers"))
op.execute(Permission.__table__.insert().values(id="c973f32c-a37b-496a-8dc5-60d2e4c39e97", name="Sections"))
op.execute(Permission.__table__.insert().values(id="7a04ba63-5d08-4078-9051-a6d91cce3e48", name="Section Printers"))
op.execute(Permission.__table__.insert().values(id="d4e1d14f-f2c2-4728-9303-a0d8c74c5ea1", name="Devices"))
op.execute(Permission.__table__.insert().values(id="5f6110ba-2d3a-41cb-9597-1157774f10cb", name="Add Devices"))
op.execute(Section.__table__.insert().values(id="3f13f6e7-dc76-4fca-8fdb-b2bbf29b35df", name="Main"))
op.execute(
+9 -20
View File
@@ -13,7 +13,7 @@ from sqlalchemy.orm import Session
from ..core.config import settings
from ..db.session import SessionLocal
from ..models.auth import Client
from ..models.auth import Device
from ..models.auth import User as UserModel
# to get a string like this run:
@@ -69,29 +69,18 @@ def get_user(username: str, id_: str, locked_out: bool, scopes: List[str]) -> Us
)
def authenticate_user(
username: str, password: str, client_id: Optional[int], otp: int, db: Session
) -> Optional[UserModel]:
def authenticate_user(username: str, password: str, db: Session) -> Optional[UserModel]:
user = UserModel.auth(username, password, db)
return user
def client_allowed(user: UserModel, client_id: int, otp: Optional[int] = None, db: Session = None) -> (bool, int):
client = db.query(Client).filter(Client.id == client_id).first() if client_id else None
allowed = "clients" in set([p.name.replace(" ", "-").lower() for r in user.roles for p in r.permissions])
if allowed or True:
return True, 0
elif client is None:
client = Client.create(db)
return False, client.id
elif client.enabled:
return True, client.id
elif client.otp == otp:
client.otp = None
client.enabled = True
return True, client.id
else:
return False, client.id
def device_allowed(user: UserModel, device_id: Optional[uuid.UUID], db: Session = None) -> (bool, Device):
device: Device = db.query(Device).filter(Device.id == device_id).first()
if device is None:
device = Device.create(db)
allowed = "add-devices" in set([p.name.replace(" ", "-").lower() for r in user.roles for p in r.permissions])
return allowed or device.enabled, device
async def get_current_user(
+3 -2
View File
@@ -7,6 +7,7 @@ from .core.config import settings
from .db.base_class import Base
from .db.session import engine
from .routers import (
device,
guest_book,
login,
menu_category,
@@ -20,7 +21,7 @@ from .routers import (
table,
tax,
)
from .routers.auth import client, role, user
from .routers.auth import role, user
from .routers.reports import (
beer_consumption_report,
bill_settlement_report,
@@ -50,7 +51,6 @@ app = FastAPI()
app.add_middleware(SessionMiddleware, secret_key=settings.MIDDLEWARE_SECRET_KEY)
app.include_router(login.router, tags=["login"])
app.include_router(client.router, prefix="/api/clients", tags=["clients"])
app.include_router(role.router, prefix="/api/roles", tags=["users"])
app.include_router(user.router, prefix="/api/users", tags=["users"])
@@ -61,6 +61,7 @@ app.include_router(printer.router, prefix="/api/printers", tags=["printers"])
app.include_router(menu_category.router, prefix="/api/menu-categories", tags=["products"])
app.include_router(product.router, prefix="/api/products", tags=["products"])
app.include_router(device.router, prefix="/api/devices", tags=["devices"])
app.include_router(sale_category.router, prefix="/api/sale-categories", tags=["products"])
app.include_router(section.router, prefix="/api/sections", tags=["sections"])
+1 -2
View File
@@ -1,11 +1,10 @@
from sqlalchemy import engine_from_config
from sqlalchemy.orm import configure_mappers, sessionmaker
from .auth import Client, Permission, Role, User, role_permissions, user_roles
from .auth import Device, Permission, Role, User, role_permissions, user_roles
from .master import (
Customer,
DbSetting,
Device,
FoodTable,
MenuCategory,
Modifier,
+38 -23
View File
@@ -5,7 +5,7 @@ import uuid
from datetime import datetime
from hashlib import md5
from sqlalchemy import Boolean, Column, DateTime, Integer, Unicode, UniqueConstraint
from sqlalchemy import Boolean, Column, DateTime, Unicode, UniqueConstraint, desc
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Session, relationship, synonym
from sqlalchemy.schema import ForeignKey, Table
@@ -17,38 +17,52 @@ def encrypt(val):
return md5(val.encode("utf-8") + "v2".encode("utf-8")).hexdigest()
class Client(Base):
__tablename__ = "clients"
class LoginHistory(Base):
__tablename__ = "login_history"
__table_args__ = (UniqueConstraint("user_id", "device_id", "date"),)
id = Column("login_history_id", UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column("user_id", UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
device_id = Column(
"device_id",
UUID(as_uuid=True),
ForeignKey("devices.id"),
nullable=False,
)
date = Column("date", DateTime(timezone=True), nullable=False)
id = Column("id", Integer, primary_key=True)
def __init__(self, user_id=None, device_id=None, date=None, id_=None):
self.user_id = user_id
self.device_id = device_id
self.date = datetime.utcnow() if date is None else date
self.id = id_
class Device(Base):
__tablename__ = "devices"
id = Column("id", UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column("name", Unicode(255), unique=True, nullable=False)
enabled = Column("enabled", Boolean, nullable=False)
otp = Column("otp", Integer)
section_id = Column("section_id", UUID(as_uuid=True), ForeignKey("sections.id"), nullable=False)
creation_date = Column("creation_date", DateTime(timezone=True), nullable=False)
def __init__(self, id_=None, name=None, enabled=False, otp=None, creation_date=None):
self.id = id_
section = relationship("Section", foreign_keys=section_id)
login_history = relationship("LoginHistory", order_by=desc(LoginHistory.date), backref="device")
def __init__(self, name=None, enabled=None, section_id=None, creation_date=None, id_=None):
self.name = name
self.enabled = enabled
self.otp = otp
self.creation_date = creation_date or datetime.utcnow()
@classmethod
def by_id(cls, id_: int, db: Session):
if id_ is None:
return None
if not isinstance(id_, int):
id_ = int(id_)
return db.query(cls).filter(cls.id == id_).first()
self.section_id = section_id
self.creation_date = datetime.utcnow() if creation_date is None else creation_date
self.id = id_
@classmethod
def create(cls, db: Session):
client_code = random.randint(1000, 9999)
otp = random.randint(1000, 9999)
name = "".join(random.choice(string.ascii_uppercase + string.digits) for x in range(6))
client = Client(client_code, name, False, otp)
db.add(client)
return client
main_section = uuid.UUID("3f13f6e7-dc76-4fca-8fdb-b2bbf29b35df")
name = "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
device = Device(name, False, main_section)
db.add(device)
return device
user_roles = Table(
@@ -84,6 +98,7 @@ class User(Base):
locked_out = Column("locked_out", Boolean, nullable=False)
roles = relationship("Role", secondary=user_roles, order_by="Role.name")
login_history = relationship("LoginHistory", order_by=desc(LoginHistory.date), backref="user")
def _get_password(self):
return self._password
+6 -15
View File
@@ -299,25 +299,16 @@ class Section(Base):
id = Column("id", UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column("name", Unicode(255), unique=True, nullable=False)
is_fixture = Column("is_fixture", Boolean, nullable=False, default=False)
def __init__(self, name=None, id_=None):
def __init__(self, name=None, is_fixture=None, id_=None):
self.id = id_
self.name = name
self.is_fixture = is_fixture
class Device(Base):
__tablename__ = "devices"
id = Column("id", UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column("name", Unicode(255), unique=True, nullable=False)
section_id = Column("section_id", UUID(as_uuid=True), ForeignKey("sections.id"), nullable=False)
section = relationship("Section", foreign_keys=section_id)
def __init__(self, name=None, section_id=None, id_=None):
self.name = name
self.section_id = section_id
self.id = id_
@classmethod
def main(cls):
return uuid.UUID("3f13f6e7-dc76-4fca-8fdb-b2bbf29b35df")
class Printer(Base):
+5 -3
View File
@@ -1,4 +1,5 @@
import asyncio
import uuid
from datetime import datetime
@@ -7,15 +8,16 @@ from barker.schemas.cashier_report import CashierReport
from sqlalchemy.orm import Session
from ..core.arq import settings as redis_settings
from ..models import Printer, SectionPrinter
from ..models import Device, Printer, SectionPrinter
def print_cashier_report(report: CashierReport, db: Session):
def print_cashier_report(report: CashierReport, device_id: uuid.UUID, db: Session):
data = design_cashier_report(report)
section_id = db.query(Device.section_id).filter(Device.id == device_id).scalar()
printer = (
db.query(Printer)
.join(SectionPrinter.printer)
# .filter(SectionPrinter.section_id == voucher.food_table.section_id) TODO: Use device's section_id
.filter(SectionPrinter.section_id == section_id)
.filter(SectionPrinter.menu_category_id == None) # noqa: E711
.first()
)
+5 -3
View File
@@ -1,19 +1,21 @@
import asyncio
import uuid
from arq import ArqRedis, create_pool
from sqlalchemy.orm import Session
from ..core.arq import settings as redis_settings
from ..models import Printer, SectionPrinter
from ..models import Device, Printer, SectionPrinter
from ..schemas.discount_report import DiscountReport
def print_discount_report(report: DiscountReport, db: Session):
def print_discount_report(report: DiscountReport, device_id: uuid.UUID, db: Session):
data = design_discount_report(report)
section_id = db.query(Device.section_id).filter(Device.id == device_id).scalar()
printer = (
db.query(Printer)
.join(SectionPrinter.printer)
# .filter(SectionPrinter.section_id == voucher.food_table.section_id) TODO: Use device's section_id
.filter(SectionPrinter.section_id == section_id)
.filter(SectionPrinter.menu_category_id == None) # noqa: E711
.first()
)
+5 -3
View File
@@ -1,4 +1,5 @@
import asyncio
import uuid
from datetime import datetime
@@ -6,16 +7,17 @@ from arq import ArqRedis, create_pool
from sqlalchemy.orm import Session
from ..core.arq import settings as redis_settings
from ..models import Printer, SectionPrinter
from ..models import Device, Printer, SectionPrinter
from ..schemas.sale_report import SaleReport
def print_sale_report(report: SaleReport, db: Session):
def print_sale_report(report: SaleReport, device_id: uuid.UUID, db: Session):
data = design_sale_report(report)
section_id = db.query(Device.section_id).filter(Device.id == device_id).scalar()
printer = (
db.query(Printer)
.join(SectionPrinter.printer)
# .filter(SectionPrinter.section_id == voucher.food_table.section_id) TODO: Use device's section_id
.filter(SectionPrinter.section_id == section_id)
.filter(SectionPrinter.menu_category_id == None) # noqa: E711
.first()
)
-116
View File
@@ -1,116 +0,0 @@
import uuid
import barker.schemas.auth as schemas
from fastapi import APIRouter, Depends, HTTPException, Security, status
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from ...core.security import get_current_active_user as get_user
from ...db.session import SessionLocal
from ...models.auth import Client
from ...schemas.auth import UserToken
router = APIRouter()
# Dependency
def get_db():
try:
db = SessionLocal()
yield db
finally:
db.close()
@router.put("/{id_}")
def update(
id_: uuid.UUID,
data: schemas.ClientIn,
db: Session = Depends(get_db),
user: UserToken = Security(get_user, scopes=["clients"]),
):
try:
item: Client = db.query(Client).filter(Client.id == id_).first()
item.enabled = data.enabled
if item.enabled:
item.otp = None
item.name = data.name
db.commit()
return {}
except SQLAlchemyError as e:
db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
except Exception:
db.rollback()
raise
@router.delete("/{id_}")
def delete(
id_: uuid.UUID,
db: Session = Depends(get_db),
user: UserToken = Security(get_user, scopes=["clients"]),
):
try:
item: Client = db.query(Client).filter(Client.id == id_).first()
# db.execute(LoginHistory.__table__.delete(LoginHistory.client_id == item.id))
db.delete(item)
db.commit()
return {}
except SQLAlchemyError as e:
db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
except Exception:
db.rollback()
raise
@router.get("/list")
async def show_list(
db: Session = Depends(get_db),
user: UserToken = Security(get_user, scopes=["clients"]),
):
list_ = db.query(Client).order_by(Client.name).all()
clients = []
for item in list_:
# last_login = (
# db.query(LoginHistory).filter(LoginHistory.client_id == item.id).order_by(desc(LoginHistory.date)).first()
# )
# last_login = "Never" if last_login is None else last_login.date.strftime("%d-%b-%Y %H:%M")
last_login = "Never"
clients.append(
{
"id": item.id,
"code": item.code,
"name": item.name,
"enabled": item.enabled,
"otp": item.otp,
"creationDate": item.creation_date.strftime("%d-%b-%Y %H:%M"),
"lastLogin": last_login,
}
)
return clients
@router.get("/{id_}")
def show_id(
id_: uuid.UUID,
db: Session = Depends(get_db),
user: UserToken = Security(get_user, scopes=["clients"]),
):
item: Client = db.query(Client).filter(Client.id == id_).first()
return {
"id": item.id,
"code": item.code,
"name": item.name,
"enabled": item.enabled,
"otp": item.otp,
}
+27 -25
View File
@@ -31,7 +31,7 @@ def save(
data: schemas.UserIn,
db: Session = Depends(get_db),
user: UserToken = Security(get_user, scopes=["users"]),
):
) -> schemas.User:
try:
item = User(name=data.name, password=data.password, locked_out=data.locked_out)
db.add(item)
@@ -53,7 +53,7 @@ def save(
def show_me(
db: Session = Depends(get_db),
user: UserToken = Depends(get_user),
):
) -> schemas.User:
item = db.query(User).filter(User.id == user.id_).first()
return user_info(item, db, user)
@@ -63,7 +63,7 @@ def update_me(
data: schemas.UserIn,
db: Session = Depends(get_db),
user: UserToken = Depends(get_user),
):
) -> schemas.User:
try:
item: User = db.query(User).filter(User.id == user.id_).first()
if "users" in user.permissions:
@@ -91,7 +91,7 @@ def update(
data: schemas.UserIn,
db: Session = Depends(get_db),
user: UserToken = Security(get_user, scopes=["users"]),
):
) -> schemas.User:
try:
item: User = db.query(User).filter(User.id == id_).first()
item.name = data.name
@@ -157,14 +157,16 @@ def show_blank(
async def show_list(
db: Session = Depends(get_db),
user: UserToken = Security(get_user, scopes=["users"]),
):
) -> List[schemas.UserList]:
return [
{
"id": item.id,
"name": item.name,
"lockedOut": item.locked_out,
"roles": [p.name for p in sorted(item.roles, key=lambda p: p.name)],
}
schemas.UserList(
id=item.id,
name=item.name,
lockedOut=item.locked_out,
roles=[p.name for p in sorted(item.roles, key=lambda p: p.name)],
lastDevice=item.login_history[0].device.name if len(item.login_history) else "Never",
lastDate=item.login_history[0].date if len(item.login_history) else None,
)
for item in db.query(User).order_by(User.name).all()
]
@@ -182,25 +184,25 @@ def show_id(
id_: uuid.UUID,
db: Session = Depends(get_db),
user: UserToken = Security(get_user, scopes=["users"]),
):
) -> schemas.User:
item = db.query(User).filter(User.id == id_).first()
return user_info(item, db, user)
def user_info(item: Optional[User], db: Session, user: UserToken):
def user_info(item: Optional[User], db: Session, user: UserToken) -> schemas.User:
if item is None:
return {
"name": "",
"lockedOut": False,
"roles": [{"id": r.id, "name": r.name, "enabled": False} for r in db.query(Role).order_by(Role.name).all()],
}
return schemas.User(
name="",
lockedOut=False,
roles=[{"id": r.id, "name": r.name, "enabled": False} for r in db.query(Role).order_by(Role.name).all()],
)
else:
return {
"id": item.id,
"name": item.name,
"password": "",
"lockedOut": item.locked_out,
"roles": [
return schemas.User(
id=item.id,
name=item.name,
password="",
lockedOut=item.locked_out,
roles=[
{
"id": r.id,
"name": r.name,
@@ -210,4 +212,4 @@ def user_info(item: Optional[User], db: Session, user: UserToken):
]
if "users" in user.permissions
else [],
}
)
+20 -47
View File
@@ -1,6 +1,6 @@
import uuid
from typing import Optional
from typing import List
import barker.schemas.master as schemas
@@ -10,7 +10,7 @@ from sqlalchemy.orm import Session
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionLocal
from ..models.master import Device
from ..models.auth import Device
from ..schemas.auth import UserToken
@@ -26,38 +26,17 @@ def get_db():
db.close()
@router.post("", response_model=schemas.Device)
def save(
data: schemas.DeviceIn,
db: Session = Depends(get_db),
user: UserToken = Security(get_user, scopes=["devices"]),
):
try:
item = Device(name=data.name, section_id=data.section.id_)
db.add(item)
db.commit()
return device_info(item)
except SQLAlchemyError as e:
db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
except Exception:
db.rollback()
raise
@router.put("/{id_}", response_model=schemas.Device)
def update(
id_: uuid.UUID,
data: schemas.DeviceIn,
db: Session = Depends(get_db),
user: UserToken = Security(get_user, scopes=["devices"]),
):
) -> schemas.Device:
try:
item: Device = db.query(Device).filter(Device.id == id_).first()
item.name = data.name
item.enabled = data.enabled
item.section_id = data.section.id_
db.commit()
return device_info(item)
@@ -72,12 +51,12 @@ def update(
raise
@router.delete("/{id_}")
@router.delete("/{id_}", response_model=schemas.Device)
def delete(
id_: uuid.UUID,
db: Session = Depends(get_db),
user: UserToken = Security(get_user, scopes=["devices"]),
):
user: UserToken = Security(get_user, scopes=["add-devices"]),
) -> schemas.Device:
try:
item: Device = db.query(Device).filter(Device.id == id_).first()
db.delete(item)
@@ -94,34 +73,28 @@ def delete(
raise
@router.get("")
def show_blank(
db: Session = Depends(get_db),
user: UserToken = Security(get_user, scopes=["devices"]),
):
return device_info(None)
@router.get("/list")
@router.get("/list", response_model=List[schemas.Device])
def show_list(db: Session = Depends(get_db), user: UserToken = Depends(get_user)):
return [device_info(item) for item in db.query(Device).order_by(Device.name).all()]
@router.get("/{id_}")
@router.get("/{id_}", response_model=schemas.Device)
def show_id(
id_: uuid.UUID,
db: Session = Depends(get_db),
user: UserToken = Security(get_user, scopes=["devices"]),
):
) -> schemas.Device:
item: Device = db.query(Device).filter(Device.id == id_).first()
return device_info(item)
def device_info(item: Optional[Device]):
if item is None:
return {"name": "", "section": {}}
return {
"id": item.id,
"name": item.name,
"section": {"id": item.section_id, "name": item.section.name},
}
def device_info(item: Device) -> schemas.Device:
return schemas.Device(
id=item.id,
name=item.name,
enabled=item.enabled,
section={"id": item.section_id, "name": item.section.name},
creationDate=item.creation_date,
lastUser=item.login_history[0].user.name if len(item.login_history) else "Never",
lastDate=item.login_history[0].date if len(item.login_history) else None,
)
+16 -9
View File
@@ -1,10 +1,12 @@
import uuid
from datetime import timedelta
from typing import Optional
from fastapi import (
APIRouter,
Cookie,
Depends,
Form,
HTTPException,
Response,
Security,
@@ -19,11 +21,12 @@ from ..core.config import settings
from ..core.security import (
Token,
authenticate_user,
client_allowed,
create_access_token,
device_allowed,
get_current_active_user,
)
from ..db.session import SessionLocal
from ..models.auth import LoginHistory
from ..schemas.auth import UserToken
@@ -43,28 +46,32 @@ def get_db():
async def login_for_access_token(
response: Response,
form_data: OAuth2PasswordRequestForm = Depends(),
client_id: int = Cookie(None),
otp: int = Form(None),
device_id: Optional[uuid.UUID] = Cookie(None),
db: Session = Depends(get_db),
):
user = authenticate_user(form_data.username, form_data.password, client_id, otp, db)
user = authenticate_user(form_data.username, form_data.password, db)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
allowed, c_id = client_allowed(user, client_id, otp, db)
allowed, device = device_allowed(user, device_id, db)
db.flush()
if allowed:
history = LoginHistory(user.id, device.id)
db.add(history)
db.commit()
if c_id and c_id != client_id:
response.set_cookie(key="client_id", value=str(c_id), max_age=10 * 365 * 24 * 60 * 60)
response.set_cookie(key="device_id", value=str(device.id), max_age=10 * 365 * 24 * 60 * 60)
response.set_cookie(key="device", value=device.name, max_age=10 * 365 * 24 * 60 * 60)
if not allowed:
not_allowed_response = JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
headers={"WWW-Authenticate": "Bearer"},
content={"detail": "Client is not registered"},
)
not_allowed_response.set_cookie(key="client_id", value=str(c_id), max_age=10 * 365 * 24 * 60 * 60)
not_allowed_response.set_cookie(key="device_id", value=str(device.id), max_age=10 * 365 * 24 * 60 * 60)
not_allowed_response.set_cookie(key="device", value=device.name, max_age=10 * 365 * 24 * 60 * 60)
return not_allowed_response
access_token_expires = timedelta(minutes=settings.JWT_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
@@ -3,7 +3,7 @@ import uuid
from datetime import date, datetime, timedelta
from typing import Dict, List
from fastapi import APIRouter, Depends, HTTPException, Security, status
from fastapi import APIRouter, Cookie, Depends, HTTPException, Security, status
from sqlalchemy import distinct
from sqlalchemy.orm import Session, joinedload
@@ -134,6 +134,7 @@ def print_report(
id_: uuid.UUID,
s: str = None,
f: str = None,
device_id: uuid.UUID = Cookie(None),
db: Session = Depends(get_db),
user: UserToken = Security(get_user, scopes=["cashier-report"]),
) -> bool:
@@ -144,8 +145,8 @@ def print_report(
status_code=status.HTTP_403_FORBIDDEN,
detail="Accounts Audit",
)
report = get_id(id_, start_date, finish_date, user, db)
print_cashier_report(report, db)
report = get_id(id_, start_date, finish_date, UserLink(id=user.id_, name=user.name), db)
print_cashier_report(report, device_id, db)
return True
@@ -1,7 +1,9 @@
import uuid
from datetime import date, datetime, timedelta
from typing import List
from fastapi import APIRouter, Depends, HTTPException, Security, status
from fastapi import APIRouter, Cookie, Depends, HTTPException, Security, status
from sqlalchemy import func
from sqlalchemy.orm import Session
@@ -76,6 +78,7 @@ def get_discount_report(s: date, f: date, db: Session) -> List[DiscountReportIte
def print_report(
s: str = None,
f: str = None,
device_id: uuid.UUID = Cookie(None),
db: Session = Depends(get_db),
user: UserToken = Security(get_user, scopes=["discount-report"]),
) -> bool:
@@ -91,5 +94,5 @@ def print_report(
finishDate=finish_date,
amounts=get_discount_report(start_date, finish_date, db),
)
print_discount_report(report, db)
print_discount_report(report, device_id, db)
return True
+5 -2
View File
@@ -1,7 +1,9 @@
import uuid
from datetime import date, datetime, timedelta
from typing import List
from fastapi import APIRouter, Depends, HTTPException, Security, status
from fastapi import APIRouter, Cookie, Depends, HTTPException, Security, status
from sqlalchemy import func
from sqlalchemy.orm import Session
@@ -117,6 +119,7 @@ def get_settlements(s: date, f: date, db: Session) -> List[SaleReportItem]:
def print_report(
s: str = None,
f: str = None,
device_id: uuid.UUID = Cookie(None),
db: Session = Depends(get_db),
user: UserToken = Security(get_user, scopes=["discount-report"]),
) -> bool:
@@ -139,5 +142,5 @@ def print_report(
),
user=UserLink(id=user.id_, name=user.name),
)
print_sale_report(report, db)
print_sale_report(report, device_id, db)
return True
+10
View File
@@ -57,6 +57,11 @@ def update(
):
try:
item: Section = db.query(Section).filter(Section.id == id_).first()
if item.is_fixture:
raise HTTPException(
status_code=status.HTTP_423_LOCKED,
detail=f"{item.name} is a fixture and cannot be edited or deleted.",
)
item.name = data.name
db.commit()
return section_info(item)
@@ -79,6 +84,11 @@ def delete(
):
try:
item: Section = db.query(Section).filter(Section.id == id_).first()
if item.is_fixture:
raise HTTPException(
status_code=status.HTTP_423_LOCKED,
detail=f"{item.name} is a fixture and cannot be edited or deleted.",
)
db.delete(item)
db.commit()
return section_info(None)
+4
View File
@@ -101,10 +101,14 @@ class UserList(BaseModel):
id_: uuid.UUID
name: str
roles: List[str]
last_device: str
last_date: Optional[datetime]
class Config:
fields = {"id_": "id"}
anystr_strip_whitespace = True
alias_generator = to_camel
json_encoders = {datetime: lambda v: v.strftime("%d-%b-%Y %H:%M")}
class UserToken(BaseModel):
+8
View File
@@ -1,5 +1,6 @@
import uuid
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, Field
@@ -10,6 +11,7 @@ from .section import SectionLink
class DeviceIn(BaseModel):
name: str = Field(..., min_length=1)
enabled: bool
section: SectionLink
class Config:
@@ -19,9 +21,15 @@ class DeviceIn(BaseModel):
class Device(DeviceIn):
id_: uuid.UUID
creation_date: datetime
last_user: str
last_date: Optional[datetime]
class Config:
fields = {"id_": "id"}
anystr_strip_whitespace = True
alias_generator = to_camel
json_encoders = {datetime: lambda v: v.strftime("%d-%b-%Y %H:%M")}
class DeviceLink(BaseModel):
+7 -1
View File
@@ -4,7 +4,9 @@ import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { environment } from '../../environments/environment';
import { Device } from '../core/device';
import { User } from '../core/user';
import { CookieService } from '../shared/cookie.service';
const loginUrl = '/token';
const refreshUrl = '/refresh';
@@ -14,10 +16,14 @@ const JWT_USER = 'JWT_USER';
export class AuthService {
private currentUserSubject: BehaviorSubject<User>;
public currentUser: Observable<User>;
public device: Device;
constructor(private http: HttpClient) {
constructor(private http: HttpClient, private cs: CookieService) {
this.checkStorage();
this.currentUser = this.currentUserSubject.asObservable();
const deviceId = this.cs.getCookie('device_id');
const device = this.cs.getCookie('device');
this.device = new Device({ id: deviceId, name: device });
}
static parseJwt(token): User {
+8
View File
@@ -3,5 +3,13 @@ import { Section } from './section';
export class Device {
id: string;
name: string;
enabled: boolean;
section: Section;
creationDate: string;
lastUser: string;
lastDate?: string;
public constructor(init?: Partial<Device>) {
Object.assign(this, init);
}
}
+2
View File
@@ -11,6 +11,8 @@ export class User {
access_token?: string;
exp?: number;
ver: string;
lastDevice: string;
lastDate?: string;
public constructor(init?: Partial<User>) {
Object.assign(this, init);
@@ -39,6 +39,9 @@
</mat-select>
</mat-form-field>
</div>
<div fxLayout="row">
<mat-checkbox formControlName="enabled">Enabled?</mat-checkbox>
</div>
</form>
</mat-card-content>
<mat-card-actions>
@@ -47,5 +50,8 @@
Delete
</button>
</mat-card-actions>
<mat-card-subtitle>
Created on <strong>{{ item.creationDate | localTime }}</strong>
</mat-card-subtitle>
</mat-card>
</div>
@@ -35,6 +35,7 @@ export class DeviceDetailComponent implements OnInit, AfterViewInit {
this.form = this.fb.group({
name: '',
section: '',
enabled: '',
});
}
@@ -50,6 +51,7 @@ export class DeviceDetailComponent implements OnInit, AfterViewInit {
this.form.setValue({
name: this.item.name || '',
section: this.item.section.id ? this.item.section.id : '',
enabled: this.item.enabled,
});
}
@@ -100,6 +102,7 @@ export class DeviceDetailComponent implements OnInit, AfterViewInit {
const formModel = this.form.value;
this.item.name = formModel.name;
this.item.section.id = formModel.section;
this.item.enabled = formModel.enabled;
return this.item;
}
}
@@ -17,11 +17,31 @@
</ng-container>
<!-- Section Column -->
<ng-container matColumnDef="tax">
<ng-container matColumnDef="section">
<mat-header-cell *matHeaderCellDef>Section</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.section.name }}</mat-cell>
</ng-container>
<!-- Section Column -->
<ng-container matColumnDef="enabled">
<mat-header-cell *matHeaderCellDef>Enabled</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.enabled }}</mat-cell>
</ng-container>
<!-- Creation Date Column -->
<ng-container matColumnDef="creationDate">
<mat-header-cell *matHeaderCellDef>Created</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.creationDate | localTime }}</mat-cell>
</ng-container>
<!-- Last Login Column -->
<ng-container matColumnDef="last">
<mat-header-cell *matHeaderCellDef>Last Login</mat-header-cell>
<mat-cell *matCellDef="let row"
>{{ row.lastUser }} @ {{ row.lastDate ? (row.lastDate | localTime) : 'Never' }}</mat-cell
>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table>
@@ -14,7 +14,7 @@ export class DeviceListComponent implements OnInit {
dataSource: DeviceListDataSource;
list: Device[];
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['name'];
displayedColumns = ['name', 'section', 'enabled', 'creationDate', 'last'];
constructor(private route: ActivatedRoute) {}
+7 -11
View File
@@ -1,4 +1,4 @@
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable';
import { catchError } from 'rxjs/operators';
@@ -6,10 +6,7 @@ import { catchError } from 'rxjs/operators';
import { Device } from '../core/device';
import { ErrorLoggerService } from '../core/error-logger.service';
const httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
};
const url = '/v1/devices';
const url = '/api/devices';
const serviceName = 'DeviceService';
@Injectable({
@@ -19,7 +16,7 @@ export class DeviceService {
constructor(private http: HttpClient, private log: ErrorLoggerService) {}
get(id: string): Observable<Device> {
const getUrl: string = id === null ? `${url}/new` : `${url}/${id}`;
const getUrl: string = id === null ? `${url}` : `${url}/${id}`;
return <Observable<Device>>(
this.http
.get<Device>(getUrl)
@@ -28,10 +25,9 @@ export class DeviceService {
}
list(): Observable<Device[]> {
const options = { params: new HttpParams().set('l', '') };
return <Observable<Device[]>>(
this.http
.get<Device[]>(url, options)
.get<Device[]>(`${url}/list`)
.pipe(catchError(this.log.handleError(serviceName, 'list')))
);
}
@@ -39,7 +35,7 @@ export class DeviceService {
save(device: Device): Observable<Device> {
return <Observable<Device>>(
this.http
.post<Device>(`${url}/new`, device, httpOptions)
.post<Device>(`${url}`, device)
.pipe(catchError(this.log.handleError(serviceName, 'save')))
);
}
@@ -47,7 +43,7 @@ export class DeviceService {
update(device: Device): Observable<Device> {
return <Observable<Device>>(
this.http
.put<Device>(`${url}/${device.id}`, device, httpOptions)
.put<Device>(`${url}/${device.id}`, device)
.pipe(catchError(this.log.handleError(serviceName, 'update')))
);
}
@@ -62,7 +58,7 @@ export class DeviceService {
delete(id: string): Observable<Device> {
return <Observable<Device>>(
this.http
.delete<Device>(`${url}/${id}`, httpOptions)
.delete<Device>(`${url}/${id}`)
.pipe(catchError(this.log.handleError(serviceName, 'delete')))
);
}
+5
View File
@@ -5,6 +5,7 @@ import { FlexLayoutModule } from '@angular/flex-layout';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatOptionModule } from '@angular/material/core';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
@@ -12,6 +13,8 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { MatTableModule } from '@angular/material/table';
import { SharedModule } from '../shared/shared.module';
import { DeviceDetailComponent } from './device-detail/device-detail.component';
import { DeviceListComponent } from './device-list/device-list.component';
import { DevicesRoutingModule } from './devices-routing.module';
@@ -31,6 +34,8 @@ import { DevicesRoutingModule } from './devices-routing.module';
MatTableModule,
ReactiveFormsModule,
DevicesRoutingModule,
MatCheckboxModule,
SharedModule,
],
declarations: [DeviceListComponent, DeviceDetailComponent],
})
+1 -1
View File
@@ -199,5 +199,5 @@
</mat-card>
</div>
<footer class="footer">
<p>Backend: v{{ auth.user?.ver }} / Frontend: v{{ version }}</p>
<p>Backend: v{{ auth.user?.ver }} / Frontend: v{{ version }} on {{ auth.device.name }}</p>
</footer>
@@ -32,6 +32,15 @@
</mat-cell>
</ng-container>
<!-- Last Login Column -->
<ng-container matColumnDef="last">
<mat-header-cell *matHeaderCellDef>Last Login</mat-header-cell>
<mat-cell *matCellDef="let row"
>{{ row.lastDevice }} @
{{ row.lastDate ? (row.lastDate | localTime) : 'Never' }}</mat-cell
>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table>
@@ -14,7 +14,7 @@ export class UserListComponent implements OnInit {
dataSource: UserListDataSource;
list: User[];
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['name', 'lockedOut', 'roles'];
displayedColumns = ['name', 'lockedOut', 'roles', 'last'];
constructor(private route: ActivatedRoute) {}