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:
@@ -67,14 +67,9 @@ bearer_scheme = HTTPBearer(auto_error=False)
|
||||
|
||||
|
||||
class CurrentUser:
|
||||
|
||||
|
||||
|
||||
username: str
|
||||
|
||||
|
||||
|
||||
role: str
|
||||
device_id: int | None = None
|
||||
|
||||
|
||||
|
||||
@@ -102,22 +97,12 @@ class CurrentUser:
|
||||
|
||||
|
||||
|
||||
def create_access_token(subject: str, role: str) -> str:
|
||||
|
||||
|
||||
|
||||
def create_access_token(subject: str, role: str, *, device_id: int | None = None) -> str:
|
||||
settings = get_settings()
|
||||
|
||||
|
||||
|
||||
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.jwt_expire_minutes)
|
||||
|
||||
|
||||
|
||||
payload = {"sub": subject, "role": role, "exp": expire}
|
||||
|
||||
|
||||
|
||||
payload: dict[str, object] = {"sub": subject, "role": role, "exp": expire}
|
||||
if device_id is not None:
|
||||
payload["device_id"] = device_id
|
||||
return jwt.encode(payload, settings.jwt_secret, algorithm=settings.jwt_algorithm)
|
||||
|
||||
|
||||
@@ -130,6 +115,33 @@ def create_access_token(subject: str, role: str) -> str:
|
||||
|
||||
|
||||
|
||||
def _assert_mobile_device_active(db: Session, device_id: int, user_id: int) -> None:
|
||||
from app.models.mobile_device import MobileDevice
|
||||
|
||||
device = db.get(MobileDevice, device_id)
|
||||
if device is None or device.user_id != user_id or device.revoked_at is not None:
|
||||
raise HTTPException(status_code=401, detail="Mobile device revoked or invalid")
|
||||
|
||||
|
||||
def get_token_device_id(token: str) -> int | None:
|
||||
settings = get_settings()
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
settings.jwt_secret,
|
||||
algorithms=[settings.jwt_algorithm],
|
||||
)
|
||||
except JWTError:
|
||||
return None
|
||||
raw = payload.get("device_id")
|
||||
if raw is None:
|
||||
return None
|
||||
try:
|
||||
return int(raw)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _user_from_db(db: Session, username: str) -> User:
|
||||
|
||||
|
||||
@@ -206,23 +218,22 @@ def _decode_current_user(token: str, db: Session | None = None) -> CurrentUser:
|
||||
|
||||
|
||||
|
||||
parsed_device_id: int | None = None
|
||||
raw_device_id = payload.get("device_id")
|
||||
if raw_device_id is not None:
|
||||
try:
|
||||
parsed_device_id = int(raw_device_id)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise HTTPException(status_code=401, detail="Invalid token") from exc
|
||||
|
||||
if db is not None:
|
||||
|
||||
|
||||
|
||||
user = _user_from_db(db, str(username))
|
||||
|
||||
|
||||
|
||||
return CurrentUser(username=user.username, role=user.role)
|
||||
|
||||
|
||||
if parsed_device_id is not None:
|
||||
_assert_mobile_device_active(db, parsed_device_id, user.id)
|
||||
return CurrentUser(username=user.username, role=user.role, device_id=parsed_device_id)
|
||||
|
||||
role = str(payload.get("role") or USER_ROLE_ADMIN)
|
||||
|
||||
|
||||
|
||||
return CurrentUser(username=str(username), role=role)
|
||||
return CurrentUser(username=str(username), role=role, device_id=parsed_device_id)
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user