diff --git a/backend/controllers/security.go b/backend/controllers/security.go
index bb4e1fa..acfd7a2 100644
--- a/backend/controllers/security.go
+++ b/backend/controllers/security.go
@@ -16,7 +16,6 @@ import (
"git.pcux.cn/xuans/Smail/dto/response"
"git.pcux.cn/xuans/Smail/models"
"git.pcux.cn/xuans/Smail/services/access"
- "git.pcux.cn/xuans/Smail/services/auth"
"git.pcux.cn/xuans/Smail/services/credentials"
"git.pcux.cn/xuans/Smail/services/externalemail"
"git.pcux.cn/xuans/Smail/services/recovery"
@@ -422,9 +421,11 @@ func RevokeRecoveryCodes(ctx *context.Context, w http.ResponseWriter, req *http.
type protocolCredentialRequest struct {
Protocol string `json:"protocol"`
Enabled *bool `json:"enabled,omitempty"`
+ Password string `json:"password"`
}
func requireProtocolCredentialContext(ctx *context.Context, w http.ResponseWriter) (*credentials.Service, bool) {
+ w.Header().Set("Cache-Control", "no-store")
if ctx == nil || ctx.UserID <= 0 {
writeSecurityError(w, response.NeedLogin, "请先登录")
return nil, false
@@ -463,6 +464,9 @@ func SetProtocolCredentialEnabled(ctx *context.Context, w http.ResponseWriter, r
writeSecurityError(w, response.ParamsError, "协议设置请求无效")
return
}
+ if !requireProtocolCredentialPassword(ctx, w, req, input.Password) {
+ return
+ }
if err := service.SetProtocolEnabled(ctx.UserID, input.Protocol, *input.Enabled); err != nil {
writeSecurityError(w, response.ParamsError, "协议设置失败")
return
@@ -481,6 +485,9 @@ func GenerateProtocolCredential(ctx *context.Context, w http.ResponseWriter, req
writeSecurityError(w, response.ParamsError, "协议请求无效")
return
}
+ if !requireProtocolCredentialPassword(ctx, w, req, input.Password) {
+ return
+ }
code, err := service.GenerateProtocolCode(ctx.UserID, input.Protocol)
if err != nil {
if writePasswordBusy(w, err) {
@@ -504,6 +511,9 @@ func RevokeProtocolCredential(ctx *context.Context, w http.ResponseWriter, req *
writeSecurityError(w, response.ParamsError, "协议请求无效")
return
}
+ if !requireProtocolCredentialPassword(ctx, w, req, input.Password) {
+ return
+ }
if err := service.RevokeProtocolCode(ctx.UserID, input.Protocol); err != nil {
writeSecurityError(w, response.ParamsError, "撤销授权码失败")
return
@@ -525,11 +535,7 @@ func RevealProtocolCredentials(ctx *context.Context, w http.ResponseWriter, req
writeSecurityError(w, response.ParamsError, "请输入当前账号密码进行安全验证")
return
}
- if _, err := auth.AuthenticateUserByID(ctx.UserID, input.Password); err != nil {
- if writePasswordBusy(w, err) {
- return
- }
- writeSecurityError(w, response.ParamsError, "密码验证失败")
+ if !requireProtocolCredentialPassword(ctx, w, req, input.Password) {
return
}
codes, err := service.RevealProtocolCodes(ctx.UserID)
diff --git a/backend/controllers/security_authentication_test.go b/backend/controllers/security_authentication_test.go
index 76285f2..0f5110d 100644
--- a/backend/controllers/security_authentication_test.go
+++ b/backend/controllers/security_authentication_test.go
@@ -2,6 +2,7 @@ package controllers
import (
"encoding/json"
+ "fmt"
"net/http"
"net/http/httptest"
"strings"
@@ -32,6 +33,11 @@ func TestSecurityHandlersRejectUnauthenticatedRequests(t *testing.T) {
{name: "generate protocol credential", hand: GenerateProtocolCredential, body: `{"protocol":"smtp"}`},
{name: "revoke protocol credential", hand: RevokeProtocolCredential, body: `{"protocol":"smtp"}`},
{name: "reveal protocol credentials", hand: RevealProtocolCredentials, body: `{"password":"FixtureP12"}`},
+ {name: "list client credentials", hand: ListClientCredentials},
+ {name: "create client credential", hand: CreateClientCredential, body: `{"client_name":"设备","password":"FixtureP12"}`},
+ {name: "regenerate client credential", hand: RegenerateClientCredential, body: `{"client_id":1,"password":"FixtureP12"}`},
+ {name: "revoke client credential", hand: RevokeClientCredential, body: `{"client_id":1,"password":"FixtureP12"}`},
+ {name: "reveal client credentials", hand: RevealClientCredentials, body: `{"password":"FixtureP12"}`},
{name: "request verification", hand: RequestVerification, body: `{"purpose":"login","account":"smail","email":"user@example.com"}`},
{name: "verify verification", hand: VerifyVerification, body: `{"id":"challenge","account":"smail","email":"user@example.com","code":"12345678","purpose":"login"}`},
{name: "list external emails", hand: ListExternalEmails},
@@ -157,7 +163,7 @@ func TestProtocolCredentialHandlersManageCurrentUserCredential(t *testing.T) {
setRequest := httptest.NewRequest(
http.MethodPut,
"/api/security/credentials/protocols/enabled",
- strings.NewReader(`{"protocol":"SMTP","enabled":true}`),
+ strings.NewReader(`{"protocol":"SMTP","enabled":true,"password":"FixtureP12"}`),
)
SetProtocolCredentialEnabled(ctx, setRecorder, setRequest)
var setResult response.Response
@@ -172,7 +178,7 @@ func TestProtocolCredentialHandlersManageCurrentUserCredential(t *testing.T) {
generateRequest := httptest.NewRequest(
http.MethodPost,
"/api/security/credentials/protocols/generate",
- strings.NewReader(`{"protocol":"smtp"}`),
+ strings.NewReader(`{"protocol":"smtp","password":"FixtureP12"}`),
)
GenerateProtocolCredential(ctx, generateRecorder, generateRequest)
var generateResult response.Response
@@ -251,7 +257,7 @@ func TestProtocolCredentialHandlersManageCurrentUserCredential(t *testing.T) {
revokeRequest := httptest.NewRequest(
http.MethodPost,
"/api/security/credentials/protocols/revoke",
- strings.NewReader(`{"protocol":"smtp"}`),
+ strings.NewReader(`{"protocol":"smtp","password":"FixtureP12"}`),
)
RevokeProtocolCredential(ctx, revokeRecorder, revokeRequest)
var revokeResult response.Response
@@ -273,3 +279,117 @@ func TestProtocolCredentialHandlersManageCurrentUserCredential(t *testing.T) {
}
}
}
+
+func TestClientCredentialEntryPointsRequireOwnPassword(t *testing.T) {
+ _, _, engine, user, other := newPublicSecurityFixture(t)
+ service, err := credentials.NewService(engine)
+ if err != nil {
+ t.Fatal(err)
+ }
+ service.SetVaultRoot(t.TempDir())
+ previousDB, previousRuntime := db.Instance, securityRuntime
+ previousUserLimiter, previousIPLimiter := reauthUserAttempts, reauthIPAttempts
+ db.Instance, securityRuntime.credentials = engine, service
+ t.Cleanup(func() {
+ db.Instance, securityRuntime = previousDB, previousRuntime
+ reauthUserAttempts, reauthIPAttempts = previousUserLimiter, previousIPLimiter
+ })
+ _, client, err := service.CreateClientCredential(user.ID, "原设备")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := service.SetProtocolEnabled(user.ID, "smtp", true); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := service.GenerateProtocolCode(user.ID, "smtp"); err != nil {
+ t.Fatal(err)
+ }
+ status := func() string {
+ t.Helper()
+ clients, err := service.ListClientCredentials(user.ID)
+ if err != nil {
+ t.Fatal(err)
+ }
+ protocols, err := service.ListProtocolCredentials(user.ID)
+ if err != nil {
+ t.Fatal(err)
+ }
+ data, err := json.Marshal([]any{clients, protocols})
+ if err != nil {
+ t.Fatal(err)
+ }
+ return string(data)
+ }
+ before := status()
+ entries := []struct {
+ name string
+ hand func(*appcontext.Context, http.ResponseWriter, *http.Request)
+ body string
+ }{
+ {"create", CreateClientCredential, `{"client_name":"新设备"`},
+ {"regenerate", RegenerateClientCredential, fmt.Sprintf(`{"client_id":%d`, client.ID)},
+ {"revoke", RevokeClientCredential, fmt.Sprintf(`{"client_id":%d`, client.ID)},
+ {"reveal", RevealClientCredentials, `{"unused":true`},
+ {"enable protocol", SetProtocolCredentialEnabled, `{"protocol":"imap","enabled":true`},
+ {"disable protocol", SetProtocolCredentialEnabled, `{"protocol":"smtp","enabled":false`},
+ {"generate protocol", GenerateProtocolCredential, `{"protocol":"smtp"`},
+ {"revoke protocol", RevokeProtocolCredential, `{"protocol":"smtp"`},
+ {"reveal protocol", RevealProtocolCredentials, `{"unused":true`},
+ }
+ for _, role := range []access.Role{access.RoleUser, access.RoleAdmin, access.RoleSuperAdmin} {
+ for _, entry := range entries {
+ t.Run(string(role)+"/"+entry.name, func(t *testing.T) {
+ reauthUserAttempts, reauthIPAttempts = newLoginLimiter(10), newLoginLimiter(50)
+ ctx := &appcontext.Context{UserID: user.ID, Role: role, RecentAuth: true}
+ for _, suffix := range []string{`}`, `,"password":"incorrect"}`, `,"password":" FixtureP12 "}`} {
+ recorder := httptest.NewRecorder()
+ entry.hand(ctx, recorder, httptest.NewRequest(http.MethodPost, "/api/security", strings.NewReader(entry.body+suffix)))
+ var result response.Response
+ if err := json.Unmarshal(recorder.Body.Bytes(), &result); err != nil || result.ErrorNo == 0 {
+ t.Fatal("missing or incorrect password must not allow credential operations")
+ }
+ if recorder.Header().Get("Cache-Control") != "no-store" {
+ t.Fatal("credential responses must not be cached")
+ }
+ if status() != before {
+ t.Fatal("rejected operation changed credential state")
+ }
+ }
+ })
+ }
+ }
+ // 本人验证通过也不能重置或撤销其他账号的客户端。
+ reauthUserAttempts, reauthIPAttempts = newLoginLimiter(10), newLoginLimiter(50)
+ for _, hand := range []func(*appcontext.Context, http.ResponseWriter, *http.Request){RegenerateClientCredential, RevokeClientCredential} {
+ recorder := httptest.NewRecorder()
+ body := fmt.Sprintf(`{"client_id":%d,"password":"FixtureP12"}`, client.ID)
+ hand(&appcontext.Context{UserID: other.ID, Role: access.RoleSuperAdmin}, recorder, httptest.NewRequest(http.MethodPost, "/api/security", strings.NewReader(body)))
+ var result response.Response
+ if err := json.Unmarshal(recorder.Body.Bytes(), &result); err != nil || result.ErrorNo == 0 || status() != before {
+ t.Fatal("credential operations must remain scoped to the current account")
+ }
+ }
+}
+
+func TestClientCredentialPasswordFailuresShareRateLimit(t *testing.T) {
+ _, _, engine, user, _ := newPublicSecurityFixture(t)
+ service, err := credentials.NewService(engine)
+ if err != nil {
+ t.Fatal(err)
+ }
+ previousDB, previousRuntime := db.Instance, securityRuntime
+ previousUserLimiter, previousIPLimiter := reauthUserAttempts, reauthIPAttempts
+ db.Instance, securityRuntime.credentials = engine, service
+ reauthUserAttempts, reauthIPAttempts = newLoginLimiter(1), newLoginLimiter(10)
+ t.Cleanup(func() {
+ db.Instance, securityRuntime = previousDB, previousRuntime
+ reauthUserAttempts, reauthIPAttempts = previousUserLimiter, previousIPLimiter
+ })
+ ctx := &appcontext.Context{UserID: user.ID, RecentAuth: true}
+ CreateClientCredential(ctx, httptest.NewRecorder(), httptest.NewRequest(http.MethodPost, "/api/security", strings.NewReader(`{"client_name":"设备","password":"incorrect"}`)))
+ recorder := httptest.NewRecorder()
+ RevealProtocolCredentials(ctx, recorder, httptest.NewRequest(http.MethodPost, "/api/security", strings.NewReader(`{"password":"FixtureP12"}`)))
+ if recorder.Code != http.StatusTooManyRequests {
+ t.Fatal("switching from client to legacy protocol entry must not bypass password failure limits")
+ }
+}
diff --git a/backend/controllers/security_client_credentials.go b/backend/controllers/security_client_credentials.go
index a9f5513..7ab7e05 100644
--- a/backend/controllers/security_client_credentials.go
+++ b/backend/controllers/security_client_credentials.go
@@ -2,6 +2,7 @@ package controllers
import (
"net/http"
+ "strconv"
"strings"
"git.pcux.cn/xuans/Smail/dto/response"
@@ -9,6 +10,32 @@ import (
"git.pcux.cn/xuans/Smail/utils/context"
)
+// requireProtocolCredentialPassword 对新旧客户端入口统一执行本人验证,不复用登录时的验证状态。
+func requireProtocolCredentialPassword(ctx *context.Context, w http.ResponseWriter, req *http.Request, plain string) bool {
+ w.Header().Set("Cache-Control", "no-store")
+ if plain == "" {
+ writeSecurityError(w, response.ParamsError, "请输入当前账号密码进行安全验证")
+ return false
+ }
+ userKey := reauthUserAttempts.accountKey(strconv.Itoa(ctx.UserID))
+ ipKey := reauthIPAttempts.ipKey(req)
+ if reauthUserAttempts.blocked(userKey) || reauthIPAttempts.blocked(ipKey) {
+ writeReauthError(w, http.StatusTooManyRequests, "REAUTH_RATE_LIMITED", "尝试次数过多,请稍后再试")
+ return false
+ }
+ if _, err := auth.AuthenticateUserByID(ctx.UserID, plain); err != nil {
+ if writePasswordBusy(w, err) {
+ return false
+ }
+ reauthUserAttempts.recordFailure(userKey)
+ reauthIPAttempts.recordFailure(ipKey)
+ writeSecurityError(w, response.ParamsError, "密码验证失败")
+ return false
+ }
+ reauthUserAttempts.clear(userKey)
+ return true
+}
+
// ListClientCredentials 返回当前用户的客户端授权码列表
func ListClientCredentials(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
service, ok := requireProtocolCredentialContext(ctx, w)
@@ -31,11 +58,15 @@ func CreateClientCredential(ctx *context.Context, w http.ResponseWriter, req *ht
}
var input struct {
ClientName string `json:"client_name"`
+ Password string `json:"password"`
}
if err := decodeJSONRequest(w, req, &input); err != nil || strings.TrimSpace(input.ClientName) == "" {
writeSecurityError(w, response.ParamsError, "请输入客户端名称")
return
}
+ if !requireProtocolCredentialPassword(ctx, w, req, input.Password) {
+ return
+ }
code, item, err := service.CreateClientCredential(ctx.UserID, input.ClientName)
if err != nil {
if writePasswordBusy(w, err) {
@@ -54,12 +85,16 @@ func RegenerateClientCredential(ctx *context.Context, w http.ResponseWriter, req
return
}
var input struct {
- ClientID int `json:"client_id"`
+ ClientID int `json:"client_id"`
+ Password string `json:"password"`
}
if err := decodeJSONRequest(w, req, &input); err != nil || input.ClientID <= 0 {
writeSecurityError(w, response.ParamsError, "客户端标识无效")
return
}
+ if !requireProtocolCredentialPassword(ctx, w, req, input.Password) {
+ return
+ }
code, err := service.RegenerateClientCredential(ctx.UserID, input.ClientID)
if err != nil {
if writePasswordBusy(w, err) {
@@ -78,12 +113,16 @@ func RevokeClientCredential(ctx *context.Context, w http.ResponseWriter, req *ht
return
}
var input struct {
- ClientID int `json:"client_id"`
+ ClientID int `json:"client_id"`
+ Password string `json:"password"`
}
if err := decodeJSONRequest(w, req, &input); err != nil || input.ClientID <= 0 {
writeSecurityError(w, response.ParamsError, "客户端标识无效")
return
}
+ if !requireProtocolCredentialPassword(ctx, w, req, input.Password) {
+ return
+ }
if err := service.RevokeClientCredential(ctx.UserID, input.ClientID); err != nil {
writeSecurityError(w, response.ParamsError, err.Error())
return
@@ -104,11 +143,7 @@ func RevealClientCredentials(ctx *context.Context, w http.ResponseWriter, req *h
writeSecurityError(w, response.ParamsError, "请输入当前账号密码进行安全验证")
return
}
- if _, err := auth.AuthenticateUserByID(ctx.UserID, input.Password); err != nil {
- if writePasswordBusy(w, err) {
- return
- }
- writeSecurityError(w, response.ParamsError, "密码验证失败")
+ if !requireProtocolCredentialPassword(ctx, w, req, input.Password) {
return
}
codes, err := service.RevealClientCredentials(ctx.UserID)
diff --git a/frontend/src/views/components/ThirdPartyAuthorization/ThirdPartyAuthorization.js b/frontend/src/views/components/ThirdPartyAuthorization/ThirdPartyAuthorization.js
index 779d1b2..7c85b04 100644
--- a/frontend/src/views/components/ThirdPartyAuthorization/ThirdPartyAuthorization.js
+++ b/frontend/src/views/components/ThirdPartyAuthorization/ThirdPartyAuthorization.js
@@ -19,12 +19,23 @@ export function usePage() {
// 新增客户端弹窗
const createDialogVisible = ref(false)
const newClientName = ref('')
+ const createPassword = ref('')
const createLoading = ref(false)
- // 密码验证查看弹窗
+ // 查看、重置及撤销均使用本人密码验证,操作目标在打开弹窗时固定。
const verifyDialogVisible = ref(false)
const verifyPassword = ref('')
const verifyLoading = ref(false)
+ const verifyAction = ref('reveal')
+ const verificationClient = ref(null)
+ const verifyActionLabel = computed(() => ({
+ reveal: '查看明文',
+ regenerate: '重新生成',
+ revoke: '撤销授权码',
+ }[verifyAction.value]))
+ const verifyDescription = computed(() => verificationClient.value
+ ? `请验证本人登录密码,以${verifyActionLabel.value}:${verificationClient.value.client_name}`
+ : '查看客户端授权码前,请验证本人登录密码。')
const currentClient = computed(() => {
if (!selectedClientId.value || !Array.isArray(clientList.value)) return null
@@ -129,25 +140,35 @@ export function usePage() {
}
const openCreateDialog = () => {
+ if (loading.value || createLoading.value || verifyLoading.value) return
newClientName.value = ''
+ createPassword.value = ''
createDialogVisible.value = true
}
const closeCreateDialog = () => {
createDialogVisible.value = false
newClientName.value = ''
+ createPassword.value = ''
}
const submitCreateClient = async () => {
+ if (createLoading.value) return
const name = newClientName.value.trim()
if (!name) {
notify('请输入客户端名称')
return
}
+ const password = createPassword.value
+ if (!password) {
+ notify('请输入当前账号登录密码')
+ return
+ }
createLoading.value = true
try {
const response = await http.post('/api/v1/security/credentials/clients/create', {
client_name: name,
+ password,
})
if (response.errorNo !== 0) {
throw new Error(response.errorMsg || '创建客户端专属授权码失败')
@@ -166,12 +187,13 @@ export function usePage() {
} catch (error) {
notify(error?.message || '创建客户端授权码失败')
} finally {
+ createPassword.value = ''
createLoading.value = false
}
}
const regenerateClientCode = async (client) => {
- if (!client) return
+ if (!client || loading.value || verifyLoading.value) return
try {
await ElMessageBox.confirm(
`确定重新生成客户端「${client.client_name}」的专属授权码吗?旧授权码将立即失效。`,
@@ -186,28 +208,11 @@ export function usePage() {
return
}
- loading.value = true
- try {
- const response = await http.post('/api/v1/security/credentials/clients/regenerate', {
- client_id: client.id,
- })
- if (response.errorNo !== 0) {
- throw new Error(response.errorMsg || '重新生成客户端授权码失败')
- }
- const newCode = response.data?.code || ''
- revealedCodes[client.id] = newCode
- startRevealCountdown(60)
- notify(`客户端「${client.client_name}」专属授权码已重新生成(60秒内有效显示)`, 'success')
- await loadClientCredentials()
- } catch (error) {
- notify(error?.message || '重新生成失败')
- } finally {
- loading.value = false
- }
+ openActionVerification('regenerate', client)
}
const revokeClientCode = async (client) => {
- if (!client) return
+ if (!client || loading.value || verifyLoading.value) return
try {
await ElMessageBox.confirm(
`确定撤销并删除客户端「${client.client_name}」吗?撤销后该设备将无法通过授权码登录收发信。`,
@@ -222,22 +227,7 @@ export function usePage() {
return
}
- loading.value = true
- try {
- const response = await http.post('/api/v1/security/credentials/clients/revoke', {
- client_id: client.id,
- })
- if (response.errorNo !== 0) {
- throw new Error(response.errorMsg || '撤销客户端失败')
- }
- delete revealedCodes[client.id]
- notify(`客户端「${client.client_name}」已成功撤销`, 'success')
- await loadClientCredentials()
- } catch (error) {
- notify(error?.message || '撤销失败')
- } finally {
- loading.value = false
- }
+ openActionVerification('revoke', client)
}
const copyText = async (text, label = '内容') => {
@@ -267,34 +257,64 @@ export function usePage() {
await copyText(codeText, '专属授权码')
}
- const openVerifyDialog = () => {
+ const openActionVerification = (action, client = null) => {
+ if (verifyLoading.value) return
+ hideCodesAutomatically(true)
+ verifyAction.value = action
+ verificationClient.value = client ? { id: client.id, client_name: client.client_name } : null
verifyPassword.value = ''
verifyDialogVisible.value = true
}
+ const openVerifyDialog = () => openActionVerification('reveal')
+
const closeVerifyDialog = () => {
verifyDialogVisible.value = false
verifyPassword.value = ''
+ verificationClient.value = null
}
const submitVerifyAndReveal = async () => {
- const password = verifyPassword.value.trim()
+ if (verifyLoading.value) return
+ const password = verifyPassword.value
if (!password) {
notify('请输入当前账号登录密码')
return
}
+ const action = verifyAction.value
+ const client = verificationClient.value
+ if (action !== 'reveal' && !client) return
verifyLoading.value = true
try {
- const response = await http.post('/api/v1/security/credentials/clients/reveal', {
+ const paths = {
+ reveal: '/api/v1/security/credentials/clients/reveal',
+ regenerate: '/api/v1/security/credentials/clients/regenerate',
+ revoke: '/api/v1/security/credentials/clients/revoke',
+ }
+ const response = await http.post(paths[action], {
password,
+ ...(client ? { client_id: client.id } : {}),
})
if (response.errorNo !== 0) {
- throw new Error(response.errorMsg || '密码验证失败,无法查看授权码')
+ throw new Error(response.errorMsg || '安全验证或授权码操作失败')
+ }
+ closeVerifyDialog()
+ if (action === 'regenerate') {
+ revealedCodes[client.id] = response.data?.code || ''
+ startRevealCountdown(60)
+ notify(`客户端「${client.client_name}」授权码已重新生成,旧授权码已失效`, 'success')
+ await loadClientCredentials()
+ return
+ }
+ if (action === 'revoke') {
+ hideCodesAutomatically(true)
+ notify(`客户端「${client.client_name}」已成功撤销`, 'success')
+ await loadClientCredentials()
+ return
}
const map = response.data || {}
Object.keys(revealedCodes).forEach(k => delete revealedCodes[k])
Object.assign(revealedCodes, map)
- closeVerifyDialog()
const count = Object.keys(map).length
if (count > 0) {
startRevealCountdown(60)
@@ -305,6 +325,7 @@ export function usePage() {
} catch (error) {
notify(error?.message || '验证失败,请检查密码是否正确')
} finally {
+ verifyPassword.value = ''
verifyLoading.value = false
}
}
@@ -315,7 +336,9 @@ export function usePage() {
})
onUnmounted(() => {
- clearCountdownTimer()
+ hideCodesAutomatically(true)
+ createPassword.value = ''
+ verifyPassword.value = ''
})
return {
@@ -329,10 +352,13 @@ export function usePage() {
revealCountdown,
createDialogVisible,
newClientName,
+ createPassword,
createLoading,
verifyDialogVisible,
verifyPassword,
verifyLoading,
+ verifyActionLabel,
+ verifyDescription,
handleClientSelect,
openCreateDialog,
closeCreateDialog,
diff --git a/frontend/src/views/components/ThirdPartyAuthorization/ThirdPartyAuthorization.vue b/frontend/src/views/components/ThirdPartyAuthorization/ThirdPartyAuthorization.vue
index 907252c..14930d6 100644
--- a/frontend/src/views/components/ThirdPartyAuthorization/ThirdPartyAuthorization.vue
+++ b/frontend/src/views/components/ThirdPartyAuthorization/ThirdPartyAuthorization.vue
@@ -264,6 +264,8 @@
title="新增客户端专属授权码"
width="min(440px, calc(100vw - 32px))"
:close-on-click-modal="false"
+ :close-on-press-escape="!createLoading"
+ :show-close="!createLoading"
class="compact-dialog"
@close="closeCreateDialog"
>
@@ -279,23 +281,32 @@
maxlength="32"
show-word-limit
autocomplete="off"
- @keyup.enter="submitCreateClient"
+ />
+
+
- 查看已保存的客户端专属授权码明文前,请先验证当前账号的登录密码: + {{ verifyDescription }}