mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-08 14:14:19 +00:00
feat: copy clients between inbounds
This commit is contained in:
parent
772d2b6de4
commit
8813d1f0d6
6 changed files with 437 additions and 0 deletions
16
deploy.sh
Normal file
16
deploy.sh
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
cd /opt/3x-uiRsNest
|
||||||
|
|
||||||
|
echo "=== Сборка бэкенда ==="
|
||||||
|
go build -o x-ui main.go
|
||||||
|
|
||||||
|
echo "=== Остановка x-ui ==="
|
||||||
|
systemctl stop x-ui
|
||||||
|
|
||||||
|
echo "=== Замена бинарника ==="
|
||||||
|
cp x-ui /usr/local/x-ui/x-ui
|
||||||
|
|
||||||
|
echo "=== Запуск x-ui ==="
|
||||||
|
systemctl start x-ui
|
||||||
|
systemctl status x-ui
|
||||||
|
|
@ -41,6 +41,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
||||||
g.POST("/clientIps/:email", a.getClientIps)
|
g.POST("/clientIps/:email", a.getClientIps)
|
||||||
g.POST("/clearClientIps/:email", a.clearClientIps)
|
g.POST("/clearClientIps/:email", a.clearClientIps)
|
||||||
g.POST("/addClient", a.addInboundClient)
|
g.POST("/addClient", a.addInboundClient)
|
||||||
|
g.POST("/:id/copyClients", a.copyInboundClients)
|
||||||
g.POST("/:id/delClient/:clientId", a.delInboundClient)
|
g.POST("/:id/delClient/:clientId", a.delInboundClient)
|
||||||
g.POST("/updateClient/:clientId", a.updateInboundClient)
|
g.POST("/updateClient/:clientId", a.updateInboundClient)
|
||||||
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
|
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
|
||||||
|
|
@ -54,6 +55,11 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
||||||
g.POST("/:id/delClientByEmail/:email", a.delInboundClientByEmail)
|
g.POST("/:id/delClientByEmail/:email", a.delInboundClientByEmail)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CopyInboundClientsRequest struct {
|
||||||
|
SourceInboundID int `json:"sourceInboundId"`
|
||||||
|
ClientEmails []string `json:"clientEmails"`
|
||||||
|
}
|
||||||
|
|
||||||
// getInbounds retrieves the list of inbounds for the logged-in user.
|
// getInbounds retrieves the list of inbounds for the logged-in user.
|
||||||
func (a *InboundController) getInbounds(c *gin.Context) {
|
func (a *InboundController) getInbounds(c *gin.Context) {
|
||||||
user := session.GetLoginUser(c)
|
user := session.GetLoginUser(c)
|
||||||
|
|
@ -260,6 +266,36 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// copyInboundClients copies clients from source inbound to target inbound.
|
||||||
|
func (a *InboundController) copyInboundClients(c *gin.Context) {
|
||||||
|
targetID, err := strconv.Atoi(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &CopyInboundClientsRequest{}
|
||||||
|
err = c.ShouldBindJSON(req)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.SourceInboundID <= 0 {
|
||||||
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), fmt.Errorf("invalid source inbound id"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, needRestart, err := a.inboundService.CopyInboundClients(targetID, req.SourceInboundID, req.ClientEmails)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonObj(c, result, nil)
|
||||||
|
if needRestart {
|
||||||
|
a.xrayService.SetToNeedRestart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// delInboundClient deletes a client from an inbound by inbound ID and client ID.
|
// delInboundClient deletes a client from an inbound by inbound ID and client ID.
|
||||||
func (a *InboundController) delInboundClient(c *gin.Context) {
|
func (a *InboundController) delInboundClient(c *gin.Context) {
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
|
|
|
||||||
|
|
@ -262,6 +262,10 @@
|
||||||
<a-icon type="usergroup-add"></a-icon>
|
<a-icon type="usergroup-add"></a-icon>
|
||||||
{{ i18n "pages.client.bulk"}}
|
{{ i18n "pages.client.bulk"}}
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
|
<a-menu-item key="copyClients">
|
||||||
|
<a-icon type="copy"></a-icon>
|
||||||
|
{{ i18n "pages.client.copyFromInbound"}}
|
||||||
|
</a-menu-item>
|
||||||
<a-menu-item key="resetClients">
|
<a-menu-item key="resetClients">
|
||||||
<a-icon type="file-done"></a-icon>
|
<a-icon type="file-done"></a-icon>
|
||||||
{{ i18n
|
{{ i18n
|
||||||
|
|
@ -777,6 +781,58 @@
|
||||||
{{template "modals/inboundInfoModal"}}
|
{{template "modals/inboundInfoModal"}}
|
||||||
{{template "modals/clientsModal"}}
|
{{template "modals/clientsModal"}}
|
||||||
{{template "modals/clientsBulkModal"}}
|
{{template "modals/clientsBulkModal"}}
|
||||||
|
<a-modal :title="copyClientsModal.title"
|
||||||
|
:visible="copyClientsModal.visible"
|
||||||
|
:confirm-loading="copyClientsModal.confirmLoading"
|
||||||
|
:ok-text="i18n('pages.client.copySelected')"
|
||||||
|
:cancel-text="i18n('cancel')"
|
||||||
|
:class="themeSwitcher.currentTheme"
|
||||||
|
@ok="submitCopyClients"
|
||||||
|
@cancel="closeCopyClientsModal"
|
||||||
|
width="900px">
|
||||||
|
<a-space direction="vertical" style="width: 100%;">
|
||||||
|
<div>
|
||||||
|
<div style="margin-bottom: 6px;">{{ i18n "pages.client.copySource" }}</div>
|
||||||
|
<a-select v-model="copyClientsModal.sourceInboundId"
|
||||||
|
style="width: 100%;"
|
||||||
|
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||||
|
@change="onCopySourceChange">
|
||||||
|
<a-select-option v-for="item in copyClientsModal.sources"
|
||||||
|
:key="item.id"
|
||||||
|
:value="item.id">
|
||||||
|
[[ item.label ]]
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
<div v-if="copyClientsModal.sourceInboundId > 0">
|
||||||
|
<a-space style="margin-bottom: 10px;">
|
||||||
|
<a-button size="small" @click="selectAllCopyClients">{{ i18n "pages.client.selectAll" }}</a-button>
|
||||||
|
<a-button size="small" @click="clearAllCopyClients">{{ i18n "pages.client.clearAll" }}</a-button>
|
||||||
|
</a-space>
|
||||||
|
<a-table :columns="copyClientsColumns"
|
||||||
|
:data-source="copyClientsModal.sourceClients"
|
||||||
|
:pagination="false"
|
||||||
|
size="small"
|
||||||
|
:row-key="item => item.email"
|
||||||
|
:scroll="{ y: 280 }">
|
||||||
|
<template slot="emailCheckbox" slot-scope="text, record">
|
||||||
|
<a-checkbox :checked="copyClientsModal.selectedEmails.includes(record.email)"
|
||||||
|
@change="event => toggleCopyClientEmail(record.email, event.target.checked)">
|
||||||
|
[[ record.email ]]
|
||||||
|
</a-checkbox>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
<div v-if="copyClientsModal.selectedEmails.length > 0">
|
||||||
|
<div style="margin-bottom: 4px;">{{ i18n "pages.client.copyEmailPreview" }}</div>
|
||||||
|
<div style="max-height: 120px; overflow-y: auto;">
|
||||||
|
<a-tag v-for="preview in copyClientsPreviewEmails" :key="preview" style="margin-bottom: 4px;">
|
||||||
|
[[ preview ]]
|
||||||
|
</a-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-space>
|
||||||
|
</a-modal>
|
||||||
<script>
|
<script>
|
||||||
const columns = [{
|
const columns = [{
|
||||||
title: "ID",
|
title: "ID",
|
||||||
|
|
@ -869,6 +925,11 @@
|
||||||
{ title: '{{ i18n "pages.inbounds.client" }}', width: 90, align: 'left', scopedSlots: { customRender: 'client' } },
|
{ title: '{{ i18n "pages.inbounds.client" }}', width: 90, align: 'left', scopedSlots: { customRender: 'client' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.info" }}', width: 10, align: 'center', scopedSlots: { customRender: 'info' } },
|
{ title: '{{ i18n "pages.inbounds.info" }}', width: 10, align: 'center', scopedSlots: { customRender: 'info' } },
|
||||||
];
|
];
|
||||||
|
const copyClientsColumns = [
|
||||||
|
{ title: '{{ i18n "pages.inbounds.email" }}', width: 300, scopedSlots: { customRender: 'emailCheckbox' } },
|
||||||
|
{ title: '{{ i18n "pages.inbounds.traffic" }}', width: 160, dataIndex: 'trafficLabel' },
|
||||||
|
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 180, dataIndex: 'expiryLabel' },
|
||||||
|
];
|
||||||
|
|
||||||
const app = new Vue({
|
const app = new Vue({
|
||||||
delimiters: ['[[', ']]'],
|
delimiters: ['[[', ']]'],
|
||||||
|
|
@ -910,6 +971,18 @@
|
||||||
showAlert: false,
|
showAlert: false,
|
||||||
ipLimitEnable: false,
|
ipLimitEnable: false,
|
||||||
pageSize: 0,
|
pageSize: 0,
|
||||||
|
copyClientsColumns,
|
||||||
|
copyClientsModal: {
|
||||||
|
visible: false,
|
||||||
|
confirmLoading: false,
|
||||||
|
title: '',
|
||||||
|
targetInboundId: 0,
|
||||||
|
targetInboundRemark: '',
|
||||||
|
sourceInboundId: 0,
|
||||||
|
sources: [],
|
||||||
|
sourceClients: [],
|
||||||
|
selectedEmails: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loading(spinning = true) {
|
loading(spinning = true) {
|
||||||
|
|
@ -1135,6 +1208,9 @@
|
||||||
case "addBulkClient":
|
case "addBulkClient":
|
||||||
this.openAddBulkClient(dbInbound.id)
|
this.openAddBulkClient(dbInbound.id)
|
||||||
break;
|
break;
|
||||||
|
case "copyClients":
|
||||||
|
this.openCopyClientsModal(dbInbound.id);
|
||||||
|
break;
|
||||||
case "export":
|
case "export":
|
||||||
this.inboundLinks(dbInbound.id);
|
this.inboundLinks(dbInbound.id);
|
||||||
break;
|
break;
|
||||||
|
|
@ -1298,6 +1374,96 @@
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
openCopyClientsModal(dbInboundId) {
|
||||||
|
const targetInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||||
|
if (!targetInbound) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sources = this.dbInbounds
|
||||||
|
.filter(row => row.id !== dbInboundId)
|
||||||
|
.map(row => {
|
||||||
|
const clients = this.getInboundClients(row) || [];
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
label: `${row.remark} (${row.protocol}, ${clients.length})`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
this.copyClientsModal.visible = true;
|
||||||
|
this.copyClientsModal.confirmLoading = false;
|
||||||
|
this.copyClientsModal.targetInboundId = dbInboundId;
|
||||||
|
this.copyClientsModal.targetInboundRemark = targetInbound.remark;
|
||||||
|
this.copyClientsModal.title = `{{ i18n "pages.client.copyToInbound" }} ${targetInbound.remark}`;
|
||||||
|
this.copyClientsModal.sources = sources;
|
||||||
|
this.copyClientsModal.sourceInboundId = 0;
|
||||||
|
this.copyClientsModal.sourceClients = [];
|
||||||
|
this.copyClientsModal.selectedEmails = [];
|
||||||
|
},
|
||||||
|
closeCopyClientsModal() {
|
||||||
|
this.copyClientsModal.visible = false;
|
||||||
|
this.copyClientsModal.confirmLoading = false;
|
||||||
|
},
|
||||||
|
onCopySourceChange(sourceInboundId) {
|
||||||
|
const sourceInbound = this.dbInbounds.find(row => row.id === sourceInboundId);
|
||||||
|
if (!sourceInbound) {
|
||||||
|
this.copyClientsModal.sourceClients = [];
|
||||||
|
this.copyClientsModal.selectedEmails = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sourceClients = this.getInboundClients(sourceInbound) || [];
|
||||||
|
this.copyClientsModal.sourceClients = sourceClients.map(client => {
|
||||||
|
const stats = this.getClientStats(sourceInbound, client.email);
|
||||||
|
const used = stats ? (stats.up + stats.down) : 0;
|
||||||
|
return {
|
||||||
|
email: client.email,
|
||||||
|
trafficLabel: SizeFormatter.sizeFormat(used),
|
||||||
|
expiryLabel: client.expiryTime > 0 ? IntlUtil.formatDate(client.expiryTime) : '{{ i18n "unlimited" }}',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
this.copyClientsModal.selectedEmails = [];
|
||||||
|
},
|
||||||
|
toggleCopyClientEmail(email, checked) {
|
||||||
|
const selected = this.copyClientsModal.selectedEmails.slice();
|
||||||
|
if (checked) {
|
||||||
|
if (!selected.includes(email)) {
|
||||||
|
selected.push(email);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const idx = selected.indexOf(email);
|
||||||
|
if (idx >= 0) {
|
||||||
|
selected.splice(idx, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.copyClientsModal.selectedEmails = selected;
|
||||||
|
},
|
||||||
|
selectAllCopyClients() {
|
||||||
|
this.copyClientsModal.selectedEmails = this.copyClientsModal.sourceClients.map(item => item.email);
|
||||||
|
},
|
||||||
|
clearAllCopyClients() {
|
||||||
|
this.copyClientsModal.selectedEmails = [];
|
||||||
|
},
|
||||||
|
async submitCopyClients() {
|
||||||
|
if (!this.copyClientsModal.sourceInboundId) {
|
||||||
|
this.$message.error('{{ i18n "pages.client.copySelectSourceFirst" }}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.copyClientsModal.confirmLoading = true;
|
||||||
|
const payload = {
|
||||||
|
sourceInboundId: this.copyClientsModal.sourceInboundId,
|
||||||
|
clientEmails: this.copyClientsModal.selectedEmails,
|
||||||
|
};
|
||||||
|
const msg = await HttpUtil.post(`/panel/api/inbounds/${this.copyClientsModal.targetInboundId}/copyClients`, payload);
|
||||||
|
this.copyClientsModal.confirmLoading = false;
|
||||||
|
if (!msg.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const obj = msg.obj || {};
|
||||||
|
const addedCount = (obj.added || []).length;
|
||||||
|
const skippedCount = (obj.skipped || []).length;
|
||||||
|
const errorCount = (obj.errors || []).length;
|
||||||
|
this.$message.success(`{{ i18n "pages.client.copyResult" }}: +${addedCount}, ~${skippedCount}, !${errorCount}`);
|
||||||
|
this.closeCopyClientsModal();
|
||||||
|
await this.getDBInbounds();
|
||||||
|
},
|
||||||
openEditClient(dbInboundId, client) {
|
openEditClient(dbInboundId, client) {
|
||||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||||
if (!dbInbound) return;
|
if (!dbInbound) return;
|
||||||
|
|
@ -1913,6 +2079,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
copyClientsPreviewEmails() {
|
||||||
|
if (!this.copyClientsModal.targetInboundId) return [];
|
||||||
|
return this.copyClientsModal.selectedEmails.map(email => `${email}_${this.copyClientsModal.targetInboundId}`);
|
||||||
|
},
|
||||||
total() {
|
total() {
|
||||||
let down = 0, up = 0, allTime = 0;
|
let down = 0, up = 0, allTime = 0;
|
||||||
let clients = 0, deactive = [], depleted = [], expiring = [];
|
let clients = 0, deactive = [], depleted = [], expiring = [];
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/mhsanaei/3x-ui/v2/database"
|
"github.com/mhsanaei/3x-ui/v2/database"
|
||||||
"github.com/mhsanaei/3x-ui/v2/database/model"
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
|
|
@ -26,6 +27,12 @@ type InboundService struct {
|
||||||
xrayApi xray.XrayAPI
|
xrayApi xray.XrayAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CopyClientsResult struct {
|
||||||
|
Added []string `json:"added"`
|
||||||
|
Skipped []string `json:"skipped"`
|
||||||
|
Errors []string `json:"errors"`
|
||||||
|
}
|
||||||
|
|
||||||
// GetInbounds retrieves all inbounds for a specific user.
|
// GetInbounds retrieves all inbounds for a specific user.
|
||||||
// Returns a slice of inbound models with their associated client statistics.
|
// Returns a slice of inbound models with their associated client statistics.
|
||||||
func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {
|
func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {
|
||||||
|
|
@ -750,6 +757,196 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
|
||||||
return needRestart, tx.Save(oldInbound).Error
|
return needRestart, tx.Save(oldInbound).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) getClientPrimaryKey(protocol model.Protocol, client model.Client) string {
|
||||||
|
switch protocol {
|
||||||
|
case model.Trojan:
|
||||||
|
return client.Password
|
||||||
|
case model.Shadowsocks:
|
||||||
|
return client.Email
|
||||||
|
case model.Hysteria:
|
||||||
|
return client.Auth
|
||||||
|
default:
|
||||||
|
return client.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) writeBackClientSubID(sourceInboundID int, sourceProtocol model.Protocol, client model.Client, subID string) (bool, error) {
|
||||||
|
client.SubID = subID
|
||||||
|
client.UpdatedAt = time.Now().UnixMilli()
|
||||||
|
clientID := s.getClientPrimaryKey(sourceProtocol, client)
|
||||||
|
if clientID == "" {
|
||||||
|
return false, common.NewError("empty client ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsBytes, err := json.Marshal(map[string][]model.Client{
|
||||||
|
"clients": []model.Client{client},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePayload := &model.Inbound{
|
||||||
|
Id: sourceInboundID,
|
||||||
|
Settings: string(settingsBytes),
|
||||||
|
}
|
||||||
|
return s.UpdateInboundClient(updatePayload, clientID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) generateRandomCredential(targetProtocol model.Protocol) string {
|
||||||
|
switch targetProtocol {
|
||||||
|
case model.VMESS, model.VLESS:
|
||||||
|
return uuid.NewString()
|
||||||
|
default:
|
||||||
|
return strings.ReplaceAll(uuid.NewString(), "-", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) buildTargetClientFromSource(source model.Client, targetProtocol model.Protocol, email string) (model.Client, error) {
|
||||||
|
nowTs := time.Now().UnixMilli()
|
||||||
|
target := source
|
||||||
|
target.Email = email
|
||||||
|
target.CreatedAt = nowTs
|
||||||
|
target.UpdatedAt = nowTs
|
||||||
|
|
||||||
|
target.ID = ""
|
||||||
|
target.Password = ""
|
||||||
|
target.Auth = ""
|
||||||
|
|
||||||
|
switch targetProtocol {
|
||||||
|
case model.VMESS, model.VLESS:
|
||||||
|
target.ID = s.generateRandomCredential(targetProtocol)
|
||||||
|
case model.Trojan, model.Shadowsocks:
|
||||||
|
target.Password = s.generateRandomCredential(targetProtocol)
|
||||||
|
case model.Hysteria:
|
||||||
|
target.Auth = s.generateRandomCredential(targetProtocol)
|
||||||
|
default:
|
||||||
|
target.ID = s.generateRandomCredential(targetProtocol)
|
||||||
|
}
|
||||||
|
|
||||||
|
return target, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) nextAvailableCopiedEmail(originalEmail string, targetID int, occupied map[string]struct{}) string {
|
||||||
|
base := fmt.Sprintf("%s_%d", originalEmail, targetID)
|
||||||
|
candidate := base
|
||||||
|
suffix := 0
|
||||||
|
for {
|
||||||
|
if _, exists := occupied[strings.ToLower(candidate)]; !exists {
|
||||||
|
occupied[strings.ToLower(candidate)] = struct{}{}
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
suffix++
|
||||||
|
candidate = fmt.Sprintf("%s_%d", base, suffix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) CopyInboundClients(targetInboundID int, sourceInboundID int, clientEmails []string) (*CopyClientsResult, bool, error) {
|
||||||
|
result := &CopyClientsResult{
|
||||||
|
Added: []string{},
|
||||||
|
Skipped: []string{},
|
||||||
|
Errors: []string{},
|
||||||
|
}
|
||||||
|
if targetInboundID == sourceInboundID {
|
||||||
|
return result, false, common.NewError("source and target inbounds must be different")
|
||||||
|
}
|
||||||
|
|
||||||
|
targetInbound, err := s.GetInbound(targetInboundID)
|
||||||
|
if err != nil {
|
||||||
|
return result, false, err
|
||||||
|
}
|
||||||
|
sourceInbound, err := s.GetInbound(sourceInboundID)
|
||||||
|
if err != nil {
|
||||||
|
return result, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceClients, err := s.GetClients(sourceInbound)
|
||||||
|
if err != nil {
|
||||||
|
return result, false, err
|
||||||
|
}
|
||||||
|
if len(sourceClients) == 0 {
|
||||||
|
return result, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
allowedEmails := map[string]struct{}{}
|
||||||
|
if len(clientEmails) > 0 {
|
||||||
|
for _, email := range clientEmails {
|
||||||
|
allowedEmails[strings.ToLower(strings.TrimSpace(email))] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
occupiedEmails := map[string]struct{}{}
|
||||||
|
allEmails, err := s.getAllEmails()
|
||||||
|
if err != nil {
|
||||||
|
return result, false, err
|
||||||
|
}
|
||||||
|
for _, email := range allEmails {
|
||||||
|
clean := strings.Trim(email, "\"")
|
||||||
|
if clean != "" {
|
||||||
|
occupiedEmails[strings.ToLower(clean)] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newClients := make([]model.Client, 0)
|
||||||
|
needRestart := false
|
||||||
|
for _, sourceClient := range sourceClients {
|
||||||
|
originalEmail := strings.TrimSpace(sourceClient.Email)
|
||||||
|
if originalEmail == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(allowedEmails) > 0 {
|
||||||
|
if _, ok := allowedEmails[strings.ToLower(originalEmail)]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sourceClient.SubID == "" {
|
||||||
|
newSubID := uuid.NewString()
|
||||||
|
subNeedRestart, subErr := s.writeBackClientSubID(sourceInbound.Id, sourceInbound.Protocol, sourceClient, newSubID)
|
||||||
|
if subErr != nil {
|
||||||
|
result.Errors = append(result.Errors, fmt.Sprintf("%s: failed to write source subId: %v", originalEmail, subErr))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if subNeedRestart {
|
||||||
|
needRestart = true
|
||||||
|
}
|
||||||
|
sourceClient.SubID = newSubID
|
||||||
|
}
|
||||||
|
|
||||||
|
targetEmail := s.nextAvailableCopiedEmail(originalEmail, targetInboundID, occupiedEmails)
|
||||||
|
targetClient, buildErr := s.buildTargetClientFromSource(sourceClient, targetInbound.Protocol, targetEmail)
|
||||||
|
if buildErr != nil {
|
||||||
|
result.Errors = append(result.Errors, fmt.Sprintf("%s: %v", originalEmail, buildErr))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newClients = append(newClients, targetClient)
|
||||||
|
result.Added = append(result.Added, targetEmail)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newClients) == 0 {
|
||||||
|
return result, needRestart, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsPayload, err := json.Marshal(map[string][]model.Client{
|
||||||
|
"clients": newClients,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return result, needRestart, err
|
||||||
|
}
|
||||||
|
|
||||||
|
addNeedRestart, err := s.AddInboundClient(&model.Inbound{
|
||||||
|
Id: targetInboundID,
|
||||||
|
Settings: string(settingsPayload),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return result, needRestart, err
|
||||||
|
}
|
||||||
|
if addNeedRestart {
|
||||||
|
needRestart = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, needRestart, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *InboundService) DelInboundClient(inboundId int, clientId string) (bool, error) {
|
func (s *InboundService) DelInboundClient(inboundId int, clientId string) (bool, error) {
|
||||||
oldInbound, err := s.GetInbound(inboundId)
|
oldInbound, err := s.GetInbound(inboundId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -298,6 +298,15 @@
|
||||||
"submitEdit" = "Save Changes"
|
"submitEdit" = "Save Changes"
|
||||||
"clientCount" = "Number of Clients"
|
"clientCount" = "Number of Clients"
|
||||||
"bulk" = "Add Bulk"
|
"bulk" = "Add Bulk"
|
||||||
|
"copyFromInbound" = "Copy Clients from Inbound"
|
||||||
|
"copyToInbound" = "Copy clients to"
|
||||||
|
"copySelected" = "Copy Selected"
|
||||||
|
"copySource" = "Source"
|
||||||
|
"copyEmailPreview" = "Resulting email preview"
|
||||||
|
"copySelectSourceFirst" = "Please select a source inbound first."
|
||||||
|
"copyResult" = "Copy result"
|
||||||
|
"selectAll" = "Select all"
|
||||||
|
"clearAll" = "Clear all"
|
||||||
"method" = "Method"
|
"method" = "Method"
|
||||||
"first" = "First"
|
"first" = "First"
|
||||||
"last" = "Last"
|
"last" = "Last"
|
||||||
|
|
|
||||||
|
|
@ -298,6 +298,15 @@
|
||||||
"submitEdit" = "Сохранить изменения"
|
"submitEdit" = "Сохранить изменения"
|
||||||
"clientCount" = "Количество клиентов"
|
"clientCount" = "Количество клиентов"
|
||||||
"bulk" = "Добавить несколько"
|
"bulk" = "Добавить несколько"
|
||||||
|
"copyFromInbound" = "Скопировать клиентов из инбаунда"
|
||||||
|
"copyToInbound" = "Скопировать клиентов в"
|
||||||
|
"copySelected" = "Скопировать выбранных"
|
||||||
|
"copySource" = "Источник"
|
||||||
|
"copyEmailPreview" = "Предпросмотр итоговых email"
|
||||||
|
"copySelectSourceFirst" = "Сначала выберите источник."
|
||||||
|
"copyResult" = "Результат копирования"
|
||||||
|
"selectAll" = "Выбрать всех"
|
||||||
|
"clearAll" = "Снять всё"
|
||||||
"method" = "Метод"
|
"method" = "Метод"
|
||||||
"first" = "Первый"
|
"first" = "Первый"
|
||||||
"last" = "Последний"
|
"last" = "Последний"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue