mirror of
https://github.com/Funkoala14/KnowledgeBase_OOIN.git
synced 2025-06-08 05:09:44 +08:00
[dev]add update profiles
This commit is contained in:
parent
1ee764d4e8
commit
8688db5cb4
@ -28,7 +28,7 @@ const Snackbar = ({ type = 'primary', message, duration = 3000, onClose }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`snackbar alert alert-${type} d-flex align-items-center justify-content-between position-fixed top-10 start-50 translate-middle w-50 z-2 gap-2 z-3`}
|
className={`snackbar alert alert-${type} d-flex align-items-center justify-content-between position-fixed top-10 start-50 translate-middle w-50 gap-2`}
|
||||||
role='alert'
|
role='alert'
|
||||||
>
|
>
|
||||||
<SvgIcon className={icons[type]} />
|
<SvgIcon className={icons[type]} />
|
||||||
|
@ -1,40 +1,205 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import '../styles/style.scss';
|
import '../styles/style.scss';
|
||||||
|
import { updateProfileThunk } from '../store/auth/auth.thunk';
|
||||||
|
|
||||||
export default function UserSettingsModal({ show, onClose }) {
|
// 部门和组别的映射关系
|
||||||
const { user } = useSelector((state) => state.auth);
|
const departmentGroups = {
|
||||||
|
达人部门: ['达人'],
|
||||||
|
商务部门: ['商务'],
|
||||||
|
样本中心: ['样本'],
|
||||||
|
产品部门: ['产品'],
|
||||||
|
AI自媒体: ['AI自媒体'],
|
||||||
|
HR: ['HR'],
|
||||||
|
技术部门: ['技术'],
|
||||||
|
};
|
||||||
|
|
||||||
|
function UserSettingsModal({ show, onClose }) {
|
||||||
|
const { user, loading } = useSelector((state) => state.auth);
|
||||||
const [lastPasswordChange] = useState('30天前'); // This would come from backend in real app
|
const [lastPasswordChange] = useState('30天前'); // This would come from backend in real app
|
||||||
|
const [formData, setFormData] = useState({});
|
||||||
|
// 可选的组别列表
|
||||||
|
const [availableGroups, setAvailableGroups] = useState([]);
|
||||||
|
|
||||||
|
const [submitted, setSubmitted] = useState(false);
|
||||||
|
const [errors, setErrors] = useState({});
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user) {
|
||||||
|
setFormData({
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
department: user.department,
|
||||||
|
group: user.group,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
// 当部门变化时更新可用的组别
|
||||||
|
useEffect(() => {
|
||||||
|
if (formData.department && departmentGroups[formData.department]) {
|
||||||
|
setAvailableGroups(departmentGroups[formData.department]);
|
||||||
|
} else {
|
||||||
|
setAvailableGroups([]);
|
||||||
|
}
|
||||||
|
}, [formData.department]);
|
||||||
|
|
||||||
if (!show) return null;
|
if (!show) return null;
|
||||||
|
|
||||||
|
const handleInputChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
|
||||||
|
if (name === 'department') {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
[name]: value,
|
||||||
|
['group']: '',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
[name]: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除对应的错误信息
|
||||||
|
if (errors[name]) {
|
||||||
|
setErrors({
|
||||||
|
...errors,
|
||||||
|
[name]: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setSubmitted(true);
|
||||||
|
|
||||||
|
if (validateForm()) {
|
||||||
|
console.log('Form submitted successfully!');
|
||||||
|
console.log('Update data:', formData);
|
||||||
|
try {
|
||||||
|
await dispatch(updateProfileThunk(formData)).unwrap();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Signup failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateForm = () => {
|
||||||
|
const newErrors = {};
|
||||||
|
if (!formData.email) {
|
||||||
|
newErrors.email = 'Email is required';
|
||||||
|
} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(formData.email)) {
|
||||||
|
newErrors.email = 'Invalid email address';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.name) {
|
||||||
|
newErrors.name = 'Name is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.department) {
|
||||||
|
newErrors.department = '请选择部门';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.group) {
|
||||||
|
newErrors.group = '请选择组别';
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrors(newErrors);
|
||||||
|
return Object.keys(newErrors).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='modal show d-block' style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
|
<div className='modal show d-block' style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
|
||||||
<div className='modal-dialog modal-dialog-centered'>
|
<div className='modal-dialog modal-dialog-centered'>
|
||||||
<div className='modal-content'>
|
<form className='modal-content' onSubmit={handleSubmit}>
|
||||||
<div className='modal-header border-0'>
|
<div className='modal-header border-0'>
|
||||||
<h5 className='modal-title'>管理员个人设置</h5>
|
<h5 className='modal-title'>个人设置</h5>
|
||||||
<button type='button' className='btn-close' onClick={onClose}></button>
|
<button type='button' className='btn-close' onClick={onClose}></button>
|
||||||
</div>
|
</div>
|
||||||
<div className='modal-body'>
|
<div className='modal-body'>
|
||||||
<div className='mb-4'>
|
<div className='mb-4'>
|
||||||
<h6 className='text-secondary mb-3'>个人信息</h6>
|
<h6 className='text-secondary mb-3'>个人信息</h6>
|
||||||
<div className='mb-3'>
|
<div className='mb-3'>
|
||||||
<label className='form-label text-secondary'>姓名</label>
|
<label className='form-label text-secondary'>用户名</label>
|
||||||
<input type='text' className='form-control' value={user?.username || ''} readOnly />
|
<input type='text' className='form-control' value={user?.username || ''} readOnly />
|
||||||
</div>
|
</div>
|
||||||
|
<div className='mb-3'>
|
||||||
|
<label className='form-label text-secondary'>姓名</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
className='form-control'
|
||||||
|
value={formData?.name || ''}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
{submitted && errors.name && <div className='invalid-feedback'>{errors.name}</div>}
|
||||||
|
</div>
|
||||||
<div className='mb-3'>
|
<div className='mb-3'>
|
||||||
<label className='form-label text-secondary'>邮箱</label>
|
<label className='form-label text-secondary'>邮箱</label>
|
||||||
<input
|
<input
|
||||||
type='email'
|
type='email'
|
||||||
className='form-control'
|
className='form-control'
|
||||||
value={user?.email || 'admin@ooin.com'}
|
value={formData?.email || 'admin@ooin.com'}
|
||||||
readOnly
|
onChange={handleInputChange}
|
||||||
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
|
{submitted && errors.email && <div className='invalid-feedback'>{errors.email}</div>}
|
||||||
|
</div>
|
||||||
|
<div className='mb-3'>
|
||||||
|
<select
|
||||||
|
className={`form-select form-select-lg${
|
||||||
|
submitted && errors.department ? ' is-invalid' : ''
|
||||||
|
}`}
|
||||||
|
id='department'
|
||||||
|
name='department'
|
||||||
|
value={formData.department}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
disabled={loading}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value='' disabled>
|
||||||
|
选择部门
|
||||||
|
</option>
|
||||||
|
{Object.keys(departmentGroups).map((dept, index) => (
|
||||||
|
<option key={index} value={dept}>
|
||||||
|
{dept}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{submitted && errors.department && (
|
||||||
|
<div className='invalid-feedback'>{errors.department}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className='mb-3'>
|
||||||
|
<select
|
||||||
|
className={`form-select form-select-lg${
|
||||||
|
submitted && errors.group ? ' is-invalid' : ''
|
||||||
|
}`}
|
||||||
|
id='group'
|
||||||
|
name='group'
|
||||||
|
value={formData.group}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
disabled={loading || !formData.department}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value='' disabled>
|
||||||
|
{formData.department ? '选择组别' : '请先选择部门'}
|
||||||
|
</option>
|
||||||
|
{availableGroups.map((group, index) => (
|
||||||
|
<option key={index} value={group}>
|
||||||
|
{group}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{submitted && errors.group && <div className='invalid-feedback'>{errors.group}</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mb-4'>
|
<div className='d-none mb-4'>
|
||||||
<h6 className='text-secondary mb-3'>安全设置</h6>
|
<h6 className='text-secondary mb-3'>安全设置</h6>
|
||||||
<div className='d-flex justify-content-between align-items-center p-3 bg-light rounded'>
|
<div className='d-flex justify-content-between align-items-center p-3 bg-light rounded'>
|
||||||
<div>
|
<div>
|
||||||
@ -58,7 +223,7 @@ export default function UserSettingsModal({ show, onClose }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className='d-none'>
|
||||||
<h6 className='text-secondary mb-3'>通知设置</h6>
|
<h6 className='text-secondary mb-3'>通知设置</h6>
|
||||||
<div className='form-check form-switch mb-3 dark-switch'>
|
<div className='form-check form-switch mb-3 dark-switch'>
|
||||||
<input
|
<input
|
||||||
@ -87,15 +252,17 @@ export default function UserSettingsModal({ show, onClose }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='modal-footer border-0'>
|
<div className='modal-footer border-0'>
|
||||||
<button type='button' className='btn btn-outline-dark' onClick={onClose}>
|
<button type='button' disabled={loading} className='btn btn-outline-dark' onClick={onClose}>
|
||||||
取消
|
取消
|
||||||
</button>
|
</button>
|
||||||
<button type='button' className='btn btn-dark'>
|
<button type='submit' className='btn btn-dark' disabled={loading}>
|
||||||
保存更改
|
保存更改
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default UserSettingsModal;
|
||||||
|
@ -121,11 +121,11 @@ export default function HeaderWithNav() {
|
|||||||
transform: 'translate(0px, 34px)',
|
transform: 'translate(0px, 34px)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<li className='d-none'>
|
<li>
|
||||||
<Link
|
<Link
|
||||||
className='dropdown-item'
|
className='dropdown-item'
|
||||||
to='#'
|
to='#'
|
||||||
// onClick={() => setShowSettings(true)}
|
onClick={() => setShowSettings(true)}
|
||||||
>
|
>
|
||||||
个人设置
|
个人设置
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -58,7 +58,7 @@ const KnowledgeBaseForm = ({
|
|||||||
|
|
||||||
// 是否显示类型更改按钮
|
// 是否显示类型更改按钮
|
||||||
const showTypeChangeButton = hasTypeChanged || (isAdmin && hasDepartmentOrGroupChanged);
|
const showTypeChangeButton = hasTypeChanged || (isAdmin && hasDepartmentOrGroupChanged);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='card border-0 shadow-sm'>
|
<div className='card border-0 shadow-sm'>
|
||||||
<div className='card-body'>
|
<div className='card-body'>
|
||||||
@ -127,47 +127,46 @@ const KnowledgeBaseForm = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 仅当不是私有知识库时才显示部门选项 */}
|
{/* 仅当不是私有知识库时才显示部门选项 */}
|
||||||
{formData.type === 'member' ||
|
{(formData.type === 'member' || formData.type === 'leader') && (
|
||||||
(formData.type === 'leader' && (
|
<div className='mb-3'>
|
||||||
<div className='mb-3'>
|
<label htmlFor='department' className='form-label'>
|
||||||
<label htmlFor='department' className='form-label'>
|
部门 {isAdmin && <span className='text-danger'>*</span>}
|
||||||
部门 {isAdmin && <span className='text-danger'>*</span>}
|
</label>
|
||||||
</label>
|
{isAdmin ? (
|
||||||
{isAdmin ? (
|
<>
|
||||||
<>
|
<select
|
||||||
<select
|
className={`form-select ${formErrors.department ? 'is-invalid' : ''}`}
|
||||||
className={`form-select ${formErrors.department ? 'is-invalid' : ''}`}
|
id='department'
|
||||||
id='department'
|
name='department'
|
||||||
name='department'
|
value={formData.department || ''}
|
||||||
value={formData.department || ''}
|
onChange={onInputChange}
|
||||||
onChange={onInputChange}
|
disabled={isSubmitting}
|
||||||
disabled={isSubmitting}
|
>
|
||||||
>
|
<option value=''>请选择部门</option>
|
||||||
<option value=''>请选择部门</option>
|
{departments.map((dept, index) => (
|
||||||
{departments.map((dept, index) => (
|
<option key={index} value={dept}>
|
||||||
<option key={index} value={dept}>
|
{dept}
|
||||||
{dept}
|
</option>
|
||||||
</option>
|
))}
|
||||||
))}
|
</select>
|
||||||
</select>
|
{formErrors.department && (
|
||||||
{formErrors.department && (
|
<div className='invalid-feedback'>{formErrors.department}</div>
|
||||||
<div className='invalid-feedback'>{formErrors.department}</div>
|
)}
|
||||||
)}
|
</>
|
||||||
</>
|
) : (
|
||||||
) : (
|
<>
|
||||||
<>
|
<input
|
||||||
<input
|
type='text'
|
||||||
type='text'
|
className='form-control bg-light'
|
||||||
className='form-control bg-light'
|
id='department'
|
||||||
id='department'
|
name='department'
|
||||||
name='department'
|
value={formData.department || ''}
|
||||||
value={formData.department || ''}
|
readOnly
|
||||||
readOnly
|
/>
|
||||||
/>
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
))}
|
|
||||||
|
|
||||||
{/* 仅当不是私有知识库时才显示组别选项 */}
|
{/* 仅当不是私有知识库时才显示组别选项 */}
|
||||||
{formData.type === 'member' && (
|
{formData.type === 'member' && (
|
||||||
|
@ -2,7 +2,7 @@ import { addNotification, markNotificationAsRead } from '../store/notificationCe
|
|||||||
import store from '../store/store'; // 修改为默认导出
|
import store from '../store/store'; // 修改为默认导出
|
||||||
|
|
||||||
// 从环境变量获取 API URL
|
// 从环境变量获取 API URL
|
||||||
const API_URL = import.meta.env.VITE_API_URL || '';
|
const API_URL = import.meta.env.VITE_API_URL || 'http://81.69.223.133:8008';
|
||||||
// 将 HTTP URL 转换为 WebSocket URL
|
// 将 HTTP URL 转换为 WebSocket URL
|
||||||
const WS_BASE_URL = API_URL.replace(/^http/, 'ws').replace(/\/api\/?$/, '');
|
const WS_BASE_URL = API_URL.replace(/^http/, 'ws').replace(/\/api\/?$/, '');
|
||||||
|
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import { checkAuthThunk, loginThunk, logoutThunk, signupThunk } from './auth.thunk';
|
import { checkAuthThunk, loginThunk, logoutThunk, signupThunk, updateProfileThunk } from './auth.thunk';
|
||||||
|
|
||||||
const setPending = (state) => {
|
const setPending = (state) => {
|
||||||
state.loading = true;
|
state.loading = true;
|
||||||
state.error = null;
|
state.error = null;
|
||||||
state.user = null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const setFulfilled = (state, action) => {
|
const setFulfilled = (state, action) => {
|
||||||
@ -49,6 +48,16 @@ const authSlice = createSlice({
|
|||||||
.addCase(signupThunk.fulfilled, setFulfilled)
|
.addCase(signupThunk.fulfilled, setFulfilled)
|
||||||
.addCase(signupThunk.rejected, setRejected)
|
.addCase(signupThunk.rejected, setRejected)
|
||||||
|
|
||||||
|
.addCase(updateProfileThunk.pending, setPending)
|
||||||
|
.addCase(updateProfileThunk.fulfilled, (state, action) => {
|
||||||
|
state.user = {
|
||||||
|
...state.user,
|
||||||
|
...action.payload,
|
||||||
|
};
|
||||||
|
state.loading = false;
|
||||||
|
})
|
||||||
|
.addCase(updateProfileThunk.rejected, setRejected)
|
||||||
|
|
||||||
.addCase(logoutThunk.pending, (state) => {
|
.addCase(logoutThunk.pending, (state) => {
|
||||||
state.loading = true;
|
state.loading = true;
|
||||||
state.error = null;
|
state.error = null;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { get, post } from '../../services/api';
|
import { get, post, put } from '../../services/api';
|
||||||
import { showNotification } from '../notification.slice';
|
import { showNotification } from '../notification.slice';
|
||||||
import { logout } from './auth.slice';
|
import { logout } from './auth.slice';
|
||||||
import CryptoJS from 'crypto-js';
|
import CryptoJS from 'crypto-js';
|
||||||
@ -40,29 +40,29 @@ export const loginThunk = createAsyncThunk(
|
|||||||
export const signupThunk = createAsyncThunk('auth/signup', async (userData, { rejectWithValue, dispatch }) => {
|
export const signupThunk = createAsyncThunk('auth/signup', async (userData, { rejectWithValue, dispatch }) => {
|
||||||
try {
|
try {
|
||||||
// 使用新的注册 API
|
// 使用新的注册 API
|
||||||
const response = await post('/auth/register/', userData);
|
const { data, message, code } = await post('/auth/register/', userData);
|
||||||
console.log('注册返回数据:', response);
|
console.log('注册返回数据:', data);
|
||||||
|
|
||||||
// 处理新的返回格式
|
if (code !== 200) {
|
||||||
if (response && response.code === 200) {
|
throw new Error(message);
|
||||||
// // 将 token 加密存储到 sessionStorage
|
}
|
||||||
// const { token } = response.data;
|
// 将 token 加密存储到 sessionStorage
|
||||||
// if (token) {
|
const { token } = data;
|
||||||
// const encryptedToken = CryptoJS.AES.encrypt(token, secretKey).toString();
|
|
||||||
// sessionStorage.setItem('token', encryptedToken);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 显示注册成功通知
|
if (token) {
|
||||||
dispatch(
|
const encryptedToken = CryptoJS.AES.encrypt(token, secretKey).toString();
|
||||||
showNotification({
|
sessionStorage.setItem('token', encryptedToken);
|
||||||
message: '注册成功',
|
|
||||||
type: 'success',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return response.data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return rejectWithValue(response.message || '注册失败');
|
// 显示注册成功通知
|
||||||
|
dispatch(
|
||||||
|
showNotification({
|
||||||
|
message: '注册成功',
|
||||||
|
type: 'success',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error.response?.data?.message || '注册失败,请稍后重试';
|
const errorMessage = error.response?.data?.message || '注册失败,请稍后重试';
|
||||||
dispatch(
|
dispatch(
|
||||||
@ -107,3 +107,33 @@ export const logoutThunk = createAsyncThunk('auth/logout', async (_, { rejectWit
|
|||||||
return rejectWithValue(errorMessage);
|
return rejectWithValue(errorMessage);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 更新个人资料
|
||||||
|
export const updateProfileThunk = createAsyncThunk('auth/updateProfile', async (userData, { rejectWithValue, dispatch }) => {
|
||||||
|
try {
|
||||||
|
const { data, message, code } = await put('/users/profile/', userData);
|
||||||
|
|
||||||
|
if (code !== 200) {
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示更新成功通知
|
||||||
|
dispatch(
|
||||||
|
showNotification({
|
||||||
|
message: '个人信息更新成功',
|
||||||
|
type: 'success',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error.response?.data?.message || '更新失败,请稍后重试';
|
||||||
|
dispatch(
|
||||||
|
showNotification({
|
||||||
|
message: errorMessage,
|
||||||
|
type: 'danger',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return rejectWithValue(errorMessage);
|
||||||
|
}
|
||||||
|
});
|
@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
.snackbar {
|
.snackbar {
|
||||||
top: 6.5rem;
|
top: 6.5rem;
|
||||||
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Markdown styling in chat messages */
|
/* Markdown styling in chat messages */
|
||||||
|
Loading…
Reference in New Issue
Block a user