|
# retoor <retoor@molodetz.nl>
|
|
|
|
import secrets
|
|
from datetime import datetime, timedelta, timezone
|
|
from typing import Optional
|
|
|
|
from devplacepy.database import get_table, get_int_setting, is_account_active
|
|
from devplacepy.config import SECONDS_PER_DAY
|
|
from devplacepy.utils import generate_uid
|
|
|
|
TOKEN_KEY_BYTES = 32 # 64-char hex, matching session token size
|
|
|
|
|
|
def issue_token(
|
|
user: dict,
|
|
label: str = "",
|
|
max_age_days: Optional[int] = None,
|
|
) -> dict:
|
|
"""Issue a DevPlace access token for *user*.
|
|
|
|
Returns a dict with *access_token*, *token_type*, *expires_in* (seconds),
|
|
*expires_at* (ISO), and *uid*.
|
|
"""
|
|
if max_age_days is None:
|
|
max_age_days = max(1, get_int_setting("session_max_age_days", 7))
|
|
max_age_seconds = max_age_days * SECONDS_PER_DAY
|
|
|
|
token = secrets.token_hex(TOKEN_KEY_BYTES)
|
|
now = datetime.now(timezone.utc)
|
|
expires_at = now + timedelta(seconds=max_age_seconds)
|
|
|
|
token_uid = generate_uid()
|
|
tokens = get_table("access_tokens")
|
|
tokens.insert(
|
|
{
|
|
"uid": token_uid,
|
|
"token": token,
|
|
"user_uid": user["uid"],
|
|
"label": label or "",
|
|
"expires_at": expires_at.isoformat(),
|
|
"created_at": now.isoformat(),
|
|
"deleted_at": None,
|
|
"deleted_by": None,
|
|
}
|
|
)
|
|
|
|
return {
|
|
"access_token": token,
|
|
"token_type": "bearer",
|
|
"expires_in": max_age_seconds,
|
|
"expires_at": expires_at.isoformat(),
|
|
"uid": token_uid,
|
|
}
|
|
|
|
|
|
def resolve_token(token: str) -> Optional[dict]:
|
|
"""Resolve a user from an access token string.
|
|
|
|
Returns the user dict or ``None`` when the token is invalid, expired, or
|
|
belongs to an inactive user.
|
|
"""
|
|
if not token:
|
|
return None
|
|
row = get_table("access_tokens").find_one(token=token, deleted_at=None)
|
|
if not row:
|
|
return None
|
|
|
|
expires_at = row.get("expires_at", "")
|
|
if expires_at:
|
|
try:
|
|
expires = datetime.fromisoformat(expires_at)
|
|
if expires.tzinfo is None:
|
|
expires = expires.replace(tzinfo=timezone.utc)
|
|
except (ValueError, TypeError):
|
|
return None
|
|
if expires < datetime.now(timezone.utc):
|
|
return None
|
|
|
|
user = get_table("users").find_one(uid=row.get("user_uid"))
|
|
if not user or not is_account_active(user):
|
|
return None
|
|
return user
|
|
|
|
|
|
def revoke_token(uid: str) -> bool:
|
|
"""Soft-delete a single access token by its uid. Returns ``True`` on success."""
|
|
tokens = get_table("access_tokens")
|
|
row = tokens.find_one(uid=uid, deleted_at=None)
|
|
if not row:
|
|
return False
|
|
stamp = datetime.now(timezone.utc).isoformat()
|
|
tokens.update({"id": row["id"], "deleted_at": stamp, "deleted_by": "manual"}, ["id"])
|
|
from devplacepy.utils.authcache import clear_user_cache
|
|
|
|
clear_user_cache(row["user_uid"])
|
|
return True
|
|
|
|
|
|
def revoke_all(user_uid: str) -> int:
|
|
"""Soft-delete every access token for *user_uid*. Returns the count revoked."""
|
|
tokens = get_table("access_tokens")
|
|
stamp = datetime.now(timezone.utc).isoformat()
|
|
count = 0
|
|
for row in list(tokens.find(user_uid=user_uid, deleted_at=None)):
|
|
tokens.update({"id": row["id"], "deleted_at": stamp, "deleted_by": "manual"}, ["id"])
|
|
count += 1
|
|
if count:
|
|
from devplacepy.utils.authcache import clear_user_cache
|
|
|
|
clear_user_cache(user_uid)
|
|
return count
|
|
|
|
|
|
def prune_expired() -> int:
|
|
"""Soft-delete all expired access tokens. Returns the count pruned."""
|
|
tokens = get_table("access_tokens")
|
|
now = datetime.now(timezone.utc)
|
|
stamp = now.isoformat()
|
|
count = 0
|
|
for row in list(tokens.find(deleted_at=None)):
|
|
expires_at = row.get("expires_at", "")
|
|
if not expires_at:
|
|
continue
|
|
try:
|
|
expires = datetime.fromisoformat(expires_at)
|
|
if expires.tzinfo is None:
|
|
expires = expires.replace(tzinfo=timezone.utc)
|
|
except (ValueError, TypeError):
|
|
continue
|
|
if expires < now:
|
|
tokens.update({"id": row["id"], "deleted_at": stamp, "deleted_by": "prune"}, ["id"])
|
|
count += 1
|
|
return count
|