Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
3613695f
Unverified
Commit
3613695f
authored
Mar 01, 2026
by
Wesley Liddick
Committed by
GitHub
Mar 01, 2026
Browse files
Merge pull request #697 from DaydreamCoding/feat/proxy-password-visibility
feat(admin): 代理密码可见性 + 复制代理 URL 功能
parents
dd8df483
8fb7d476
Changes
6
Show whitespace changes
Inline
Side-by-side
backend/internal/handler/admin/proxy_handler.go
View file @
3613695f
...
...
@@ -64,9 +64,9 @@ func (h *ProxyHandler) List(c *gin.Context) {
return
}
out
:=
make
([]
dto
.
ProxyWithAccountCount
,
0
,
len
(
proxies
))
out
:=
make
([]
dto
.
Admin
ProxyWithAccountCount
,
0
,
len
(
proxies
))
for
i
:=
range
proxies
{
out
=
append
(
out
,
*
dto
.
ProxyWithAccountCountFromService
(
&
proxies
[
i
]))
out
=
append
(
out
,
*
dto
.
ProxyWithAccountCountFromService
Admin
(
&
proxies
[
i
]))
}
response
.
Paginated
(
c
,
out
,
total
,
page
,
pageSize
)
}
...
...
@@ -83,9 +83,9 @@ func (h *ProxyHandler) GetAll(c *gin.Context) {
response
.
ErrorFrom
(
c
,
err
)
return
}
out
:=
make
([]
dto
.
ProxyWithAccountCount
,
0
,
len
(
proxies
))
out
:=
make
([]
dto
.
Admin
ProxyWithAccountCount
,
0
,
len
(
proxies
))
for
i
:=
range
proxies
{
out
=
append
(
out
,
*
dto
.
ProxyWithAccountCountFromService
(
&
proxies
[
i
]))
out
=
append
(
out
,
*
dto
.
ProxyWithAccountCountFromService
Admin
(
&
proxies
[
i
]))
}
response
.
Success
(
c
,
out
)
return
...
...
@@ -97,9 +97,9 @@ func (h *ProxyHandler) GetAll(c *gin.Context) {
return
}
out
:=
make
([]
dto
.
Proxy
,
0
,
len
(
proxies
))
out
:=
make
([]
dto
.
Admin
Proxy
,
0
,
len
(
proxies
))
for
i
:=
range
proxies
{
out
=
append
(
out
,
*
dto
.
ProxyFromService
(
&
proxies
[
i
]))
out
=
append
(
out
,
*
dto
.
ProxyFromService
Admin
(
&
proxies
[
i
]))
}
response
.
Success
(
c
,
out
)
}
...
...
@@ -119,7 +119,7 @@ func (h *ProxyHandler) GetByID(c *gin.Context) {
return
}
response
.
Success
(
c
,
dto
.
ProxyFromService
(
proxy
))
response
.
Success
(
c
,
dto
.
ProxyFromService
Admin
(
proxy
))
}
// Create handles creating a new proxy
...
...
@@ -143,7 +143,7 @@ func (h *ProxyHandler) Create(c *gin.Context) {
if
err
!=
nil
{
return
nil
,
err
}
return
dto
.
ProxyFromService
(
proxy
),
nil
return
dto
.
ProxyFromService
Admin
(
proxy
),
nil
})
}
...
...
@@ -176,7 +176,7 @@ func (h *ProxyHandler) Update(c *gin.Context) {
return
}
response
.
Success
(
c
,
dto
.
ProxyFromService
(
proxy
))
response
.
Success
(
c
,
dto
.
ProxyFromService
Admin
(
proxy
))
}
// Delete handles deleting a proxy
...
...
backend/internal/handler/dto/mappers.go
View file @
3613695f
...
...
@@ -293,7 +293,6 @@ func ProxyFromService(p *service.Proxy) *Proxy {
Host
:
p
.
Host
,
Port
:
p
.
Port
,
Username
:
p
.
Username
,
Password
:
p
.
Password
,
Status
:
p
.
Status
,
CreatedAt
:
p
.
CreatedAt
,
UpdatedAt
:
p
.
UpdatedAt
,
...
...
@@ -323,6 +322,51 @@ func ProxyWithAccountCountFromService(p *service.ProxyWithAccountCount) *ProxyWi
}
}
// ProxyFromServiceAdmin converts a service Proxy to AdminProxy DTO for admin users.
// It includes the password field - user-facing endpoints must not use this.
func
ProxyFromServiceAdmin
(
p
*
service
.
Proxy
)
*
AdminProxy
{
if
p
==
nil
{
return
nil
}
base
:=
ProxyFromService
(
p
)
if
base
==
nil
{
return
nil
}
return
&
AdminProxy
{
Proxy
:
*
base
,
Password
:
p
.
Password
,
}
}
// ProxyWithAccountCountFromServiceAdmin converts a service ProxyWithAccountCount to AdminProxyWithAccountCount DTO.
// It includes the password field - user-facing endpoints must not use this.
func
ProxyWithAccountCountFromServiceAdmin
(
p
*
service
.
ProxyWithAccountCount
)
*
AdminProxyWithAccountCount
{
if
p
==
nil
{
return
nil
}
admin
:=
ProxyFromServiceAdmin
(
&
p
.
Proxy
)
if
admin
==
nil
{
return
nil
}
return
&
AdminProxyWithAccountCount
{
AdminProxy
:
*
admin
,
AccountCount
:
p
.
AccountCount
,
LatencyMs
:
p
.
LatencyMs
,
LatencyStatus
:
p
.
LatencyStatus
,
LatencyMessage
:
p
.
LatencyMessage
,
IPAddress
:
p
.
IPAddress
,
Country
:
p
.
Country
,
CountryCode
:
p
.
CountryCode
,
Region
:
p
.
Region
,
City
:
p
.
City
,
QualityStatus
:
p
.
QualityStatus
,
QualityScore
:
p
.
QualityScore
,
QualityGrade
:
p
.
QualityGrade
,
QualitySummary
:
p
.
QualitySummary
,
QualityChecked
:
p
.
QualityChecked
,
}
}
func
ProxyAccountSummaryFromService
(
a
*
service
.
ProxyAccountSummary
)
*
ProxyAccountSummary
{
if
a
==
nil
{
return
nil
...
...
backend/internal/handler/dto/types.go
View file @
3613695f
...
...
@@ -221,6 +221,32 @@ type ProxyWithAccountCount struct {
QualityChecked
*
int64
`json:"quality_checked,omitempty"`
}
// AdminProxy 是管理员接口使用的 proxy DTO(包含密码等敏感字段)。
// 注意:普通接口不得使用此 DTO。
type
AdminProxy
struct
{
Proxy
Password
string
`json:"password,omitempty"`
}
// AdminProxyWithAccountCount 是管理员接口使用的带账号统计的 proxy DTO。
type
AdminProxyWithAccountCount
struct
{
AdminProxy
AccountCount
int64
`json:"account_count"`
LatencyMs
*
int64
`json:"latency_ms,omitempty"`
LatencyStatus
string
`json:"latency_status,omitempty"`
LatencyMessage
string
`json:"latency_message,omitempty"`
IPAddress
string
`json:"ip_address,omitempty"`
Country
string
`json:"country,omitempty"`
CountryCode
string
`json:"country_code,omitempty"`
Region
string
`json:"region,omitempty"`
City
string
`json:"city,omitempty"`
QualityStatus
string
`json:"quality_status,omitempty"`
QualityScore
*
int
`json:"quality_score,omitempty"`
QualityGrade
string
`json:"quality_grade,omitempty"`
QualitySummary
string
`json:"quality_summary,omitempty"`
QualityChecked
*
int64
`json:"quality_checked,omitempty"`
}
type
ProxyAccountSummary
struct
{
ID
int64
`json:"id"`
Name
string
`json:"name"`
...
...
frontend/src/i18n/locales/en.ts
View file @
3613695f
...
...
@@ -2345,6 +2345,8 @@ export default {
dataExportConfirm
:
'
Confirm Export
'
,
dataExported
:
'
Data exported successfully
'
,
dataExportFailed
:
'
Failed to export data
'
,
copyProxyUrl
:
'
Copy Proxy URL
'
,
urlCopied
:
'
Proxy URL copied
'
,
searchProxies
:
'
Search proxies...
'
,
allProtocols
:
'
All Protocols
'
,
allStatus
:
'
All Status
'
,
...
...
@@ -2358,6 +2360,7 @@ export default {
name
:
'
Name
'
,
protocol
:
'
Protocol
'
,
address
:
'
Address
'
,
auth
:
'
Auth
'
,
location
:
'
Location
'
,
status
:
'
Status
'
,
accounts
:
'
Accounts
'
,
...
...
frontend/src/i18n/locales/zh.ts
View file @
3613695f
...
...
@@ -2459,6 +2459,7 @@ export default {
name
:
'
名称
'
,
protocol
:
'
协议
'
,
address
:
'
地址
'
,
auth
:
'
认证
'
,
location
:
'
地理位置
'
,
status
:
'
状态
'
,
accounts
:
'
账号数
'
,
...
...
@@ -2486,6 +2487,8 @@ export default {
allStatuses
:
'
全部状态
'
},
// Additional keys used in ProxiesView
copyProxyUrl
:
'
复制代理 URL
'
,
urlCopied
:
'
代理 URL 已复制
'
,
allProtocols
:
'
全部协议
'
,
allStatus
:
'
全部状态
'
,
searchProxies
:
'
搜索代理...
'
,
...
...
frontend/src/views/admin/ProxiesView.vue
View file @
3613695f
...
...
@@ -124,7 +124,54 @@
</
template
>
<
template
#cell-address=
"{ row }"
>
<div
class=
"flex items-center gap-1.5"
>
<code
class=
"code text-xs"
>
{{
row
.
host
}}
:
{{
row
.
port
}}
</code>
<div
class=
"relative"
>
<button
type=
"button"
class=
"rounded p-0.5 text-gray-400 hover:text-primary-600 dark:hover:text-primary-400"
:title=
"t('admin.proxies.copyProxyUrl')"
@
click.stop=
"copyProxyUrl(row)"
@
contextmenu.prevent=
"toggleCopyMenu(row.id)"
>
<Icon
name=
"copy"
size=
"sm"
/>
</button>
<!-- 右键展开格式选择菜单 -->
<div
v-if=
"copyMenuProxyId === row.id"
class=
"absolute left-0 top-full z-50 mt-1 w-auto min-w-[180px] rounded-lg border border-gray-200 bg-white py-1 shadow-lg dark:border-dark-500 dark:bg-dark-700"
>
<button
v-for=
"fmt in getCopyFormats(row)"
:key=
"fmt.label"
class=
"flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs hover:bg-gray-100 dark:hover:bg-dark-600"
@
click.stop=
"copyFormat(fmt.value)"
>
<span
class=
"truncate font-mono text-gray-600 dark:text-gray-300"
>
{{
fmt
.
label
}}
</span>
</button>
</div>
</div>
</div>
</
template
>
<
template
#cell-auth=
"{ row }"
>
<div
v-if=
"row.username || row.password"
class=
"flex items-center gap-1.5"
>
<div
class=
"flex flex-col text-xs"
>
<span
v-if=
"row.username"
class=
"text-gray-700 dark:text-gray-200"
>
{{
row
.
username
}}
</span>
<span
v-if=
"row.password"
class=
"font-mono text-gray-500 dark:text-gray-400"
>
{{
visiblePasswordIds
.
has
(
row
.
id
)
?
row
.
password
:
'
••••••
'
}}
</span>
</div>
<button
v-if=
"row.password"
type=
"button"
class=
"ml-1 rounded p-0.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
@
click.stop=
"visiblePasswordIds.has(row.id) ? visiblePasswordIds.delete(row.id) : visiblePasswordIds.add(row.id)"
>
<Icon
:name=
"visiblePasswordIds.has(row.id) ? 'eyeOff' : 'eye'"
size=
"sm"
/>
</button>
</div>
<span
v-else
class=
"text-sm text-gray-400"
>
-
</span>
</
template
>
<
template
#cell-location=
"{ row }"
>
...
...
@@ -397,12 +444,21 @@
<
/div
>
<
div
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.proxies.password
'
)
}}
<
/label
>
<
div
class
=
"
relative
"
>
<
input
v
-
model
=
"
createForm.password
"
type
=
"
password
"
class
=
"
input
"
:
type
=
"
createPasswordVisible ? 'text' : '
password
'
"
class
=
"
input
pr-10
"
:
placeholder
=
"
t('admin.proxies.optionalAuth')
"
/>
<
button
type
=
"
button
"
class
=
"
absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300
"
@
click
=
"
createPasswordVisible = !createPasswordVisible
"
>
<
Icon
:
name
=
"
createPasswordVisible ? 'eyeOff' : 'eye'
"
size
=
"
md
"
/>
<
/button
>
<
/div
>
<
/div
>
<
/form
>
...
...
@@ -581,12 +637,22 @@
<
/div
>
<
div
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.proxies.password
'
)
}}
<
/label
>
<
div
class
=
"
relative
"
>
<
input
v
-
model
=
"
editForm.password
"
type
=
"
password
"
:
type
=
"
editPasswordVisible ? 'text' : '
password
'
"
:
placeholder
=
"
t('admin.proxies.leaveEmptyToKeep')
"
class
=
"
input
"
class
=
"
input pr-10
"
@
input
=
"
editPasswordDirty = true
"
/>
<
button
type
=
"
button
"
class
=
"
absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300
"
@
click
=
"
editPasswordVisible = !editPasswordVisible
"
>
<
Icon
:
name
=
"
editPasswordVisible ? 'eyeOff' : 'eye'
"
size
=
"
md
"
/>
<
/button
>
<
/div
>
<
/div
>
<
div
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.proxies.status
'
)
}}
<
/label
>
...
...
@@ -813,15 +879,18 @@ import ImportDataModal from '@/components/admin/proxy/ImportDataModal.vue'
import
Select
from
'
@/components/common/Select.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
PlatformTypeBadge
from
'
@/components/common/PlatformTypeBadge.vue
'
import
{
useClipboard
}
from
'
@/composables/useClipboard
'
const
{
t
}
=
useI18n
()
const
appStore
=
useAppStore
()
const
{
copyToClipboard
}
=
useClipboard
()
const
columns
=
computed
<
Column
[]
>
(()
=>
[
{
key
:
'
select
'
,
label
:
''
,
sortable
:
false
}
,
{
key
:
'
name
'
,
label
:
t
(
'
admin.proxies.columns.name
'
),
sortable
:
true
}
,
{
key
:
'
protocol
'
,
label
:
t
(
'
admin.proxies.columns.protocol
'
),
sortable
:
true
}
,
{
key
:
'
address
'
,
label
:
t
(
'
admin.proxies.columns.address
'
),
sortable
:
false
}
,
{
key
:
'
auth
'
,
label
:
t
(
'
admin.proxies.columns.auth
'
),
sortable
:
false
}
,
{
key
:
'
location
'
,
label
:
t
(
'
admin.proxies.columns.location
'
),
sortable
:
false
}
,
{
key
:
'
account_count
'
,
label
:
t
(
'
admin.proxies.columns.accounts
'
),
sortable
:
true
}
,
{
key
:
'
latency
'
,
label
:
t
(
'
admin.proxies.columns.latency
'
),
sortable
:
false
}
,
...
...
@@ -858,6 +927,8 @@ const editStatusOptions = computed(() => [
])
const
proxies
=
ref
<
Proxy
[]
>
([])
const
visiblePasswordIds
=
reactive
(
new
Set
<
number
>
())
const
copyMenuProxyId
=
ref
<
number
|
null
>
(
null
)
const
loading
=
ref
(
false
)
const
searchQuery
=
ref
(
''
)
const
filters
=
reactive
({
...
...
@@ -872,7 +943,10 @@ const pagination = reactive({
}
)
const
showCreateModal
=
ref
(
false
)
const
createPasswordVisible
=
ref
(
false
)
const
showEditModal
=
ref
(
false
)
const
editPasswordVisible
=
ref
(
false
)
const
editPasswordDirty
=
ref
(
false
)
const
showImportData
=
ref
(
false
)
const
showDeleteDialog
=
ref
(
false
)
const
showBatchDeleteDialog
=
ref
(
false
)
...
...
@@ -1030,6 +1104,7 @@ const closeCreateModal = () => {
createForm
.
port
=
8080
createForm
.
username
=
''
createForm
.
password
=
''
createPasswordVisible
.
value
=
false
batchInput
.
value
=
''
batchParseResult
.
total
=
0
batchParseResult
.
valid
=
0
...
...
@@ -1173,14 +1248,18 @@ const handleEdit = (proxy: Proxy) => {
editForm
.
host
=
proxy
.
host
editForm
.
port
=
proxy
.
port
editForm
.
username
=
proxy
.
username
||
''
editForm
.
password
=
''
editForm
.
password
=
proxy
.
password
||
''
editForm
.
status
=
proxy
.
status
editPasswordVisible
.
value
=
false
editPasswordDirty
.
value
=
false
showEditModal
.
value
=
true
}
const
closeEditModal
=
()
=>
{
showEditModal
.
value
=
false
editingProxy
.
value
=
null
editPasswordVisible
.
value
=
false
editPasswordDirty
.
value
=
false
}
const
handleUpdateProxy
=
async
()
=>
{
...
...
@@ -1209,10 +1288,9 @@ const handleUpdateProxy = async () => {
status
:
editForm
.
status
}
// Only include password if it was changed
const
trimmedPassword
=
editForm
.
password
.
trim
()
if
(
trimmedPassword
)
{
updateData
.
password
=
trimmedPassword
// Only include password if user actually modified the field
if
(
editPasswordDirty
.
value
)
{
updateData
.
password
=
editForm
.
password
.
trim
()
||
null
}
await
adminAPI
.
proxies
.
update
(
editingProxy
.
value
.
id
,
updateData
)
...
...
@@ -1715,12 +1793,60 @@ const closeAccountsModal = () => {
proxyAccounts
.
value
=
[]
}
// ── Proxy URL copy ──
function
buildAuthPart
(
row
:
any
):
string
{
const
user
=
row
.
username
?
encodeURIComponent
(
row
.
username
)
:
''
const
pass
=
row
.
password
?
encodeURIComponent
(
row
.
password
)
:
''
if
(
user
&&
pass
)
return
`${user
}
:${pass
}
@`
if
(
user
)
return
`${user
}
@`
if
(
pass
)
return
`:${pass
}
@`
return
''
}
function
buildProxyUrl
(
row
:
any
):
string
{
return
`${row.protocol
}
://${buildAuthPart(row)
}
${row.host
}
:${row.port
}
`
}
function
getCopyFormats
(
row
:
any
)
{
const
hasAuth
=
row
.
username
||
row
.
password
const
fullUrl
=
buildProxyUrl
(
row
)
const
formats
=
[
{
label
:
fullUrl
,
value
:
fullUrl
}
,
]
if
(
hasAuth
)
{
const
withoutProtocol
=
fullUrl
.
replace
(
/^
[^
:
]
+:
\/\/
/
,
''
)
formats
.
push
({
label
:
withoutProtocol
,
value
:
withoutProtocol
}
)
}
formats
.
push
({
label
:
`${row.host
}
:${row.port
}
`
,
value
:
`${row.host
}
:${row.port
}
`
}
)
return
formats
}
function
copyProxyUrl
(
row
:
any
)
{
copyToClipboard
(
buildProxyUrl
(
row
),
t
(
'
admin.proxies.urlCopied
'
))
copyMenuProxyId
.
value
=
null
}
function
toggleCopyMenu
(
id
:
number
)
{
copyMenuProxyId
.
value
=
copyMenuProxyId
.
value
===
id
?
null
:
id
}
function
copyFormat
(
value
:
string
)
{
copyToClipboard
(
value
,
t
(
'
admin.proxies.urlCopied
'
))
copyMenuProxyId
.
value
=
null
}
function
closeCopyMenu
()
{
copyMenuProxyId
.
value
=
null
}
onMounted
(()
=>
{
loadProxies
()
document
.
addEventListener
(
'
click
'
,
closeCopyMenu
)
}
)
onUnmounted
(()
=>
{
clearTimeout
(
searchTimeout
)
abortController
?.
abort
()
document
.
removeEventListener
(
'
click
'
,
closeCopyMenu
)
}
)
<
/script
>
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment