diff --git a/uffd/models/service.py b/uffd/models/service.py index 7aa928d429b50ddf4b7facd2c50c9cec8ff8e111..2fa9fb80aedbe9588257d67391e866b15710eb8c 100644 --- a/uffd/models/service.py +++ b/uffd/models/service.py @@ -2,7 +2,7 @@ import enum from flask import current_app from flask_babel import get_locale -from sqlalchemy import Column, Integer, String, ForeignKey, Boolean, Enum +from sqlalchemy import Column, Integer, String, ForeignKey, Boolean, Enum, DateTime from sqlalchemy.orm import relationship, validates from uffd.database import db @@ -55,6 +55,11 @@ class ServiceUser(db.Model): user_id = Column(Integer(), ForeignKey('user.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False) user = relationship('User', viewonly=True) + needs_sync = Column(Boolean()) + last_sync = Column(DateTime()) + sync_token = Column(String(32)) + sync_started = Column(DateTime()) + @property def has_access(self): return not self.service.limit_access or self.service.access_group in self.user.groups @@ -168,6 +173,27 @@ class ServiceUser(db.Model): ) return query.filter(db.and_(db.not_(remailer_enabled), real_email_matches)) +@db.event.listens_for(db.Session, 'after_flush') # pylint: disable=no-member +def mark_service_users_changed(session, flush_context): # pylint: disable=unused-argument + user_ids = set() + service_ids = set() + for obj in session.dirty: + if isinstance(obj, User): + user_ids.add(obj.id) + # We modify ServiceUser during the sync API call, so if we want to react + # to ServiceUser changes, we must be more selective + #elif isinstance(obj, ServiceUser): + # user_ids.add(obj.user_id) + elif isinstance(obj, Service): + service_ids.add(obj.id) + ServiceUser.query.filter(db.or_( + ServiceUser.service_id.in_(service_ids), + ServiceUser.user_id.in_(user_ids), + )).update(dict( + needs_sync=True, + sync_token=None, + ), synchronize_session=False) + @db.event.listens_for(db.Session, 'after_flush') # pylint: disable=no-member def create_service_users(session, flush_context): # pylint: disable=unused-argument # pylint completely fails to understand SQLAlchemy's query functions diff --git a/uffd/views/api.py b/uffd/views/api.py index 815a4d4fa3905f57aa00bbef8aec9ccd4b1c24d7..f7c499345404cf0310f2eb3608bb666a851cd8f0 100644 --- a/uffd/views/api.py +++ b/uffd/views/api.py @@ -1,4 +1,6 @@ import functools +import datetime +from secrets import token_hex from flask import Blueprint, jsonify, request, abort, Response @@ -222,3 +224,50 @@ def prometheus_metrics(): registry.register(PLATFORM_COLLECTOR) registry.register(UffdCollector()) return Response(response=generate_latest(registry=registry),content_type=CONTENT_TYPE_LATEST) + +@bp.route('/sync', methods=['GET']) +@apikey_required('users') +def start_sync(): + sync_token = token_hex(16) + service_users = ServiceUser.query.filter( + ServiceUser.service == request.api_client.service, + db.or_( + ServiceUser.needs_sync == True, + ServiceUser.needs_sync == None, + ServiceUser.last_sync < datetime.datetime.utcnow() - datetime.timedelta(hours=24), + ), + db.or_( + ServiceUser.sync_token == None, + ServiceUser.sync_started < datetime.datetime.utcnow() - datetime.timedelta(seconds=10), + ) + ).order_by(ServiceUser.needs_sync, ServiceUser.last_sync.desc()).limit(10).with_for_update().all() + for service_user in service_users: + service_user.sync_started = datetime.datetime.utcnow() + service_user.sync_token = sync_token + db.session.commit() + return jsonify([ + { + 'sync_token': f'{sync_token}-{service_user.user_id}', + 'data': generate_user_dict(service_user), + } + for service_user in service_users + ]) + +@bp.route('/sync', methods=['POST']) +@apikey_required('users') +def finish_sync(): + for result in request.json: + sync_token, user_id = result['sync_token'].split('-', 1) + query = ServiceUser.query.filter_by( + service=request.api_client.service, + user_id=user_id, + sync_token=sync_token, + ) + if result['status'] == 'ok': + query.update(dict( + sync_token=None, + last_sync=datetime.datetime.utcnow(), + needs_sync=False, + )) + db.session.commit() + return 'OK', 200