临时授权-优先取入参,其次oss默认配置过期时间
This commit is contained in:
parent
0f873d6065
commit
7a3e538910
@ -268,7 +268,7 @@ public class WebFileController {
|
|||||||
/**
|
/**
|
||||||
* 授权给第三方下载-生成临时url
|
* 授权给第三方下载-生成临时url
|
||||||
*/
|
*/
|
||||||
@PostMapping(value = "/signUrl/download")
|
@PostMapping(value = "/signUrl/fetchDownload")
|
||||||
@CrossOrigin
|
@CrossOrigin
|
||||||
public CommonResponse<List<SignUrlDownloadResponse>> signUrlDownload(@Valid @RequestBody SignUrlDownloadDto request) {
|
public CommonResponse<List<SignUrlDownloadResponse>> signUrlDownload(@Valid @RequestBody SignUrlDownloadDto request) {
|
||||||
//获取用户信息
|
//获取用户信息
|
||||||
@ -278,7 +278,7 @@ public class WebFileController {
|
|||||||
/**
|
/**
|
||||||
* 授权给第三方上传-生成临时url
|
* 授权给第三方上传-生成临时url
|
||||||
*/
|
*/
|
||||||
@PostMapping(value = "/signUrl/upload")
|
@PostMapping(value = "/signUrl/fetchUpload")
|
||||||
@CrossOrigin
|
@CrossOrigin
|
||||||
public CommonResponse<SignUrlUploadResponse> signUrlUpload(@Valid @RequestBody SignUrlUploadDto request) {
|
public CommonResponse<SignUrlUploadResponse> signUrlUpload(@Valid @RequestBody SignUrlUploadDto request) {
|
||||||
ContextInfo.LiteSaasContext liteSaasContext = Objects.nonNull(ContextInfoHolder.get()) ? ContextInfoHolder.get().lite() : null;
|
ContextInfo.LiteSaasContext liteSaasContext = Objects.nonNull(ContextInfoHolder.get()) ? ContextInfoHolder.get().lite() : null;
|
||||||
|
|||||||
@ -144,14 +144,6 @@ public class File extends Model<File> {
|
|||||||
@TableField("is_delete")
|
@TableField("is_delete")
|
||||||
private Integer isDelete;
|
private Integer isDelete;
|
||||||
|
|
||||||
/**
|
|
||||||
* 临时授权失效时间
|
|
||||||
* 根据业务入参决定,如果业务没有入参则读取file_upload_config#expiration,默认300s
|
|
||||||
*/
|
|
||||||
@TableField("expiration")
|
|
||||||
private Long expiration;
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Serializable pkVal() {
|
protected Serializable pkVal() {
|
||||||
return this.id;
|
return this.id;
|
||||||
|
|||||||
@ -96,6 +96,18 @@ public class FileBusinessScene extends Model<FileBusinessScene> {
|
|||||||
@TableField("is_delete")
|
@TableField("is_delete")
|
||||||
private Integer isDelete;
|
private Integer isDelete;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传-临时授权失效时间,默认1800s即30分钟
|
||||||
|
*/
|
||||||
|
@TableField("upload_expiration")
|
||||||
|
private Long uploadExpiration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载-临时授权失效时间,默认1800s即30分钟
|
||||||
|
*/
|
||||||
|
@TableField("download_expiration")
|
||||||
|
private Long downloadExpiration;
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Serializable pkVal() {
|
protected Serializable pkVal() {
|
||||||
|
|||||||
@ -108,13 +108,6 @@ public class FileUploadConfig extends Model<FileUploadConfig> {
|
|||||||
@TableField("is_delete")
|
@TableField("is_delete")
|
||||||
private Integer isDelete;
|
private Integer isDelete;
|
||||||
|
|
||||||
/**
|
|
||||||
* 临时授权失效时间,默认300s即5分钟
|
|
||||||
*/
|
|
||||||
@TableField("expiration")
|
|
||||||
private Long expiration;
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Serializable pkVal() {
|
protected Serializable pkVal() {
|
||||||
return this.id;
|
return this.id;
|
||||||
|
|||||||
@ -3,6 +3,8 @@ package cn.axzo.oss.dal.repository;
|
|||||||
import cn.axzo.oss.dal.entity.FileBusinessScene;
|
import cn.axzo.oss.dal.entity.FileBusinessScene;
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
* 文件业务场景 服务类
|
* 文件业务场景 服务类
|
||||||
@ -21,4 +23,10 @@ public interface FileBusinessSceneDao extends IService<FileBusinessScene> {
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
FileBusinessScene getByBucketNoAndScene(String bucketNo, String bizScene);
|
FileBusinessScene getByBucketNoAndScene(String bucketNo, String bizScene);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据bucketNo与场景,获取批量FileBusinessScene对象
|
||||||
|
*/
|
||||||
|
List<FileBusinessScene> queryByBucketNoAndScene(List<String> bucketNoList, String bizScen);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,13 @@ import cn.axzo.oss.dal.entity.FileBusinessScene;
|
|||||||
import cn.axzo.oss.dal.mapper.FileBusinessSceneMapper;
|
import cn.axzo.oss.dal.mapper.FileBusinessSceneMapper;
|
||||||
import cn.axzo.oss.dal.repository.FileBusinessSceneDao;
|
import cn.axzo.oss.dal.repository.FileBusinessSceneDao;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
@ -31,4 +37,17 @@ public class FileBusinessSceneDaoImpl extends
|
|||||||
.eq(FileBusinessScene::getBusinessScene,bizScen)
|
.eq(FileBusinessScene::getBusinessScene,bizScen)
|
||||||
.eq(FileBusinessScene::getIsDelete, IsDeleteEnum.NO.getCode()).one();
|
.eq(FileBusinessScene::getIsDelete, IsDeleteEnum.NO.getCode()).one();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据bucketNo与场景,获取批量FileBusinessScene对象
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<FileBusinessScene> queryByBucketNoAndScene(List<String> bucketNoList, String bizScen) {
|
||||||
|
if (CollectionUtils.isEmpty(bucketNoList) || StringUtils.isBlank(bizScen)) {
|
||||||
|
return Lists.newArrayList();
|
||||||
|
}
|
||||||
|
return lambdaQuery().in(FileBusinessScene::getAppChannelBucketNo, bucketNoList)
|
||||||
|
.eq(FileBusinessScene::getBusinessScene,bizScen)
|
||||||
|
.eq(FileBusinessScene::getIsDelete, IsDeleteEnum.NO.getCode()).list();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,4 +25,9 @@ public class ApiSignUrlDownloadRequest {
|
|||||||
@NotEmpty(message = "fileKeys not empty")
|
@NotEmpty(message = "fileKeys not empty")
|
||||||
private List<String> fileKeys;
|
private List<String> fileKeys;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* bizScene,对应目录
|
||||||
|
*/
|
||||||
|
private String bizScene;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,8 @@ package cn.axzo.oss.manager.api;
|
|||||||
|
|
||||||
import cn.axzo.oss.dal.entity.FileBusinessScene;
|
import cn.axzo.oss.dal.entity.FileBusinessScene;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author: zhangran
|
* @author: zhangran
|
||||||
* @date: 20210803 15:42
|
* @date: 20210803 15:42
|
||||||
@ -17,4 +19,9 @@ public interface FileBusinessSceneManager {
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
FileBusinessScene getByBucketNoAndScene(String bucketNo,String bizScene);
|
FileBusinessScene getByBucketNoAndScene(String bucketNo,String bizScene);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 指定appcode文件业务场景
|
||||||
|
*/
|
||||||
|
List<FileBusinessScene> queryByBucketNoAndScene(List<String> bucketNoList, String bizScene);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import lombok.Builder;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
import javax.validation.constraints.NotEmpty;
|
import javax.validation.constraints.NotEmpty;
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -26,4 +27,9 @@ public class SignUrlDownloadDto {
|
|||||||
@NotEmpty(message = "fileKeys not empty")
|
@NotEmpty(message = "fileKeys not empty")
|
||||||
private List<String> fileKeys;
|
private List<String> fileKeys;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* bizScene,对应目录
|
||||||
|
*/
|
||||||
|
private String bizScene;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,8 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author: zhangran
|
* @author: zhangran
|
||||||
* @date: 20210803 16:36
|
* @date: 20210803 16:36
|
||||||
@ -35,4 +37,13 @@ public class FileBusinessSceneManagerImpl implements FileBusinessSceneManager {
|
|||||||
BizException.isEmpty(fileBusinessScene, CodeEnum.APP_CHANNEL_NOT_FOUND);
|
BizException.isEmpty(fileBusinessScene, CodeEnum.APP_CHANNEL_NOT_FOUND);
|
||||||
return fileBusinessScene;
|
return fileBusinessScene;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据bucketNo与场景,获取批量FileBusinessScene对象
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<FileBusinessScene> queryByBucketNoAndScene(List<String> bucketNoList, String bizScene) {
|
||||||
|
return fileBusinessSceneDao
|
||||||
|
.queryByBucketNoAndScene(bucketNoList, bizScene);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,6 +32,7 @@ import org.springframework.beans.factory.annotation.Value;
|
|||||||
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||||
import org.springframework.core.env.Environment;
|
import org.springframework.core.env.Environment;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -41,6 +42,7 @@ import java.nio.file.Files;
|
|||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static cn.axzo.oss.common.constans.CommonConstants.APP_PRO_BUCKET_NAME;
|
import static cn.axzo.oss.common.constans.CommonConstants.APP_PRO_BUCKET_NAME;
|
||||||
@ -264,9 +266,6 @@ public class FileServiceImpl implements FileService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private FileUploadConfig getFileUploadConfig(String appCode, String bizScene, Integer channelType) {
|
private FileUploadConfig getFileUploadConfig(String appCode, String bizScene, Integer channelType) {
|
||||||
// 检查appCode
|
|
||||||
checkAppCode(appCode);
|
|
||||||
|
|
||||||
// 通过appcode获取文件渠道桶信息
|
// 通过appcode获取文件渠道桶信息
|
||||||
AppChannelBucket appChannelBucket = appChannelBucketManager.getByAppCode(appCode, channelType);
|
AppChannelBucket appChannelBucket = appChannelBucketManager.getByAppCode(appCode, channelType);
|
||||||
|
|
||||||
@ -337,7 +336,6 @@ public class FileServiceImpl implements FileService {
|
|||||||
ossFile.setStatus(FileStatusEnum.STATUS_UPLOAD_SUCCESS.getCode());
|
ossFile.setStatus(FileStatusEnum.STATUS_UPLOAD_SUCCESS.getCode());
|
||||||
ossFile.setFileName(fileName);
|
ossFile.setFileName(fileName);
|
||||||
ossFile.setFileMd5(fileMd5);
|
ossFile.setFileMd5(fileMd5);
|
||||||
ossFile.setExpiration(expiration);
|
|
||||||
fileDao.save(ossFile);
|
fileDao.save(ossFile);
|
||||||
return ossFile;
|
return ossFile;
|
||||||
}
|
}
|
||||||
@ -737,12 +735,18 @@ public class FileServiceImpl implements FileService {
|
|||||||
public List<SignUrlDownloadResponse> signUrlDownload(SignUrlDownloadDto dto) {
|
public List<SignUrlDownloadResponse> signUrlDownload(SignUrlDownloadDto dto) {
|
||||||
log.info("signUrl download dto = {}", JsonUtil.obj2Str(dto));
|
log.info("signUrl download dto = {}", JsonUtil.obj2Str(dto));
|
||||||
|
|
||||||
|
|
||||||
List<File> fileList = fileDao.getByFileUuids(dto.getFileKeys());
|
List<File> fileList = fileDao.getByFileUuids(dto.getFileKeys());
|
||||||
|
if (CollectionUtils.isEmpty(fileList)) {
|
||||||
|
return Lists.newArrayList();
|
||||||
|
}
|
||||||
|
List<FileBusinessScene> fileBusinessSceneList = fileBusinessSceneManager.queryByBucketNoAndScene(fileList.stream().map(File::getAppChannelBucketNo).collect(Collectors.toList()), dto.getBizScene());
|
||||||
|
Map<String, Long> bizSceneExpireMap = fileBusinessSceneList.stream().collect(Collectors.toMap(FileBusinessScene::getAppChannelBucketNo, FileBusinessScene::getDownloadExpiration, (x, y) -> y));
|
||||||
|
|
||||||
return fileList.stream().map(item -> {
|
return fileList.stream().map(item -> {
|
||||||
|
Long expire = bizSceneExpireMap.get(item.getAppChannelBucketNo());
|
||||||
// bucket下的key
|
// bucket下的key
|
||||||
String tgtFileKey = Utility.generateFileKey(item.getDirectory(), item.getFileUuid(), item.getFileFormat());
|
String tgtFileKey = Utility.generateFileKey(item.getDirectory(), item.getFileUuid(), item.getFileFormat());
|
||||||
String signUrl = this.fileManager.signUrlDownload(item.getBucketName(), tgtFileKey, SIGN_URL_DOWNLOAD_EXPIRE_SECOND , item.getChannelCode());
|
String signUrl = this.fileManager.signUrlDownload(item.getBucketName(), tgtFileKey, Objects.nonNull(expire) ? expire : SIGN_URL_DOWNLOAD_EXPIRE_SECOND , item.getChannelCode());
|
||||||
return SignUrlDownloadResponse.builder()
|
return SignUrlDownloadResponse.builder()
|
||||||
.signUrl(signUrl)
|
.signUrl(signUrl)
|
||||||
.fileKey(item.getFileUuid())
|
.fileKey(item.getFileUuid())
|
||||||
@ -758,12 +762,36 @@ public class FileServiceImpl implements FileService {
|
|||||||
log.info("signUrl upload dto = {}", JsonUtil.obj2Str(dto));
|
log.info("signUrl upload dto = {}", JsonUtil.obj2Str(dto));
|
||||||
//1 校验
|
//1 校验
|
||||||
checkAppCode(dto.getAppCode());
|
checkAppCode(dto.getAppCode());
|
||||||
//2 获取文件配置(多个配置获取优先级高)
|
|
||||||
FileUploadConfig fileUploadConfig = this.signUrlBuildUploadConfig(dto);
|
FileUploadConfig fileUploadConfig = new FileUploadConfig();
|
||||||
|
FileBusinessScene scene = new FileBusinessScene();
|
||||||
|
if (Objects.nonNull(dto.getChannelType()) && StringUtils.isNoneBlank(dto.getBucketName()) && StringUtils.isNoneBlank(dto.getBizScene())) {
|
||||||
|
AppChannelBucket appChannelBucket = appChannelBucketManager.getByAppCodeChannelCodeBucket(dto.getAppCode(), dto.getChannelType(), dto.getBucketName());
|
||||||
|
|
||||||
|
// 通过渠道桶编码获取到具体文件业务场景
|
||||||
|
scene = fileBusinessSceneManager
|
||||||
|
.getByBucketNoAndScene(appChannelBucket.getAppChannelBucketNo(), dto.getBizScene());
|
||||||
|
|
||||||
|
// 通过渠道码和桶名称获取指定上传配置
|
||||||
|
fileUploadConfig = fileUploadConfigManager
|
||||||
|
.getByUploadConfig(scene.getAppChannelBucketNo(), scene.getDirectory());
|
||||||
|
} else {
|
||||||
|
// 通过appcode获取文件渠道桶信息
|
||||||
|
AppChannelBucket appChannelBucket = appChannelBucketManager.getByAppCode(dto.getAppCode(), dto.getChannelType());
|
||||||
|
|
||||||
|
// 通过渠道桶编码获取到具体文件业务场景
|
||||||
|
scene = fileBusinessSceneManager
|
||||||
|
.getByBucketNoAndScene(appChannelBucket.getAppChannelBucketNo(), dto.getBizScene());
|
||||||
|
|
||||||
|
// 通过渠道码和桶名称获取获取指定上传配置
|
||||||
|
fileUploadConfig = fileUploadConfigManager
|
||||||
|
.getByUploadConfig(scene.getAppChannelBucketNo(), scene.getDirectory());
|
||||||
|
}
|
||||||
|
|
||||||
//操作日志记录
|
//操作日志记录
|
||||||
operateLog(dto.toString(), dto.getServiceName(), FILE_UPLOAD_CODE, FILE_UPLOAD_NAME, liteSaasContext);
|
operateLog(dto.toString(), dto.getServiceName(), FILE_UPLOAD_CODE, FILE_UPLOAD_NAME, liteSaasContext);
|
||||||
//3 保存File对象
|
//3 保存File对象
|
||||||
return this.signUrlSaveFile(dto, fileUploadConfig);
|
return this.signUrlSaveFile(dto, fileUploadConfig, scene);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -772,7 +800,7 @@ public class FileServiceImpl implements FileService {
|
|||||||
* @param fileUploadConfig
|
* @param fileUploadConfig
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
private SignUrlUploadResponse signUrlSaveFile(SignUrlUploadDto dto, FileUploadConfig fileUploadConfig) {
|
private SignUrlUploadResponse signUrlSaveFile(SignUrlUploadDto dto, FileUploadConfig fileUploadConfig, FileBusinessScene scene) {
|
||||||
String uuid = Utility.getUUID();
|
String uuid = Utility.getUUID();
|
||||||
|
|
||||||
int lastIndexOf = dto.getFileName().lastIndexOf(FileClassEnum.DOT.type);
|
int lastIndexOf = dto.getFileName().lastIndexOf(FileClassEnum.DOT.type);
|
||||||
@ -790,7 +818,7 @@ public class FileServiceImpl implements FileService {
|
|||||||
// 生成上传文件的唯一key
|
// 生成上传文件的唯一key
|
||||||
String tgtFileKey = Utility.generateFileKey(fileUploadConfig.getDirectory(), uuid, fileFormat);
|
String tgtFileKey = Utility.generateFileKey(fileUploadConfig.getDirectory(), uuid, fileFormat);
|
||||||
// 失效时间
|
// 失效时间
|
||||||
Long expiration = this.buildExpiration(dto, fileUploadConfig);
|
Long expiration = this.buildExpiration(dto, scene);
|
||||||
//1 调用阿里云/华为云 获取临时授权signUrl
|
//1 调用阿里云/华为云 获取临时授权signUrl
|
||||||
SignUrlUploadVo signUrlUpload = this.fileManager.signUrlUpload(fileUploadConfig.getBucketName(), tgtFileKey, dto.getFileName(),expiration
|
SignUrlUploadVo signUrlUpload = this.fileManager.signUrlUpload(fileUploadConfig.getBucketName(), tgtFileKey, dto.getFileName(),expiration
|
||||||
, fileUploadConfig.getChannelCode(), StringUtils.isNotBlank(dto.getContentType()) ? dto.getContentType() : "multipart/form-data");
|
, fileUploadConfig.getChannelCode(), StringUtils.isNotBlank(dto.getContentType()) ? dto.getContentType() : "multipart/form-data");
|
||||||
@ -810,11 +838,11 @@ public class FileServiceImpl implements FileService {
|
|||||||
* 临时授权失效时间
|
* 临时授权失效时间
|
||||||
* 优先从业务入参取值,业务入参失效时间为空,则从oss配置中取失效时间
|
* 优先从业务入参取值,业务入参失效时间为空,则从oss配置中取失效时间
|
||||||
*/
|
*/
|
||||||
private Long buildExpiration(SignUrlUploadDto dto, FileUploadConfig fileUploadConfig) {
|
private Long buildExpiration(SignUrlUploadDto dto, FileBusinessScene scene) {
|
||||||
if (Objects.nonNull(dto) && Objects.nonNull(dto.getExpiration())) {
|
if (Objects.nonNull(dto) && Objects.nonNull(dto.getExpiration())) {
|
||||||
return dto.getExpiration();
|
return dto.getExpiration();
|
||||||
}
|
}
|
||||||
return fileUploadConfig.getExpiration();
|
return scene.getUploadExpiration();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user