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:
@@ -0,0 +1,68 @@
|
||||
"""Хеширование enrollment/refresh токенов и генерация."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import secrets
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.config import get_settings
|
||||
from app.models.mobile_refresh_token import MobileRefreshToken
|
||||
|
||||
|
||||
def hash_mobile_secret(raw: str) -> str:
|
||||
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def generate_enrollment_code() -> tuple[str, str, str]:
|
||||
"""(plaintext, prefix, hash)"""
|
||||
raw = f"sacmob_{secrets.token_urlsafe(18)}"
|
||||
prefix = raw[:12]
|
||||
return raw, prefix, hash_mobile_secret(raw)
|
||||
|
||||
|
||||
def generate_refresh_token() -> tuple[str, str]:
|
||||
"""(plaintext, hash)"""
|
||||
raw = secrets.token_urlsafe(48)
|
||||
return raw, hash_mobile_secret(raw)
|
||||
|
||||
|
||||
def store_refresh_token(db: Session, *, device_id: int) -> str:
|
||||
settings = get_settings()
|
||||
days = max(1, int(settings.sac_mobile_refresh_expire_days))
|
||||
plaintext, token_hash = generate_refresh_token()
|
||||
expires_at = datetime.now(timezone.utc) + timedelta(days=days)
|
||||
row = MobileRefreshToken(
|
||||
device_id=device_id,
|
||||
token_hash=token_hash,
|
||||
expires_at=expires_at,
|
||||
)
|
||||
db.add(row)
|
||||
return plaintext
|
||||
|
||||
|
||||
def revoke_refresh_tokens_for_device(db: Session, device_id: int) -> None:
|
||||
now = datetime.now(timezone.utc)
|
||||
rows = db.scalars(
|
||||
select(MobileRefreshToken).where(
|
||||
MobileRefreshToken.device_id == device_id,
|
||||
MobileRefreshToken.revoked_at.is_(None),
|
||||
)
|
||||
).all()
|
||||
for row in rows:
|
||||
row.revoked_at = now
|
||||
|
||||
|
||||
def find_valid_refresh_token(db: Session, raw_token: str) -> MobileRefreshToken | None:
|
||||
token_hash = hash_mobile_secret(raw_token)
|
||||
now = datetime.now(timezone.utc)
|
||||
return db.scalar(
|
||||
select(MobileRefreshToken).where(
|
||||
MobileRefreshToken.token_hash == token_hash,
|
||||
MobileRefreshToken.revoked_at.is_(None),
|
||||
MobileRefreshToken.expires_at > now,
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user