Add V2 admin console
This commit is contained in:
24
ai_knowledge_base_v2/apps/backend/app/api/admin_auth.py
Normal file
24
ai_knowledge_base_v2/apps/backend/app/api/admin_auth.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.dependencies import get_current_admin
|
||||
from app.core.responses import api_success
|
||||
from app.models.admin import Admin
|
||||
from app.schemas.admin import AdminLoginRequest, AdminLoginResponse, AdminRead
|
||||
from app.services.admin_service import AdminAuthService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
def login(payload: AdminLoginRequest, db: Session = Depends(get_db)) -> dict:
|
||||
result = AdminAuthService.login(db, payload.username, payload.password)
|
||||
return api_success(AdminLoginResponse.model_validate(result).model_dump(mode="json"))
|
||||
|
||||
|
||||
@router.get("/profile")
|
||||
def profile(current_admin: Admin = Depends(get_current_admin)) -> dict:
|
||||
return api_success(AdminRead.model_validate(current_admin).model_dump())
|
||||
22
ai_knowledge_base_v2/apps/backend/app/api/admin_dashboard.py
Normal file
22
ai_knowledge_base_v2/apps/backend/app/api/admin_dashboard.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.dependencies import get_current_admin
|
||||
from app.core.responses import api_success
|
||||
from app.models.admin import Admin
|
||||
from app.schemas.admin import DashboardStats
|
||||
from app.services.admin_service import AdminDashboardService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/dashboard")
|
||||
def dashboard(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
stats = AdminDashboardService.stats(db)
|
||||
return api_success(DashboardStats.model_validate(stats).model_dump())
|
||||
95
ai_knowledge_base_v2/apps/backend/app/api/admin_knowledge.py
Normal file
95
ai_knowledge_base_v2/apps/backend/app/api/admin_knowledge.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.dependencies import get_current_admin
|
||||
from app.core.responses import api_success
|
||||
from app.models.admin import Admin
|
||||
from app.models.knowledge import Knowledge
|
||||
from app.schemas.admin import KnowledgeSaveRequest
|
||||
from app.services.admin_service import OperationLogService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/knowledge/list")
|
||||
def list_knowledge(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)) -> dict:
|
||||
items = db.scalars(select(Knowledge).order_by(Knowledge.id.desc())).all()
|
||||
return api_success([_knowledge_dict(item) for item in items])
|
||||
|
||||
|
||||
@router.post("/knowledge")
|
||||
def create_knowledge(
|
||||
payload: KnowledgeSaveRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
knowledge = Knowledge(
|
||||
name=payload.name,
|
||||
feishu_space_id=payload.feishuSpaceId,
|
||||
feishu_node_id=payload.feishuNodeId,
|
||||
status=payload.status,
|
||||
remark=payload.remark,
|
||||
)
|
||||
db.add(knowledge)
|
||||
db.flush()
|
||||
OperationLogService.write(db, admin_id=current_admin.id, module="knowledge", action="create", target_id=knowledge.id)
|
||||
db.commit()
|
||||
db.refresh(knowledge)
|
||||
return api_success(_knowledge_dict(knowledge))
|
||||
|
||||
|
||||
@router.put("/knowledge/{knowledge_id}")
|
||||
def update_knowledge(
|
||||
knowledge_id: int,
|
||||
payload: KnowledgeSaveRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
knowledge = _get_knowledge(db, knowledge_id)
|
||||
knowledge.name = payload.name
|
||||
knowledge.feishu_space_id = payload.feishuSpaceId
|
||||
knowledge.feishu_node_id = payload.feishuNodeId
|
||||
knowledge.status = payload.status
|
||||
knowledge.remark = payload.remark
|
||||
db.add(knowledge)
|
||||
OperationLogService.write(db, admin_id=current_admin.id, module="knowledge", action="update", target_id=knowledge.id)
|
||||
db.commit()
|
||||
db.refresh(knowledge)
|
||||
return api_success(_knowledge_dict(knowledge))
|
||||
|
||||
|
||||
@router.delete("/knowledge/{knowledge_id}")
|
||||
def delete_knowledge(
|
||||
knowledge_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
knowledge = _get_knowledge(db, knowledge_id)
|
||||
db.delete(knowledge)
|
||||
OperationLogService.write(db, admin_id=current_admin.id, module="knowledge", action="delete", target_id=knowledge.id)
|
||||
db.commit()
|
||||
return api_success()
|
||||
|
||||
|
||||
def _get_knowledge(db: Session, knowledge_id: int) -> Knowledge:
|
||||
knowledge = db.get(Knowledge, knowledge_id)
|
||||
if knowledge is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="知识库不存在")
|
||||
return knowledge
|
||||
|
||||
|
||||
def _knowledge_dict(knowledge: Knowledge) -> dict:
|
||||
return {
|
||||
"id": knowledge.id,
|
||||
"name": knowledge.name,
|
||||
"feishuSpaceId": knowledge.feishu_space_id,
|
||||
"feishuNodeId": knowledge.feishu_node_id,
|
||||
"status": knowledge.status,
|
||||
"remark": knowledge.remark,
|
||||
"createdAt": knowledge.created_at,
|
||||
"updatedAt": knowledge.updated_at,
|
||||
}
|
||||
118
ai_knowledge_base_v2/apps/backend/app/api/admin_records.py
Normal file
118
ai_knowledge_base_v2/apps/backend/app/api/admin_records.py
Normal file
@@ -0,0 +1,118 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.dependencies import get_current_admin
|
||||
from app.core.responses import api_success
|
||||
from app.models.admin import Admin
|
||||
from app.models.chat import ChatMessage, ChatSession
|
||||
from app.models.logs import AiRequestLog, OperationLog
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/chat/list")
|
||||
def chat_list(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
sessions = db.scalars(
|
||||
select(ChatSession)
|
||||
.where(ChatSession.is_deleted == 0)
|
||||
.order_by(ChatSession.updated_at.desc())
|
||||
.limit(200)
|
||||
).all()
|
||||
return api_success(
|
||||
[
|
||||
{
|
||||
"id": item.id,
|
||||
"userId": item.user_id,
|
||||
"title": item.title,
|
||||
"messageCount": item.message_count,
|
||||
"lastMessageAt": item.last_message_at,
|
||||
"updatedAt": item.updated_at,
|
||||
}
|
||||
for item in sessions
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@router.get("/chat/{session_id}")
|
||||
def chat_detail(
|
||||
session_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
messages = db.scalars(
|
||||
select(ChatMessage)
|
||||
.where(ChatMessage.session_id == session_id)
|
||||
.order_by(ChatMessage.created_at.asc(), ChatMessage.id.asc())
|
||||
).all()
|
||||
return api_success(
|
||||
[
|
||||
{
|
||||
"id": item.id,
|
||||
"role": item.role,
|
||||
"content": item.content,
|
||||
"messageStatus": item.message_status,
|
||||
"tokenInput": item.token_input,
|
||||
"tokenOutput": item.token_output,
|
||||
"responseTimeMs": item.response_time_ms,
|
||||
"createdAt": item.created_at,
|
||||
}
|
||||
for item in messages
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@router.get("/log/list")
|
||||
def operation_logs(
|
||||
module: str = Query(default=""),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
query = select(OperationLog).order_by(OperationLog.created_at.desc()).limit(200)
|
||||
if module:
|
||||
query = query.where(OperationLog.module == module)
|
||||
logs = db.scalars(query).all()
|
||||
return api_success(
|
||||
[
|
||||
{
|
||||
"id": item.id,
|
||||
"adminId": item.admin_id,
|
||||
"module": item.module,
|
||||
"action": item.action,
|
||||
"targetId": item.target_id,
|
||||
"result": item.result,
|
||||
"createdAt": item.created_at,
|
||||
}
|
||||
for item in logs
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@router.get("/ai-log/list")
|
||||
def ai_logs(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)) -> dict:
|
||||
logs = db.scalars(select(AiRequestLog).order_by(AiRequestLog.created_at.desc()).limit(200)).all()
|
||||
return api_success(
|
||||
[
|
||||
{
|
||||
"id": item.id,
|
||||
"sessionId": item.session_id,
|
||||
"messageId": item.message_id,
|
||||
"userId": item.user_id,
|
||||
"modelName": item.model_name,
|
||||
"knowledgeIds": item.knowledge_ids,
|
||||
"retrieveCount": item.retrieve_count,
|
||||
"totalToken": item.total_token,
|
||||
"costMs": item.cost_ms,
|
||||
"status": item.status,
|
||||
"errorMessage": item.error_message,
|
||||
"createdAt": item.created_at,
|
||||
}
|
||||
for item in logs
|
||||
]
|
||||
)
|
||||
159
ai_knowledge_base_v2/apps/backend/app/api/admin_settings.py
Normal file
159
ai_knowledge_base_v2/apps/backend/app/api/admin_settings.py
Normal file
@@ -0,0 +1,159 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.dependencies import get_current_admin
|
||||
from app.core.responses import api_success
|
||||
from app.models.admin import Admin
|
||||
from app.models.ai_config import ModelConfig, Prompt, SystemConfig
|
||||
from app.schemas.admin import EnableModelRequest, ModelSaveRequest, PromptSaveRequest, SystemConfigSaveRequest
|
||||
from app.services.admin_service import OperationLogService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
DEFAULT_PROMPT = "你是企业飞书知识库 AI 助手。你只能基于提供的知识片段回答,不能编造。"
|
||||
|
||||
|
||||
@router.get("/prompt")
|
||||
def get_prompt(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)) -> dict:
|
||||
prompt = db.scalar(select(Prompt).order_by(Prompt.updated_at.desc(), Prompt.id.desc()).limit(1))
|
||||
return api_success({"promptContent": prompt.prompt_content if prompt else DEFAULT_PROMPT})
|
||||
|
||||
|
||||
@router.put("/prompt")
|
||||
def save_prompt(
|
||||
payload: PromptSaveRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
prompt = Prompt(prompt_content=payload.promptContent, updated_by=current_admin.id)
|
||||
db.add(prompt)
|
||||
db.flush()
|
||||
OperationLogService.write(db, admin_id=current_admin.id, module="prompt", action="save", target_id=prompt.id)
|
||||
db.commit()
|
||||
return api_success({"promptContent": prompt.prompt_content})
|
||||
|
||||
|
||||
@router.post("/prompt/reset")
|
||||
def reset_prompt(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)) -> dict:
|
||||
prompt = Prompt(prompt_content=DEFAULT_PROMPT, updated_by=current_admin.id)
|
||||
db.add(prompt)
|
||||
db.flush()
|
||||
OperationLogService.write(db, admin_id=current_admin.id, module="prompt", action="reset", target_id=prompt.id)
|
||||
db.commit()
|
||||
return api_success({"promptContent": prompt.prompt_content})
|
||||
|
||||
|
||||
@router.get("/model/list")
|
||||
def list_models(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)) -> dict:
|
||||
models = db.scalars(select(ModelConfig).order_by(ModelConfig.id.desc())).all()
|
||||
return api_success([_model_dict(model) for model in models])
|
||||
|
||||
|
||||
@router.post("/model")
|
||||
def create_model(
|
||||
payload: ModelSaveRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
model = ModelConfig(
|
||||
provider=payload.provider,
|
||||
model_name=payload.modelName,
|
||||
api_url=payload.apiUrl,
|
||||
api_key=payload.apiKey,
|
||||
temperature=payload.temperature,
|
||||
max_token=payload.maxToken,
|
||||
timeout_second=payload.timeoutSecond,
|
||||
enabled=0,
|
||||
)
|
||||
db.add(model)
|
||||
db.flush()
|
||||
OperationLogService.write(db, admin_id=current_admin.id, module="model", action="create", target_id=model.id)
|
||||
db.commit()
|
||||
db.refresh(model)
|
||||
return api_success(_model_dict(model))
|
||||
|
||||
|
||||
@router.post("/model/enable")
|
||||
def enable_model(
|
||||
payload: EnableModelRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
target = db.get(ModelConfig, payload.modelId)
|
||||
if target is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="模型不存在")
|
||||
for model in db.scalars(select(ModelConfig)).all():
|
||||
model.enabled = 1 if model.id == target.id else 0
|
||||
db.add(model)
|
||||
OperationLogService.write(db, admin_id=current_admin.id, module="model", action="enable", target_id=target.id)
|
||||
db.commit()
|
||||
return api_success()
|
||||
|
||||
|
||||
@router.delete("/model/{model_id}")
|
||||
def delete_model(
|
||||
model_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
model = db.get(ModelConfig, model_id)
|
||||
if model is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="模型不存在")
|
||||
db.delete(model)
|
||||
OperationLogService.write(db, admin_id=current_admin.id, module="model", action="delete", target_id=model.id)
|
||||
db.commit()
|
||||
return api_success()
|
||||
|
||||
|
||||
@router.get("/config")
|
||||
def list_config(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)) -> dict:
|
||||
configs = db.scalars(select(SystemConfig).order_by(SystemConfig.config_key.asc())).all()
|
||||
return api_success([_config_dict(config) for config in configs])
|
||||
|
||||
|
||||
@router.put("/config")
|
||||
def save_config(
|
||||
payload: SystemConfigSaveRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
config = db.scalar(select(SystemConfig).where(SystemConfig.config_key == payload.configKey))
|
||||
if config is None:
|
||||
config = SystemConfig(config_key=payload.configKey, config_value=payload.configValue)
|
||||
config.config_value = payload.configValue
|
||||
config.description = payload.description
|
||||
config.updated_by = current_admin.id
|
||||
db.add(config)
|
||||
db.flush()
|
||||
OperationLogService.write(db, admin_id=current_admin.id, module="config", action="save", target_id=config.id)
|
||||
db.commit()
|
||||
db.refresh(config)
|
||||
return api_success(_config_dict(config))
|
||||
|
||||
|
||||
def _model_dict(model: ModelConfig) -> dict:
|
||||
return {
|
||||
"id": model.id,
|
||||
"provider": model.provider,
|
||||
"modelName": model.model_name,
|
||||
"apiUrl": model.api_url,
|
||||
"apiKeyMasked": "******" if model.api_key else "",
|
||||
"temperature": float(model.temperature) if model.temperature is not None else None,
|
||||
"maxToken": model.max_token,
|
||||
"timeoutSecond": model.timeout_second,
|
||||
"enabled": model.enabled,
|
||||
}
|
||||
|
||||
|
||||
def _config_dict(config: SystemConfig) -> dict:
|
||||
return {
|
||||
"id": config.id,
|
||||
"configKey": config.config_key,
|
||||
"configValue": config.config_value,
|
||||
"description": config.description,
|
||||
"updatedAt": config.updated_at,
|
||||
}
|
||||
98
ai_knowledge_base_v2/apps/backend/app/api/admin_users.py
Normal file
98
ai_knowledge_base_v2/apps/backend/app/api/admin_users.py
Normal file
@@ -0,0 +1,98 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.dependencies import get_current_admin
|
||||
from app.core.responses import api_success
|
||||
from app.models.admin import Admin
|
||||
from app.models.user import User
|
||||
from app.schemas.admin import AdminUserUpdateRequest
|
||||
from app.services.admin_service import OperationLogService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/user/list")
|
||||
def list_users(
|
||||
keyword: str = Query(default=""),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
query = select(User).where(User.is_deleted == 0).order_by(User.id.desc())
|
||||
if keyword:
|
||||
like = f"%{keyword}%"
|
||||
query = query.where((User.phone.like(like)) | (User.name.like(like)))
|
||||
users = db.scalars(query.limit(200)).all()
|
||||
return api_success([_user_dict(user) for user in users])
|
||||
|
||||
|
||||
@router.get("/user/{user_id}")
|
||||
def user_detail(
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
user = _get_user(db, user_id)
|
||||
return api_success(_user_dict(user))
|
||||
|
||||
|
||||
@router.put("/user/{user_id}")
|
||||
def update_user(
|
||||
user_id: int,
|
||||
payload: AdminUserUpdateRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
user = _get_user(db, user_id)
|
||||
if payload.status is not None:
|
||||
user.status = payload.status
|
||||
if payload.dailyChatLimit is not None:
|
||||
user.daily_chat_limit = payload.dailyChatLimit
|
||||
if payload.expiredAt is not None:
|
||||
user.expired_at = payload.expiredAt.replace(tzinfo=None)
|
||||
db.add(user)
|
||||
OperationLogService.write(db, admin_id=current_admin.id, module="user", action="update", target_id=user.id)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return api_success(_user_dict(user))
|
||||
|
||||
|
||||
@router.delete("/user/{user_id}")
|
||||
def delete_user(
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
user = _get_user(db, user_id)
|
||||
user.is_deleted = 1
|
||||
db.add(user)
|
||||
OperationLogService.write(db, admin_id=current_admin.id, module="user", action="delete", target_id=user.id)
|
||||
db.commit()
|
||||
return api_success()
|
||||
|
||||
|
||||
def _get_user(db: Session, user_id: int) -> User:
|
||||
user = db.get(User, user_id)
|
||||
if user is None or user.is_deleted:
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
|
||||
return user
|
||||
|
||||
|
||||
def _user_dict(user: User) -> dict:
|
||||
return {
|
||||
"id": user.id,
|
||||
"phone": user.phone,
|
||||
"name": user.name,
|
||||
"nickname": user.nickname,
|
||||
"status": user.status,
|
||||
"dailyChatLimit": user.daily_chat_limit,
|
||||
"dailyChatUsed": user.daily_chat_used,
|
||||
"expiredAt": user.expired_at,
|
||||
"lastLoginAt": user.last_login_at,
|
||||
"createdAt": user.created_at,
|
||||
}
|
||||
@@ -2,10 +2,27 @@ from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api import auth, chat, health, user
|
||||
from app.api import (
|
||||
admin_auth,
|
||||
admin_dashboard,
|
||||
admin_knowledge,
|
||||
admin_records,
|
||||
admin_settings,
|
||||
admin_users,
|
||||
auth,
|
||||
chat,
|
||||
health,
|
||||
user,
|
||||
)
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(health.router, tags=["health"])
|
||||
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
||||
api_router.include_router(user.router, prefix="/user", tags=["user"])
|
||||
api_router.include_router(chat.router, prefix="/chat", tags=["chat"])
|
||||
api_router.include_router(admin_auth.router, prefix="/admin", tags=["admin"])
|
||||
api_router.include_router(admin_dashboard.router, prefix="/admin", tags=["admin"])
|
||||
api_router.include_router(admin_users.router, prefix="/admin", tags=["admin"])
|
||||
api_router.include_router(admin_knowledge.router, prefix="/admin", tags=["admin"])
|
||||
api_router.include_router(admin_settings.router, prefix="/admin", tags=["admin"])
|
||||
api_router.include_router(admin_records.router, prefix="/admin", tags=["admin"])
|
||||
|
||||
Reference in New Issue
Block a user