mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-28 07:36:38 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fef8eeb32 |
@@ -2,7 +2,7 @@ name: Build and Push Images Based on Version
|
||||
|
||||
# 在这里定义统一版本号
|
||||
env:
|
||||
VERSION: "2.0.1-beta"
|
||||
VERSION: "2.0.2-beta"
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # 获取所有历史和 tags
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Display version
|
||||
run: |
|
||||
@@ -67,22 +67,38 @@ jobs:
|
||||
sudo mv upx-4.2.1-amd64_linux/upx /usr/local/bin/
|
||||
rm -rf upx-4.2.1-amd64_linux*
|
||||
|
||||
- name: Build GOST binary
|
||||
- name: Build GOST binary (AMD64)
|
||||
working-directory: ./go-gost
|
||||
run: |
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o gost
|
||||
|
||||
- name: Compress with UPX
|
||||
- name: Build GOST binary (ARM64)
|
||||
working-directory: ./go-gost
|
||||
run: |
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o gost-arm64
|
||||
|
||||
- name: Compress with UPX (AMD64)
|
||||
working-directory: ./go-gost
|
||||
run: |
|
||||
upx --best --lzma gost
|
||||
|
||||
- name: Upload GOST artifact
|
||||
- name: Compress with UPX (ARM64)
|
||||
working-directory: ./go-gost
|
||||
run: |
|
||||
upx --best --lzma gost-arm64
|
||||
|
||||
- name: Upload GOST AMD64 artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gost-binary
|
||||
name: gost-binary-amd64
|
||||
path: ./go-gost/gost
|
||||
|
||||
- name: Upload GOST ARM64 artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gost-binary-arm64
|
||||
path: ./go-gost/gost-arm64
|
||||
|
||||
build-vite:
|
||||
name: Build & Push Vite Frontend
|
||||
needs: check-version
|
||||
@@ -109,7 +125,7 @@ jobs:
|
||||
run: |
|
||||
VERSION="${{ needs.check-version.outputs.version }}"
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
-t bqlpfy/vite-frontend:latest \
|
||||
-t bqlpfy/vite-frontend:${VERSION} \
|
||||
@@ -153,7 +169,7 @@ jobs:
|
||||
run: |
|
||||
VERSION="${{ needs.check-version.outputs.version }}"
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
-t bqlpfy/springboot-backend:latest \
|
||||
-t bqlpfy/springboot-backend:${VERSION} \
|
||||
@@ -169,11 +185,22 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Download GOST binary
|
||||
- name: Download GOST AMD64 binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: gost-binary
|
||||
path: ./artifacts
|
||||
name: gost-binary-amd64
|
||||
path: ./artifacts/amd64
|
||||
|
||||
- name: Download GOST ARM64 binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: gost-binary-arm64
|
||||
path: ./artifacts/arm64
|
||||
|
||||
- name: Rename binaries
|
||||
run: |
|
||||
mv ./artifacts/amd64/gost ./artifacts/gost
|
||||
mv ./artifacts/arm64/gost-arm64 ./artifacts/gost-arm64
|
||||
|
||||
- name: Create Release
|
||||
env:
|
||||
@@ -202,9 +229,10 @@ jobs:
|
||||
- **Commit**: [\`${GITHUB_SHA:0:7}\`](https://github.com/${{ github.repository }}/commit/${{ github.sha }})
|
||||
|
||||
## 📦 Components
|
||||
- **GOST Binary** (Linux AMD64, compressed with UPX)
|
||||
- **Vite Frontend** Docker Image: \`bqlpfy/vite-frontend:${VERSION}\` / \`bqlpfy/vite-frontend:latest\`
|
||||
- **Spring Boot Backend** Docker Image: \`bqlpfy/springboot-backend:${VERSION}\` / \`bqlpfy/springboot-backend:latest\`
|
||||
- **GOST Binary AMD64** - \`gost\` (Linux AMD64, compressed with UPX)
|
||||
- **GOST Binary ARM64** - \`gost-arm64\` (Linux ARM64, compressed with UPX)
|
||||
- **Vite Frontend** Docker Image: \`bqlpfy/vite-frontend:${VERSION}\` / \`bqlpfy/vite-frontend:latest\` (支持 AMD64/ARM64)
|
||||
- **Spring Boot Backend** Docker Image: \`bqlpfy/springboot-backend:${VERSION}\` / \`bqlpfy/springboot-backend:latest\` (支持 AMD64/ARM64)
|
||||
|
||||
## 📥 Installation Files
|
||||
- \`install.sh\` - GOST 客户端安装脚本
|
||||
@@ -218,8 +246,9 @@ jobs:
|
||||
--repo ${{ github.repository }}
|
||||
|
||||
# 上传所有文件到 release
|
||||
echo "📤 上传 GOST 二进制..."
|
||||
echo "📤 上传 GOST 二进制文件..."
|
||||
gh release upload "${VERSION}" ./artifacts/gost --clobber
|
||||
gh release upload "${VERSION}" ./artifacts/gost-arm64 --clobber
|
||||
|
||||
echo "📤 上传安装脚本..."
|
||||
gh release upload "${VERSION}" ./install.sh --clobber
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
backend:
|
||||
image: bqlpfy/springboot-backend:2.0.1-beta
|
||||
image: bqlpfy/springboot-backend:2.0.2-beta
|
||||
container_name: springboot-backend
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@@ -23,7 +23,7 @@ services:
|
||||
start_period: 60s
|
||||
|
||||
frontend:
|
||||
image: bqlpfy/vite-frontend:2.0.1-beta
|
||||
image: bqlpfy/vite-frontend:2.0.2-beta
|
||||
container_name: vite-frontend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
backend:
|
||||
image: bqlpfy/springboot-backend:2.0.1-beta
|
||||
image: bqlpfy/springboot-backend:2.0.2-beta
|
||||
container_name: springboot-backend
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@@ -23,7 +23,7 @@ services:
|
||||
start_period: 60s
|
||||
|
||||
frontend:
|
||||
image: bqlpfy/vite-frontend:2.0.1-beta
|
||||
image: bqlpfy/vite-frontend:2.0.2-beta
|
||||
container_name: vite-frontend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# 下载地址
|
||||
DOWNLOAD_URL="https://github.com/bqlpfy/flux-panel/releases/download/2.0.1-beta/gost"
|
||||
DOWNLOAD_URL="https://github.com/bqlpfy/flux-panel/releases/download/2.0.2-beta/gost"
|
||||
INSTALL_DIR="/etc/flux_agent"
|
||||
COUNTRY=$(curl -s https://ipinfo.io/country)
|
||||
if [ "$COUNTRY" = "CN" ]; then
|
||||
|
||||
+2
-2
@@ -8,8 +8,8 @@ export LC_ALL=C
|
||||
|
||||
|
||||
# 全局下载地址配置
|
||||
DOCKER_COMPOSEV4_URL="https://github.com/bqlpfy/flux-panel/releases/download/2.0.1-beta/docker-compose-v4.yml"
|
||||
DOCKER_COMPOSEV6_URL="https://github.com/bqlpfy/flux-panel/releases/download/2.0.1-beta/docker-compose-v6.yml"
|
||||
DOCKER_COMPOSEV4_URL="https://github.com/bqlpfy/flux-panel/releases/download/2.0.2-beta/docker-compose-v4.yml"
|
||||
DOCKER_COMPOSEV6_URL="https://github.com/bqlpfy/flux-panel/releases/download/2.0.2-beta/docker-compose-v6.yml"
|
||||
|
||||
COUNTRY=$(curl -s https://ipinfo.io/country)
|
||||
if [ "$COUNTRY" = "CN" ]; then
|
||||
|
||||
@@ -21,4 +21,6 @@ public class ForwardDto {
|
||||
private String remoteAddr;
|
||||
|
||||
private String strategy;
|
||||
|
||||
private Integer inPort;
|
||||
}
|
||||
@@ -25,4 +25,6 @@ public class ForwardUpdateDto {
|
||||
|
||||
private String strategy;
|
||||
|
||||
private Integer inPort;
|
||||
|
||||
}
|
||||
@@ -57,5 +57,5 @@ public interface TunnelService extends IService<Tunnel> {
|
||||
*/
|
||||
R diagnoseTunnel(Long tunnelId);
|
||||
|
||||
Integer getNodePort(Long nodeId, Integer type);
|
||||
Integer getNodePort(Long nodeId, Integer type, Integer port);
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
||||
|
||||
List<ChainTunnel> chainTunnels = chainTunnelService.list(new QueryWrapper<ChainTunnel>().eq("tunnel_id", tunnel.getId()).eq("chain_type", 1));
|
||||
for (ChainTunnel chainTunnel : chainTunnels) {
|
||||
Integer nodePort = tunnelService.getNodePort(chainTunnel.getNodeId(), 2);
|
||||
Integer nodePort = tunnelService.getNodePort(chainTunnel.getNodeId(), 2, forwardDto.getInPort());
|
||||
|
||||
ForwardPort forwardPort = new ForwardPort();
|
||||
forwardPort.setForwardId(forward.getId());
|
||||
@@ -205,7 +205,7 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
||||
return R.err(permissionResult.getErrorMessage());
|
||||
}
|
||||
|
||||
UserTunnel userTunnel = null;
|
||||
UserTunnel userTunnel;
|
||||
if (currentUser.getRoleId() != 0) {
|
||||
userTunnel = getUserTunnel(currentUser.getUserId(), tunnel.getId().intValue());
|
||||
if (userTunnel == null) {
|
||||
@@ -236,6 +236,13 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
||||
if (forwardPort == null){
|
||||
return R.err("部分节点不存在1");
|
||||
}
|
||||
if (forwardUpdateDto.getInPort() != null && !forwardUpdateDto.getInPort().equals(forwardPort.getPort())) {
|
||||
Integer nodePort = tunnelService.getNodePort(forwardPort.getNodeId(), 2, forwardUpdateDto.getInPort());
|
||||
if (Objects.equals(nodePort, forwardUpdateDto.getInPort())) {
|
||||
forwardPort.setPort(nodePort);
|
||||
forwardPortService.updateById(forwardPort);
|
||||
}
|
||||
}
|
||||
GostUtil.AddAndUpdateService(serviceName, limiter, node, existForward, forwardPort, tunnel, "UpdateService");
|
||||
}
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ public class NodeServiceImpl extends ServiceImpl<NodeMapper, Node> implements No
|
||||
ViteConfig viteConfig = viteConfigService.getOne(new QueryWrapper<ViteConfig>().eq("name", "ip"));
|
||||
if (viteConfig == null) return R.err("请先前往网站配置中设置ip");
|
||||
StringBuilder command = new StringBuilder();
|
||||
command.append("curl -L https://github.com/bqlpfy/flux-panel/releases/download/2.0.1-beta/install.sh")
|
||||
command.append("curl -L https://github.com/bqlpfy/flux-panel/releases/download/2.0.2-beta/install.sh")
|
||||
.append(" -o ./install.sh && chmod +x ./install.sh && ");
|
||||
String processedServerAddr = processServerAddress(viteConfig.getValue());
|
||||
command.append("./install.sh")
|
||||
|
||||
@@ -81,7 +81,7 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
||||
Node node = nodeService.getById(chain_node.getNodeId());
|
||||
if (node == null) return R.err("节点不存在");
|
||||
nodes.put(node.getId(), node);
|
||||
Integer nodePort = getNodePort(chain_node.getNodeId(), 1);
|
||||
Integer nodePort = getNodePort(chain_node.getNodeId(), 1, null);
|
||||
chain_node.setPort(nodePort);
|
||||
chain_node.setInx(inx); // 设置转发链序号
|
||||
chainTunnels.add(chain_node);
|
||||
@@ -93,7 +93,7 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
||||
Node node = nodeService.getById(out_node.getNodeId());
|
||||
if (node == null) return R.err("节点不存在");
|
||||
nodes.put(node.getId(), node);
|
||||
Integer nodePort = getNodePort(out_node.getNodeId(), 1);
|
||||
Integer nodePort = getNodePort(out_node.getNodeId(), 1, null);
|
||||
out_node.setPort(nodePort);
|
||||
chainTunnels.add(out_node);
|
||||
}
|
||||
@@ -472,7 +472,7 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getNodePort(Long nodeId,Integer type) {
|
||||
public Integer getNodePort(Long nodeId,Integer type, Integer port) {
|
||||
|
||||
Node node = nodeService.getById(nodeId);
|
||||
if (node == null){
|
||||
@@ -508,6 +508,9 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
||||
if (type == 1) {
|
||||
return availablePorts.getLast();
|
||||
}else {
|
||||
if (port != null && availablePorts.contains(port)) {
|
||||
return port;
|
||||
}
|
||||
return availablePorts.getFirst();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ export type SiteConfig = typeof siteConfig;
|
||||
|
||||
// 缓存相关常量
|
||||
const CACHE_PREFIX = 'vite_config_';
|
||||
const VERSION = "2.0.1-beta";
|
||||
const VERSION = "2.0.2-beta";
|
||||
const APP_VERSION = "1.0.3";
|
||||
|
||||
const getInitialConfig = () => {
|
||||
|
||||
@@ -406,6 +406,14 @@ export default function ForwardPage() {
|
||||
newErrors.tunnelId = '请选择关联隧道';
|
||||
}
|
||||
|
||||
// 验证入口端口(可选,如果填写则验证)
|
||||
if (form.inPort !== null && form.inPort !== undefined) {
|
||||
const port = Number(form.inPort);
|
||||
if (isNaN(port) || port < 1 || port > 65535) {
|
||||
newErrors.inPort = '端口必须在 1-65535 之间';
|
||||
}
|
||||
}
|
||||
|
||||
if (!form.remoteAddr.trim()) {
|
||||
newErrors.remoteAddr = '请输入远程地址';
|
||||
} else {
|
||||
@@ -526,6 +534,7 @@ export default function ForwardPage() {
|
||||
userId: form.userId,
|
||||
name: form.name,
|
||||
tunnelId: form.tunnelId,
|
||||
inPort: form.inPort,
|
||||
remoteAddr: processedRemoteAddr,
|
||||
strategy: addressCount > 1 ? form.strategy : 'fifo'
|
||||
};
|
||||
@@ -535,6 +544,7 @@ export default function ForwardPage() {
|
||||
const createData = {
|
||||
name: form.name,
|
||||
tunnelId: form.tunnelId,
|
||||
inPort: form.inPort,
|
||||
remoteAddr: processedRemoteAddr,
|
||||
strategy: addressCount > 1 ? form.strategy : 'fifo'
|
||||
};
|
||||
@@ -1605,6 +1615,24 @@ export default function ForwardPage() {
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Input
|
||||
label="入口端口"
|
||||
placeholder="留空则自动分配可用端口"
|
||||
type="number"
|
||||
value={form.inPort !== null ? form.inPort.toString() : ''}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
inPort: value ? parseInt(value) : null
|
||||
}));
|
||||
}}
|
||||
isInvalid={!!errors.inPort}
|
||||
errorMessage={errors.inPort}
|
||||
variant="bordered"
|
||||
description="指定入口端口,留空则从节点可用端口中自动分配"
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label="远程地址"
|
||||
|
||||
@@ -357,6 +357,9 @@ export default function TunnelPage() {
|
||||
.map(group => group.filter(node => node.nodeId !== -1))
|
||||
.filter(group => group.length > 0); // 移除空组
|
||||
|
||||
// 过滤掉出口节点中的占位节点
|
||||
const cleanedOutNodeId = (form.outNodeId || []).filter(node => node.nodeId !== -1);
|
||||
|
||||
// 将换行符分隔的IP转换为逗号分隔
|
||||
const inIpString = form.inIp
|
||||
.split('\n')
|
||||
@@ -367,6 +370,7 @@ export default function TunnelPage() {
|
||||
const data = {
|
||||
...form,
|
||||
inIp: inIpString,
|
||||
outNodeId: cleanedOutNodeId,
|
||||
chainNodes: cleanedChainNodes
|
||||
};
|
||||
|
||||
@@ -1052,7 +1056,7 @@ export default function TunnelPage() {
|
||||
label="节点"
|
||||
placeholder="请选择出口节点(可多选)"
|
||||
selectionMode="multiple"
|
||||
selectedKeys={form.outNodeId ? form.outNodeId.map(ct => ct.nodeId.toString()) : []}
|
||||
selectedKeys={form.outNodeId ? form.outNodeId.filter(ct => ct.nodeId !== -1).map(ct => ct.nodeId.toString()) : []}
|
||||
disabledKeys={[
|
||||
...nodes.filter(node => node.status !== 1).map(node => node.id.toString()),
|
||||
...form.inNodeId.map(ct => ct.nodeId.toString()),
|
||||
@@ -1061,12 +1065,17 @@ export default function TunnelPage() {
|
||||
onSelectionChange={(keys) => {
|
||||
const selectedIds = Array.from(keys).map(key => parseInt(key as string));
|
||||
const currentOutNodes = form.outNodeId || [];
|
||||
const protocol = currentOutNodes.length > 0 ? currentOutNodes[0].protocol || 'tls' : 'tls';
|
||||
const strategy = currentOutNodes.length > 0 ? currentOutNodes[0].strategy || 'round' : 'round';
|
||||
|
||||
let protocol = 'tls';
|
||||
let strategy = 'round';
|
||||
if (currentOutNodes.length > 0) {
|
||||
protocol = currentOutNodes[0].protocol || 'tls';
|
||||
strategy = currentOutNodes[0].strategy || 'round';
|
||||
}
|
||||
|
||||
const realNodes = currentOutNodes.filter(ct => ct.nodeId !== -1);
|
||||
const newOutNodeId: ChainTunnel[] = selectedIds.map(nodeId => {
|
||||
// 保留已有的配置或使用默认值
|
||||
const existing = currentOutNodes.find(ct => ct.nodeId === nodeId);
|
||||
const existing = realNodes.find(ct => ct.nodeId === nodeId);
|
||||
return existing || { nodeId, chainType: 3, protocol, strategy };
|
||||
});
|
||||
setForm(prev => ({ ...prev, outNodeId: newOutNodeId }));
|
||||
@@ -1116,14 +1125,30 @@ export default function TunnelPage() {
|
||||
<Select
|
||||
label="协议"
|
||||
placeholder="选择协议"
|
||||
selectedKeys={[form.outNodeId && form.outNodeId.length > 0 ? form.outNodeId[0].protocol || 'tls' : 'tls']}
|
||||
selectedKeys={[(() => {
|
||||
if (!form.outNodeId || form.outNodeId.length === 0) return 'tls';
|
||||
return form.outNodeId[0].protocol || 'tls';
|
||||
})()]}
|
||||
onSelectionChange={(keys) => {
|
||||
const selectedKey = Array.from(keys)[0] as string;
|
||||
if (selectedKey && form.outNodeId) {
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
outNodeId: (prev.outNodeId || []).map(ct => ({ ...ct, protocol: selectedKey }))
|
||||
}));
|
||||
if (selectedKey) {
|
||||
setForm(prev => {
|
||||
const currentOutNodes = prev.outNodeId || [];
|
||||
const currentStrategy = currentOutNodes.length > 0 ? currentOutNodes[0].strategy || 'round' : 'round';
|
||||
|
||||
if (currentOutNodes.length === 0) {
|
||||
// 如果还没有出口节点,创建一个占位节点保存设置
|
||||
return {
|
||||
...prev,
|
||||
outNodeId: [{ nodeId: -1, chainType: 3, protocol: selectedKey, strategy: currentStrategy }]
|
||||
};
|
||||
}
|
||||
// 更新所有出口节点的协议
|
||||
return {
|
||||
...prev,
|
||||
outNodeId: currentOutNodes.map(ct => ({ ...ct, protocol: selectedKey }))
|
||||
};
|
||||
});
|
||||
}
|
||||
}}
|
||||
isInvalid={!!errors.protocol}
|
||||
@@ -1147,14 +1172,28 @@ export default function TunnelPage() {
|
||||
<Select
|
||||
label="负载策略"
|
||||
placeholder="选择策略"
|
||||
selectedKeys={[form.outNodeId && form.outNodeId.length > 0 ? form.outNodeId[0].strategy || 'round' : 'round']}
|
||||
selectedKeys={[(() => {
|
||||
if (!form.outNodeId || form.outNodeId.length === 0) return 'round';
|
||||
return form.outNodeId[0].strategy || 'round';
|
||||
})()]}
|
||||
onSelectionChange={(keys) => {
|
||||
const selectedKey = Array.from(keys)[0] as string;
|
||||
if (selectedKey && form.outNodeId) {
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
outNodeId: (prev.outNodeId || []).map(ct => ({ ...ct, strategy: selectedKey }))
|
||||
}));
|
||||
if (selectedKey) {
|
||||
setForm(prev => {
|
||||
const currentOutNodes = prev.outNodeId || [];
|
||||
const currentProtocol = currentOutNodes.length > 0 ? currentOutNodes[0].protocol || 'tls' : 'tls';
|
||||
|
||||
if (currentOutNodes.length === 0) {
|
||||
return {
|
||||
...prev,
|
||||
outNodeId: [{ nodeId: -1, chainType: 3, protocol: currentProtocol, strategy: selectedKey }]
|
||||
};
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
outNodeId: currentOutNodes.map(ct => ({ ...ct, strategy: selectedKey }))
|
||||
};
|
||||
});
|
||||
}
|
||||
}}
|
||||
variant="bordered"
|
||||
|
||||
Reference in New Issue
Block a user