mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 20:12:39 +08:00
Merge pull request #7111 from ouchihao/codex/bug-proxy-clear-credentials
fix(proxy): allow explicitly clearing stored credentials
This commit is contained in:
@@ -318,8 +318,8 @@ func (h *AccountHandler) importData(ctx context.Context, req DataImportRequest)
|
||||
Protocol: proxy.Protocol,
|
||||
Host: proxy.Host,
|
||||
Port: proxy.Port,
|
||||
Username: proxy.Username,
|
||||
Password: proxy.Password,
|
||||
Username: &proxy.Username,
|
||||
Password: &proxy.Password,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -394,8 +394,8 @@ func (h *AccountHandler) importData(ctx context.Context, req DataImportRequest)
|
||||
Protocol: created.Protocol,
|
||||
Host: created.Host,
|
||||
Port: created.Port,
|
||||
Username: created.Username,
|
||||
Password: created.Password,
|
||||
Username: &created.Username,
|
||||
Password: &created.Password,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
//go:build unit
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestProxyHandlerUpdateCredentialPresence(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
check := func(name, body string, username, password *string) {
|
||||
t.Helper()
|
||||
t.Run(name, func(t *testing.T) {
|
||||
svc := &proxyPartialUpdateService{}
|
||||
router := gin.New()
|
||||
router.PUT("/proxies/:id", NewProxyHandler(svc).Update)
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPut, "/proxies/9", strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
require.Equal(t, http.StatusOK, w.Code, w.Body.String())
|
||||
require.NotNil(t, svc.input)
|
||||
require.Equal(t, username, svc.input.Username)
|
||||
require.Equal(t, password, svc.input.Password)
|
||||
})
|
||||
}
|
||||
empty, username, password := "", "user", "pass"
|
||||
check("omitted", `{"status":"inactive"}`, nil, nil)
|
||||
check("null preserves", `{"username":null,"password":null}`, nil, nil)
|
||||
check("clear both", `{"username":"","password":""}`, &empty, &empty)
|
||||
check("clear username only", `{"username":""}`, &empty, nil)
|
||||
check("clear password only", `{"password":""}`, nil, &empty)
|
||||
check("trim values", `{"username":" user ","password":" pass "}`, &username, &password)
|
||||
check("whitespace clears", `{"username":" ","password":" "}`, &empty, &empty)
|
||||
}
|
||||
@@ -182,8 +182,8 @@ func (h *ProxyHandler) ImportData(c *gin.Context) {
|
||||
Protocol: existing.Protocol,
|
||||
Host: existing.Host,
|
||||
Port: existing.Port,
|
||||
Username: existing.Username,
|
||||
Password: existing.Password,
|
||||
Username: &existing.Username,
|
||||
Password: &existing.Password,
|
||||
}
|
||||
if _, err := h.adminService.UpdateProxy(ctx, existing.ID, updateInput); err != nil {
|
||||
result.Errors = append(result.Errors, DataImportError{
|
||||
@@ -266,8 +266,8 @@ func (h *ProxyHandler) ImportData(c *gin.Context) {
|
||||
Protocol: created.Protocol,
|
||||
Host: created.Host,
|
||||
Port: created.Port,
|
||||
Username: created.Username,
|
||||
Password: created.Password,
|
||||
Username: &created.Username,
|
||||
Password: &created.Password,
|
||||
}); err != nil {
|
||||
result.Errors = append(result.Errors, DataImportError{
|
||||
Kind: "proxy",
|
||||
|
||||
@@ -45,8 +45,8 @@ type UpdateProxyRequest struct {
|
||||
Protocol string `json:"protocol" binding:"omitempty,oneof=http https socks5 socks5h"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port" binding:"omitempty,min=1,max=65535"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
Status string `json:"status" binding:"omitempty,oneof=active inactive"`
|
||||
ExpiresAt dto.NullableInt64Field `json:"expires_at"`
|
||||
FallbackMode string `json:"fallback_mode" binding:"omitempty,oneof=none proxy direct"`
|
||||
@@ -187,13 +187,19 @@ func (h *ProxyHandler) Update(c *gin.Context) {
|
||||
t := time.Unix(*req.ExpiresAt.Value, 0).UTC()
|
||||
expiresAt = &t
|
||||
}
|
||||
if req.Username != nil {
|
||||
*req.Username = strings.TrimSpace(*req.Username)
|
||||
}
|
||||
if req.Password != nil {
|
||||
*req.Password = strings.TrimSpace(*req.Password)
|
||||
}
|
||||
proxy, err := h.adminService.UpdateProxy(c.Request.Context(), proxyID, &service.UpdateProxyInput{
|
||||
Name: strings.TrimSpace(req.Name),
|
||||
Protocol: strings.TrimSpace(req.Protocol),
|
||||
Host: strings.TrimSpace(req.Host),
|
||||
Port: req.Port,
|
||||
Username: strings.TrimSpace(req.Username),
|
||||
Password: strings.TrimSpace(req.Password),
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
Status: strings.TrimSpace(req.Status),
|
||||
ExpiresAt: expiresAt,
|
||||
ClearExpiresAt: req.ExpiresAt.Set && expiresAt == nil,
|
||||
|
||||
@@ -134,11 +134,11 @@ func (s *adminServiceImpl) UpdateProxy(ctx context.Context, id int64, input *Upd
|
||||
if input.Port != 0 {
|
||||
proxy.Port = input.Port
|
||||
}
|
||||
if input.Username != "" {
|
||||
proxy.Username = input.Username
|
||||
if input.Username != nil {
|
||||
proxy.Username = *input.Username
|
||||
}
|
||||
if input.Password != "" {
|
||||
proxy.Password = input.Password
|
||||
if input.Password != nil {
|
||||
proxy.Password = *input.Password
|
||||
}
|
||||
if input.Status != "" {
|
||||
proxy.Status = input.Status
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
//go:build unit
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAdminProxyUpdateCredentials(t *testing.T) {
|
||||
check := func(name string, input UpdateProxyInput, username, password string) {
|
||||
t.Helper()
|
||||
t.Run(name, func(t *testing.T) {
|
||||
repo := &updatingProxyRepoStub{proxyRepoStub: &proxyRepoStub{}, proxy: &Proxy{
|
||||
ID: 9, Username: "old-user", Password: "old-pass", FallbackMode: FallbackModeNone,
|
||||
}}
|
||||
svc := &adminServiceImpl{proxyRepo: repo}
|
||||
got, err := svc.UpdateProxy(context.Background(), 9, &input)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, username, got.Username)
|
||||
require.Equal(t, password, got.Password)
|
||||
require.Equal(t, 1, repo.updateCalls)
|
||||
})
|
||||
}
|
||||
empty, username, password := "", "new-user", "new-pass"
|
||||
check("omitted", UpdateProxyInput{Status: "inactive"}, "old-user", "old-pass")
|
||||
check("clear both", UpdateProxyInput{Username: &empty, Password: &empty}, "", "")
|
||||
check("clear username only", UpdateProxyInput{Username: &empty}, "", "old-pass")
|
||||
check("clear password only", UpdateProxyInput{Password: &empty}, "old-user", "")
|
||||
check("replace both", UpdateProxyInput{Username: &username, Password: &password}, username, password)
|
||||
}
|
||||
@@ -541,8 +541,8 @@ type UpdateProxyInput struct {
|
||||
Protocol string
|
||||
Host string
|
||||
Port int
|
||||
Username string
|
||||
Password string
|
||||
Username *string
|
||||
Password *string
|
||||
Status string
|
||||
ExpiresAt *time.Time
|
||||
ClearExpiresAt bool
|
||||
|
||||
@@ -1461,7 +1461,7 @@ const handleUpdateProxy = async () => {
|
||||
protocol: editForm.protocol,
|
||||
host: editForm.host.trim(),
|
||||
port: editForm.port,
|
||||
username: editForm.username.trim() || null,
|
||||
username: editForm.username.trim(),
|
||||
status: editForm.status,
|
||||
expires_at: editForm.expires_at ? Math.floor(new Date(editForm.expires_at).getTime() / 1000) : null,
|
||||
fallback_mode: editForm.fallback_mode,
|
||||
@@ -1471,7 +1471,7 @@ const handleUpdateProxy = async () => {
|
||||
|
||||
// Only include password if user actually modified the field
|
||||
if (editPasswordDirty.value) {
|
||||
updateData.password = editForm.password.trim() || null
|
||||
updateData.password = editForm.password.trim()
|
||||
}
|
||||
|
||||
await adminAPI.proxies.update(editingProxy.value.id, updateData)
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { flushPromises, shallowMount } from '@vue/test-utils'
|
||||
import ProxiesView from '../ProxiesView.vue'
|
||||
|
||||
const { list, update, getAllWithCount } = vi.hoisted(() => ({ list: vi.fn(), update: vi.fn(), getAllWithCount: vi.fn() }))
|
||||
vi.mock('@/api/admin', () => ({ adminAPI: { proxies: { list, update, getAllWithCount } } }))
|
||||
vi.mock('@/stores/app', () => ({ useAppStore: () => ({ showError: vi.fn(), showSuccess: vi.fn() }) }))
|
||||
vi.mock('vue-i18n', async () => ({
|
||||
...await vi.importActual<typeof import('vue-i18n')>('vue-i18n'),
|
||||
useI18n: () => ({ t: (key: string) => key }),
|
||||
}))
|
||||
const mountView = () => shallowMount(ProxiesView, {
|
||||
global: { stubs: {
|
||||
AppLayout: { template: '<div><slot /></div>' },
|
||||
TablePageLayout: { template: '<div><slot name="table" /></div>' },
|
||||
DataTable: { props: ['data'], template: '<div v-for="row in data" :key="row.id"><slot name="cell-actions" :row="row" /></div>' },
|
||||
BaseDialog: { props: ['show'], template: '<div v-if="show"><slot /><slot name="footer" /></div>' },
|
||||
} },
|
||||
})
|
||||
let wrapper: ReturnType<typeof mountView>
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
list.mockResolvedValue({ items: [{ id: 9, name: 'proxy', protocol: 'http', host: 'proxy.example', port: 8080, username: 'old-user', status: 'active' }], total: 1, pages: 1 })
|
||||
getAllWithCount.mockResolvedValue([])
|
||||
update.mockResolvedValue({})
|
||||
})
|
||||
afterEach(() => wrapper?.unmount())
|
||||
async function edit() {
|
||||
wrapper = mountView(); await flushPromises()
|
||||
await wrapper.findAll('button').find(button => button.text() === 'common.edit')!.trigger('click')
|
||||
}
|
||||
async function submit() {
|
||||
await wrapper.get('#edit-proxy-form').trigger('submit'); await flushPromises()
|
||||
expect(update).toHaveBeenCalledTimes(1)
|
||||
return update.mock.calls[0][1]
|
||||
}
|
||||
|
||||
describe('proxy credential updates', () => {
|
||||
it('sends an explicit empty username when cleared', async () => {
|
||||
await edit()
|
||||
const username = wrapper.findAll<HTMLInputElement>('#edit-proxy-form input').find(input => input.element.value === 'old-user')!
|
||||
await username.setValue('')
|
||||
const payload = await submit()
|
||||
expect(payload.username).toBe('')
|
||||
expect(payload).not.toHaveProperty('password')
|
||||
})
|
||||
|
||||
it('sends an explicit empty password when the user clears the field', async () => {
|
||||
await edit()
|
||||
await wrapper.get('#edit-proxy-form input[type="password"]').setValue('')
|
||||
expect((await submit()).password).toBe('')
|
||||
})
|
||||
|
||||
it('omits an untouched password when saving other settings', async () => {
|
||||
await edit()
|
||||
const payload = await submit()
|
||||
expect(payload.username).toBe('old-user')
|
||||
expect(payload).not.toHaveProperty('password')
|
||||
})
|
||||
|
||||
it('keeps trimming a replacement password', async () => {
|
||||
await edit()
|
||||
await wrapper.get('#edit-proxy-form input[type="password"]').setValue(' new-password ')
|
||||
expect((await submit()).password).toBe('new-password')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user