from dataclasses import dataclass from datetime import datetime from hashlib import shake_256 import uuid def phash(pw,salt=None): if salt is None: salt=uuid.uuid64() return shake_256(f'{pw}{salt}'.encode('utf-8')).hexdigest(),salt class AbstractUser(): pass # It fixes circular dep on user.invited_by @dataclass class User(AbstractUser): username: str password_hash: str salt: str invited_by: AbstractUser # Root node will just reference itself email: str='' @property def csv(self): return ','.join([self.username,self.password_hash,self.salt,self.invited_by.username,self.email]) def create_inv_token(self,*args,**kwargs): return InviteToken(*args,**kwargs) def change_password(self,old_pw:str|None,new_pw:str): pass def save(self): backend.save_user(self) @classmethod def login(cls,username:str,password:str): u=lookup_user(username) if u is None: raise Exception("User doesn't exist") if phash(password,u.salt)[0]==u.password_hash: return AccessToken(u) raise Exception("Incorrect password") @classmethod def register(cls,username:str,password:str,email:str|None): if set([chr(n) for n in range(32)]+[','])&set(username): raise Exception('Invalid username') u=backend.load_user(username) if u is not None: raise Exception("User already exists") u=User(username,*phash(password),email) u.save() return u @dataclass class Token(): value: str owner: User def __init__(self,value,owner=None): if owner is None: owner=value self.value=uuid.uuid4() else: self.value=uuid.UUID(value) self.owner=owner def revoke(self): backend.delete_token(self) @dataclass class InviteToken(Token): uses: int _max_uses: int=-1 _expires: datetime=None @property def max_uses(self): return self._max_uses @max_uses.setter def max_uses(self,val): if val==0: self.revoke() self._max_uses=val @dataclass class AccessToken(Token): pass