Skip to content

Commit

Permalink
Refactor crud and rest apis
Browse files Browse the repository at this point in the history
  • Loading branch information
luozhouyang committed Apr 1, 2024
1 parent 95ddf74 commit 971e6a7
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 121 deletions.
35 changes: 0 additions & 35 deletions melody/identity/api.py

This file was deleted.

142 changes: 57 additions & 85 deletions melody/identity/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,109 +4,86 @@

import bcrypt
from sqlalchemy import delete, insert, select, update
from sqlmodel.ext.asyncio.session import AsyncSession

from melody import utils
from melody.deps import database

from .models import (
EmailIdentityCreateRequest,
EmailIdentityPatchRequest,
EmailIdentityResetPasswordRequest,
EmailIdentityUpdateRequest,
OAuth2IdentityCreateRequest,
OAuth2IdentityPatchRequest,
OAuth2IdentityUpdateRequest,
)
from .tables import Identity

logger = logging.getLogger("melody.identity")


async def retrieve_identity(id: uuid.UUID) -> Identity | None:
async def retrieve_identity(session: AsyncSession, *, id: uuid.UUID) -> Identity | None:
sql = select(Identity).where(Identity.id == id)
logger.debug(f"retrieving identity sql: {sql}")
identity = await database.execute(sql)
identity = await session.exec(sql).first()
logger.debug(f"retrieved identity: {identity}")
return identity


async def retrieve_identities_by_user_id(user_id: uuid.UUID) -> List[Identity] | None:
async def retrieve_identities_by_user_id(session: AsyncSession, *, user_id: uuid.UUID) -> List[Identity] | None:
sql = select(Identity).where(Identity.user_id == user_id)
logger.debug(f"retrieving identity sql: {sql}")
identity = await database.fetch_all(sql)
logger.debug(f"retrieved identity: {identity}")
return identity
identities = await session.exec(sql).all()
logger.debug(f"retrieved identity: {identities}")
return identities


@database.transaction()
async def create_oauth2_identity(
user_id: uuid.UUID, provider_id: str, provider_uid: str, props: dict | None = None
) -> Identity:
async def create_oauth2_identity(session: AsyncSession, *, request: OAuth2IdentityCreateRequest) -> Identity:
sql = (
insert(Identity)
.values(
user_id=user_id,
iden_type="OAUTH_" + provider_id.upper(),
iden_value=provider_uid,
user_id=request,
iden_type="OAUTH_" + request.provider_id.upper(),
iden_value=request.provider_uid,
credential=None,
status="ACTIVE",
last_signin_at=None,
props=props or {},
props=request.props or {},
)
.returning(Identity)
)
logger.debug(f"created oauth identity sql: {sql}")
identity = await database.execute(sql)
identity = await session.exec(sql).one()
logger.debug(f"created oauth identity: {identity}")
return identity


@database.transaction()
async def update_oauth2_identity(
id: uuid.UUID, user_id: uuid.UUID, provider_id: str, provider_uid: str, status: str, props: dict
session: AsyncSession, *, id: uuid.UUID, request: OAuth2IdentityUpdateRequest
) -> Identity | None:
sql = (
update(Identity)
.where(Identity.id == id)
.values(
user_id=user_id,
iden_type="OAUTH_" + provider_id.upper(),
iden_value=provider_uid,
status=status,
props=props,
updated_at=utils.utc_now(),
)
.returning(Identity)
)
values = request.model_dump()
sql = update(Identity).where(Identity.id == id).values(**values, updated_at=utils.utc_now()).returning(Identity)
logger.debug(f"updated oauth identity sql: {sql}")
identity = database.execute(sql)
identity = await session.exec(sql).one()
logger.debug(f"updated oauth identity: {identity}")
return identity


@database.transaction()
async def patch_oauth2_identity(
id: uuid.UUID,
user_id: uuid.UUID | None = None,
provider_id: str | None = None,
provider_uid: str | None = None,
status: str | None = None,
props: dict | None = None,
session: AsyncSession, *, id: uuid.UUID, request: OAuth2IdentityPatchRequest
) -> Identity | None:
data = {}
if user_id:
data["user_id"] = user_id
if provider_id:
data["iden_type"] = "OAUTH_" + provider_id.upper()
if provider_uid:
data["iden_value"] = provider_uid
if status:
data["status"] = status
if props:
data["props"] = props
if not data:
values = request.model_dump(exclude_unset=True)
if not values:
logger.info(f"no data to patch, skipped.")
return None
sql = update(Identity).where(Identity.id == id).values(**data, updated_at=utils.utc_now()).returning(Identity)
sql = update(Identity).where(Identity.id == id).values(**values, updated_at=utils.utc_now()).returning(Identity)
logger.debug(f"patch oauth identity sql: {sql}")
identity = database.execute(sql)
identity = await session.exec(sql).one()
logger.debug(f"patch oauth identity: {identity}")
return identity


@database.transaction()
async def delete_identity(id: uuid.UUID, soft_delete: bool = False) -> Identity | None:
async def delete_identity(session: AsyncSession, *, id: uuid.UUID, soft_delete: bool = False) -> Identity | None:
if soft_delete:
sql = (
update(Identity).where(Identity.id == id).values(status="DELETED", deleted_at=utils.utc_now()).returning(Identity)
Expand All @@ -115,69 +92,64 @@ async def delete_identity(id: uuid.UUID, soft_delete: bool = False) -> Identity
sql = delete(Identity).where(Identity.id == id).returning(Identity)
logger.debug(f"delete identity sql: {sql}")

identity: Identity = database.execute(delete(Identity).where(Identity.id == id).returning(Identity))
if identity:
identity.credential = None
identity: Identity = await session.exec(sql).first()
logger.debug(f"deleted identity (soft={soft_delete}): {identity}")
return identity


@database.transaction()
async def create_email_identity(user_id: uuid.UUID, email: str, password: str, props: dict | None = None) -> Identity:
credential = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
async def create_email_identity(session: AsyncSession, *, request: EmailIdentityCreateRequest) -> Identity:
credential = bcrypt.hashpw(request.password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
values = request.model_dump(exclude_none=True)
sql = (
insert(Identity)
.values(
user_id=user_id,
iden_type="EMAIL",
iden_value=email,
**values,
credential=credential,
status="ACTIVE",
last_signin_at=None,
props=props or {},
)
.returning(Identity)
)
logger.debug(f"created email identity sql: {sql}")
identity: Identity = await database.execute(sql)
identity: Identity = await session.exec(sql).one()
logger.debug(f"created email identity: {identity}")
return identity


@database.transaction()
async def update_email_identity(
id: uuid.UUID, user_id: uuid.UUID, email: str, status: str, props: dict | None = None
session: AsyncSession, *, id: uuid.UUID, request: EmailIdentityUpdateRequest
) -> Identity | None:
"""Update email identity. All fields to update are required, except props."""
data = {
"user_id": user_id,
"iden_type": "EMAIL",
"iden_value": email,
"status": status,
"updated_at": utils.utc_now(),
}
if props:
data["props"] = props
if not data:
values = request.model_dump()
if not values:
logger.info(f"email identity {id} not changed, skipped to update.")
return None
sql = update(Identity).where(Identity.id == id).values(**data).returning(Identity)
sql = update(Identity).where(Identity.id == id).values(**values, updated_at=utils.utc_now()).returning(Identity)
logger.debug(f"update email identity sql: {sql}")
identity: Identity = await database.execute(sql)
identity: Identity = await session.exec(sql).first()
logger.debug(f"updated email identity: {identity}")
return identity


@database.transaction()
async def reset_email_password(email: str, password: str) -> Identity | None:
credential = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
async def patch_email_identity(session: AsyncSession, *, id: uuid.UUID, request: EmailIdentityPatchRequest) -> Identity | None:
"""Patch email identity. All fields to patch are optional."""
values = request.model_dump(exclude_unset=True)
sql = update(Identity).where(Identity.id == id).values(**values, updated_at=utils.utc_now()).returning(Identity)
logger.debug(f"patch email identity sql: {sql}")
identity: Identity = await session.exec(sql).first()
logger.debug(f"patched email identity: {identity}")
return identity


async def reset_email_password(session: AsyncSession, *, request: EmailIdentityResetPasswordRequest) -> Identity | None:
credential = bcrypt.hashpw(request.password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
sql = (
update(Identity)
.where(Identity.iden_type == "EMAIL", Identity.iden_value == email)
.where(Identity.iden_type == "EMAIL", Identity.iden_value == request.email)
.values(credential=credential, updated_at=utils.utc_now())
.returning(Identity)
)
logger.debug(f"reset email password sql: {sql}")
identity: Identity = database.execute(sql)
identity: Identity = await session.exec(sql).first()
logger.debug(f"upated email identity: {identity}")
return identity
56 changes: 56 additions & 0 deletions melody/identity/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import uuid

from pydantic import BaseModel
from sqlmodel import Field, SQLModel


class OAuth2IdentityCreateRequest(SQLModel):
user_id: uuid.UUID = Field(nullable=False, description="The user id of this identity")
provider_id: str = Field(nullable=False, description="Identity type, such as EMAIL, PHONE, OAUTH_GITHUB, OAUTH_GOOGLE")
provider_uid: str = Field(nullable=False, description="Identity value, such as email address, phone number, or oauth uid.")
props: dict | None = Field(nullable=True, description="Additional properties of the record")


class OAuth2IdentityUpdateRequest(BaseModel):
user_id: str = Field(nullable=False, description="The user id of this identity")
provider_id: str = Field(nullable=False, description="Identity type, such as EMAIL, PHONE, OAUTH_GITHUB, OAUTH_GOOGLE")
provider_uid: str = Field(nullable=False, description="Identity value, such as email address, phone number, or oauth uid.")
status: str = Field(nullable=False, description="Identity status, such as ACTIVE, INACTIVE, DELETED")
props: dict = Field(nullable=False, description="Additional properties of the record")


class OAuth2IdentityPatchRequest(BaseModel):
user_id: str | None = Field(nullable=True, description="The user id of this identity")
provider_id: str | None = Field(
nullable=True, description="Identity type, such as EMAIL, PHONE, OAUTH_GITHUB, OAUTH_GOOGLE"
)
provider_uid: str | None = Field(
nullable=True, description="Identity value, such as email address, phone number, or oauth uid."
)
status: str | None = Field(nullable=True, description="Identity status, such as ACTIVE, INACTIVE, DELETED")
props: dict | None = Field(nullable=True, description="Additional properties of the record")


class EmailIdentityCreateRequest(SQLModel):
user_id: uuid.UUID = Field(nullable=False, description="The user id of this identity")
email: str = Field(nullable=False, description="Identity value, such as email address, phone number, or oauth uid.")
password: str = Field(nullable=False, description="Identity value, such as email address, phone number, or oauth uid.")
props: dict | None = Field(nullable=True, description="Additional properties of the record")


class EmailIdentityUpdateRequest(SQLModel):
user_id: uuid.UUID = Field(nullable=False, description="The user id of this identity")
email: str = Field(nullable=False, description="Identity value, such as email address, phone number, or oauth uid.")
status: str = Field(nullable=False, description="Identity status, such as ACTIVE, INACTIVE, DELETED")
props: dict = Field(nullable=False, description="Additional properties of the record")


class EmailIdentityPatchRequest(SQLModel):
user_id: uuid.UUID | None = Field(nullable=True, description="The user id of this identity")
email: str | None = Field(nullable=True, description="Identity value, such as email address, phone number, or oauth uid.")
status: str | None = Field(nullable=True, description="Identity status, such as ACTIVE, INACTIVE, DELETED")


class EmailIdentityResetPasswordRequest(SQLModel):
email: str = Field(nullable=False, description="Identity value, such as email address, phone number, or oauth uid.")
password: str = Field(nullable=False, description="Identity value, such as email address, phone number, or oauth uid.")
66 changes: 66 additions & 0 deletions melody/identity/rest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import uuid

from fastapi import APIRouter

from melody import deps

from . import crud, models, tables

router = APIRouter()


@router.post("/identities/oauth2")
async def create_oauth2_identity(
session: deps.DatabaseSession, request: models.OAuth2IdentityCreateRequest
) -> tables.Identity:
return await crud.create_oauth2_identity(session, request=request)


@router.post("/identities/oauth2/{id}")
async def update_oauth2_identity(
session: deps.DatabaseSession, id: uuid.UUID, request: models.OAuth2IdentityUpdateRequest
) -> tables.Identity | None:
return await crud.update_oauth2_identity(session, id=id, request=request)


@router.patch("/identities/oauth2/{id}")
async def patch_oauth2_identity(
session: deps.DatabaseSession, id: uuid.UUID, request: models.OAuth2IdentityPatchRequest
) -> tables.Identity | None:
return await crud.patch_oauth2_identity(session, id=id, request=request)


@router.delete("/identities/oauth2/{id}")
async def delete_oauth2_identity(session: deps.DatabaseSession, id: uuid.UUID) -> tables.Identity | None:
return await crud.delete_oauth2_identity(session, id=id)


@router.post("/identities/email")
async def create_email_identity(session: deps.DatabaseSession, request: models.EmailIdentityCreateRequest) -> tables.Identity:
return await crud.create_email_identity(session, request=request)


@router.post("/identities/email/{id}")
async def update_email_identity(
session: deps.DatabaseSession, id: uuid.UUID, request: models.EmailIdentityUpdateRequest
) -> tables.Identity | None:
return await crud.update_email_identity(session, id=id, request=request)


@router.patch("/identities/email/{id}")
async def patch_email_identity(
session: deps.DatabaseSession, id: uuid.UUID, request: models.EmailIdentityPatchRequest
) -> tables.Identity | None:
return await crud.patch_email_identity(session, id=id, request=request)


@router.delete("/identities/email/{id}")
async def delete_email_identity(session: deps.DatabaseSession, id: uuid.UUID) -> tables.Identity | None:
return await crud.delete_identity(session, id=id)


@router.post("/identities/email/resetpw")
async def reset_email_password(
session: deps.DatabaseSession, request: models.EmailIdentityResetPasswordRequest
) -> tables.Identity | None:
return await crud.reset_email_password(session, request=request)
Loading

0 comments on commit 971e6a7

Please sign in to comment.