Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bc478ad4ed | |||
| 161a68a0a2 | |||
| 3b59f7acd7 | |||
| 1e29d1d721 | |||
| 9342b6484c | |||
| dc3842aa9b | |||
| a440ac842d |
@@ -17,7 +17,7 @@ jobs:
|
||||
CATALINA_BASE: /prod/eapim/adminportal
|
||||
CATALINA_HOME: /prod/eapim/apache-tomcat-9.0.116
|
||||
CATALINA_PID: /prod/eapim/adminportal/temp/catalina.pid
|
||||
DEPLOY_HTTP_PORT: "30120"
|
||||
DEPLOY_HTTP_PORT: "39120"
|
||||
defaults:
|
||||
run:
|
||||
working-directory: eapim-admin
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
cp eapim-admin/build/libs/eapim-admin.war "$DEPLOY_DIR/monitoring.war"
|
||||
ls -la "$DEPLOY_DIR"
|
||||
|
||||
- name: Start tomcat and readiness probe (timeout 180s)
|
||||
- name: Start tomcat and readiness probe (timeout 300s)
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
LOG="$CATALINA_BASE/logs/catalina.$(date +%Y-%m-%d).log"
|
||||
@@ -99,14 +99,17 @@ jobs:
|
||||
# systemd 가 Tomcat 을 새 cgroup 으로 띄움 — runner step 종료와 무관하게 살아남음
|
||||
systemctl --user start eapim-admin
|
||||
|
||||
DEADLINE=$(($(date +%s) + 180))
|
||||
# /loginForm.do 는 GET 매핑(MainController#loginForm) → tomcat·webapp 양쪽 살아있으면 200
|
||||
# /login.do 는 POST 처리 — GET 으로 probe 시 405/잘못된 응답 가능성 있어 회피
|
||||
PROBE_URL="http://localhost:$DEPLOY_HTTP_PORT/monitoring/loginForm.do"
|
||||
DEADLINE=$(($(date +%s) + 300))
|
||||
STATUS=000
|
||||
while [ $(date +%s) -lt $DEADLINE ]; do
|
||||
STATUS=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 3 \
|
||||
"http://localhost:$DEPLOY_HTTP_PORT/monitoring/login.do" 2>/dev/null || echo "000")
|
||||
"$PROBE_URL" 2>/dev/null || echo "000")
|
||||
case "$STATUS" in
|
||||
200|302|401)
|
||||
echo "Readiness OK (/monitoring/login.do HTTP $STATUS)"
|
||||
echo "Readiness OK ($PROBE_URL HTTP $STATUS)"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
@@ -122,8 +125,13 @@ jobs:
|
||||
case "$STATUS" in
|
||||
200|302|401) ;;
|
||||
*)
|
||||
echo "::error::Readiness probe failed within 180s (last HTTP status: $STATUS)"
|
||||
[ -f "$LOG" ] && tail -c +$((BEFORE+1)) "$LOG" | tail -100
|
||||
echo "::error::Readiness probe failed within 300s (last HTTP status: $STATUS)"
|
||||
echo "--- last 300 lines of catalina log ---"
|
||||
[ -f "$LOG" ] && tail -c +$((BEFORE+1)) "$LOG" | tail -300
|
||||
echo "--- systemd unit status ---"
|
||||
systemctl --user status eapim-admin --no-pager || true
|
||||
echo "--- listening ports ---"
|
||||
ss -tlnp 2>/dev/null | grep -E ":$DEPLOY_HTTP_PORT\b" || echo "(port $DEPLOY_HTTP_PORT 미점유)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -46,8 +46,39 @@
|
||||
var icon = rowObject.hasAttachment ? '<img src="' + iconPath + '" alt="File Attached" style="vertical-align: middle; margin-left: 5px; width: 16px; height: 16px;">' : '';
|
||||
return cellvalue + icon;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function deleteSelectedRows() {
|
||||
var selectedIds = $("#grid").jqGrid('getGridParam', 'selarrrow');
|
||||
|
||||
if (selectedIds.length === 0) {
|
||||
alert('항목을 선택하세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm('선택된 ' + selectedIds.length + '개 항목을 삭제하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: url,
|
||||
dataType: "json",
|
||||
data: {
|
||||
cmd: 'DELETE_BULK',
|
||||
ids: selectedIds.join(',')
|
||||
},
|
||||
success: function(data) {
|
||||
alert('삭제되었습니다.');
|
||||
$("#grid").jqGrid('clearGridData', true);
|
||||
$("#grid").trigger("reloadGrid");
|
||||
},
|
||||
error: function(e) {
|
||||
alert('삭제 중 오류가 발생했습니다.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function init(){
|
||||
$.ajax({
|
||||
type : "POST",
|
||||
@@ -103,6 +134,8 @@
|
||||
autowidth: true,
|
||||
viewrecords: true,
|
||||
rowList : eval('[${rmsDefaultRowList}]'),
|
||||
multiselect: true,
|
||||
multiboxonly: true,
|
||||
ondblClickRow: function(rowId) {
|
||||
var rowData = $(this).getRowData(rowId);
|
||||
var id = rowData['id'];
|
||||
@@ -157,6 +190,10 @@
|
||||
goNav(url2);
|
||||
});
|
||||
|
||||
$("#btn_delete_selected").click(function(){
|
||||
deleteSelectedRows();
|
||||
});
|
||||
|
||||
$("select[name=searchUseYn], input[name^=search]").keydown(function(key){
|
||||
if (key.keyCode == 13){
|
||||
$("#btn_search").click();
|
||||
@@ -179,6 +216,7 @@
|
||||
<div class="search_wrap">
|
||||
<button type="button" class="cssbtn" id="btn_new" level="W"><i class="material-icons">add</i> <%= localeMessage.getString("button.new") %></button>
|
||||
<button type="button" class="cssbtn" id="btn_search" level="R"><i class="material-icons">search</i> <%= localeMessage.getString("button.search") %></button>
|
||||
<button type="button" class="cssbtn" id="btn_delete_selected" level="W" style="background-color: #dc3545; border-color: #dc3545; color: white;"><i class="material-icons">delete_forever</i> 선택 삭제</button>
|
||||
</div>
|
||||
<div class="title">공지사항 목록<span class="tooltip">공지사항을 관리합니다.</span></div>
|
||||
<form id="ajaxForm" onsubmit="return false;">
|
||||
|
||||
@@ -15,4 +15,6 @@ public interface EMSScheduler {
|
||||
|
||||
void deleteAndAddJob(Scheduler scheduler, String jobName) throws SchedulerException, ClassNotFoundException;
|
||||
|
||||
void reloadJob(String jobName) throws SchedulerException, ClassNotFoundException;
|
||||
|
||||
}
|
||||
|
||||
@@ -120,6 +120,32 @@ public class EMSSchedulerImpl implements EMSScheduler {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reloadJob(String jobName) throws SchedulerException, ClassNotFoundException {
|
||||
JobInfo jobInfo = jobInfoService.getById(jobName);
|
||||
if (CLUSTERED.equals(jobInfo.getInstanceName())) {
|
||||
reloadClusteredJob(jobInfo);
|
||||
} else {
|
||||
deleteAndAddJob(scheduler, jobInfo);
|
||||
deleteJob(clusteredScheduler, jobName);
|
||||
}
|
||||
}
|
||||
|
||||
private void reloadClusteredJob(JobInfo jobInfo) throws SchedulerException, ClassNotFoundException {
|
||||
TriggerKey triggerKey = new TriggerKey(getTriggerName(jobInfo.getId()), Scheduler.DEFAULT_GROUP);
|
||||
Trigger existing = clusteredScheduler.getTrigger(triggerKey);
|
||||
if (existing != null) {
|
||||
CronTrigger newTrigger = newTrigger()
|
||||
.withIdentity(triggerKey)
|
||||
.withSchedule(cronSchedule(jobInfo.getCronExp()))
|
||||
.build();
|
||||
Date ft = clusteredScheduler.rescheduleJob(triggerKey, newTrigger);
|
||||
log.info("{} rescheduled at: {} cron: {}", jobInfo.getId(), ft, jobInfo.getCronExp());
|
||||
} else {
|
||||
deleteAndAddJob(clusteredScheduler, jobInfo);
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteJob(Scheduler inScheduler, String jobNm) throws SchedulerException {
|
||||
JobKey key = new JobKey(jobNm, Scheduler.DEFAULT_GROUP);
|
||||
JobDetail jobDetail = inScheduler.getJobDetail(key);
|
||||
|
||||
@@ -45,6 +45,9 @@ public class SchedulerManController extends BaseAnnotationController {
|
||||
@Qualifier("monitoringSyncAgent")
|
||||
private MonitoringSyncAgent monitoringSyncAgent;
|
||||
|
||||
@Autowired
|
||||
private EMSScheduler emsScheduler;
|
||||
|
||||
@GetMapping(value = "/common/scheduler/schedulerMan.view")
|
||||
public String viewList() {
|
||||
return "/common/scheduler/schedulerMan";
|
||||
@@ -96,6 +99,11 @@ public class SchedulerManController extends BaseAnnotationController {
|
||||
@PostMapping(value = "/common/scheduler/schedulerMan.json", params = "cmd=UPDATE")
|
||||
public ResponseEntity<Void> update(JobInfoUI jobInfoUI) {
|
||||
service.update(jobInfoUI);
|
||||
try {
|
||||
emsScheduler.reloadJob(jobInfoUI.getId());
|
||||
} catch (Exception e) {
|
||||
logger.error("reloadJob error on current server", e);
|
||||
}
|
||||
syncReloadScheduler(jobInfoUI.getId());
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
@@ -115,7 +123,7 @@ public class SchedulerManController extends BaseAnnotationController {
|
||||
String url = "/monitoring/schedulerReload.do";
|
||||
|
||||
try {
|
||||
ret = monitoringSyncAgent.sync(url, paramMap);
|
||||
ret = monitoringSyncAgent.targetSync(url, paramMap);
|
||||
} catch (Exception e) {
|
||||
logger.error("syncReloadScheduler error", e);
|
||||
} finally {
|
||||
|
||||
+6
@@ -13,10 +13,16 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@Transactional(transactionManager = "transactionManagerForEMS")
|
||||
public class PortalNoticeService extends AbstractEMSDataSerivce<PortalNotice, String, PortalNoticeRepository> {
|
||||
|
||||
public void deleteByIds(List<String> ids) {
|
||||
repository.deleteAllById(ids);
|
||||
}
|
||||
|
||||
public Page<PortalNotice> findAll(Pageable pageable, PortalNoticeUISearch portalNoticeUISearch) {
|
||||
QPortalNotice qPortalNotice = QPortalNotice.portalNotice;
|
||||
|
||||
|
||||
@@ -39,11 +39,10 @@ public interface ApiStatusRepository extends BaseRepository<ApiStatus, String> {
|
||||
+ " FROM ("
|
||||
+ " SELECT A.EAISVCNAME"
|
||||
+ " , NVL(B.STATUS_CODE, 'N') AS STATUS_CODE"
|
||||
//TODO: 게시판 임시로 생성하여 사용. 유차장이 완료하면 변경할것
|
||||
+ " , NVL(( SELECT 'Y' FROM PTL_NOTICE_API X "
|
||||
+ " WHERE EXISTS (SELECT 1 FROM EMSAPP.PTL_NOTICE Y WHERE X.NOTICE_ID = Y.ID)"
|
||||
//+ " AND TO_CHAR(SYSDATE, 'YYYYMMDDHH24MI') BETWEEN START_TIME AND END_TIME) "
|
||||
+ " AND A.EAISVCNAME = X.API_NAME),'N') AS CTRL_YN "
|
||||
+ " , NVL(( SELECT MAX('Y') FROM EMSAPP.DJB_APISTATUS_INCIDENT_API X"
|
||||
+ " WHERE EXISTS (SELECT 1 FROM EMSAPP.DJB_APISTATUS_INCIDENT Y WHERE X.INCIDENT_ID = Y.INCIDENT_ID "
|
||||
+ " AND SYSTIMESTAMP BETWEEN STARTED_AT AND END_AT) "
|
||||
+ " AND A.EAISVCNAME = X.API_ID),'N') AS CTRL_YN "
|
||||
+ " , (SELECT CASE WHEN TOTAL = 0 THEN 'X'"
|
||||
+ " WHEN (TOTAL - SUCCESS) * 100 / TOTAL > :errorRate THEN 'Y'"
|
||||
+ " ELSE 'N' END"
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.eactive.eai.rms.ext.djb.job;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.quartz.DisallowConcurrentExecution;
|
||||
@@ -68,10 +71,24 @@ public class ApiStatusMonitorJob implements Job {
|
||||
|
||||
@Override
|
||||
public void execute(JobExecutionContext context) throws JobExecutionException {
|
||||
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
|
||||
|
||||
// execute.instances 가 지정된 경우 해당 인스턴스에서만 실행
|
||||
String allowedInstances = jobDataMap.getString("execute.instances");
|
||||
if (StringUtils.isNotEmpty(allowedInstances)) {
|
||||
Set<String> instanceSet = Arrays.stream(allowedInstances.split(","))
|
||||
.map(s -> s.trim().toLowerCase())
|
||||
.collect(Collectors.toSet());
|
||||
String currentInstance = StringUtils.defaultString(System.getProperty("inst.Name")).trim().toLowerCase();
|
||||
if (!instanceSet.contains(currentInstance)) {
|
||||
log.debug("Job 실행 인스턴스 제한 - 현재: {}, 허용: {}", currentInstance, allowedInstances);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
log.info("*** START ApiStatusUpdateJob run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm"));
|
||||
|
||||
// Job 파라미터 로깅
|
||||
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
|
||||
logJobParameters(jobDataMap);
|
||||
|
||||
HashMap<String, String> param = this.checkParameters(jobDataMap);
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package com.eactive.eai.rms.ext.djb.job;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.quartz.DisallowConcurrentExecution;
|
||||
import org.quartz.Job;
|
||||
import org.quartz.JobDataMap;
|
||||
@@ -48,6 +52,21 @@ public class InflowTokenMonitorJob implements Job {
|
||||
|
||||
@Override
|
||||
public void execute(JobExecutionContext context) throws JobExecutionException {
|
||||
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
|
||||
|
||||
// execute.instances 가 지정된 경우 해당 인스턴스에서만 실행
|
||||
String allowedInstances = jobDataMap.getString("execute.instances");
|
||||
if (StringUtils.isNotEmpty(allowedInstances)) {
|
||||
Set<String> instanceSet = Arrays.stream(allowedInstances.split(","))
|
||||
.map(s -> s.trim().toLowerCase())
|
||||
.collect(Collectors.toSet());
|
||||
String currentInstance = StringUtils.defaultString(System.getProperty("inst.Name")).trim().toLowerCase();
|
||||
if (!instanceSet.contains(currentInstance)) {
|
||||
log.debug("Job 실행 인스턴스 제한 - 현재: {}, 허용: {}", currentInstance, allowedInstances);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
log.info("*** START InflowTokenFailMonitorJob run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm"));
|
||||
|
||||
long execMinute = 1; //default 배치 실행주기(분)
|
||||
|
||||
@@ -5,14 +5,20 @@ import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
|
||||
import com.eactive.eai.rms.common.util.CommonUtil;
|
||||
import com.eactive.eai.rms.onl.apim.portalInquiry.PortalInquiryClosingService;
|
||||
import com.eactive.eai.rms.onl.common.util.DateUtil;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.quartz.DisallowConcurrentExecution;
|
||||
import org.quartz.Job;
|
||||
import org.quartz.JobDataMap;
|
||||
import org.quartz.JobExecutionContext;
|
||||
import org.quartz.JobExecutionException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 매일 00:05 실행 - RESPONDED 상태 문의글 자동 종료 배치
|
||||
*
|
||||
@@ -44,6 +50,21 @@ public class PortalInquiryClosingJob implements Job {
|
||||
|
||||
@Override
|
||||
public void execute(JobExecutionContext context) throws JobExecutionException {
|
||||
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
|
||||
|
||||
// execute.instances 가 지정된 경우 해당 인스턴스에서만 실행
|
||||
String allowedInstances = jobDataMap.getString("execute.instances");
|
||||
if (StringUtils.isNotEmpty(allowedInstances)) {
|
||||
Set<String> instanceSet = Arrays.stream(allowedInstances.split(","))
|
||||
.map(s -> s.trim().toLowerCase())
|
||||
.collect(Collectors.toSet());
|
||||
String currentInstance = StringUtils.defaultString(System.getProperty("inst.Name")).trim().toLowerCase();
|
||||
if (!instanceSet.contains(currentInstance)) {
|
||||
log.debug("Job 실행 인스턴스 제한 - 현재: {}, 허용: {}", currentInstance, allowedInstances);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
log.info("*** START PortalInquiryCommentClosingService run({})", CommonUtil.getToday("yyyy-MM-dd HH:mm"));
|
||||
|
||||
int closingDays = DEFAULT_CLOSING_DAYS;
|
||||
|
||||
@@ -80,4 +80,10 @@ public class PortalNoticeManController extends BaseAnnotationController {
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PostMapping(value = "/onl/apim/portalnotice/portalNoticeMan.json", params = "cmd=DELETE_BULK")
|
||||
public ResponseEntity<String> deleteBulk(String ids) {
|
||||
portalNoticeManService.deleteBulk(ids);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
@@ -282,4 +283,20 @@ public class PortalNoticeManService extends BaseService {
|
||||
portalNoticeService.deleteById(id);
|
||||
}
|
||||
|
||||
public void deleteBulk(String ids) {
|
||||
if (StringUtils.isBlank(ids)) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> idList = Arrays.asList(ids.split(","));
|
||||
|
||||
// 첨부파일 정리 (단건 삭제와 동일한 처리)
|
||||
idList.forEach(id -> {
|
||||
PortalNotice portalNotice = portalNoticeService.getById(id);
|
||||
Optional.ofNullable(portalNotice.getFileId()).ifPresent(fileService::deleteFile);
|
||||
});
|
||||
|
||||
portalNoticeService.deleteByIds(idList);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+12
-15
@@ -1,10 +1,10 @@
|
||||
package com.eactive.eai.rms.onl.common.controller;
|
||||
|
||||
import com.eactive.eai.rms.common.interceptor.InterceptorSkipController;
|
||||
import com.eactive.eai.rms.common.scheduler.EMSScheduler;
|
||||
import com.eactive.eai.rms.onl.manage.adapter.spcftmadapter.AdapterJobInfo;
|
||||
import com.eactive.eai.rms.onl.manage.adapter.spcftmadapter.AdapterSchedulerUtil;
|
||||
import com.eactive.eai.rms.onl.manage.adapter.spcftmadapter.SpcfTmAdapterDao;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.quartz.Scheduler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -15,9 +15,11 @@ import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import com.eactive.eai.rms.common.interceptor.InterceptorSkipController;
|
||||
import com.eactive.eai.rms.common.scheduler.EMSScheduler;
|
||||
import com.eactive.eai.rms.onl.manage.adapter.spcftmadapter.AdapterJobInfo;
|
||||
import com.eactive.eai.rms.onl.manage.adapter.spcftmadapter.AdapterSchedulerUtil;
|
||||
import com.eactive.eai.rms.onl.manage.adapter.spcftmadapter.SpcfTmAdapterDao;
|
||||
|
||||
@Controller
|
||||
public class SchedulerReloadController implements InterceptorSkipController {
|
||||
@@ -34,19 +36,14 @@ public class SchedulerReloadController implements InterceptorSkipController {
|
||||
private EMSScheduler emsScheduler;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("scheduler")
|
||||
@Qualifier("scheduler")
|
||||
private Scheduler scheduler;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("clusteredScheduler")
|
||||
private Scheduler clusteredScheduler;
|
||||
|
||||
@RequestMapping("/schedulerReload.do")
|
||||
public ResponseEntity<Map<String, Object>> rmsSchedulerReload(String jobName) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
try {
|
||||
emsScheduler.deleteAndAddJob(scheduler, jobName);
|
||||
emsScheduler.deleteAndAddJob(clusteredScheduler, jobName);
|
||||
emsScheduler.reloadJob(jobName);
|
||||
result.put("status", "success");
|
||||
} catch (Exception e) {
|
||||
logger.error("", e);
|
||||
|
||||
Reference in New Issue
Block a user