modify: auth 插件
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
from chongming_worker import auto_bind_models
|
||||
from chongming_worker.runtime import create_workers as _create_workers
|
||||
from chongming_password import hash_password
|
||||
|
||||
from .router import *
|
||||
|
||||
|
||||
auto_bind_models("auth")
|
||||
|
||||
|
||||
async def create_workers(session, config, count=1, worker_manager=None):
|
||||
from chongming_database import Database
|
||||
from .global_var import set_global_var
|
||||
from .models.auth import Users
|
||||
from sqlalchemy import select
|
||||
|
||||
db_path = Path(config.get("data_dir", "./data")) / "auth.db"
|
||||
db = Database(db_path)
|
||||
await db.initialize()
|
||||
|
||||
# 初始化管理员
|
||||
async with db.get_session() as sess:
|
||||
select_stmt = select(Users) \
|
||||
.where(Users.username == "admin") # type: ignore
|
||||
result = await sess.execute(select_stmt)
|
||||
user: Users | None = result.scalar_one_or_none()
|
||||
|
||||
if user is None:
|
||||
user = Users(
|
||||
username="admin",
|
||||
hashed_password=hash_password("admin"),
|
||||
email="admin@example.com",
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
is_superuser=True
|
||||
)
|
||||
sess.add(user)
|
||||
await sess.commit()
|
||||
|
||||
set_global_var("db", db)
|
||||
|
||||
return _create_workers(session, config, count, worker_manager)
|
||||
|
||||
async def cleanup(worker_item: str):
|
||||
from .global_var import db
|
||||
|
||||
if db is not None:
|
||||
await db.dispose()
|
||||
print(f"{worker_item} 清理完毕")
|
||||
@@ -0,0 +1,29 @@
|
||||
from typing import Any
|
||||
|
||||
from chongming_config import get_config
|
||||
from chongming_jwt import JWT
|
||||
from chongming_database import Database
|
||||
from .models import *
|
||||
|
||||
def set_global_var(name: str, value: Any) -> None:
|
||||
"""
|
||||
设置当前模块的全局变量。
|
||||
|
||||
Args:
|
||||
name: 变量名(字符串)
|
||||
value: 任意值
|
||||
"""
|
||||
globals()[name] = value
|
||||
|
||||
config = get_config()
|
||||
|
||||
db: Database | None = None
|
||||
|
||||
secret = config["security"]["secret_key"]
|
||||
algorithm = config["security"]["algorithm"]
|
||||
jwt = JWT(secret, algorithm)
|
||||
|
||||
__all__ = [
|
||||
"db",
|
||||
"jwt"
|
||||
]
|
||||
@@ -0,0 +1,33 @@
|
||||
"""
|
||||
模型导出 — auth
|
||||
自动生成,请勿手动修改
|
||||
"""
|
||||
|
||||
from .pydantic import AuthResponse
|
||||
from .pydantic import ChangePasswordPostRequest
|
||||
from .pydantic import ChangePasswordPostResponse
|
||||
from .pydantic import LoginPostRequest
|
||||
from .pydantic import LoginPostResponse
|
||||
from .pydantic import MeGetRequest
|
||||
from .pydantic import MeGetResponse
|
||||
from .pydantic import RegisterPostRequest
|
||||
from .pydantic import RegisterPostResponse
|
||||
from .pydantic import UpdateProfilePostRequest
|
||||
from .pydantic import UpdateProfilePostResponse
|
||||
|
||||
from .auth import Users
|
||||
|
||||
__all__ = [
|
||||
"AuthResponse",
|
||||
"ChangePasswordPostRequest",
|
||||
"ChangePasswordPostResponse",
|
||||
"LoginPostRequest",
|
||||
"LoginPostResponse",
|
||||
"MeGetRequest",
|
||||
"MeGetResponse",
|
||||
"RegisterPostRequest",
|
||||
"RegisterPostResponse",
|
||||
"UpdateProfilePostRequest",
|
||||
"UpdateProfilePostResponse",
|
||||
"Users",
|
||||
]
|
||||
@@ -0,0 +1,102 @@
|
||||
import os
|
||||
import time
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
|
||||
from sqlmodel import Field, SQLModel, Column
|
||||
from sqlalchemy import func, DateTime
|
||||
|
||||
|
||||
def primary_key_create():
|
||||
return f"{uuid4()}_{os.getpid()}_{int(time.time())}"
|
||||
|
||||
|
||||
class Users(SQLModel, table=True):
|
||||
__tablename__ = "users" # type: ignore
|
||||
|
||||
# ---------- 主键 ----------
|
||||
id: str = Field(
|
||||
default_factory=primary_key_create,
|
||||
primary_key=True,
|
||||
index=True,
|
||||
description="用户唯一ID(UUID格式)"
|
||||
)
|
||||
|
||||
# ---------- 登录凭证(唯一索引) ----------
|
||||
username: str = Field(
|
||||
max_length=50,
|
||||
unique=True,
|
||||
index=True,
|
||||
nullable=False,
|
||||
description="登录用户名"
|
||||
)
|
||||
email: str = Field(
|
||||
max_length=255,
|
||||
unique=True,
|
||||
index=True,
|
||||
nullable=False,
|
||||
description="电子邮箱"
|
||||
)
|
||||
hashed_password: str = Field(
|
||||
max_length=255,
|
||||
nullable=False,
|
||||
description="加密后的密码(不存明文)"
|
||||
)
|
||||
|
||||
# ---------- 个人信息(可选) ----------
|
||||
full_name: Optional[str] = Field(
|
||||
default=None,
|
||||
max_length=100,
|
||||
description="用户全名"
|
||||
)
|
||||
avatar_url: Optional[str] = Field(
|
||||
default=None,
|
||||
max_length=500,
|
||||
description="头像图片链接"
|
||||
)
|
||||
phone: Optional[str] = Field(
|
||||
default=None,
|
||||
max_length=20,
|
||||
description="手机号码"
|
||||
)
|
||||
|
||||
# ---------- 安全与状态标识 ----------
|
||||
is_active: bool = Field(
|
||||
default=True,
|
||||
description="账户是否启用(用于软禁用)"
|
||||
)
|
||||
is_verified: bool = Field(
|
||||
default=False,
|
||||
description="邮箱/手机是否已验证"
|
||||
)
|
||||
is_superuser: bool = Field(
|
||||
default=False,
|
||||
description="是否为超级管理员"
|
||||
)
|
||||
|
||||
# ---------- 审计时间戳(数据库自动维护) ----------
|
||||
created_at: datetime = Field(
|
||||
sa_column=Column(
|
||||
DateTime(timezone=True),
|
||||
server_default=func.now(),
|
||||
nullable=False,
|
||||
comment="创建时间"
|
||||
)
|
||||
)
|
||||
updated_at: datetime = Field(
|
||||
sa_column=Column(
|
||||
DateTime(timezone=True),
|
||||
server_default=func.now(),
|
||||
onupdate=func.now(),
|
||||
nullable=False,
|
||||
comment="最后更新时间"
|
||||
)
|
||||
)
|
||||
|
||||
# ---------- 最后登录记录(可选) ----------
|
||||
last_login_at: Optional[datetime] = Field(
|
||||
default=None,
|
||||
sa_column=Column(DateTime(timezone=True), nullable=True),
|
||||
description="最后一次登录时间"
|
||||
)
|
||||
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
自动生成的 Pydantic 模型 — auth
|
||||
来源: config/workers/auth/
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class ChangePasswordPostRequest(BaseModel):
|
||||
"""修改密码 — 请求体"""
|
||||
user_id: str = Field(...)
|
||||
old_password: str = Field(...)
|
||||
new_password: str = Field(...)
|
||||
|
||||
class ChangePasswordPostResponse(BaseModel):
|
||||
"""修改密码 — 响应体"""
|
||||
result: str = Field(...)
|
||||
message: str = Field(...)
|
||||
timestamp: Optional[float] = Field(default=None)
|
||||
|
||||
class LoginPostRequest(BaseModel):
|
||||
"""登录 — 请求体"""
|
||||
username: str = Field(...)
|
||||
password: str = Field(...)
|
||||
|
||||
class LoginPostResponse(BaseModel):
|
||||
"""登录 — 响应体"""
|
||||
result: str = Field(...)
|
||||
token: str = Field(...)
|
||||
message: str = Field(...)
|
||||
timestamp: Optional[float] = Field(default=None)
|
||||
|
||||
class MeGetRequest(BaseModel):
|
||||
"""获取当前用户信息 — 请求体"""
|
||||
user_id: str = Field(...)
|
||||
|
||||
class MeGetResponse(BaseModel):
|
||||
"""获取当前用户信息 — 响应体"""
|
||||
result: str = Field(...)
|
||||
id: str = Field(...)
|
||||
username: str = Field(...)
|
||||
email: str = Field(...)
|
||||
full_name: Optional[str] = Field(default=None)
|
||||
avatar_url: Optional[str] = Field(default=None)
|
||||
phone: Optional[str] = Field(default=None)
|
||||
is_active: bool = Field(...)
|
||||
is_verified: bool = Field(...)
|
||||
is_superuser: bool = Field(...)
|
||||
created_at: Optional[str] = Field(default=None)
|
||||
updated_at: Optional[str] = Field(default=None)
|
||||
last_login_at: Optional[str] = Field(default=None)
|
||||
|
||||
class RegisterPostRequest(BaseModel):
|
||||
"""用户注册 — 请求体"""
|
||||
username: str = Field(...)
|
||||
password: str = Field(...)
|
||||
email: str = Field(...)
|
||||
full_name: str = Field(...)
|
||||
phone: str = Field(...)
|
||||
|
||||
class RegisterPostResponse(BaseModel):
|
||||
"""用户注册 — 响应体"""
|
||||
result: str = Field(...)
|
||||
message: str = Field(...)
|
||||
user_id: Optional[str] = Field(default=None)
|
||||
timestamp: Optional[float] = Field(default=None)
|
||||
|
||||
class UpdateProfilePostRequest(BaseModel):
|
||||
"""更新用户资料 — 请求体"""
|
||||
user_id: str = Field(...)
|
||||
full_name: str = Field(...)
|
||||
email: str = Field(...)
|
||||
avatar_url: str = Field(...)
|
||||
phone: str = Field(...)
|
||||
|
||||
class UpdateProfilePostResponse(BaseModel):
|
||||
"""更新用户资料 — 响应体"""
|
||||
result: str = Field(...)
|
||||
message: str = Field(...)
|
||||
timestamp: Optional[float] = Field(default=None)
|
||||
|
||||
|
||||
# 所有响应模型的联合类型
|
||||
AuthResponse = LoginPostResponse | ChangePasswordPostResponse | MeGetResponse | UpdateProfilePostResponse | RegisterPostResponse
|
||||
@@ -0,0 +1,8 @@
|
||||
from .login import login
|
||||
from .register import register
|
||||
from .me import me
|
||||
from .update_profile import update_profile
|
||||
from .change_password import change_password
|
||||
|
||||
|
||||
__all__ = ["login", "register", "me", "update_profile", "change_password"]
|
||||
@@ -0,0 +1,81 @@
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from chongming_worker import rpc_method
|
||||
from chongming_password import hash_password, verify_password
|
||||
|
||||
from ..models.pydantic import (
|
||||
ChangePasswordPostRequest,
|
||||
ChangePasswordPostResponse
|
||||
)
|
||||
from ..models.auth import Users
|
||||
|
||||
|
||||
@rpc_method(
|
||||
subject="auth.change_password",
|
||||
method="POST",
|
||||
summary="修改密码",
|
||||
response_model=ChangePasswordPostResponse,
|
||||
)
|
||||
async def change_password(change_password_post_request: ChangePasswordPostRequest) -> ChangePasswordPostResponse:
|
||||
from ..global_var import db
|
||||
|
||||
if db is None:
|
||||
raise RuntimeError("Database not initialized")
|
||||
|
||||
user_id = change_password_post_request.user_id
|
||||
old_password = change_password_post_request.old_password
|
||||
new_password = change_password_post_request.new_password
|
||||
|
||||
if not old_password:
|
||||
return ChangePasswordPostResponse(
|
||||
result="fail",
|
||||
message="旧密码不能为空",
|
||||
timestamp=time.time()
|
||||
)
|
||||
if not new_password:
|
||||
return ChangePasswordPostResponse(
|
||||
result="fail",
|
||||
message="新密码不能为空",
|
||||
timestamp=time.time()
|
||||
)
|
||||
if old_password == new_password:
|
||||
return ChangePasswordPostResponse(
|
||||
result="fail",
|
||||
message="新密码不能与旧密码相同",
|
||||
timestamp=time.time()
|
||||
)
|
||||
|
||||
async with db.get_session() as session:
|
||||
stmt = select(Users).filter_by(id=user_id)
|
||||
result = await session.execute(stmt)
|
||||
user: Users | None = result.scalar_one_or_none()
|
||||
|
||||
if user is None:
|
||||
return ChangePasswordPostResponse(
|
||||
result="fail",
|
||||
message="用户不存在",
|
||||
timestamp=time.time()
|
||||
)
|
||||
|
||||
# 验证旧密码
|
||||
if not verify_password(old_password, user.hashed_password):
|
||||
return ChangePasswordPostResponse(
|
||||
result="fail",
|
||||
message="旧密码错误",
|
||||
timestamp=time.time()
|
||||
)
|
||||
|
||||
# 更新密码
|
||||
user.hashed_password = hash_password(new_password)
|
||||
user.updated_at = datetime.now()
|
||||
session.add(user)
|
||||
await session.commit()
|
||||
|
||||
return ChangePasswordPostResponse(
|
||||
result="success",
|
||||
message="密码修改成功",
|
||||
timestamp=time.time()
|
||||
)
|
||||
@@ -0,0 +1,85 @@
|
||||
import time
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from chongming_worker import rpc_method
|
||||
from chongming_password import verify_password
|
||||
|
||||
from ..models.pydantic import (
|
||||
LoginPostRequest,
|
||||
LoginPostResponse
|
||||
)
|
||||
from ..models.auth import Users
|
||||
|
||||
|
||||
@rpc_method(
|
||||
subject="auth.login",
|
||||
method="POST",
|
||||
summary="登录",
|
||||
response_model=LoginPostResponse,
|
||||
)
|
||||
async def login(login_post_request: LoginPostRequest) -> LoginPostResponse:
|
||||
from ..global_var import jwt, db
|
||||
|
||||
if db is None:
|
||||
raise RuntimeError("Database not initialized")
|
||||
async with db.get_session() as session:
|
||||
statement = select(Users).filter_by(
|
||||
username=login_post_request.username,
|
||||
)
|
||||
result = await session.execute(statement)
|
||||
user: Users | None = result.scalar_one_or_none()
|
||||
|
||||
if user is None:
|
||||
return LoginPostResponse(
|
||||
result="fail",
|
||||
message="用户名错误",
|
||||
token="",
|
||||
timestamp=time.time()
|
||||
)
|
||||
|
||||
if not verify_password(login_post_request.password, user.hashed_password):
|
||||
return LoginPostResponse(
|
||||
result="fail",
|
||||
message="密码错误",
|
||||
token="",
|
||||
timestamp=time.time()
|
||||
)
|
||||
|
||||
if not user.is_active:
|
||||
return LoginPostResponse(
|
||||
result="fail",
|
||||
message="用户已经注销或被禁用",
|
||||
token="",
|
||||
timestamp=time.time()
|
||||
)
|
||||
|
||||
user.last_login_at = datetime.now()
|
||||
|
||||
payload = {
|
||||
"sub": user.id,
|
||||
"username": user.username,
|
||||
"email": user.email,
|
||||
"full_name": user.full_name,
|
||||
"avatar_url": user.avatar_url,
|
||||
"phone": user.phone,
|
||||
"is_verified": user.is_verified,
|
||||
"is_superuser": user.is_superuser,
|
||||
"created_at": user.created_at.isoformat(),
|
||||
"updated_at": user.updated_at.isoformat(),
|
||||
"last_login_at": user.last_login_at.isoformat()
|
||||
}
|
||||
|
||||
token = jwt.encode(
|
||||
json.dumps(payload),
|
||||
expires_in_seconds=10800 # 3小时有效期
|
||||
)
|
||||
|
||||
return LoginPostResponse(
|
||||
result="success",
|
||||
message="登录成功",
|
||||
token=token,
|
||||
timestamp=time.time()
|
||||
)
|
||||
@@ -0,0 +1,56 @@
|
||||
import time
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from chongming_worker import rpc_method
|
||||
|
||||
from ..models.pydantic import (
|
||||
MeGetRequest,
|
||||
MeGetResponse
|
||||
)
|
||||
from ..models.auth import Users
|
||||
|
||||
|
||||
@rpc_method(
|
||||
subject="auth.me",
|
||||
method="GET",
|
||||
summary="获取当前用户信息",
|
||||
response_model=MeGetResponse,
|
||||
)
|
||||
async def me(me_get_request: MeGetRequest) -> MeGetResponse:
|
||||
from ..global_var import db
|
||||
|
||||
if db is None:
|
||||
raise RuntimeError("Database not initialized")
|
||||
|
||||
async with db.get_session() as session:
|
||||
stmt = select(Users).filter_by(id=me_get_request.user_id)
|
||||
result = await session.execute(stmt)
|
||||
user: Users | None = result.scalar_one_or_none()
|
||||
|
||||
if user is None:
|
||||
return MeGetResponse(
|
||||
result="fail",
|
||||
id="",
|
||||
username="",
|
||||
email="",
|
||||
is_active=False,
|
||||
is_verified=False,
|
||||
is_superuser=False,
|
||||
)
|
||||
|
||||
return MeGetResponse(
|
||||
result="success",
|
||||
id=user.id,
|
||||
username=user.username,
|
||||
email=user.email,
|
||||
full_name=user.full_name,
|
||||
avatar_url=user.avatar_url,
|
||||
phone=user.phone,
|
||||
is_active=user.is_active,
|
||||
is_verified=user.is_verified,
|
||||
is_superuser=user.is_superuser,
|
||||
created_at=user.created_at.isoformat() if user.created_at else None,
|
||||
updated_at=user.updated_at.isoformat() if user.updated_at else None,
|
||||
last_login_at=user.last_login_at.isoformat() if user.last_login_at else None,
|
||||
)
|
||||
@@ -0,0 +1,93 @@
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from chongming_worker import rpc_method
|
||||
from chongming_password import hash_password
|
||||
|
||||
from ..models.pydantic import (
|
||||
RegisterPostRequest,
|
||||
RegisterPostResponse
|
||||
)
|
||||
from ..models.auth import Users
|
||||
|
||||
|
||||
@rpc_method(
|
||||
subject="auth.register",
|
||||
method="POST",
|
||||
summary="用户注册",
|
||||
response_model=RegisterPostResponse,
|
||||
)
|
||||
async def register(register_post_request: RegisterPostRequest) -> RegisterPostResponse:
|
||||
from ..global_var import db
|
||||
|
||||
if db is None:
|
||||
raise RuntimeError("Database not initialized")
|
||||
|
||||
username = register_post_request.username.strip()
|
||||
email = register_post_request.email.strip()
|
||||
full_name = register_post_request.full_name.strip() or None
|
||||
phone = register_post_request.phone.strip() or None
|
||||
|
||||
# 校验必填字段
|
||||
if not username:
|
||||
return RegisterPostResponse(
|
||||
result="fail",
|
||||
message="用户名不能为空",
|
||||
timestamp=time.time()
|
||||
)
|
||||
if not email:
|
||||
return RegisterPostResponse(
|
||||
result="fail",
|
||||
message="邮箱不能为空",
|
||||
timestamp=time.time()
|
||||
)
|
||||
if not register_post_request.password:
|
||||
return RegisterPostResponse(
|
||||
result="fail",
|
||||
message="密码不能为空",
|
||||
timestamp=time.time()
|
||||
)
|
||||
|
||||
async with db.get_session() as session:
|
||||
# 检查用户名是否已存在
|
||||
stmt = select(Users).filter_by(username=username)
|
||||
result = await session.execute(stmt)
|
||||
if result.scalar_one_or_none() is not None:
|
||||
return RegisterPostResponse(
|
||||
result="fail",
|
||||
message="用户名已存在",
|
||||
timestamp=time.time()
|
||||
)
|
||||
|
||||
# 检查邮箱是否已存在
|
||||
stmt = select(Users).filter_by(email=email)
|
||||
result = await session.execute(stmt)
|
||||
if result.scalar_one_or_none() is not None:
|
||||
return RegisterPostResponse(
|
||||
result="fail",
|
||||
message="邮箱已被注册",
|
||||
timestamp=time.time()
|
||||
)
|
||||
|
||||
# 创建新用户
|
||||
user = Users(
|
||||
username=username,
|
||||
hashed_password=hash_password(register_post_request.password),
|
||||
email=email,
|
||||
full_name=full_name,
|
||||
phone=phone,
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
session.add(user)
|
||||
await session.commit()
|
||||
await session.refresh(user)
|
||||
|
||||
return RegisterPostResponse(
|
||||
result="success",
|
||||
message="注册成功",
|
||||
user_id=user.id,
|
||||
timestamp=time.time()
|
||||
)
|
||||
@@ -0,0 +1,74 @@
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from chongming_worker import rpc_method
|
||||
|
||||
from ..models.pydantic import (
|
||||
UpdateProfilePostRequest,
|
||||
UpdateProfilePostResponse
|
||||
)
|
||||
from ..models.auth import Users
|
||||
|
||||
|
||||
@rpc_method(
|
||||
subject="auth.update_profile",
|
||||
method="POST",
|
||||
summary="更新用户资料",
|
||||
response_model=UpdateProfilePostResponse,
|
||||
)
|
||||
async def update_profile(update_profile_post_request: UpdateProfilePostRequest) -> UpdateProfilePostResponse:
|
||||
from ..global_var import db
|
||||
|
||||
if db is None:
|
||||
raise RuntimeError("Database not initialized")
|
||||
|
||||
user_id = update_profile_post_request.user_id
|
||||
|
||||
async with db.get_session() as session:
|
||||
stmt = select(Users).filter_by(id=user_id)
|
||||
result = await session.execute(stmt)
|
||||
user: Users | None = result.scalar_one_or_none()
|
||||
|
||||
if user is None:
|
||||
return UpdateProfilePostResponse(
|
||||
result="fail",
|
||||
message="用户不存在",
|
||||
timestamp=time.time()
|
||||
)
|
||||
|
||||
# 更新字段(只更新非空值)
|
||||
full_name = update_profile_post_request.full_name.strip()
|
||||
email = update_profile_post_request.email.strip()
|
||||
avatar_url = update_profile_post_request.avatar_url.strip()
|
||||
phone = update_profile_post_request.phone.strip()
|
||||
|
||||
if full_name:
|
||||
user.full_name = full_name
|
||||
if email:
|
||||
# 检查邮箱是否被其他用户占用
|
||||
if email != user.email:
|
||||
check_stmt = select(Users).filter_by(email=email)
|
||||
check_result = await session.execute(check_stmt)
|
||||
if check_result.scalar_one_or_none() is not None:
|
||||
return UpdateProfilePostResponse(
|
||||
result="fail",
|
||||
message="邮箱已被其他用户使用",
|
||||
timestamp=time.time()
|
||||
)
|
||||
user.email = email
|
||||
if avatar_url:
|
||||
user.avatar_url = avatar_url
|
||||
if phone:
|
||||
user.phone = phone
|
||||
|
||||
user.updated_at = datetime.now()
|
||||
session.add(user)
|
||||
await session.commit()
|
||||
|
||||
return UpdateProfilePostResponse(
|
||||
result="success",
|
||||
message="资料更新成功",
|
||||
timestamp=time.time()
|
||||
)
|
||||
Reference in New Issue
Block a user