diff --git a/.gitignore b/.gitignore index 2a4e7eb..8567071 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .venv __pycache__ media -*.env \ No newline at end of file +*.env +test.py \ No newline at end of file diff --git a/src/app/core/config.py b/src/app/core/config.py index 0352acb..35c6750 100644 --- a/src/app/core/config.py +++ b/src/app/core/config.py @@ -1,4 +1,3 @@ -from typing import Optional from pathlib import Path from pydantic_settings import BaseSettings diff --git a/src/app/core/security.py b/src/app/core/security.py index 0b5597d..88b4f6d 100644 --- a/src/app/core/security.py +++ b/src/app/core/security.py @@ -1,5 +1,11 @@ -from datetime import datetime, timedelta -from typing import Any, Union +from datetime import ( + datetime, + timedelta +) +from typing import ( + Any, + Union +) from jose import jwt from passlib.context import CryptContext diff --git a/src/app/crud/base.py b/src/app/crud/base.py index 9a8b5c3..855381a 100644 --- a/src/app/crud/base.py +++ b/src/app/crud/base.py @@ -1,7 +1,18 @@ -from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union +from typing import ( + Any, + Dict, + Generic, + List, + Optional, + Type, + TypeVar, + Union +) + +from pydantic import BaseModel from fastapi.encoders import jsonable_encoder -from pydantic import BaseModel + from sqlalchemy.future import select from sqlalchemy.orm import Session @@ -37,6 +48,14 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): obj = await db.execute(q) return obj.scalars() + def sync_get_multi( + self, db: Session, *, skip: int = 0, limit: int = 100 + ) -> List[ModelType]: + + q = select(self.model).offset(skip).limit(limit).order_by(self.model.id.desc()) + obj = db.execute(q) + return obj.scalars() + async def create(self, db: Session, *, obj_in: CreateSchemaType) -> ModelType: obj_in_data = jsonable_encoder(obj_in) db_obj = self.model(**obj_in_data) # type: ignore diff --git a/src/app/crud/crud_user.py b/src/app/crud/crud_user.py index d84c42a..c588f65 100644 --- a/src/app/crud/crud_user.py +++ b/src/app/crud/crud_user.py @@ -15,6 +15,11 @@ class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]): obj = await db.execute(q) return obj.scalar_one_or_none() + def sync_get_by_email(self, db: Session, *, email: str) -> Optional[User]: + q = select(self.model).where(self.model.email == email) + obj = db.execute(q) + return obj.scalar_one_or_none() + async def create(self, db: Session, *, obj_in: UserCreate) -> User: db_obj = User( email=obj_in.email, @@ -29,6 +34,20 @@ class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]): return db_obj + def sync_create(self, db: Session, *, obj_in: UserCreate) -> User: + db_obj = User( + email=obj_in.email, + hashed_password=get_password_hash(obj_in.password), + username=obj_in.username, + is_superuser=obj_in.is_superuser, + ) + + db.add(db_obj) + db.commit() + db.refresh(db_obj) + + return db_obj + async def update( self, db: Session, *, db_obj: User, obj_in: Union[UserUpdate, Dict[str, Any]] ) -> User: diff --git a/src/app/db/base_class.py b/src/app/db/base_class.py index 4d4414d..df4c135 100644 --- a/src/app/db/base_class.py +++ b/src/app/db/base_class.py @@ -1,7 +1,10 @@ from typing import Any from sqlalchemy.ext.declarative import as_declarative, declared_attr -from sqlalchemy import DateTime, Column +from sqlalchemy import ( + DateTime, + Column +) from sqlalchemy.sql import func diff --git a/src/app/main.py b/src/app/main.py index 5f40f7c..a3f06e9 100644 --- a/src/app/main.py +++ b/src/app/main.py @@ -11,7 +11,7 @@ from app.schemas.login import LoginForm from app.schemas.post import Post, PostCreate, PostUpdate from sqlalchemy.ext.asyncio import AsyncSession from app.schemas.user import User, UserCreate -from fastapi.responses import HTMLResponse +from fastapi.responses import FileResponse, HTMLResponse from app.views.depends import get_async_db, handle_image from typing import Annotated, Any @@ -46,6 +46,11 @@ async def validation_exception_handler(request, exc): return await request_validation_exception_handler(request, exc) +@app.exception_handler(404) +async def custom_404_handler(_, __): + return FileResponse(settings.STATIC_FOLDER / '404.jpg') + + # @app.post("/login", response_model=JWTToken) # async def login( # db: AsyncSession = Depends(get_async_db), diff --git a/src/app/models/post.py b/src/app/models/post.py index 8f91191..775cd67 100644 --- a/src/app/models/post.py +++ b/src/app/models/post.py @@ -1,3 +1,4 @@ +from contextlib import contextmanager from slugify import slugify from sqlalchemy.orm.base import NO_VALUE @@ -34,16 +35,15 @@ from app.core.config import settings def generate_slug(target, value, oldvalue, initiator): slug = slugify(value) - db = next(get_db()) + with contextmanager(get_db)() as db: + number = 1 + temp_slug = slug - number = 1 - temp_slug = slug + while db.query(Post).filter(Post.slug == temp_slug).first() is not None: + temp_slug = f'{slug}-{number}' + number += 1 - while db.query(Post).filter(Post.slug == temp_slug).first() is not None: - temp_slug = f'{slug}-{number}' - number += 1 - - target.slug = temp_slug + target.slug = temp_slug listen(Post.title, 'set', generate_slug) diff --git a/src/app/schemas/__init__.py b/src/app/schemas/__init__.py index 8e7cc3e..94e4d11 100644 --- a/src/app/schemas/__init__.py +++ b/src/app/schemas/__init__.py @@ -10,7 +10,8 @@ from .user import ( User, UserCreate, UserInDBBase, - UserUpdate + UserUpdate, + UserBase ) from .login import ( JWTToken, diff --git a/src/app/schemas/login.py b/src/app/schemas/login.py index f9d5d60..3c0d13c 100644 --- a/src/app/schemas/login.py +++ b/src/app/schemas/login.py @@ -1,5 +1,6 @@ from dataclasses import dataclass from typing import Optional + from fastapi import Form from pydantic import ( diff --git a/src/app/schemas/user.py b/src/app/schemas/user.py index 39d8605..593a307 100644 --- a/src/app/schemas/user.py +++ b/src/app/schemas/user.py @@ -1,6 +1,9 @@ from typing import Optional -from pydantic import BaseModel, EmailStr +from pydantic import ( + BaseModel, + EmailStr +) class UserBase(BaseModel): diff --git a/src/app/templates/admin/login.html b/src/app/templates/admin/login.html index 2204ec6..13ea62d 100644 --- a/src/app/templates/admin/login.html +++ b/src/app/templates/admin/login.html @@ -13,28 +13,53 @@
-
+
- +
- +
- +
+ + diff --git a/src/app/templates/components/navbar.html b/src/app/templates/components/navbar.html deleted file mode 100644 index 9a8c7aa..0000000 --- a/src/app/templates/components/navbar.html +++ /dev/null @@ -1 +0,0 @@ -

Navbar

\ No newline at end of file diff --git a/src/app/utils/create_user.py b/src/app/utils/create_user.py index e69de29..89fecb6 100644 --- a/src/app/utils/create_user.py +++ b/src/app/utils/create_user.py @@ -0,0 +1,119 @@ +from sys import path +path.append('/src') + +from contextlib import contextmanager +from typing import Optional + +from pydantic import ( + EmailStr, + ValidationError, + ConfigDict +) + +from app import crud +from app.schemas import UserCreate +from app.views.depends import get_db + + +class UserCreateCommand(UserCreate): + model_config = ConfigDict(validate_assignment=True) + + email: Optional[EmailStr] = None + password: Optional[str] = None + + +if __name__ == "__main__": + user_in = UserCreateCommand() + + while 1: + email = input('*Email: ') + + if not email: + print('Email is required\n') + continue + + try: + user_in.email = email + + except ValidationError as e: + print('\n', e, end='\n\n') + continue + + with contextmanager(get_db)() as db: + user = crud.user.sync_get_by_email( + db, + email=user_in.email + ) + + if user: + print('User already exists\n') + continue + + break + + while 1: + username = input('Username: ') + + if username: + try: + user_in.username = username + + except ValidationError as e: + print('\n', e, end='\n\n') + continue + + break + + while 1: + password = input('*Password: ') + + if not password: + print('Password is required\n') + continue + + try: + user_in.password = password + + except ValidationError as e: + print('\n', e, end='\n\n') + continue + + break + + while 1: + is_active = input('Is active? y/n (y): ') or 'y' + + if is_active == 'y': + user_in.is_active = True + + elif is_active == 'n': + user_in.is_active = False + + else: + print('Invalid input\n') + continue + + break + + while 1: + is_superuser = input('Is superuser? y/n (n): ') or 'n' + + if is_superuser == 'y': + user_in.is_superuser = True + + elif is_superuser == 'n': + user_in.is_superuser = False + + else: + print('Invalid input\n') + continue + + break + + with contextmanager(get_db)() as db: + user = crud.user.sync_create( + db, + obj_in=user_in + ) + + print(f'\nUser created:\n{user_in}\n') \ No newline at end of file diff --git a/src/app/utils/email.py b/src/app/utils/email_utils.py similarity index 100% rename from src/app/utils/email.py rename to src/app/utils/email_utils.py diff --git a/src/app/views/depends.py b/src/app/views/depends.py index 11db439..3c69903 100644 --- a/src/app/views/depends.py +++ b/src/app/views/depends.py @@ -1,8 +1,12 @@ import io -from pathlib import Path -from typing import Annotated, Generator +from typing import ( + Annotated, + Generator +) + from PIL import Image from jose import jwt + from pydantic import ValidationError from sqlalchemy.ext.asyncio import AsyncSession @@ -20,7 +24,10 @@ from fastapi.security import OAuth2PasswordBearer from app.models.user import User as UserModel from app.core import security from app.core.config import settings -from app.db.session import SessionLocal, AsyncSessionLocal +from app.db.session import ( + SessionLocal, + AsyncSessionLocal +) from app import crud from app import schemas from app.utils.image_operations import ( @@ -31,22 +38,15 @@ from app.utils.image_operations import ( reusable_oauth2 = OAuth2PasswordBearer(tokenUrl='/login') -def get_db() -> Generator: - try: - db = SessionLocal() - yield db - finally: - db.close() +def get_db() -> Generator: + with SessionLocal() as db: + yield db async def get_async_db() -> Generator: - try: - async with AsyncSessionLocal() as async_db: - yield async_db - - finally: - await async_db.close() + async with AsyncSessionLocal() as async_db: + yield await async_db async def get_access_token_from_cookie(access_token: Annotated[str, Cookie()]): diff --git a/src/app/views/routers/main.py b/src/app/views/routers/main.py index 6d093de..1b6b483 100644 --- a/src/app/views/routers/main.py +++ b/src/app/views/routers/main.py @@ -19,7 +19,7 @@ from app import crud from app.core.config import settings from app.schemas import ListPostInTemplate from app.schemas.main import SendEmail -from app.utils.email import send_email_notification +from app.utils.email_utils import send_email_notification from app.views.depends import get_async_db diff --git a/src/app/views/routers/user.py b/src/app/views/routers/user.py index 22645ab..89f7144 100644 --- a/src/app/views/routers/user.py +++ b/src/app/views/routers/user.py @@ -1,10 +1,12 @@ -from typing import Any from datetime import timedelta from sqlalchemy.ext.asyncio import AsyncSession from fastapi.templating import Jinja2Templates -from fastapi.responses import FileResponse, HTMLResponse +from fastapi.responses import ( + FileResponse, + HTMLResponse +) from fastapi import ( APIRouter, HTTPException, @@ -36,7 +38,8 @@ async def login( return templates.TemplateResponse( 'admin/login.html', { - 'request': request + 'request': request, + 'login_url': f'/{settings.SECRET_KEY[-10:]}' } ) diff --git a/src/static/404.jpg b/src/static/404.jpg new file mode 100644 index 0000000..7ece241 Binary files /dev/null and b/src/static/404.jpg differ