Shop registration works now
This commit is contained in:
1 parent
a0d238c840
commit
d87c280109
15 files changed
+98
-727
No files matched your search
@@ -5,7 +5,7 @@ from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||
from jwt.exceptions import InvalidTokenError
|
||||
from pydantic import ValidationError
|
||||
from sqlmodel import Session
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app.core import security
|
||||
from app.core.config import settings
|
||||
@@ -19,7 +19,7 @@ reusable_oauth2 = OAuth2PasswordBearer(
|
||||
|
||||
SessionDep = Annotated[Session, Depends(get_session)]
|
||||
TokenDep = Annotated[str, Depends(reusable_oauth2)]
|
||||
LoginDep = Annotated[OAuth2PasswordRequestForm, Depends()]
|
||||
LoginDep = Annotated[OAuth2PasswordRequestForm, Depends(OAuth2PasswordRequestForm)]
|
||||
|
||||
|
||||
def get_current_user(session: SessionDep, token: TokenDep) -> User:
|
||||
@@ -33,11 +33,10 @@ def get_current_user(session: SessionDep, token: TokenDep) -> User:
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Could not validate credentials",
|
||||
)
|
||||
user = session.get(User, {"uuid": token_data.sub})
|
||||
stmt = select(User).where(User.uuid == token_data.sub)
|
||||
user = session.exec(stmt).one_or_none()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
if not user.is_active:
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
return user
|
||||
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ def login_access_token(
|
||||
"""
|
||||
user = None
|
||||
user = user_crud.authenticate(
|
||||
session=session, email=form_data.username, password=form_data.password, shop_id=None
|
||||
session=session, email=form_data.username, password=form_data.password, shop_uuid=None
|
||||
)
|
||||
if not user:
|
||||
raise HTTPException(status_code=400, detail="Incorrect email or password")
|
||||
@@ -39,6 +39,6 @@ def login_access_token(
|
||||
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
return Token(
|
||||
access_token=security.create_access_token(
|
||||
user.id, expires_delta=access_token_expires
|
||||
user.uuid, expires_delta=access_token_expires
|
||||
)
|
||||
)
|
||||
@@ -1,18 +1,18 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
|
||||
from app.api.dependencies import CurrentOwnerUser, SessionDep
|
||||
from app.schemas.shop_schemas import ShopCreate
|
||||
from app.schemas.user_schemas import Token
|
||||
from app.crud.shop_crud import create_shop
|
||||
|
||||
router = APIRouter(prefix="/shop", tags=["Shop"])
|
||||
router = APIRouter(prefix="/shop", tags=["Dashboard"])
|
||||
|
||||
|
||||
@router.post("")
|
||||
@router.post("", response_model=bool, status_code=201)
|
||||
def register_new_shop(
|
||||
session: SessionDep,
|
||||
current_user: CurrentOwnerUser,
|
||||
shop_data: ShopCreate
|
||||
) -> Token:
|
||||
"""
|
||||
OAuth2 compatible token login, get an access token for future requests
|
||||
"""
|
||||
create_shop(session, shop_data, current_user)
|
||||
return True
|
||||
@@ -1,38 +1,35 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
from fastapi import APIRouter, Body, HTTPException, status
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from app.api.dependencies import SessionDep
|
||||
from app.api.dependencies import CurrentOwnerUser, SessionDep
|
||||
from app.crud import user_crud
|
||||
from app.database.models.user_model import UserRole
|
||||
from app.schemas.user_schemas import UserRegister
|
||||
from app.schemas.user_schemas import UserRegister, UserUpdate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
INTERNAL_SERVER_ERROR = HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Internal server error")
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/user",
|
||||
tags=["User"]
|
||||
tags=["User", "Dashboard"]
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/delete", summary="Delete user")
|
||||
async def delete_user():
|
||||
raise NotImplementedError("delete_user() needs to be implemented.")
|
||||
@router.get("", summary="Get information about currently logged in user")
|
||||
async def get_user(session: SessionDep, current_user: CurrentOwnerUser):
|
||||
pass
|
||||
|
||||
|
||||
@router.delete("/logout", summary="User logout")
|
||||
async def logout():
|
||||
raise NotImplementedError("logout() needs to be implemented.")
|
||||
|
||||
|
||||
@router.post("/register", summary="Register new user")
|
||||
@router.post("", summary="Register new user", status_code=status.HTTP_201_CREATED, response_model=bool)
|
||||
async def register(session: SessionDep, user_data: UserRegister):
|
||||
try:
|
||||
user_crud.create_user(session, user_data, None, UserRole.OWNER)
|
||||
return {"detail": "Registered succeesfully"}
|
||||
return True
|
||||
except IntegrityError as e:
|
||||
field_mapping = {"uuid": "email"} # If a UUID is duplicate, it means email is in use
|
||||
|
||||
@@ -49,10 +46,17 @@ async def register(session: SessionDep, user_data: UserRegister):
|
||||
detail=detail
|
||||
)
|
||||
except Exception as e:
|
||||
if isinstance(e, HTTPException):
|
||||
raise
|
||||
logger.error(e)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to register")
|
||||
raise INTERNAL_SERVER_ERROR
|
||||
|
||||
|
||||
@router.put("/update", summary="Update user details")
|
||||
async def update_user(data: dict = Body(...)):
|
||||
raise NotImplementedError("update_user() needs to be implemented.")
|
||||
@router.delete("", summary="Delete user")
|
||||
async def delete_user():
|
||||
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="delete_user() not implemented")
|
||||
|
||||
|
||||
@router.put("", summary="Update user details")
|
||||
async def update_user(data: UserUpdate):
|
||||
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="update_user() not implemented")
|
||||
@@ -34,6 +34,14 @@ class ShopBusinessHours(BaseModel):
|
||||
hours: list[ShopBusinessHourEntry]
|
||||
|
||||
|
||||
class ShopAddress(SQLModel):
|
||||
street: str = Field(max_length=255, nullable=False)
|
||||
city: str = Field(max_length=100, nullable=False)
|
||||
state: str = Field(max_length=100, nullable=True)
|
||||
postal_code: str = Field(max_length=20, nullable=False)
|
||||
country: str = Field(max_length=100, nullable=False)
|
||||
|
||||
|
||||
class Shop(SQLModel, table=True):
|
||||
__tablename__ = 'shop'
|
||||
|
||||
@@ -48,16 +56,11 @@ class Shop(SQLModel, table=True):
|
||||
logo: Optional[str] = Field(max_length=100)
|
||||
contact_email: str = Field(max_length=128, nullable=False, unique=True)
|
||||
phone_number: str = Field(max_length=15, nullable=False)
|
||||
address: str = Field(nullable=True)
|
||||
address: ShopAddress = Field(sa_column=Column(JSONB, nullable=False, default=lambda: {}))
|
||||
business_hours: ShopBusinessHours = Field(sa_column=Column(JSONB, nullable=False, default=lambda: {}))
|
||||
links: ShopLinks = Field(sa_column=Column(JSONB, nullable=False, default=lambda: {}))
|
||||
currency: str = Field(max_length=3, nullable=False)
|
||||
|
||||
business_hours: ShopBusinessHours = Field(
|
||||
sa_column=Column(JSONB, nullable=False, default=lambda: {})
|
||||
)
|
||||
links: ShopLinks = Field(
|
||||
sa_column=Column(JSONB, nullable=False, default=lambda: {})
|
||||
)
|
||||
|
||||
owner: Optional["User"] = Relationship(
|
||||
back_populates='owned_shop',
|
||||
sa_relationship_kwargs={"foreign_keys": "[Shop.owner_id]"}
|
||||
@@ -68,10 +71,11 @@ class Shop(SQLModel, table=True):
|
||||
sa_relationship_kwargs={"foreign_keys": "[User.shop_id]"}
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
CheckConstraint("business_hours ? 'hours'", name="check_business_hours_keys"),
|
||||
CheckConstraint("links ? 'links'", name="check_links_keys"),
|
||||
)
|
||||
# __table_args__ = (
|
||||
# CheckConstraint("business_hours ? 'hours'", name="check_business_hours_keys"),
|
||||
# CheckConstraint("links ? 'links'", name="check_links_keys"),
|
||||
# CheckConstraint("address ? 'address'", name="check_address_keys")
|
||||
# )
|
||||
|
||||
|
||||
__all__ = ["Shop", "ShopBusinessHours", "ShopLinks", "ShopStatus"]
|
||||
@@ -1,8 +1,14 @@
|
||||
|
||||
from pydantic import EmailStr
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
from app.database.models.shop_model import ShopAddress
|
||||
|
||||
|
||||
class ShopCreate(SQLModel):
|
||||
name: str = Field(max_length=100, nullable=False, unique=True)
|
||||
description: str = Field(max_length=500, nullable=False)
|
||||
currency: str = Field(max_length=3, nullable=False)
|
||||
contact_email: EmailStr = Field()
|
||||
contact_phone_number: str = Field(min_length=2, max_length=16, schema_extra={"pattern": r'^\+[1-9]\d{1,14}$'})
|
||||
address: ShopAddress
|
||||
@@ -1,14 +1,15 @@
|
||||
import re
|
||||
from pydantic import EmailStr, model_validator
|
||||
from sqlmodel import Field, SQLModel
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, EmailStr, Field, model_validator
|
||||
|
||||
|
||||
class UserRegister(SQLModel):
|
||||
class UserRegister(BaseModel):
|
||||
username: str = Field(min_length=3, max_length=64)
|
||||
email: EmailStr = Field()
|
||||
phone_number: str = Field(min_length=2, max_length=16, schema_extra={"pattern": r'^\+[1-9]\d{1,14}$'})
|
||||
password: str = Field(min_length=8, max_length=128,
|
||||
description="Password must be at least 8 and at most 128 characters long, contain a lower case, upper case letter, a number and a special character (#?!@$ %^&*-)")
|
||||
phone_number: str = Field(min_length=2, max_length=16, pattern=r'^\+[1-9]\d{1,14}$')
|
||||
password: str = Field(min_length=8, max_length=128, examples=["Abc123#!"],
|
||||
description="Password must conform to this regex: \n```\n^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$ %^&*-]).{8,128}$\n```")
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_using_regex(self):
|
||||
@@ -20,11 +21,29 @@ class UserRegister(SQLModel):
|
||||
if not re.match(password_regex, self.password):
|
||||
raise ValueError("Password is too weak")
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
username: Optional[str] = Field(None, min_length=3, max_length=64)
|
||||
first_name: Optional[str] = Field(None, max_length=64)
|
||||
last_name: Optional[str] = Field(None, max_length=64)
|
||||
email: Optional[EmailStr] = Field(None)
|
||||
phone_number: Optional[str] = Field(None, min_length=2, max_length=16, pattern=r'^\+[1-9]\d{1,14}$')
|
||||
password: Optional[str] = Field(None, min_length=8, max_length=128, examples=["Abc123#!"],
|
||||
description="Password must conform to this regex: \n```\n^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$ %^&*-]).{8,128}$\n```")
|
||||
|
||||
class Token(SQLModel):
|
||||
@model_validator(mode="after")
|
||||
def validate_using_regex(self):
|
||||
self.__validate_password()
|
||||
return self
|
||||
|
||||
def __validate_password(self):
|
||||
password_regex = r"^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$ %^&*-]).{8,128}$"
|
||||
if not re.match(password_regex, self.password):
|
||||
raise ValueError("Password is too weak")
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str = "bearer"
|
||||
|
||||
|
||||
class TokenPayload(SQLModel):
|
||||
class TokenPayload(BaseModel):
|
||||
sub: str | None = None
|
||||
@@ -1,27 +1,19 @@
|
||||
import logging
|
||||
from functools import wraps
|
||||
|
||||
from fastapi import HTTPException
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from sqlalchemy.exc import DatabaseError as SqlAlchemyDatabaseError
|
||||
|
||||
from app.core.errors import RepositoryError, ServiceError
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def propagate_db_error_to_service(func):
|
||||
def raise_http_errors(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except (SqlAlchemyDatabaseError, RepositoryError) as e:
|
||||
raise ServiceError(str(e)) from e
|
||||
return wrapper
|
||||
|
||||
|
||||
def propagate_service_errors_to_http_errors(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except ServiceError as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
except Exception as e:
|
||||
if isinstance(e, HTTPException):
|
||||
raise
|
||||
logger.error(e)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Internal server error")
|
||||
return wrapper
|
||||
Reference in new issue
Block a user