diff --git a/brewman/models/__init__.py b/brewman/models/__init__.py index 9f5cd16f..279eb45e 100644 --- a/brewman/models/__init__.py +++ b/brewman/models/__init__.py @@ -28,7 +28,7 @@ def fixtures(engine): from brewman.models.messaging import Tag, Thread, Subscriber, thread_tag, Post from brewman.models.voucher import Attendance, Batch, Fingerprint, Inventory, Journal, Product, SalaryDeduction, Voucher, VoucherType from brewman.models.master import Product, AttendanceType, CostCenter, Employee, Ledger, LedgerBase, LedgerType, ProductGroup, MenuItem, Recipe, RecipeItem - from brewman.models.auth import Client, Group, Role, User, role_group, user_group + from brewman.models.auth import Client, Group, Role, User, role_group, user_group, LoginHistory Base.metadata.create_all(engine) diff --git a/brewman/models/auth.py b/brewman/models/auth.py index 96f76410..f05bf660 100644 --- a/brewman/models/auth.py +++ b/brewman/models/auth.py @@ -2,9 +2,10 @@ import random import string import uuid from hashlib import md5 +from datetime import datetime from sqlalchemy.schema import ForeignKey, Table -from sqlalchemy import Column, Boolean, Unicode, Integer +from sqlalchemy import Column, Boolean, Unicode, Integer, DateTime, UniqueConstraint from sqlalchemy.orm import synonym, relationship from brewman.models.guidtype import GUID @@ -19,17 +20,22 @@ def encrypt(val): class Client(Base): __tablename__ = 'auth_clients' - id = Column('ClientID', GUID(), primary_key=True, default=uuid.uuid4) - code = Column('Code', Integer, unique=True, nullable=False) - name = Column('Name', Unicode(255), unique=True, nullable=False) - enabled = Column('Enabled', Boolean, nullable=False) - otp = Column('OTP', Integer) + id = Column('client_id', GUID(), primary_key=True, default=uuid.uuid4) + code = Column('code', Integer, unique=True, nullable=False) + name = Column('name', Unicode(255), unique=True, nullable=False) + enabled = Column('enabled', Boolean, nullable=False) + otp = Column('otp', Integer) + creation_date = Column('creation_date', DateTime(timezone=True), nullable=False) - def __init__(self, code=None, name=None, enabled=False, otp=None): + login_history = relationship('LoginHistory', backref='client') + + def __init__(self, code=None, name=None, enabled=False, otp=None, creation_date=None, id=None): self.code = code self.name = name self.enabled = enabled self.otp = otp + self.creation_date = datetime.utcnow() if creation_date is None else creation_date + self.id = id @classmethod def by_id(cls, id): @@ -85,6 +91,7 @@ class User(Base): locked_out = Column('LockedOut', Boolean) groups = relationship("Group", secondary=user_group) + login_history = relationship('LoginHistory', backref='user') def _get_password(self): return self._password @@ -146,6 +153,21 @@ class User(Base): return query.order_by(cls.name) +class LoginHistory(Base): + __tablename__ = 'auth_login_history' + __table_args__ = (UniqueConstraint('user_id', 'client_id', 'date'), ) + id = Column('login_history_id', GUID(), primary_key=True, default=uuid.uuid4) + user_id = Column('user_id', GUID(), ForeignKey('auth_users.UserID'), nullable=False) + client_id = Column('client_id', GUID(), 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 Group(Base): __tablename__ = 'auth_groups' diff --git a/brewman/models/voucher.py b/brewman/models/voucher.py index 07f6252b..9207bf11 100644 --- a/brewman/models/voucher.py +++ b/brewman/models/voucher.py @@ -2,8 +2,9 @@ from datetime import datetime import uuid from sqlalchemy.dialects.postgresql import BYTEA +from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.hybrid import hybrid_property -from sqlalchemy import Column, Integer, Boolean, Unicode, DateTime, Numeric, ForeignKey, UniqueConstraint +from sqlalchemy import Column, Integer, Boolean, Unicode, DateTime, Numeric, ForeignKey, UniqueConstraint, Index from sqlalchemy.orm import relationship, synonym, backref from brewman.models.guidtype import GUID @@ -53,7 +54,7 @@ class Voucher(Base): __tablename__ = 'vouchers' id = Column('VoucherID', GUID(), primary_key=True, default=uuid.uuid4) - date = Column('Date', DateTime, nullable=False) + date = Column('Date', DateTime, nullable=False, index=True) reconcile_date = Column('ReconcileDate', DateTime, nullable=False) is_reconciled = Column('IsReconciled', Boolean, nullable=False) narration = Column('Narration', Unicode(1000), nullable=False) @@ -72,6 +73,8 @@ class Voucher(Base): salary_deductions = relationship('SalaryDeduction', backref='voucher', cascade="delete, delete-orphan", cascade_backrefs=False) + + def _get_type(self): return self._type # for item in VoucherType.list(): @@ -123,7 +126,7 @@ class Journal(Base): id = Column('JournalID', GUID(), primary_key=True, default=uuid.uuid4) debit = Column('Debit', Integer) amount = Column('Amount', Numeric) - voucher_id = Column('VoucherID', GUID(), ForeignKey('vouchers.VoucherID'), nullable=False) + voucher_id = Column('VoucherID', GUID(), ForeignKey('vouchers.VoucherID'), nullable=False, index=True) ledger_id = Column('LedgerID', GUID(), ForeignKey('ledgers.LedgerID'), nullable=False) cost_center_id = Column('CostCenterID', GUID(), ForeignKey('cost_centers.CostCenterID'), nullable=False) @@ -190,7 +193,7 @@ class Inventory(Base): __tablename__ = 'inventories' __table_args__ = (UniqueConstraint('VoucherID', 'BatchID'), ) id = Column('InventoryID', GUID(), primary_key=True, default=uuid.uuid4) - voucher_id = Column('VoucherID', GUID(), ForeignKey('vouchers.VoucherID'), nullable=False) + voucher_id = Column('VoucherID', GUID(), ForeignKey('vouchers.VoucherID'), nullable=False, index=True) product_id = Column('ProductID', GUID(), ForeignKey('products.ProductID'), nullable=False) batch_id = Column('BatchID', GUID(), ForeignKey('batches.BatchID'), nullable=False) quantity = Column('Quantity', Numeric) diff --git a/brewman/static/partial/client-list.html b/brewman/static/partial/client-list.html index 1fa72002..cb65faca 100644 --- a/brewman/static/partial/client-list.html +++ b/brewman/static/partial/client-list.html @@ -6,6 +6,7 @@ Name Enabled OTP + Created @@ -14,6 +15,7 @@ {{item.Name}} {{item.Enabled}} {{item.OTP}} + {{item.CreationDate | localTime}} diff --git a/brewman/views/auth/client.py b/brewman/views/auth/client.py index 5e128380..e05b522a 100644 --- a/brewman/views/auth/client.py +++ b/brewman/views/auth/client.py @@ -1,12 +1,12 @@ import uuid + import pkg_resources from pyramid.response import Response, FileResponse - from pyramid.view import view_config import transaction + from brewman.models import DBSession from brewman.models.auth import Client - from brewman.models.validation_exception import TryCatchFunction @@ -56,6 +56,7 @@ def show_list(request): for item in list: clients.append( {'ClientID': item.id, 'Code': item.code, 'Name': item.name, 'Enabled': item.enabled, 'OTP': item.otp, + 'CreationDate': item.creation_date.strftime('%d-%b-%Y %H:%M'), 'Url': request.route_url('client_id', id=item.id)}) return clients diff --git a/brewman/views/services/login.py b/brewman/views/services/login.py index 83db91fe..215aa8f4 100644 --- a/brewman/views/services/login.py +++ b/brewman/views/services/login.py @@ -1,11 +1,18 @@ +from datetime import datetime, timedelta + from pyramid.httpexceptions import HTTPFound from pyramid.response import Response -from pyramid.security import remember, forget +from pyramid.security import remember, forget from pyramid.view import view_config +from sqlalchemy import and_, or_ import transaction + from brewman import groupfinder -from brewman.models.auth import User, Client +from brewman.models import DBSession +from brewman.models.auth import User, Client, LoginHistory +from brewman.models.validation_exception import TryCatchFunction + @view_config(route_name='logout') def logout(request): @@ -15,32 +22,35 @@ def logout(request): @view_config(request_method='POST', route_name='api_login', renderer='json') +@TryCatchFunction def login(request): username = request.json_body.get('username', None) password = request.json_body.get('password', None) found, user = User.auth(username, password) - client = request.cookies.get('ClientID', None) + client = Client.by_code(request.cookies.get('ClientID', None)) otp = request.json_body.get('otp', None) client_name = request.json_body.get('ClientName', None) - allowed, response = check_client(client, otp, client_name, user if found else None) + allowed, client, response = check_client(client, otp, client_name, user if found else None) if found and allowed: headers = remember(request, str(user.id)) request.response.headers = headers - if allowed and isinstance(response, Client): - request.response.set_cookie('ClientID', value=str(response.code), max_age=10 * 365 * 24 * 60 * 60) + request.response.set_cookie('ClientID', value=str(client.code), max_age=365 * 24 * 60 * 60) + record_login(user.id, client) + transaction.commit() return request.response elif not found: response = Response("Login failed") response.status_int = 403 + transaction.commit() return response else: + transaction.commit() return response def check_client(client, otp, client_name, user): - client = None if client is None else Client.by_code(client) outside_login_allowed = False if user is None else True if 'Clients' in groupfinder(user.id, None) else False if len(Client.enabled_list()) == 0 and outside_login_allowed: @@ -48,41 +58,66 @@ def check_client(client, otp, client_name, user): client.otp = None client.name = 'Created on login by ' + user.name client.enabled = True - transaction.commit() - return True, client + return True, client, None if client is None: if outside_login_allowed: client = Client.create() - transaction.commit() - return True, client + return True, client, None else: client = Client.create() response = Response("Unknown Client") response.status_int = 403 response.set_cookie('ClientID', value=str(client.code), max_age=10 * 365 * 24 * 60 * 60) - transaction.commit() - return False, response + return False, None, response if client.enabled or outside_login_allowed: - return True, None + return True, client, None - if client.otp is None: + if not client.otp is None: response = Response("Client is Forbidden") response.status_int = 403 - return False, response + return False, None, response if otp is None: response = Response("OTP not supplied") response.status_int = 403 - return False, response + return False, None, response elif client.otp != int(otp): response = Response("OTP is wrong") response.status_int = 403 - return False, response + return False, None, response else: client.otp = None client.enabled = True client.name = client_name - transaction.commit() - return True, None + return True, client, None + + +def record_login(user_id, client): + history = LoginHistory(user_id) + history.client = client + DBSession.add(history) + + recent_logins = DBSession.query(LoginHistory.client_id.distinct()) \ + .filter(LoginHistory.date > datetime.utcnow() - timedelta(days=90)).subquery() + + deletable_clients = DBSession.query(Client.id) \ + .filter(Client.creation_date < datetime.utcnow() - timedelta(days=3)) \ + .filter(Client.enabled == False).subquery() + + LoginHistory.__table__.delete( + and_( + ~LoginHistory.client_id.in_(recent_logins), + LoginHistory.client_id.in_(deletable_clients) + ) + ).execute() + + Client.__table__.delete( + and_( + Client.creation_date < datetime.utcnow() - timedelta(days=3), + Client.enabled == False, + ~Client.id.in_(recent_logins) + ) + ).execute() +