Skip to content

Commit a97765a

Browse files
authored
feat: 删除所有密钥 (#142)
1 parent 3b5733c commit a97765a

File tree

7 files changed

+180
-25
lines changed

7 files changed

+180
-25
lines changed

internal/handler/key_handler.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,27 @@ func (s *Server) ClearAllInvalidKeys(c *gin.Context) {
338338
response.Success(c, gin.H{"message": fmt.Sprintf("%d invalid keys cleared.", rowsAffected)})
339339
}
340340

341+
// ClearAllKeys deletes all keys from a group.
342+
func (s *Server) ClearAllKeys(c *gin.Context) {
343+
var req GroupIDRequest
344+
if err := c.ShouldBindJSON(&req); err != nil {
345+
response.Error(c, app_errors.NewAPIError(app_errors.ErrInvalidJSON, err.Error()))
346+
return
347+
}
348+
349+
if _, ok := s.findGroupByID(c, req.GroupID); !ok {
350+
return
351+
}
352+
353+
rowsAffected, err := s.KeyService.ClearAllKeys(req.GroupID)
354+
if err != nil {
355+
response.Error(c, app_errors.ParseDBError(err))
356+
return
357+
}
358+
359+
response.Success(c, gin.H{"message": fmt.Sprintf("%d keys cleared.", rowsAffected)})
360+
}
361+
341362
// ExportKeys handles exporting keys to a text file.
342363
func (s *Server) ExportKeys(c *gin.Context) {
343364
groupID, err := validateGroupIDFromQuery(c)

internal/keypool/provider.go

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -442,27 +442,43 @@ func (p *KeyProvider) RestoreMultipleKeys(groupID uint, keyValues []string) (int
442442

443443
// RemoveInvalidKeys 移除组内所有无效的 Key。
444444
func (p *KeyProvider) RemoveInvalidKeys(groupID uint) (int64, error) {
445-
var invalidKeys []models.APIKey
445+
return p.removeKeysByStatus(groupID, models.KeyStatusInvalid)
446+
}
447+
448+
// RemoveAllKeys 移除组内所有的 Key。
449+
func (p *KeyProvider) RemoveAllKeys(groupID uint) (int64, error) {
450+
return p.removeKeysByStatus(groupID)
451+
}
452+
453+
// removeKeysByStatus is a generic function to remove keys by status.
454+
// If no status is provided, it removes all keys in the group.
455+
func (p *KeyProvider) removeKeysByStatus(groupID uint, status ...string) (int64, error) {
456+
var keysToRemove []models.APIKey
446457
var removedCount int64
447458

448459
err := p.db.Transaction(func(tx *gorm.DB) error {
449-
if err := tx.Where("group_id = ? AND status = ?", groupID, models.KeyStatusInvalid).Find(&invalidKeys).Error; err != nil {
460+
query := tx.Where("group_id = ?", groupID)
461+
if len(status) > 0 {
462+
query = query.Where("status IN ?", status)
463+
}
464+
465+
if err := query.Find(&keysToRemove).Error; err != nil {
450466
return err
451467
}
452468

453-
if len(invalidKeys) == 0 {
469+
if len(keysToRemove) == 0 {
454470
return nil
455471
}
456472

457-
result := tx.Where("id IN ?", pluckIDs(invalidKeys)).Delete(&models.APIKey{})
473+
result := tx.Where("id IN ?", pluckIDs(keysToRemove)).Delete(&models.APIKey{})
458474
if result.Error != nil {
459475
return result.Error
460476
}
461477
removedCount = result.RowsAffected
462478

463-
for _, key := range invalidKeys {
479+
for _, key := range keysToRemove {
464480
if err := p.removeKeyFromStore(key.ID, key.GroupID); err != nil {
465-
logrus.WithFields(logrus.Fields{"keyID": key.ID, "error": err}).Error("Failed to remove invalid key from store after DB deletion, rolling back transaction")
481+
logrus.WithFields(logrus.Fields{"keyID": key.ID, "error": err}).Error("Failed to remove key from store after DB deletion, rolling back transaction")
466482
return err
467483
}
468484
}

internal/router/router.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ func registerProtectedAPIRoutes(api *gin.RouterGroup, serverHandler *handler.Ser
124124
keys.POST("/restore-multiple", serverHandler.RestoreMultipleKeys)
125125
keys.POST("/restore-all-invalid", serverHandler.RestoreAllInvalidKeys)
126126
keys.POST("/clear-all-invalid", serverHandler.ClearAllInvalidKeys)
127+
keys.POST("/clear-all", serverHandler.ClearAllKeys)
127128
keys.POST("/validate-group", serverHandler.ValidateGroupKeys)
128129
keys.POST("/test-multiple", serverHandler.TestMultipleKeys)
129130
}

internal/services/key_service.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,11 @@ func (s *KeyService) ClearAllInvalidKeys(groupID uint) (int64, error) {
243243
return s.KeyProvider.RemoveInvalidKeys(groupID)
244244
}
245245

246+
// ClearAllKeys deletes all keys from a group.
247+
func (s *KeyService) ClearAllKeys(groupID uint) (int64, error) {
248+
return s.KeyProvider.RemoveAllKeys(groupID)
249+
}
250+
246251
// DeleteMultipleKeys handles the business logic of deleting keys from a text block.
247252
func (s *KeyService) DeleteMultipleKeys(groupID uint, keysText string) (*DeleteKeysResult, error) {
248253
keysToDelete := s.ParseKeysFromText(keysText)

web/src/api/keys.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,17 @@ export const keysApi = {
147147
);
148148
},
149149

150+
// 清空所有密钥
151+
clearAllKeys(group_id: number): Promise<{ data: { message: string } }> {
152+
return http.post(
153+
"/keys/clear-all",
154+
{ group_id },
155+
{
156+
hideMessage: true,
157+
}
158+
);
159+
},
160+
150161
// 导出密钥
151162
exportKeys(groupId: number, status: "all" | "active" | "invalid" = "all") {
152163
const authKey = localStorage.getItem("authKey");

web/src/components/keys/GroupInfoCard.vue

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ import {
1616
NGrid,
1717
NGridItem,
1818
NIcon,
19+
NInput,
1920
NSpin,
2021
NTag,
2122
NTooltip,
2223
useDialog,
2324
} from "naive-ui";
24-
import { computed, onMounted, ref, watch } from "vue";
25+
import { computed, h, onMounted, ref, watch } from "vue";
2526
import GroupFormModal from "./GroupFormModal.vue";
2627
2728
interface Props {
@@ -42,6 +43,7 @@ const loading = ref(false);
4243
const dialog = useDialog();
4344
const showEditModal = ref(false);
4445
const delLoading = ref(false);
46+
const confirmInput = ref("");
4547
const expandedName = ref<string[]>([]);
4648
const configOptions = ref<GroupConfigOption[]>([]);
4749
const showProxyKeys = ref(false);
@@ -171,26 +173,55 @@ async function handleDelete() {
171173
return;
172174
}
173175
174-
const d = dialog.warning({
176+
dialog.warning({
175177
title: "删除分组",
176-
content: `确定要删除分组 "${getGroupDisplayName(props.group)}" 吗?此操作不可恢复。`,
178+
content: `确定要删除分组 "${getGroupDisplayName(
179+
props.group
180+
)}" 吗?此操作将删除分组及其下所有密钥,且不可恢复。`,
177181
positiveText: "确定",
178182
negativeText: "取消",
179-
onPositiveClick: async () => {
180-
d.loading = true;
181-
delLoading.value = true;
182-
183-
try {
184-
if (props.group?.id) {
185-
await keysApi.deleteGroup(props.group.id);
186-
emit("delete", props.group);
187-
}
188-
} catch (error) {
189-
console.error("删除分组失败:", error);
190-
} finally {
191-
d.loading = false;
192-
delLoading.value = false;
193-
}
183+
onPositiveClick: () => {
184+
confirmInput.value = ""; // Reset before opening second dialog
185+
dialog.create({
186+
title: "请输入分组名称以确认删除",
187+
content: () =>
188+
h("div", null, [
189+
h("p", null, [
190+
"这是一个非常危险的操作。为防止误操作,请输入分组名称 ",
191+
h("strong", { style: { color: "#d03050" } }, props.group!.name),
192+
" 以确认删除。",
193+
]),
194+
h(NInput, {
195+
value: confirmInput.value,
196+
"onUpdate:value": (v) => {
197+
confirmInput.value = v;
198+
},
199+
placeholder: "请输入分组名称",
200+
}),
201+
]),
202+
positiveText: "确认删除",
203+
negativeText: "取消",
204+
onPositiveClick: async () => {
205+
if (confirmInput.value !== props.group!.name) {
206+
window.$message.error("分组名称输入不正确");
207+
return false; // Prevent dialog from closing
208+
}
209+
210+
delLoading.value = true;
211+
try {
212+
if (props.group?.id) {
213+
await keysApi.deleteGroup(props.group.id);
214+
emit("delete", props.group);
215+
window.$message.success("分组已成功删除");
216+
}
217+
} catch (error) {
218+
console.error("删除分组失败:", error);
219+
window.$message.error("删除分组失败,请稍后重试");
220+
} finally {
221+
delLoading.value = false;
222+
}
223+
},
224+
});
194225
},
195226
});
196227
}

web/src/components/keys/KeyTable.vue

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
useDialog,
2727
type MessageReactive,
2828
} from "naive-ui";
29-
import { ref, watch } from "vue";
29+
import { h, ref, watch } from "vue";
3030
import KeyCreateDialog from "./KeyCreateDialog.vue";
3131
import KeyDeleteDialog from "./KeyDeleteDialog.vue";
3232
@@ -49,6 +49,7 @@ const pageSize = ref(12);
4949
const total = ref(0);
5050
const totalPages = ref(0);
5151
const dialog = useDialog();
52+
const confirmInput = ref("");
5253
5354
// 状态过滤选项
5455
const statusOptions = [
@@ -65,6 +66,7 @@ const moreOptions = [
6566
{ type: "divider" },
6667
{ label: "恢复所有无效密钥", key: "restoreAll" },
6768
{ label: "清空所有无效密钥", key: "clearInvalid", props: { style: { color: "#d03050" } } },
69+
{ label: "清空所有密钥", key: "clearAll", props: { style: { color: "red", fontWeight: "bold" } } },
6870
{ type: "divider" },
6971
{ label: "验证所有密钥", key: "validateAll" },
7072
{ label: "验证有效密钥", key: "validateActive" },
@@ -163,6 +165,9 @@ function handleMoreAction(key: string) {
163165
case "clearInvalid":
164166
clearAllInvalid();
165167
break;
168+
case "clearAll":
169+
clearAll();
170+
break;
166171
}
167172
}
168173
@@ -464,6 +469,71 @@ async function clearAllInvalid() {
464469
});
465470
}
466471
472+
async function clearAll() {
473+
if (!props.selectedGroup?.id || isDeling.value) {
474+
return;
475+
}
476+
477+
dialog.warning({
478+
title: "清空所有密钥",
479+
content: "此操作将永久删除该分组下的所有密钥,且不可恢复!确定要继续吗?",
480+
positiveText: "确定",
481+
negativeText: "取消",
482+
onPositiveClick: () => {
483+
confirmInput.value = ""; // Reset before opening second dialog
484+
dialog.create({
485+
title: "请输入分组名称以确认",
486+
content: () =>
487+
h("div", null, [
488+
h("p", null, [
489+
"这是一个非常危险的操作,将删除此分组下的",
490+
h("strong", null, "所有"),
491+
"密钥。为防止误操作,请输入分组名称 ",
492+
h(
493+
"strong",
494+
{ style: { color: "#d03050" } },
495+
props.selectedGroup!.name
496+
),
497+
" 以确认。",
498+
]),
499+
h(NInput, {
500+
value: confirmInput.value,
501+
"onUpdate:value": (v) => {
502+
confirmInput.value = v;
503+
},
504+
placeholder: "请输入分组名称",
505+
}),
506+
]),
507+
positiveText: "确认清空",
508+
negativeText: "取消",
509+
onPositiveClick: async () => {
510+
if (confirmInput.value !== props.selectedGroup!.name) {
511+
window.$message.error("分组名称输入不正确");
512+
return false; // Prevent dialog from closing
513+
}
514+
515+
if (!props.selectedGroup?.id) {
516+
return;
517+
}
518+
519+
isDeling.value = true;
520+
try {
521+
await keysApi.clearAllKeys(props.selectedGroup.id);
522+
window.$message.success("已成功清空所有密钥");
523+
await loadKeys();
524+
// Trigger sync operation refresh
525+
triggerSyncOperationRefresh(props.selectedGroup.name, "CLEAR_ALL");
526+
} catch (_error) {
527+
console.error("清空失败", _error);
528+
} finally {
529+
isDeling.value = false;
530+
}
531+
},
532+
});
533+
},
534+
});
535+
}
536+
467537
function changePage(page: number) {
468538
currentPage.value = page;
469539
}

0 commit comments

Comments
 (0)