api.auth.user: POST with hashing of input password

This commit is contained in:
Helmut Merz 2026-11-06 15:35:11 +01:00
parent 8494f3737d
commit b6e6f1c833
5 changed files with 29 additions and 8 deletions

View file

@ -1,5 +1,6 @@
# scopes.api.auth user # scopes.api.auth user
from datetime import datetime
from fastapi import APIRouter, Body from fastapi import APIRouter, Body
import jwt import jwt
from pwdlib import PasswordHash from pwdlib import PasswordHash
@ -13,6 +14,7 @@ from scopes.storage.common import Storage
class User(BaseModel): class User(BaseModel):
uid: str | None = None uid: str | None = None
timeStamp: datetime | None = None
name: str name: str
login: str login: str
display_name: str display_name: str
@ -20,6 +22,9 @@ class User(BaseModel):
pw_hash: str | None = Field(exclude=True, default=None) pw_hash: str | None = Field(exclude=True, default=None)
groups: List[str] = Field(default_factory=list) groups: List[str] = Field(default_factory=list)
class UserIn(User):
password: str | None = None
# password handling, authentication # password handling, authentication
pw_hash = PasswordHash.recommended() pw_hash = PasswordHash.recommended()
@ -52,7 +57,7 @@ async def get_users(storage: deps.Storage):
return result return result
@router.post('/users', response_model=User) @router.post('/users', response_model=User)
async def post_user(user: Annotated[User, Body()], storage: deps.Storage): async def post_user(user: Annotated[UserIn, Body()], storage: deps.Storage):
users = storage.create(st_user.Users) users = storage.create(st_user.Users)
tr = st_user.User.fromObject(user) tr = st_user.User.fromObject(user)
users.save(tr) users.save(tr)

View file

@ -55,9 +55,14 @@ class Record:
def toObject(self, cls): def toObject(self, cls):
return cls(**self.asDict(flatten=True)) return cls(**self.asDict(flatten=True))
@staticmethod
def prepareData(data):
return data
@classmethod @classmethod
def fromObject(cls, obj): def fromObject(cls, obj):
inp = obj.model_dump() inp = obj.model_dump()
headValues = [inp.pop(hf) for hf in cls.headFields] headValues = [inp.pop(hf) for hf in cls.headFields]
return cls(*headValues, data=inp) data = cls.prepareData(inp)
return cls(*headValues, data=data)

View file

@ -33,11 +33,11 @@ class Record(shape.Record):
return base | self.head | self.data return base | self.head | self.data
return base | super().asDict() return base | super().asDict()
@classmethod @staticmethod
def fromObject(cls, obj): def prepareData(data):
tr = super().fromObject(obj) data.pop('uid', None)
tr.data.pop('uid', None) data.pop('timeStamp', None)
return tr return data
@property @property
def uid(self): def uid(self):

View file

@ -8,6 +8,15 @@ class User(concept.Concept):
headFields = ('name', 'login') headFields = ('name', 'login')
prefix = 'user' prefix = 'user'
@staticmethod
def prepareData(data):
from scopes.api.auth.user import hashPassword
data = concept.Concept.prepareData(data)
pw = data.pop('password', None)
if pw:
data['pw_hash'] = hashPassword(pw)
return data
@registerContainerClass @registerContainerClass
class Users(concept.Concepts): class Users(concept.Concepts):

View file

@ -73,8 +73,10 @@ def test_user(storage, caplog):
u01i = users[0] u01i = users[0]
assert u01i.get(pw_hash) is None assert u01i.get(pw_hash) is None
resp = clt.post('/users', json={'name': 'jane', 'login': 'jane', resp = clt.post('/users', json={'name': 'jane', 'login': 'jane',
'display_name': 'Jane', 'email': 'jane@xmpl.org'}) 'display_name': 'Jane', 'email': 'jane@xmpl.org',
'password': 'secret'})
assert resp.status_code == 200 assert resp.status_code == 200
u02 = resp.json() u02 = resp.json()
assert u02['uid'] == 'user-2' assert u02['uid'] == 'user-2'
assert u02.get('timeStamp') is not None