diff --git a/server/backend/endpoints.py b/server/backend/endpoints.py index 4551cd1..78eb653 100644 --- a/server/backend/endpoints.py +++ b/server/backend/endpoints.py @@ -5,7 +5,7 @@ from fastapi.security import OAuth2PasswordRequestForm from pydantic import EmailStr -from . import pydentic, JWT, password +from . import pydentic, JWT, password, permissions from server.database import db from datetime import datetime, timedelta @@ -32,13 +32,13 @@ async def protected(current_user: str = Depends(JWT.current_user)): return {"msg": f"Hello, {current_user}"} @api.get("/", response_model=list[pydentic.UserOut]) #список! -async def get_all_rows(current_user: str = Depends(JWT.current_user)): +async def get_all_rows(current_user: str = Depends(JWT.current_user), user=Depends(permissions.check_permission("is_admin"))): users = await db.get_all_rows() if not users: raise HTTPException(status_code=401, detail="The user isn't found") return users @api.get("/get_user_by_email/{email}", response_model=pydentic.UserOut) -async def get_user_by_email(email:str, current_user: str = Depends(JWT.current_user)): +async def get_user_by_email(email:str, current_user: str = Depends(JWT.current_user), user=Depends(permissions.check_permission("can_view"))): user = await db.get_user_by_email(email) if user: return user @@ -54,41 +54,45 @@ async def create_user(row:pydentic.CreateUser): user = await db.get_user_by_email(row.email) return user @api.delete("/user_delete/{email}", response_model=pydentic.UserOut) -async def delete_user(email:str,current_user: str = Depends(JWT.current_user)): +async def delete_user(email:str,current_user: str = Depends(JWT.current_user), user = Depends(permissions.check_permission("can_delete"))): user = await db.get_user_by_email(email) if not user: raise HTTPException(status_code=401, detail="The user isn't found") await db.delete_user(email) return 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] +async def update_user(email:str, updated_row: pydentic.UserUpdate, current_user: str = Depends(JWT.current_user), user = Depends(permissions.check_permission("can_edit"))): + user = await db.get_user_by_email(email) #user из бд, которого запросили + perm = user.permissions[0] #права по запрошенному user + current = await db.get_user_by_email(current_user) #user из бд по jwt + current_perms = current.permissions[0] #Права по jwt + + if not user: raise HTTPException(status_code=401, detail="The user isn't found") changed = False - if updated_row.email is not None and updated_row.email != user.email: - user.email = updated_row.email - changed = True - if updated_row.description is not None and updated_row.description != user.description: - user.description = updated_row.description - changed = True - if updated_row.activated is not None and updated_row.activated != user.activated: - user.activated = updated_row.activated - changed = True - 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 + #изменение только определенных колонок + updatable_fields = ["email", "description", "activated"] + for field in updatable_fields: + new_value = getattr(updated_row, field) + if new_value is not None and new_value != getattr(user, field): + setattr(user, field, new_value) + changed = True + # пароль + if updated_row.password: + if not verify_password(updated_row.password, user.password): + user.password = updated_row.password + changed = True + # права (только для админа) + if current_perms.is_admin: + perm_fields = ["can_edit", "can_delete", "can_view", "is_admin"] + for field in perm_fields: + new_value = getattr(updated_row, field) + if new_value is not None and new_value != getattr(perm, field): + setattr(perm, field, new_value) + changed = True if changed: user = await db.update_user(user_info=user, perm_info=perm) - else: - pass return user @api.post("/login") async def login_user(form_data: OAuth2PasswordRequestForm = Depends()): @@ -114,4 +118,9 @@ async def reset_user(row:pydentic.UserReset): password.send_password(new_row) user = await db.reset_user(new_row) return user - +@api.get("/admin/{email}") +async def admin_stuff( + email: str, + user = Depends(permissions.check_permission("can_delete")) +): + return {"msg": f"Добро пожаловать, {user.email}"} \ No newline at end of file diff --git a/server/backend/permissions.py b/server/backend/permissions.py new file mode 100644 index 0000000..4b810ae --- /dev/null +++ b/server/backend/permissions.py @@ -0,0 +1,30 @@ +from fastapi import Depends, HTTPException, status, Path, Request +from . import JWT +from server.database import db + +def check_permission(required: str): + async def wrapper( + request: Request, + current_user = Depends(JWT.current_user), + ): + requested_email = request.path_params.get("email") + user = await db.get_user_by_email(current_user) + perms = user.permissions[0] + # если админ → разрешено всегда + if perms.is_admin: + return user + # проверяем, что у пользователя есть нужное право + if not getattr(perms, required, False): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"You don't have a permission" + ) + # проверяем, что работает только со своим email + if current_user.lower() != requested_email.lower(): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"You can only do this with your own account" + ) + + return user + return wrapper diff --git a/server/backend/pydentic.py b/server/backend/pydentic.py index a2cc9d3..70dd936 100644 --- a/server/backend/pydentic.py +++ b/server/backend/pydentic.py @@ -34,6 +34,7 @@ class UserUpdate(BaseModel): 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") + can_view:Optional[bool]=Field(None, descriptiopn="The user can view something") @validator('password') def password_validator(cls, password): return check_password_complexity(cls, password) diff --git a/server/database/alembic/alembic/versions/1ee14fd147d8_.py b/server/database/alembic/alembic/versions/1ee14fd147d8_.py new file mode 100644 index 0000000..5f2b9f9 --- /dev/null +++ b/server/database/alembic/alembic/versions/1ee14fd147d8_.py @@ -0,0 +1,32 @@ +"""empty message + +Revision ID: 1ee14fd147d8 +Revises: f828091b6f7d +Create Date: 2025-10-05 12:59:47.055178 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '1ee14fd147d8' +down_revision: Union[str, Sequence[str], None] = 'f828091b6f7d' +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.add_column('permissions', sa.Column('is_admin', sa.Boolean(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('permissions', 'is_admin') + # ### end Alembic commands ### diff --git a/server/database/alembic/alembic/versions/f828091b6f7d_.py b/server/database/alembic/alembic/versions/f828091b6f7d_.py new file mode 100644 index 0000000..92f6bfa --- /dev/null +++ b/server/database/alembic/alembic/versions/f828091b6f7d_.py @@ -0,0 +1,32 @@ +"""empty message + +Revision ID: f828091b6f7d +Revises: 500009136941 +Create Date: 2025-10-05 12:11:47.000416 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'f828091b6f7d' +down_revision: Union[str, Sequence[str], None] = '500009136941' +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.add_column('permissions', sa.Column('can_view', sa.Boolean(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('permissions', 'can_view') + # ### end Alembic commands ### diff --git a/server/database/db.py b/server/database/db.py index 7c73bef..bca81ac 100644 --- a/server/database/db.py +++ b/server/database/db.py @@ -49,8 +49,10 @@ class Permission(Base): 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) + can_edit = Column(Boolean, default=True) + can_delete = Column(Boolean, default=True) + can_view = Column(Boolean, default=True) + is_admin = Column(Boolean, default=False) # обратная связь к User user = relationship("User", back_populates="permissions") @@ -79,7 +81,6 @@ async def update_user(user_info, perm_info): 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