Implement usernames and profile sections

This commit is contained in:
2025-10-20 00:18:34 +03:00
Unverified
parent f0be91c0e7
commit 818b7c1b46
21 changed files with 425 additions and 122 deletions
+3
View File
@@ -13,6 +13,7 @@ class User(Base):
id = Column(Integer, primary_key=True, index=True)
username = Column(String(50), unique=True, nullable=False, index=True)
display_name = Column(String(64), nullable=False)
password_hash = Column(String(200), nullable=False)
profile_picture = Column(String(255), nullable=True)
bio = Column(Text, nullable=True)
@@ -149,6 +150,7 @@ class LoginRequest(BaseModel):
class RegisterRequest(BaseModel):
username: str
display_name: str
password: str
confirm_password: str
@@ -178,6 +180,7 @@ class PushSubscriptionRequest(BaseModel):
class UserProfileResponse(BaseModel):
id: int
username: str
display_name: str
profile_picture: str | None
bio: str | None
online: bool
+11 -2
View File
@@ -6,7 +6,7 @@ from constants import OWNER_USERNAME
from dependencies import get_current_user, get_db
from models import LoginRequest, RegisterRequest, User, CryptoPublicKey, CryptoBackup
from utils import create_token, get_password_hash, verify_password
from validation import is_valid_password, is_valid_username
from validation import is_valid_password, is_valid_username, is_valid_display_name
router = APIRouter()
@@ -17,6 +17,7 @@ def convert_user(user: User) -> dict:
"last_seen": user.last_seen.isoformat(),
"online": user.online,
"username": user.username,
"display_name": user.display_name,
"profile_picture": user.profile_picture,
"bio": user.bio,
"admin": user.username == OWNER_USERNAME
@@ -58,6 +59,7 @@ def login(request: LoginRequest, db: Session = Depends(get_db)):
@router.post("/register")
def register(request: RegisterRequest, db: Session = Depends(get_db)):
username = request.username.strip()
display_name = request.display_name.strip()
password = request.password.strip()
confirm_password = request.confirm_password.strip()
@@ -75,7 +77,13 @@ def register(request: RegisterRequest, db: Session = Depends(get_db)):
if not is_valid_username(username):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Имя пользователя должно быть от 3 до 20 символов и не содержать пробелов"
detail="Имя пользователя должно быть от 3 до 20 символов и содержать только английские буквы, цифры, дефисы и подчеркивания"
)
if not is_valid_display_name(display_name):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Отображаемое имя должно быть от 1 до 64 символов и не может быть пустым"
)
if not is_valid_password(password):
@@ -107,6 +115,7 @@ def register(request: RegisterRequest, db: Session = Depends(get_db)):
hashed_password = get_password_hash(password)
new_user = User(
username=username,
display_name=display_name,
password_hash=hashed_password,
online=True,
last_seen=datetime.now()
+4 -3
View File
@@ -48,16 +48,17 @@ def convert_message(msg: Message) -> dict:
reactions_dict[emoji]["count"] += 1
reactions_dict[emoji]["users"].append({
"id": reaction.user_id,
"username": reaction.user.username
"username": reaction.user.display_name
})
return {
"id": msg.id,
"user_id": msg.author.id,
"content": msg.content,
"timestamp": msg.timestamp.isoformat(),
"is_read": msg.is_read,
"is_edited": msg.is_edited,
"username": msg.author.username,
"username": msg.author.display_name,
"profile_picture": msg.author.profile_picture,
"reply_to": convert_message(msg.reply_to) if msg.reply_to else None,
"reactions": list(reactions_dict.values()),
@@ -88,7 +89,7 @@ def convert_dm_envelope(envelope: DMEnvelope) -> dict:
reactions_dict[emoji]["count"] += 1
reactions_dict[emoji]["users"].append({
"id": reaction.user_id,
"username": reaction.user.username
"username": reaction.user.display_name
})
return {
+53 -10
View File
@@ -11,12 +11,14 @@ import io
from dependencies import get_db, get_current_user
from models import User, UpdateBioRequest, UserProfileResponse
from pydantic import BaseModel
from validation import is_valid_username, is_valid_display_name
router = APIRouter()
# Request models
class UpdateProfileRequest(BaseModel):
nickname: str | None = None
username: str | None = None
display_name: str | None = None
description: str | None = None
# Create uploads directory if it doesn't exist
@@ -102,6 +104,7 @@ async def get_user_profile(
return {
"id": current_user.id,
"username": current_user.username,
"display_name": current_user.display_name,
"profile_picture": current_user.profile_picture,
"bio": current_user.bio,
"online": current_user.online,
@@ -121,19 +124,32 @@ async def update_user_profile(
updated = False
# Update username if provided
if request.nickname is not None:
nickname = request.nickname.strip()
if len(nickname) < 3:
raise HTTPException(status_code=400, detail="Username must be at least 3 characters long")
if len(nickname) > 50:
raise HTTPException(status_code=400, detail="Username must be 50 characters or less")
if request.username is not None:
username = request.username.strip()
if not is_valid_username(username):
raise HTTPException(
status_code=400,
detail="Имя пользователя должно быть от 3 до 20 символов и содержать только английские буквы, цифры, дефисы и подчеркивания"
)
# Check if username is already taken by another user
existing_user = db.query(User).filter(User.username == nickname, User.id != current_user.id).first()
existing_user = db.query(User).filter(User.username == username, User.id != current_user.id).first()
if existing_user:
raise HTTPException(status_code=400, detail="Username already taken")
raise HTTPException(status_code=400, detail="Это имя пользователя уже занято")
current_user.username = nickname
current_user.username = username
updated = True
# Update display name if provided
if request.display_name is not None:
display_name = request.display_name.strip()
if not is_valid_display_name(display_name):
raise HTTPException(
status_code=400,
detail="Отображаемое имя должно быть от 1 до 64 символов и не может быть пустым"
)
current_user.display_name = display_name
updated = True
# Update bio if provided
@@ -150,12 +166,14 @@ async def update_user_profile(
return {
"message": "Profile updated successfully",
"username": current_user.username,
"display_name": current_user.display_name,
"bio": current_user.bio
}
else:
return {
"message": "No changes made",
"username": current_user.username,
"display_name": current_user.display_name,
"bio": current_user.bio
}
@@ -197,6 +215,31 @@ async def get_user_by_username(
return UserProfileResponse(
id=user.id,
username=user.username,
display_name=user.display_name,
profile_picture=user.profile_picture,
bio=user.bio,
online=user.online,
last_seen=user.last_seen,
created_at=user.created_at
)
@router.get("/user/id/{user_id}")
async def get_user_by_id(
user_id: int,
db: Session = Depends(get_db)
):
"""
Get user profile by user ID
"""
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
return UserProfileResponse(
id=user.id,
username=user.username,
display_name=user.display_name,
profile_picture=user.profile_picture,
bio=user.bio,
online=user.online,
+11 -1
View File
@@ -3,7 +3,17 @@ import re
def is_valid_username(username: str) -> bool:
if len(username) < 3 or len(username) > 20:
return False
if re.search(r'[\s\u180E\u200B-\u200D\u2060\uFEFF]', username):
# Only allow English letters, numbers, dashes and underscores
if not re.match(r'^[a-zA-Z0-9_-]+$', username):
return False
return True
def is_valid_display_name(display_name: str) -> bool:
if len(display_name) < 1 or len(display_name) > 64:
return False
# Check if not blank (only whitespace)
if not display_name.strip():
return False
return True