auth/models.py

98 lines
2.7 KiB
Python

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.uuid4()
return shake_256(f'{pw}{salt}'.encode('utf-8')).hexdigest(256),salt
class AbstractUser():
pass # It fixes circular dep on user.invited_by
# And on User.register (InviteToken)
@dataclass
class Token():
value: str
owner: AbstractUser
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)
@property
def serialise(self):
return ','.join([self.value,self.owner])
@dataclass
class InviteToken(Token):
_uses: int=0
_max_uses: int=-1
expires: datetime=None
def __init__(self,*args,**kwargs):
return super().__init__(*args,**kwargs)
@property
def uses(self): return self._uses
@uses.setter
def uses(self,val):
if -1<self.max_uses<=val: self.revoke()
self._uses=val
@property
def max_uses(self): return self._max_uses
@max_uses.setter
def max_uses(self,val):
if -1<val<=self.uses: self.revoke()
self._max_uses=val
@property
def serialise(self):
return ','.join([self.value,self.owner,self.uses,self.max_uses,self.expires])
@dataclass
class AccessToken(Token):
def __init__(self,*args,**kwargs):
return super().__init__(*args,**kwargs)
@dataclass
class User(AbstractUser):
username: str
password_hash: str
salt: str
_invited_by: AbstractUser # Root node will just reference itself
@property
def invited_by(self):
if isinstance(self._invited_by,str): self._invited_by=backend.load_user(self._invited_by)
return self._invited_by
email: str=''
@property
def serialise(self):
return ','.join([self.username,self.password_hash,str(self.salt),self.invited_by.username,self.email])
def create_inv_token(self,*args,**kwargs):
tok=InviteToken(self,*args,**kwargs)
backend.save_invite(tok)
return tok
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=backend.load_user(username)
if u is None: raise Exception("User doesn't exist")
if phash(password,u.salt)[0]==u.password_hash:
a=AccessToken(u)
backend.save_token(a)
return a
raise Exception("Incorrect password")
@classmethod
def register(cls,username:str,password:str,invite:InviteToken,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),invite.owner,email)
u.save()
invite.uses+=1
return u