You've already forked Promocode-API
mirror of
https://github.com/devitq/Promocode-API.git
synced 2026-05-22 20:57:11 +00:00
feat: added business models, implemented business signup and signin
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PingConfig(AppConfig):
|
||||
name = "api.v1.business"
|
||||
label = "api_v1_business"
|
||||
@@ -0,0 +1,45 @@
|
||||
import uuid
|
||||
from typing import ClassVar
|
||||
|
||||
from ninja import ModelSchema, Schema
|
||||
from pydantic import EmailStr
|
||||
|
||||
from apps.business.models import Business
|
||||
|
||||
|
||||
class BusinessSignUpIn(ModelSchema):
|
||||
email: EmailStr
|
||||
|
||||
class Meta:
|
||||
model = Business
|
||||
fields: ClassVar[list[str]] = [
|
||||
Business.name.field.name,
|
||||
Business.password.field.name,
|
||||
]
|
||||
|
||||
|
||||
class BusinessSignUpOut(Schema):
|
||||
token: str
|
||||
company_id: uuid.UUID
|
||||
|
||||
|
||||
class BusinessSignInIn(ModelSchema):
|
||||
email: EmailStr
|
||||
|
||||
class Meta:
|
||||
model = Business
|
||||
fields: ClassVar[list[str]] = [
|
||||
Business.password.field.name,
|
||||
]
|
||||
|
||||
|
||||
class BusinessSignInOut(Schema):
|
||||
token: str
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BusinessSignInIn",
|
||||
"BusinessSignInOut",
|
||||
"BusinessSignUpIn",
|
||||
"BusinessSignUpOut",
|
||||
]
|
||||
@@ -0,0 +1,57 @@
|
||||
from http import HTTPStatus as status
|
||||
|
||||
from django.http import HttpRequest
|
||||
from ninja import Router
|
||||
from ninja.errors import AuthenticationError
|
||||
|
||||
from api.v1 import schemas as global_schemas
|
||||
from api.v1.business import schemas
|
||||
from apps.business.models import Business
|
||||
|
||||
router = Router(tags=["business"])
|
||||
|
||||
|
||||
@router.post(
|
||||
"/auth/sign-up",
|
||||
response={
|
||||
status.OK: schemas.BusinessSignUpOut,
|
||||
status.BAD_REQUEST: global_schemas.ValidationError,
|
||||
status.CONFLICT: global_schemas.UniqueConstraintError,
|
||||
},
|
||||
)
|
||||
def signup(
|
||||
request: HttpRequest, business: schemas.BusinessSignUpIn
|
||||
) -> tuple[int, schemas.BusinessSignUpOut]:
|
||||
business_obj = Business(**business.dict())
|
||||
business_obj.save()
|
||||
|
||||
return status.OK, schemas.BusinessSignUpOut(
|
||||
token=business_obj.generate_token(), company_id=business_obj.id
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/auth/sign-in",
|
||||
response={
|
||||
status.OK: schemas.BusinessSignInOut,
|
||||
status.BAD_REQUEST: global_schemas.ValidationError,
|
||||
status.UNAUTHORIZED: global_schemas.UnauthorizedError,
|
||||
},
|
||||
)
|
||||
def signin(
|
||||
request: HttpRequest, login_data: schemas.BusinessSignInIn
|
||||
) -> tuple[int, schemas.BusinessSignInOut]:
|
||||
try:
|
||||
business_obj = Business.objects.get(email=login_data.email)
|
||||
except Business.DoesNotExist:
|
||||
raise AuthenticationError from None
|
||||
|
||||
if business_obj.password != login_data.password:
|
||||
raise AuthenticationError
|
||||
|
||||
business_obj.token_version += 1
|
||||
business_obj.save()
|
||||
|
||||
return status.OK, schemas.BusinessSignInOut(
|
||||
token=business_obj.generate_token()
|
||||
)
|
||||
@@ -6,9 +6,28 @@ import django.http
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from ninja import NinjaAPI, errors
|
||||
|
||||
from config.errors import UniqueConstraintError
|
||||
|
||||
logger = logging.getLogger("django")
|
||||
|
||||
|
||||
def handle_unique_constraint_error(
|
||||
request: HttpRequest,
|
||||
exc: UniqueConstraintError,
|
||||
router: NinjaAPI,
|
||||
) -> HttpResponse:
|
||||
detail = list(exc.validation_error)
|
||||
|
||||
if hasattr(exc, "error_dict"):
|
||||
detail = dict(exc.validation_error)
|
||||
|
||||
return router.create_response(
|
||||
request,
|
||||
{"detail": detail},
|
||||
status=status.CONFLICT,
|
||||
)
|
||||
|
||||
|
||||
def handle_django_validation_error(
|
||||
request: HttpRequest,
|
||||
exc: django.core.exceptions.ValidationError,
|
||||
@@ -22,7 +41,7 @@ def handle_django_validation_error(
|
||||
return router.create_response(
|
||||
request,
|
||||
{"detail": detail},
|
||||
status=status.UNPROCESSABLE_ENTITY,
|
||||
status=status.BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
@@ -42,7 +61,7 @@ def handle_validation_error(
|
||||
return router.create_response(
|
||||
request,
|
||||
{"detail": exc.errors},
|
||||
status=status.UNPROCESSABLE_ENTITY,
|
||||
status=status.BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
@@ -69,6 +88,7 @@ def handle_unknown_exception(
|
||||
|
||||
|
||||
exception_handlers = [
|
||||
(UniqueConstraintError, handle_unique_constraint_error),
|
||||
(django.core.exceptions.ValidationError, handle_django_validation_error),
|
||||
(errors.AuthenticationError, handle_authentication_error),
|
||||
(errors.ValidationError, handle_validation_error),
|
||||
|
||||
@@ -3,6 +3,7 @@ from functools import partial
|
||||
from ninja import NinjaAPI
|
||||
|
||||
from api.v1 import handlers
|
||||
from api.v1.business.views import router as business_router
|
||||
from api.v1.ping.views import router as ping_router
|
||||
|
||||
router = NinjaAPI(
|
||||
@@ -20,6 +21,10 @@ router.add_router(
|
||||
"ping",
|
||||
ping_router,
|
||||
)
|
||||
router.add_router(
|
||||
"business",
|
||||
business_router,
|
||||
)
|
||||
|
||||
|
||||
# Register exception handlers
|
||||
|
||||
@@ -9,3 +9,19 @@ class UnauthorizedError(Schema):
|
||||
|
||||
class NotFoundError(Schema):
|
||||
detail: str = status.NOT_FOUND.phrase
|
||||
|
||||
|
||||
class ValidationError(Schema):
|
||||
detail: str
|
||||
|
||||
|
||||
class UniqueConstraintError(Schema):
|
||||
detail: str
|
||||
|
||||
|
||||
__all__ = [
|
||||
"NotFoundError",
|
||||
"UnauthorizedError",
|
||||
"UniqueConstraintError",
|
||||
"ValidationError",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from apps.business.models import Business
|
||||
|
||||
admin.site.register(Business)
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BusinessConfig(AppConfig):
|
||||
name = "apps.business"
|
||||
label = "business"
|
||||
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 5.1.5 on 2025-01-20 11:21
|
||||
|
||||
import django.core.validators
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Business',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=50, validators=[django.core.validators.MinLengthValidator(5)])),
|
||||
('email', models.EmailField(max_length=120, unique=True, validators=[django.core.validators.MinLengthValidator(8)])),
|
||||
('password', models.CharField(max_length=60, validators=[django.core.validators.RegexValidator('^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$')])),
|
||||
('token_version', models.BigIntegerField(default=0)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,37 @@
|
||||
import jwt
|
||||
from django.conf import settings
|
||||
from django.core.validators import MinLengthValidator, RegexValidator
|
||||
from django.db import models
|
||||
|
||||
from apps.core.models import BaseModel
|
||||
|
||||
|
||||
class Business(BaseModel):
|
||||
name = models.CharField(max_length=50, validators=[MinLengthValidator(5)])
|
||||
email = models.EmailField(
|
||||
unique=True,
|
||||
max_length=120,
|
||||
validators=[MinLengthValidator(8)],
|
||||
)
|
||||
password = models.CharField(
|
||||
max_length=60,
|
||||
validators=[
|
||||
RegexValidator(
|
||||
r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$"
|
||||
),
|
||||
],
|
||||
)
|
||||
token_version = models.BigIntegerField(default=0)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def generate_token(self) -> str:
|
||||
return jwt.encode(
|
||||
{
|
||||
"business_id": str(self.id),
|
||||
"token_version": self.token_version,
|
||||
},
|
||||
settings.SECRET_KEY,
|
||||
algorithm="HS256",
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CoreConfig(AppConfig):
|
||||
name = "apps.core"
|
||||
label = "core"
|
||||
@@ -0,0 +1,23 @@
|
||||
import uuid
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
|
||||
from config.errors import UniqueConstraintError
|
||||
|
||||
|
||||
class BaseModel(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def save(self, *args, **kwargs) -> None: # noqa: ANN002, ANN003
|
||||
self.full_clean(validate_unique=False)
|
||||
|
||||
try:
|
||||
self.validate_unique()
|
||||
except ValidationError as e:
|
||||
raise UniqueConstraintError(e) from None
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
@@ -0,0 +1,9 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
|
||||
class UniqueConstraintError(Exception):
|
||||
def __init__(self, validation_error: ValidationError) -> None:
|
||||
self.validation_error = validation_error
|
||||
|
||||
|
||||
__all__ = ["UniqueConstraintError"]
|
||||
@@ -414,8 +414,11 @@ INSTALLED_APPS = [
|
||||
"django_guid",
|
||||
"ninja",
|
||||
# Internal apps
|
||||
"apps.core",
|
||||
"apps.business",
|
||||
# API v1 apps
|
||||
"api.v1.ping",
|
||||
"api.v1.business",
|
||||
]
|
||||
|
||||
# GUID
|
||||
|
||||
@@ -14,6 +14,9 @@ dependencies = [
|
||||
"gunicorn>=23.0.0",
|
||||
"httpx>=0.28.1",
|
||||
"psycopg2-binary>=2.9.10",
|
||||
"pydantic>=2.10.5",
|
||||
"pydantic[email]>=2.10.5",
|
||||
"pyjwt>=2.10.1",
|
||||
"python-json-logger>=3.2.1",
|
||||
"redis>=5.2.1",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user