Jun 24 - Implement devices tracker

This commit is contained in:
Nguyen Ngo
2026-06-24 17:00:00 -04:00
parent a40eaa9106
commit 6d249d88f1
8 changed files with 506 additions and 0 deletions
+3
View File
@@ -50,4 +50,7 @@ def register_api(app):
from app.api.comments import bp as comments_bp
api_bp.register_blueprint(comments_bp)
from app.api.devices import bp as devices_bp
api_bp.register_blueprint(devices_bp)
app.register_blueprint(api_bp)
+95
View File
@@ -0,0 +1,95 @@
"""
app/api/devices.py
------------------
Mobile API endpoint for device registration.
POST /api/v1/devices/register
Upserts a device record for the authenticated user.
Called on every app foreground (active scenePhase) so last_seen_at
stays current and the admin can identify stale / outdated installs.
Request JSON
------------
{
"device_id": "stable-uuid-from-keychain", // required
"device_name": "Nguyen's iPad", // UIDevice.current.name
"app_version": "1.2.0", // CFBundleShortVersionString
"ios_version": "18.3.1" // UIDevice.current.systemVersion
}
Response 200
------------
{ "ok": true, "data": { "registered": true } }
"""
import logging
from flask import Blueprint, request, g
from app import db
from app.models.device_registration import DeviceRegistration
from app.api.errors import api_ok, api_error
from app.api.decorators import jwt_required
from app.utils.audit import log_action, ACTION_UPDATE, ACTION_CREATE
from app.utils.time_utils import now_eastern
logger = logging.getLogger(__name__)
bp = Blueprint('api_devices', __name__)
_ALLOWED_ROLES = {'admin', 'director', 'inspector', 'project_manager'}
@bp.route('/devices/register', methods=['POST'])
@jwt_required
def register_device():
user = g.api_user
if user.role not in _ALLOWED_ROLES:
return api_error('Access denied', 403)
data = request.get_json(silent=True) or {}
device_id = (data.get('device_id') or '').strip()
device_name = (data.get('device_name') or '').strip()[:255]
app_version = (data.get('app_version') or '').strip()[:32]
ios_version = (data.get('ios_version') or '').strip()[:32]
if not device_id:
return api_error('device_id is required', 400)
if len(device_id) > 64:
return api_error('device_id too long', 400)
now = now_eastern()
existing = DeviceRegistration.query.filter_by(device_id=device_id).first()
if existing:
# Update — always refresh last_seen_at and app/ios version
existing.user_id = user.id # re-bind if different user logs in same device
existing.device_name = device_name or existing.device_name
existing.app_version = app_version or existing.app_version
existing.ios_version = ios_version or existing.ios_version
existing.last_seen_at = now
db.session.commit()
log_action(ACTION_UPDATE, 'DeviceRegistration', existing.id,
f'{device_name} v{app_version}',
f'user={user.username}; ios={ios_version}')
logger.info('API DEVICES | updated | device_id=%s | user=%s | app=%s',
device_id[:8], user.username, app_version)
else:
reg = DeviceRegistration(
device_id = device_id,
user_id = user.id,
device_name = device_name,
app_version = app_version,
ios_version = ios_version,
registered_at = now,
last_seen_at = now,
)
db.session.add(reg)
db.session.commit()
log_action(ACTION_CREATE, 'DeviceRegistration', reg.id,
f'{device_name} v{app_version}',
f'user={user.username}; ios={ios_version}')
logger.info('API DEVICES | registered | device_id=%s | user=%s | app=%s',
device_id[:8], user.username, app_version)
return api_ok({'registered': True})