推送远程分支前的排查和修复代码
This commit is contained in:
@@ -32,6 +32,7 @@ const eslintConfig = defineConfig([
|
|||||||
'@stylistic/block-spacing': 'off',
|
'@stylistic/block-spacing': 'off',
|
||||||
'@typescript-eslint/no-empty-object-type': 'off',
|
'@typescript-eslint/no-empty-object-type': 'off',
|
||||||
'@typescript-eslint/no-unused-vars': 'off',
|
'@typescript-eslint/no-unused-vars': 'off',
|
||||||
|
'@react-hooks/set-state-in-effect': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|||||||
8
src/actions/batch.ts
Normal file
8
src/actions/batch.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
'use server'
|
||||||
|
import {PageRecord} from '@/lib/api'
|
||||||
|
import {Batch} from '@/lib/models/batch'
|
||||||
|
import {callByUser} from './base'
|
||||||
|
|
||||||
|
export async function pageBatch(props: {page: number, size: number}) {
|
||||||
|
return callByUser<PageRecord<Batch>>('/api/batch/page', props)
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import {callByUser, callPublic} from '@/actions/base'
|
import {callByUser, callPublic} from '@/actions/base'
|
||||||
import {Channel} from '@/lib/models'
|
import {Channel} from '@/lib/models'
|
||||||
import {PageRecord} from '@/lib/api'
|
import {PageRecord} from '@/lib/api'
|
||||||
import {BatchRecord} from '@/lib/models/batch'
|
import {Batch} from '@/lib/models/batch'
|
||||||
|
|
||||||
export async function listChannels(props: {
|
export async function listChannels(props: {
|
||||||
page: number
|
page: number
|
||||||
@@ -16,10 +16,6 @@ export async function listChannels(props: {
|
|||||||
return callByUser<PageRecord<Channel>>('/api/channel/list', props)
|
return callByUser<PageRecord<Channel>>('/api/channel/list', props)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function extractRecord(props: {page: number, size: number}) {
|
|
||||||
return callByUser<PageRecord<BatchRecord>>('/api/batch/page', props)
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateChannelsResp = {
|
type CreateChannelsResp = {
|
||||||
host: string
|
host: string
|
||||||
port: string
|
port: string
|
||||||
|
|||||||
@@ -87,5 +87,9 @@ export async function payClose(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getPrice(props: CreateResourceReq) {
|
export async function getPrice(props: CreateResourceReq) {
|
||||||
return callByDevice<{price: string}>('/api/resource/price', props)
|
return callByDevice<{
|
||||||
|
price: string
|
||||||
|
discounted_price?: string
|
||||||
|
discounted?: number
|
||||||
|
}>('/api/resource/price', props)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export default function Footer(props: FooterProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-none mt-6 pt-6 border-t border-gray-700 flex flex-col text-gray-300">
|
<div className="flex-none mt-6 pt-6 border-t border-gray-700 flex text-center flex-col text-gray-300">
|
||||||
<p className="text-xs">
|
<p className="text-xs">
|
||||||
蓝狐代理仅提供IP服务,用户使用蓝狐代理IP从事的任何行为均不代表蓝狐代理IP的意志和观点,与蓝狐代理的立场无关。
|
蓝狐代理仅提供IP服务,用户使用蓝狐代理IP从事的任何行为均不代表蓝狐代理IP的意志和观点,与蓝狐代理的立场无关。
|
||||||
<br/>
|
<br/>
|
||||||
|
|||||||
22
src/app/admin/_components/addr.tsx
Normal file
22
src/app/admin/_components/addr.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import {Badge} from '@/components/ui/badge'
|
||||||
|
import {Channel} from '@/lib/models'
|
||||||
|
import {isBefore} from 'date-fns'
|
||||||
|
|
||||||
|
export default function Addr({channel}: {
|
||||||
|
channel: Channel
|
||||||
|
}) {
|
||||||
|
const ip = channel.host
|
||||||
|
const port = channel.port
|
||||||
|
const expired = isBefore(channel.expired_at, new Date())
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${expired ? 'text-weak' : ''}`}>
|
||||||
|
<span>{ip}:{port}</span>
|
||||||
|
{expired && (
|
||||||
|
<Badge variant="secondary">
|
||||||
|
已过期
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -12,5 +12,5 @@ export type ChannelsLayoutProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function ChannelsLayout(props: ChannelsLayoutProps) {
|
export default async function ChannelsLayout(props: ChannelsLayoutProps) {
|
||||||
return <Suspense>{props.children}</Suspense>
|
return props.children
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import {useCallback, useEffect, useState} from 'react'
|
import {Suspense, useCallback, useEffect, useState} from 'react'
|
||||||
import {useStatus} from '@/lib/states'
|
import {useStatus} from '@/lib/states'
|
||||||
import {PageRecord} from '@/lib/api'
|
import {PageRecord} from '@/lib/api'
|
||||||
import {Channel} from '@/lib/models'
|
import {Channel} from '@/lib/models'
|
||||||
@@ -17,6 +17,7 @@ import {Button} from '@/components/ui/button'
|
|||||||
import {EraserIcon, SearchIcon} from 'lucide-react'
|
import {EraserIcon, SearchIcon} from 'lucide-react'
|
||||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/components/ui/select'
|
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/components/ui/select'
|
||||||
import {Badge} from '@/components/ui/badge'
|
import {Badge} from '@/components/ui/badge'
|
||||||
|
import Addr from '../_components/addr'
|
||||||
export type ChannelsPageProps = {}
|
export type ChannelsPageProps = {}
|
||||||
|
|
||||||
export default function ChannelsPage(props: ChannelsPageProps) {
|
export default function ChannelsPage(props: ChannelsPageProps) {
|
||||||
@@ -50,11 +51,6 @@ export default function ChannelsPage(props: ChannelsPageProps) {
|
|||||||
expire_before: undefined,
|
expire_before: undefined,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
// 检查是否过期
|
|
||||||
const isExpired = (expiredAt: string | Date) => {
|
|
||||||
const date = typeof expiredAt === 'string' ? new Date(expiredAt) : expiredAt
|
|
||||||
return isBefore(date, new Date())
|
|
||||||
}
|
|
||||||
|
|
||||||
const refresh = useCallback(async (page: number, size: number) => {
|
const refresh = useCallback(async (page: number, size: number) => {
|
||||||
try {
|
try {
|
||||||
@@ -63,12 +59,8 @@ export default function ChannelsPage(props: ChannelsPageProps) {
|
|||||||
// 筛选条件
|
// 筛选条件
|
||||||
const filter = filterForm.getValues()
|
const filter = filterForm.getValues()
|
||||||
const auth_type = filter.auth_type ? parseInt(filter.auth_type) : undefined
|
const auth_type = filter.auth_type ? parseInt(filter.auth_type) : undefined
|
||||||
const expired_status = filter.expired_status
|
|
||||||
|
|
||||||
// 请求数据
|
// 请求数据
|
||||||
console.log({
|
|
||||||
page, size, ...filter, auth_type,
|
|
||||||
})
|
|
||||||
const resp = await listChannels({
|
const resp = await listChannels({
|
||||||
page, size, ...filter, auth_type,
|
page, size, ...filter, auth_type,
|
||||||
})
|
})
|
||||||
@@ -77,20 +69,8 @@ export default function ChannelsPage(props: ChannelsPageProps) {
|
|||||||
throw new Error(resp.message)
|
throw new Error(resp.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
let filteredList = resp.data.list
|
|
||||||
if (expired_status !== undefined && expired_status !== 'all') {
|
|
||||||
filteredList = resp.data.list.filter((channel) => {
|
|
||||||
const expired = isExpired(channel.expired_at)
|
|
||||||
return !expired
|
|
||||||
})
|
|
||||||
resp.data.total = filteredList.length
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新数据
|
// 更新数据
|
||||||
setData({
|
setData(resp.data)
|
||||||
...resp.data,
|
|
||||||
list: filteredList,
|
|
||||||
})
|
|
||||||
setStatus('done')
|
setStatus('done')
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
@@ -182,81 +162,67 @@ export default function ChannelsPage(props: ChannelsPageProps) {
|
|||||||
</Form>
|
</Form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<DataTable
|
<Suspense>
|
||||||
status={status}
|
<DataTable
|
||||||
data={data.list}
|
status={status}
|
||||||
pagination={{
|
data={data.list}
|
||||||
page: data.page,
|
pagination={{
|
||||||
size: data.size,
|
page: data.page,
|
||||||
total: data.total,
|
size: data.size,
|
||||||
onPageChange: page => refresh(page, data.size),
|
total: data.total,
|
||||||
onSizeChange: size => refresh(1, size),
|
onPageChange: page => refresh(page, data.size),
|
||||||
}}
|
onSizeChange: size => refresh(1, size),
|
||||||
columns={[
|
}}
|
||||||
{
|
columns={[
|
||||||
header: '代理地址',
|
{
|
||||||
cell: ({row}) => {
|
header: '代理地址',
|
||||||
const channel = row.original
|
cell: ({row}) => <Addr channel={row.original}/>,
|
||||||
const ip = channel.host
|
|
||||||
const port = channel.port
|
|
||||||
const expired = isExpired(channel.expired_at)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`${expired ? 'text-weak' : ''}`}>
|
|
||||||
<span>{ip}:{port}</span>
|
|
||||||
{expired && (
|
|
||||||
<Badge variant="secondary">
|
|
||||||
已过期
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
header: '认证方式',
|
||||||
header: '认证方式',
|
cell: ({row}) => {
|
||||||
cell: ({row}) => {
|
const channel = row.original
|
||||||
const channel = row.original
|
const hasWhitelist = channel.whitelists && channel.whitelists.trim() !== ''
|
||||||
const hasWhitelist = channel.whitelists && channel.whitelists.trim() !== ''
|
const hasAuth = channel.username && channel.password
|
||||||
const hasAuth = channel.username && channel.password
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1 min-w-0">
|
<div className="flex flex-col gap-1 min-w-0">
|
||||||
{hasWhitelist ? (
|
{hasWhitelist ? (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span >白名单</span>
|
<span >白名单</span>
|
||||||
<div className="flex flex-wrap gap-1 max-w-[200px]">
|
<div className="flex flex-wrap gap-1 max-w-[200px]">
|
||||||
{channel.whitelists.split(',').map((ip, index) => (
|
{channel.whitelists.split(',').map((ip, index) => (
|
||||||
<Badge key={index} variant="secondary">
|
<Badge key={index} variant="secondary">
|
||||||
{ip.trim()}
|
{ip.trim()}
|
||||||
</Badge >
|
</Badge >
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : hasAuth ? (
|
||||||
) : hasAuth ? (
|
<div className="flex flex-col">
|
||||||
<div className="flex flex-col">
|
<span>账号密码</span>
|
||||||
<span>账号密码</span>
|
<Badge variant="secondary">
|
||||||
<Badge variant="secondary">
|
{channel.username}:{channel.password}
|
||||||
{channel.username}:{channel.password}
|
</Badge >
|
||||||
</Badge >
|
</div>
|
||||||
</div>
|
) : (
|
||||||
) : (
|
<span className="text-sm text-gray-400">无认证</span>
|
||||||
<span className="text-sm text-gray-400">无认证</span>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
)
|
||||||
)
|
},
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
header: '提取时间',
|
||||||
header: '提取时间',
|
cell: ({row}) => format(row.original.created_at, 'yyyy-MM-dd HH:mm'),
|
||||||
cell: ({row}) => format(row.original.created_at, 'yyyy-MM-dd HH:mm'),
|
},
|
||||||
},
|
{
|
||||||
{
|
header: '过期时间',
|
||||||
header: '过期时间',
|
cell: ({row}) => format(row.original.expired_at, 'yyyy-MM-dd HH:mm:ss'),
|
||||||
cell: ({row}) => format(row.original.expired_at, 'yyyy-MM-dd HH:mm:ss'),
|
},
|
||||||
},
|
]}
|
||||||
]}
|
/>
|
||||||
/>
|
</Suspense>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import {PageRecord} from '@/lib/api'
|
|||||||
import Page from '@/components/page'
|
import Page from '@/components/page'
|
||||||
import DataTable from '@/components/data-table'
|
import DataTable from '@/components/data-table'
|
||||||
import {toast} from 'sonner'
|
import {toast} from 'sonner'
|
||||||
import {extractRecord} from '@/actions/channel'
|
import {Batch} from '@/lib/models/batch'
|
||||||
import {BatchRecord} from '@/lib/models/batch'
|
|
||||||
import {format} from 'date-fns'
|
import {format} from 'date-fns'
|
||||||
import {Form, FormField} from '@/components/ui/form'
|
import {Form, FormField} from '@/components/ui/form'
|
||||||
import {z} from 'zod'
|
import {z} from 'zod'
|
||||||
@@ -15,12 +14,13 @@ import {zodResolver} from '@hookform/resolvers/zod'
|
|||||||
import DatePicker from '@/components/date-picker'
|
import DatePicker from '@/components/date-picker'
|
||||||
import {Button} from '@/components/ui/button'
|
import {Button} from '@/components/ui/button'
|
||||||
import {EraserIcon, SearchIcon} from 'lucide-react'
|
import {EraserIcon, SearchIcon} from 'lucide-react'
|
||||||
|
import {pageBatch} from '@/actions/batch'
|
||||||
|
|
||||||
export type RecordPageProps = {}
|
export type RecordPageProps = {}
|
||||||
|
|
||||||
export default function RecordPage(props: RecordPageProps) {
|
export default function RecordPage(props: RecordPageProps) {
|
||||||
const [status, setStatus] = useStatus()
|
const [status, setStatus] = useStatus()
|
||||||
const [data, setData] = useState<PageRecord<BatchRecord>>({
|
const [data, setData] = useState<PageRecord<Batch>>({
|
||||||
page: 1,
|
page: 1,
|
||||||
size: 10,
|
size: 10,
|
||||||
total: 0,
|
total: 0,
|
||||||
@@ -50,9 +50,7 @@ export default function RecordPage(props: RecordPageProps) {
|
|||||||
setStatus('load')
|
setStatus('load')
|
||||||
// 获取筛选条件
|
// 获取筛选条件
|
||||||
const filter = filterForm.getValues()
|
const filter = filterForm.getValues()
|
||||||
console.log(filter, 'filter')
|
const result = await pageBatch({
|
||||||
|
|
||||||
const result = await extractRecord({
|
|
||||||
page,
|
page,
|
||||||
size,
|
size,
|
||||||
...filter,
|
...filter,
|
||||||
|
|||||||
@@ -18,12 +18,7 @@ import {useFormContext, useWatch} from 'react-hook-form'
|
|||||||
import {Schema} from '@/components/composites/purchase/long/form'
|
import {Schema} from '@/components/composites/purchase/long/form'
|
||||||
import {Card} from '@/components/ui/card'
|
import {Card} from '@/components/ui/card'
|
||||||
import {getPrice, CreateResourceReq} from '@/actions/resource'
|
import {getPrice, CreateResourceReq} from '@/actions/resource'
|
||||||
|
import {ExtraResp} from '@/lib/api'
|
||||||
interface PriceData {
|
|
||||||
price: string
|
|
||||||
discounted_price?: string
|
|
||||||
discounted?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Right() {
|
export default function Right() {
|
||||||
const {control} = useFormContext<Schema>()
|
const {control} = useFormContext<Schema>()
|
||||||
@@ -33,7 +28,7 @@ export default function Right() {
|
|||||||
const quota = useWatch({control, name: 'quota'})
|
const quota = useWatch({control, name: 'quota'})
|
||||||
const expire = useWatch({control, name: 'expire'})
|
const expire = useWatch({control, name: 'expire'})
|
||||||
const dailyLimit = useWatch({control, name: 'daily_limit'})
|
const dailyLimit = useWatch({control, name: 'daily_limit'})
|
||||||
const [priceData, setPriceData] = useState<PriceData>({
|
const [priceData, setPriceData] = useState<ExtraResp<typeof getPrice>>({
|
||||||
price: '0.00',
|
price: '0.00',
|
||||||
discounted_price: '0.00',
|
discounted_price: '0.00',
|
||||||
discounted: 0,
|
discounted: 0,
|
||||||
@@ -41,29 +36,27 @@ export default function Right() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const price = async () => {
|
const price = async () => {
|
||||||
const params: CreateResourceReq = {
|
|
||||||
type: 2,
|
|
||||||
long: {
|
|
||||||
live: Number(live),
|
|
||||||
mode: Number(mode),
|
|
||||||
quota: Number(mode) === 1 ? Number(dailyLimit) : Number(quota),
|
|
||||||
expire: Number(mode) === 1 ? Number(expire) : undefined,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const priceValue = await getPrice(params)
|
const resp = await getPrice({
|
||||||
|
type: 2,
|
||||||
if (priceValue.success && priceValue.data?.price) {
|
long: {
|
||||||
const data: PriceData = priceValue.data
|
live: Number(live),
|
||||||
setPriceData({
|
mode: Number(mode),
|
||||||
price: data.price,
|
quota: mode === '1' ? Number(dailyLimit) : Number(quota),
|
||||||
discounted_price: data.discounted_price ?? data.price ?? '',
|
expire: mode === '1' ? Number(expire) : undefined,
|
||||||
discounted: data.discounted,
|
},
|
||||||
})
|
})
|
||||||
|
if (!resp.success) {
|
||||||
|
throw new Error('获取价格失败')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setPriceData({
|
||||||
|
price: resp.data.price,
|
||||||
|
discounted_price: resp.data.discounted_price ?? resp.data.price ?? '',
|
||||||
|
discounted: resp.data.discounted,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error('获取价格失败:', error)
|
|
||||||
setPriceData({
|
setPriceData({
|
||||||
price: '0.00',
|
price: '0.00',
|
||||||
discounted_price: '0.00',
|
discounted_price: '0.00',
|
||||||
|
|||||||
@@ -17,12 +17,7 @@ import Pay from '@/components/composites/purchase/pay'
|
|||||||
import {useFormContext, useWatch} from 'react-hook-form'
|
import {useFormContext, useWatch} from 'react-hook-form'
|
||||||
import {Card} from '@/components/ui/card'
|
import {Card} from '@/components/ui/card'
|
||||||
import {CreateResourceReq, getPrice} from '@/actions/resource'
|
import {CreateResourceReq, getPrice} from '@/actions/resource'
|
||||||
|
import {ExtraResp} from '@/lib/api'
|
||||||
interface PriceData {
|
|
||||||
price: string
|
|
||||||
discounted_price?: string
|
|
||||||
discounted?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Right() {
|
export default function Right() {
|
||||||
const {control} = useFormContext<Schema>()
|
const {control} = useFormContext<Schema>()
|
||||||
@@ -32,7 +27,7 @@ export default function Right() {
|
|||||||
const expire = useWatch({control, name: 'expire'})
|
const expire = useWatch({control, name: 'expire'})
|
||||||
const quota = useWatch({control, name: 'quota'})
|
const quota = useWatch({control, name: 'quota'})
|
||||||
const dailyLimit = useWatch({control, name: 'daily_limit'})
|
const dailyLimit = useWatch({control, name: 'daily_limit'})
|
||||||
const [priceData, setPriceData] = useState<PriceData>({
|
const [priceData, setPriceData] = useState<ExtraResp<typeof getPrice>>({
|
||||||
price: '0.00',
|
price: '0.00',
|
||||||
discounted_price: '0.00',
|
discounted_price: '0.00',
|
||||||
discounted: 0,
|
discounted: 0,
|
||||||
@@ -40,26 +35,26 @@ export default function Right() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const price = async () => {
|
const price = async () => {
|
||||||
const params: CreateResourceReq = {
|
|
||||||
type: 1,
|
|
||||||
short: {
|
|
||||||
live: Number(live),
|
|
||||||
mode: Number(mode),
|
|
||||||
quota: Number(mode) === 1 ? Number(dailyLimit) : Number(quota),
|
|
||||||
expire: Number(mode) === 1 ? Number(expire) : undefined,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const priceResponse = await getPrice(params)
|
const priceResponse = await getPrice({
|
||||||
|
type: 1,
|
||||||
if (priceResponse.success && priceResponse.data) {
|
short: {
|
||||||
const data: PriceData = priceResponse.data
|
live: Number(live),
|
||||||
setPriceData({
|
mode: Number(mode),
|
||||||
price: data.price,
|
quota: mode === '1' ? Number(dailyLimit) : Number(quota),
|
||||||
discounted_price: data.discounted_price ?? data.price ?? '',
|
expire: mode === '1' ? Number(expire) : undefined,
|
||||||
discounted: data.discounted,
|
},
|
||||||
})
|
})
|
||||||
|
if (!priceResponse.success) {
|
||||||
|
throw new Error('获取价格失败')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = priceResponse.data
|
||||||
|
setPriceData({
|
||||||
|
price: data.price,
|
||||||
|
discounted_price: data.discounted_price ?? data.price ?? '',
|
||||||
|
discounted: data.discounted,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error('获取价格失败:', error)
|
console.error('获取价格失败:', error)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export type BatchRecord = {
|
export type Batch = {
|
||||||
batch_no: string
|
batch_no: string
|
||||||
ip: string
|
ip: string
|
||||||
id: number
|
id: number
|
||||||
|
|||||||
Reference in New Issue
Block a user