diff --git a/brewman/brewman/models/auth.py b/brewman/brewman/models/auth.py index 9eb6c362..cc21745e 100644 --- a/brewman/brewman/models/auth.py +++ b/brewman/brewman/models/auth.py @@ -1,11 +1,22 @@ +from __future__ import annotations + import random import string import uuid from datetime import datetime from hashlib import md5 +from typing import List, Optional -from sqlalchemy import Boolean, Column, DateTime, Integer, Unicode, UniqueConstraint +from sqlalchemy import ( + Boolean, + Column, + DateTime, + Integer, + Unicode, + UniqueConstraint, + desc, +) from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Session, relationship, synonym from sqlalchemy.schema import ForeignKey, Table @@ -17,6 +28,26 @@ def encrypt(val): return md5(val.encode("utf-8") + "Salt".encode("utf-8")).hexdigest() +class LoginHistory(Base): + __tablename__ = "auth_login_history" + __table_args__ = (UniqueConstraint("user_id", "client_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("auth_users.id"), nullable=False) + client_id = Column( + "client_id", + UUID(as_uuid=True), + ForeignKey("auth_clients.client_id"), + nullable=False, + ) + date = Column("date", DateTime(timezone=True), nullable=False) + + def __init__(self, user_id=None, client_id=None, date=None, id_=None): + self.user_id = user_id + self.client_id = client_id + self.date = datetime.utcnow() if date is None else date + self.id = id_ + + class Client(Base): __tablename__ = "auth_clients" @@ -27,7 +58,7 @@ class Client(Base): otp = Column("otp", Integer) creation_date = Column("creation_date", DateTime(timezone=True), nullable=False) - login_history = relationship("LoginHistory", backref="client") + login_history: List[LoginHistory] = relationship("LoginHistory", order_by=desc(LoginHistory.date), backref="client") def __init__( self, @@ -80,8 +111,8 @@ class User(Base): _password = Column("password", Unicode(60)) locked_out = Column("disabled", Boolean) - roles = relationship("Role", secondary=user_role) - login_history = relationship("LoginHistory", backref="user") + roles: List[Role] = relationship("Role", secondary=user_role) + login_history: List[LoginHistory] = relationship("LoginHistory", order_by=desc(LoginHistory.date), backref="user") def _get_password(self): return self._password @@ -103,7 +134,7 @@ class User(Base): self.id = id_ @classmethod - def auth(cls, name, password, db) -> any: + def auth(cls, name: str, password: str, db: Session) -> Optional[User]: if password is None: return None user = db.query(User).filter(User.name.ilike(name)).first() @@ -115,26 +146,6 @@ class User(Base): return user -class LoginHistory(Base): - __tablename__ = "auth_login_history" - __table_args__ = (UniqueConstraint("user_id", "client_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("auth_users.id"), nullable=False) - client_id = Column( - "client_id", - UUID(as_uuid=True), - ForeignKey("auth_clients.client_id"), - nullable=False, - ) - date = Column("date", DateTime(timezone=True), nullable=False) - - def __init__(self, user_id=None, client_id=None, date=None, id_=None): - self.user_id = user_id - self.client_id = client_id - self.date = datetime.utcnow() if date is None else date - self.id = id_ - - class Role(Base): __tablename__ = "auth_roles" @@ -152,7 +163,7 @@ class Permission(Base): id = Column("id", UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) name = Column("name", Unicode(255), unique=True) - roles = relationship("Role", secondary=role_permission, backref="permissions") + roles: List[Role] = relationship("Role", secondary=role_permission, backref="permissions") def __init__(self, name=None, id_=None): self.name = name diff --git a/brewman/brewman/routers/auth/client.py b/brewman/brewman/routers/auth/client.py index 772d901b..ce4012b1 100644 --- a/brewman/brewman/routers/auth/client.py +++ b/brewman/brewman/routers/auth/client.py @@ -1,5 +1,7 @@ import uuid +from typing import List + import brewman.schemas.client as schemas from fastapi import APIRouter, Depends, HTTPException, Security, status @@ -74,28 +76,25 @@ def delete( raise -@router.get("/list") +@router.get("/list", response_model=List[schemas.ClientList]) async def show_list( db: Session = Depends(get_db), user: UserToken = Security(get_user, scopes=["clients"]), -): +) -> List[schemas.ClientList]: 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") 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, - } + schemas.ClientList( + id=item.id, + code=item.code, + name=item.name, + enabled=item.enabled, + otp=item.otp, + creationDate=item.creation_date, + lastUser=item.login_history[0].user.name if len(item.login_history) else "None", + lastDate=item.login_history[0].date if len(item.login_history) else None, + ) ) return clients diff --git a/brewman/brewman/routers/auth/user.py b/brewman/brewman/routers/auth/user.py index b4e66ff8..f9bcead5 100644 --- a/brewman/brewman/routers/auth/user.py +++ b/brewman/brewman/routers/auth/user.py @@ -164,6 +164,8 @@ async def show_list( 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].client.name if len(item.login_history) else "None", + lastDate=item.login_history[0].date if len(item.login_history) else None, ) for item in db.query(User).order_by(User.name).all() ] diff --git a/brewman/brewman/schemas/client.py b/brewman/brewman/schemas/client.py index 2bb16d79..a88840af 100644 --- a/brewman/brewman/schemas/client.py +++ b/brewman/brewman/schemas/client.py @@ -3,6 +3,7 @@ import uuid from datetime import datetime from typing import Optional +from brewman.schemas import to_camel from pydantic.main import BaseModel @@ -16,3 +17,13 @@ class Client(ClientIn): id_: uuid.UUID code: int creation_date: datetime + + +class ClientList(Client): + last_user: str + last_date: Optional[datetime] + + class Config: + anystr_strip_whitespace = True + alias_generator = to_camel + json_encoders = {datetime: lambda v: v.strftime("%d-%b-%Y %H:%M")} diff --git a/brewman/brewman/schemas/product.py b/brewman/brewman/schemas/product.py index e5636885..090e3b8d 100644 --- a/brewman/brewman/schemas/product.py +++ b/brewman/brewman/schemas/product.py @@ -19,7 +19,7 @@ class ProductLink(BaseModel): class ProductIn(BaseModel): name: str = Field(..., min_length=1) units: str - fraction: Decimal = Field(le=1, default=1) + fraction: Decimal = Field(ge=1, default=1) fraction_units: str product_yield: Decimal = Field(gt=0, le=1, default=1) product_group: ProductGroupLink = Field(...) diff --git a/brewman/brewman/schemas/user.py b/brewman/brewman/schemas/user.py index 8f42d933..b150caa0 100644 --- a/brewman/brewman/schemas/user.py +++ b/brewman/brewman/schemas/user.py @@ -1,6 +1,7 @@ import uuid -from typing import List +from datetime import datetime +from typing import List, Optional from brewman.schemas import to_camel from brewman.schemas.role import RoleItem @@ -31,10 +32,13 @@ 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): diff --git a/overlord/src/app/client/client-list/client-list.component.html b/overlord/src/app/client/client-list/client-list.component.html index bdc47b69..0520e244 100644 --- a/overlord/src/app/client/client-list/client-list.component.html +++ b/overlord/src/app/client/client-list/client-list.component.html @@ -33,13 +33,15 @@ Created - {{ row.created | localTime }} + {{ row.creationDate | localTime }} - + Last Login - {{ row.lastLogin | localTime }} + {{ row.lastUser }} @ {{ row.lastDate ? (row.lastDate | localTime) : 'Never' }} diff --git a/overlord/src/app/client/client-list/client-list.component.ts b/overlord/src/app/client/client-list/client-list.component.ts index c2488173..6aedffb3 100644 --- a/overlord/src/app/client/client-list/client-list.component.ts +++ b/overlord/src/app/client/client-list/client-list.component.ts @@ -18,7 +18,7 @@ export class ClientListComponent implements OnInit { list: Client[] = []; dataSource: ClientListDataSource = new ClientListDataSource(this.list); /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ - displayedColumns = ['code', 'name', 'enabled', 'otp', 'created', 'lastLogin']; + displayedColumns = ['code', 'name', 'enabled', 'otp', 'created', 'last']; constructor(private route: ActivatedRoute) {} diff --git a/overlord/src/app/client/client.ts b/overlord/src/app/client/client.ts index 9a821e36..f7af0bbf 100644 --- a/overlord/src/app/client/client.ts +++ b/overlord/src/app/client/client.ts @@ -3,18 +3,18 @@ export class Client { name: string; code: number; enabled: boolean; - otp: number; + otp?: number; creationDate: string; - lastLogin: string; + lastUser: string; + lastDate?: string; public constructor(init?: Partial) { this.id = ''; this.name = ''; this.code = 0; this.enabled = true; - this.otp = 0; this.creationDate = ''; - this.lastLogin = ''; + this.lastUser = ''; Object.assign(this, init); } } diff --git a/overlord/src/app/core/user-group.ts b/overlord/src/app/core/user-role.ts similarity index 65% rename from overlord/src/app/core/user-group.ts rename to overlord/src/app/core/user-role.ts index 517a00ea..1cb6145e 100644 --- a/overlord/src/app/core/user-group.ts +++ b/overlord/src/app/core/user-role.ts @@ -1,9 +1,9 @@ -export class UserGroup { +export class UserRole { id: string | undefined; name: string; enabled: boolean; - public constructor(init?: Partial) { + public constructor(init?: Partial) { this.name = ''; this.enabled = true; Object.assign(this, init); diff --git a/overlord/src/app/core/user.ts b/overlord/src/app/core/user.ts index 17fdbc3c..cd4a85a7 100644 --- a/overlord/src/app/core/user.ts +++ b/overlord/src/app/core/user.ts @@ -1,16 +1,18 @@ -import { UserGroup } from './user-group'; +import { UserRole } from './user-role'; export class User { id: string | undefined; name: string; password: string; lockedOut: boolean; - roles: UserGroup[]; + roles: UserRole[]; perms: string[]; isAuthenticated: boolean; access_token?: string; exp: number; ver: string; + lastDevice: string; + lastDate?: string; public constructor(init?: Partial) { this.name = ''; @@ -21,6 +23,7 @@ export class User { this.isAuthenticated = false; this.exp = 0; this.ver = ''; + this.lastDevice = ''; Object.assign(this, init); } } diff --git a/overlord/src/app/user/user-list/user-list.component.html b/overlord/src/app/user/user-list/user-list.component.html index 5a40fa18..7cdffd11 100644 --- a/overlord/src/app/user/user-list/user-list.component.html +++ b/overlord/src/app/user/user-list/user-list.component.html @@ -22,16 +22,25 @@ {{ row.lockedOut }} - - - Groups + + + Roles
    -
  • {{ group }}
  • +
  • {{ role }}
+ + + Last Login + {{ row.lastDevice }} @ + {{ row.lastDate ? (row.lastDate | localTime) : 'Never' }} + + diff --git a/overlord/src/app/user/user-list/user-list.component.ts b/overlord/src/app/user/user-list/user-list.component.ts index 9538e86e..68e1cb4b 100644 --- a/overlord/src/app/user/user-list/user-list.component.ts +++ b/overlord/src/app/user/user-list/user-list.component.ts @@ -18,7 +18,7 @@ export class UserListComponent implements OnInit { list: User[] = []; dataSource: UserListDataSource = new UserListDataSource(this.list); /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ - displayedColumns = ['name', 'lockedOut', 'groups']; + displayedColumns = ['name', 'lockedOut', 'roles', 'last']; constructor(private route: ActivatedRoute) {}