diff --git a/scopes/api/auth/user.py b/scopes/api/auth/user.py index 9122f3e..c57d0d4 100644 --- a/scopes/api/auth/user.py +++ b/scopes/api/auth/user.py @@ -1,5 +1,6 @@ # scopes.api.auth user +from datetime import datetime from fastapi import APIRouter, Body import jwt from pwdlib import PasswordHash @@ -13,6 +14,7 @@ from scopes.storage.common import Storage class User(BaseModel): uid: str | None = None + timeStamp: datetime | None = None name: str login: str display_name: str @@ -20,6 +22,9 @@ class User(BaseModel): pw_hash: str | None = Field(exclude=True, default=None) groups: List[str] = Field(default_factory=list) +class UserIn(User): + password: str | None = None + # password handling, authentication pw_hash = PasswordHash.recommended() @@ -52,7 +57,7 @@ async def get_users(storage: deps.Storage): return result @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) tr = st_user.User.fromObject(user) users.save(tr) diff --git a/scopes/shape.py b/scopes/shape.py index 6edc0e8..884d930 100644 --- a/scopes/shape.py +++ b/scopes/shape.py @@ -55,9 +55,14 @@ class Record: def toObject(self, cls): return cls(**self.asDict(flatten=True)) + @staticmethod + def prepareData(data): + return data + @classmethod def fromObject(cls, obj): inp = obj.model_dump() headValues = [inp.pop(hf) for hf in cls.headFields] - return cls(*headValues, data=inp) + data = cls.prepareData(inp) + return cls(*headValues, data=data) diff --git a/scopes/storage/tracking.py b/scopes/storage/tracking.py index 981cf20..86df089 100644 --- a/scopes/storage/tracking.py +++ b/scopes/storage/tracking.py @@ -33,11 +33,11 @@ class Record(shape.Record): return base | self.head | self.data return base | super().asDict() - @classmethod - def fromObject(cls, obj): - tr = super().fromObject(obj) - tr.data.pop('uid', None) - return tr + @staticmethod + def prepareData(data): + data.pop('uid', None) + data.pop('timeStamp', None) + return data @property def uid(self): diff --git a/scopes/storage/user.py b/scopes/storage/user.py index 6191cbe..9e22d90 100644 --- a/scopes/storage/user.py +++ b/scopes/storage/user.py @@ -8,6 +8,15 @@ class User(concept.Concept): headFields = ('name', 'login') 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 class Users(concept.Concepts): diff --git a/tests/test_api.py b/tests/test_api.py index 69eefcf..fc8c354 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -73,8 +73,10 @@ def test_user(storage, caplog): u01i = users[0] assert u01i.get(pw_hash) is None 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 u02 = resp.json() assert u02['uid'] == 'user-2' + assert u02.get('timeStamp') is not None