mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-08-27 19:20:33 +08:00
Compare commits
4 Commits
v1.11.0
...
release/v1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1572f4cd0f | ||
|
|
aeb2ac687d | ||
|
|
108b8ef4ac | ||
|
|
2762741226 |
18
CHANGELOG.md
18
CHANGELOG.md
@@ -6,7 +6,23 @@
|
||||
<a href="CHANGELOG_EN.md">English</a>
|
||||
</p>
|
||||
|
||||
## v1.11.0(main)
|
||||
## v1.11.1(main)
|
||||
|
||||
### Features
|
||||
|
||||
- feat: |Admin| 数据库页面新增 D1 存储容量展示,支持选择并保存 Free 或 Workers Paid 套餐,对比当前数据库大小和容量上限
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix: |Admin| 修复切换一级标签页时二级标签页偶发无选中项、内容不显示及指示条偏移的问题
|
||||
|
||||
### Improvements
|
||||
|
||||
### Testing
|
||||
|
||||
- test: |E2E| 覆盖 D1 数据库大小响应、配置键隔离,以及数据库页面套餐选择的持久化与刷新恢复
|
||||
|
||||
## v1.11.0
|
||||
|
||||
### Features
|
||||
|
||||
|
||||
@@ -6,7 +6,23 @@
|
||||
<a href="CHANGELOG_EN.md">English</a>
|
||||
</p>
|
||||
|
||||
## v1.11.0(main)
|
||||
## v1.11.1(main)
|
||||
|
||||
### Features
|
||||
|
||||
- feat: |Admin| Add D1 storage capacity details to the database page, with persistent Free and Workers Paid plan selection and a comparison between the current database size and capacity limit
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix: |Admin| Fix secondary tabs occasionally losing their active item, hiding content, and leaving the indicator offset after switching primary tabs
|
||||
|
||||
### Improvements
|
||||
|
||||
### Testing
|
||||
|
||||
- test: |E2E| Cover the D1 database-size response, config-key isolation, and persistence of the database-page plan selection across reloads
|
||||
|
||||
## v1.11.0
|
||||
|
||||
### Features
|
||||
|
||||
|
||||
73
e2e/tests/api/admin-config.spec.ts
Normal file
73
e2e/tests/api/admin-config.spec.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { WORKER_URL } from '../../fixtures/test-helpers';
|
||||
|
||||
const ADMIN_HEADERS = { 'x-admin-auth': 'e2e-admin-pass' };
|
||||
|
||||
test.describe('Admin Config and D1 Storage', () => {
|
||||
test('reports database size in the database status response', async ({ request }) => {
|
||||
const response = await request.get(`${WORKER_URL}/admin/db_version`, {
|
||||
headers: ADMIN_HEADERS,
|
||||
});
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
const body = await response.json();
|
||||
expect(body.database_size).toEqual(expect.any(Number));
|
||||
expect(body.database_size).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('saves and retrieves a namespaced config value', async ({ request }) => {
|
||||
const saveResponse = await request.post(`${WORKER_URL}/admin/config`, {
|
||||
headers: ADMIN_HEADERS,
|
||||
data: { key: 'd1_storage_plan', value: 'free' },
|
||||
});
|
||||
|
||||
expect(saveResponse.ok()).toBe(true);
|
||||
expect(await saveResponse.json()).toEqual({
|
||||
success: true,
|
||||
key: 'd1_storage_plan',
|
||||
value: 'free',
|
||||
});
|
||||
|
||||
const getResponse = await request.get(`${WORKER_URL}/admin/config/d1_storage_plan`, {
|
||||
headers: ADMIN_HEADERS,
|
||||
});
|
||||
|
||||
expect(getResponse.ok()).toBe(true);
|
||||
expect(await getResponse.json()).toEqual({
|
||||
key: 'd1_storage_plan',
|
||||
value: 'free',
|
||||
});
|
||||
});
|
||||
|
||||
test('keeps config values isolated from internal settings', async ({ request }) => {
|
||||
const versionBeforeResponse = await request.get(`${WORKER_URL}/admin/db_version`, {
|
||||
headers: ADMIN_HEADERS,
|
||||
});
|
||||
const versionBefore = (await versionBeforeResponse.json()).current_db_version;
|
||||
|
||||
const saveResponse = await request.post(`${WORKER_URL}/admin/config`, {
|
||||
headers: ADMIN_HEADERS,
|
||||
data: { key: 'db_version', value: 'shadow-version' },
|
||||
});
|
||||
expect(saveResponse.ok()).toBe(true);
|
||||
|
||||
const versionAfterResponse = await request.get(`${WORKER_URL}/admin/db_version`, {
|
||||
headers: ADMIN_HEADERS,
|
||||
});
|
||||
expect((await versionAfterResponse.json()).current_db_version).toBe(versionBefore);
|
||||
});
|
||||
|
||||
test('rejects invalid keys and non-string values', async ({ request }) => {
|
||||
const invalidKeyResponse = await request.post(`${WORKER_URL}/admin/config`, {
|
||||
headers: ADMIN_HEADERS,
|
||||
data: { key: 'invalid key', value: 'value' },
|
||||
});
|
||||
expect(invalidKeyResponse.status()).toBe(400);
|
||||
|
||||
const invalidValueResponse = await request.post(`${WORKER_URL}/admin/config`, {
|
||||
headers: ADMIN_HEADERS,
|
||||
data: { key: 'valid_key', value: 1 },
|
||||
});
|
||||
expect(invalidValueResponse.status()).toBe(400);
|
||||
});
|
||||
});
|
||||
37
e2e/tests/browser/database-storage.spec.ts
Normal file
37
e2e/tests/browser/database-storage.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { FRONTEND_URL, WORKER_URL } from '../../fixtures/test-helpers';
|
||||
|
||||
const ADMIN_HEADERS = { 'x-admin-auth': 'e2e-admin-pass' };
|
||||
|
||||
test('persists the selected D1 plan and restores it after reload', async ({ page, request }) => {
|
||||
const seedResponse = await request.post(`${WORKER_URL}/admin/config`, {
|
||||
headers: ADMIN_HEADERS,
|
||||
data: { key: 'd1_storage_plan', value: 'free' },
|
||||
});
|
||||
expect(seedResponse.ok()).toBe(true);
|
||||
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem('adminAuth', 'e2e-admin-pass');
|
||||
sessionStorage.setItem('adminTab', 'qucickSetup');
|
||||
});
|
||||
await page.goto(`${FRONTEND_URL}/en/admin`);
|
||||
|
||||
const storagePanel = page.locator('.storage-panel');
|
||||
const planSelect = storagePanel.locator('.plan-select .n-select');
|
||||
|
||||
await expect(storagePanel.getByText('Current Database Size', { exact: true })).toBeVisible();
|
||||
await expect(storagePanel.getByText('Database Capacity Limit', { exact: true })).toBeVisible();
|
||||
await expect(storagePanel.getByText('Capacity Usage', { exact: true })).toBeVisible();
|
||||
await expect(planSelect).toContainText('Free');
|
||||
|
||||
await planSelect.click();
|
||||
await page.locator('.n-base-select-option').filter({ hasText: 'Workers Paid' }).click();
|
||||
|
||||
await expect(page.getByText('Workers plan saved')).toBeVisible();
|
||||
await expect(storagePanel).toContainText('10.0 GB');
|
||||
|
||||
await page.reload();
|
||||
|
||||
await expect(planSelect).toContainText('Workers Paid');
|
||||
await expect(storagePanel).toContainText('10.0 GB');
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cloudflare_temp_email",
|
||||
"version": "1.11.0",
|
||||
"version": "1.11.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
export const deMessages = {
|
||||
"views.admin.DatabaseManager.current_database_size": "Aktuelle Datenbankgröße",
|
||||
"views.admin.DatabaseManager.free_plan": "Free",
|
||||
"views.admin.DatabaseManager.paid_plan": "Workers Paid",
|
||||
"views.admin.DatabaseManager.plan": "Workers-Tarif",
|
||||
"views.admin.DatabaseManager.plan_placeholder": "Cloudflare-Workers-Tarif auswählen",
|
||||
"views.admin.DatabaseManager.planSaved": "Workers-Tarif gespeichert",
|
||||
"views.admin.DatabaseManager.single_database_limit": "Datenbank-Kapazitätslimit",
|
||||
"views.admin.DatabaseManager.storage_description": "Vergleiche die aktuelle Datenbankgröße mit den Tariflimits.",
|
||||
"views.admin.DatabaseManager.storage_tip": "Die Auslastung wird anhand der aktuellen Datenbankgröße und des gewählten Tariflimits berechnet.",
|
||||
"views.admin.DatabaseManager.storage_title": "D1-Speicherkapazität",
|
||||
"views.admin.DatabaseManager.storage_usage": "Kapazitätsauslastung",
|
||||
"views.admin.DatabaseManager.unavailable": "Nicht verfügbar",
|
||||
"views.index.SimpleIndex.mailCount": "{current} / {total} E-Mails",
|
||||
"views.admin.Statistics.activeAddressCount30days": "Aktive Adressanzahl in 30 Tagen",
|
||||
"views.admin.Statistics.activeAddressCount7days": "Aktive Adressanzahl in 7 Tagen",
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
export const esMessages = {
|
||||
"views.admin.DatabaseManager.current_database_size": "Tamaño actual de la base de datos",
|
||||
"views.admin.DatabaseManager.free_plan": "Free",
|
||||
"views.admin.DatabaseManager.paid_plan": "Workers Paid",
|
||||
"views.admin.DatabaseManager.plan": "Plan de Workers",
|
||||
"views.admin.DatabaseManager.plan_placeholder": "Selecciona tu plan de Cloudflare Workers",
|
||||
"views.admin.DatabaseManager.planSaved": "Plan de Workers guardado",
|
||||
"views.admin.DatabaseManager.single_database_limit": "Límite de capacidad de la base de datos",
|
||||
"views.admin.DatabaseManager.storage_description": "Compara el tamaño actual de la base de datos con los límites de tu plan.",
|
||||
"views.admin.DatabaseManager.storage_tip": "El uso se calcula con el tamaño actual de la base de datos y el límite del plan seleccionado.",
|
||||
"views.admin.DatabaseManager.storage_title": "Capacidad de almacenamiento D1",
|
||||
"views.admin.DatabaseManager.storage_usage": "Uso de capacidad",
|
||||
"views.admin.DatabaseManager.unavailable": "No disponible",
|
||||
"views.index.SimpleIndex.mailCount": "{current} / {total} correos",
|
||||
"views.admin.Statistics.activeAddressCount30days": "Cantidad de direcciones activas en 30 días",
|
||||
"views.admin.Statistics.activeAddressCount7days": "Cantidad de direcciones activas en 7 días",
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
export const jaMessages = {
|
||||
"views.admin.DatabaseManager.current_database_size": "現在のデータベースサイズ",
|
||||
"views.admin.DatabaseManager.free_plan": "Free",
|
||||
"views.admin.DatabaseManager.paid_plan": "Workers Paid",
|
||||
"views.admin.DatabaseManager.plan": "Workers プラン",
|
||||
"views.admin.DatabaseManager.plan_placeholder": "Cloudflare Workers プランを選択",
|
||||
"views.admin.DatabaseManager.planSaved": "Workers プランを保存しました",
|
||||
"views.admin.DatabaseManager.single_database_limit": "データベース容量上限",
|
||||
"views.admin.DatabaseManager.storage_description": "現在のデータベースサイズとプラン上限を比較します。",
|
||||
"views.admin.DatabaseManager.storage_tip": "使用率は現在のデータベースサイズと選択したプラン上限から計算されます。",
|
||||
"views.admin.DatabaseManager.storage_title": "D1 ストレージ容量",
|
||||
"views.admin.DatabaseManager.storage_usage": "容量使用率",
|
||||
"views.admin.DatabaseManager.unavailable": "利用不可",
|
||||
"views.index.SimpleIndex.mailCount": "{current} / {total} 件のメール",
|
||||
"views.admin.Statistics.activeAddressCount30days": "30日間のアクティブなアドレス数",
|
||||
"views.admin.Statistics.activeAddressCount7days": "7日間のアクティブなアドレス数",
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
export const ptBRMessages = {
|
||||
"views.admin.DatabaseManager.current_database_size": "Tamanho atual do banco de dados",
|
||||
"views.admin.DatabaseManager.free_plan": "Free",
|
||||
"views.admin.DatabaseManager.paid_plan": "Workers Paid",
|
||||
"views.admin.DatabaseManager.plan": "Plano do Workers",
|
||||
"views.admin.DatabaseManager.plan_placeholder": "Selecione seu plano do Cloudflare Workers",
|
||||
"views.admin.DatabaseManager.planSaved": "Plano do Workers salvo",
|
||||
"views.admin.DatabaseManager.single_database_limit": "Limite de capacidade do banco de dados",
|
||||
"views.admin.DatabaseManager.storage_description": "Compare o tamanho atual do banco de dados com os limites do seu plano.",
|
||||
"views.admin.DatabaseManager.storage_tip": "O uso é calculado com o tamanho atual do banco de dados e o limite do plano selecionado.",
|
||||
"views.admin.DatabaseManager.storage_title": "Capacidade de armazenamento D1",
|
||||
"views.admin.DatabaseManager.storage_usage": "Uso da capacidade",
|
||||
"views.admin.DatabaseManager.unavailable": "Indisponível",
|
||||
"views.index.SimpleIndex.mailCount": "{current} / {total} e-mails",
|
||||
"views.admin.Statistics.activeAddressCount30days": "Quantidade de endereços ativos em 30 dias",
|
||||
"views.admin.Statistics.activeAddressCount7days": "Quantidade de endereços ativos em 7 dias",
|
||||
|
||||
@@ -1694,6 +1694,14 @@ export const MESSAGE_REGISTRY = {
|
||||
"en": "Current DB Version",
|
||||
"zh": "当前数据库版本"
|
||||
},
|
||||
"current_database_size": {
|
||||
"en": "Current Database Size",
|
||||
"zh": "当前数据库大小"
|
||||
},
|
||||
"free_plan": {
|
||||
"en": "Free",
|
||||
"zh": "Free"
|
||||
},
|
||||
"init": {
|
||||
"en": "Initialize Database",
|
||||
"zh": "初始化数据库"
|
||||
@@ -1717,6 +1725,46 @@ export const MESSAGE_REGISTRY = {
|
||||
"need_migration_tip": {
|
||||
"en": "Database migration is required. Please migrate the database.",
|
||||
"zh": "需要迁移数据库,请迁移数据库"
|
||||
},
|
||||
"paid_plan": {
|
||||
"en": "Workers Paid",
|
||||
"zh": "Workers Paid"
|
||||
},
|
||||
"plan": {
|
||||
"en": "Workers Plan",
|
||||
"zh": "Workers 套餐"
|
||||
},
|
||||
"plan_placeholder": {
|
||||
"en": "Select your Cloudflare Workers plan",
|
||||
"zh": "请选择 Cloudflare Workers 套餐"
|
||||
},
|
||||
"planSaved": {
|
||||
"en": "Workers plan saved",
|
||||
"zh": "Workers 套餐已保存"
|
||||
},
|
||||
"single_database_limit": {
|
||||
"en": "Database Capacity Limit",
|
||||
"zh": "数据库容量上限"
|
||||
},
|
||||
"storage_description": {
|
||||
"en": "Compare the current database size with your plan limits.",
|
||||
"zh": "将当前数据库大小与套餐容量上限进行对比"
|
||||
},
|
||||
"storage_tip": {
|
||||
"en": "Usage is calculated from the current database size and the selected plan limit.",
|
||||
"zh": "使用率按当前数据库大小与所选套餐的数据库容量上限计算。"
|
||||
},
|
||||
"storage_title": {
|
||||
"en": "D1 Storage Capacity",
|
||||
"zh": "D1 存储容量"
|
||||
},
|
||||
"storage_usage": {
|
||||
"en": "Capacity Usage",
|
||||
"zh": "容量使用率"
|
||||
},
|
||||
"unavailable": {
|
||||
"en": "Unavailable",
|
||||
"zh": "暂不可用"
|
||||
}
|
||||
},
|
||||
"views.admin.IpBlacklistSettings": {
|
||||
|
||||
@@ -120,7 +120,7 @@ onMounted(async () => {
|
||||
</n-modal>
|
||||
<n-tabs v-if="showAdminPage" type="card" v-model:value="adminTab" :placement="globalTabplacement">
|
||||
<n-tab-pane name="qucickSetup" :tab="t('qucickSetup')">
|
||||
<n-tabs type="bar" justify-content="center" animated>
|
||||
<n-tabs key="quick-setup-tabs" type="bar" justify-content="center" animated>
|
||||
<n-tab-pane name="database" :tab="t('database')">
|
||||
<DatabaseManager />
|
||||
</n-tab-pane>
|
||||
@@ -136,7 +136,7 @@ onMounted(async () => {
|
||||
</n-tabs>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="account" :tab="t('account')">
|
||||
<n-tabs type="bar" justify-content="center" animated>
|
||||
<n-tabs key="account-tabs" type="bar" justify-content="center" animated>
|
||||
<n-tab-pane name="account" :tab="t('account')">
|
||||
<Account />
|
||||
</n-tab-pane>
|
||||
@@ -161,7 +161,7 @@ onMounted(async () => {
|
||||
</n-tabs>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="user" :tab="t('user')">
|
||||
<n-tabs type="bar" justify-content="center" animated>
|
||||
<n-tabs key="user-tabs" type="bar" justify-content="center" animated>
|
||||
<n-tab-pane name="user_management" :tab="t('user_management')">
|
||||
<UserManagement />
|
||||
</n-tab-pane>
|
||||
@@ -177,7 +177,7 @@ onMounted(async () => {
|
||||
</n-tabs>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="mails" :tab="t('mails')">
|
||||
<n-tabs type="bar" justify-content="center" animated>
|
||||
<n-tabs key="mails-tabs" type="bar" justify-content="center" animated>
|
||||
<n-tab-pane name="mails" :tab="t('mails')">
|
||||
<Mails />
|
||||
</n-tab-pane>
|
||||
@@ -202,7 +202,7 @@ onMounted(async () => {
|
||||
<Statistics />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="maintenance" :tab="t('maintenance')">
|
||||
<n-tabs type="bar" justify-content="center" animated>
|
||||
<n-tabs key="maintenance-tabs" type="bar" justify-content="center" animated>
|
||||
<n-tab-pane name="database" :tab="t('database')">
|
||||
<DatabaseManager />
|
||||
</n-tab-pane>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { computed, ref, onMounted } from 'vue';
|
||||
import { useScopedI18n } from '@/i18n/app'
|
||||
import { CleaningServicesFilled } from '@vicons/material'
|
||||
|
||||
@@ -7,24 +7,95 @@ import { api } from '../../api'
|
||||
import { init } from 'vooks/lib/on-fonts-ready';
|
||||
|
||||
const message = useMessage()
|
||||
const D1_STORAGE_PLAN_CONFIG_KEY = 'd1_storage_plan'
|
||||
const dbVersionData = ref({
|
||||
need_initialization: false,
|
||||
need_migration: false,
|
||||
current_db_version: '',
|
||||
code_db_version: ''
|
||||
code_db_version: '',
|
||||
database_size: null
|
||||
})
|
||||
const selectedPlan = ref(null)
|
||||
const savedPlan = ref(null)
|
||||
const savingPlan = ref(false)
|
||||
|
||||
const planOptions = computed(() => [
|
||||
{
|
||||
label: t('free_plan'),
|
||||
value: 'free',
|
||||
databaseLimit: 500 * 1024 ** 2
|
||||
},
|
||||
{
|
||||
label: t('paid_plan'),
|
||||
value: 'paid',
|
||||
databaseLimit: 10 * 1024 ** 3
|
||||
}
|
||||
])
|
||||
|
||||
const selectedPlanDetails = computed(() => (
|
||||
planOptions.value.find((plan) => plan.value === selectedPlan.value)
|
||||
))
|
||||
|
||||
const storagePercentage = computed(() => {
|
||||
if (!selectedPlanDetails.value || dbVersionData.value.database_size === null) return 0
|
||||
return dbVersionData.value.database_size / selectedPlanDetails.value.databaseLimit * 100
|
||||
})
|
||||
|
||||
const progressPercentage = computed(() => Math.min(storagePercentage.value, 100))
|
||||
|
||||
const progressStatus = computed(() => {
|
||||
if (storagePercentage.value >= 90) return 'error'
|
||||
if (storagePercentage.value >= 75) return 'warning'
|
||||
return 'success'
|
||||
})
|
||||
|
||||
const { t } = useScopedI18n('views.admin.DatabaseManager')
|
||||
|
||||
const formatBytes = (bytes) => {
|
||||
if (bytes === null || bytes === undefined) return t('unavailable')
|
||||
if (bytes === 0) return '0 B'
|
||||
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
const unitIndex = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1)
|
||||
const value = bytes / 1024 ** unitIndex
|
||||
return `${value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2)} ${units[unitIndex]}`
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res = await api.fetch('/admin/db_version');
|
||||
if (res) Object.assign(dbVersionData.value, res);
|
||||
const [versionRes, configRes] = await Promise.all([
|
||||
api.fetch('/admin/db_version'),
|
||||
api.fetch(`/admin/config/${D1_STORAGE_PLAN_CONFIG_KEY}`)
|
||||
]);
|
||||
if (versionRes) Object.assign(dbVersionData.value, versionRes);
|
||||
|
||||
const configuredPlan = configRes?.value
|
||||
if (planOptions.value.some((plan) => plan.value === configuredPlan)) {
|
||||
selectedPlan.value = configuredPlan
|
||||
savedPlan.value = configuredPlan
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const savePlan = async (plan) => {
|
||||
savingPlan.value = true
|
||||
try {
|
||||
await api.fetch('/admin/config', {
|
||||
method: 'POST',
|
||||
body: { key: D1_STORAGE_PLAN_CONFIG_KEY, value: plan }
|
||||
})
|
||||
savedPlan.value = plan
|
||||
message.success(t('planSaved'))
|
||||
} catch (error) {
|
||||
selectedPlan.value = savedPlan.value
|
||||
message.error(error.message || "error")
|
||||
} finally {
|
||||
savingPlan.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const initialization = async () => {
|
||||
try {
|
||||
await api.fetch('/admin/db_initialize', {
|
||||
@@ -77,6 +148,58 @@ onMounted(async () => {
|
||||
</span>
|
||||
</n-alert>
|
||||
|
||||
<div class="storage-panel">
|
||||
<div class="storage-heading">
|
||||
<div>
|
||||
<h3>{{ t('storage_title') }}</h3>
|
||||
<p>{{ t('storage_description') }}</p>
|
||||
</div>
|
||||
<div class="plan-select">
|
||||
<span>{{ t('plan') }}</span>
|
||||
<n-select
|
||||
v-model:value="selectedPlan"
|
||||
:options="planOptions"
|
||||
:placeholder="t('plan_placeholder')"
|
||||
:disabled="dbVersionData.need_initialization"
|
||||
:loading="savingPlan"
|
||||
@update:value="savePlan"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<n-grid cols="1 s:2" responsive="screen" :x-gap="12" :y-gap="12">
|
||||
<n-grid-item>
|
||||
<div class="storage-stat">
|
||||
<span>{{ t('current_database_size') }}</span>
|
||||
<strong>{{ formatBytes(dbVersionData.database_size) }}</strong>
|
||||
</div>
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<div class="storage-stat">
|
||||
<span>{{ t('single_database_limit') }}</span>
|
||||
<strong>{{ selectedPlanDetails ? formatBytes(selectedPlanDetails.databaseLimit) : '—' }}</strong>
|
||||
</div>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
|
||||
<div v-if="selectedPlanDetails" class="storage-progress">
|
||||
<div class="storage-progress-label">
|
||||
<span>{{ t('storage_usage') }}</span>
|
||||
<span>{{ storagePercentage.toFixed(2) }}%</span>
|
||||
</div>
|
||||
<n-progress
|
||||
type="line"
|
||||
:percentage="progressPercentage"
|
||||
:status="progressStatus"
|
||||
:show-indicator="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<n-alert class="storage-tip" type="default" :show-icon="false" :bordered="false">
|
||||
{{ t('storage_tip') }}
|
||||
</n-alert>
|
||||
</div>
|
||||
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -100,4 +223,88 @@ onMounted(async () => {
|
||||
.n-button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.storage-panel {
|
||||
margin-top: 18px;
|
||||
padding-top: 18px;
|
||||
border-top: 1px solid var(--n-border-color);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.storage-heading {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.storage-heading h3,
|
||||
.storage-heading p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.storage-heading p {
|
||||
margin-top: 4px;
|
||||
color: var(--n-text-color-3);
|
||||
}
|
||||
|
||||
.plan-select {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.plan-select > span {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
color: var(--n-text-color-2);
|
||||
}
|
||||
|
||||
.storage-stat {
|
||||
display: flex;
|
||||
min-height: 72px;
|
||||
padding: 14px;
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
border: 1px solid var(--n-border-color);
|
||||
border-radius: var(--n-border-radius);
|
||||
}
|
||||
|
||||
.storage-stat span {
|
||||
color: var(--n-text-color-3);
|
||||
}
|
||||
|
||||
.storage-stat strong {
|
||||
margin-top: 8px;
|
||||
font-size: 18px;
|
||||
color: var(--n-text-color);
|
||||
}
|
||||
|
||||
.storage-progress {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.storage-progress-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
color: var(--n-text-color-2);
|
||||
}
|
||||
|
||||
.storage-tip {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.storage-heading {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.plan-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "temp-email-pages",
|
||||
"version": "1.11.0",
|
||||
"version": "1.11.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "temp-mail-docs",
|
||||
"private": true,
|
||||
"version": "1.11.0",
|
||||
"version": "1.11.1",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@types/node": "^26.2.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cloudflare_temp_email",
|
||||
"version": "1.11.0",
|
||||
"version": "1.11.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
31
worker/src/admin_api/config_api.ts
Normal file
31
worker/src/admin_api/config_api.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Context } from "hono";
|
||||
import { getSetting, saveSetting } from "../utils";
|
||||
|
||||
const CONFIG_KEY_PREFIX = "admin-config:";
|
||||
const CONFIG_KEY_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
|
||||
|
||||
const getStorageKey = (key: string) => `${CONFIG_KEY_PREFIX}${key}`;
|
||||
|
||||
export default {
|
||||
get: async (c: Context<HonoCustomType>) => {
|
||||
const key = c.req.param("key");
|
||||
if (!CONFIG_KEY_PATTERN.test(key)) {
|
||||
return c.text("Invalid config key", 400);
|
||||
}
|
||||
|
||||
const value = await getSetting(c, getStorageKey(key));
|
||||
return c.json({ key, value });
|
||||
},
|
||||
save: async (c: Context<HonoCustomType>) => {
|
||||
const { key, value } = await c.req.json<{ key?: unknown, value?: unknown }>();
|
||||
if (typeof key !== "string" || !CONFIG_KEY_PATTERN.test(key)) {
|
||||
return c.text("Invalid config key", 400);
|
||||
}
|
||||
if (typeof value !== "string") {
|
||||
return c.text("Config value must be a string", 400);
|
||||
}
|
||||
|
||||
await saveSetting(c, getStorageKey(key), value);
|
||||
return c.json({ success: true, key, value });
|
||||
},
|
||||
}
|
||||
@@ -219,11 +219,13 @@ export default {
|
||||
},
|
||||
getVersion: async (c: Context<HonoCustomType>) => {
|
||||
const version = await utils.getSetting(c, CONSTANTS.DB_VERSION_KEY);
|
||||
const sizeResult = await c.env.DB.prepare("SELECT 1").run();
|
||||
return c.json({
|
||||
need_initialization: !version,
|
||||
need_migration: version && version != CONSTANTS.DB_VERSION,
|
||||
current_db_version: version,
|
||||
code_db_version: CONSTANTS.DB_VERSION
|
||||
code_db_version: CONSTANTS.DB_VERSION,
|
||||
database_size: sizeResult.meta.size_after ?? null
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import db_api from './db_api'
|
||||
import ip_blacklist_settings from './ip_blacklist_settings'
|
||||
import ai_extract_settings from './ai_extract_settings'
|
||||
import e2e_test_api from './e2e_test_api'
|
||||
import config_api from './config_api'
|
||||
|
||||
export const api = new Hono<HonoCustomType>()
|
||||
|
||||
@@ -96,6 +97,10 @@ api.get('admin/db_version', db_api.getVersion)
|
||||
api.post('admin/db_initialize', db_api.initialize)
|
||||
api.post('admin/db_migration', db_api.migrate)
|
||||
|
||||
// generic admin config
|
||||
api.get('/admin/config/:key', config_api.get)
|
||||
api.post('/admin/config', config_api.save)
|
||||
|
||||
// IP blacklist settings
|
||||
api.get('/admin/ip_blacklist/settings', ip_blacklist_settings.getIpBlacklistSettings)
|
||||
api.post('/admin/ip_blacklist/settings', ip_blacklist_settings.saveIpBlacklistSettings)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const CONSTANTS = {
|
||||
VERSION: 'v' + '1.11.0',
|
||||
VERSION: 'v' + '1.11.1',
|
||||
|
||||
// DB Version
|
||||
DB_VERSION_KEY: 'db_version',
|
||||
|
||||
Reference in New Issue
Block a user