feat: Seaca mobile API, enrollment, FCM push and admin UI (0.9.0)
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>
This commit is contained in:
@@ -9,6 +9,10 @@ from app.models.login_attempt import LoginAttempt
|
||||
from app.models.ui_settings import UiSettings
|
||||
from app.models.event_severity_override import EventSeverityOverride
|
||||
from app.models.user import User
|
||||
from app.models.mobile_settings import MobileSettings
|
||||
from app.models.mobile_enrollment_code import MobileEnrollmentCode
|
||||
from app.models.mobile_device import MobileDevice
|
||||
from app.models.mobile_refresh_token import MobileRefreshToken
|
||||
|
||||
__all__ = [
|
||||
"ApiKey",
|
||||
@@ -23,4 +27,8 @@ __all__ = [
|
||||
"EventSeverityOverride",
|
||||
"LoginAttempt",
|
||||
"UiSettings",
|
||||
"MobileSettings",
|
||||
"MobileEnrollmentCode",
|
||||
"MobileDevice",
|
||||
"MobileRefreshToken",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class MobileDevice(Base):
|
||||
__tablename__ = "mobile_devices"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
user_id: Mapped[int] = mapped_column(ForeignKey("sac_users.id", ondelete="CASCADE"), index=True)
|
||||
device_uuid: Mapped[str] = mapped_column(String(64), unique=True, index=True)
|
||||
display_name: Mapped[str] = mapped_column(String(128), default="")
|
||||
platform: Mapped[str] = mapped_column(String(32), default="android")
|
||||
app_version: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||||
fcm_token: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
fcm_token_updated_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
enrollment_code_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("mobile_enrollment_codes.id", ondelete="SET NULL"),
|
||||
)
|
||||
enrolled_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
last_seen_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
@@ -0,0 +1,23 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Integer, String, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class MobileEnrollmentCode(Base):
|
||||
__tablename__ = "mobile_enrollment_codes"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
label: Mapped[str] = mapped_column(String(128), default="")
|
||||
code_hash: Mapped[str] = mapped_column(String(64), unique=True, index=True)
|
||||
code_prefix: Mapped[str] = mapped_column(String(16))
|
||||
created_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("sac_users.id", ondelete="SET NULL"))
|
||||
target_user_id: Mapped[int | None] = mapped_column(ForeignKey("sac_users.id", ondelete="SET NULL"))
|
||||
login_mode: Mapped[str] = mapped_column(String(16), default="password")
|
||||
max_uses: Mapped[int] = mapped_column(Integer, default=1)
|
||||
use_count: Mapped[int] = mapped_column(Integer, default=0)
|
||||
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
||||
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
@@ -0,0 +1,17 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class MobileRefreshToken(Base):
|
||||
__tablename__ = "mobile_refresh_tokens"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
device_id: Mapped[int] = mapped_column(ForeignKey("mobile_devices.id", ondelete="CASCADE"))
|
||||
token_hash: Mapped[str] = mapped_column(String(64), unique=True, index=True)
|
||||
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
@@ -0,0 +1,21 @@
|
||||
from sqlalchemy import Boolean, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.database import Base
|
||||
|
||||
MOBILE_SETTINGS_ROW_ID = 1
|
||||
|
||||
LOGIN_MODE_PASSWORD = "password"
|
||||
LOGIN_MODE_CODE_ONLY = "code_only"
|
||||
LOGIN_MODES = frozenset({LOGIN_MODE_PASSWORD, LOGIN_MODE_CODE_ONLY})
|
||||
|
||||
|
||||
class MobileSettings(Base):
|
||||
"""Singleton: глобальные настройки мобильного клиента Seaca."""
|
||||
|
||||
__tablename__ = "mobile_settings"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
devices_allowed: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
max_devices_per_user: Mapped[int] = mapped_column(Integer, default=3)
|
||||
min_app_version: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||||
@@ -16,3 +16,4 @@ class NotificationPolicy(Base):
|
||||
use_telegram: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
use_webhook: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
use_email: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
use_mobile: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
|
||||
Reference in New Issue
Block a user