Skip to content

Commit 751a44a

Browse files
authored
Merge pull request #138 from ilyarolf/feature/sqladmin
Feature/sqladmin
2 parents f7284d2 + 9414839 commit 751a44a

20 files changed

Lines changed: 466 additions & 8 deletions

.env

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,8 @@ REFERRER_BONUS_PERCENT = "3"
3131
REFERRER_BONUS_DEPOSIT_LIMIT = "5"
3232
REFERRAL_BONUS_CAP_PERCENT = "7"
3333
REFERRER_BONUS_CAP_PERCENT = "7"
34-
TOTAL_BONUS_CAP_PERCENT = "12"
34+
TOTAL_BONUS_CAP_PERCENT = "12"
35+
SQLADMIN_RAW_PASSWORD = ""
36+
JWT_EXPIRE_MINUTES = "30"
37+
JWT_ALGORITHM = "HS256"
38+
JWT_SECRET_KEY = ""

admin.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from sqladmin.authentication import AuthenticationBackend
2+
from starlette.requests import Request
3+
4+
import config
5+
from config import SQLADMIN_HASHED_PASSWORD
6+
from enums.user_role import UserRole
7+
from utils.utils import create_access_token, decode_token, verify_password
8+
9+
10+
class AdminAuth(AuthenticationBackend):
11+
async def login(self, request: Request) -> bool:
12+
form = await request.form()
13+
username = form.get("username")
14+
password = form.get("password")
15+
16+
if username == "admin" and verify_password(password, SQLADMIN_HASHED_PASSWORD):
17+
token = create_access_token(
18+
{
19+
"sub": UserRole.ADMIN.name,
20+
}
21+
)
22+
23+
request.session.update({"token": token})
24+
return True
25+
else:
26+
return False
27+
28+
async def logout(self, request: Request) -> bool:
29+
request.session.clear()
30+
return True
31+
32+
async def authenticate(self, request: Request) -> bool:
33+
token = request.session.get("token")
34+
if not token:
35+
return False
36+
37+
payload = decode_token(token)
38+
if not payload:
39+
return False
40+
41+
return True
42+
43+
44+
authentication_backend = AdminAuth(secret_key=config.JWT_SECRET_KEY)

bot.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,32 @@
66
from aiogram.fsm.storage.redis import RedisStorage
77
from aiogram.types import BufferedInputFile
88
from redis.asyncio import Redis
9+
from sqladmin import Admin
10+
911
import config
1012
from aiogram import Bot, Dispatcher
1113
from aiogram.enums import ParseMode
1214
from fastapi import FastAPI, Request, status, HTTPException
13-
from db import create_db_and_tables
15+
16+
from admin import authentication_backend
17+
from db import create_db_and_tables, engine
1418
import uvicorn
1519
from fastapi.responses import JSONResponse
1620
from enums.cryptocurrency import Cryptocurrency
21+
from models.buy import BuyAdmin
22+
from models.buyItem import BuyItemAdmin
23+
from models.cart import CartAdmin
24+
from models.cartItem import CartItemAdmin
25+
from models.category import CategoryAdmin
26+
from models.coupon import CouponAdmin
27+
from models.deposit import DepositAdmin
28+
from models.item import ItemAdmin
29+
from models.payment import PaymentAdmin
30+
from models.referral import ReferralBonusAdmin
31+
from models.review import ReviewAdmin
32+
from models.shipping_option import ShippingOptionAdmin
33+
from models.subcategory import SubcategoryAdmin
34+
from models.user import UserAdmin
1735
from processing.processing import processing_router
1836
from repositories.button_media import ButtonMediaRepository
1937
from services.notification import NotificationService
@@ -24,6 +42,22 @@
2442
bot = Bot(config.TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
2543
dp = Dispatcher(storage=RedisStorage(redis))
2644
app = FastAPI()
45+
admin = Admin(app=app, engine=engine, authentication_backend=authentication_backend)
46+
admin.add_model_view(UserAdmin)
47+
admin.add_model_view(BuyAdmin)
48+
admin.add_model_view(ShippingOptionAdmin)
49+
admin.add_model_view(CouponAdmin)
50+
admin.add_model_view(CategoryAdmin)
51+
admin.add_model_view(SubcategoryAdmin)
52+
admin.add_model_view(ItemAdmin)
53+
admin.add_model_view(DepositAdmin)
54+
admin.add_model_view(BuyItemAdmin)
55+
admin.add_model_view(PaymentAdmin)
56+
admin.add_model_view(CartAdmin)
57+
admin.add_model_view(CartItemAdmin)
58+
admin.add_model_view(ReferralBonusAdmin)
59+
admin.add_model_view(ReviewAdmin)
60+
2761
app.include_router(processing_router)
2862

2963

config.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from enums.currency import Currency
66
from enums.runtime_environment import RuntimeEnvironment
7-
from utils.utils import get_sslipio_external_url, start_ngrok
7+
from utils.utils import get_sslipio_external_url, start_ngrok, hash_password
88

99
load_dotenv(".env")
1010
RUNTIME_ENVIRONMENT = RuntimeEnvironment(os.environ.get("RUNTIME_ENVIRONMENT"))
@@ -48,3 +48,9 @@
4848
REFERRAL_BONUS_CAP_PERCENT = float(os.environ.get("REFERRAL_BONUS_CAP_PERCENT", "7"))
4949
REFERRER_BONUS_CAP_PERCENT = float(os.environ.get("REFERRER_BONUS_CAP_PERCENT", "7"))
5050
TOTAL_BONUS_CAP_PERCENT = float(os.environ.get("TOTAL_BONUS_CAP_PERCENT", "12"))
51+
# SQLADMIN
52+
SQLADMIN_RAW_PASSWORD = os.environ.get("SQLADMIN_RAW_PASSWORD")
53+
SQLADMIN_HASHED_PASSWORD = hash_password(SQLADMIN_RAW_PASSWORD)
54+
JWT_EXPIRE_MINUTES = int(os.environ.get("JWT_EXPIRE_MINUTES", "30"))
55+
JWT_ALGORITHM = os.environ.get("JWT_ALGORITHM", "HS256")
56+
JWT_SECRET_KEY = os.environ.get("JWT_SECRET_KEY")

models/buy.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from datetime import datetime
22

33
from pydantic import BaseModel
4+
from sqladmin import ModelView
45
from sqlalchemy import Column, Integer, Float, DateTime, ForeignKey, func, CheckConstraint, Enum, String
56
from sqlalchemy.orm import relationship
67

@@ -17,21 +18,34 @@ class Buy(Base):
1718

1819
id = Column(Integer, primary_key=True)
1920
buyer_id = Column(Integer, ForeignKey('users.id'), nullable=True)
20-
buyer = relationship('User', backref='buys')
21+
buyer = relationship("User", back_populates="buys")
2122
total_price = Column(Float, nullable=False)
2223
buy_datetime = Column(DateTime, default=func.now())
2324
status = Column(Enum(BuyStatus), nullable=False)
2425
coupon_id = Column(Integer, ForeignKey('coupons.id'), nullable=True)
26+
coupon = relationship("Coupon", back_populates="buys")
2527
discount = Column(Float, nullable=False, default=0.0)
2628
shipping_address = Column(String, nullable=True)
2729
track_number = Column(String, nullable=True)
2830
shipping_option_id = Column(Integer, ForeignKey('shipping_options.id'), nullable=True)
31+
shipping_option = relationship("ShippingOption", back_populates="buys")
32+
buy_items = relationship(
33+
"BuyItem",
34+
back_populates="buy",
35+
cascade="all, delete-orphan"
36+
)
2937

3038
__table_args__ = (
31-
CheckConstraint('quantity > 0', name='check_quantity_positive'),
3239
CheckConstraint('total_price > 0', name='check_total_price_positive'),
3340
)
3441

42+
def __repr__(self):
43+
return get_text(Language.EN, BotEntity.USER, "purchase_history_item").format(
44+
buy_id=self.id,
45+
total_price=self.total_price,
46+
currency_sym=config.CURRENCY.get_localized_symbol()
47+
)
48+
3549

3650
class BuyDTO(BaseModel):
3751
id: int | None = None
@@ -60,3 +74,20 @@ class RefundDTO(BaseModel):
6074
item_ids: list[int] | None = None
6175
buy_id: int | None = None
6276
language: Language
77+
78+
79+
class BuyAdmin(ModelView, model=Buy):
80+
column_exclude_list = [Buy.shipping_option_id,
81+
Buy.coupon_id,
82+
Buy.buyer_id]
83+
column_sortable_list = [Buy.id,
84+
Buy.total_price,
85+
Buy.buy_datetime,
86+
Buy.status,
87+
Buy.discount,
88+
Buy.shipping_address,
89+
Buy.track_number]
90+
can_create = False
91+
can_delete = False
92+
can_edit = False
93+
can_export = False

models/buyItem.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from pydantic import BaseModel
2+
from sqladmin import ModelView
23
from sqlalchemy import Column, Integer, ForeignKey, JSON
4+
from sqlalchemy.orm import relationship
35

46
from models.base import Base
57

@@ -9,12 +11,34 @@ class BuyItem(Base):
911

1012
id = Column(Integer, primary_key=True)
1113
buy_id = Column(Integer, ForeignKey("buys.id", ondelete="CASCADE"), nullable=False)
14+
buy = relationship(
15+
"Buy",
16+
back_populates="buy_items"
17+
)
1218
# ARRAY CRUTCH FOR SQLALCHEMY+SQLITE 🩼
1319
# item_ids = Column(ARRAY, nullable=False)
1420
item_ids = Column(JSON, nullable=False)
21+
review = relationship(
22+
"Review",
23+
back_populates="buy_item",
24+
uselist=False,
25+
cascade="all, delete-orphan",
26+
passive_deletes=True
27+
)
28+
29+
def __repr__(self):
30+
return f"BuyItem ID:{self.id}"
1531

1632

1733
class BuyItemDTO(BaseModel):
1834
id: int | None = None
1935
buy_id: int | None = None
2036
item_ids: list[int] | None = None
37+
38+
39+
class BuyItemAdmin(ModelView, model=BuyItem):
40+
column_exclude_list = [BuyItem.buy_id]
41+
can_create = False
42+
can_delete = False
43+
can_edit = False
44+
can_export = False

models/cart.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
# note that the item is NOT reserved or blocked so that the availability of the item
66
# needs to be checked again during checkout
77
from pydantic import BaseModel
8+
from sqladmin import ModelView
89
from sqlalchemy import Column, Integer, ForeignKey
10+
from sqlalchemy.orm import relationship
11+
912
from models.base import Base
1013

1114

@@ -14,8 +17,24 @@ class Cart(Base):
1417

1518
id = Column(Integer, primary_key=True)
1619
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
20+
user = relationship("User", back_populates="cart", uselist=False)
21+
cart_items = relationship("CartItem", back_populates="cart", cascade="all, delete-orphan")
22+
23+
def __repr__(self):
24+
return f"Cart ID:{self.id}"
1725

1826

1927
class CartDTO(BaseModel):
2028
id: int | None = None
2129
user_id: int | None = None
30+
31+
32+
class CartAdmin(ModelView, model=Cart):
33+
name = "Cart"
34+
name_plural = "Carts"
35+
column_sortable_list = [Cart.id]
36+
column_exclude_list = [Cart.user_id, Cart.cart_items]
37+
can_delete = False
38+
can_edit = False
39+
can_create = False
40+
can_export = False

models/cartItem.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from pydantic import BaseModel
2+
from sqladmin import ModelView
23
from sqlalchemy import Column, Integer, ForeignKey, CheckConstraint, Enum
4+
from sqlalchemy.orm import relationship
35

46
from enums.item_type import ItemType
57
from models.base import Base
@@ -10,15 +12,21 @@ class CartItem(Base):
1012

1113
id = Column(Integer, primary_key=True)
1214
cart_id = Column(Integer, ForeignKey("carts.id"), nullable=False)
15+
cart = relationship("Cart", back_populates="cart_items")
1316
item_type = Column(Enum(ItemType), nullable=False)
1417
category_id = Column(Integer, ForeignKey('categories.id'), nullable=False)
18+
category = relationship("Category", back_populates="cart_items")
1519
subcategory_id = Column(Integer, ForeignKey('subcategories.id'), nullable=False)
20+
subcategory = relationship("Subcategory", back_populates="cart_items")
1621
quantity = Column(Integer, nullable=False)
1722

1823
__table_args__ = (
1924
CheckConstraint('quantity > 0', name='check_quantity_positive'),
2025
)
2126

27+
def __repr__(self):
28+
return f"CartItem ID: {self.id}"
29+
2230

2331
class CartItemDTO(BaseModel):
2432
id: int | None = None
@@ -27,3 +35,17 @@ class CartItemDTO(BaseModel):
2735
category_id: int | None = None
2836
subcategory_id: int | None = None
2937
quantity: int | None = None
38+
39+
40+
class CartItemAdmin(ModelView, model=CartItem):
41+
column_sortable_list = [CartItem.id,
42+
CartItem.item_type,
43+
CartItem.quantity,
44+
CartItem.item_type]
45+
column_exclude_list = [CartItem.category_id,
46+
CartItem.subcategory_id,
47+
CartItem.cart_id]
48+
can_create = False
49+
can_delete = False
50+
can_edit = False
51+
can_export = False

models/category.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from pydantic import BaseModel
2+
from sqladmin import ModelView
23
from sqlalchemy import Integer, Column, String
4+
from sqlalchemy.orm import relationship
35

46
from models.base import Base
57

@@ -10,9 +12,28 @@ class Category(Base):
1012
id = Column(Integer, primary_key=True)
1113
name = Column(String, nullable=False, unique=True, index=True)
1214
media_id = Column(String, nullable=False)
15+
items = relationship("Item", back_populates="category")
16+
cart_items = relationship("CartItem", back_populates="category")
17+
18+
def __repr__(self):
19+
return self.name
1320

1421

1522
class CategoryDTO(BaseModel):
1623
id: int | None = None
1724
name: str | None = None
1825
media_id: str | None = None
26+
27+
28+
class CategoryAdmin(ModelView, model=Category):
29+
name = "Category"
30+
name_plural = "Categories"
31+
column_searchable_list = [Category.name]
32+
column_sortable_list = [Category.id, Category.name]
33+
column_exclude_list = [Category.items,
34+
Category.media_id,
35+
Category.cart_items]
36+
can_delete = False
37+
can_edit = True
38+
can_create = True
39+
can_export = False

0 commit comments

Comments
 (0)