diff --git a/backend/constants.py b/backend/constants.py index e3e8bb7..271642c 100644 --- a/backend/constants.py +++ b/backend/constants.py @@ -1,4 +1,5 @@ DATABASE_URL = "sqlite:///./data/database.db" JWT_SECRET_KEY = "fromchat-jwt-secret" JWT_ALGORITHM = "HS256" -ACCESS_TOKEN_EXPIRE_HOURS = 24 \ No newline at end of file +ACCESS_TOKEN_EXPIRE_HOURS = 24 +OWNER_USERNAME = "denis0001-dev" \ No newline at end of file diff --git a/backend/routes/account.py b/backend/routes/account.py index 32a822a..eaa8ece 100644 --- a/backend/routes/account.py +++ b/backend/routes/account.py @@ -3,6 +3,7 @@ from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from routes.messaging import convert_message +from constants import OWNER_USERNAME from dependencies import get_current_user, get_db from models import LoginRequest, RegisterRequest, User from utils import create_token, get_password_hash, verify_password @@ -16,14 +17,16 @@ def convert_user(user: User) -> dict: "created_at": user.created_at.isoformat(), "last_seen": user.last_seen.isoformat(), "online": user.online, - "username": user.username + "username": user.username, + "admin": user.username == OWNER_USERNAME } @router.get("/check_auth") def check_auth(current_user: User = Depends(get_current_user)): return { "authenticated": True, - "username": current_user.username + "username": current_user.username, + "admin": current_user.username == OWNER_USERNAME } @@ -57,6 +60,16 @@ def register(request: RegisterRequest, db: Session = Depends(get_db)): password = request.password.strip() confirm_password = request.confirm_password.strip() + # Determine if owner already exists + owner_exists = db.query(User).filter(User.username == OWNER_USERNAME).first() is not None + + # If owner not yet registered, only allow the owner to register + if not owner_exists and username != OWNER_USERNAME: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Регистрация временно закрыта до регистрации владельца" + ) + # Validate input if not is_valid_username(username): raise HTTPException( @@ -76,6 +89,13 @@ def register(request: RegisterRequest, db: Session = Depends(get_db)): detail="Пароли не совпадают" ) + # After owner exists, disallow registering the reserved owner username via public registration + if owner_exists and username == OWNER_USERNAME: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Это имя пользователя зарезервировано" + ) + existing_user = db.query(User).filter(User.username == username).first() if existing_user: raise HTTPException( @@ -100,6 +120,34 @@ def register(request: RegisterRequest, db: Session = Depends(get_db)): "message": "Регистрация прошла успешно Теперь вы можете войти." } + +@router.delete("/admin/user/{user_id}") +def delete_user_as_owner( + user_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + # Only owner can delete users + if current_user.username != OWNER_USERNAME: + raise HTTPException(status_code=403, detail="Only owner can perform this action") + + user = db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException(status_code=404, detail="User not found") + + # Prevent deleting the owner account via API + if user.username == OWNER_USERNAME: + raise HTTPException(status_code=400, detail="Cannot delete owner account") + + # Manually delete user's messages to satisfy FK constraints + from models import Message # local import to avoid circular + db.query(Message).filter(Message.user_id == user.id).delete() + + db.delete(user) + db.commit() + + return {"status": "success", "deleted_user_id": user_id} + @router.get("/logout") def logout( current_user: User = Depends(get_current_user), diff --git a/backend/routes/messaging.py b/backend/routes/messaging.py index 0da9a5a..36f60d8 100644 --- a/backend/routes/messaging.py +++ b/backend/routes/messaging.py @@ -4,6 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketDisco from fastapi.security import HTTPAuthorizationCredentials from sqlalchemy.orm import Session from dependencies import get_current_user, get_db +from constants import OWNER_USERNAME from models import Message, SendMessageRequest, EditMessageRequest, ReplyMessageRequest, User router = APIRouter() @@ -105,7 +106,8 @@ async def delete_message( if not message: raise HTTPException(status_code=404, detail="Message not found") - if message.user_id != current_user.id: + # Allow owner to delete any message + if current_user.username != OWNER_USERNAME and message.user_id != current_user.id: raise HTTPException(status_code=403, detail="You can only delete your own messages") db.delete(message) diff --git a/frontend/src/message-context-menu.ts b/frontend/src/message-context-menu.ts index 8375c47..60c0669 100644 --- a/frontend/src/message-context-menu.ts +++ b/frontend/src/message-context-menu.ts @@ -78,17 +78,15 @@ function bindEvents(): void { export function show(message: Message, x: number, y: number): void { currentMessage = message; - // Only show edit and delete for own messages + // Show delete for own messages and for owner on any message const editItem = menu.querySelector('[data-action="edit"]') as HTMLElement; const deleteItem = menu.querySelector('[data-action="delete"]') as HTMLElement; - if (message.username === currentUser?.username) { - editItem.style.display = 'flex'; - deleteItem.style.display = 'flex'; - } else { - editItem.style.display = 'none'; - deleteItem.style.display = 'none'; - } + const isAuthor = message.username === currentUser?.username; + const isOwner = !!currentUser?.admin; + + editItem.style.display = isAuthor ? 'flex' : 'none'; + deleteItem.style.display = (isAuthor || isOwner) ? 'flex' : 'none'; // Position the menu properly menu.style.display = 'block'; diff --git a/frontend/src/types.ts b/frontend/src/types.ts index a4b8077..4447a1d 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -81,6 +81,7 @@ export interface User { last_seen: string; online: boolean; username: string; + admin?: boolean; bio?: string; }