mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-05-06 20:32:55 +08:00
feat: improve address credential connections
This commit is contained in:
@@ -10,8 +10,13 @@
|
|||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
|
- feat: |Frontend| 将邮箱地址凭证弹窗升级为“地址凭证与连接方式”,复用普通用户与 admin 创建邮箱结果弹窗;支持通过 `ENABLE_AGENT_EMAIL_INFO` 展示 AI Agent 接入信息,并通过 `SMTP_IMAP_PROXY_CONFIG` 展示 SMTP/IMAP 客户端连接信息
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
|
- fix: |Admin| 管理员重置邮箱地址密码时改为前端 SHA-256 后提交,后端只接受并存储哈希值,避免该接口继续接收明文密码
|
||||||
|
- fix: |Address| 管理员邮箱地址列表与用户绑定地址列表不再返回已存储的地址密码哈希值,避免列表接口暴露敏感字段
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
|
|
||||||
## v1.8.0
|
## v1.8.0
|
||||||
|
|||||||
@@ -10,8 +10,13 @@
|
|||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
|
- feat: |Frontend| Upgrade the address credential dialog to "Address Credentials & Connection Methods" and reuse it for both normal users and admin-created addresses; support showing AI Agent access via `ENABLE_AGENT_EMAIL_INFO` and SMTP/IMAP client settings via `SMTP_IMAP_PROXY_CONFIG`
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
|
- fix: |Admin| Hash address passwords in the frontend before admin reset requests, and make the backend accept and store only the hash instead of plaintext
|
||||||
|
- fix: |Address| Stop returning stored address password hashes from the admin address list and user bound-address list APIs to avoid exposing sensitive fields
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
|
|
||||||
## v1.8.0
|
## v1.8.0
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { APIRequestContext } from '@playwright/test';
|
import type { APIRequestContext } from '@playwright/test';
|
||||||
|
import { createHash } from 'crypto';
|
||||||
import WebSocket from 'ws';
|
import WebSocket from 'ws';
|
||||||
|
|
||||||
export const WORKER_URL = process.env.WORKER_URL!;
|
export const WORKER_URL = process.env.WORKER_URL!;
|
||||||
@@ -10,6 +11,13 @@ export const FRONTEND_URL = process.env.FRONTEND_URL!;
|
|||||||
export const MAILPIT_API = process.env.MAILPIT_API!;
|
export const MAILPIT_API = process.env.MAILPIT_API!;
|
||||||
export const TEST_DOMAIN = 'test.example.com';
|
export const TEST_DOMAIN = 'test.example.com';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SHA-256 hash matching the frontend hashPassword utility.
|
||||||
|
*/
|
||||||
|
export function hashPassword(password: string): string {
|
||||||
|
return createHash('sha256').update(password).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new email address via the worker API.
|
* Create a new email address via the worker API.
|
||||||
* Appends a timestamp suffix to avoid UNIQUE constraint collisions
|
* Appends a timestamp suffix to avoid UNIQUE constraint collisions
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
import { WORKER_URL, createTestAddress, deleteAddress } from '../../fixtures/test-helpers';
|
import { WORKER_URL, createTestAddress, deleteAddress, hashPassword } from '../../fixtures/test-helpers';
|
||||||
|
|
||||||
test.describe('Address Password Login', () => {
|
test.describe('Address Password Login', () => {
|
||||||
test('set password then login with it', async ({ request }) => {
|
test('set password then login with it', async ({ request }) => {
|
||||||
const { jwt, address } = await createTestAddress(request, 'pwd-login');
|
const { jwt, address } = await createTestAddress(request, 'pwd-login');
|
||||||
|
const passwordHash = hashPassword('test-password-123');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Set a password on the address
|
// Set a password on the address
|
||||||
const changePwdRes = await request.post(`${WORKER_URL}/api/address_change_password`, {
|
const changePwdRes = await request.post(`${WORKER_URL}/api/address_change_password`, {
|
||||||
headers: { Authorization: `Bearer ${jwt}` },
|
headers: { Authorization: `Bearer ${jwt}` },
|
||||||
data: { new_password: 'test-password-123' },
|
data: { new_password: passwordHash },
|
||||||
});
|
});
|
||||||
expect(changePwdRes.ok()).toBe(true);
|
expect(changePwdRes.ok()).toBe(true);
|
||||||
const changePwdBody = await changePwdRes.json();
|
const changePwdBody = await changePwdRes.json();
|
||||||
@@ -17,7 +18,7 @@ test.describe('Address Password Login', () => {
|
|||||||
|
|
||||||
// Login with the correct password
|
// Login with the correct password
|
||||||
const loginRes = await request.post(`${WORKER_URL}/api/address_login`, {
|
const loginRes = await request.post(`${WORKER_URL}/api/address_login`, {
|
||||||
data: { email: address, password: 'test-password-123' },
|
data: { email: address, password: passwordHash },
|
||||||
});
|
});
|
||||||
expect(loginRes.ok()).toBe(true);
|
expect(loginRes.ok()).toBe(true);
|
||||||
const loginBody = await loginRes.json();
|
const loginBody = await loginRes.json();
|
||||||
@@ -36,12 +37,13 @@ test.describe('Address Password Login', () => {
|
|||||||
|
|
||||||
test('login with wrong password returns 401', async ({ request }) => {
|
test('login with wrong password returns 401', async ({ request }) => {
|
||||||
const { jwt, address } = await createTestAddress(request, 'pwd-wrong');
|
const { jwt, address } = await createTestAddress(request, 'pwd-wrong');
|
||||||
|
const passwordHash = hashPassword('correct-password');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Set a password
|
// Set a password
|
||||||
const changePwdRes = await request.post(`${WORKER_URL}/api/address_change_password`, {
|
const changePwdRes = await request.post(`${WORKER_URL}/api/address_change_password`, {
|
||||||
headers: { Authorization: `Bearer ${jwt}` },
|
headers: { Authorization: `Bearer ${jwt}` },
|
||||||
data: { new_password: 'correct-password' },
|
data: { new_password: passwordHash },
|
||||||
});
|
});
|
||||||
expect(changePwdRes.ok()).toBe(true);
|
expect(changePwdRes.ok()).toBe(true);
|
||||||
const changePwdBody = await changePwdRes.json();
|
const changePwdBody = await changePwdRes.json();
|
||||||
@@ -49,11 +51,117 @@ test.describe('Address Password Login', () => {
|
|||||||
|
|
||||||
// Login with wrong password
|
// Login with wrong password
|
||||||
const loginRes = await request.post(`${WORKER_URL}/api/address_login`, {
|
const loginRes = await request.post(`${WORKER_URL}/api/address_login`, {
|
||||||
data: { email: address, password: 'wrong-password' },
|
data: { email: address, password: hashPassword('wrong-password') },
|
||||||
});
|
});
|
||||||
expect(loginRes.status()).toBe(401);
|
expect(loginRes.status()).toBe(401);
|
||||||
} finally {
|
} finally {
|
||||||
await deleteAddress(request, jwt);
|
await deleteAddress(request, jwt);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('admin reset stores frontend-hashed address password', async ({ request }) => {
|
||||||
|
const { jwt, address, address_id } = await createTestAddress(request, 'pwd-admin-reset');
|
||||||
|
const plainPassword = `admin-reset-${Date.now()}`;
|
||||||
|
const passwordHash = hashPassword(plainPassword);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resetRes = await request.post(`${WORKER_URL}/admin/address/${address_id}/reset_password`, {
|
||||||
|
data: { password: passwordHash },
|
||||||
|
});
|
||||||
|
expect(resetRes.ok()).toBe(true);
|
||||||
|
await expect(resetRes.json()).resolves.toMatchObject({ success: true });
|
||||||
|
|
||||||
|
const plaintextLoginRes = await request.post(`${WORKER_URL}/api/address_login`, {
|
||||||
|
data: { email: address, password: plainPassword },
|
||||||
|
});
|
||||||
|
expect(plaintextLoginRes.status()).toBe(401);
|
||||||
|
|
||||||
|
const loginRes = await request.post(`${WORKER_URL}/api/address_login`, {
|
||||||
|
data: { email: address, password: passwordHash },
|
||||||
|
});
|
||||||
|
expect(loginRes.ok()).toBe(true);
|
||||||
|
const loginBody = await loginRes.json();
|
||||||
|
expect(loginBody.jwt).toBeTruthy();
|
||||||
|
expect(loginBody.address).toBe(address);
|
||||||
|
} finally {
|
||||||
|
await deleteAddress(request, jwt);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('admin address list does not expose stored password hash', async ({ request }) => {
|
||||||
|
const { jwt, address } = await createTestAddress(request, 'pwd-list-hidden');
|
||||||
|
const passwordHash = hashPassword('list-hidden-password');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const changePwdRes = await request.post(`${WORKER_URL}/api/address_change_password`, {
|
||||||
|
headers: { Authorization: `Bearer ${jwt}` },
|
||||||
|
data: { new_password: passwordHash },
|
||||||
|
});
|
||||||
|
expect(changePwdRes.ok()).toBe(true);
|
||||||
|
|
||||||
|
const listRes = await request.get(
|
||||||
|
`${WORKER_URL}/admin/address?limit=10&offset=0&query=${encodeURIComponent(address)}`
|
||||||
|
);
|
||||||
|
expect(listRes.ok()).toBe(true);
|
||||||
|
const listBody = await listRes.json();
|
||||||
|
const listedAddress = listBody.results.find((row: { name: string }) => row.name === address);
|
||||||
|
expect(listedAddress).toBeTruthy();
|
||||||
|
expect(listedAddress).not.toHaveProperty('password');
|
||||||
|
} finally {
|
||||||
|
await deleteAddress(request, jwt);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('user bind address list does not expose stored password hash', async ({ request }) => {
|
||||||
|
const userEmail = `pwd-bind-hidden-${Date.now()}@test.example.com`;
|
||||||
|
const userPasswordHash = hashPassword('bind-hidden-user-password');
|
||||||
|
const { jwt, address } = await createTestAddress(request, 'pwd-bind-hidden');
|
||||||
|
const addressPasswordHash = hashPassword('bind-hidden-address-password');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const enableRes = await request.post(`${WORKER_URL}/admin/user_settings`, {
|
||||||
|
data: {
|
||||||
|
enable: true,
|
||||||
|
enableMailVerify: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(enableRes.ok()).toBe(true);
|
||||||
|
|
||||||
|
const registerRes = await request.post(`${WORKER_URL}/user_api/register`, {
|
||||||
|
data: { email: userEmail, password: userPasswordHash },
|
||||||
|
});
|
||||||
|
expect(registerRes.ok()).toBe(true);
|
||||||
|
|
||||||
|
const loginRes = await request.post(`${WORKER_URL}/user_api/login`, {
|
||||||
|
data: { email: userEmail, password: userPasswordHash },
|
||||||
|
});
|
||||||
|
expect(loginRes.ok()).toBe(true);
|
||||||
|
const { jwt: userJwt } = await loginRes.json();
|
||||||
|
|
||||||
|
const changePwdRes = await request.post(`${WORKER_URL}/api/address_change_password`, {
|
||||||
|
headers: { Authorization: `Bearer ${jwt}` },
|
||||||
|
data: { new_password: addressPasswordHash },
|
||||||
|
});
|
||||||
|
expect(changePwdRes.ok()).toBe(true);
|
||||||
|
|
||||||
|
const bindRes = await request.post(`${WORKER_URL}/user_api/bind_address`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${jwt}`,
|
||||||
|
'x-user-token': userJwt,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(bindRes.ok()).toBe(true);
|
||||||
|
|
||||||
|
const listRes = await request.get(`${WORKER_URL}/user_api/bind_address`, {
|
||||||
|
headers: { 'x-user-token': userJwt },
|
||||||
|
});
|
||||||
|
expect(listRes.ok()).toBe(true);
|
||||||
|
const listBody = await listRes.json();
|
||||||
|
const listedAddress = listBody.results.find((row: { name: string }) => row.name === address);
|
||||||
|
expect(listedAddress).toBeTruthy();
|
||||||
|
expect(listedAddress).not.toHaveProperty('password');
|
||||||
|
} finally {
|
||||||
|
await deleteAddress(request, jwt);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
import { WORKER_URL, createTestAddress, deleteAddress } from '../../fixtures/test-helpers';
|
import { WORKER_URL, createTestAddress, deleteAddress, hashPassword } from '../../fixtures/test-helpers';
|
||||||
import * as crypto from 'crypto';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SHA-256 hash matching frontend hashPassword utility.
|
|
||||||
*/
|
|
||||||
function hashPassword(password: string): string {
|
|
||||||
return crypto.createHash('sha256').update(password).digest('hex');
|
|
||||||
}
|
|
||||||
|
|
||||||
test.describe('Turnstile Login Endpoints (ENABLE_GLOBAL_TURNSTILE_CHECK disabled)', () => {
|
test.describe('Turnstile Login Endpoints (ENABLE_GLOBAL_TURNSTILE_CHECK disabled)', () => {
|
||||||
|
|
||||||
@@ -110,14 +102,14 @@ test.describe('Turnstile Login Endpoints (ENABLE_GLOBAL_TURNSTILE_CHECK disabled
|
|||||||
// Set a password
|
// Set a password
|
||||||
await request.post(`${WORKER_URL}/api/address_change_password`, {
|
await request.post(`${WORKER_URL}/api/address_change_password`, {
|
||||||
headers: { Authorization: `Bearer ${jwt}` },
|
headers: { Authorization: `Bearer ${jwt}` },
|
||||||
data: { new_password: 'addr-pass-123' },
|
data: { new_password: hashPassword('addr-pass-123') },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Login with cf_token field present but empty
|
// Login with cf_token field present but empty
|
||||||
const loginRes = await request.post(`${WORKER_URL}/api/address_login`, {
|
const loginRes = await request.post(`${WORKER_URL}/api/address_login`, {
|
||||||
data: {
|
data: {
|
||||||
email: address,
|
email: address,
|
||||||
password: 'addr-pass-123',
|
password: hashPassword('addr-pass-123'),
|
||||||
cf_token: ''
|
cf_token: ''
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -105,6 +105,8 @@ const getOpenSettings = async (message, notification) => {
|
|||||||
enableWebhook: res["enableWebhook"] || false,
|
enableWebhook: res["enableWebhook"] || false,
|
||||||
isS3Enabled: res["isS3Enabled"] || false,
|
isS3Enabled: res["isS3Enabled"] || false,
|
||||||
enableAddressPassword: res["enableAddressPassword"] || false,
|
enableAddressPassword: res["enableAddressPassword"] || false,
|
||||||
|
enableAgentEmailInfo: res["enableAgentEmailInfo"] || false,
|
||||||
|
smtpImapProxyConfig: res["smtpImapProxyConfig"] || openSettings.value.smtpImapProxyConfig,
|
||||||
statusUrl: res["statusUrl"] || "",
|
statusUrl: res["statusUrl"] || "",
|
||||||
enableGlobalTurnstileCheck: res["enableGlobalTurnstileCheck"] || false,
|
enableGlobalTurnstileCheck: res["enableGlobalTurnstileCheck"] || false,
|
||||||
});
|
});
|
||||||
|
|||||||
322
frontend/src/components/AddressCredentialModal.vue
Normal file
322
frontend/src/components/AddressCredentialModal.vue
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useScopedI18n } from '@/i18n/app'
|
||||||
|
|
||||||
|
import { useGlobalState } from '../store'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
address: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
jwt: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
addressPassword: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:show'])
|
||||||
|
|
||||||
|
const { openSettings, auth } = useGlobalState()
|
||||||
|
const { locale, t } = useScopedI18n('components.AddressCredentialModal')
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const modalShow = computed({
|
||||||
|
get: () => props.show,
|
||||||
|
set: (value) => emit('update:show', value),
|
||||||
|
})
|
||||||
|
|
||||||
|
const configuredApiBaseUrl = import.meta.env.VITE_API_BASE || ''
|
||||||
|
const frontendBaseUrl = computed(() => window.location.origin)
|
||||||
|
const apiBaseUrl = computed(() => (configuredApiBaseUrl || frontendBaseUrl.value).replace(/\/$/, ''))
|
||||||
|
const docLocale = computed(() => locale.value === 'zh' ? 'zh' : 'en')
|
||||||
|
const agentDocUrl = computed(() => `https://temp-mail-docs.awsl.uk/${docLocale.value}/guide/feature/agent-email.html`)
|
||||||
|
const smtpImapDocUrl = computed(() => `https://temp-mail-docs.awsl.uk/${docLocale.value}/guide/feature/config-smtp-proxy.html`)
|
||||||
|
const agentSkillUrl = 'https://github.com/dreamhunter2333/cloudflare_temp_email/blob/main/skills/cf-temp-mail-agent-mail/SKILL.md'
|
||||||
|
const autoLoginUrl = computed(() => `${frontendBaseUrl.value}/?jwt=${encodeURIComponent(props.jwt)}`)
|
||||||
|
const showAgent = computed(() => !!openSettings.value.enableAgentEmailInfo)
|
||||||
|
const smtpImapConfig = computed(() => openSettings.value.smtpImapProxyConfig || {})
|
||||||
|
const smtpConfig = computed(() => smtpImapConfig.value.smtp || {})
|
||||||
|
const imapConfig = computed(() => smtpImapConfig.value.imap || {})
|
||||||
|
const showSmtpImap = computed(() => !!smtpConfig.value.host || !!imapConfig.value.host)
|
||||||
|
const securityLabel = computed(() =>
|
||||||
|
smtpConfig.value.starttls || imapConfig.value.starttls ? t('starttls') : t('plainOrProxyTls')
|
||||||
|
)
|
||||||
|
const agentConfigJson = computed(() => JSON.stringify({
|
||||||
|
base: apiBaseUrl.value,
|
||||||
|
jwt: props.jwt,
|
||||||
|
site_password: auth.value || '',
|
||||||
|
}, null, 2))
|
||||||
|
const agentText = computed(() => [
|
||||||
|
`${t('currentAddress')}: ${props.address || '-'}`,
|
||||||
|
`${t('apiBase')}: ${apiBaseUrl.value}`,
|
||||||
|
`${t('agentSkill')}: ${agentSkillUrl}`,
|
||||||
|
`${t('agentConfig')}:`,
|
||||||
|
agentConfigJson.value,
|
||||||
|
].join('\n'))
|
||||||
|
const smtpImapText = computed(() => [
|
||||||
|
`${t('smtpHost')}: ${smtpConfig.value.host || '-'}`,
|
||||||
|
`${t('smtpPort')}: ${smtpConfig.value.port || 8025}`,
|
||||||
|
`${t('imapHost')}: ${imapConfig.value.host || '-'}`,
|
||||||
|
`${t('imapPort')}: ${imapConfig.value.port || 11143}`,
|
||||||
|
`${t('security')}: ${securityLabel.value}`,
|
||||||
|
`${t('username')}: ${props.address || '-'}`,
|
||||||
|
`${t('password')}: ${props.jwt}`,
|
||||||
|
].join('\n'))
|
||||||
|
|
||||||
|
const copyText = async (text) => {
|
||||||
|
if (!text) return
|
||||||
|
try {
|
||||||
|
if (navigator.clipboard?.writeText) {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
message.success(t('copySuccess'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const textarea = document.createElement('textarea')
|
||||||
|
try {
|
||||||
|
textarea.value = text
|
||||||
|
textarea.setAttribute('readonly', '')
|
||||||
|
textarea.style.position = 'fixed'
|
||||||
|
textarea.style.opacity = '0'
|
||||||
|
document.body.appendChild(textarea)
|
||||||
|
textarea.select()
|
||||||
|
if (document.execCommand('copy')) {
|
||||||
|
message.success(t('copySuccess'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
message.error(t('copyFailed'))
|
||||||
|
} finally {
|
||||||
|
textarea.parentNode?.removeChild(textarea)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
message.error(t('copyFailed'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-modal v-model:show="modalShow" preset="card" :title="t('title')"
|
||||||
|
style="width: min(760px, calc(100vw - 32px));">
|
||||||
|
<n-alert type="info" :show-icon="false" :bordered="false">
|
||||||
|
{{ t('tip') }}
|
||||||
|
</n-alert>
|
||||||
|
<section class="credential-panel">
|
||||||
|
<h3 class="credential-title">{{ t('addressCredential') }}</h3>
|
||||||
|
<div class="credential-section">
|
||||||
|
<div class="credential-field" v-if="address">
|
||||||
|
<span class="credential-label">{{ t('currentAddress') }}</span>
|
||||||
|
<div class="credential-copy-row">
|
||||||
|
<code class="credential-code">{{ address }}</code>
|
||||||
|
<n-button size="tiny" tertiary type="primary" @click="copyText(address)">
|
||||||
|
{{ t('copySection') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="credential-field">
|
||||||
|
<span class="credential-label">{{ t('addressCredentialLabel') }}</span>
|
||||||
|
<div class="credential-copy-row">
|
||||||
|
<code class="credential-code">{{ jwt }}</code>
|
||||||
|
<n-button size="tiny" tertiary type="primary" @click="copyText(jwt)">
|
||||||
|
{{ t('copySection') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="credential-field" v-if="addressPassword">
|
||||||
|
<span class="credential-label">{{ t('addressPassword') }}</span>
|
||||||
|
<code class="credential-code">{{ addressPassword }}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<n-collapse accordion class="credential-collapse">
|
||||||
|
<n-collapse-item v-if="showAgent" name="agent" :title="t('agentAccess')">
|
||||||
|
<template #header-extra>
|
||||||
|
<n-button size="tiny" tertiary type="primary" @click.stop="copyText(agentText)">
|
||||||
|
{{ t('copySection') }}
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
<div class="credential-section">
|
||||||
|
<p class="credential-tip">{{ t('agentAccessTip') }}</p>
|
||||||
|
<div class="credential-field">
|
||||||
|
<span class="credential-label">{{ t('apiBase') }}</span>
|
||||||
|
<code class="credential-code">{{ apiBaseUrl }}</code>
|
||||||
|
</div>
|
||||||
|
<div class="credential-field">
|
||||||
|
<span class="credential-label">{{ t('agentSkill') }}</span>
|
||||||
|
<code class="credential-code">
|
||||||
|
<a :href="agentSkillUrl" target="_blank" rel="noopener noreferrer">{{ agentSkillUrl }}</a>
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
<div class="credential-field">
|
||||||
|
<span class="credential-label">{{ t('agentConfig') }}</span>
|
||||||
|
<pre class="credential-code credential-code-block">{{ agentConfigJson }}</pre>
|
||||||
|
</div>
|
||||||
|
<div class="credential-actions">
|
||||||
|
<n-button tag="a" :href="agentDocUrl" target="_blank" rel="noopener noreferrer" text type="primary">
|
||||||
|
{{ t('docs') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
|
||||||
|
<n-collapse-item v-if="showSmtpImap" name="smtp-imap" :title="t('smtpImapAccess')">
|
||||||
|
<template #header-extra>
|
||||||
|
<n-button size="tiny" tertiary type="primary" @click.stop="copyText(smtpImapText)">
|
||||||
|
{{ t('copySection') }}
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
<div class="credential-section">
|
||||||
|
<p class="credential-tip">{{ t('smtpImapTip') }}</p>
|
||||||
|
<div class="credential-grid">
|
||||||
|
<div class="credential-field">
|
||||||
|
<span class="credential-label">{{ t('smtpHost') }}</span>
|
||||||
|
<code class="credential-code">{{ smtpConfig.host || '-' }}</code>
|
||||||
|
</div>
|
||||||
|
<div class="credential-field">
|
||||||
|
<span class="credential-label">{{ t('smtpPort') }}</span>
|
||||||
|
<code class="credential-code">{{ smtpConfig.port || 8025 }}</code>
|
||||||
|
</div>
|
||||||
|
<div class="credential-field">
|
||||||
|
<span class="credential-label">{{ t('imapHost') }}</span>
|
||||||
|
<code class="credential-code">{{ imapConfig.host || '-' }}</code>
|
||||||
|
</div>
|
||||||
|
<div class="credential-field">
|
||||||
|
<span class="credential-label">{{ t('imapPort') }}</span>
|
||||||
|
<code class="credential-code">{{ imapConfig.port || 11143 }}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="credential-field">
|
||||||
|
<span class="credential-label">{{ t('security') }}</span>
|
||||||
|
<code class="credential-code">{{ securityLabel }}</code>
|
||||||
|
</div>
|
||||||
|
<div class="credential-field">
|
||||||
|
<span class="credential-label">{{ t('username') }}</span>
|
||||||
|
<code class="credential-code">{{ address }}</code>
|
||||||
|
</div>
|
||||||
|
<div class="credential-field">
|
||||||
|
<span class="credential-label">{{ t('password') }}</span>
|
||||||
|
<code class="credential-code">{{ jwt }}</code>
|
||||||
|
</div>
|
||||||
|
<div class="credential-actions">
|
||||||
|
<n-button tag="a" :href="smtpImapDocUrl" target="_blank" rel="noopener noreferrer" text type="primary">
|
||||||
|
{{ t('docs') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
|
||||||
|
<n-collapse-item name="share-link" :title="t('autoLoginLink')">
|
||||||
|
<template #header-extra>
|
||||||
|
<n-button size="tiny" tertiary type="primary" @click.stop="copyText(autoLoginUrl)">
|
||||||
|
{{ t('copySection') }}
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
<div class="credential-section">
|
||||||
|
<div class="credential-field">
|
||||||
|
<code class="credential-code">{{ autoLoginUrl }}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-collapse-item>
|
||||||
|
</n-collapse>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.credential-collapse {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-panel {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-section {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-tip {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--n-text-color-2);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-field {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-label {
|
||||||
|
color: var(--n-text-color-2);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-code {
|
||||||
|
display: block;
|
||||||
|
min-width: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: var(--n-color-embedded);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-copy-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
align-items: start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-code-block {
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.credential-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-copy-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -586,5 +586,32 @@ export const deMessages = {
|
|||||||
"views.admin.AccountSettings.tip": "Die folgenden Mehrfachauswahlwerte können manuell eingegeben und mit Enter hinzugefügt werden",
|
"views.admin.AccountSettings.tip": "Die folgenden Mehrfachauswahlwerte können manuell eingegeben und mit Enter hinzugefügt werden",
|
||||||
"components.MailBox.emptyInbox": "Dein Posteingang ist leer",
|
"components.MailBox.emptyInbox": "Dein Posteingang ist leer",
|
||||||
"views.index.SendMail.fromName": "Dein Name und deine Adresse; Namen leer lassen, um die E-Mail-Adresse zu verwenden",
|
"views.index.SendMail.fromName": "Dein Name und deine Adresse; Namen leer lassen, um die E-Mail-Adresse zu verwenden",
|
||||||
"views.admin.SendMail.fromName": "Dein Name und deine Adresse; Namen leer lassen, um die E-Mail-Adresse zu verwenden"
|
"views.admin.SendMail.fromName": "Dein Name und deine Adresse; Namen leer lassen, um die E-Mail-Adresse zu verwenden",
|
||||||
|
"components.AddressCredentialModal.addressCredential": "Adresszugangsdaten",
|
||||||
|
"components.AddressCredentialModal.addressCredentialLabel": "Address JWT",
|
||||||
|
"components.AddressCredentialModal.addressPassword": "Adresspasswort",
|
||||||
|
"components.AddressCredentialModal.agentAccess": "AI Agent",
|
||||||
|
"components.AddressCredentialModal.agentAccessTip": "Verwende dieses Postfach in einem AI Agent mit dem Address JWT und den parsed-mail APIs.",
|
||||||
|
"components.AddressCredentialModal.agentConfig": "Agent-Konfiguration",
|
||||||
|
"components.AddressCredentialModal.agentSkill": "Agent skill",
|
||||||
|
"components.AddressCredentialModal.apiBase": "API-Basisadresse",
|
||||||
|
"components.AddressCredentialModal.autoLoginLink": "Auto-Login-Link",
|
||||||
|
"components.AddressCredentialModal.copyFailed": "Kopieren fehlgeschlagen",
|
||||||
|
"components.AddressCredentialModal.copySection": "Kopieren",
|
||||||
|
"components.AddressCredentialModal.copySuccess": "Kopiert",
|
||||||
|
"components.AddressCredentialModal.currentAddress": "Aktuelle Adresse",
|
||||||
|
"components.AddressCredentialModal.docs": "Dokumentation",
|
||||||
|
"components.AddressCredentialModal.imapHost": "IMAP-Host",
|
||||||
|
"components.AddressCredentialModal.imapPort": "IMAP-Port",
|
||||||
|
"components.AddressCredentialModal.password": "Passwort",
|
||||||
|
"components.AddressCredentialModal.plainOrProxyTls": "Klartext oder Proxy-TLS",
|
||||||
|
"components.AddressCredentialModal.security": "Sicherheit",
|
||||||
|
"components.AddressCredentialModal.smtpHost": "SMTP-Host",
|
||||||
|
"components.AddressCredentialModal.smtpImapAccess": "SMTP / IMAP",
|
||||||
|
"components.AddressCredentialModal.smtpImapTip": "Verwende diese Werte in Mail-Clients, nachdem der Administrator den SMTP/IMAP-Proxy konfiguriert hat. Als Passwort kannst du den hier angezeigten Address JWT oder ein vorhandenes Adresspasswort verwenden.",
|
||||||
|
"components.AddressCredentialModal.smtpPort": "SMTP-Port",
|
||||||
|
"components.AddressCredentialModal.starttls": "STARTTLS",
|
||||||
|
"components.AddressCredentialModal.tip": "Verwende diese Zugangsdaten nur mit Clients und Agents, denen du vertraust.",
|
||||||
|
"components.AddressCredentialModal.title": "Adresszugangsdaten & Verbindungsmethoden",
|
||||||
|
"components.AddressCredentialModal.username": "Benutzername"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -586,5 +586,32 @@ export const esMessages = {
|
|||||||
"views.admin.AccountSettings.tip": "Puedes introducir manualmente los siguientes valores y pulsar Enter para añadirlos",
|
"views.admin.AccountSettings.tip": "Puedes introducir manualmente los siguientes valores y pulsar Enter para añadirlos",
|
||||||
"components.MailBox.emptyInbox": "Tu bandeja de entrada está vacía",
|
"components.MailBox.emptyInbox": "Tu bandeja de entrada está vacía",
|
||||||
"views.index.SendMail.fromName": "Tu nombre y dirección; deja el nombre vacío para usar el correo",
|
"views.index.SendMail.fromName": "Tu nombre y dirección; deja el nombre vacío para usar el correo",
|
||||||
"views.admin.SendMail.fromName": "Tu nombre y dirección; deja el nombre vacío para usar el correo"
|
"views.admin.SendMail.fromName": "Tu nombre y dirección; deja el nombre vacío para usar el correo",
|
||||||
|
"components.AddressCredentialModal.addressCredential": "Credencial de dirección",
|
||||||
|
"components.AddressCredentialModal.addressCredentialLabel": "Address JWT",
|
||||||
|
"components.AddressCredentialModal.addressPassword": "Contraseña de la dirección",
|
||||||
|
"components.AddressCredentialModal.agentAccess": "AI Agent",
|
||||||
|
"components.AddressCredentialModal.agentAccessTip": "Usa este buzón desde un AI Agent con el Address JWT y las APIs parsed-mail.",
|
||||||
|
"components.AddressCredentialModal.agentConfig": "Configuración del agente",
|
||||||
|
"components.AddressCredentialModal.agentSkill": "Agent skill",
|
||||||
|
"components.AddressCredentialModal.apiBase": "Base de la API",
|
||||||
|
"components.AddressCredentialModal.autoLoginLink": "Enlace de inicio automático",
|
||||||
|
"components.AddressCredentialModal.copyFailed": "Error al copiar",
|
||||||
|
"components.AddressCredentialModal.copySection": "Copiar",
|
||||||
|
"components.AddressCredentialModal.copySuccess": "Copiado",
|
||||||
|
"components.AddressCredentialModal.currentAddress": "Dirección actual",
|
||||||
|
"components.AddressCredentialModal.docs": "Documentación",
|
||||||
|
"components.AddressCredentialModal.imapHost": "Host IMAP",
|
||||||
|
"components.AddressCredentialModal.imapPort": "Puerto IMAP",
|
||||||
|
"components.AddressCredentialModal.password": "Contraseña",
|
||||||
|
"components.AddressCredentialModal.plainOrProxyTls": "Texto plano o TLS del proxy",
|
||||||
|
"components.AddressCredentialModal.security": "Seguridad",
|
||||||
|
"components.AddressCredentialModal.smtpHost": "Host SMTP",
|
||||||
|
"components.AddressCredentialModal.smtpImapAccess": "SMTP / IMAP",
|
||||||
|
"components.AddressCredentialModal.smtpImapTip": "Usa estos valores en clientes de correo después de que el administrador configure el proxy SMTP/IMAP. Como contraseña puedes usar el Address JWT mostrado aquí o la contraseña de la dirección si la tienes.",
|
||||||
|
"components.AddressCredentialModal.smtpPort": "Puerto SMTP",
|
||||||
|
"components.AddressCredentialModal.starttls": "STARTTLS",
|
||||||
|
"components.AddressCredentialModal.tip": "Usa estas credenciales solo con clientes y agentes de confianza.",
|
||||||
|
"components.AddressCredentialModal.title": "Credenciales de dirección y métodos de conexión",
|
||||||
|
"components.AddressCredentialModal.username": "Usuario"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -586,5 +586,32 @@ export const jaMessages = {
|
|||||||
"views.admin.AccountSettings.tip": "以下の複数選択項目は手動入力して Enter で追加できます",
|
"views.admin.AccountSettings.tip": "以下の複数選択項目は手動入力して Enter で追加できます",
|
||||||
"components.MailBox.emptyInbox": "受信箱は空です",
|
"components.MailBox.emptyInbox": "受信箱は空です",
|
||||||
"views.index.SendMail.fromName": "あなたの名前とアドレス。名前を空欄にするとメールアドレスを使用します",
|
"views.index.SendMail.fromName": "あなたの名前とアドレス。名前を空欄にするとメールアドレスを使用します",
|
||||||
"views.admin.SendMail.fromName": "あなたの名前とアドレス。名前を空欄にするとメールアドレスを使用します"
|
"views.admin.SendMail.fromName": "あなたの名前とアドレス。名前を空欄にするとメールアドレスを使用します",
|
||||||
|
"components.AddressCredentialModal.addressCredential": "アドレス認証情報",
|
||||||
|
"components.AddressCredentialModal.addressCredentialLabel": "Address JWT",
|
||||||
|
"components.AddressCredentialModal.addressPassword": "アドレスパスワード",
|
||||||
|
"components.AddressCredentialModal.agentAccess": "AI Agent",
|
||||||
|
"components.AddressCredentialModal.agentAccessTip": "AI Agent から Address JWT と parsed-mail API を使ってこのメールボックスを利用できます。",
|
||||||
|
"components.AddressCredentialModal.agentConfig": "Agent 設定",
|
||||||
|
"components.AddressCredentialModal.agentSkill": "Agent skill",
|
||||||
|
"components.AddressCredentialModal.apiBase": "API ベース",
|
||||||
|
"components.AddressCredentialModal.autoLoginLink": "自動ログインリンク",
|
||||||
|
"components.AddressCredentialModal.copyFailed": "コピーに失敗しました",
|
||||||
|
"components.AddressCredentialModal.copySection": "コピー",
|
||||||
|
"components.AddressCredentialModal.copySuccess": "コピーしました",
|
||||||
|
"components.AddressCredentialModal.currentAddress": "現在のアドレス",
|
||||||
|
"components.AddressCredentialModal.docs": "ドキュメント",
|
||||||
|
"components.AddressCredentialModal.imapHost": "IMAP ホスト",
|
||||||
|
"components.AddressCredentialModal.imapPort": "IMAP ポート",
|
||||||
|
"components.AddressCredentialModal.password": "パスワード",
|
||||||
|
"components.AddressCredentialModal.plainOrProxyTls": "平文またはプロキシ側 TLS",
|
||||||
|
"components.AddressCredentialModal.security": "セキュリティ",
|
||||||
|
"components.AddressCredentialModal.smtpHost": "SMTP ホスト",
|
||||||
|
"components.AddressCredentialModal.smtpImapAccess": "SMTP / IMAP",
|
||||||
|
"components.AddressCredentialModal.smtpImapTip": "管理者が SMTP/IMAP プロキシを設定した後、メールクライアントでこれらの値を使用できます。パスワードにはここに表示される Address JWT、または手元にあるアドレスパスワードを使用できます。",
|
||||||
|
"components.AddressCredentialModal.smtpPort": "SMTP ポート",
|
||||||
|
"components.AddressCredentialModal.starttls": "STARTTLS",
|
||||||
|
"components.AddressCredentialModal.tip": "これらの認証情報は信頼できるクライアントと Agent でのみ使用してください。",
|
||||||
|
"components.AddressCredentialModal.title": "アドレス認証情報と接続方法",
|
||||||
|
"components.AddressCredentialModal.username": "ユーザー名"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -586,5 +586,32 @@ export const ptBRMessages = {
|
|||||||
"views.admin.AccountSettings.tip": "Você pode inserir manualmente os seguintes valores e pressionar Enter para adicioná-los",
|
"views.admin.AccountSettings.tip": "Você pode inserir manualmente os seguintes valores e pressionar Enter para adicioná-los",
|
||||||
"components.MailBox.emptyInbox": "Sua caixa de entrada está vazia",
|
"components.MailBox.emptyInbox": "Sua caixa de entrada está vazia",
|
||||||
"views.index.SendMail.fromName": "Seu nome e endereço; deixe o nome em branco para usar o e-mail",
|
"views.index.SendMail.fromName": "Seu nome e endereço; deixe o nome em branco para usar o e-mail",
|
||||||
"views.admin.SendMail.fromName": "Seu nome e endereço; deixe o nome em branco para usar o e-mail"
|
"views.admin.SendMail.fromName": "Seu nome e endereço; deixe o nome em branco para usar o e-mail",
|
||||||
|
"components.AddressCredentialModal.addressCredential": "Credencial do endereço",
|
||||||
|
"components.AddressCredentialModal.addressCredentialLabel": "Address JWT",
|
||||||
|
"components.AddressCredentialModal.addressPassword": "Senha do endereço",
|
||||||
|
"components.AddressCredentialModal.agentAccess": "AI Agent",
|
||||||
|
"components.AddressCredentialModal.agentAccessTip": "Use esta caixa de entrada em um AI Agent com o Address JWT e as APIs parsed-mail.",
|
||||||
|
"components.AddressCredentialModal.agentConfig": "Configuração do Agent",
|
||||||
|
"components.AddressCredentialModal.agentSkill": "Agent skill",
|
||||||
|
"components.AddressCredentialModal.apiBase": "Base da API",
|
||||||
|
"components.AddressCredentialModal.autoLoginLink": "Link de login automático",
|
||||||
|
"components.AddressCredentialModal.copyFailed": "Falha ao copiar",
|
||||||
|
"components.AddressCredentialModal.copySection": "Copiar",
|
||||||
|
"components.AddressCredentialModal.copySuccess": "Copiado",
|
||||||
|
"components.AddressCredentialModal.currentAddress": "Endereço atual",
|
||||||
|
"components.AddressCredentialModal.docs": "Documentação",
|
||||||
|
"components.AddressCredentialModal.imapHost": "Host IMAP",
|
||||||
|
"components.AddressCredentialModal.imapPort": "Porta IMAP",
|
||||||
|
"components.AddressCredentialModal.password": "Senha",
|
||||||
|
"components.AddressCredentialModal.plainOrProxyTls": "Texto puro ou TLS do proxy",
|
||||||
|
"components.AddressCredentialModal.security": "Segurança",
|
||||||
|
"components.AddressCredentialModal.smtpHost": "Host SMTP",
|
||||||
|
"components.AddressCredentialModal.smtpImapAccess": "SMTP / IMAP",
|
||||||
|
"components.AddressCredentialModal.smtpImapTip": "Use estes valores em clientes de e-mail depois que o administrador configurar o proxy SMTP/IMAP. Como senha, use o Address JWT mostrado aqui ou a senha do endereço quando você a tiver.",
|
||||||
|
"components.AddressCredentialModal.smtpPort": "Porta SMTP",
|
||||||
|
"components.AddressCredentialModal.starttls": "STARTTLS",
|
||||||
|
"components.AddressCredentialModal.tip": "Use estas credenciais somente com clientes e agents confiáveis.",
|
||||||
|
"components.AddressCredentialModal.title": "Credenciais do endereço e métodos de conexão",
|
||||||
|
"components.AddressCredentialModal.username": "Nome de usuário"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -281,6 +281,116 @@ export const MESSAGE_REGISTRY = {
|
|||||||
"zh": "用户地址"
|
"zh": "用户地址"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"components.AddressCredentialModal": {
|
||||||
|
"addressCredential": {
|
||||||
|
"en": "Address Credential",
|
||||||
|
"zh": "地址凭证"
|
||||||
|
},
|
||||||
|
"addressCredentialLabel": {
|
||||||
|
"en": "Address JWT",
|
||||||
|
"zh": "Address JWT"
|
||||||
|
},
|
||||||
|
"addressPassword": {
|
||||||
|
"en": "Address Password",
|
||||||
|
"zh": "地址密码"
|
||||||
|
},
|
||||||
|
"agentAccess": {
|
||||||
|
"en": "AI Agent",
|
||||||
|
"zh": "AI Agent"
|
||||||
|
},
|
||||||
|
"agentAccessTip": {
|
||||||
|
"en": "Use this mailbox from an AI agent with the Address JWT and parsed-mail APIs.",
|
||||||
|
"zh": "AI Agent 可使用 Address JWT 和 parsed-mail API 读取这个邮箱。"
|
||||||
|
},
|
||||||
|
"agentConfig": {
|
||||||
|
"en": "Agent config",
|
||||||
|
"zh": "Agent 配置"
|
||||||
|
},
|
||||||
|
"agentSkill": {
|
||||||
|
"en": "Agent skill",
|
||||||
|
"zh": "Agent skill"
|
||||||
|
},
|
||||||
|
"apiBase": {
|
||||||
|
"en": "API Base",
|
||||||
|
"zh": "API 地址"
|
||||||
|
},
|
||||||
|
"autoLoginLink": {
|
||||||
|
"en": "Auto-login link",
|
||||||
|
"zh": "自动登录链接"
|
||||||
|
},
|
||||||
|
"copyFailed": {
|
||||||
|
"en": "Copy failed",
|
||||||
|
"zh": "复制失败"
|
||||||
|
},
|
||||||
|
"copySection": {
|
||||||
|
"en": "Copy",
|
||||||
|
"zh": "复制"
|
||||||
|
},
|
||||||
|
"copySuccess": {
|
||||||
|
"en": "Copied",
|
||||||
|
"zh": "已复制"
|
||||||
|
},
|
||||||
|
"currentAddress": {
|
||||||
|
"en": "Current address",
|
||||||
|
"zh": "当前邮箱"
|
||||||
|
},
|
||||||
|
"docs": {
|
||||||
|
"en": "Docs",
|
||||||
|
"zh": "文档"
|
||||||
|
},
|
||||||
|
"imapHost": {
|
||||||
|
"en": "IMAP host",
|
||||||
|
"zh": "IMAP 主机"
|
||||||
|
},
|
||||||
|
"imapPort": {
|
||||||
|
"en": "IMAP port",
|
||||||
|
"zh": "IMAP 端口"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"en": "Password",
|
||||||
|
"zh": "密码"
|
||||||
|
},
|
||||||
|
"plainOrProxyTls": {
|
||||||
|
"en": "Plain or proxy TLS",
|
||||||
|
"zh": "明文或代理层 TLS"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"en": "Security",
|
||||||
|
"zh": "安全"
|
||||||
|
},
|
||||||
|
"smtpHost": {
|
||||||
|
"en": "SMTP host",
|
||||||
|
"zh": "SMTP 主机"
|
||||||
|
},
|
||||||
|
"smtpImapAccess": {
|
||||||
|
"en": "SMTP / IMAP",
|
||||||
|
"zh": "SMTP / IMAP"
|
||||||
|
},
|
||||||
|
"smtpImapTip": {
|
||||||
|
"en": "Use these values in mail clients after the administrator configures the SMTP/IMAP proxy. The password can be the Address JWT shown here, or the address password when you have it.",
|
||||||
|
"zh": "管理员配置 SMTP/IMAP 代理后,可在邮件客户端中使用这些信息。密码可使用这里展示的 Address JWT,也可使用你持有的地址密码。"
|
||||||
|
},
|
||||||
|
"smtpPort": {
|
||||||
|
"en": "SMTP port",
|
||||||
|
"zh": "SMTP 端口"
|
||||||
|
},
|
||||||
|
"starttls": {
|
||||||
|
"en": "STARTTLS",
|
||||||
|
"zh": "STARTTLS"
|
||||||
|
},
|
||||||
|
"tip": {
|
||||||
|
"en": "Use these credentials only with clients and agents you trust.",
|
||||||
|
"zh": "请只在可信的客户端和 Agent 中使用这些凭证。"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"en": "Address Credentials & Connection Methods",
|
||||||
|
"zh": "地址凭证与连接方式"
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"en": "Username",
|
||||||
|
"zh": "用户名"
|
||||||
|
}
|
||||||
|
},
|
||||||
"views.user.UserMailBox": {
|
"views.user.UserMailBox": {
|
||||||
"addressQueryTip": {
|
"addressQueryTip": {
|
||||||
"en": "Leave blank to query all addresses",
|
"en": "Leave blank to query all addresses",
|
||||||
@@ -817,8 +927,8 @@ export const MESSAGE_REGISTRY = {
|
|||||||
"zh": "密码不匹配"
|
"zh": "密码不匹配"
|
||||||
},
|
},
|
||||||
"showAddressCredential": {
|
"showAddressCredential": {
|
||||||
"en": "Show Address Credential",
|
"en": "Credentials & Connection Methods",
|
||||||
"zh": "查看邮箱地址凭证"
|
"zh": "地址凭证与连接方式"
|
||||||
},
|
},
|
||||||
"success": {
|
"success": {
|
||||||
"en": "Success",
|
"en": "Success",
|
||||||
|
|||||||
@@ -61,8 +61,20 @@ router.beforeEach((to, from, next) => {
|
|||||||
preferredLocale.value = getPreferredLocale('', getBrowserLocales())
|
preferredLocale.value = getPreferredLocale('', getBrowserLocales())
|
||||||
}
|
}
|
||||||
|
|
||||||
if (to.query.jwt) {
|
if (Object.prototype.hasOwnProperty.call(to.query, 'jwt')) {
|
||||||
jwt.value = to.query.jwt
|
const jwtQuery = Array.isArray(to.query.jwt) ? to.query.jwt[0] : to.query.jwt
|
||||||
|
if (typeof jwtQuery === 'string') {
|
||||||
|
jwt.value = jwtQuery
|
||||||
|
}
|
||||||
|
const query = { ...to.query }
|
||||||
|
delete query.jwt
|
||||||
|
next({
|
||||||
|
path: to.path,
|
||||||
|
query,
|
||||||
|
hash: to.hash,
|
||||||
|
replace: true,
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (routeLocale) {
|
if (routeLocale) {
|
||||||
|
|||||||
@@ -40,6 +40,19 @@ export const useGlobalState = createGlobalState(
|
|||||||
showGithub: true,
|
showGithub: true,
|
||||||
disableAdminPasswordCheck: false,
|
disableAdminPasswordCheck: false,
|
||||||
enableAddressPassword: false,
|
enableAddressPassword: false,
|
||||||
|
enableAgentEmailInfo: false,
|
||||||
|
smtpImapProxyConfig: {
|
||||||
|
smtp: {
|
||||||
|
host: '',
|
||||||
|
port: 8025,
|
||||||
|
starttls: false,
|
||||||
|
},
|
||||||
|
imap: {
|
||||||
|
host: '',
|
||||||
|
port: 11143,
|
||||||
|
starttls: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
statusUrl: '',
|
statusUrl: '',
|
||||||
enableGlobalTurnstileCheck: false,
|
enableGlobalTurnstileCheck: false,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ onMounted(async () => {
|
|||||||
<n-modal v-model:show="showAdminPasswordModal" :closable="false" :closeOnEsc="false" :maskClosable="false"
|
<n-modal v-model:show="showAdminPasswordModal" :closable="false" :closeOnEsc="false" :maskClosable="false"
|
||||||
preset="dialog" :title="t('accessHeader')">
|
preset="dialog" :title="t('accessHeader')">
|
||||||
<p>{{ t('accessTip') }}</p>
|
<p>{{ t('accessTip') }}</p>
|
||||||
<n-input v-model:value="tmpAdminAuth" type="password" show-password-on="click" />
|
<n-input v-model:value="tmpAdminAuth" type="password" show-password-on="click" @keyup.enter="authFunc" />
|
||||||
<Turnstile ref="turnstileRef" v-if="openSettings.enableGlobalTurnstileCheck" v-model:value="cfToken" />
|
<Turnstile ref="turnstileRef" v-if="openSettings.enableGlobalTurnstileCheck" v-model:value="cfToken" />
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button @click="authFunc" type="primary" :loading="loading">
|
<n-button @click="authFunc" type="primary" :loading="loading">
|
||||||
|
|||||||
@@ -314,7 +314,7 @@ onMounted(async () => {
|
|||||||
<n-modal v-model:show="showAuth" :closable="false" :closeOnEsc="false" :maskClosable="false" preset="dialog"
|
<n-modal v-model:show="showAuth" :closable="false" :closeOnEsc="false" :maskClosable="false" preset="dialog"
|
||||||
:title="t('accessHeader')">
|
:title="t('accessHeader')">
|
||||||
<p>{{ t('accessTip') }}</p>
|
<p>{{ t('accessTip') }}</p>
|
||||||
<n-input v-model:value="auth" type="password" show-password-on="click" />
|
<n-input v-model:value="auth" type="password" show-password-on="click" @keyup.enter="authFunc" />
|
||||||
<Turnstile ref="turnstileRef" v-if="openSettings.enableGlobalTurnstileCheck" v-model:value="cfToken" />
|
<Turnstile ref="turnstileRef" v-if="openSettings.enableGlobalTurnstileCheck" v-model:value="cfToken" />
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button :loading="loading" @click="authFunc" type="primary">
|
<n-button :loading="loading" @click="authFunc" type="primary">
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import { useScopedI18n } from '@/i18n/app'
|
|||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
import { useGlobalState } from '../../store'
|
||||||
import { api } from '../../api'
|
import { api } from '../../api'
|
||||||
|
import { hashPassword } from '../../utils'
|
||||||
import { NButton, NMenu } from 'naive-ui';
|
import { NButton, NMenu } from 'naive-ui';
|
||||||
import { MenuFilled } from '@vicons/material'
|
import { MenuFilled } from '@vicons/material'
|
||||||
|
import AddressCredentialModal from '../../components/AddressCredentialModal.vue'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
loading, adminTab, openSettings,
|
loading, adminTab, openSettings,
|
||||||
@@ -18,6 +20,7 @@ const { t } = useScopedI18n('views.admin.Account')
|
|||||||
|
|
||||||
const showEmailCredential = ref(false)
|
const showEmailCredential = ref(false)
|
||||||
const curEmailCredential = ref("")
|
const curEmailCredential = ref("")
|
||||||
|
const curEmailAddress = ref("")
|
||||||
const curDeleteAddressId = ref(0);
|
const curDeleteAddressId = ref(0);
|
||||||
const curClearInboxAddressId = ref(0);
|
const curClearInboxAddressId = ref(0);
|
||||||
const curClearSentItemsAddressId = ref(0);
|
const curClearSentItemsAddressId = ref(0);
|
||||||
@@ -46,14 +49,16 @@ const showDeleteAccount = ref(false)
|
|||||||
const showClearInbox = ref(false)
|
const showClearInbox = ref(false)
|
||||||
const showClearSentItems = ref(false)
|
const showClearSentItems = ref(false)
|
||||||
|
|
||||||
const showCredential = async (id) => {
|
const showCredential = async (row) => {
|
||||||
try {
|
try {
|
||||||
curEmailCredential.value = await api.adminShowAddressCredential(id)
|
curEmailAddress.value = row.name
|
||||||
|
curEmailCredential.value = await api.adminShowAddressCredential(row.id)
|
||||||
showEmailCredential.value = true
|
showEmailCredential.value = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error.message || "error");
|
message.error(error.message || "error");
|
||||||
showEmailCredential.value = false
|
showEmailCredential.value = false
|
||||||
curEmailCredential.value = ""
|
curEmailCredential.value = ""
|
||||||
|
curEmailAddress.value = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,11 +103,16 @@ const clearSentItems = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resetPassword = async () => {
|
const resetPassword = async () => {
|
||||||
|
const normalizedPassword = newPassword.value.trim()
|
||||||
|
if (!normalizedPassword) {
|
||||||
|
message.error(t("newPassword"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await api.fetch(`/admin/address/${curResetPasswordAddressId.value}/reset_password`, {
|
await api.fetch(`/admin/address/${curResetPasswordAddressId.value}/reset_password`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
password: newPassword.value
|
password: await hashPassword(normalizedPassword)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
message.success(t("passwordResetSuccess"));
|
message.success(t("passwordResetSuccess"));
|
||||||
@@ -365,7 +375,7 @@ const columns = computed(() => [
|
|||||||
label: () => h(NButton,
|
label: () => h(NButton,
|
||||||
{
|
{
|
||||||
text: true,
|
text: true,
|
||||||
onClick: () => showCredential(row.id)
|
onClick: () => showCredential(row)
|
||||||
},
|
},
|
||||||
{ default: () => t('showCredential') }
|
{ default: () => t('showCredential') }
|
||||||
),
|
),
|
||||||
@@ -467,19 +477,8 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div style="margin-top: 10px;">
|
<div style="margin-top: 10px;">
|
||||||
<n-modal v-model:show="showEmailCredential" preset="dialog" title="Dialog">
|
<AddressCredentialModal v-model:show="showEmailCredential" :address="curEmailAddress"
|
||||||
<template #header>
|
:jwt="curEmailCredential" />
|
||||||
<div>{{ t("addressCredential") }}</div>
|
|
||||||
</template>
|
|
||||||
<span>
|
|
||||||
<p>{{ t("addressCredentialTip") }}</p>
|
|
||||||
</span>
|
|
||||||
<n-card :bordered="false" embedded>
|
|
||||||
<b>{{ curEmailCredential }}</b>
|
|
||||||
</n-card>
|
|
||||||
<template #action>
|
|
||||||
</template>
|
|
||||||
</n-modal>
|
|
||||||
<n-modal v-model:show="showDeleteAccount" preset="dialog" :title="t('deleteAccount')">
|
<n-modal v-model:show="showDeleteAccount" preset="dialog" :title="t('deleteAccount')">
|
||||||
<p>{{ t('deleteTip') }}</p>
|
<p>{{ t('deleteTip') }}</p>
|
||||||
<template #action>
|
<template #action>
|
||||||
@@ -507,7 +506,8 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<n-modal v-model:show="showResetPassword" preset="dialog" :title="t('resetPassword')">
|
<n-modal v-model:show="showResetPassword" preset="dialog" :title="t('resetPassword')">
|
||||||
<n-form-item :label="t('newPassword')">
|
<n-form-item :label="t('newPassword')">
|
||||||
<n-input v-model:value="newPassword" type="password" placeholder="" show-password-on="click" />
|
<n-input v-model:value="newPassword" type="password" placeholder="" show-password-on="click"
|
||||||
|
@keyup.enter="resetPassword" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button :loading="loading" @click="resetPassword" size="small" tertiary type="info">
|
<n-button :loading="loading" @click="resetPassword" size="small" tertiary type="info">
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useScopedI18n } from '@/i18n/app'
|
|||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
import { useGlobalState } from '../../store'
|
||||||
import { api } from '../../api'
|
import { api } from '../../api'
|
||||||
|
import AddressCredentialModal from '../../components/AddressCredentialModal.vue'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
loading, openSettings,
|
loading, openSettings,
|
||||||
@@ -59,10 +60,6 @@ const newEmail = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUrlWithJwt = () => {
|
|
||||||
return `${window.location.origin}/?jwt=${result.value}`
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (openSettings.prefix) {
|
if (openSettings.prefix) {
|
||||||
enablePrefix.value = true
|
enablePrefix.value = true
|
||||||
@@ -73,27 +70,8 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<n-modal v-model:show="showReultModal" preset="dialog" :title="t('addressCredential')">
|
<AddressCredentialModal v-model:show="showReultModal" :address="createdAddress" :jwt="result"
|
||||||
<span>
|
:address-password="addressPassword" />
|
||||||
<p>{{ t("addressCredentialTip") }}</p>
|
|
||||||
</span>
|
|
||||||
<n-card embedded>
|
|
||||||
<b>{{ result }}</b>
|
|
||||||
</n-card>
|
|
||||||
<n-card embedded v-if="addressPassword">
|
|
||||||
<p><b>{{ createdAddress }}</b></p>
|
|
||||||
<p>{{ t('addressPassword') }}: <b>{{ addressPassword }}</b></p>
|
|
||||||
</n-card>
|
|
||||||
<n-card embedded>
|
|
||||||
<n-collapse>
|
|
||||||
<n-collapse-item :title='t("linkWithAddressCredential")'>
|
|
||||||
<n-card embedded>
|
|
||||||
<b>{{ getUrlWithJwt() }}</b>
|
|
||||||
</n-card>
|
|
||||||
</n-collapse-item>
|
|
||||||
</n-collapse>
|
|
||||||
</n-card>
|
|
||||||
</n-modal>
|
|
||||||
<n-card :bordered="false" embedded style="max-width: 600px;">
|
<n-card :bordered="false" embedded style="max-width: 600px;">
|
||||||
<n-form-item-row v-if="openSettings.prefix" :label="t('enablePrefix')">
|
<n-form-item-row v-if="openSettings.prefix" :label="t('enablePrefix')">
|
||||||
<n-switch v-model:value="enablePrefix" :round="false" />
|
<n-switch v-model:value="enablePrefix" :round="false" />
|
||||||
|
|||||||
@@ -304,7 +304,8 @@ onMounted(async () => {
|
|||||||
<n-input v-model:value="user.email" />
|
<n-input v-model:value="user.email" />
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-form-item-row :label="t('password')" required>
|
<n-form-item-row :label="t('password')" required>
|
||||||
<n-input v-model:value="user.password" type="password" show-password-on="click" />
|
<n-input v-model:value="user.password" type="password" show-password-on="click"
|
||||||
|
@keyup.enter="createUser" />
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
</n-form>
|
</n-form>
|
||||||
<template #action>
|
<template #action>
|
||||||
@@ -315,7 +316,8 @@ onMounted(async () => {
|
|||||||
</n-modal>
|
</n-modal>
|
||||||
<n-modal v-model:show="showResetPassword" preset="dialog" :title="t('resetPassword')">
|
<n-modal v-model:show="showResetPassword" preset="dialog" :title="t('resetPassword')">
|
||||||
<n-form-item-row :label="t('password')" required>
|
<n-form-item-row :label="t('password')" required>
|
||||||
<n-input v-model:value="newResetPassword" type="password" show-password-on="click" />
|
<n-input v-model:value="newResetPassword" type="password" show-password-on="click"
|
||||||
|
@keyup.enter="resetPassword" />
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button :loading="loading" @click="resetPassword" size="small" tertiary type="primary">
|
<n-button :loading="loading" @click="resetPassword" size="small" tertiary type="primary">
|
||||||
|
|||||||
@@ -260,7 +260,8 @@ onMounted(async () => {
|
|||||||
<n-input v-model:value="loginAddress" />
|
<n-input v-model:value="loginAddress" />
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-form-item-row :label="t('password')" required>
|
<n-form-item-row :label="t('password')" required>
|
||||||
<n-input v-model:value="loginPassword" type="password" show-password-on="click" />
|
<n-input v-model:value="loginPassword" type="password" show-password-on="click"
|
||||||
|
@keyup.enter="login" />
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ const changePassword = async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="center" v-if="settings.address">
|
<div class="center" v-if="settings.address">
|
||||||
<n-card :bordered="false" embedded>
|
<n-card :bordered="false" embedded class="account-card">
|
||||||
<n-button @click="showAddressCredential = true" type="primary" secondary block strong>
|
<n-button @click="showAddressCredential = true" type="primary" secondary block strong>
|
||||||
{{ t('showAddressCredential') }}
|
{{ t('showAddressCredential') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
@@ -110,11 +110,13 @@ const changePassword = async () => {
|
|||||||
<n-button @click="showLogout = true" secondary block strong>
|
<n-button @click="showLogout = true" secondary block strong>
|
||||||
{{ t('logout') }}
|
{{ t('logout') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
|
<n-divider v-if="openSettings.enableUserDeleteEmail" />
|
||||||
<n-button v-if="openSettings.enableUserDeleteEmail" @click="showDeleteAccount = true" type="error" secondary
|
<n-button v-if="openSettings.enableUserDeleteEmail" @click="showDeleteAccount = true" type="error" secondary
|
||||||
block strong>
|
block strong>
|
||||||
{{ t('deleteAccount') }}
|
{{ t('deleteAccount') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<n-modal v-model:show="showLogout" preset="dialog" :title="t('logout')">
|
<n-modal v-model:show="showLogout" preset="dialog" :title="t('logout')">
|
||||||
<p>{{ t('logoutConfirm') }}</p>
|
<p>{{ t('logoutConfirm') }}</p>
|
||||||
<template #action>
|
<template #action>
|
||||||
@@ -151,10 +153,12 @@ const changePassword = async () => {
|
|||||||
<n-modal v-model:show="showChangePassword" preset="dialog" :title="t('changePassword')">
|
<n-modal v-model:show="showChangePassword" preset="dialog" :title="t('changePassword')">
|
||||||
<n-form :model="{ newPassword, confirmPassword }">
|
<n-form :model="{ newPassword, confirmPassword }">
|
||||||
<n-form-item :label="t('newPassword')">
|
<n-form-item :label="t('newPassword')">
|
||||||
<n-input v-model:value="newPassword" type="password" placeholder="" show-password-on="click" />
|
<n-input v-model:value="newPassword" type="password" placeholder="" show-password-on="click"
|
||||||
|
@keyup.enter="changePassword" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item :label="t('confirmPassword')">
|
<n-form-item :label="t('confirmPassword')">
|
||||||
<n-input v-model:value="confirmPassword" type="password" placeholder="" show-password-on="click" />
|
<n-input v-model:value="confirmPassword" type="password" placeholder="" show-password-on="click"
|
||||||
|
@keyup.enter="changePassword" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
<template #action>
|
<template #action>
|
||||||
@@ -172,13 +176,13 @@ const changePassword = async () => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account-card {
|
||||||
.n-card {
|
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.n-button {
|
.n-button {
|
||||||
margin-top: 10px;
|
margin-top: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import LocalAddress from './LocalAddress.vue'
|
|||||||
import AddressManagement from '../user/AddressManagement.vue'
|
import AddressManagement from '../user/AddressManagement.vue'
|
||||||
import { getRouterPathWithLang } from '../../utils'
|
import { getRouterPathWithLang } from '../../utils'
|
||||||
import AddressSelect from '../../components/AddressSelect.vue'
|
import AddressSelect from '../../components/AddressSelect.vue'
|
||||||
|
import AddressCredentialModal from '../../components/AddressCredentialModal.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
@@ -24,10 +25,6 @@ const { locale, t } = useScopedI18n('views.index.AddressBar')
|
|||||||
|
|
||||||
const showAddressManage = ref(false)
|
const showAddressManage = ref(false)
|
||||||
|
|
||||||
const getUrlWithJwt = () => {
|
|
||||||
return `${window.location.origin}/?jwt=${jwt.value}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const onUserLogin = async () => {
|
const onUserLogin = async () => {
|
||||||
await router.push(getRouterPathWithLang("/user", locale.value))
|
await router.push(getRouterPathWithLang("/user", locale.value))
|
||||||
}
|
}
|
||||||
@@ -78,27 +75,8 @@ onMounted(async () => {
|
|||||||
</n-button>
|
</n-button>
|
||||||
</n-card>
|
</n-card>
|
||||||
</div>
|
</div>
|
||||||
<n-modal v-model:show="showAddressCredential" preset="dialog" :title="t('addressCredential')">
|
<AddressCredentialModal v-model:show="showAddressCredential" :address="settings.address" :jwt="jwt"
|
||||||
<span>
|
:address-password="addressPassword" />
|
||||||
<p>{{ t("addressCredentialTip") }}</p>
|
|
||||||
</span>
|
|
||||||
<n-card embedded>
|
|
||||||
<b>{{ jwt }}</b>
|
|
||||||
</n-card>
|
|
||||||
<n-card embedded v-if="addressPassword">
|
|
||||||
<p><b>{{ settings.address }}</b></p>
|
|
||||||
<p>{{ t('addressPassword') }}: <b>{{ addressPassword }}</b></p>
|
|
||||||
</n-card>
|
|
||||||
<n-card embedded>
|
|
||||||
<n-collapse>
|
|
||||||
<n-collapse-item :title='t("linkWithAddressCredential")'>
|
|
||||||
<n-card embedded>
|
|
||||||
<b>{{ getUrlWithJwt() }}</b>
|
|
||||||
</n-card>
|
|
||||||
</n-collapse-item>
|
|
||||||
</n-collapse>
|
|
||||||
</n-card>
|
|
||||||
</n-modal>
|
|
||||||
<n-modal v-model:show="showAddressManage" preset="card" :title="t('addressManage')"
|
<n-modal v-model:show="showAddressManage" preset="card" :title="t('addressManage')"
|
||||||
style="width: 720px;">
|
style="width: 720px;">
|
||||||
<TelegramAddress v-if="isTelegram" />
|
<TelegramAddress v-if="isTelegram" />
|
||||||
|
|||||||
@@ -187,7 +187,8 @@ onMounted(async () => {
|
|||||||
<n-input v-model:value="user.email" />
|
<n-input v-model:value="user.email" />
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-form-item-row :label="t('password')" required>
|
<n-form-item-row :label="t('password')" required>
|
||||||
<n-input v-model:value="user.password" type="password" show-password-on="click" />
|
<n-input v-model:value="user.password" type="password" show-password-on="click"
|
||||||
|
@keyup.enter="emailLogin" />
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<Turnstile ref="loginTurnstileRef" v-if="openSettings.enableGlobalTurnstileCheck" v-model:value="loginCfToken" />
|
<Turnstile ref="loginTurnstileRef" v-if="openSettings.enableGlobalTurnstileCheck" v-model:value="loginCfToken" />
|
||||||
<n-button @click="emailLogin" type="primary" block secondary strong>
|
<n-button @click="emailLogin" type="primary" block secondary strong>
|
||||||
@@ -218,7 +219,8 @@ onMounted(async () => {
|
|||||||
<n-input v-model:value="user.email" />
|
<n-input v-model:value="user.email" />
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-form-item-row :label="t('password')" required>
|
<n-form-item-row :label="t('password')" required>
|
||||||
<n-input v-model:value="user.password" type="password" show-password-on="click" />
|
<n-input v-model:value="user.password" type="password" show-password-on="click"
|
||||||
|
@keyup.enter="emailSignup" />
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<Turnstile ref="signupTurnstileRef" v-if="userOpenSettings.enableMailVerify" v-model:value="signupCfToken" />
|
<Turnstile ref="signupTurnstileRef" v-if="userOpenSettings.enableMailVerify" v-model:value="signupCfToken" />
|
||||||
<n-form-item-row v-if="userOpenSettings.enableMailVerify" :label="t('verifyCode')" required>
|
<n-form-item-row v-if="userOpenSettings.enableMailVerify" :label="t('verifyCode')" required>
|
||||||
@@ -244,7 +246,8 @@ onMounted(async () => {
|
|||||||
<n-input v-model:value="user.email" />
|
<n-input v-model:value="user.email" />
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-form-item-row :label="t('password')" required>
|
<n-form-item-row :label="t('password')" required>
|
||||||
<n-input v-model:value="user.password" type="password" show-password-on="click" />
|
<n-input v-model:value="user.password" type="password" show-password-on="click"
|
||||||
|
@keyup.enter="emailSignup" />
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<Turnstile ref="resetTurnstileRef" v-model:value="resetCfToken" />
|
<Turnstile ref="resetTurnstileRef" v-model:value="resetCfToken" />
|
||||||
<n-form-item-row :label="t('verifyCode')" required>
|
<n-form-item-row :label="t('verifyCode')" required>
|
||||||
|
|||||||
@@ -43,6 +43,8 @@
|
|||||||
| `ENABLE_AUTO_REPLY` | Text/JSON | Allow automatic email replies. Sender filter (`source_prefix`) supports three modes: empty to match all senders, prefix for `startsWith` matching, or `/regex/` syntax for regex matching (e.g. `/@example\.com$/`) | `true` |
|
| `ENABLE_AUTO_REPLY` | Text/JSON | Allow automatic email replies. Sender filter (`source_prefix`) supports three modes: empty to match all senders, prefix for `startsWith` matching, or `/regex/` syntax for regex matching (e.g. `/@example\.com$/`) | `true` |
|
||||||
| `DEFAULT_SEND_BALANCE` | Text/JSON | Default email sending balance. When greater than `0`, it is auto-initialized when users open the settings page or send mail for the first time. Defaults to `0` if unset | `1` |
|
| `DEFAULT_SEND_BALANCE` | Text/JSON | Default email sending balance. When greater than `0`, it is auto-initialized when users open the settings page or send mail for the first time. Defaults to `0` if unset | `1` |
|
||||||
| `ENABLE_ADDRESS_PASSWORD` | Text/JSON | Enable address password feature, when enabled, passwords will be auto-generated for new addresses, supports password login and modification | `true` |
|
| `ENABLE_ADDRESS_PASSWORD` | Text/JSON | Enable address password feature, when enabled, passwords will be auto-generated for new addresses, supports password login and modification | `true` |
|
||||||
|
| `ENABLE_AGENT_EMAIL_INFO` | Text/JSON | Whether to show AI Agent access info in the frontend "Address Credentials & Connection Methods" dialog (Address JWT, parsed-mail APIs, skill link) | `true` |
|
||||||
|
| `SMTP_IMAP_PROXY_CONFIG` | JSON | Show SMTP/IMAP proxy connection info in the frontend "Address Credentials & Connection Methods" dialog; display-only, does not start the proxy service, which must be deployed separately | See example below |
|
||||||
| `SEND_MAIL_DOMAINS` | JSON | Restrict which sender domains can use the `SEND_MAIL` binding; when unset or empty, all domains are allowed | `["example.com", "mail.example.com"]` |
|
| `SEND_MAIL_DOMAINS` | JSON | Restrict which sender domains can use the `SEND_MAIL` binding; when unset or empty, all domains are allowed | `["example.com", "mail.example.com"]` |
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
@@ -66,6 +68,17 @@
|
|||||||
>
|
>
|
||||||
> `SEND_MAIL_DOMAINS` only affects the `SEND_MAIL` binding fallback path and
|
> `SEND_MAIL_DOMAINS` only affects the `SEND_MAIL` binding fallback path and
|
||||||
> `/admin/send_mail_by_binding`. It does not affect Resend, SMTP, or `verifiedAddressList`.
|
> `/admin/send_mail_by_binding`. It does not affect Resend, SMTP, or `verifiedAddressList`.
|
||||||
|
>
|
||||||
|
> `SMTP_IMAP_PROXY_CONFIG` example:
|
||||||
|
>
|
||||||
|
> ```json
|
||||||
|
> {
|
||||||
|
> "smtp": { "host": "smtp.example.com", "port": 8025, "starttls": true },
|
||||||
|
> "imap": { "host": "imap.example.com", "port": 11143, "starttls": true }
|
||||||
|
> }
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> SMTP and IMAP can use different hostnames, which is useful for reverse proxies or separate port mappings.
|
||||||
|
|
||||||
## Email Reception Related Variables
|
## Email Reception Related Variables
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,8 @@
|
|||||||
| `ENABLE_AUTO_REPLY` | 文本/JSON | 允许自动回复邮件。发件人过滤(`source_prefix`)支持三种模式:留空匹配所有发件人、填写前缀进行 `startsWith` 匹配、使用 `/regex/` 语法进行正则匹配(如 `/@example\.com$/`) | `true` |
|
| `ENABLE_AUTO_REPLY` | 文本/JSON | 允许自动回复邮件。发件人过滤(`source_prefix`)支持三种模式:留空匹配所有发件人、填写前缀进行 `startsWith` 匹配、使用 `/regex/` 语法进行正则匹配(如 `/@example\.com$/`) | `true` |
|
||||||
| `DEFAULT_SEND_BALANCE` | 文本/JSON | 默认发送邮件余额;当值大于 `0` 时,用户打开前端设置页或首次发送邮件时会自动初始化该额度。如果不设置,将为 `0` | `1` |
|
| `DEFAULT_SEND_BALANCE` | 文本/JSON | 默认发送邮件余额;当值大于 `0` 时,用户打开前端设置页或首次发送邮件时会自动初始化该额度。如果不设置,将为 `0` | `1` |
|
||||||
| `ENABLE_ADDRESS_PASSWORD` | 文本/JSON | 启用邮箱地址密码功能,启用后创建新地址时会自动生成密码,并支持密码登录和修改 | `true` |
|
| `ENABLE_ADDRESS_PASSWORD` | 文本/JSON | 启用邮箱地址密码功能,启用后创建新地址时会自动生成密码,并支持密码登录和修改 | `true` |
|
||||||
|
| `ENABLE_AGENT_EMAIL_INFO` | 文本/JSON | 是否在前端“地址凭证与连接方式”弹窗中展示 AI Agent 接入信息(Address JWT、parsed-mail API、skill 链接) | `true` |
|
||||||
|
| `SMTP_IMAP_PROXY_CONFIG` | JSON | 在前端“地址凭证与连接方式”弹窗中展示 SMTP/IMAP 代理连接信息;仅用于展示给用户,不会启动代理服务,代理服务仍需单独部署 | 见下方示例 |
|
||||||
| `SEND_MAIL_DOMAINS` | JSON | 限制 `SEND_MAIL` binding 可用于哪些发件域名;留空或不配置时允许所有域名 | `["example.com", "mail.example.com"]` |
|
| `SEND_MAIL_DOMAINS` | JSON | 限制 `SEND_MAIL` binding 可用于哪些发件域名;留空或不配置时允许所有域名 | `["example.com", "mail.example.com"]` |
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
@@ -62,6 +64,17 @@
|
|||||||
>
|
>
|
||||||
> `SEND_MAIL_DOMAINS` 只影响 `SEND_MAIL` binding 的兜底发信路径和 `/admin/send_mail_by_binding`。
|
> `SEND_MAIL_DOMAINS` 只影响 `SEND_MAIL` binding 的兜底发信路径和 `/admin/send_mail_by_binding`。
|
||||||
> 它不影响 Resend、SMTP、`verifiedAddressList` 等其他发信通道。
|
> 它不影响 Resend、SMTP、`verifiedAddressList` 等其他发信通道。
|
||||||
|
>
|
||||||
|
> `SMTP_IMAP_PROXY_CONFIG` 示例:
|
||||||
|
>
|
||||||
|
> ```json
|
||||||
|
> {
|
||||||
|
> "smtp": { "host": "smtp.example.com", "port": 8025, "starttls": true },
|
||||||
|
> "imap": { "host": "imap.example.com", "port": 11143, "starttls": true }
|
||||||
|
> }
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> SMTP 与 IMAP 可以使用不同主机名,便于反向代理或不同端口映射。
|
||||||
|
|
||||||
## 接受邮件相关变量
|
## 接受邮件相关变量
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Context } from 'hono'
|
|||||||
import { Jwt } from 'hono/utils/jwt'
|
import { Jwt } from 'hono/utils/jwt'
|
||||||
|
|
||||||
import i18n from '../i18n'
|
import i18n from '../i18n'
|
||||||
import { getBooleanValue, hashPassword } from '../utils'
|
import { getBooleanValue } from '../utils'
|
||||||
import { newAddress, handleListQuery } from '../common'
|
import { newAddress, handleListQuery } from '../common'
|
||||||
|
|
||||||
const listAddresses = async (c: Context<HonoCustomType>) => {
|
const listAddresses = async (c: Context<HonoCustomType>) => {
|
||||||
@@ -32,7 +32,7 @@ const listAddresses = async (c: Context<HonoCustomType>) => {
|
|||||||
+ ` FROM address a`
|
+ ` FROM address a`
|
||||||
+ ` where ${whereClause}`,
|
+ ` where ${whereClause}`,
|
||||||
`SELECT count(*) as count FROM address where ${whereClause}`,
|
`SELECT count(*) as count FROM address where ${whereClause}`,
|
||||||
[param], limit, offset, orderBy
|
[param], limit, offset, orderBy, ['password']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return await handleListQuery(c,
|
return await handleListQuery(c,
|
||||||
@@ -41,7 +41,7 @@ const listAddresses = async (c: Context<HonoCustomType>) => {
|
|||||||
+ ` (SELECT COUNT(*) FROM sendbox WHERE address = a.name) AS send_count`
|
+ ` (SELECT COUNT(*) FROM sendbox WHERE address = a.name) AS send_count`
|
||||||
+ ` FROM address a`,
|
+ ` FROM address a`,
|
||||||
`SELECT count(*) as count FROM address`,
|
`SELECT count(*) as count FROM address`,
|
||||||
[], limit, offset, orderBy
|
[], limit, offset, orderBy, ['password']
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -137,16 +137,16 @@ const resetPassword = async (c: Context<HonoCustomType>) => {
|
|||||||
const msgs = i18n.getMessagesbyContext(c);
|
const msgs = i18n.getMessagesbyContext(c);
|
||||||
const { id } = c.req.param();
|
const { id } = c.req.param();
|
||||||
const { password } = await c.req.json();
|
const { password } = await c.req.json();
|
||||||
|
// NOTE: Keep the admin API field as password, but the value is a frontend SHA-256 hash.
|
||||||
if (!getBooleanValue(c.env.ENABLE_ADDRESS_PASSWORD)) {
|
if (!getBooleanValue(c.env.ENABLE_ADDRESS_PASSWORD)) {
|
||||||
return c.text(msgs.PasswordChangeDisabledMsg, 403);
|
return c.text(msgs.PasswordChangeDisabledMsg, 403);
|
||||||
}
|
}
|
||||||
if (!password) {
|
if (!password) {
|
||||||
return c.text(msgs.NewPasswordRequiredMsg, 400);
|
return c.text(msgs.NewPasswordRequiredMsg, 400);
|
||||||
}
|
}
|
||||||
const hashedPassword = await hashPassword(password);
|
|
||||||
const { success } = await c.env.DB.prepare(
|
const { success } = await c.env.DB.prepare(
|
||||||
`UPDATE address SET password = ?, updated_at = datetime('now') WHERE id = ?`
|
`UPDATE address SET password = ?, updated_at = datetime('now') WHERE id = ?`
|
||||||
).bind(hashedPassword, id).run();
|
).bind(password, id).run();
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return c.text(msgs.FailedUpdatePasswordMsg, 500);
|
return c.text(msgs.FailedUpdatePasswordMsg, 500);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ api.get('/open_api/settings', async (c) => {
|
|||||||
const auth = c.req.raw.headers.get("x-custom-auth");
|
const auth = c.req.raw.headers.get("x-custom-auth");
|
||||||
needAuth = !auth || !passwords.includes(auth);
|
needAuth = !auth || !passwords.includes(auth);
|
||||||
}
|
}
|
||||||
|
const smtpImapProxyConfig = utils.getJsonObjectValue<SmtpImapProxyConfig>(
|
||||||
|
c.env.SMTP_IMAP_PROXY_CONFIG
|
||||||
|
) || {};
|
||||||
|
const smtpProxyConfig = smtpImapProxyConfig.smtp || {};
|
||||||
|
const imapProxyConfig = smtpImapProxyConfig.imap || {};
|
||||||
|
|
||||||
return c.json({
|
return c.json({
|
||||||
"title": c.env.TITLE,
|
"title": c.env.TITLE,
|
||||||
@@ -45,6 +50,19 @@ api.get('/open_api/settings', async (c) => {
|
|||||||
"showGithub": !utils.getBooleanValue(c.env.DISABLE_SHOW_GITHUB),
|
"showGithub": !utils.getBooleanValue(c.env.DISABLE_SHOW_GITHUB),
|
||||||
"disableAdminPasswordCheck": utils.getBooleanValue(c.env.DISABLE_ADMIN_PASSWORD_CHECK),
|
"disableAdminPasswordCheck": utils.getBooleanValue(c.env.DISABLE_ADMIN_PASSWORD_CHECK),
|
||||||
"enableAddressPassword": utils.getBooleanValue(c.env.ENABLE_ADDRESS_PASSWORD),
|
"enableAddressPassword": utils.getBooleanValue(c.env.ENABLE_ADDRESS_PASSWORD),
|
||||||
|
"enableAgentEmailInfo": utils.getBooleanValue(c.env.ENABLE_AGENT_EMAIL_INFO),
|
||||||
|
"smtpImapProxyConfig": {
|
||||||
|
"smtp": {
|
||||||
|
"host": utils.getStringValue(smtpProxyConfig.host),
|
||||||
|
"port": utils.getIntValue(smtpProxyConfig.port, 8025),
|
||||||
|
"starttls": utils.getBooleanValue(smtpProxyConfig.starttls),
|
||||||
|
},
|
||||||
|
"imap": {
|
||||||
|
"host": utils.getStringValue(imapProxyConfig.host),
|
||||||
|
"port": utils.getIntValue(imapProxyConfig.port, 11143),
|
||||||
|
"starttls": utils.getBooleanValue(imapProxyConfig.starttls),
|
||||||
|
},
|
||||||
|
},
|
||||||
"statusUrl": utils.getStringValue(c.env.STATUS_URL),
|
"statusUrl": utils.getStringValue(c.env.STATUS_URL),
|
||||||
"enableGlobalTurnstileCheck": utils.isGlobalTurnstileEnabled(c)
|
"enableGlobalTurnstileCheck": utils.isGlobalTurnstileEnabled(c)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -611,7 +611,8 @@ export const handleListQuery = async (
|
|||||||
limit: string | number | undefined | null,
|
limit: string | number | undefined | null,
|
||||||
offset: string | number | undefined | null,
|
offset: string | number | undefined | null,
|
||||||
/** Must be pre-validated (e.g. whitelist), NOT raw user input. Interpolated directly into SQL. */
|
/** Must be pre-validated (e.g. whitelist), NOT raw user input. Interpolated directly into SQL. */
|
||||||
orderBy?: string
|
orderBy?: string,
|
||||||
|
hiddenFields: string[] = []
|
||||||
): Promise<Response> => {
|
): Promise<Response> => {
|
||||||
const msgs = i18n.getMessagesbyContext(c);
|
const msgs = i18n.getMessagesbyContext(c);
|
||||||
if (typeof limit === "string") {
|
if (typeof limit === "string") {
|
||||||
@@ -634,7 +635,23 @@ export const handleListQuery = async (
|
|||||||
const count = offset == 0 ? await c.env.DB.prepare(
|
const count = offset == 0 ? await c.env.DB.prepare(
|
||||||
countQuery
|
countQuery
|
||||||
).bind(...params).first("count") : 0;
|
).bind(...params).first("count") : 0;
|
||||||
return c.json({ results, count });
|
if (hiddenFields.length === 0) {
|
||||||
|
return c.json({ results, count });
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredResults = results.map((row) => hideObjectFields(row, hiddenFields));
|
||||||
|
return c.json({ results: filteredResults, count });
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hideObjectFields = <T extends Record<string, unknown>>(
|
||||||
|
row: T,
|
||||||
|
fields: string[]
|
||||||
|
): T => {
|
||||||
|
const filteredRow = { ...row };
|
||||||
|
for (const field of fields) {
|
||||||
|
delete filteredRow[field];
|
||||||
|
}
|
||||||
|
return filteredRow;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default {
|
|||||||
return c.text(msgs.InvalidAddressTokenMsg, 400);
|
return c.text(msgs.InvalidAddressTokenMsg, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新密码
|
// NOTE: new_password is the frontend SHA-256 hash, stored directly in address.password.
|
||||||
const { success } = await c.env.DB.prepare(
|
const { success } = await c.env.DB.prepare(
|
||||||
`UPDATE address SET password = ?, updated_at = datetime('now') WHERE id = ?`
|
`UPDATE address SET password = ?, updated_at = datetime('now') WHERE id = ?`
|
||||||
).bind(new_password, address_id).run();
|
).bind(new_password, address_id).run();
|
||||||
@@ -67,7 +67,7 @@ export default {
|
|||||||
return c.text(msgs.AddressNotFoundMsg, 404);
|
return c.text(msgs.AddressNotFoundMsg, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证密码
|
// NOTE: password is the frontend SHA-256 hash, compared directly with address.password.
|
||||||
if (address.password !== password) {
|
if (address.password !== password) {
|
||||||
return c.text(msgs.InvalidEmailOrPasswordMsg, 401);
|
return c.text(msgs.InvalidEmailOrPasswordMsg, 401);
|
||||||
}
|
}
|
||||||
|
|||||||
15
worker/src/types.d.ts
vendored
15
worker/src/types.d.ts
vendored
@@ -4,6 +4,19 @@ type UserRole = {
|
|||||||
prefix: string | undefined | null
|
prefix: string | undefined | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SmtpImapProxyConfig = {
|
||||||
|
smtp?: {
|
||||||
|
host?: string
|
||||||
|
port?: number | string
|
||||||
|
starttls?: boolean | string
|
||||||
|
}
|
||||||
|
imap?: {
|
||||||
|
host?: string
|
||||||
|
port?: number | string
|
||||||
|
starttls?: boolean | string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Bindings = {
|
type Bindings = {
|
||||||
// bindings
|
// bindings
|
||||||
DB: D1Database
|
DB: D1Database
|
||||||
@@ -45,6 +58,8 @@ type Bindings = {
|
|||||||
DISABLE_ANONYMOUS_USER_CREATE_EMAIL: string | boolean | undefined
|
DISABLE_ANONYMOUS_USER_CREATE_EMAIL: string | boolean | undefined
|
||||||
ENABLE_USER_DELETE_EMAIL: string | boolean | undefined
|
ENABLE_USER_DELETE_EMAIL: string | boolean | undefined
|
||||||
ENABLE_ADDRESS_PASSWORD: string | boolean | undefined
|
ENABLE_ADDRESS_PASSWORD: string | boolean | undefined
|
||||||
|
ENABLE_AGENT_EMAIL_INFO: string | boolean | undefined
|
||||||
|
SMTP_IMAP_PROXY_CONFIG: string | SmtpImapProxyConfig | undefined
|
||||||
ENABLE_INDEX_ABOUT: string | boolean | undefined
|
ENABLE_INDEX_ABOUT: string | boolean | undefined
|
||||||
DEFAULT_SEND_BALANCE: number | string | undefined
|
DEFAULT_SEND_BALANCE: number | string | undefined
|
||||||
NO_LIMIT_SEND_ROLE: string | undefined | null
|
NO_LIMIT_SEND_ROLE: string | undefined | null
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Jwt } from 'hono/utils/jwt'
|
|||||||
import { isAddressCountLimitReached } from "../utils"
|
import { isAddressCountLimitReached } from "../utils"
|
||||||
import { unbindTelegramByAddress } from '../telegram_api/common';
|
import { unbindTelegramByAddress } from '../telegram_api/common';
|
||||||
import i18n from '../i18n';
|
import i18n from '../i18n';
|
||||||
import { updateAddressUpdatedAt, commonGetUserRole } from '../common';
|
import { updateAddressUpdatedAt, commonGetUserRole, hideObjectFields } from '../common';
|
||||||
|
|
||||||
const UserBindAddressModule = {
|
const UserBindAddressModule = {
|
||||||
bind: async (c: Context<HonoCustomType>) => {
|
bind: async (c: Context<HonoCustomType>) => {
|
||||||
@@ -140,7 +140,7 @@ const UserBindAddressModule = {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}>();
|
}>();
|
||||||
return results || [];
|
return (results || []).map((row) => hideObjectFields(row, ['password']));
|
||||||
},
|
},
|
||||||
getBindedAddressJwt: async (c: Context<HonoCustomType>) => {
|
getBindedAddressJwt: async (c: Context<HonoCustomType>) => {
|
||||||
const msgs = i18n.getMessagesbyContext(c);
|
const msgs = i18n.getMessagesbyContext(c);
|
||||||
|
|||||||
@@ -81,6 +81,15 @@ ENABLE_AUTO_REPLY = false
|
|||||||
# ENABLE_WEBHOOK = true
|
# ENABLE_WEBHOOK = true
|
||||||
# Enable address password feature, if set true, will generate password for new address and support password login and change
|
# Enable address password feature, if set true, will generate password for new address and support password login and change
|
||||||
# ENABLE_ADDRESS_PASSWORD = false
|
# ENABLE_ADDRESS_PASSWORD = false
|
||||||
|
# Show AI Agent mailbox connection info in the address credential modal
|
||||||
|
# ENABLE_AGENT_EMAIL_INFO = true
|
||||||
|
# Show SMTP/IMAP client connection info in the address credential modal
|
||||||
|
# SMTP_IMAP_PROXY_CONFIG = """
|
||||||
|
# {
|
||||||
|
# "smtp": { "host": "smtp.example.com", "port": 8025, "starttls": true },
|
||||||
|
# "imap": { "host": "imap.example.com", "port": 11143, "starttls": true }
|
||||||
|
# }
|
||||||
|
# """
|
||||||
# Footer text
|
# Footer text
|
||||||
# COPYRIGHT = "Dream Hunter"
|
# COPYRIGHT = "Dream Hunter"
|
||||||
# DISABLE_SHOW_GITHUB = true
|
# DISABLE_SHOW_GITHUB = true
|
||||||
|
|||||||
Reference in New Issue
Block a user