Made database datetime timezone aware

This commit is contained in:
Ferdinand Thiessen 2020-10-20 17:53:29 +02:00
parent addfb7c7c4
commit db96d4b178
5 changed files with 44 additions and 12 deletions

View File

@ -21,6 +21,7 @@ class CustomJSONEncoder(JSONEncoder):
if isinstance(o, datetime): if isinstance(o, datetime):
return o.isoformat() return o.isoformat()
# Check if iterable # Check if iterable
try: try:
iterable = iter(o) iterable = iter(o)

View File

@ -14,7 +14,7 @@ from flaschengeist.system.decorator import login_required
from flaschengeist.system.controller import sessionController, userController, messageController from flaschengeist.system.controller import sessionController, userController, messageController
from flaschengeist.system.models.session import Session from flaschengeist.system.models.session import Session
session_controller = LocalProxy(lambda: sessionController.SessionController()) session_controller: sessionController.SessionController = LocalProxy(lambda: sessionController.SessionController())
auth_bp = Blueprint("auth", __name__) auth_bp = Blueprint("auth", __name__)

View File

@ -36,7 +36,7 @@ class SessionController(metaclass=Singleton):
access_token = Session.query.filter_by(token=token).one_or_none() access_token = Session.query.filter_by(token=token).one_or_none()
if access_token: if access_token:
logger.debug("token found, check if expired or invalid user agent differs") logger.debug("token found, check if expired or invalid user agent differs")
if access_token.expires >= datetime.utcnow() and ( if access_token.expires >= datetime.now(timezone.utc) and (
access_token.browser == user_agent.browser and access_token.platform == user_agent.platform access_token.browser == user_agent.browser and access_token.platform == user_agent.platform
): ):
if not permissions or access_token.user.has_permissions(permissions): if not permissions or access_token.user.has_permissions(permissions):
@ -71,7 +71,6 @@ class SessionController(metaclass=Singleton):
session.refresh() session.refresh()
db.session.add(session) db.session.add(session)
db.session.commit() db.session.commit()
logger.debug("access token is {{ {} }}".format(session.token)) logger.debug("access token is {{ {} }}".format(session.token))
return session return session
@ -95,8 +94,7 @@ class SessionController(metaclass=Singleton):
def get_users_sessions(self, user): def get_users_sessions(self, user):
return Session.query.filter(Session._user == user) return Session.query.filter(Session._user == user)
@staticmethod def delete_session(self, token: Session):
def delete_session(token: Session):
"""Deletes given Session """Deletes given Session
Args: Args:
@ -105,8 +103,7 @@ class SessionController(metaclass=Singleton):
db.session.delete(token) db.session.delete(token)
db.session.commit() db.session.commit()
@staticmethod def update_session(self, session):
def update_session(session):
session.refresh() session.refresh()
db.session.commit() db.session.commit()
@ -117,6 +114,6 @@ class SessionController(metaclass=Singleton):
def clear_expired(self): def clear_expired(self):
"""Remove expired tokens from database""" """Remove expired tokens from database"""
logger.debug("Clear expired Sessions") logger.debug("Clear expired Sessions")
deleted = Session.query.filter(Session.expires < datetime.utcnow()).delete() deleted = Session.query.filter(Session.expires < datetime.now(timezone.utc)).delete()
logger.debug("{} sessions have been removed".format(deleted)) logger.debug("{} sessions have been removed".format(deleted))
db.session.commit() db.session.commit()

View File

@ -1,3 +1,7 @@
import datetime
from sqlalchemy.types import DateTime, TypeDecorator
class ModelSerializeMixin: class ModelSerializeMixin:
def serialize(self): def serialize(self):
d = {param: getattr(self, param) for param in self.__class__.__annotations__ if not param.startswith("_")} d = {param: getattr(self, param) for param in self.__class__.__annotations__ if not param.startswith("_")}
@ -5,3 +9,34 @@ class ModelSerializeMixin:
key, value = d.popitem() key, value = d.popitem()
return value return value
return d return d
class UtcDateTime(TypeDecorator):
"""Almost equivalent to :class:`~sqlalchemy.types.DateTime` with
``timezone=True`` option, but it differs from that by:
- Never silently take naive :class:`~datetime.datetime`, instead it
always raise :exc:`ValueError` unless time zone aware value.
- :class:`~datetime.datetime` value's :attr:`~datetime.datetime.tzinfo`
is always converted to UTC.
- Unlike SQLAlchemy's built-in :class:`~sqlalchemy.types.DateTime`,
it never return naive :class:`~datetime.datetime`, but time zone
aware value, even with SQLite or MySQL.
"""
impl = DateTime(timezone=True)
def process_bind_param(self, value, dialect):
if value is not None:
if not isinstance(value, datetime.datetime):
raise TypeError('expected datetime.datetime, not ' +
repr(value))
elif value.tzinfo is None:
raise ValueError('naive datetime is disallowed')
return value.astimezone(datetime.timezone.utc)
def process_result_value(self, value, dialect):
if value is not None:
if value.tzinfo is not None:
value = value.astimezone(datetime.timezone.utc)
value = value.replace(tzinfo=datetime.timezone.utc)
return value

View File

@ -1,6 +1,6 @@
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from . import ModelSerializeMixin from . import ModelSerializeMixin, UtcDateTime
from .user import User from .user import User
from ..database import db from ..database import db
from secrets import compare_digest from secrets import compare_digest
@ -17,7 +17,7 @@ class Session(db.Model, ModelSerializeMixin):
""" """
__tablename__ = "session" __tablename__ = "session"
expires: datetime = db.Column(db.DateTime) expires: datetime = db.Column(UtcDateTime)
token: str = db.Column(db.String(32), unique=True) token: str = db.Column(db.String(32), unique=True)
lifetime: int = db.Column(db.Integer) lifetime: int = db.Column(db.Integer)
browser: str = db.Column(db.String(30)) browser: str = db.Column(db.String(30))
@ -33,8 +33,7 @@ class Session(db.Model, ModelSerializeMixin):
Update the Timestamp to the current Time. Update the Timestamp to the current Time.
""" """
logger.debug("update timestamp from session with token {{ {} }}".format(self)) logger.debug("update timestamp from session with token {{ {} }}".format(self))
self.expires = datetime.utcnow() + timedelta(seconds=self.lifetime) self.expires = datetime.now(timezone.utc) + timedelta(seconds=self.lifetime)
self.expires.replace(tzinfo=timezone.utc)
def __eq__(self, token): def __eq__(self, token):
return compare_digest(self.token, token) return compare_digest(self.token, token)