#!/usr/bin/env python3.14# -*- coding: utf-8 -*-"""Persistent "remember me" tokens at `<state>/remember.json`.A remember-me cookie is `<selector>:<validator>`. Per selector the storekeeps the user's email, an HMAC of the validator (keyed by the persistent32-byte `<state>/session-secret`, created on first start), and an expiry —so logins survive a service restart even though sessions are in-memory.On each use the validator **rotates** (one-time-use): a fresh cookie replacesthe old one. A selector presented with a wrong validator is treated as theft orforgery and deleted — failing secure to a re-login."""from__future__importannotations__all__=['RememberStore','load_or_create_secret']importhmacimportosimportsecretsimporttimefromhashlibimportsha256frompathlibimportPathfromtypingimportAnyfromeuler_site.authimportpolicyfromeuler_site.auth.storageimportload_json,save_jsonfromeuler_site.auth.usersimportnormalize_emaildefload_or_create_secret(path:Path)->bytes:"""Return the 32-byte server secret at *path*, creating it (mode 0600) if absent."""ifpath.is_file():returnpath.read_bytes()path.parent.mkdir(parents=True,exist_ok=True)secret=secrets.token_bytes(32)fd=os.open(path,os.O_WRONLY|os.O_CREAT|os.O_TRUNC,0o600)withos.fdopen(fd,'wb')ashandle:handle.write(secret)returnsecretclassRememberStore:"""Selector → (email, HMAC(validator), expiry), rotated on every redemption."""def__init__(self,path:Path,secret:bytes)->None:self._path=pathself._secret=secretdef_mac(self,validator:str)->str:returnhmac.new(self._secret,validator.encode(),sha256).hexdigest()def_load(self)->dict[str,dict[str,Any]]:now=time.time()records=load_json(self._path)return{key:recforkey,recinrecords.items()ifisinstance(rec,dict)andfloat(rec.get('expiry',0))>now}defissue(self,email:str)->str:"""Mint a fresh token for *email*; return the cookie value `selector:validator`."""records=self._load()selector=secrets.token_urlsafe(9)validator=secrets.token_urlsafe(32)records[selector]={'email':normalize_email(email),'validator':self._mac(validator),'expiry':time.time()+policy.REMEMBER_TTL_SECONDS}save_json(self._path,records)returnf'{selector}:{validator}'defredeem(self,cookie:str)->tuple[str,str]|None:"""Validate and rotate; return `(email, new_cookie)` or None. A known selector with a wrong validator is deleted (stolen/forged token); the legitimate holder simply logs in again. """selector,_,validator=cookie.partition(':')ifnotselectorornotvalidator:returnNonerecords=self._load()record=records.get(selector)ifrecordisNone:returnNonedelrecords[selector]# one-time-use: old token dies either wayifnothmac.compare_digest(str(record.get('validator','')),self._mac(validator)):save_json(self._path,records)# theft signal — drop it, fail securereturnNoneemail=str(record.get('email',''))new_selector=secrets.token_urlsafe(9)new_validator=secrets.token_urlsafe(32)records[new_selector]={'email':email,'validator':self._mac(new_validator),'expiry':time.time()+policy.REMEMBER_TTL_SECONDS}save_json(self._path,records)returnemail,f'{new_selector}:{new_validator}'defrevoke(self,cookie:str)->None:"""Drop the token in *cookie* (logout)."""selector=cookie.partition(':')[0]records=self._load()ifrecords.pop(selector,None)isnotNone:save_json(self._path,records)defrevoke_all(self)->int:"""Drop every token (`logout-all`); return how many."""records=self._load()ifrecords:save_json(self._path,{})returnlen(records)defrevoke_email(self,email:str)->int:"""Drop every token for *email* (disable/remove); return how many."""records=self._load()key_email=normalize_email(email)keep={key:recforkey,recinrecords.items()ifrec.get('email')!=key_email}dropped=len(records)-len(keep)ifdropped:save_json(self._path,keep)returndropped