2026.06.04 frontend építés közben
This commit is contained in:
@@ -11,6 +11,7 @@ from app.db.session import get_db
|
||||
from app.core.security import decode_token, DEFAULT_RANK_MAP
|
||||
from app.models.identity import User, UserRole # JAVÍTVA: Új Identity modell használata
|
||||
from app.core.config import settings
|
||||
from app.core.translation_helper import t # Translation helper
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -46,8 +47,8 @@ async def get_current_token_payload(
|
||||
payload = decode_token(token)
|
||||
if not payload or payload.get("type") != "access":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Érvénytelen vagy lejárt munkamenet."
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=t("AUTH.INVALID_OR_EXPIRED_SESSION")
|
||||
)
|
||||
return payload
|
||||
|
||||
@@ -62,7 +63,7 @@ async def get_current_user(
|
||||
if not user_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Token azonosítási hiba."
|
||||
detail=t("AUTH.TOKEN_IDENTIFICATION_ERROR")
|
||||
)
|
||||
|
||||
# JAVÍTVA: Modern SQLAlchemy 2.0 aszinkron lekérdezés with eager loading
|
||||
@@ -74,7 +75,7 @@ async def get_current_user(
|
||||
if not user or user.is_deleted:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="A felhasználó nem található."
|
||||
detail=t("AUTH.USER_NOT_FOUND")
|
||||
)
|
||||
return user
|
||||
|
||||
@@ -86,8 +87,8 @@ async def get_current_active_user(
|
||||
"""
|
||||
if not current_user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="A művelethez aktív profil és KYC azonosítás szükséges."
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=t("AUTH.ACTIVE_PROFILE_KYC_REQUIRED")
|
||||
)
|
||||
return current_user
|
||||
|
||||
@@ -113,8 +114,8 @@ async def check_resource_access(
|
||||
return True
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Nincs jogosultsága ehhez az erőforráshoz."
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=t("AUTH.NO_PERMISSION_FOR_RESOURCE")
|
||||
)
|
||||
|
||||
def check_min_rank(role_key: str):
|
||||
@@ -160,6 +161,6 @@ async def get_current_admin(
|
||||
if current_user.role not in allowed_roles:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Nincs megfelelő jogosultságod (Admin/Moderátor)!"
|
||||
detail=t("AUTH.INSUFFICIENT_ADMIN_PERMISSIONS")
|
||||
)
|
||||
return current_user
|
||||
@@ -220,9 +220,7 @@ async def create_or_claim_vehicle(
|
||||
db=db,
|
||||
user_id=current_user.id,
|
||||
org_id=org_id,
|
||||
vin=payload.vin,
|
||||
license_plate=payload.license_plate,
|
||||
catalog_id=payload.catalog_id
|
||||
asset_data=payload
|
||||
)
|
||||
return asset
|
||||
except ValueError as e:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/auth.py
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request, Form, Response
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
@@ -7,10 +7,12 @@ from app.db.session import get_db
|
||||
from app.services.auth_service import AuthService
|
||||
from app.core.security import create_tokens, DEFAULT_RANK_MAP
|
||||
from app.core.config import settings
|
||||
from app.services.config_service import config
|
||||
from app.schemas.auth import UserLiteRegister, Token, UserKYCComplete
|
||||
from app.api.deps import get_current_user
|
||||
from app.models.identity import User # JAVÍTVA: Új központi modell
|
||||
from pydantic import BaseModel, Field
|
||||
from app.core.translation_helper import t # Translation helper
|
||||
from pydantic import BaseModel, Field, EmailStr
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -22,16 +24,27 @@ async def register(user_in: UserLiteRegister, db: AsyncSession = Depends(get_db)
|
||||
user = await AuthService.register_lite(db, user_in)
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Regisztráció sikeres. Aktivációs e-mail elküldve.",
|
||||
"message": t("AUTH.REGISTRATION_SUCCESS"),
|
||||
"user_id": user.id,
|
||||
"email": user.email
|
||||
}
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login(db: AsyncSession = Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
async def login(
|
||||
response: Response,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
form_data: OAuth2PasswordRequestForm = Depends(),
|
||||
remember_me: bool = Form(False)
|
||||
):
|
||||
"""
|
||||
Bejelentkezés remember_me opcióval.
|
||||
|
||||
A remember_me paramétert explicit Form mezőként kell elküldeni, mivel az
|
||||
OAuth2PasswordRequestForm nem támogatja alapból.
|
||||
"""
|
||||
user = await AuthService.authenticate(db, form_data.username, form_data.password)
|
||||
if not user:
|
||||
raise HTTPException(status_code=401, detail="Hibás adatok.")
|
||||
raise HTTPException(status_code=401, detail=t("AUTH.INVALID_CREDENTIALS"))
|
||||
|
||||
ranks = await settings.get_db_setting(db, "rbac_rank_matrix", default=DEFAULT_RANK_MAP)
|
||||
role_name = user.role.value if hasattr(user.role, 'value') else str(user.role)
|
||||
@@ -45,7 +58,26 @@ async def login(db: AsyncSession = Depends(get_db), form_data: OAuth2PasswordReq
|
||||
"scope_id": str(user.scope_id) if user.scope_id else str(user.id)
|
||||
}
|
||||
|
||||
access, refresh = create_tokens(data=token_data)
|
||||
access, refresh = create_tokens(data=token_data, remember_me=remember_me)
|
||||
|
||||
# Kinyerjük a beállításokból, hogy meddig él a refresh token (itt is, vagy a create_tokens is ezt csinálja)
|
||||
# A SSoT szerint auth_remember_me_days = 30
|
||||
if remember_me:
|
||||
max_age_days = await config.get_setting(db, "auth_remember_me_days", default=30)
|
||||
else:
|
||||
max_age_days = await config.get_setting(db, "auth_refresh_default_days", default=1)
|
||||
|
||||
max_age_sec = int(max_age_days) * 24 * 60 * 60
|
||||
|
||||
response.set_cookie(
|
||||
key="refresh_token",
|
||||
value=refresh,
|
||||
httponly=True,
|
||||
secure=True, # Use Secure
|
||||
samesite="lax",
|
||||
max_age=max_age_sec
|
||||
)
|
||||
|
||||
return {"access_token": access, "refresh_token": refresh, "token_type": "bearer", "is_active": user.is_active}
|
||||
|
||||
class VerifyEmailRequest(BaseModel):
|
||||
@@ -58,8 +90,42 @@ async def verify_email(request: VerifyEmailRequest, db: AsyncSession = Depends(g
|
||||
"""
|
||||
success = await AuthService.verify_email(db, request.token)
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail="Érvénytelen vagy lejárt token.")
|
||||
return {"status": "success", "message": "Email sikeresen megerősítve."}
|
||||
raise HTTPException(status_code=400, detail=t("AUTH.INVALID_OR_EXPIRED_TOKEN"))
|
||||
return {"status": "success", "message": t("AUTH.EMAIL_VERIFICATION_SUCCESS")}
|
||||
|
||||
class ForgotPasswordRequest(BaseModel):
|
||||
email: EmailStr = Field(..., description="Email cím a jelszó visszaállításhoz")
|
||||
|
||||
@router.post("/forgot-password")
|
||||
async def forgot_password(request: ForgotPasswordRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""
|
||||
Elfelejtett jelszó folyamat indítása.
|
||||
Mindig sikeres választ ad, hogy megelőzzük az email enumerációt.
|
||||
"""
|
||||
result = await AuthService.initiate_password_reset(db, request.email)
|
||||
# Mindig ugyanazt a választ adjuk, függetlenül attól, hogy létezik-e a felhasználó
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Ha ez az email cím regisztrálva van, akkor elküldtünk egy jelszó-visszaállítási linket."
|
||||
}
|
||||
|
||||
class ResetPasswordRequest(BaseModel):
|
||||
email: EmailStr = Field(..., description="Email cím")
|
||||
token: str = Field(..., description="Jelszó visszaállítási token")
|
||||
new_password: str = Field(..., description="Új jelszó")
|
||||
|
||||
@router.post("/reset-password")
|
||||
async def reset_password(request: ResetPasswordRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""
|
||||
Jelszó visszaállítása token alapján.
|
||||
"""
|
||||
success = await AuthService.reset_password(db, request.email, request.token, request.new_password)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Érvénytelen token, lejárt token vagy nem létező felhasználó."
|
||||
)
|
||||
return {"status": "success", "message": "Jelszó sikeresen megváltoztatva."}
|
||||
|
||||
@router.post("/complete-kyc")
|
||||
async def complete_kyc(kyc_in: UserKYCComplete, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
|
||||
@@ -3,18 +3,19 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.db.session import get_db
|
||||
from app.services.asset_service import AssetService
|
||||
from app.api import deps
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Secured endpoint: Closed premium ecosystem
|
||||
@router.get("/makes", response_model=List[str])
|
||||
async def list_makes(
|
||||
vehicle_class: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user = Depends(deps.get_current_user)
|
||||
):
|
||||
"""1. Szint: Márkák listázása."""
|
||||
return await AssetService.get_makes(db)
|
||||
"""1. Szint: Márkák listázása, opcionálisan vehicle_class szerint szűrve."""
|
||||
return await AssetService.get_makes(db, vehicle_class)
|
||||
|
||||
# Secured endpoint: Closed premium ecosystem
|
||||
@router.get("/models", response_model=List[str])
|
||||
|
||||
@@ -13,7 +13,7 @@ from sqlalchemy import select
|
||||
from app.db.session import get_db
|
||||
from app.api.deps import get_current_user
|
||||
from app.schemas.organization import CorpOnboardIn, CorpOnboardResponse
|
||||
from app.models.marketplace.organization import Organization, OrgType, OrganizationMember
|
||||
from app.models.marketplace.organization import Organization, OrgType, OrganizationMember, Branch
|
||||
from app.models.identity import User # JAVÍTVA: Központi Identity modell
|
||||
from app.core.config import settings
|
||||
|
||||
@@ -82,9 +82,24 @@ async def onboard_organization(
|
||||
)
|
||||
|
||||
db.add(new_org)
|
||||
await db.flush()
|
||||
await db.flush()
|
||||
|
||||
# 5. TULAJDONOS RÖGZÍTÉSE
|
||||
# 5. ALAPÉRTELMEZETT KÖZPONTI TELEPHELY LÉTREHOZÁSA
|
||||
main_branch = Branch(
|
||||
organization_id=new_org.id,
|
||||
name="Központi Telephely",
|
||||
is_main=True,
|
||||
postal_code=org_in.address_zip,
|
||||
city=org_in.address_city,
|
||||
street_name=org_in.address_street_name,
|
||||
street_type=org_in.address_street_type,
|
||||
house_number=org_in.address_house_number,
|
||||
hrsz=org_in.address_hrsz,
|
||||
status="active"
|
||||
)
|
||||
db.add(main_branch)
|
||||
|
||||
# 6. TULAJDONOS RÖGZÍTÉSE
|
||||
owner_member = OrganizationMember(
|
||||
organization_id=new_org.id,
|
||||
user_id=current_user.id,
|
||||
@@ -92,7 +107,7 @@ async def onboard_organization(
|
||||
)
|
||||
db.add(owner_member)
|
||||
|
||||
# 6. NAS Mappa létrehozása
|
||||
# 7. NAS Mappa létrehozása
|
||||
try:
|
||||
base_path = getattr(settings, "NAS_STORAGE_PATH", "/mnt/nas/app_data")
|
||||
org_path = os.path.join(base_path, "organizations", str(new_org.id))
|
||||
@@ -135,4 +150,128 @@ async def get_my_organizations(
|
||||
"subscription_plan": o.subscription_plan
|
||||
}
|
||||
for o in orgs
|
||||
]
|
||||
]
|
||||
# --- B2B MEGHÍVÓ LOGIKA ---
|
||||
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from app.models.identity import VerificationToken
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
|
||||
class OrgInvitationIn(BaseModel):
|
||||
email: EmailStr
|
||||
role: str = "DRIVER"
|
||||
|
||||
class OrgInvitationResponse(BaseModel):
|
||||
status: str
|
||||
message: str
|
||||
|
||||
@router.post("/{org_id}/invitations", response_model=OrgInvitationResponse, status_code=status.HTTP_200_OK)
|
||||
async def invite_to_organization(
|
||||
org_id: int,
|
||||
invite_in: OrgInvitationIn,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
B2B Meghívó küldése egy szervezetbe.
|
||||
Ha a felhasználó már létezik, egy pending tagot hozunk létre.
|
||||
Ha nem létezik, token készül az email címre.
|
||||
"""
|
||||
# 1. Jogosultság ellenőrzése
|
||||
stmt_member = select(OrganizationMember).where(
|
||||
(OrganizationMember.organization_id == org_id) &
|
||||
(OrganizationMember.user_id == current_user.id) &
|
||||
(OrganizationMember.role.in_(["OWNER", "ADMIN"]))
|
||||
)
|
||||
member = (await db.execute(stmt_member)).scalar_one_or_none()
|
||||
if not member:
|
||||
raise HTTPException(status_code=403, detail="Nincs jogosultságod meghívót küldeni (csak OWNER/ADMIN).")
|
||||
|
||||
# 2. Célpont keresése
|
||||
stmt_target = select(User).where(User.email == invite_in.email)
|
||||
target_user = (await db.execute(stmt_target)).scalar_one_or_none()
|
||||
|
||||
if target_user:
|
||||
# Létező felhasználó, van-e már tagsága?
|
||||
stmt_exist = select(OrganizationMember).where(
|
||||
(OrganizationMember.organization_id == org_id) &
|
||||
(OrganizationMember.user_id == target_user.id)
|
||||
)
|
||||
if (await db.execute(stmt_exist)).scalar_one_or_none():
|
||||
raise HTTPException(status_code=400, detail="A felhasználó már tagja a szervezetnek.")
|
||||
|
||||
new_member = OrganizationMember(
|
||||
organization_id=org_id,
|
||||
user_id=target_user.id,
|
||||
role=invite_in.role.upper(),
|
||||
status="pending" # Válaszolni kell a meghívóra
|
||||
)
|
||||
db.add(new_member)
|
||||
await db.commit()
|
||||
logger.info(f"Értesítő email küldve a létező felhasználónak: {invite_in.email}")
|
||||
return {"status": "success", "message": "Meghívó elküldve a meglévő felhasználónak."}
|
||||
else:
|
||||
# Új felhasználó -> Token
|
||||
token_val = uuid.uuid4()
|
||||
new_token = VerificationToken(
|
||||
token=token_val,
|
||||
user_id=None, # Mivel még nincs User
|
||||
token_type="org_invite",
|
||||
expires_at=datetime.now(timezone.utc) + timedelta(days=7),
|
||||
extra_data={"org_id": org_id, "role": invite_in.role.upper(), "email": invite_in.email}
|
||||
)
|
||||
db.add(new_token)
|
||||
await db.commit()
|
||||
logger.info(f"Meghívó email küldve az új felhasználónak: {invite_in.email}, Token: {token_val}")
|
||||
return {"status": "success", "message": "Meghívó email elküldve (új felhasználó)."}
|
||||
|
||||
@router.post("/invitations/{token}/accept", response_model=OrgInvitationResponse, status_code=status.HTTP_200_OK)
|
||||
async def accept_invitation(
|
||||
token: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Meghívó elfogadása token alapján. (Új felhasználó számára, miután regisztrált)
|
||||
"""
|
||||
stmt = select(VerificationToken).where(
|
||||
(VerificationToken.token == token) &
|
||||
(VerificationToken.token_type == "org_invite") &
|
||||
(VerificationToken.is_used == False)
|
||||
)
|
||||
token_rec = (await db.execute(stmt)).scalar_one_or_none()
|
||||
|
||||
if not token_rec or token_rec.expires_at < datetime.now(timezone.utc):
|
||||
raise HTTPException(status_code=400, detail="Érvénytelen vagy lejárt meghívó.")
|
||||
|
||||
extra = token_rec.extra_data
|
||||
if not extra or extra.get("email") != current_user.email:
|
||||
raise HTTPException(status_code=403, detail="A meghívó nem a te e-mail címedre szól.")
|
||||
|
||||
org_id = extra.get("org_id")
|
||||
role = extra.get("role")
|
||||
|
||||
# Van-e már tagsága?
|
||||
stmt_exist = select(OrganizationMember).where(
|
||||
(OrganizationMember.organization_id == org_id) &
|
||||
(OrganizationMember.user_id == current_user.id)
|
||||
)
|
||||
exist_member = (await db.execute(stmt_exist)).scalar_one_or_none()
|
||||
if exist_member:
|
||||
exist_member.status = "active"
|
||||
exist_member.role = role
|
||||
else:
|
||||
new_member = OrganizationMember(
|
||||
organization_id=org_id,
|
||||
user_id=current_user.id,
|
||||
role=role,
|
||||
status="active"
|
||||
)
|
||||
db.add(new_member)
|
||||
|
||||
token_rec.is_used = True
|
||||
await db.commit()
|
||||
|
||||
return {"status": "success", "message": "Meghívó sikeresen elfogadva."}
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from typing import Dict, Any
|
||||
from sqlalchemy import select, or_
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from app.api.deps import get_db, get_current_user
|
||||
from app.schemas.user import UserResponse, UserUpdate, ActiveOrganizationUpdate, UserWithTokenResponse
|
||||
@@ -10,10 +12,27 @@ from app.models.identity import User
|
||||
from app.services.trust_engine import TrustEngine
|
||||
from app.core.security import create_tokens, DEFAULT_RANK_MAP
|
||||
from app.core.config import settings
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter()
|
||||
trust_engine = TrustEngine()
|
||||
|
||||
# MLM Hálózat válasz sémák
|
||||
class NetworkMemberL1(BaseModel):
|
||||
email: str
|
||||
referral_code: str
|
||||
folder_slug: Optional[str]
|
||||
joined_at: datetime
|
||||
|
||||
class NetworkMemberL2L3(BaseModel):
|
||||
referral_code: str
|
||||
joined_at: datetime
|
||||
|
||||
class NetworkResponse(BaseModel):
|
||||
level1: List[NetworkMemberL1]
|
||||
level2: List[NetworkMemberL2L3]
|
||||
level3: List[NetworkMemberL2L3]
|
||||
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
async def read_users_me(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
@@ -222,3 +241,67 @@ async def update_active_organization(
|
||||
access_token=access_token,
|
||||
token_type="bearer"
|
||||
)
|
||||
|
||||
@router.get("/me/network", response_model=NetworkResponse)
|
||||
async def get_my_network(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Visszaadja a felhasználó MLM hálózatát 3 szinten:
|
||||
- L1: Közvetlen meghívottak (email + kód)
|
||||
- L2: L1 meghívottai (csak kód)
|
||||
- L3: L2 meghívottai (csak kód)
|
||||
"""
|
||||
# L1: Közvetlen meghívottak
|
||||
l1_stmt = select(User).where(User.referred_by_id == current_user.id)
|
||||
l1_result = await db.execute(l1_stmt)
|
||||
l1_users = l1_result.scalars().all()
|
||||
|
||||
level1 = [
|
||||
NetworkMemberL1(
|
||||
email=u.email,
|
||||
referral_code=u.referral_code or "",
|
||||
folder_slug=u.folder_slug,
|
||||
joined_at=u.created_at
|
||||
)
|
||||
for u in l1_users
|
||||
]
|
||||
|
||||
# L2: L1 userek által meghívottak
|
||||
level2 = []
|
||||
if l1_users:
|
||||
l1_ids = [u.id for u in l1_users]
|
||||
l2_stmt = select(User).where(User.referred_by_id.in_(l1_ids))
|
||||
l2_result = await db.execute(l2_stmt)
|
||||
l2_users = l2_result.scalars().all()
|
||||
|
||||
level2 = [
|
||||
NetworkMemberL2L3(
|
||||
referral_code=u.referral_code or "",
|
||||
joined_at=u.created_at
|
||||
)
|
||||
for u in l2_users
|
||||
]
|
||||
|
||||
# L3: L2 userek által meghívottak
|
||||
level3 = []
|
||||
if l2_users:
|
||||
l2_ids = [u.id for u in l2_users]
|
||||
l3_stmt = select(User).where(User.referred_by_id.in_(l2_ids))
|
||||
l3_result = await db.execute(l3_stmt)
|
||||
l3_users = l3_result.scalars().all()
|
||||
|
||||
level3 = [
|
||||
NetworkMemberL2L3(
|
||||
referral_code=u.referral_code or "",
|
||||
joined_at=u.created_at
|
||||
)
|
||||
for u in l3_users
|
||||
]
|
||||
|
||||
return NetworkResponse(
|
||||
level1=level1,
|
||||
level2=level2,
|
||||
level3=level3
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/vehicles.py
|
||||
"""
|
||||
Jármű értékelési végpontok a Social 1 modulhoz.
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user