Users can be saved and loaded

This commit is contained in:
Zergling_man 2023-12-28 23:51:38 +11:00
parent 7a975a5782
commit d097acaecf
4 changed files with 78 additions and 45 deletions

View File

@ -3,7 +3,7 @@ Here are the headers that every .py in this folder should implement.
Each file is free to define its own set of config options that will be passed in AFTER the file is imported, under the name "config". It is generally assumed that this will be a dict, but there is no strict requirement, other than "it must be a valid json value". Each file is free to define its own set of config options that will be passed in AFTER the file is imported, under the name "config". It is generally assumed that this will be a dict, but there is no strict requirement, other than "it must be a valid json value".
The init() function will be called with no arguments after config is passed in. The init() function will be called with no arguments after config is passed in.
the *token functions are about access tokens, as distinct from invite tokens. the *token functions are about access tokens, as distinct from invite tokens.
The backend will also have access to utils.py, under the name utils. The backend will also have access to utils.py, under the name utils and models.py under the name models.
""" """
def init(): pass def init(): pass

View File

@ -1,4 +1,4 @@
from os import path from os import path,rename
def init(): def init():
if not path.exists(config['path']): raise Exception(f"Backend directory doesn't exist: {config['path']}") if not path.exists(config['path']): raise Exception(f"Backend directory doesn't exist: {config['path']}")
for n in ['users','invite_tokens','access_tokens']: for n in ['users','invite_tokens','access_tokens']:
@ -8,7 +8,7 @@ def init():
if path.isdir(p): raise Exception(f"Backend file is a directory: {p}") if path.isdir(p): raise Exception(f"Backend file is a directory: {p}")
try: open(p).close() try: open(p).close()
except PermissionError: raise Exception(f"Backend file can't be read: {p}") except PermissionError: raise Exception(f"Backend file can't be read: {p}")
try: open(p,'w').close() try: open(p,'a').close()
except PermissionError: raise Exception(f"Backend file can't be written: {p}") except PermissionError: raise Exception(f"Backend file can't be written: {p}")
def load_user(username): def load_user(username):
@ -21,7 +21,11 @@ def save_user(user):
split=line.split(',') split=line.split(',')
if split[0]==user.username: update(users,{gen.send(True):user.csv}); break if split[0]==user.username: update(users,{gen.send(True):user.csv}); break
else: update(users,{-1:user.csv}) else: update(users,{-1:user.csv})
def delete_user(user): pass def delete_user(user):
gen=linegen(users)
for line in gen:
split=line.split(',')
if split[0]==user.username: remove(users,[gen.send(True)]); break
def load_tokens(user): pass def load_tokens(user): pass
def save_token(token): pass def save_token(token): pass
@ -43,15 +47,28 @@ def linegen(path):
file.close() file.close()
def update(path,lines): def update(path,lines):
def _action(file,i,line):
if i in lines: file.write(lines[i]+'\n')
else: file.write(line)
def _post(file):
if -1 in lines: file.write(lines[-1]+'\n')
writefile(path,_action,_post)
def remove(path,lines):
def _action(file,i,line):
if i not in lines: file.write(line)
writefile(path,_action)
def writefile(path,action,post=None):
i=0 i=0
file=open(path) file=open(path)
write=open(f'{path}.tmp','w') write=open(f'{path}.tmp','w')
line=file.readline() line=file.readline()
while line: while line:
i+=1 i+=1
if i in lines: write.write(lines[i]+'\n') action(write,i,line)
else: write.write(line)
line=file.readline() line=file.readline()
if -1 in lines: write.write(lines[-1]) if post is not None: post(write)
file.close() file.close()
write.close() write.close()
rename(f'{path}.tmp',path)

View File

@ -2,7 +2,9 @@ from importlib import import_module
from . import utils, models from . import utils, models
from .config import config from .config import config
globals()['backend']=import_module('backends.'+config['backend']['type']) globals()['backend']=import_module('.backends.'+config['backend']['type'],'auth')
backend.config=config['backend']['options'] backend.config=config['backend']['options']
backend.utils=utils backend.utils=utils
backend.models=models
models.backend=backend models.backend=backend
backend.init()

View File

@ -9,42 +9,12 @@ def phash(pw,salt=None):
class AbstractUser(): class AbstractUser():
pass # It fixes circular dep on user.invited_by pass # It fixes circular dep on user.invited_by
# And on User.register (InviteToken)
@dataclass
class User(AbstractUser):
username: str
password_hash: str
salt: str
invited_by: AbstractUser=None # 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,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()
return u
@dataclass @dataclass
class Token(): class Token():
value: str value: str
owner: User owner: AbstractUser
def __init__(self,value,owner=None): def __init__(self,value,owner=None):
if owner is None: if owner is None:
owner=value owner=value
@ -57,15 +27,59 @@ class Token():
@dataclass @dataclass
class InviteToken(Token): class InviteToken(Token):
uses: int _uses: int=0
_max_uses: int=-1 _max_uses: int=-1
_expires: datetime=None _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 @property
def max_uses(self): return self._max_uses def max_uses(self): return self._max_uses
@max_uses.setter @max_uses.setter
def max_uses(self,val): def max_uses(self,val):
if val==0: self.revoke() if -1<val<=self.uses: self.revoke()
self._max_uses=val self._max_uses=val
@dataclass @dataclass
class AccessToken(Token): pass 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=None # Root node will just reference itself
email: str=''
@property
def csv(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_token(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: return AccessToken(u)
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