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) {}