Add tool upgrade api
This commit is contained in:
@@ -2,14 +2,12 @@ package top.fatweb.oxygen.api.controller.tool
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation
|
||||
import jakarta.validation.Valid
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import top.fatweb.oxygen.api.annotation.BaseController
|
||||
import top.fatweb.oxygen.api.entity.common.ResponseCode
|
||||
import top.fatweb.oxygen.api.entity.common.ResponseResult
|
||||
import top.fatweb.oxygen.api.param.tool.ToolCreateParam
|
||||
import top.fatweb.oxygen.api.param.tool.ToolUpgradeParam
|
||||
import top.fatweb.oxygen.api.service.tool.IEditService
|
||||
import top.fatweb.oxygen.api.vo.tool.ToolCategoryVo
|
||||
import top.fatweb.oxygen.api.vo.tool.ToolTemplateVo
|
||||
@@ -79,6 +77,17 @@ class EditController(
|
||||
fun create(@RequestBody @Valid toolCreateParam: ToolCreateParam): ResponseResult<ToolVo> =
|
||||
ResponseResult.databaseSuccess(ResponseCode.DATABASE_INSERT_SUCCESS, data = editService.create(toolCreateParam))
|
||||
|
||||
/**
|
||||
* Upgrade tool
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Operation(summary = "更新工具")
|
||||
@PatchMapping
|
||||
fun upgrade(@RequestBody @Valid toolUpgradeParam: ToolUpgradeParam): ResponseResult<ToolVo> =
|
||||
ResponseResult.databaseSuccess(ResponseCode.DATABASE_UPDATE_SUCCESS, data = editService.upgrade(toolUpgradeParam))
|
||||
|
||||
/**
|
||||
* Get personal tool
|
||||
*
|
||||
@@ -103,4 +112,16 @@ class EditController(
|
||||
ResponseCode.DATABASE_SELECT_SUCCESS,
|
||||
data = editService.detail(username, toolId, ver)
|
||||
)
|
||||
|
||||
/**
|
||||
* Delete tool
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Operation(summary = "删除工具")
|
||||
@DeleteMapping("/{id}")
|
||||
fun delete(@PathVariable id: Long): ResponseResult<Nothing> =
|
||||
if (editService.delete(id)) ResponseResult.databaseSuccess(ResponseCode.DATABASE_DELETE_SUCCESS)
|
||||
else ResponseResult.databaseFail(ResponseCode.DATABASE_DELETE_FAILED)
|
||||
}
|
||||
@@ -31,6 +31,14 @@ enum class BusinessCode(val code: Int) {
|
||||
*/
|
||||
DATABASE(300),
|
||||
|
||||
/**
|
||||
* Tool
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
TOOL(400),
|
||||
|
||||
/**
|
||||
* Avatar API
|
||||
*
|
||||
|
||||
@@ -58,6 +58,8 @@ enum class ResponseCode(val code: Int) {
|
||||
DATABASE_DUPLICATE_KEY(BusinessCode.DATABASE, 51),
|
||||
DATABASE_NO_RECORD_FOUND(BusinessCode.DATABASE, 52),
|
||||
|
||||
TOOL_ILLEGAL_VERSION(BusinessCode.TOOL, 50),
|
||||
|
||||
API_AVATAR_SUCCESS(BusinessCode.API_AVATAR, 0),
|
||||
API_AVATAR_ERROR(BusinessCode.API_AVATAR, 50);
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package top.fatweb.oxygen.api.exception
|
||||
|
||||
class IllegalVersionException : RuntimeException("Illegal Version")
|
||||
@@ -211,6 +211,12 @@ class ExceptionHandler {
|
||||
ResponseResult.fail(ResponseCode.DATABASE_EXECUTE_ERROR, e.localizedMessage, null)
|
||||
}
|
||||
|
||||
/* Tool */
|
||||
is IllegalVersionException -> {
|
||||
logger.debug(e.localizedMessage, e)
|
||||
ResponseResult.fail(ResponseCode.TOOL_ILLEGAL_VERSION, e.localizedMessage, null)
|
||||
}
|
||||
|
||||
/* Other */
|
||||
is MatchSensitiveWordException -> {
|
||||
logger.debug(e.localizedMessage, e)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package top.fatweb.oxygen.api.param.tool
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import jakarta.validation.constraints.NotBlank
|
||||
import jakarta.validation.constraints.Pattern
|
||||
|
||||
/**
|
||||
* Upgrade tool parameters
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "更新工具请求参数")
|
||||
data class ToolUpgradeParam(
|
||||
/**
|
||||
* Tool ID
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "工具唯一 ID", required = true, example = "tool_a")
|
||||
@field: NotBlank(message = "ToolId can not be blank")
|
||||
@field: Pattern(
|
||||
regexp = "^[a-zA-Z-_][0-9a-zA-Z-_]{2,19}\$",
|
||||
message = "Ver can only match '^[a-zA-Z-_][0-9a-zA-Z-_]{2,19}\$'"
|
||||
)
|
||||
val toolId: String?,
|
||||
|
||||
/**
|
||||
* Version
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "版本", required = true, example = "1.0.3")
|
||||
@field: NotBlank(message = "Ver can not be blank")
|
||||
@field: Pattern(regexp = "^\\d+\\.\\d+\\.\\d+\$", message = "Ver can only match '<number>.<number>.<number>'")
|
||||
val ver: String?
|
||||
)
|
||||
@@ -99,7 +99,7 @@ class AuthenticationServiceImpl(
|
||||
|
||||
@Transactional
|
||||
override fun resend() {
|
||||
val user = userService.getById(WebUtil.getLoginUserId()) ?: throw AccessDeniedException("Access Denied")
|
||||
val user = userService.getById(WebUtil.getLoginUserId())
|
||||
|
||||
user.verify ?: throw NoVerificationRequiredException()
|
||||
|
||||
@@ -124,7 +124,7 @@ class AuthenticationServiceImpl(
|
||||
@EventLogRecord(EventLog.Event.VERIFY)
|
||||
@Transactional
|
||||
override fun verify(verifyParam: VerifyParam) {
|
||||
val user = userService.getById(WebUtil.getLoginUserId()) ?: throw AccessDeniedException("Access Denied")
|
||||
val user = userService.getById(WebUtil.getLoginUserId())
|
||||
user.verify ?: throw NoVerificationRequiredException()
|
||||
if (LocalDateTime.ofInstant(Instant.ofEpochMilli(user.verify!!.split("-").first().toLong()), ZoneOffset.UTC)
|
||||
.isBefore(LocalDateTime.now(ZoneOffset.UTC).minusHours(2)) || user.verify != verifyParam.code
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.service.IService
|
||||
import top.fatweb.oxygen.api.entity.tool.Tool
|
||||
import top.fatweb.oxygen.api.param.tool.ToolCreateParam
|
||||
import top.fatweb.oxygen.api.param.tool.ToolUpdateParam
|
||||
import top.fatweb.oxygen.api.param.tool.ToolUpgradeParam
|
||||
import top.fatweb.oxygen.api.vo.tool.ToolCategoryVo
|
||||
import top.fatweb.oxygen.api.vo.tool.ToolTemplateVo
|
||||
import top.fatweb.oxygen.api.vo.tool.ToolVo
|
||||
@@ -51,6 +52,7 @@ interface IEditService : IService<Tool> {
|
||||
/**
|
||||
* Get tool by ID
|
||||
*
|
||||
* @param
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@@ -64,6 +66,16 @@ interface IEditService : IService<Tool> {
|
||||
*/
|
||||
fun create(toolCreateParam: ToolCreateParam): ToolVo
|
||||
|
||||
/**
|
||||
* Upgrade tool
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
* @see ToolUpgradeParam
|
||||
* @see ToolVo
|
||||
*/
|
||||
fun upgrade(toolUpgradeParam: ToolUpgradeParam): ToolVo
|
||||
|
||||
/**
|
||||
* Update tool
|
||||
*
|
||||
@@ -87,4 +99,12 @@ interface IEditService : IService<Tool> {
|
||||
* @since 1.0.0
|
||||
*/
|
||||
fun detail(username: String, toolId: String, ver: String): ToolVo
|
||||
|
||||
/**
|
||||
* Delete tool
|
||||
*
|
||||
* @author FatttSnake, fatttsnake@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
fun delete(id: Long): Boolean
|
||||
}
|
||||
@@ -2,17 +2,19 @@ package top.fatweb.oxygen.api.service.tool.impl
|
||||
|
||||
import com.baomidou.mybatisplus.extension.kotlin.KtQueryWrapper
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl
|
||||
import org.springframework.dao.DuplicateKeyException
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import top.fatweb.oxygen.api.converter.tool.ToolCategoryConverter
|
||||
import top.fatweb.oxygen.api.converter.tool.ToolConverter
|
||||
import top.fatweb.oxygen.api.converter.tool.ToolTemplateConverter
|
||||
import top.fatweb.oxygen.api.entity.tool.*
|
||||
import top.fatweb.oxygen.api.exception.IllegalVersionException
|
||||
import top.fatweb.oxygen.api.exception.NoRecordFoundException
|
||||
import top.fatweb.oxygen.api.exception.UserNotFoundException
|
||||
import top.fatweb.oxygen.api.mapper.tool.EditMapper
|
||||
import top.fatweb.oxygen.api.param.tool.ToolCreateParam
|
||||
import top.fatweb.oxygen.api.param.tool.ToolUpdateParam
|
||||
import top.fatweb.oxygen.api.param.tool.ToolUpgradeParam
|
||||
import top.fatweb.oxygen.api.service.tool.*
|
||||
import top.fatweb.oxygen.api.util.WebUtil
|
||||
import top.fatweb.oxygen.api.vo.tool.ToolCategoryVo
|
||||
@@ -49,11 +51,15 @@ class EditServiceImpl(
|
||||
.map(ToolCategoryConverter::toolCategoryToToolCategoryVo)
|
||||
|
||||
override fun getOne(id: Long): ToolVo =
|
||||
baseMapper.selectOne(id, WebUtil.getLoginUserId() ?: throw UserNotFoundException())
|
||||
baseMapper.selectOne(id, WebUtil.getLoginUserId()!!)
|
||||
?.let(ToolConverter::toolToToolVo) ?: throw NoRecordFoundException()
|
||||
|
||||
@Transactional
|
||||
override fun create(toolCreateParam: ToolCreateParam): ToolVo {
|
||||
baseMapper.selectOne(
|
||||
KtQueryWrapper(Tool()).eq(Tool::toolId, toolCreateParam.toolId!!)
|
||||
.eq(Tool::authorId, WebUtil.getLoginUserId()!!)
|
||||
)?.let { throw DuplicateKeyException("Duplicate Key") }
|
||||
val template = this.getTemplate(toolCreateParam.templateId!!)
|
||||
val newSource = ToolData().apply { data = template.source!!.data }
|
||||
val newDist = ToolData().apply { data = "" }
|
||||
@@ -65,11 +71,12 @@ class EditServiceImpl(
|
||||
icon = toolCreateParam.icon
|
||||
description = toolCreateParam.description
|
||||
baseId = template.base!!.id
|
||||
authorId = WebUtil.getLoginUserId() ?: throw UserNotFoundException()
|
||||
authorId = WebUtil.getLoginUserId()!!
|
||||
ver = toolCreateParam.ver!!.split(".").map(String::toLong).joinToString(".")
|
||||
keywords = toolCreateParam.keywords
|
||||
sourceId = newSource.id
|
||||
distId = newDist.id
|
||||
entryPoint = template.entryPoint
|
||||
}
|
||||
|
||||
this.save(tool)
|
||||
@@ -85,13 +92,60 @@ class EditServiceImpl(
|
||||
return this.getOne(tool.id!!)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
override fun upgrade(toolUpgradeParam: ToolUpgradeParam): ToolVo {
|
||||
val originalTool = this.detail("!", toolUpgradeParam.toolId!!, "latest")
|
||||
|
||||
val originalVersion = originalTool.ver!!
|
||||
if (originalVersion.split(".").map(String::toLong).joinToString(".") == toolUpgradeParam.ver!!.split(".")
|
||||
.map(String::toLong).joinToString(".")
|
||||
) {
|
||||
throw IllegalVersionException()
|
||||
}
|
||||
originalVersion.split(".").forEachIndexed { index, s ->
|
||||
if ((toolUpgradeParam.ver.split(".")[index].toLong() < s.toLong())) {
|
||||
throw IllegalVersionException()
|
||||
}
|
||||
}
|
||||
|
||||
val newSource = ToolData().apply { data = originalTool.source!!.data }
|
||||
val newDist = ToolData().apply { data = "" }
|
||||
toolDataService.saveBatch(listOf(newSource, newDist))
|
||||
|
||||
val tool = Tool().apply {
|
||||
name = originalTool.name!!
|
||||
toolId = originalTool.toolId
|
||||
icon = originalTool.icon
|
||||
description = originalTool.description
|
||||
baseId = originalTool.base!!.id
|
||||
authorId = WebUtil.getLoginUserId()!!
|
||||
ver = toolUpgradeParam.ver.split(".").map(String::toLong).joinToString(".")
|
||||
keywords = originalTool.keywords
|
||||
sourceId = newSource.id
|
||||
distId = newDist.id
|
||||
entryPoint = originalTool.entryPoint
|
||||
}
|
||||
|
||||
this.save(tool)
|
||||
|
||||
originalTool.categories!!.forEach {
|
||||
toolCategoryService.getById(it.id) ?: throw NoRecordFoundException()
|
||||
rToolCategoryService.save(RToolCategory().apply {
|
||||
toolId = tool.id
|
||||
categoryId = it.id
|
||||
})
|
||||
}
|
||||
|
||||
return this.getOne(tool.id!!)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
override fun update(toolUpdateParam: ToolUpdateParam): ToolVo {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun get(): List<ToolVo> =
|
||||
baseMapper.selectPersonal(WebUtil.getLoginUserId() ?: throw UserNotFoundException())
|
||||
baseMapper.selectPersonal(WebUtil.getLoginUserId()!!)
|
||||
.map(ToolConverter::toolToToolVo)
|
||||
|
||||
override fun detail(username: String, toolId: String, ver: String): ToolVo {
|
||||
@@ -102,4 +156,16 @@ class EditServiceImpl(
|
||||
return baseMapper.detail(username, toolId, ver, WebUtil.getLoginUsername())?.let(ToolConverter::toolToToolVo)
|
||||
?: throw NoRecordFoundException()
|
||||
}
|
||||
|
||||
@Transactional
|
||||
override fun delete(id: Long): Boolean {
|
||||
val tool = baseMapper.selectOne(
|
||||
KtQueryWrapper(Tool()).eq(Tool::id, id)
|
||||
.eq(Tool::authorId, WebUtil.getLoginUserId()!!)
|
||||
) ?: throw NoRecordFoundException()
|
||||
|
||||
toolDataService.removeBatchByIds(listOf(tool.sourceId, tool.distId))
|
||||
rToolCategoryService.remove(KtQueryWrapper(RToolCategory()).eq(RToolCategory::toolId, tool.id))
|
||||
return this.removeById(id)
|
||||
}
|
||||
}
|
||||
@@ -31,50 +31,50 @@
|
||||
</select>
|
||||
|
||||
<select id="selectOne" resultMap="toolWithDataMap">
|
||||
select t_b_tool_main.id as tool_id,
|
||||
t_b_tool_main.name as tool_name,
|
||||
t_b_tool_main.tool_id as tool_tool_id,
|
||||
t_b_tool_main.icon as tool_icon,
|
||||
t_b_tool_main.description as tool_description,
|
||||
t_b_tool_main.base_id as tool_base_id,
|
||||
t_b_tool_main.author_id as tool_author_id,
|
||||
t_b_tool_main.ver as tool_ver,
|
||||
t_b_tool_main.keywords as tool_keywords,
|
||||
t_b_tool_main.source_id as tool_source_id,
|
||||
t_b_tool_main.dist_id as tool_dist_id,
|
||||
t_b_tool_main.entry_point as tool_entry_point,
|
||||
t_b_tool_main.publish as tool_publish,
|
||||
t_b_tool_main.review as tool_review,
|
||||
t_b_tool_main.create_time as tool_create_time,
|
||||
t_b_tool_main.update_time as tool_update_time,
|
||||
t_b_tool_main.deleted as tool_deleted,
|
||||
t_b_tool_main.version as tool_version,
|
||||
tsu.id as user_id,
|
||||
tsu.username as user_username,
|
||||
tsui.id as user_info_id,
|
||||
tsui.nickname as user_info_nickname,
|
||||
tsui.avatar as user_info_avatar,
|
||||
tsui.email as user_info_email,
|
||||
tbtb.name as tool_base_name,
|
||||
tbtb.dist_id as tool_base_dist_id,
|
||||
tbtbd.data as tool_base_dist_data,
|
||||
tbts.data as tool_source_data,
|
||||
tbts.create_time as tool_source_create_time,
|
||||
tbts.update_time as tool_source_update_time,
|
||||
tbts.deleted as tool_source_deleted,
|
||||
tbts.version as tool_source_version,
|
||||
tbtd.data as tool_dist_data,
|
||||
tbtd.create_time as tool_dist_create_time,
|
||||
tbtd.update_time as tool_dist_update_time,
|
||||
tbtd.deleted as tool_dist_deleted,
|
||||
tbtd.version as tool_dist_version,
|
||||
tbtc.id as tool_category_id,
|
||||
tbtc.name as tool_category_name,
|
||||
tbtc.enable as tool_category_enable,
|
||||
tbtc.create_time as tool_category_create_time,
|
||||
tbtc.update_time as tool_category_update_time,
|
||||
tbtc.deleted as tool_category_deleted,
|
||||
tbtc.version as tool_category_version
|
||||
select t_b_tool_main.id as tool_id,
|
||||
t_b_tool_main.name as tool_name,
|
||||
t_b_tool_main.tool_id as tool_tool_id,
|
||||
t_b_tool_main.icon as tool_icon,
|
||||
t_b_tool_main.description as tool_description,
|
||||
t_b_tool_main.base_id as tool_base_id,
|
||||
t_b_tool_main.author_id as tool_author_id,
|
||||
t_b_tool_main.ver as tool_ver,
|
||||
t_b_tool_main.keywords as tool_keywords,
|
||||
t_b_tool_main.source_id as tool_source_id,
|
||||
t_b_tool_main.dist_id as tool_dist_id,
|
||||
t_b_tool_main.entry_point as tool_entry_point,
|
||||
t_b_tool_main.publish as tool_publish,
|
||||
t_b_tool_main.review as tool_review,
|
||||
t_b_tool_main.create_time as tool_create_time,
|
||||
t_b_tool_main.update_time as tool_update_time,
|
||||
t_b_tool_main.deleted as tool_deleted,
|
||||
t_b_tool_main.version as tool_version,
|
||||
tsu.id as user_id,
|
||||
tsu.username as user_username,
|
||||
tsui.id as user_info_id,
|
||||
tsui.nickname as user_info_nickname,
|
||||
tsui.avatar as user_info_avatar,
|
||||
tsui.email as user_info_email,
|
||||
tbtb.name as tool_base_name,
|
||||
tbtb.dist_id as tool_base_dist_id,
|
||||
tbtbd.data as tool_base_dist_data,
|
||||
tbts.data as tool_source_data,
|
||||
tbts.create_time as tool_source_create_time,
|
||||
tbts.update_time as tool_source_update_time,
|
||||
tbts.deleted as tool_source_deleted,
|
||||
tbts.version as tool_source_version,
|
||||
tbtd.data as tool_dist_data,
|
||||
tbtd.create_time as tool_dist_create_time,
|
||||
tbtd.update_time as tool_dist_update_time,
|
||||
tbtd.deleted as tool_dist_deleted,
|
||||
tbtd.version as tool_dist_version,
|
||||
tbtc.id as tool_category_id,
|
||||
tbtc.name as tool_category_name,
|
||||
tbtc.enable as tool_category_enable,
|
||||
tbtc.create_time as tool_category_create_time,
|
||||
tbtc.update_time as tool_category_update_time,
|
||||
tbtc.deleted as tool_category_deleted,
|
||||
tbtc.version as tool_category_version
|
||||
from t_b_tool_main
|
||||
left join (select * from t_s_user where deleted = 0) as tsu on tsu.id = t_b_tool_main.author_id
|
||||
left join (select * from t_s_user_info where deleted = 0) as tsui
|
||||
@@ -125,7 +125,7 @@
|
||||
on tbtc.id = trtmc.category_id
|
||||
where t_b_tool_main.deleted = 0
|
||||
and t_b_tool_main.author_id = #{userId}
|
||||
order by t_b_tool_main.tool_id desc
|
||||
order by t_b_tool_main.id desc
|
||||
</select>
|
||||
|
||||
<select id="detail" resultMap="toolWithDataMap">
|
||||
@@ -208,9 +208,11 @@
|
||||
</choose>
|
||||
</where>
|
||||
order by t_b_tool_main.id desc
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<resultMap id="toolTemplateWithBaseDataMap" type="toolTemplate" extends="top.fatweb.oxygen.api.mapper.tool.ToolTemplateMapper.toolTemplateWithDataMap">
|
||||
<resultMap id="toolTemplateWithBaseDataMap" type="toolTemplate"
|
||||
extends="top.fatweb.oxygen.api.mapper.tool.ToolTemplateMapper.toolTemplateWithDataMap">
|
||||
<association property="base">
|
||||
<id property="id" column="tool_template_base_id"/>
|
||||
<result property="name" column="tool_template_base_name"/>
|
||||
@@ -236,8 +238,9 @@
|
||||
<result property="createTime" column="tool_create_time"/>
|
||||
<result property="deleted" column="tool_deleted"/>
|
||||
<result property="version" column="tool_version"/>
|
||||
<collection property="keywords" ofType="string" column="tool_keywords"/>
|
||||
<collection property="categories" resultMap="top.fatweb.oxygen.api.mapper.tool.ToolCategoryMapper.toolCategoryMap"/>
|
||||
<result property="keywords" column="tool_keywords" typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"/>
|
||||
<collection property="categories"
|
||||
resultMap="top.fatweb.oxygen.api.mapper.tool.ToolCategoryMapper.toolCategoryMap"/>
|
||||
</resultMap>
|
||||
|
||||
<resultMap id="toolWithAuthor" type="tool" extends="toolMap">
|
||||
|
||||
Reference in New Issue
Block a user