mirror of
https://gitee.com/devlive-community/datacap.git
synced 2024-11-30 11:07:41 +08:00
web: Support security for login
This commit is contained in:
parent
89f8e2fd55
commit
61c273f375
@ -11,7 +11,8 @@ public enum ServiceState
|
||||
USER_NOT_FOUND(4001, "User dose not exists"),
|
||||
USER_ROLE_NOT_FOUND(4002, "User role dose not exists"),
|
||||
USER_UNAUTHORIZED(4003, "Insufficient current user permissions"),
|
||||
USER_EXISTS(4004, "User exists");
|
||||
USER_EXISTS(4004, "User exists"),
|
||||
USER_BAD_CREDENTIALS(4005, "The account or password is incorrect");
|
||||
|
||||
private Integer code;
|
||||
private String value;
|
||||
|
@ -4,6 +4,7 @@ import io.edurt.datacap.server.common.JSON;
|
||||
import io.edurt.datacap.server.common.Response;
|
||||
import io.edurt.datacap.server.common.ServiceState;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.stereotype.Component;
|
||||
@ -24,6 +25,11 @@ public class JwtAuthEntryPoint
|
||||
throws IOException
|
||||
{
|
||||
log.error("Unauthorized error: {}", authException.getMessage());
|
||||
response.getWriter().print(JSON.toJSON(Response.failure(ServiceState.USER_UNAUTHORIZED)));
|
||||
if (authException instanceof BadCredentialsException) {
|
||||
response.getWriter().print(JSON.toJSON(Response.failure(ServiceState.USER_BAD_CREDENTIALS)));
|
||||
}
|
||||
else {
|
||||
response.getWriter().print(JSON.toJSON(Response.failure(ServiceState.USER_UNAUTHORIZED)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
3
web/console-fe/src/common/Common.ts
Normal file
3
web/console-fe/src/common/Common.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default {
|
||||
token: 'AuthToken'
|
||||
}
|
@ -1,27 +1,37 @@
|
||||
import { ResponseModel } from "@/model/ResponseModel";
|
||||
import { message } from "ant-design-vue";
|
||||
import {ResponseModel} from "@/model/ResponseModel";
|
||||
import {message} from "ant-design-vue";
|
||||
import axios from 'axios';
|
||||
import Common from "@/common/Common";
|
||||
|
||||
axios.defaults.headers.post['Access-Control-Allow-Origin'] = '*';
|
||||
|
||||
const configure = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
cancelToken: undefined
|
||||
}
|
||||
export class HttpCommon
|
||||
{
|
||||
private configure;
|
||||
|
||||
export class HttpCommon {
|
||||
constructor() {
|
||||
constructor()
|
||||
{
|
||||
if (process.env.NODE_ENV === 'development' ||
|
||||
window.location.hostname === 'localhost') {
|
||||
axios.defaults.baseURL = 'http://localhost:9096';
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
axios.defaults.baseURL = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port : '');
|
||||
}
|
||||
|
||||
const auth = JSON.parse(localStorage.getItem(Common.token) || '{}');
|
||||
this.configure = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': auth.type + ' ' + auth.token
|
||||
},
|
||||
cancelToken: undefined,
|
||||
params: undefined
|
||||
}
|
||||
}
|
||||
|
||||
handlerSuccessful(result: any): ResponseModel {
|
||||
handlerSuccessful(result: any): ResponseModel
|
||||
{
|
||||
const data = result.data;
|
||||
let message = data.message;
|
||||
if (data.message instanceof Array) {
|
||||
@ -40,7 +50,8 @@ export class HttpCommon {
|
||||
return response;
|
||||
}
|
||||
|
||||
handlerFailed(error: any): ResponseModel {
|
||||
handlerFailed(error: any): ResponseModel
|
||||
{
|
||||
const response: ResponseModel = {
|
||||
code: 0,
|
||||
message: error.message,
|
||||
@ -49,9 +60,11 @@ export class HttpCommon {
|
||||
return response;
|
||||
}
|
||||
|
||||
get(url: string, params?: any): Promise<ResponseModel> {
|
||||
get(url: string, params?: any): Promise<ResponseModel>
|
||||
{
|
||||
return new Promise((resolve) => {
|
||||
axios.get(url, { params: params })
|
||||
this.configure.params = params;
|
||||
axios.get(url, this.configure)
|
||||
.then(result => {
|
||||
resolve(this.handlerSuccessful(result));
|
||||
}, error => {
|
||||
@ -61,10 +74,11 @@ export class HttpCommon {
|
||||
});
|
||||
}
|
||||
|
||||
post(url: string, data = {}, cancelToken?: any): Promise<ResponseModel> {
|
||||
post(url: string, data = {}, cancelToken?: any): Promise<ResponseModel>
|
||||
{
|
||||
return new Promise((resolve) => {
|
||||
configure.cancelToken = cancelToken;
|
||||
axios.post(url, data, configure)
|
||||
this.configure.cancelToken = cancelToken;
|
||||
axios.post(url, data, this.configure)
|
||||
.then(result => {
|
||||
resolve(this.handlerSuccessful(result));
|
||||
}, error => {
|
||||
@ -74,9 +88,10 @@ export class HttpCommon {
|
||||
});
|
||||
}
|
||||
|
||||
put(url: string, data = {}): Promise<ResponseModel> {
|
||||
put(url: string, data = {}): Promise<ResponseModel>
|
||||
{
|
||||
return new Promise((resolve) => {
|
||||
axios.put(url, data, configure)
|
||||
axios.put(url, data, this.configure)
|
||||
.then(result => {
|
||||
resolve(this.handlerSuccessful(result));
|
||||
}, error => {
|
||||
@ -86,9 +101,10 @@ export class HttpCommon {
|
||||
});
|
||||
}
|
||||
|
||||
delete(url: string): Promise<ResponseModel> {
|
||||
delete(url: string): Promise<ResponseModel>
|
||||
{
|
||||
return new Promise((resolve) => {
|
||||
axios.delete(url)
|
||||
axios.delete(url, this.configure)
|
||||
.then(result => {
|
||||
resolve(this.handlerSuccessful(result));
|
||||
}, error => {
|
||||
@ -98,7 +114,8 @@ export class HttpCommon {
|
||||
});
|
||||
}
|
||||
|
||||
getAxios() {
|
||||
getAxios()
|
||||
{
|
||||
return axios;
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +0,0 @@
|
||||
const en = {
|
||||
common: {
|
||||
home: 'Home',
|
||||
query: 'Query',
|
||||
admin: 'Admin',
|
||||
source: 'Source',
|
||||
history: 'History'
|
||||
}
|
||||
}
|
||||
|
||||
export default en;
|
13
web/console-fe/src/i18n/langs/en/common.ts
Normal file
13
web/console-fe/src/i18n/langs/en/common.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export default {
|
||||
home: 'Home',
|
||||
query: 'Query',
|
||||
admin: 'Admin',
|
||||
source: 'DataSource',
|
||||
history: 'History',
|
||||
username: 'Username',
|
||||
password: 'Password',
|
||||
login: 'Login',
|
||||
logout: 'Logout',
|
||||
english: 'English',
|
||||
chinese: 'Chinese'
|
||||
}
|
9
web/console-fe/src/i18n/langs/en/index.ts
Normal file
9
web/console-fe/src/i18n/langs/en/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import enGB from 'ant-design-vue/es/locale/en_GB'
|
||||
import common from "@/i18n/langs/en/common";
|
||||
import required from "@/i18n/langs/en/required";
|
||||
|
||||
export default {
|
||||
...enGB,
|
||||
common: common,
|
||||
required: required
|
||||
}
|
4
web/console-fe/src/i18n/langs/en/required.ts
Normal file
4
web/console-fe/src/i18n/langs/en/required.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export default {
|
||||
username: 'Please input your username!',
|
||||
password: 'Please input your password!'
|
||||
}
|
@ -1,15 +1,7 @@
|
||||
import en from "@/i18n/langs/en";
|
||||
import zhCn from "@/i18n/langs/zhCn";
|
||||
import enGB from 'ant-design-vue/es/locale/en_GB';
|
||||
import zhCN from 'ant-design-vue/es/locale/zh_CN';
|
||||
import en from "@/i18n/langs/en/index";
|
||||
import zhCn from "@/i18n/langs/zhCn/index";
|
||||
|
||||
export default {
|
||||
en: {
|
||||
...en,
|
||||
...enGB,
|
||||
},
|
||||
zh_cn: {
|
||||
...zhCn,
|
||||
...zhCN,
|
||||
}
|
||||
en: en,
|
||||
zh_cn: zhCn
|
||||
}
|
||||
|
@ -1,11 +0,0 @@
|
||||
const zhCn = {
|
||||
common: {
|
||||
home: '主页',
|
||||
query: '查询',
|
||||
admin: '管理',
|
||||
source: '源',
|
||||
history: '历史'
|
||||
}
|
||||
}
|
||||
|
||||
export default zhCn;
|
13
web/console-fe/src/i18n/langs/zhCn/common.ts
Normal file
13
web/console-fe/src/i18n/langs/zhCn/common.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export default {
|
||||
home: '主页',
|
||||
query: '查询',
|
||||
admin: '管理',
|
||||
source: '数据源',
|
||||
history: '历史',
|
||||
username: '用户名',
|
||||
password: '密码',
|
||||
login: '登录',
|
||||
logout: '注销',
|
||||
english: '英语',
|
||||
chinese: '中文'
|
||||
}
|
9
web/console-fe/src/i18n/langs/zhCn/index.ts
Normal file
9
web/console-fe/src/i18n/langs/zhCn/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import zhCN from 'ant-design-vue/es/locale/zh_CN';
|
||||
import common from "@/i18n/langs/zhCn/common";
|
||||
import required from "@/i18n/langs/zhCn/required";
|
||||
|
||||
export default {
|
||||
...zhCN,
|
||||
common: common,
|
||||
required: required
|
||||
}
|
4
web/console-fe/src/i18n/langs/zhCn/required.ts
Normal file
4
web/console-fe/src/i18n/langs/zhCn/required.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export default {
|
||||
username: '请输入用户名!',
|
||||
password: '请输入密码!'
|
||||
}
|
8
web/console-fe/src/model/AuthResponse.ts
Normal file
8
web/console-fe/src/model/AuthResponse.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface AuthResponse
|
||||
{
|
||||
token: string;
|
||||
type: string;
|
||||
id: number;
|
||||
username: string;
|
||||
roles: [];
|
||||
}
|
8
web/console-fe/src/model/AuthUser.ts
Normal file
8
web/console-fe/src/model/AuthUser.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface AuthUser
|
||||
{
|
||||
username: string;
|
||||
password: string;
|
||||
// Marks the error message returned after an operation
|
||||
message?: string;
|
||||
loading?: boolean;
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
import LayoutContainer from "@/views/layout/Layout.vue";
|
||||
import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router";
|
||||
import {createRouter, createWebHashHistory, RouteRecordRaw} from "vue-router";
|
||||
|
||||
import NProgress from "nprogress";
|
||||
import "nprogress/nprogress.css";
|
||||
import Common from "@/common/Common";
|
||||
|
||||
NProgress.configure({
|
||||
easing: 'ease',
|
||||
@ -25,6 +26,9 @@ const routes: Array<RouteRecordRaw> = [
|
||||
name: "dashboard",
|
||||
redirect: "/dashboard/index",
|
||||
component: LayoutContainer,
|
||||
meta: {
|
||||
requireAuth: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "index",
|
||||
@ -37,6 +41,9 @@ const routes: Array<RouteRecordRaw> = [
|
||||
name: "console",
|
||||
redirect: "/console/index",
|
||||
component: LayoutContainer,
|
||||
meta: {
|
||||
requireAuth: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "index",
|
||||
@ -48,6 +55,9 @@ const routes: Array<RouteRecordRaw> = [
|
||||
path: "/admin",
|
||||
name: "admin",
|
||||
component: LayoutContainer,
|
||||
meta: {
|
||||
requireAuth: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "source",
|
||||
@ -69,6 +79,17 @@ const routes: Array<RouteRecordRaw> = [
|
||||
component: () => import("../views/common/NotFound.vue")
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: "/auth",
|
||||
name: "auth",
|
||||
children: [
|
||||
{
|
||||
name: "signin",
|
||||
path: "signin",
|
||||
component: () => import("../views/pages/auth/AuthLogin.vue")
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@ -77,13 +98,30 @@ const router = createRouter({
|
||||
routes
|
||||
});
|
||||
|
||||
const authRouter = '/auth/signin';
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
NProgress.start();
|
||||
if (to.matched.length === 0) {
|
||||
next({ name: "routerNotFound" })
|
||||
next({name: "routerNotFound"})
|
||||
}
|
||||
else {
|
||||
next();
|
||||
if (to.meta.requireAuth) {
|
||||
if (localStorage.getItem(Common.token)) {
|
||||
next();
|
||||
}
|
||||
else {
|
||||
next(authRouter);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (localStorage.getItem(Common.token) && to.path == authRouter) {
|
||||
next('/');
|
||||
}
|
||||
else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
13
web/console-fe/src/services/AuthService.ts
Normal file
13
web/console-fe/src/services/AuthService.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import {ResponseModel} from "@/model/ResponseModel";
|
||||
import {HttpCommon} from "@/common/HttpCommon";
|
||||
import {AuthUser} from "@/model/AuthUser";
|
||||
|
||||
const defaultAuth = "/api/auth";
|
||||
|
||||
export class AuthService
|
||||
{
|
||||
signin(configure: AuthUser): Promise<ResponseModel>
|
||||
{
|
||||
return new HttpCommon().post(defaultAuth + '/signin', configure);
|
||||
}
|
||||
}
|
@ -1,62 +1,100 @@
|
||||
<template>
|
||||
<a-menu theme="dark" mode="horizontal" :style="{ lineHeight: '64px' }">
|
||||
<a-menu-item>DataCap(incubator)</a-menu-item>
|
||||
<a-menu-item key="dashboard">
|
||||
<router-link to="/">
|
||||
<home-filled/>
|
||||
{{ $t('common.home') }}
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="console">
|
||||
<router-link to="/console/index">
|
||||
<console-sql-outlined/>
|
||||
{{ $t('common.query') }}
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
<a-sub-menu key="admin">
|
||||
<template #icon>
|
||||
<setting-outlined/>
|
||||
</template>
|
||||
<template #title>{{ $t('common.admin') }}</template>
|
||||
<a-menu-item key="admin_source">
|
||||
<router-link to="/admin/source">
|
||||
<aim-outlined/>
|
||||
{{ $t('common.source') }}
|
||||
<div :style="{ clear: 'both' }">
|
||||
<a-menu theme="dark" mode="horizontal" :style="{lineHeight: '64px', float: 'left', width: '60%'}">
|
||||
<a-menu-item>DataCap(incubator)</a-menu-item>
|
||||
<a-menu-item key="dashboard">
|
||||
<router-link to="/">
|
||||
<home-filled/>
|
||||
{{ $t('common.home') }}
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="admin_history">
|
||||
<router-link to="/admin/history">
|
||||
<history-outlined/>
|
||||
{{ $t('common.history') }}
|
||||
<a-menu-item key="console">
|
||||
<router-link to="/console/index">
|
||||
<console-sql-outlined/>
|
||||
{{ $t('common.query') }}
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
<a-sub-menu key="language">
|
||||
<template #icon>
|
||||
<a-dropdown placement="bottom">
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="en" @click="handlerChangeLang('en')">
|
||||
English
|
||||
</a-menu-item>
|
||||
<a-menu-item key="zhCN" @click="handlerChangeLang('zh_cn')">
|
||||
中文
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<translation-outlined/>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
</a-sub-menu>
|
||||
</a-menu>
|
||||
<a-sub-menu key="admin">
|
||||
<template #icon>
|
||||
<setting-outlined/>
|
||||
</template>
|
||||
<template #title>{{ $t('common.admin') }}</template>
|
||||
<a-menu-item key="admin_source">
|
||||
<router-link to="/admin/source">
|
||||
<aim-outlined/>
|
||||
{{ $t('common.source') }}
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="admin_history">
|
||||
<router-link to="/admin/history">
|
||||
<history-outlined/>
|
||||
{{ $t('common.history') }}
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
</a-menu>
|
||||
<a-menu theme="dark" mode="horizontal" :style="{lineHeight: '64px', float: 'right'}">
|
||||
<a-sub-menu key="language">
|
||||
<template #icon>
|
||||
<a-dropdown placement="bottom">
|
||||
<template #overlay>
|
||||
<a-menu style="margin-top: 5px;">
|
||||
<a-menu-item key="en" @click="handlerChangeLang('en')">
|
||||
{{ $t('common.english') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="zhCN" @click="handlerChangeLang('zh_cn')">
|
||||
{{ $t('common.chinese') }}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<translation-outlined/>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
</a-sub-menu>
|
||||
<a-sub-menu>
|
||||
<template #icon>
|
||||
<a-dropdown placement="bottomRight">
|
||||
<template #overlay>
|
||||
<a-menu style="margin-top: 5px;">
|
||||
<a-menu-item @click="handlerLogout">
|
||||
{{ $t('common.logout') }}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<a-avatar style="background-color: #87d068">{{ username }}</a-avatar>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
</a-sub-menu>
|
||||
</a-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from "vue";
|
||||
import {AimOutlined, HomeFilled, SettingOutlined} from '@ant-design/icons-vue';
|
||||
import Common from "@/common/Common";
|
||||
import {AuthResponse} from "@/model/AuthResponse";
|
||||
import router from "@/router";
|
||||
|
||||
export default defineComponent({
|
||||
name: "LayoutHeader",
|
||||
setup()
|
||||
{
|
||||
let username;
|
||||
const authUser = JSON.parse(localStorage.getItem(Common.token) || '{}') as AuthResponse;
|
||||
if (authUser) {
|
||||
username = authUser.username;
|
||||
}
|
||||
|
||||
const handlerLogout = () => {
|
||||
localStorage.removeItem(Common.token);
|
||||
router.push('/auth/signin')
|
||||
}
|
||||
return {
|
||||
username,
|
||||
handlerLogout
|
||||
}
|
||||
},
|
||||
components: {HomeFilled, SettingOutlined, AimOutlined},
|
||||
computed: {},
|
||||
methods: {
|
||||
|
76
web/console-fe/src/views/pages/auth/AuthLogin.vue
Normal file
76
web/console-fe/src/views/pages/auth/AuthLogin.vue
Normal file
@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div class="main">
|
||||
<a-row :gutter="[8,8]">
|
||||
<a-col :span="8"/>
|
||||
<a-col :span="8">
|
||||
<a-result :title="'DataCap ' + $t('common.login')">
|
||||
<template #icon>
|
||||
<smile-twoTone/>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-alert v-if="formState.message" :message="formState.message" type="error" show-icon style="margin-bottom: 10px;"/>
|
||||
<a-form :model="formState" name="basic" :label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 16 }" @finish="handlerAuthLogin">
|
||||
<a-form-item :label="$t('common.username')" name="username"
|
||||
:rules="[{ required: true, message: $t('required.username') }]">
|
||||
<a-input v-model:value="formState.username"/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="$t('common.password')" name="password"
|
||||
:rules="[{ required: true, message: $t('required.password') }]">
|
||||
<a-input-password v-model:value="formState.password"/>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{ offset: 6, span: 16 }">
|
||||
<a-button :disabled="disabled" :loading="formState.loading" type="primary" html-type="submit">{{ $t('common.login') }}</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
</a-result>
|
||||
</a-col>
|
||||
<a-col :span="8"/>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, reactive} from 'vue';
|
||||
import {AuthUser} from "@/model/AuthUser";
|
||||
import {AuthService} from "@/services/AuthService";
|
||||
import Common from "@/common/Common";
|
||||
import router from "@/router";
|
||||
|
||||
export default defineComponent({
|
||||
setup()
|
||||
{
|
||||
const formState = reactive<AuthUser>({
|
||||
username: '',
|
||||
password: ''
|
||||
});
|
||||
|
||||
const disabled = computed(() => {
|
||||
return !(formState.username && formState.password);
|
||||
});
|
||||
|
||||
const handlerAuthLogin = (values: any) => {
|
||||
formState.loading = true;
|
||||
new AuthService().signin(values)
|
||||
.then(response => {
|
||||
if (response.status) {
|
||||
localStorage.setItem(Common.token, JSON.stringify(response.data));
|
||||
router.push('/');
|
||||
}
|
||||
else {
|
||||
formState.message = response.message;
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
formState.loading = undefined;
|
||||
});
|
||||
};
|
||||
return {
|
||||
formState,
|
||||
disabled,
|
||||
handlerAuthLogin
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
Loading…
Reference in New Issue
Block a user