563b836acc
Adds mobile device registration by admin codes, refresh tokens, push channel in notification policy, and Settings section for managing Seaca clients. Co-authored-by: Cursor <cursoragent@cursor.com>
151 lines
4.8 KiB
Python
151 lines
4.8 KiB
Python
"""Mobile / Seaca API."""
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
from app.models.mobile_device import MobileDevice
|
|
from app.models.mobile_enrollment_code import MobileEnrollmentCode
|
|
from app.models.mobile_settings import MOBILE_SETTINGS_ROW_ID, MobileSettings
|
|
from app.services.mobile_tokens import hash_mobile_secret
|
|
|
|
|
|
def _enable_mobile(db_session):
|
|
row = db_session.get(MobileSettings, MOBILE_SETTINGS_ROW_ID)
|
|
if row is None:
|
|
row = MobileSettings(id=MOBILE_SETTINGS_ROW_ID, devices_allowed=True, max_devices_per_user=3)
|
|
db_session.add(row)
|
|
else:
|
|
row.devices_allowed = True
|
|
db_session.commit()
|
|
|
|
|
|
def test_mobile_settings_admin(jwt_headers, client, db_session):
|
|
_enable_mobile(db_session)
|
|
response = client.get("/api/v1/mobile/settings", headers=jwt_headers)
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["devices_allowed"] is True
|
|
assert body["max_devices_per_user"] == 3
|
|
|
|
|
|
def test_create_enrollment_code_and_enroll(jwt_headers, client, db_session):
|
|
_enable_mobile(db_session)
|
|
create = client.post(
|
|
"/api/v1/mobile/enrollment-codes",
|
|
headers=jwt_headers,
|
|
json={"label": "test phone", "login_mode": "password", "expires_in_hours": 1},
|
|
)
|
|
assert create.status_code == 200
|
|
created = create.json()
|
|
assert created["enrollment_code"].startswith("sacmob_")
|
|
assert created["code_prefix"]
|
|
|
|
enroll = client.post(
|
|
"/api/v1/mobile/enroll",
|
|
json={
|
|
"enrollment_code": created["enrollment_code"],
|
|
"username": "test-monitor",
|
|
"password": "test-monitor-password",
|
|
"device_uuid": "uuid-test-device-001",
|
|
"display_name": "Pixel Test",
|
|
"fcm_token": "fcm-token-test",
|
|
},
|
|
)
|
|
assert enroll.status_code == 200
|
|
body = enroll.json()
|
|
assert body["username"] == "test-monitor"
|
|
assert body["role"] == "monitor"
|
|
assert body["device_id"] > 0
|
|
assert body["access_token"]
|
|
assert body["refresh_token"]
|
|
|
|
devices = client.get("/api/v1/mobile/devices", headers=jwt_headers)
|
|
assert devices.status_code == 200
|
|
assert len(devices.json()) == 1
|
|
assert devices.json()[0]["display_name"] == "Pixel Test"
|
|
|
|
|
|
def test_enroll_blocked_when_disabled(client, db_session, jwt_headers):
|
|
code_plain = "sacmob_testcode123456"
|
|
db_session.add(
|
|
MobileEnrollmentCode(
|
|
label="x",
|
|
code_hash=hash_mobile_secret(code_plain),
|
|
code_prefix=code_plain[:12],
|
|
login_mode="password",
|
|
max_uses=1,
|
|
use_count=0,
|
|
expires_at=datetime.now(timezone.utc) + timedelta(hours=1),
|
|
)
|
|
)
|
|
db_session.commit()
|
|
|
|
response = client.post(
|
|
"/api/v1/mobile/enroll",
|
|
json={
|
|
"enrollment_code": code_plain,
|
|
"username": "test-monitor",
|
|
"password": "test-monitor-password",
|
|
"device_uuid": "uuid-disabled-001",
|
|
},
|
|
)
|
|
assert response.status_code == 403
|
|
|
|
|
|
def test_refresh_token_rotation(client, db_session, jwt_headers):
|
|
_enable_mobile(db_session)
|
|
create = client.post(
|
|
"/api/v1/mobile/enrollment-codes",
|
|
headers=jwt_headers,
|
|
json={"login_mode": "password"},
|
|
)
|
|
code = create.json()["enrollment_code"]
|
|
enroll = client.post(
|
|
"/api/v1/mobile/enroll",
|
|
json={
|
|
"enrollment_code": code,
|
|
"username": "test-admin",
|
|
"password": "test-admin-password",
|
|
"device_uuid": "uuid-refresh-001",
|
|
},
|
|
)
|
|
data = enroll.json()
|
|
refresh = client.post(
|
|
"/api/v1/mobile/auth/refresh",
|
|
json={"refresh_token": data["refresh_token"], "device_uuid": "uuid-refresh-001"},
|
|
)
|
|
assert refresh.status_code == 200
|
|
refreshed = refresh.json()
|
|
assert refreshed["refresh_token"] != data["refresh_token"]
|
|
assert refreshed["device_id"] == data["device_id"]
|
|
|
|
|
|
def test_revoke_device_blocks_token(client, db_session, jwt_headers):
|
|
_enable_mobile(db_session)
|
|
create = client.post(
|
|
"/api/v1/mobile/enrollment-codes",
|
|
headers=jwt_headers,
|
|
json={"login_mode": "password"},
|
|
)
|
|
code = create.json()["enrollment_code"]
|
|
enroll = client.post(
|
|
"/api/v1/mobile/enroll",
|
|
json={
|
|
"enrollment_code": code,
|
|
"username": "test-monitor",
|
|
"password": "test-monitor-password",
|
|
"device_uuid": "uuid-revoke-001",
|
|
},
|
|
)
|
|
data = enroll.json()
|
|
headers = {"Authorization": f"Bearer {data['access_token']}"}
|
|
|
|
revoke = client.delete(f"/api/v1/mobile/devices/{data['device_id']}", headers=jwt_headers)
|
|
assert revoke.status_code == 200
|
|
|
|
me = client.put(
|
|
"/api/v1/mobile/devices/me/fcm",
|
|
headers=headers,
|
|
json={"fcm_token": "new-token"},
|
|
)
|
|
assert me.status_code == 401
|