添加密码登录&调整接口数据展示&配置底部导航跳转
This commit is contained in:
@@ -16,12 +16,13 @@ export async function login(props: {
|
||||
username: string
|
||||
password: string
|
||||
remember: boolean
|
||||
mode: 'phone_code' | 'password'
|
||||
}): Promise<ApiResponse> {
|
||||
// 尝试登录
|
||||
const result = await callByDevice<TokenResp>('/api/auth/token', {
|
||||
...props,
|
||||
grant_type: 'password',
|
||||
login_type: 'phone_code',
|
||||
login_type: props.mode,
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
|
||||
@@ -4,32 +4,32 @@ import {ApiResponse, ExtraResp} from '@/lib/api'
|
||||
import {callByUser} from './base'
|
||||
import {listAnnouncements} from './announcement'
|
||||
|
||||
type listAccountReq = {
|
||||
type statisticsResourceUsageReq = {
|
||||
resource_no?: string
|
||||
create_after?: Date
|
||||
create_before?: Date
|
||||
}
|
||||
|
||||
type listAccountResp = {
|
||||
type statisticsResourceUsageResp = {
|
||||
date: string
|
||||
count: number
|
||||
}[]
|
||||
|
||||
export async function listAccount(props: listAccountReq) {
|
||||
return await callByUser<listAccountResp>('/api/resource/statistics/usage', props)
|
||||
export async function statisticsResourceUsage(props: statisticsResourceUsageReq) {
|
||||
return await callByUser<statisticsResourceUsageResp>('/api/resource/statistics/usage', props)
|
||||
}
|
||||
|
||||
export async function statisticsResourceFree() {
|
||||
return await callByUser<{
|
||||
long: {
|
||||
ResourceCount: number
|
||||
ResourceDailyFreeSum: number
|
||||
ResourceQuotaSum: number
|
||||
resource_count: number
|
||||
resource_daily_free_sum: number
|
||||
resource_quota_sum: number
|
||||
}
|
||||
short: {
|
||||
ResourceCount: number
|
||||
ResourceDailyFreeSum: number
|
||||
ResourceQuotaSum: number
|
||||
resource_count: number
|
||||
resource_daily_free_sum: number
|
||||
resource_quota_sum: number
|
||||
}
|
||||
}>('/api/resource/statistics/free')
|
||||
}
|
||||
@@ -37,7 +37,7 @@ export async function statisticsResourceFree() {
|
||||
type listInitializationResp = {
|
||||
anno: ExtraResp<typeof listAnnouncements>
|
||||
free: ExtraResp<typeof statisticsResourceFree>
|
||||
usage: ExtraResp<typeof listAccount>
|
||||
usage: ExtraResp<typeof statisticsResourceUsage>
|
||||
}
|
||||
export async function listInitialization(): Promise<ApiResponse<listInitializationResp>> {
|
||||
const free = await statisticsResourceFree()
|
||||
@@ -59,7 +59,7 @@ export async function listInitialization(): Promise<ApiResponse<listInitializati
|
||||
message: '公告数据获取失败',
|
||||
}
|
||||
}
|
||||
const usage = await listAccount({
|
||||
const usage = await statisticsResourceUsage({
|
||||
create_after: new Date(),
|
||||
create_before: new Date(),
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import {useState, useCallback, useRef, useContext} from 'react'
|
||||
import {useState, useCallback, useRef} from 'react'
|
||||
import {Input} from '@/components/ui/input'
|
||||
import {Button} from '@/components/ui/button'
|
||||
import {Checkbox} from '@/components/ui/checkbox'
|
||||
@@ -32,24 +32,31 @@ import Link from 'next/link'
|
||||
|
||||
export type LoginPageProps = {}
|
||||
|
||||
// 定义表单验证模式
|
||||
const formSchema = zod.object({
|
||||
const smsSchema = zod.object({
|
||||
username: zod.string().min(11, '请输入正确的手机号码').max(11, '请输入正确的手机号码'),
|
||||
password: zod.string().min(1, '请输入验证码'),
|
||||
remember: zod.boolean().default(false),
|
||||
})
|
||||
|
||||
type FormValues = zod.infer<typeof formSchema>
|
||||
const pwdSchema = zod.object({
|
||||
username: zod.string().min(11, '请输入正确的手机号码').max(11, '请输入正确的手机号码'),
|
||||
password: zod.string().min(6, '请输入至少6位密码'),
|
||||
remember: zod.boolean().default(false),
|
||||
})
|
||||
|
||||
type FormValues = zod.infer<typeof smsSchema>
|
||||
|
||||
export default function LoginPage(props: LoginPageProps) {
|
||||
const router = useRouter()
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [countdown, setCountdown] = useState(0)
|
||||
const [showCaptcha, setShowCaptcha] = useState(false)
|
||||
const [loginMode, setLoginMode] = useState<'sms' | 'password'>('sms')
|
||||
const [showPwd, setShowPwd] = useState(false)
|
||||
const timerRef = useRef<NodeJS.Timeout>(undefined)
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
resolver: zodResolver(loginMode === 'sms' ? smsSchema : pwdSchema),
|
||||
defaultValues: {
|
||||
username: '',
|
||||
password: '',
|
||||
@@ -103,7 +110,6 @@ export default function LoginPage(props: LoginPageProps) {
|
||||
toast.error(resp.message)
|
||||
return true
|
||||
}
|
||||
|
||||
setShowCaptcha(false)
|
||||
waiting = parseInt(resp.message)
|
||||
console.log(resp.message)
|
||||
@@ -132,12 +138,30 @@ export default function LoginPage(props: LoginPageProps) {
|
||||
return prev - 1
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
return false
|
||||
}, [username])
|
||||
|
||||
// 处理表单提交
|
||||
const onSubmit = async (values: FormValues) => {
|
||||
// 密码登录时增加更严格的校验
|
||||
if (loginMode === 'password') {
|
||||
const pwd = values.password || ''
|
||||
// 至少6位,包含字母和数字
|
||||
if (pwd.length < 6) {
|
||||
form.setError('password', {
|
||||
type: 'manual',
|
||||
message: '密码长度至少6位',
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!/[A-Za-z]/.test(pwd) || !/[0-9]/.test(pwd)) {
|
||||
form.setError('password', {
|
||||
type: 'manual',
|
||||
message: '密码需包含字母和数字',
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
try {
|
||||
setSubmitting(true)
|
||||
|
||||
@@ -153,7 +177,7 @@ export default function LoginPage(props: LoginPageProps) {
|
||||
if (!values.password) {
|
||||
form.setError('password', {
|
||||
type: 'manual',
|
||||
message: '请输入验证码',
|
||||
message: loginMode === 'sms' ? '请输入验证码' : '请输入密码',
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -163,11 +187,12 @@ export default function LoginPage(props: LoginPageProps) {
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
remember: values.remember,
|
||||
mode: loginMode === 'sms' ? 'phone_code' : 'password', // 后端区分登录方式
|
||||
})
|
||||
|
||||
// 登录失败
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || '请检查手机号码和验证码是否正确')
|
||||
throw new Error(result.message || '请检查账号和密码/验证码是否正确')
|
||||
}
|
||||
|
||||
// 登录成功
|
||||
@@ -193,7 +218,6 @@ export default function LoginPage(props: LoginPageProps) {
|
||||
|
||||
const params = useSearchParams()
|
||||
const redirect = params.get('redirect')
|
||||
|
||||
const refreshProfile = useProfileStore(store => store.refreshProfile)
|
||||
|
||||
// ======================
|
||||
@@ -207,7 +231,6 @@ export default function LoginPage(props: LoginPageProps) {
|
||||
`flex justify-center xl:justify-end items-center`,
|
||||
)}>
|
||||
<Image src={bg} alt="背景图" fill priority className="absolute -z-20 object-cover"/>
|
||||
|
||||
<Link href="/">
|
||||
<Image src={logo} alt="logo" priority height={64} className="absolute top-8 left-8 -z-10"/>
|
||||
</Link>
|
||||
@@ -217,8 +240,32 @@ export default function LoginPage(props: LoginPageProps) {
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-2xl">登录/注册</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="px-8">
|
||||
{/* 登录方式切换 */}
|
||||
<Card className="mb-6">
|
||||
<CardContent className="flex justify-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
theme={loginMode === 'sms' ? 'gradient' : 'outline'}
|
||||
onClick={() => {
|
||||
setLoginMode('sms')
|
||||
form.reset({username: form.getValues('username'), password: '', remember: false})
|
||||
}}
|
||||
>
|
||||
验证码登录
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
theme={loginMode === 'password' ? 'gradient' : 'outline'}
|
||||
onClick={() => {
|
||||
setLoginMode('password')
|
||||
form.reset({username: form.getValues('username'), password: '', remember: false})
|
||||
}}
|
||||
>
|
||||
密码登录
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Form<FormValues> className="space-y-6" onSubmit={onSubmit} form={form}>
|
||||
<FormField name="username" label="手机号码">
|
||||
{({id, field}) => (
|
||||
@@ -231,29 +278,65 @@ export default function LoginPage(props: LoginPageProps) {
|
||||
/>
|
||||
)}
|
||||
</FormField>
|
||||
|
||||
<FormField name="password" label="验证码">
|
||||
{({id, field}) => (
|
||||
<div className="flex space-x-4">
|
||||
<Input
|
||||
{...field}
|
||||
id={id}
|
||||
className="h-12"
|
||||
placeholder="请输入验证码"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
className="whitespace-nowrap h-12"
|
||||
onClick={checkUsername}
|
||||
disabled={countdown > 0}
|
||||
>
|
||||
{countdown > 0 ? `${countdown}秒后重发` : '获取验证码'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<FormField name="password" label={loginMode === 'sms' ? '验证码' : '密码'}>
|
||||
{({id, field}) =>
|
||||
loginMode === 'sms' ? (
|
||||
<div className="flex space-x-4">
|
||||
<Input
|
||||
{...field}
|
||||
id={id}
|
||||
className="h-12"
|
||||
placeholder="请输入验证码"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
className="whitespace-nowrap h-12"
|
||||
onClick={checkUsername}
|
||||
disabled={countdown > 0}
|
||||
>
|
||||
{countdown > 0 ? `${countdown}秒后重发` : '获取验证码'}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative">
|
||||
<Input
|
||||
{...field}
|
||||
id={id}
|
||||
type={showPwd ? 'text' : 'password'}
|
||||
className="h-12 pr-10"
|
||||
placeholder="至少6位密码,需包含字母和数字"
|
||||
autoComplete="current-password"
|
||||
minLength={6}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
onClick={() => setShowPwd(v => !v)}
|
||||
aria-label={showPwd ? '隐藏密码' : '显示密码'}
|
||||
>
|
||||
{showPwd ? (
|
||||
// 眼睛开 icon
|
||||
<svg width="20" height="20" fill="none" viewBox="0 0 20 20">
|
||||
<path stroke="currentColor" strokeWidth="1.5" d="M1.667 10S4.167 4.167 10 4.167 18.333 10 18.333 10 15.833 15.833 10 15.833 1.667 10 1.667 10Z"/>
|
||||
<circle cx="10" cy="10" r="2.5" stroke="currentColor" strokeWidth="1.5"/>
|
||||
</svg>
|
||||
) : (
|
||||
// 眼睛关 icon
|
||||
<>
|
||||
<svg width="20" height="20" fill="none" viewBox="0 0 20 20">
|
||||
<path stroke="currentColor" strokeWidth="1.5" d="M1.667 10S4.167 4.167 10 4.167c1.57 0 2.97.33 4.13.87M18.333 10s-2.5 5.833-8.333 5.833c-1.57 0-2.97-.33-4.13-.87"/>
|
||||
<circle cx="10" cy="10" r="2.5" stroke="currentColor" strokeWidth="1.5"/>
|
||||
<path stroke="currentColor" strokeWidth="1.5" d="M3 3l14 14"/>
|
||||
</svg>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</FormField>
|
||||
|
||||
<FormField name="remember">
|
||||
{({id, field}) => (
|
||||
<div className="flex flex-row items-start space-x-2 space-y-0">
|
||||
@@ -268,7 +351,6 @@ export default function LoginPage(props: LoginPageProps) {
|
||||
</div>
|
||||
)}
|
||||
</FormField>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button
|
||||
className="w-full h-12 text-lg"
|
||||
@@ -276,9 +358,8 @@ export default function LoginPage(props: LoginPageProps) {
|
||||
theme="gradient"
|
||||
disabled={submitting}
|
||||
>
|
||||
{submitting ? '登录中...' : '注册 / 登录'}
|
||||
{submitting ? '登录中...' : (loginMode === 'sms' ? '注册 / 登录' : '密码登录')}
|
||||
</Button>
|
||||
|
||||
<p className="text-xs text-center text-gray-500">
|
||||
登录即表示您同意
|
||||
<a href="#" className="text-blue-600 hover:text-blue-500">《用户协议》</a>
|
||||
@@ -289,14 +370,14 @@ export default function LoginPage(props: LoginPageProps) {
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 图形验证码弹窗 */}
|
||||
<Captcha
|
||||
showCaptcha={showCaptcha}
|
||||
setShowCaptcha={setShowCaptcha}
|
||||
handleSendCode={sendCode}
|
||||
/>
|
||||
|
||||
{loginMode === 'sms' && (
|
||||
<Captcha
|
||||
showCaptcha={showCaptcha}
|
||||
setShowCaptcha={setShowCaptcha}
|
||||
handleSendCode={sendCode}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export default function Footer(props: FooterProps) {
|
||||
<SiteNavList
|
||||
title="使用案例"
|
||||
items={[
|
||||
{name: `数据抓取`, href: `#`},
|
||||
{name: `数据抓取`, href: `/data-capture`},
|
||||
{name: `媒体矩阵`, href: `#`},
|
||||
{name: `广告验证`, href: `#`},
|
||||
{name: `价格监控`, href: `#`},
|
||||
|
||||
@@ -12,7 +12,7 @@ import zod from 'zod'
|
||||
import {merge} from '@/lib/utils'
|
||||
import {Button} from '@/components/ui/button'
|
||||
import {useState} from 'react'
|
||||
import {listAccount} from '@/actions/dashboard'
|
||||
import {statisticsResourceUsage} from '@/actions/dashboard'
|
||||
import {ExtraResp} from '@/lib/api'
|
||||
import {toast} from 'sonner'
|
||||
import {addDays, format} from 'date-fns'
|
||||
@@ -20,19 +20,13 @@ import {Label} from '@/components/ui/label'
|
||||
import {ChartConfig, ChartContainer} from '@/components/ui/chart'
|
||||
import {CartesianGrid, XAxis, YAxis, Tooltip, Area, AreaChart, Legend} from 'recharts'
|
||||
|
||||
type ChartDataItem = {
|
||||
date: string
|
||||
count: number
|
||||
count2?: number
|
||||
}
|
||||
|
||||
type ChartsProps = {
|
||||
initialData?: ExtraResp<typeof listAccount>
|
||||
initialData?: ExtraResp<typeof statisticsResourceUsage>
|
||||
}
|
||||
|
||||
export default function Charts({initialData}: ChartsProps) {
|
||||
// const [submittedData, setSubmittedData] = useState<ExtraReq<typeof listAccount>>()
|
||||
const [submittedData, setSubmittedData] = useState<ExtraResp<typeof listAccount>>(initialData || [])
|
||||
const [submittedData, setSubmittedData] = useState<ExtraResp<typeof statisticsResourceUsage>>(initialData || [])
|
||||
const formSchema = zod.object({
|
||||
resource_no: zod.string().optional(),
|
||||
create_after: zod.date().optional(),
|
||||
@@ -58,7 +52,7 @@ export default function Charts({initialData}: ChartsProps) {
|
||||
create_before: value.create_before ?? today,
|
||||
}
|
||||
|
||||
const resp = await listAccount(res)
|
||||
const resp = await statisticsResourceUsage(res)
|
||||
if (!resp.success) {
|
||||
toast.error('接口请求失败:' + resp.message)
|
||||
return
|
||||
@@ -149,7 +143,7 @@ const config = {
|
||||
} satisfies ChartConfig
|
||||
|
||||
type DashboardChartProps = {
|
||||
data: ExtraResp<typeof listAccount>
|
||||
data: ExtraResp<typeof statisticsResourceUsage>
|
||||
}
|
||||
|
||||
function DashboardChart(props: DashboardChartProps) {
|
||||
|
||||
@@ -9,7 +9,7 @@ import Charts from './_client/charts'
|
||||
import UserCenter from './_client/userCenter'
|
||||
import soon from './_assets/coming-soon.svg'
|
||||
import mask from './_assets/Mask group.webp'
|
||||
import {Button} from '@/components/ui/button'
|
||||
import {ExtraResp} from '@/lib/api'
|
||||
|
||||
export type DashboardPageProps = {}
|
||||
|
||||
@@ -43,12 +43,7 @@ export default async function DashboardPage(props: DashboardPageProps) {
|
||||
|
||||
{/* 磁贴集 */}
|
||||
{initData && (
|
||||
<Pins
|
||||
short_term={String(initData.free.short.ResourceCount)}
|
||||
short_term_monthly={String(initData.free.short.ResourceQuotaSum)}
|
||||
long_term={String(initData.free.long.ResourceCount)}
|
||||
long_term_monthly={String(initData.free.long.ResourceDailyFreeSum)}
|
||||
/>
|
||||
<Pins {...initData.free}/>
|
||||
)}
|
||||
|
||||
{/* 图表 */}
|
||||
@@ -69,13 +64,9 @@ export default async function DashboardPage(props: DashboardPageProps) {
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
type DashboardChartProps = {
|
||||
short_term: string
|
||||
short_term_monthly: string
|
||||
long_term: string
|
||||
long_term_monthly: string
|
||||
}
|
||||
function Pins(props: DashboardChartProps) {
|
||||
type DashboardPinsProps = ExtraResp<typeof listInitialization>['free']
|
||||
|
||||
function Pins(props: DashboardPinsProps) {
|
||||
return (
|
||||
<div className="flex md:row-start-2 md:col-start-1 md:col-span-3 gap-4 max-md:flex-col">
|
||||
{/* 短效 */}
|
||||
@@ -91,7 +82,7 @@ function Pins(props: DashboardChartProps) {
|
||||
<h4>包时</h4>
|
||||
<p className="flex flex-col items-end">
|
||||
<span className="text-sm text-weak">当日可提取数量</span>
|
||||
<span className="text-sm">{props.short_term}</span>
|
||||
<span className="text-sm">{props.short.resource_daily_free_sum}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="border-b"></div>
|
||||
@@ -99,7 +90,7 @@ function Pins(props: DashboardChartProps) {
|
||||
<h4 className="text-balance">包量</h4>
|
||||
<p className="flex flex-col items-end">
|
||||
<span className="text-sm text-weak">剩余可提取数量</span>
|
||||
<span className="text-sm">{props.short_term_monthly}</span>
|
||||
<span className="text-sm">{props.short.resource_quota_sum}</span>
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -118,7 +109,7 @@ function Pins(props: DashboardChartProps) {
|
||||
<h4 className="text-balance">包时</h4>
|
||||
<p className="flex flex-col items-end">
|
||||
<span className="text-sm text-weak" >当日可提取数量</span>
|
||||
<span className="text-sm">{props.long_term}</span>
|
||||
<span className="text-sm">{props.long.resource_daily_free_sum}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="border-b"></div>
|
||||
@@ -126,7 +117,7 @@ function Pins(props: DashboardChartProps) {
|
||||
<h4 className="text-balance">包量</h4>
|
||||
<p className="flex flex-col items-end">
|
||||
<span className="text-sm text-weak">剩余可提取数量</span>
|
||||
<span className="text-sm">{props.long_term_monthly}</span>
|
||||
<span className="text-sm">{props.long.resource_quota_sum}</span>
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -324,7 +324,16 @@ function PasswordForm(props: {
|
||||
type Schema = z.infer<typeof schema>
|
||||
|
||||
const form = useForm<Schema>({
|
||||
resolver: zodResolver(schema),
|
||||
resolver: zodResolver(
|
||||
schema.refine(
|
||||
data =>
|
||||
/^(?=.*[a-z])(?=.*[A-Z]).{6,}$/.test(data.password),
|
||||
{
|
||||
message: '密码需包含大小写字母,且不少于6位',
|
||||
path: ['password'],
|
||||
},
|
||||
),
|
||||
),
|
||||
defaultValues: {
|
||||
phone: '',
|
||||
captcha: '',
|
||||
@@ -333,6 +342,7 @@ function PasswordForm(props: {
|
||||
confirm_password: '',
|
||||
},
|
||||
})
|
||||
const router = useRouter()
|
||||
const handler = form.handleSubmit(async (value) => {
|
||||
try {
|
||||
const resp = await updatePassword({
|
||||
@@ -344,8 +354,10 @@ function PasswordForm(props: {
|
||||
throw new Error(resp.message)
|
||||
}
|
||||
|
||||
toast.success(`保存成功`)
|
||||
toast.success(`保存成功,请重新登录`)
|
||||
setOpen(false)
|
||||
// 立即跳转到登录页
|
||||
router.replace('/login')
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
|
||||
@@ -230,14 +230,14 @@ export default function WhitelistPage(props: WhitelistPageProps) {
|
||||
<Plus/>
|
||||
添加白名单
|
||||
</Button>
|
||||
<Button
|
||||
{/* <Button
|
||||
theme="fail"
|
||||
className="ml-2"
|
||||
disabled={selection.size === 0 || wait}
|
||||
onClick={() => confirmRemove()}>
|
||||
<Trash2/>
|
||||
删除选中
|
||||
</Button>
|
||||
</Button> */}
|
||||
</section>
|
||||
|
||||
{/* 数据表 */}
|
||||
|
||||
@@ -276,7 +276,11 @@ const FormFields = memo(() => {
|
||||
{...field}
|
||||
id={id}
|
||||
type="number"
|
||||
onChange={e => field.onChange(Number(e.target.value))}
|
||||
min={1}
|
||||
onChange={(e) => {
|
||||
const value = Math.max(1, Number(e.target.value))
|
||||
field.onChange(value)
|
||||
}}
|
||||
className="h-10"
|
||||
placeholder="输入提取数量"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user