Compare commits
6 Commits
19321e3ea2
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92ee087e5d | ||
|
|
37f2b39bd2 | ||
|
|
c1fab8feea | ||
|
|
0a5e8a62eb | ||
|
|
4e2cee6625 | ||
|
|
222f528b5e |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
|||||||
/.venv/
|
/.venv/
|
||||||
/.idea
|
/.idea
|
||||||
|
/src/db/*.db
|
||||||
|
.env
|
||||||
0
src/api/dependacies/__init__.py
Normal file
0
src/api/dependacies/__init__.py
Normal file
38
src/api/dependacies/dependancies.py
Normal file
38
src/api/dependacies/dependancies.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
from typing import Annotated, AsyncGenerator
|
||||||
|
|
||||||
|
from fastapi import Depends
|
||||||
|
from fastapi_users.authentication.strategy import AccessTokenDatabase
|
||||||
|
from fastapi_users_db_sqlalchemy import SQLAlchemyUserDatabase
|
||||||
|
from fastapi_users_db_sqlalchemy.access_token import SQLAlchemyAccessTokenDatabase
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.db.database import async_session_maker
|
||||||
|
from src.models import UsersORM, AccessToken
|
||||||
|
from src.utils.user_manager import UserManager
|
||||||
|
|
||||||
|
|
||||||
|
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
||||||
|
async with async_session_maker as db:
|
||||||
|
yield db
|
||||||
|
|
||||||
|
|
||||||
|
DBDep = Annotated[AsyncSession, Depends(get_db)]
|
||||||
|
|
||||||
|
|
||||||
|
async def get_users_db(session: DBDep):
|
||||||
|
yield SQLAlchemyUserDatabase(session, UsersORM)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_access_token_db(
|
||||||
|
session: DBDep,
|
||||||
|
):
|
||||||
|
yield SQLAlchemyAccessTokenDatabase(session, AccessToken)
|
||||||
|
|
||||||
|
|
||||||
|
ATDep = Annotated[AccessTokenDatabase[AccessToken], Depends(get_access_token_db)]
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user_manager(
|
||||||
|
users_db: Annotated[SQLAlchemyUserDatabase, Depends(get_users_db)],
|
||||||
|
):
|
||||||
|
yield UserManager(users_db)
|
||||||
21
src/api/dependacies/strategy.py
Normal file
21
src/api/dependacies/strategy.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from fastapi import Depends
|
||||||
|
from fastapi_users.authentication import AuthenticationBackend
|
||||||
|
from fastapi_users.authentication.strategy import DatabaseStrategy, AccessTokenDatabase
|
||||||
|
|
||||||
|
from src.api.dependacies.dependancies import get_access_token_db
|
||||||
|
from src.api.dependacies.transport import bearer_transport
|
||||||
|
from src.models import AccessToken
|
||||||
|
from src.settings import settings
|
||||||
|
|
||||||
|
|
||||||
|
def get_database_strategy(
|
||||||
|
access_token_db: AccessTokenDatabase[AccessToken] = Depends(get_access_token_db),
|
||||||
|
) -> DatabaseStrategy:
|
||||||
|
return DatabaseStrategy(access_token_db, lifetime_seconds=settings.lifetime)
|
||||||
|
|
||||||
|
|
||||||
|
auth_backend = AuthenticationBackend(
|
||||||
|
name="database",
|
||||||
|
transport=bearer_transport,
|
||||||
|
get_strategy=get_database_strategy,
|
||||||
|
)
|
||||||
4
src/api/dependacies/transport.py
Normal file
4
src/api/dependacies/transport.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from fastapi_users.authentication import BearerTransport
|
||||||
|
|
||||||
|
|
||||||
|
bearer_transport = BearerTransport(tokenUrl="auth/login")
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
from typing import Annotated, AsyncGenerator
|
|
||||||
|
|
||||||
from fastapi import Depends
|
|
||||||
from fastapi_users_db_sqlalchemy import SQLAlchemyUserDatabase
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
|
|
||||||
from src.db.database import async_session_maker
|
|
||||||
from src.models import UsersORM
|
|
||||||
|
|
||||||
|
|
||||||
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
|
||||||
async with async_session_maker as db:
|
|
||||||
yield db
|
|
||||||
|
|
||||||
|
|
||||||
DBDep = Annotated[AsyncSession, Depends(get_db)]
|
|
||||||
|
|
||||||
|
|
||||||
async def get_user_db(session: DBDep):
|
|
||||||
yield SQLAlchemyUserDatabase(session, UsersORM)
|
|
||||||
@@ -7,21 +7,21 @@ router = APIRouter(prefix="/tasks", tags=["Tasks"])
|
|||||||
async def get_tasks(): ...
|
async def get_tasks(): ...
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{task_id}")
|
@router.get("/{id}")
|
||||||
async def get_task_id(task_id: int): ...
|
async def get_task_id(id: int): ...
|
||||||
|
|
||||||
|
|
||||||
@router.post("/")
|
@router.post("/")
|
||||||
async def post_task(): ...
|
async def post_task(): ...
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{task_id}")
|
@router.put("/{id}")
|
||||||
async def put_task(task_id: int): ...
|
async def put_task(id: int): ...
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/{task_id}")
|
@router.patch("/{id}")
|
||||||
async def patch_task(task_id: int): ...
|
async def patch_task(id: int): ...
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{task_id}")
|
@router.delete("/{id}")
|
||||||
async def delete_task(task_id: int): ...
|
async def delete_task(id: int): ...
|
||||||
|
|||||||
@@ -68,9 +68,7 @@ def run_migrations_online() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
with connectable.connect() as connection:
|
with connectable.connect() as connection:
|
||||||
context.configure(
|
context.configure(connection=connection, target_metadata=target_metadata)
|
||||||
connection=connection, target_metadata=target_metadata
|
|
||||||
)
|
|
||||||
|
|
||||||
with context.begin_transaction():
|
with context.begin_transaction():
|
||||||
context.run_migrations()
|
context.run_migrations()
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ Revises:
|
|||||||
Create Date: 2025-06-22 11:52:49.691545
|
Create Date: 2025-06-22 11:52:49.691545
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from typing import Sequence, Union
|
||||||
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
revision: str = '932121e6b220'
|
revision: str = "932121e6b220"
|
||||||
down_revision: Union[str, None] = None
|
down_revision: Union[str, None] = None
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
@@ -19,41 +20,68 @@ depends_on: Union[str, Sequence[str], None] = None
|
|||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
"""Upgrade schema."""
|
"""Upgrade schema."""
|
||||||
op.create_table('users',
|
op.create_table(
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
"users",
|
||||||
sa.Column('username', sa.String(length=30), nullable=False),
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
sa.Column('telegram_id', sa.BigInteger(), nullable=True),
|
sa.Column("username", sa.String(length=30), nullable=False),
|
||||||
sa.Column('avatar_path', sa.String(length=255), nullable=True),
|
sa.Column("telegram_id", sa.BigInteger(), nullable=True),
|
||||||
sa.Column('email', sa.String(length=320), nullable=False),
|
sa.Column("avatar_path", sa.String(length=255), nullable=True),
|
||||||
sa.Column('hashed_password', sa.String(length=1024), nullable=False),
|
sa.Column("email", sa.String(length=320), nullable=False),
|
||||||
sa.Column('is_active', sa.Boolean(), nullable=False),
|
sa.Column("hashed_password", sa.String(length=1024), nullable=False),
|
||||||
sa.Column('is_superuser', sa.Boolean(), nullable=False),
|
sa.Column("is_active", sa.Boolean(), nullable=False),
|
||||||
sa.Column('is_verified', sa.Boolean(), nullable=False),
|
sa.Column("is_superuser", sa.Boolean(), nullable=False),
|
||||||
sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
sa.Column("is_verified", sa.Boolean(), nullable=False),
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.Column(
|
||||||
|
"created_at",
|
||||||
|
sa.TIMESTAMP(timezone=True),
|
||||||
|
server_default=sa.text("(CURRENT_TIMESTAMP)"),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
)
|
)
|
||||||
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
|
op.create_index(op.f("ix_users_email"), "users", ["email"], unique=True)
|
||||||
op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True)
|
op.create_index(op.f("ix_users_username"), "users", ["username"], unique=True)
|
||||||
op.create_table('tasks',
|
op.create_table(
|
||||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
"tasks",
|
||||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||||
sa.Column('title', sa.String(length=100), nullable=False),
|
sa.Column("user_id", sa.Integer(), nullable=False),
|
||||||
sa.Column('description', sa.Text(), nullable=True),
|
sa.Column("title", sa.String(length=100), nullable=False),
|
||||||
sa.Column('due_date', sa.Date(), nullable=True),
|
sa.Column("description", sa.Text(), nullable=True),
|
||||||
sa.Column('status', sa.Enum('open', 'closed', 'in_progress', 'todo', name='status_enum'), nullable=False),
|
sa.Column("due_date", sa.Date(), nullable=True),
|
||||||
sa.CheckConstraint("status IN ('open', 'closed', 'in_progress', 'todo')", name="ck_status_enum"),
|
sa.Column(
|
||||||
sa.Column('priority', sa.Enum('low', 'medium', 'high', 'critical', name='priority_enum'), nullable=False),
|
"status",
|
||||||
sa.CheckConstraint("priority in ('low', 'medium', 'high', 'critical')", name='ck_priority_enum'),
|
sa.Enum("open", "closed", "in_progress", "todo", name="status_enum"),
|
||||||
sa.Column('time_spent', sa.Integer(), nullable=False),
|
nullable=False,
|
||||||
sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
),
|
||||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
sa.CheckConstraint(
|
||||||
sa.PrimaryKeyConstraint('id')
|
"status IN ('open', 'closed', 'in_progress', 'todo')", name="ck_status_enum"
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"priority",
|
||||||
|
sa.Enum("low", "medium", "high", "critical", name="priority_enum"),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.CheckConstraint(
|
||||||
|
"priority in ('low', 'medium', 'high', 'critical')", name="ck_priority_enum"
|
||||||
|
),
|
||||||
|
sa.Column("time_spent", sa.Integer(), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"created_at",
|
||||||
|
sa.TIMESTAMP(timezone=True),
|
||||||
|
server_default=sa.text("(CURRENT_TIMESTAMP)"),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["user_id"],
|
||||||
|
["users.id"],
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
"""Downgrade schema."""
|
"""Downgrade schema."""
|
||||||
op.drop_table('tasks')
|
op.drop_table("tasks")
|
||||||
op.drop_index(op.f('ix_users_username'), table_name='users')
|
op.drop_index(op.f("ix_users_username"), table_name="users")
|
||||||
op.drop_index(op.f('ix_users_email'), table_name='users')
|
op.drop_index(op.f("ix_users_email"), table_name="users")
|
||||||
op.drop_table('users')
|
op.drop_table("users")
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
"""access token
|
||||||
|
|
||||||
|
Revision ID: bc0bdd74718c
|
||||||
|
Revises: 932121e6b220
|
||||||
|
Create Date: 2025-06-22 12:11:19.223212
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
import fastapi_users_db_sqlalchemy
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
revision: str = "bc0bdd74718c"
|
||||||
|
down_revision: Union[str, None] = "932121e6b220"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema."""
|
||||||
|
op.create_table(
|
||||||
|
"accesstoken",
|
||||||
|
sa.Column("user_id", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("token", sa.String(length=43), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"created_at",
|
||||||
|
fastapi_users_db_sqlalchemy.generics.TIMESTAMPAware(timezone=True),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="cascade"),
|
||||||
|
sa.PrimaryKeyConstraint("token"),
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
op.f("ix_accesstoken_created_at"), "accesstoken", ["created_at"], unique=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
op.drop_index(op.f("ix_accesstoken_created_at"), table_name="accesstoken")
|
||||||
|
op.drop_table("accesstoken")
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
|
from src.models.token import AccessToken
|
||||||
from src.models.users import UsersORM
|
from src.models.users import UsersORM
|
||||||
from src.models.tasks import TasksORM
|
from src.models.tasks import TasksORM
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"UsersORM",
|
"UsersORM",
|
||||||
"TasksORM",
|
"TasksORM",
|
||||||
|
"AccessToken",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ priority_enum = Enum("low", "medium", "high", "critical", name="priority_enum")
|
|||||||
|
|
||||||
|
|
||||||
class TasksORM(Base):
|
class TasksORM(Base):
|
||||||
|
|
||||||
__tablename__ = "tasks"
|
__tablename__ = "tasks"
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||||
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
|
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
|
||||||
@@ -25,4 +24,4 @@ class TasksORM(Base):
|
|||||||
priority: Mapped[str] = mapped_column(priority_enum, default="medium")
|
priority: Mapped[str] = mapped_column(priority_enum, default="medium")
|
||||||
time_spent: Mapped[int] = mapped_column(default=0)
|
time_spent: Mapped[int] = mapped_column(default=0)
|
||||||
|
|
||||||
user: Mapped["Users"] = relationship(back_populates="tasks")
|
user: Mapped["UsersORM"] = relationship(back_populates="tasks")
|
||||||
|
|||||||
13
src/models/token.py
Normal file
13
src/models/token.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from fastapi_users_db_sqlalchemy.access_token import SQLAlchemyBaseAccessTokenTable
|
||||||
|
from sqlalchemy import Integer, ForeignKey
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, declared_attr
|
||||||
|
|
||||||
|
from src.db.database import Base
|
||||||
|
|
||||||
|
|
||||||
|
class AccessToken(SQLAlchemyBaseAccessTokenTable[int], Base):
|
||||||
|
@declared_attr
|
||||||
|
def user_id(cls) -> Mapped[int]:
|
||||||
|
return mapped_column(
|
||||||
|
Integer, ForeignKey("users.id", ondelete="cascade"), nullable=False
|
||||||
|
)
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi_users import schemas
|
||||||
|
|
||||||
|
|
||||||
|
class UserRead(schemas.BaseUser[int]):
|
||||||
|
username: str
|
||||||
|
|
||||||
|
|
||||||
|
class UserCreate(schemas.BaseUserCreate):
|
||||||
|
username: str
|
||||||
|
|
||||||
|
|
||||||
|
class UserUpdate(schemas.BaseUserUpdate):
|
||||||
|
username: Optional[str] = None
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
from pydantic_settings import BaseSettings
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
...
|
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
|
||||||
|
LIFETIME: int
|
||||||
|
SECRET: str
|
||||||
|
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
|
|||||||
@@ -1,2 +1,34 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import Request
|
||||||
|
from fastapi_users import BaseUserManager, IntegerIDMixin
|
||||||
|
|
||||||
|
from src.models import UsersORM
|
||||||
|
from src.settings import settings
|
||||||
|
|
||||||
|
logger = logging.getLogger()
|
||||||
|
|
||||||
|
|
||||||
|
class UserManager(IntegerIDMixin, BaseUserManager[UsersORM, int]):
|
||||||
|
reset_password_token_secret = settings.SECRET
|
||||||
|
verification_token_secret = settings.SECRET
|
||||||
|
|
||||||
|
async def on_after_register(
|
||||||
|
self, user: UsersORM, request: Optional[Request] = None
|
||||||
|
):
|
||||||
|
logger.warning("User %r has registered.", user.id)
|
||||||
|
|
||||||
|
async def on_after_forgot_password(
|
||||||
|
self, user: UsersORM, token: str, request: Optional[Request] = None
|
||||||
|
):
|
||||||
|
logger.warning(
|
||||||
|
"User %r has forgot their password. Reset token: %r", user.id, token
|
||||||
|
)
|
||||||
|
|
||||||
|
async def on_after_request_verify(
|
||||||
|
self, user: UsersORM, token: str, request: Optional[Request] = None
|
||||||
|
):
|
||||||
|
logger.warning(
|
||||||
|
"Verification requested for user %r. Verification token: %r", user.id, token
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user