feat: 创建订单必须通过 token 认证,移除 user_id 参数
- POST /api/orders 改为通过 token 解析用户身份,移除 user_id - 前端不再从 URL 读取 user_id,完全依赖 token - 前端提交前检查 pending 订单数量,超过 3 个禁止提交并提示 - 后端 createOrder 保留 MAX_PENDING_ORDERS=3 的服务端校验 - PaymentForm 增加 pendingBlocked 状态提示和按钮禁用 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,9 +3,10 @@ import { z } from 'zod';
|
|||||||
import { createOrder, OrderError } from '@/lib/order/service';
|
import { createOrder, OrderError } from '@/lib/order/service';
|
||||||
import { getEnv } from '@/lib/config';
|
import { getEnv } from '@/lib/config';
|
||||||
import { initPaymentProviders, paymentRegistry } from '@/lib/payment';
|
import { initPaymentProviders, paymentRegistry } from '@/lib/payment';
|
||||||
|
import { getCurrentUserByToken } from '@/lib/sub2api/client';
|
||||||
|
|
||||||
const createOrderSchema = z.object({
|
const createOrderSchema = z.object({
|
||||||
user_id: z.number().int().positive(),
|
token: z.string().min(1),
|
||||||
amount: z.number().positive(),
|
amount: z.number().positive(),
|
||||||
payment_type: z.string().min(1),
|
payment_type: z.string().min(1),
|
||||||
src_host: z.string().max(253).optional(),
|
src_host: z.string().max(253).optional(),
|
||||||
@@ -24,7 +25,16 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: '参数错误', details: parsed.error.flatten().fieldErrors }, { status: 400 });
|
return NextResponse.json({ error: '参数错误', details: parsed.error.flatten().fieldErrors }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { user_id, amount, payment_type, src_host, src_url, is_mobile } = parsed.data;
|
const { token, amount, payment_type, src_host, src_url, is_mobile } = parsed.data;
|
||||||
|
|
||||||
|
// 通过 token 解析用户身份
|
||||||
|
let userId: number;
|
||||||
|
try {
|
||||||
|
const user = await getCurrentUserByToken(token);
|
||||||
|
userId = user.id;
|
||||||
|
} catch {
|
||||||
|
return NextResponse.json({ error: '无效的 token,请重新登录', code: 'INVALID_TOKEN' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
// Validate amount range
|
// Validate amount range
|
||||||
if (amount < env.MIN_RECHARGE_AMOUNT || amount > env.MAX_RECHARGE_AMOUNT) {
|
if (amount < env.MIN_RECHARGE_AMOUNT || amount > env.MAX_RECHARGE_AMOUNT) {
|
||||||
@@ -43,7 +53,7 @@ export async function POST(request: NextRequest) {
|
|||||||
request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || request.headers.get('x-real-ip') || '127.0.0.1';
|
request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || request.headers.get('x-real-ip') || '127.0.0.1';
|
||||||
|
|
||||||
const result = await createOrder({
|
const result = await createOrder({
|
||||||
userId: user_id,
|
userId,
|
||||||
amount,
|
amount,
|
||||||
paymentType: payment_type,
|
paymentType: payment_type,
|
||||||
clientIp,
|
clientIp,
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ interface AppConfig {
|
|||||||
|
|
||||||
function PayContent() {
|
function PayContent() {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const userId = Number(searchParams.get('user_id'));
|
|
||||||
const token = (searchParams.get('token') || '').trim();
|
const token = (searchParams.get('token') || '').trim();
|
||||||
const theme = searchParams.get('theme') === 'dark' ? 'dark' : 'light';
|
const theme = searchParams.get('theme') === 'dark' ? 'dark' : 'light';
|
||||||
const uiMode = searchParams.get('ui_mode') || 'standalone';
|
const uiMode = searchParams.get('ui_mode') || 'standalone';
|
||||||
@@ -58,6 +57,7 @@ function PayContent() {
|
|||||||
const [ordersHasMore, setOrdersHasMore] = useState(false);
|
const [ordersHasMore, setOrdersHasMore] = useState(false);
|
||||||
const [ordersLoadingMore, setOrdersLoadingMore] = useState(false);
|
const [ordersLoadingMore, setOrdersLoadingMore] = useState(false);
|
||||||
const [activeMobileTab, setActiveMobileTab] = useState<'pay' | 'orders'>('pay');
|
const [activeMobileTab, setActiveMobileTab] = useState<'pay' | 'orders'>('pay');
|
||||||
|
const [pendingCount, setPendingCount] = useState(0);
|
||||||
|
|
||||||
const [config, setConfig] = useState<AppConfig>({
|
const [config, setConfig] = useState<AppConfig>({
|
||||||
enabledPaymentTypes: [],
|
enabledPaymentTypes: [],
|
||||||
@@ -68,12 +68,13 @@ function PayContent() {
|
|||||||
const [userNotFound, setUserNotFound] = useState(false);
|
const [userNotFound, setUserNotFound] = useState(false);
|
||||||
const [helpImageOpen, setHelpImageOpen] = useState(false);
|
const [helpImageOpen, setHelpImageOpen] = useState(false);
|
||||||
|
|
||||||
const effectiveUserId = resolvedUserId || userId;
|
|
||||||
const isEmbedded = uiMode === 'embedded' && isIframeContext;
|
|
||||||
const hasToken = token.length > 0;
|
const hasToken = token.length > 0;
|
||||||
|
const isEmbedded = uiMode === 'embedded' && isIframeContext;
|
||||||
const helpImageUrl = (config.helpImageUrl || '').trim();
|
const helpImageUrl = (config.helpImageUrl || '').trim();
|
||||||
const helpText = (config.helpText || '').trim();
|
const helpText = (config.helpText || '').trim();
|
||||||
const hasHelpContent = Boolean(helpImageUrl || helpText);
|
const hasHelpContent = Boolean(helpImageUrl || helpText);
|
||||||
|
const MAX_PENDING = 3;
|
||||||
|
const pendingBlocked = pendingCount >= MAX_PENDING;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
@@ -92,12 +93,49 @@ function PayContent() {
|
|||||||
}, [isMobile, step, tab]);
|
}, [isMobile, step, tab]);
|
||||||
|
|
||||||
const loadUserAndOrders = async () => {
|
const loadUserAndOrders = async () => {
|
||||||
if (!userId || Number.isNaN(userId) || userId <= 0) return;
|
if (!token) return;
|
||||||
|
|
||||||
setUserNotFound(false);
|
setUserNotFound(false);
|
||||||
try {
|
try {
|
||||||
// 始终获取服务端配置(不含隐私信息)
|
// 通过 token 获取用户详情和订单
|
||||||
const cfgRes = await fetch(`/api/user?user_id=${userId}`);
|
const meRes = await fetch(`/api/orders/my?token=${encodeURIComponent(token)}`);
|
||||||
|
if (!meRes.ok) {
|
||||||
|
setUserNotFound(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const meData = await meRes.json();
|
||||||
|
const meUser = meData.user || {};
|
||||||
|
const meId = Number(meUser.id);
|
||||||
|
if (!Number.isInteger(meId) || meId <= 0) {
|
||||||
|
setUserNotFound(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setResolvedUserId(meId);
|
||||||
|
setPendingCount(meData.summary?.pending ?? 0);
|
||||||
|
|
||||||
|
setUserInfo({
|
||||||
|
id: meId,
|
||||||
|
username:
|
||||||
|
(typeof meUser.displayName === 'string' && meUser.displayName.trim()) ||
|
||||||
|
(typeof meUser.username === 'string' && meUser.username.trim()) ||
|
||||||
|
`用户 #${meId}`,
|
||||||
|
balance: typeof meUser.balance === 'number' ? meUser.balance : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Array.isArray(meData.orders)) {
|
||||||
|
setMyOrders(meData.orders);
|
||||||
|
setOrdersPage(1);
|
||||||
|
setOrdersHasMore((meData.total_pages ?? 1) > 1);
|
||||||
|
} else {
|
||||||
|
setMyOrders([]);
|
||||||
|
setOrdersPage(1);
|
||||||
|
setOrdersHasMore(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取服务端支付配置
|
||||||
|
const cfgRes = await fetch(`/api/user?user_id=${meId}`);
|
||||||
if (cfgRes.ok) {
|
if (cfgRes.ok) {
|
||||||
const cfgData = await cfgRes.json();
|
const cfgData = await cfgRes.json();
|
||||||
if (cfgData.config) {
|
if (cfgData.config) {
|
||||||
@@ -111,54 +149,11 @@ function PayContent() {
|
|||||||
helpText: cfgData.config.helpText ?? null,
|
helpText: cfgData.config.helpText ?? null,
|
||||||
stripePublishableKey: cfgData.config.stripePublishableKey ?? null,
|
stripePublishableKey: cfgData.config.stripePublishableKey ?? null,
|
||||||
});
|
});
|
||||||
// 应用自定义 sublabel
|
|
||||||
if (cfgData.config.sublabelOverrides) {
|
if (cfgData.config.sublabelOverrides) {
|
||||||
applySublabelOverrides(cfgData.config.sublabelOverrides);
|
applySublabelOverrides(cfgData.config.sublabelOverrides);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (cfgRes.status === 404) {
|
|
||||||
setUserNotFound(true);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 有 token 时才尝试获取用户详情和订单
|
|
||||||
if (token) {
|
|
||||||
const meRes = await fetch(`/api/orders/my?token=${encodeURIComponent(token)}`);
|
|
||||||
if (meRes.ok) {
|
|
||||||
const meData = await meRes.json();
|
|
||||||
const meUser = meData.user || {};
|
|
||||||
const meId = Number(meUser.id);
|
|
||||||
if (Number.isInteger(meId) && meId > 0) {
|
|
||||||
setResolvedUserId(meId);
|
|
||||||
}
|
|
||||||
|
|
||||||
setUserInfo({
|
|
||||||
id: Number.isInteger(meId) && meId > 0 ? meId : userId,
|
|
||||||
username:
|
|
||||||
(typeof meUser.displayName === 'string' && meUser.displayName.trim()) ||
|
|
||||||
(typeof meUser.username === 'string' && meUser.username.trim()) ||
|
|
||||||
`用户 #${userId}`,
|
|
||||||
balance: typeof meUser.balance === 'number' ? meUser.balance : undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (Array.isArray(meData.orders)) {
|
|
||||||
setMyOrders(meData.orders);
|
|
||||||
setOrdersPage(1);
|
|
||||||
setOrdersHasMore((meData.total_pages ?? 1) > 1);
|
|
||||||
} else {
|
|
||||||
setMyOrders([]);
|
|
||||||
setOrdersPage(1);
|
|
||||||
setOrdersHasMore(false);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 无 token 或 token 失效:只显示用户 ID,不展示隐私信息(不显示余额)
|
|
||||||
setUserInfo({ id: userId, username: `用户 #${userId}` });
|
|
||||||
setMyOrders([]);
|
|
||||||
setOrdersPage(1);
|
|
||||||
setOrdersHasMore(false);
|
|
||||||
} catch {
|
} catch {
|
||||||
// ignore and keep page usable
|
// ignore and keep page usable
|
||||||
}
|
}
|
||||||
@@ -189,7 +184,7 @@ function PayContent() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadUserAndOrders();
|
loadUserAndOrders();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [userId, token]);
|
}, [token]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (step !== 'result' || finalStatus !== 'COMPLETED') return;
|
if (step !== 'result' || finalStatus !== 'COMPLETED') return;
|
||||||
@@ -205,11 +200,11 @@ function PayContent() {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [step, finalStatus]);
|
}, [step, finalStatus]);
|
||||||
|
|
||||||
if (!effectiveUserId || Number.isNaN(effectiveUserId) || effectiveUserId <= 0) {
|
if (!hasToken) {
|
||||||
return (
|
return (
|
||||||
<div className={`flex min-h-screen items-center justify-center p-4 ${isDark ? 'bg-slate-950' : 'bg-slate-50'}`}>
|
<div className={`flex min-h-screen items-center justify-center p-4 ${isDark ? 'bg-slate-950' : 'bg-slate-50'}`}>
|
||||||
<div className="text-center text-red-500">
|
<div className="text-center text-red-500">
|
||||||
<p className="text-lg font-medium">无效的用户 ID</p>
|
<p className="text-lg font-medium">缺少认证信息</p>
|
||||||
<p className="mt-2 text-sm text-gray-500">请从 Sub2API 平台正确访问充值页面</p>
|
<p className="mt-2 text-sm text-gray-500">请从 Sub2API 平台正确访问充值页面</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -229,7 +224,6 @@ function PayContent() {
|
|||||||
|
|
||||||
const buildScopedUrl = (path: string, forceOrdersTab = false) => {
|
const buildScopedUrl = (path: string, forceOrdersTab = false) => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (effectiveUserId) params.set('user_id', String(effectiveUserId));
|
|
||||||
if (token) params.set('token', token);
|
if (token) params.set('token', token);
|
||||||
params.set('theme', theme);
|
params.set('theme', theme);
|
||||||
params.set('ui_mode', uiMode);
|
params.set('ui_mode', uiMode);
|
||||||
@@ -242,6 +236,11 @@ function PayContent() {
|
|||||||
const ordersUrl = isMobile ? mobileOrdersUrl : pcOrdersUrl;
|
const ordersUrl = isMobile ? mobileOrdersUrl : pcOrdersUrl;
|
||||||
|
|
||||||
const handleSubmit = async (amount: number, paymentType: string) => {
|
const handleSubmit = async (amount: number, paymentType: string) => {
|
||||||
|
if (pendingBlocked) {
|
||||||
|
setError(`您有 ${pendingCount} 个待支付订单,请先完成或取消后再试(最多 ${MAX_PENDING} 个)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
@@ -250,7 +249,7 @@ function PayContent() {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
user_id: effectiveUserId,
|
token,
|
||||||
amount,
|
amount,
|
||||||
payment_type: paymentType,
|
payment_type: paymentType,
|
||||||
is_mobile: isMobile,
|
is_mobile: isMobile,
|
||||||
@@ -263,6 +262,7 @@ function PayContent() {
|
|||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const codeMessages: Record<string, string> = {
|
const codeMessages: Record<string, string> = {
|
||||||
|
INVALID_TOKEN: '认证已失效,请重新从平台进入充值页面',
|
||||||
USER_INACTIVE: '账户已被禁用,无法充值,请联系管理员',
|
USER_INACTIVE: '账户已被禁用,无法充值,请联系管理员',
|
||||||
TOO_MANY_PENDING: '您有过多待支付订单,请先完成或取消现有订单后再试',
|
TOO_MANY_PENDING: '您有过多待支付订单,请先完成或取消现有订单后再试',
|
||||||
USER_NOT_FOUND: '用户不存在,请检查链接是否正确',
|
USER_NOT_FOUND: '用户不存在,请检查链接是否正确',
|
||||||
@@ -402,7 +402,7 @@ function PayContent() {
|
|||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
activeMobileTab === 'pay' ? (
|
activeMobileTab === 'pay' ? (
|
||||||
<PaymentForm
|
<PaymentForm
|
||||||
userId={effectiveUserId}
|
userId={resolvedUserId ?? 0}
|
||||||
userName={userInfo?.username}
|
userName={userInfo?.username}
|
||||||
userBalance={userInfo?.balance}
|
userBalance={userInfo?.balance}
|
||||||
enabledPaymentTypes={config.enabledPaymentTypes}
|
enabledPaymentTypes={config.enabledPaymentTypes}
|
||||||
@@ -412,6 +412,8 @@ function PayContent() {
|
|||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
dark={isDark}
|
dark={isDark}
|
||||||
|
pendingBlocked={pendingBlocked}
|
||||||
|
pendingCount={pendingCount}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<MobileOrderList
|
<MobileOrderList
|
||||||
@@ -428,7 +430,7 @@ function PayContent() {
|
|||||||
<div className="grid gap-5 lg:grid-cols-[minmax(0,1.45fr)_minmax(300px,0.8fr)]">
|
<div className="grid gap-5 lg:grid-cols-[minmax(0,1.45fr)_minmax(300px,0.8fr)]">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<PaymentForm
|
<PaymentForm
|
||||||
userId={effectiveUserId}
|
userId={resolvedUserId ?? 0}
|
||||||
userName={userInfo?.username}
|
userName={userInfo?.username}
|
||||||
userBalance={userInfo?.balance}
|
userBalance={userInfo?.balance}
|
||||||
enabledPaymentTypes={config.enabledPaymentTypes}
|
enabledPaymentTypes={config.enabledPaymentTypes}
|
||||||
@@ -438,6 +440,8 @@ function PayContent() {
|
|||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
dark={isDark}
|
dark={isDark}
|
||||||
|
pendingBlocked={pendingBlocked}
|
||||||
|
pendingCount={pendingCount}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -452,9 +456,6 @@ function PayContent() {
|
|||||||
<li>订单完成后会自动到账</li>
|
<li>订单完成后会自动到账</li>
|
||||||
<li>如需历史记录请查看「我的订单」</li>
|
<li>如需历史记录请查看「我的订单」</li>
|
||||||
{config.maxDailyAmount > 0 && <li>每日最大充值 ¥{config.maxDailyAmount.toFixed(2)}</li>}
|
{config.maxDailyAmount > 0 && <li>每日最大充值 ¥{config.maxDailyAmount.toFixed(2)}</li>}
|
||||||
{!hasToken && (
|
|
||||||
<li className={isDark ? 'text-amber-200' : 'text-amber-700'}>当前链接无 token,订单查询受限</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ interface PaymentFormProps {
|
|||||||
onSubmit: (amount: number, paymentType: string) => Promise<void>;
|
onSubmit: (amount: number, paymentType: string) => Promise<void>;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
dark?: boolean;
|
dark?: boolean;
|
||||||
|
pendingBlocked?: boolean;
|
||||||
|
pendingCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QUICK_AMOUNTS = [10, 20, 50, 100, 200, 500, 1000, 2000];
|
const QUICK_AMOUNTS = [10, 20, 50, 100, 200, 500, 1000, 2000];
|
||||||
@@ -43,6 +45,8 @@ export default function PaymentForm({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
loading,
|
loading,
|
||||||
dark = false,
|
dark = false,
|
||||||
|
pendingBlocked = false,
|
||||||
|
pendingCount = 0,
|
||||||
}: PaymentFormProps) {
|
}: PaymentFormProps) {
|
||||||
const [amount, setAmount] = useState<number | ''>('');
|
const [amount, setAmount] = useState<number | ''>('');
|
||||||
const [paymentType, setPaymentType] = useState(enabledPaymentTypes[0] || 'alipay');
|
const [paymentType, setPaymentType] = useState(enabledPaymentTypes[0] || 'alipay');
|
||||||
@@ -321,12 +325,26 @@ export default function PaymentForm({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Pending order limit warning */}
|
||||||
|
{pendingBlocked && (
|
||||||
|
<div
|
||||||
|
className={[
|
||||||
|
'rounded-lg border p-3 text-sm',
|
||||||
|
dark
|
||||||
|
? 'border-amber-700 bg-amber-900/30 text-amber-300'
|
||||||
|
: 'border-amber-200 bg-amber-50 text-amber-700',
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
您有 {pendingCount} 个待支付订单,请先完成或取消后再充值
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Submit */}
|
{/* Submit */}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!isValid || loading}
|
disabled={!isValid || loading || pendingBlocked}
|
||||||
className={`w-full rounded-lg py-3 text-center font-medium text-white transition-colors ${
|
className={`w-full rounded-lg py-3 text-center font-medium text-white transition-colors ${
|
||||||
isValid && !loading
|
isValid && !loading && !pendingBlocked
|
||||||
? getPaymentMeta(effectivePaymentType).buttonClass
|
? getPaymentMeta(effectivePaymentType).buttonClass
|
||||||
: dark
|
: dark
|
||||||
? 'cursor-not-allowed bg-slate-700 text-slate-300'
|
? 'cursor-not-allowed bg-slate-700 text-slate-300'
|
||||||
@@ -335,6 +353,8 @@ export default function PaymentForm({
|
|||||||
>
|
>
|
||||||
{loading
|
{loading
|
||||||
? '处理中...'
|
? '处理中...'
|
||||||
|
: pendingBlocked
|
||||||
|
? '待支付订单过多'
|
||||||
: `立即充值 ¥${(feeRate > 0 && selectedAmount > 0 ? payAmount : selectedAmount || 0).toFixed(2)}`}
|
: `立即充值 ¥${(feeRate > 0 && selectedAmount > 0 ? payAmount : selectedAmount || 0).toFixed(2)}`}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in New Issue
Block a user