Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Monitor users by time since last request #541

Merged
merged 17 commits into from Dec 9, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 12 additions & 2 deletions flexmeasures/auth/__init__.py
@@ -1,6 +1,6 @@
from flask import Flask
from flask_security import Security, SQLAlchemySessionUserDatastore
from flask_login import user_logged_in
from flask_login import user_logged_in, current_user
from werkzeug.exceptions import Forbidden, Unauthorized

from flexmeasures.data import db
Expand All @@ -21,7 +21,12 @@ def register_at(app: Flask):
unauthorized_handler,
unauthorized_handler_e,
) # noqa: F401
from flexmeasures.data.models.user import User, Role, remember_login # noqa: F401
from flexmeasures.data.models.user import (
User,
Role,
remember_login,
remember_last_seen,
) # noqa: F401

# Setup Flask-Security-Too for user authentication & authorization
user_datastore = SQLAlchemySessionUserDatastore(db.session, User, Role)
Expand All @@ -39,3 +44,8 @@ def register_at(app: Flask):

# add our custom handler for a user login event
user_logged_in.connect(remember_login)

# also store when the last contact was
@app.before_request
def record_last_seen():
remember_last_seen(current_user)
28 changes: 26 additions & 2 deletions flexmeasures/cli/monitor.py
Expand Up @@ -54,7 +54,31 @@ def send_monitoring_alert(
app.logger.error(f"{msg} {latest_run_txt} NOTE: {custom_msg}")


@fm_monitor.command("tasks") # TODO: a better name would be "latest-run"
@fm_monitor.command("tasks") # TODO: deprecate, this is the old name
@click.option(
"--task",
type=(str, int),
multiple=True,
required=True,
help="The name of the task and the maximal allowed minutes between successful runs. Use multiple times if needed.",
)
@click.option(
"--custom-message",
type=str,
default="",
help="Add this message to the monitoring alert (if one is sent).",
)
@click.pass_context
def monitor_task(ctx, task, custom_message):
"""
DEPRECATED, use `latest-run`.
nhoening marked this conversation as resolved.
Show resolved Hide resolved
Check if the given task's last successful execution happened less than the allowed time ago.
If not, alert someone, via email or sentry.
"""
ctx.forward(monitor_latest_run)


@fm_monitor.command("latest-run")
@with_appcontext
@click.option(
"--task",
Expand All @@ -69,7 +93,7 @@ def send_monitoring_alert(
default="",
help="Add this message to the monitoring alert (if one is sent).",
)
def monitor_tasks(task, custom_message):
def monitor_latest_run(task, custom_message):
"""
Check if the given task's last successful execution happened less than the allowed time ago.
If not, alert someone, via email or sentry.
Expand Down
@@ -0,0 +1,27 @@
"""new field last_seen_at in user model

Revision ID: 75f53d2dbfae
Revises: 650b085c0ad3
Create Date: 2022-11-27 00:15:26.403169

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = "75f53d2dbfae"
down_revision = "650b085c0ad3"
branch_labels = None
depends_on = None


def upgrade():
op.add_column("fm_user", sa.Column("last_seen_at", sa.DateTime(), nullable=True))
op.execute(
"update fm_user set last_seen_at = last_login_at where last_seen_at is null"
)


def downgrade():
op.drop_column("fm_user", "last_seen_at")
12 changes: 11 additions & 1 deletion flexmeasures/data/models/user.py
Expand Up @@ -17,6 +17,7 @@
)
from flexmeasures.data.models.parsing_utils import parse_source_arg
from flexmeasures.auth.policy import AuthModelMixin
from flexmeasures.utils.time_utils import server_now

if TYPE_CHECKING:
from flexmeasures.data.models.data_sources import DataSource
Expand Down Expand Up @@ -188,6 +189,7 @@ class User(db.Model, UserMixin, AuthModelMixin):
username = Column(String(255), unique=True)
password = Column(String(255))
last_login_at = Column(DateTime())
last_seen_at = db.Column(DateTime())
nhoening marked this conversation as resolved.
Show resolved Hide resolved
login_count = Column(Integer)
active = Column(Boolean())
# Faster token checking
Expand Down Expand Up @@ -258,12 +260,20 @@ def has_role(self, role: Union[str, Role]) -> bool:

def remember_login(the_app, user):
"""We do not use the tracking feature of flask_security, but this basic meta data are quite handy to know"""
user.last_login_at = datetime.utcnow()
user.last_login_at = server_now()
if user.login_count is None:
user.login_count = 0
user.login_count = user.login_count + 1


def remember_last_seen(user):
"""Update the last_seen field"""
if user is not None and user.is_authenticated:
user.last_seen_at = server_now()
db.session.add(user)
db.session.commit()


def is_user(o) -> bool:
"""True if object is or proxies a User, False otherwise.

Expand Down
1 change: 1 addition & 0 deletions flexmeasures/data/schemas/users.py
Expand Up @@ -27,3 +27,4 @@ def validate_timezone(self, timezone):
timezone = ma.auto_field()
flexmeasures_roles = ma.auto_field()
last_login_at = AwareDateTimeField()
last_seen_at = AwareDateTimeField()
7 changes: 3 additions & 4 deletions flexmeasures/ui/crud/users.py
Expand Up @@ -61,10 +61,9 @@ def process_internal_api_response(
role_ids = tuple(user_data.get("flexmeasures_roles", []))
user_data["flexmeasures_roles"] = Role.query.filter(Role.id.in_(role_ids)).all()
user_data.pop("status", None) # might have come from requests.response
if "last_login_at" in user_data and user_data["last_login_at"] is not None:
user_data["last_login_at"] = datetime.fromisoformat(
user_data["last_login_at"]
)
for date_field in ("last_login_at", "last_seen_at"):
if date_field in user_data and user_data[date_field] is not None:
user_data[date_field] = datetime.fromisoformat(user_data[date_field])
if user_id:
user_data["id"] = user_id
if make_obj:
Expand Down
12 changes: 10 additions & 2 deletions flexmeasures/ui/templates/admin/logged_in_user.html
Expand Up @@ -62,10 +62,18 @@ <h2>User overview</h2>
</tr>
<tr>
<td>
Last login was
Last login
</td>
<td title="{{ logged_in_user.last_login_at | localized_datetime }}">
{{ logged_in_user.last_login_at | naturalized_datetime }}
</td>
</tr>
<tr>
<td>
{{ logged_in_user.last_login_at | localized_datetime }}
Last seen
</td>
<td title="{{ logged_in_user.last_seen_at | localized_datetime }}">
{{ logged_in_user.last_seen_at | naturalized_datetime }}
</td>
</tr>
<tr>
Expand Down
10 changes: 9 additions & 1 deletion flexmeasures/ui/templates/crud/user.html
Expand Up @@ -70,8 +70,16 @@ <h2>User overview</h2>
<td>
Last login was
</td>
<td title="{{ user.last_login_at | localized_datetime }}">
{{ user.last_login_at | naturalized_datetime }}
</td>
</tr>
<tr>
<td>
{{ user.last_login_at | localized_datetime }}
Last seen
</td>
<td title="{{ user.last_seen_at | localized_datetime }}">
{{ user.last_seen_at | naturalized_datetime }}
</td>
</tr>
<tr>
Expand Down
6 changes: 5 additions & 1 deletion flexmeasures/ui/templates/crud/users.html
Expand Up @@ -29,6 +29,7 @@ <h3>All {% if not include_inactive %}active {% endif %}users</h3>
<th>Account</th>
<th>Timezone</th>
<th>Last Login</th>
<th>Last Seen</th>
<th>Active</th>
</tr>
</thead>
Expand All @@ -51,9 +52,12 @@ <h3>All {% if not include_inactive %}active {% endif %}users</h3>
<td>
{{ user.timezone }}
</td>
<td>
<td title="{{ user.last_login_at | localized_datetime }}">
{{ user.last_login_at | naturalized_datetime}}
</td>
<td title="{{ user.last_seen_at | localized_datetime }}">
{{ user.last_seen_at | naturalized_datetime}}
</td>
<td>
{{ user.active }}
</td>
Expand Down