Shop registration works now

This commit is contained in:
Thastertyn committed 2025-03-14 13:40:50 +01:00
1 parent a0d238c840
commit d87c280109
15 files changed
+98 -727

No files matched your search

+4 -5
View File
@@ -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
+2 -2
View File
@@ -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
)
)
+6 -6
View File
@@ -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
+22 -18
View File
@@ -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")
+16 -12
View File
@@ -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"]
+6
View File
@@ -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
+27 -8
View File
@@ -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
+9 -17
View File
@@ -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