diff --git a/makefile b/makefile index 82ddef0..4be451d 100644 --- a/makefile +++ b/makefile @@ -1,18 +1,18 @@ -VENV=./venv/bin/python +VENV=source ./.venv/bin/activate; ALEMBIC=alembic -c ./server/database/alembic/alembic.ini .PHONY: test run migrate_head migrate_down migrate_history migrate_current migrate test: - pytest -c ./server/testing/pytest.ini ./server/testing/tests/ + $(VENV) pytest -c ./server/testing/pytest.ini ./server/testing/tests/ run: - $(VENV) run.py + $(VENV) python run.py migrate_head: - $(ALEMBIC) upgrade head + $(VENV) $(ALEMBIC) upgrade head migrate_down: - $(ALEMBIC) downgrade -1 + $(VENV) $(ALEMBIC) downgrade -1 migrate_history: - $(ALEMBIC) history + $(VENV) $(ALEMBIC) history migrate_current: - $(ALEMBIC) current + $(VENV) $(ALEMBIC) current migrate: - $(ALEMBIC) revision --autogenerate \ No newline at end of file + $(VENV) $(ALEMBIC) revision --autogenerate \ No newline at end of file diff --git a/server/backend/endpoints.py b/server/backend/endpoints.py index b52dafa..4551cd1 100644 --- a/server/backend/endpoints.py +++ b/server/backend/endpoints.py @@ -63,6 +63,7 @@ async def delete_user(email:str,current_user: str = Depends(JWT.current_user)): @api.put("/user_update/{email}", response_model=pydentic.UserOut) async def update_user(email:str, updated_row: pydentic.UserUpdate, current_user: str = Depends(JWT.current_user)): user = await db.get_user_by_email(email) + perm = user.permissions[0] if not user: raise HTTPException(status_code=401, detail="The user isn't found") changed = False @@ -78,8 +79,14 @@ async def update_user(email:str, updated_row: pydentic.UserUpdate, current_user: if updated_row.password is not None and updated_row.password != user.password: user.password = updated_row.password changed = True + if updated_row.can_edit is not None and updated_row.can_edit != perm.can_edit: + perm.can_edit = updated_row.can_edit + changed = True + if updated_row.can_delete is not None and updated_row.can_delete != perm.can_delete: + perm.can_delete = updated_row.can_delete + changed = True if changed: - await db.update_user(user) + user = await db.update_user(user_info=user, perm_info=perm) else: pass return user diff --git a/server/backend/pydentic.py b/server/backend/pydentic.py index dfc725d..a2cc9d3 100644 --- a/server/backend/pydentic.py +++ b/server/backend/pydentic.py @@ -32,6 +32,8 @@ class UserUpdate(BaseModel): description:Optional[str] = Field(None, description="description of the user") activated:Optional[bool] = Field(None, description="Has the user activated their account") password:Optional[constr(min_length=8)] = Field(None, description="Password with min 8 chars, letters and digits") + can_edit:Optional[bool] = Field(None, description="The user can edit something") + can_delete:Optional[bool] = Field(None, description="The user can delete something") @validator('password') def password_validator(cls, password): return check_password_complexity(cls, password) diff --git a/server/database/alembic/alembic/versions/17250f0912ea_.py b/server/database/alembic/alembic/versions/17250f0912ea_.py new file mode 100644 index 0000000..74b1fb2 --- /dev/null +++ b/server/database/alembic/alembic/versions/17250f0912ea_.py @@ -0,0 +1,32 @@ +"""empty message + +Revision ID: 17250f0912ea +Revises: 4182906fdea3 +Create Date: 2025-10-04 16:42:19.812353 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '17250f0912ea' +down_revision: Union[str, Sequence[str], None] = '4182906fdea3' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/server/database/alembic/alembic/versions/500009136941_.py b/server/database/alembic/alembic/versions/500009136941_.py new file mode 100644 index 0000000..3b232fd --- /dev/null +++ b/server/database/alembic/alembic/versions/500009136941_.py @@ -0,0 +1,39 @@ +"""empty message + +Revision ID: 500009136941 +Revises: 17250f0912ea +Create Date: 2025-10-04 17:24:48.096744 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '500009136941' +down_revision: Union[str, Sequence[str], None] = '17250f0912ea' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('permissions', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('can_edit', sa.Boolean(), nullable=True), + sa.Column('can_delete', sa.Boolean(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('permissions') + # ### end Alembic commands ### diff --git a/server/database/db.py b/server/database/db.py index 10cc7d9..7c73bef 100644 --- a/server/database/db.py +++ b/server/database/db.py @@ -3,9 +3,9 @@ import asyncio from datetime import datetime,timezone #from sqlalchemy import create_engine #Не async -from sqlalchemy.orm import DeclarativeBase, sessionmaker +from sqlalchemy.orm import DeclarativeBase, sessionmaker, relationship,selectinload from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine -from sqlalchemy import Column, Integer, String, Boolean, select,func, DateTime +from sqlalchemy import Column, Integer, String, Boolean, select,func, DateTime, ForeignKey, event from pathlib import Path db_folder = Path(__file__).parent / "DB" @@ -39,6 +39,20 @@ class User(Base): created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now()) last_login = Column(DateTime(timezone=True)) + # связь с Permission + permissions = relationship( + "Permission", back_populates="user", cascade="all, delete-orphan" #back populates = backref, только пишется в обоих классах с которыми связь + )#cascade - если ты удаляешь юзера, SQLAlchemy автоматом удалит все его пермишены.Если ты отцепил пермишен от юзера — он считается “сиротой” и тоже удаляется. + +class Permission(Base): + __tablename__ = "permissions" + + id = Column(Integer, primary_key=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + can_edit = Column(Boolean, default=False) + can_delete = Column(Boolean, default=False) + # обратная связь к User + user = relationship("User", back_populates="permissions") async def create_user(user_info): async with AsyncSessionLocal() as session: @@ -48,7 +62,7 @@ async def create_user(user_info): await session.refresh(new_user) async def get_user_by_email(email): async with AsyncSessionLocal() as session: - result = await session.execute(select(User).where(User.email==email)) + result = await session.execute(select(User).options(selectinload(User.permissions)).where(User.email==email)) user = result.scalar_one_or_none() return user async def get_all_rows(): @@ -56,16 +70,21 @@ async def get_all_rows(): result = await session.execute(select(User)) users = result.scalars().all() return users -async def update_user(user_info): +async def update_user(user_info, perm_info): async with AsyncSessionLocal() as session: - result = await session.execute(select(User).where(User.id==user_info.id)) + result = await session.execute(select(User).options(selectinload(User.permissions)).where(User.email==user_info.email)) user = result.scalar_one_or_none() if user: user.email = user_info.email user.description = user_info.description user.activated = user_info.activated user.password = hash_password(user_info.password) + + perm = user.permissions[0] # если у юзера одна запись - Это связь один-ко-многим: у одного User может быть список из нескольких Permission. + perm.can_edit = perm_info.can_edit + perm.can_delete = perm_info.can_delete await session.commit() + return user async def delete_user(email): async with AsyncSessionLocal() as session: result = await session.execute(select(User).where(User.email==email)) @@ -90,4 +109,9 @@ async def reset_user(user_info): user.password = hash_password(user_info.new_password) await session.commit() return user - return None \ No newline at end of file + return None +@event.listens_for(User, "after_insert") #listener не работает в async +def create_permission(mapper, connection, target): + connection.execute( + Permission.__table__.insert().values(user_id=target.id) + ) \ No newline at end of file