From d6971d0808e788d0ef837dfb76432a203e1bb0ee Mon Sep 17 00:00:00 2001 From: Winston Hsiao Date: Sat, 3 Aug 2024 10:12:29 -0400 Subject: [PATCH] user and oauth normalized + required crud --- store/app/crud/users.py | 19 +++++++++++------ store/app/model.py | 47 +++++++++++++++++++++++++++-------------- 2 files changed, 43 insertions(+), 23 deletions(-) diff --git a/store/app/crud/users.py b/store/app/crud/users.py index 125f7cf3..e4a293db 100644 --- a/store/app/crud/users.py +++ b/store/app/crud/users.py @@ -42,10 +42,15 @@ async def _create_user_from_email(self, email: str, password: str) -> User: await self._add_item(user, unique_fields=["email"]) return user - async def _create_user_from_auth_key(self, auth_key: str, email: str) -> User: - user = await self._create_user_from_email(email) - key = OAuthKey.create(auth_key, user.id) - await self._add_item(key, unique_fields=["user_token"]) + async def _create_user_from_oauth(self, email: str, provider: str, token: str) -> User: + user = User.create(email=email, password=None) + if provider == "github": + user.github_id = token + elif provider == "google": + user.google_id = token + await self._add_item(user, unique_fields=["email"]) + oauth_key = OAuthKey.create(user_id=user.id, provider=provider, token=token) + await self._add_item(oauth_key, unique_fields=["user_token"]) return user @overload @@ -71,7 +76,7 @@ async def get_user_from_github_token(self, token: str, email: str) -> User: user = await self._get_user_from_auth_key(auth_key) if user is not None: return user - return await self._create_user_from_auth_key(auth_key, email) + return await self._create_user_from_oauth(email, "github", auth_key) async def delete_github_token(self, github_id: str) -> None: await self._delete_item(await self._get_oauth_key(github_auth_key(github_id), throw_if_missing=True)) @@ -81,7 +86,7 @@ async def get_user_from_google_token(self, token: str, email: str) -> User | Non user = await self._get_user_from_auth_key(auth_key) if user is not None: return user - return await self._create_user_from_auth_key(auth_key, email) + return await self._create_user_from_oauth(email, "google", auth_key) async def delete_google_token(self, google_id: str) -> None: await self._delete_item(await self._get_oauth_key(google_auth_key(google_id), throw_if_missing=True)) @@ -126,7 +131,7 @@ async def delete_api_key(self, token: APIKey | str) -> None: async def test_adhoc() -> None: async with UserCrud() as crud: - await crud._create_user_from_email(email="ben@kscale.dev") + await crud._create_user_from_email(email="ben@kscale.dev", password="examplepas$w0rd") if __name__ == "__main__": diff --git a/store/app/model.py b/store/app/model.py index 6ea1bc61..ca8cc21d 100644 --- a/store/app/model.py +++ b/store/app/model.py @@ -7,9 +7,9 @@ import time from datetime import datetime, timedelta -from typing import Literal, Self +from typing import Optional, Self, Set, Literal -from pydantic import BaseModel +from pydantic import BaseModel, EmailStr from store.settings import settings from store.store.app.utils.password import hash_password @@ -33,28 +33,38 @@ class RobolistBaseModel(BaseModel): class User(RobolistBaseModel): """Defines the user model for the API. - Users are defined by their email and hashed_password. This is the - simplest form of authentication, and is used for users who sign up with - their email and password. + Users are defined by their id and email (both unique). + Hashed password is set if user signs up with email and password, and is + left empty if the user signed up with Google or Github OAuth. """ - email: str - hashed_password: str - permissions: set[UserPermission] | None = None + email: EmailStr + hashed_password: Optional[str] = None + permissions: Optional[Set[UserPermission]] = None created_at: int updated_at: int - email_verified_at: int | None = None + email_verified_at: Optional[int] = None + github_id: Optional[str] = None + google_id: Optional[str] = None @classmethod - def create(cls, email: str, password: str) -> Self: + def create( + cls, + email: str, + password: Optional[str] = None, + github_id: Optional[str] = None, + google_id: Optional[str] = None, + ) -> Self: now = int(time.time()) + hashed_pw = hash_password(password) if password else None return cls( id=new_uuid(), email=email, - hashed_password=hash_password(password), - permissions=None, + hashed_password=hashed_pw, created_at=now, updated_at=now, + github_id=github_id, + google_id=google_id, ) def update_timestamp(self) -> None: @@ -66,13 +76,18 @@ def verify_email(self) -> None: class OAuthKey(RobolistBaseModel): """Keys for OAuth providers which identify users.""" - user_id: str - user_token: str + provider: str + token: str @classmethod - def create(cls, user_token: str, user_id: str) -> Self: - return cls(id=new_uuid(), user_id=user_id, user_token=user_token) + def create(cls, user_id: str, provider: str, token: str) -> Self: + return cls( + id=new_uuid(), + user_id=user_id, + provider=provider, + token=token + ) APIKeySource = Literal["user", "oauth"]