Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions app/products/web/admin/tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ class ToggleTokenDisabledRequest(BaseModel):
disabled: bool


class ToggleTokensDisabledRequest(BaseModel):
tokens: list[str]
disabled: bool


class TokenImportItem(BaseModel):
token: str
tags: list[str] = []
Expand Down Expand Up @@ -358,6 +363,69 @@ async def toggle_token_disabled(
return _json({"status": "success", "token": token, "disabled": False})


@router.post("/tokens/disabled/batch")
async def toggle_tokens_disabled(
req: ToggleTokensDisabledRequest,
repo: "AccountRepository" = Depends(get_repo),
):
cleaned: list[str] = []
seen: set[str] = set()
for raw in req.tokens:
token = _sanitize(raw)
if token and token not in seen:
seen.add(token)
cleaned.append(token)
if not cleaned:
raise ValidationError("No valid tokens provided", param="tokens")

records = await repo.get_accounts(cleaned)
if not records:
raise AppError(
"No matching accounts found",
kind=ErrorKind.VALIDATION,
code="account_not_found",
status=404,
)

ts = now_ms()
patches: list[AccountPatch] = []
for record in records:
if req.disabled:
patches.append(AccountPatch(
token=record.token,
status=AccountStatus.DISABLED,
state_reason="operator_disabled",
ext_merge={
**record.ext,
"disabled_at": ts,
"disabled_reason": "operator_disabled",
},
))
else:
patches.append(AccountPatch(
token=record.token,
status=AccountStatus.ACTIVE,
clear_failures=True,
))

result = await repo.patch_accounts(patches)
logger.info(
"admin tokens disabled batch updated: disabled={} requested_count={} patched_count={}",
req.disabled,
len(cleaned),
result.patched,
)
return _json({
"status": "success",
"disabled": req.disabled,
"summary": {
"total": len(cleaned),
"ok": result.patched,
"fail": max(0, len(cleaned) - result.patched),
},
})


@router.put("/tokens/pool")
async def replace_pool(
req: ReplacePoolRequest,
Expand Down
51 changes: 49 additions & 2 deletions app/statics/admin/account.html
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,12 @@
<button type="button" onclick="batchRefreshSel()" id="btn-refresh" class="toolbar-icon-btn" style="display:none">
<svg viewBox="0 0 24 24" stroke-width="1.8"><path d="M20 11a8 8 0 0 0-14.6-4.6"/><path d="M4 4v5h5"/><path d="M4 13a8 8 0 0 0 14.6 4.6"/><path d="M20 20v-5h-5"/></svg>
</button>
<button type="button" onclick="batchDisableSel()" id="btn-disable" class="toolbar-icon-btn" style="display:none">
<svg viewBox="0 0 24 24" stroke-width="1.8"><circle cx="12" cy="12" r="8"/><path d="M8.5 8.5 15.5 15.5"/></svg>
</button>
<button type="button" onclick="batchRestoreSel()" id="btn-restore" class="toolbar-icon-btn" style="display:none">
<svg viewBox="0 0 24 24" stroke-width="1.8"><path d="M3 12a9 9 0 1 0 3-6.708"/><path d="M3 4v5h5"/></svg>
</button>
<button type="button" onclick="batchDeleteSel()" id="btn-delete" class="toolbar-icon-btn toolbar-icon-btn-danger" style="display:none">
<svg viewBox="0 0 24 24" stroke-width="1.8"><path d="M5 7h14"/><path d="M9 7V4h6v3"/><path d="M8 10v7"/><path d="M12 10v7"/><path d="M16 10v7"/><path d="M7 7l1 13h8l1-13"/></svg>
</button>
Expand Down Expand Up @@ -1119,6 +1125,8 @@
['btn-export', 'account.export', '导出数据'],
['btn-nsfw', 'account.batchNsfw', '开启 NSFW'],
['btn-refresh', 'account.batchRefresh', '刷新选中'],
['btn-disable', 'account.batchDisable', '禁用选中'],
['btn-restore', 'account.batchRestore', '恢复选中'],
['btn-delete', 'account.batchDelete', '删除选中'],
['btn-batch-cancel', 'account.cancel', '取消'],
];
Expand Down Expand Up @@ -1580,7 +1588,7 @@
}
function updateBatchBtns() {
const show = sel.size > 0;
['btn-nsfw','btn-refresh','btn-delete'].forEach(id =>
['btn-nsfw','btn-refresh','btn-disable','btn-restore','btn-delete'].forEach(id =>
document.getElementById(id).style.display = show ? '' : 'none');
}

Expand Down Expand Up @@ -1811,7 +1819,7 @@

async function _runBatch(endpoint, tokens, label, onDone) {
const btnCancel = document.getElementById('btn-batch-cancel');
['btn-nsfw','btn-refresh','btn-delete'].forEach(id =>
['btn-nsfw','btn-refresh','btn-disable','btn-restore','btn-delete'].forEach(id =>
document.getElementById(id).style.display = 'none');
btnCancel.style.display = '';

Expand Down Expand Up @@ -1930,6 +1938,45 @@
function disableOne(token) { setDisabled(token, true); }
function restoreOne(token) { setDisabled(token, false); }

function batchSetDisabled(disabled) {
if (!sel.size) return;
const tokens = [...sel];
const n = tokens.length;
const title = disabled
? tr('account.batchDisableConfirmTitle', null, '批量禁用账号')
: tr('account.batchRestoreConfirmTitle', null, '批量恢复账号');
const body = disabled
? tr('account.batchDisableConfirmBody', { n }, `确认禁用选中的 <b>${n}</b> 个账户?<br><small style="color:var(--fg-muted)">禁用后这些账号不会参与请求分配,但可随时恢复。</small>`)
: tr('account.batchRestoreConfirmBody', { n }, `确认恢复选中的 <b>${n}</b> 个账户?<br><small style="color:var(--fg-muted)">恢复后这些账号将重新参与请求分配。</small>`);

openConfirm(title, body, async () => {
try {
showToast(
disabled
? tr('account.disablingMany', { n }, `正在禁用 ${n} 个账户…`)
: tr('account.restoringMany', { n }, `正在恢复 ${n} 个账户…`),
'info',
);
const d = await _api('POST', '/tokens/disabled/batch', { tokens, disabled });
const ok = d.summary?.ok ?? 0;
const fail = d.summary?.fail ?? 0;
showToast(
disabled
? tr('account.disableManyDone', { ok, fail }, `禁用完成:成功 ${ok} 个,失败 ${fail} 个`)
: tr('account.restoreManyDone', { ok, fail }, `恢复完成:成功 ${ok} 个,失败 ${fail} 个`),
fail > 0 ? 'error' : 'success',
);
sel.clear();
await load();
} catch (e) {
showToast(`${tr('account.operationFailed', null, '操作失败')}: ${e.message}`, 'error');
}
});
}

function batchDisableSel() { batchSetDisabled(true); }
function batchRestoreSel() { batchSetDisabled(false); }

async function batchRefreshSel() {
if (!sel.size) return;
await _runBatch('/batch/refresh', [...sel],
Expand Down
10 changes: 10 additions & 0 deletions app/statics/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@
"batchNsfw": "NSFW aktivieren",
"batchNsfwDisable": "NSFW deaktivieren",
"batchRefresh": "Auswahl aktualisieren",
"batchDisable": "Auswahl deaktivieren",
"batchRestore": "Auswahl wiederherstellen",
"batchDelete": "Auswahl löschen",
"colToken": "Token",
"colType": "Kontotyp",
Expand Down Expand Up @@ -264,6 +266,14 @@
"restoreConfirmBody": "{token} wiederherstellen?<br><small style=\"color:var(--fg-muted)\">Das Konto nimmt danach wieder an der Anfrageverteilung teil.</small>",
"restoringOne": "Konto wird wiederhergestellt…",
"restoreDone": "Konto wiederhergestellt",
"batchDisableConfirmTitle": "Konten deaktivieren",
"batchDisableConfirmBody": "Die ausgewählten <b>{n}</b> Konten deaktivieren?<br><small style=\"color:var(--fg-muted)\">Deaktivierte Konten werden nicht für Anfragen verwendet und können später wiederhergestellt werden.</small>",
"disablingMany": "{n} Konten werden deaktiviert…",
"disableManyDone": "Deaktivierung abgeschlossen: {ok} erfolgreich, {fail} fehlgeschlagen",
"batchRestoreConfirmTitle": "Konten wiederherstellen",
"batchRestoreConfirmBody": "Die ausgewählten <b>{n}</b> Konten wiederherstellen?<br><small style=\"color:var(--fg-muted)\">Wiederhergestellte Konten werden erneut für Anfragen verwendet.</small>",
"restoringMany": "{n} Konten werden wiederhergestellt…",
"restoreManyDone": "Wiederherstellung abgeschlossen: {ok} erfolgreich, {fail} fehlgeschlagen",
"nsfwConfirmTitle": "NSFW aktivieren",
"nsfwConfirmBody": "NSFW für <b>{n}</b> ausgewählte Konten aktivieren?",
"nsfwEnablingOne": "NSFW wird aktiviert…",
Expand Down
10 changes: 10 additions & 0 deletions app/statics/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@
"batchNsfw": "Enable NSFW",
"batchNsfwDisable": "Disable NSFW",
"batchRefresh": "Refresh Selected",
"batchDisable": "Disable Selected",
"batchRestore": "Restore Selected",
"batchDelete": "Delete Selected",
"colToken": "Token",
"colType": "Account Type",
Expand Down Expand Up @@ -265,6 +267,14 @@
"restoreConfirmBody": "Restore {token}?<br><small style=\"color:var(--fg-muted)\">The account will be returned to request allocation.</small>",
"restoringOne": "Restoring account…",
"restoreDone": "Account restored",
"batchDisableConfirmTitle": "Disable Accounts",
"batchDisableConfirmBody": "Disable the selected <b>{n}</b> accounts?<br><small style=\"color:var(--fg-muted)\">Disabled accounts are removed from request allocation and can be restored later.</small>",
"disablingMany": "Disabling {n} accounts…",
"disableManyDone": "Disable completed: {ok} succeeded, {fail} failed",
"batchRestoreConfirmTitle": "Restore Accounts",
"batchRestoreConfirmBody": "Restore the selected <b>{n}</b> accounts?<br><small style=\"color:var(--fg-muted)\">Restored accounts will be returned to request allocation.</small>",
"restoringMany": "Restoring {n} accounts…",
"restoreManyDone": "Restore completed: {ok} succeeded, {fail} failed",
"nsfwConfirmTitle": "Enable NSFW",
"nsfwConfirmBody": "Enable NSFW for <b>{n}</b> selected accounts?",
"nsfwEnablingOne": "Enabling NSFW…",
Expand Down
10 changes: 10 additions & 0 deletions app/statics/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@
"batchNsfw": "Activar NSFW",
"batchNsfwDisable": "Desactivar NSFW",
"batchRefresh": "Actualizar selección",
"batchDisable": "Desactivar selección",
"batchRestore": "Restaurar selección",
"batchDelete": "Eliminar selección",
"colToken": "Token",
"colType": "Tipo de cuenta",
Expand Down Expand Up @@ -264,6 +266,14 @@
"restoreConfirmBody": "¿Restaurar {token}?<br><small style=\"color:var(--fg-muted)\">La cuenta volverá a participar en la asignación de solicitudes.</small>",
"restoringOne": "Restaurando cuenta…",
"restoreDone": "Cuenta restaurada",
"batchDisableConfirmTitle": "Desactivar cuentas",
"batchDisableConfirmBody": "¿Desactivar las <b>{n}</b> cuentas seleccionadas?<br><small style=\"color:var(--fg-muted)\">Las cuentas desactivadas no participarán en la asignación de solicitudes y podrán restaurarse más tarde.</small>",
"disablingMany": "Desactivando {n} cuentas…",
"disableManyDone": "Desactivación completada: {ok} correctas, {fail} fallidas",
"batchRestoreConfirmTitle": "Restaurar cuentas",
"batchRestoreConfirmBody": "¿Restaurar las <b>{n}</b> cuentas seleccionadas?<br><small style=\"color:var(--fg-muted)\">Las cuentas restauradas volverán a participar en la asignación de solicitudes.</small>",
"restoringMany": "Restaurando {n} cuentas…",
"restoreManyDone": "Restauración completada: {ok} correctas, {fail} fallidas",
"nsfwConfirmTitle": "Activar NSFW",
"nsfwConfirmBody": "¿Activar NSFW para <b>{n}</b> cuentas seleccionadas?",
"nsfwEnablingOne": "Activando NSFW…",
Expand Down
10 changes: 10 additions & 0 deletions app/statics/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@
"batchNsfw": "Activer NSFW",
"batchNsfwDisable": "Désactiver NSFW",
"batchRefresh": "Actualiser la sélection",
"batchDisable": "Désactiver la sélection",
"batchRestore": "Restaurer la sélection",
"batchDelete": "Supprimer la sélection",
"colToken": "Token",
"colType": "Type de compte",
Expand Down Expand Up @@ -264,6 +266,14 @@
"restoreConfirmBody": "Restaurer {token} ?<br><small style=\"color:var(--fg-muted)\">Le compte reprendra la participation à l’allocation des requêtes.</small>",
"restoringOne": "Restauration du compte…",
"restoreDone": "Compte restauré",
"batchDisableConfirmTitle": "Désactiver les comptes",
"batchDisableConfirmBody": "Désactiver les <b>{n}</b> comptes sélectionnés ?<br><small style=\"color:var(--fg-muted)\">Les comptes désactivés ne participeront plus à l’allocation des requêtes et pourront être restaurés plus tard.</small>",
"disablingMany": "Désactivation de {n} comptes…",
"disableManyDone": "Désactivation terminée : {ok} réussies, {fail} échouées",
"batchRestoreConfirmTitle": "Restaurer les comptes",
"batchRestoreConfirmBody": "Restaurer les <b>{n}</b> comptes sélectionnés ?<br><small style=\"color:var(--fg-muted)\">Les comptes restaurés participeront de nouveau à l’allocation des requêtes.</small>",
"restoringMany": "Restauration de {n} comptes…",
"restoreManyDone": "Restauration terminée : {ok} réussies, {fail} échouées",
"nsfwConfirmTitle": "Activer NSFW",
"nsfwConfirmBody": "Activer NSFW pour <b>{n}</b> comptes sélectionnés ?",
"nsfwEnablingOne": "Activation de NSFW…",
Expand Down
10 changes: 10 additions & 0 deletions app/statics/i18n/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@
"batchNsfw": "NSFW を有効化",
"batchNsfwDisable": "NSFW を無効化",
"batchRefresh": "選択項目を更新",
"batchDisable": "選択項目を無効化",
"batchRestore": "選択項目を復元",
"batchDelete": "選択項目を削除",
"colToken": "トークン",
"colType": "アカウント種別",
Expand Down Expand Up @@ -264,6 +266,14 @@
"restoreConfirmBody": "{token} を復元しますか?<br><small style=\"color:var(--fg-muted)\">復元後、このアカウントは再びリクエスト割り当てに参加します。</small>",
"restoringOne": "アカウントを復元しています…",
"restoreDone": "アカウントを復元しました",
"batchDisableConfirmTitle": "アカウントを一括無効化",
"batchDisableConfirmBody": "選択した <b>{n}</b> 件のアカウントを無効化しますか?<br><small style=\"color:var(--fg-muted)\">無効化されたアカウントはリクエスト割り当てに参加せず、後で復元できます。</small>",
"disablingMany": "{n} 件のアカウントを無効化しています…",
"disableManyDone": "無効化完了: 成功 {ok} 件、失敗 {fail} 件",
"batchRestoreConfirmTitle": "アカウントを一括復元",
"batchRestoreConfirmBody": "選択した <b>{n}</b> 件のアカウントを復元しますか?<br><small style=\"color:var(--fg-muted)\">復元後、これらのアカウントは再びリクエスト割り当てに参加します。</small>",
"restoringMany": "{n} 件のアカウントを復元しています…",
"restoreManyDone": "復元完了: 成功 {ok} 件、失敗 {fail} 件",
"nsfwConfirmTitle": "NSFW を有効化",
"nsfwConfirmBody": "選択した <b>{n}</b> 件のアカウントで NSFW を有効にしますか?",
"nsfwEnablingOne": "NSFW を有効化しています…",
Expand Down
10 changes: 10 additions & 0 deletions app/statics/i18n/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@
"batchNsfw": "开启 NSFW",
"batchNsfwDisable": "关闭 NSFW",
"batchRefresh": "刷新选中",
"batchDisable": "禁用选中",
"batchRestore": "恢复选中",
"batchDelete": "删除选中",
"colToken": "Token",
"colType": "账户类型",
Expand Down Expand Up @@ -265,6 +267,14 @@
"restoreConfirmBody": "确认恢复 {token}?<br><small style=\"color:var(--fg-muted)\">恢复后该账号将重新参与请求分配。</small>",
"restoringOne": "正在恢复账号…",
"restoreDone": "账号已恢复",
"batchDisableConfirmTitle": "批量禁用账号",
"batchDisableConfirmBody": "确认禁用选中的 <b>{n}</b> 个账户?<br><small style=\"color:var(--fg-muted)\">禁用后这些账号不会参与请求分配,但可随时恢复。</small>",
"disablingMany": "正在禁用 {n} 个账户…",
"disableManyDone": "禁用完成:成功 {ok} 个,失败 {fail} 个",
"batchRestoreConfirmTitle": "批量恢复账号",
"batchRestoreConfirmBody": "确认恢复选中的 <b>{n}</b> 个账户?<br><small style=\"color:var(--fg-muted)\">恢复后这些账号将重新参与请求分配。</small>",
"restoringMany": "正在恢复 {n} 个账户…",
"restoreManyDone": "恢复完成:成功 {ok} 个,失败 {fail} 个",
"nsfwConfirmTitle": "启用 NSFW",
"nsfwConfirmBody": "确认为选中的 <b>{n}</b> 个账户启用 NSFW?",
"nsfwEnablingOne": "正在启用 NSFW…",
Expand Down
Loading