refactor(cors): 简化CORS配置并移除未使用的参数
feat(email): 提取邮箱验证逻辑到共享模块并增强验证 docs: 更新文档移除不再使用的ALLOW_ORIGIN配置 style(admin): 为设置页面添加加载状态和样式优化 fix(comments): 在邮件发送前增加邮箱格式验证
This commit is contained in:
@@ -62,14 +62,13 @@ src/
|
|||||||
|
|
||||||
## 环境变量
|
## 环境变量
|
||||||
|
|
||||||
| 变量名 | 说明 | 必填 |
|
| 变量名 | 说明 | 必填 |
|
||||||
|--------|------|------|
|
| ------------------- | --------------- | ---- |
|
||||||
| `ADMIN_NAME` | 管理员用户名 | 是 |
|
| `ADMIN_NAME` | 管理员用户名 | 是 |
|
||||||
| `ADMIN_PASSWORD` | 管理员密码 | 是 |
|
| `ADMIN_PASSWORD` | 管理员密码 | 是 |
|
||||||
| `ALLOW_ORIGIN` | CORS 白名单(逗号分隔) | 是 |
|
| `RESEND_API_KEY` | Resend API 密钥 | 否 |
|
||||||
| `RESEND_API_KEY` | Resend API 密钥 | 否 |
|
| `RESEND_FROM_EMAIL` | 发件邮箱 | 否 |
|
||||||
| `RESEND_FROM_EMAIL` | 发件邮箱 | 否 |
|
| `EMAIL_ADDRESS` | 站长接收邮箱 | 否 |
|
||||||
| `EMAIL_ADDRESS` | 站长接收邮箱 | 否 |
|
|
||||||
|
|
||||||
## 本地开发
|
## 本地开发
|
||||||
|
|
||||||
|
|||||||
17
README.md
17
README.md
@@ -5,15 +5,13 @@
|
|||||||
|
|
||||||
Cloudflare Worker 版本基于 Cloudflare Workers + D1 + KV 实现,无需服务器即可部署运行的评论组件。
|
Cloudflare Worker 版本基于 Cloudflare Workers + D1 + KV 实现,无需服务器即可部署运行的评论组件。
|
||||||
|
|
||||||
> 基于 https://github.com/Motues/Momo-Backend 进行二次开发的 Cloudflare Worker 版本,做了大量扩展更新。
|
|
||||||
|
|
||||||
[文档地址](https://cwd-comments.zishu.me)
|
[文档地址](https://cwd-comments.zishu.me)
|
||||||
|
|
||||||
## 特性
|
## 特性
|
||||||
|
|
||||||
- ⚡️ **极速响应**:基于 Cloudflare 全球边缘网络
|
- ⚡️ **极速响应**:基于 Cloudflare 全球边缘网络
|
||||||
- 🔒 **安全可靠**:内置管理员认证、CORS 保护
|
- 🔒 **安全可靠**:内置管理员认证、CORS 保护
|
||||||
- 📧 **邮件通知**:支持 Resend 邮件服务
|
- 📧 **邮件通知**:基于 Cloudflare Workers 发送邮件
|
||||||
- 🎨 **易于集成**:提供完整的 REST API
|
- 🎨 **易于集成**:提供完整的 REST API
|
||||||
|
|
||||||
## 前置要求
|
## 前置要求
|
||||||
@@ -27,13 +25,18 @@ Cloudflare Worker 版本基于 Cloudflare Workers + D1 + KV 实现,无需服
|
|||||||
```bash
|
```bash
|
||||||
# 克隆项目
|
# 克隆项目
|
||||||
git clone https://github.com/anghunk/cwd-comments
|
git clone https://github.com/anghunk/cwd-comments
|
||||||
cd cwd-comments
|
|
||||||
|
|
||||||
# 安装依赖
|
# API 项目
|
||||||
|
cd cwd-comments-api
|
||||||
|
# 部署请查看文档
|
||||||
|
|
||||||
|
# 前端项目
|
||||||
|
cd cwd-comments-web
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 配置
|
## 配置
|
||||||
|
|
||||||
- [后端配置](./backend-config.md)
|
- [后端配置](https://cwd-comments-docs.zishu.me/guide/backend-config.html)
|
||||||
- [前端配置](./frontend-config.md)
|
- [前端配置](https://cwd-comments-docs.zishu.me/guide/frontend-config.html)
|
||||||
|
|||||||
@@ -399,6 +399,38 @@ onMounted(() => {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 3px solid #f0f0f0;
|
||||||
|
border-top-color: #0969da;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #57606a;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,151 +1,199 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<h2 class="page-title">系统设置</h2>
|
<h2 class="page-title">系统设置</h2>
|
||||||
<div class="card">
|
<div v-if="loading" class="page-hint">加载中...</div>
|
||||||
<h3 class="card-title">通知邮箱设置</h3>
|
<div v-else>
|
||||||
<div class="form-item">
|
<div class="card">
|
||||||
<label class="form-label">管理员通知邮箱</label>
|
<h3 class="card-title">通知邮箱设置</h3>
|
||||||
<input v-model="email" class="form-input" type="email" />
|
<div class="form-item">
|
||||||
</div>
|
<label class="form-label">管理员通知邮箱</label>
|
||||||
<div v-if="message" :class="['form-message', messageType === 'error' ? 'form-message-error' : 'form-message-success']">
|
<input v-model="email" class="form-input" type="email" />
|
||||||
{{ message }}
|
</div>
|
||||||
</div>
|
<div
|
||||||
<div class="card-actions">
|
v-if="message"
|
||||||
<button class="card-button" :disabled="saving" @click="save">
|
:class="[
|
||||||
<span v-if="saving">保存中...</span>
|
'form-message',
|
||||||
<span v-else>保存</span>
|
messageType === 'error' ? 'form-message-error' : 'form-message-success',
|
||||||
</button>
|
]"
|
||||||
</div>
|
>
|
||||||
</div>
|
{{ message }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<button class="card-button" :disabled="saving" @click="save">
|
||||||
|
<span v-if="saving">保存中...</span>
|
||||||
|
<span v-else>保存</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from "vue";
|
||||||
import { fetchAdminEmail, saveAdminEmail } from '../api/admin';
|
import { fetchAdminEmail, saveAdminEmail } from "../api/admin";
|
||||||
|
|
||||||
const email = ref('');
|
const email = ref("");
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
const message = ref('');
|
const loading = ref(false);
|
||||||
const messageType = ref<'success' | 'error'>('success');
|
const message = ref("");
|
||||||
|
const messageType = ref<"success" | "error">("success");
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
try {
|
loading.value = true;
|
||||||
const res = await fetchAdminEmail();
|
try {
|
||||||
email.value = res.email || '';
|
const res = await fetchAdminEmail();
|
||||||
} catch (e: any) {
|
email.value = res.email || "";
|
||||||
message.value = e.message || '加载失败';
|
} catch (e: any) {
|
||||||
messageType.value = 'error';
|
message.value = e.message || "加载失败";
|
||||||
}
|
messageType.value = "error";
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
if (!email.value) {
|
if (!email.value) {
|
||||||
message.value = '请输入邮箱';
|
message.value = "请输入邮箱";
|
||||||
messageType.value = 'error';
|
messageType.value = "error";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
message.value = '';
|
message.value = "";
|
||||||
try {
|
try {
|
||||||
const res = await saveAdminEmail(email.value);
|
const res = await saveAdminEmail(email.value);
|
||||||
message.value = res.message || '保存成功';
|
message.value = res.message || "保存成功";
|
||||||
messageType.value = 'success';
|
messageType.value = "success";
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
message.value = e.message || '保存失败';
|
message.value = e.message || "保存失败";
|
||||||
messageType.value = 'error';
|
messageType.value = "error";
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false;
|
saving.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
load();
|
load();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.page {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
max-width: 520px;
|
max-width: 520px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-title {
|
.page-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: #24292f;
|
color: #24292f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: 1px solid #d0d7de;
|
border: 1px solid #d0d7de;
|
||||||
padding: 16px 18px;
|
padding: 16px 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-title {
|
.card-title {
|
||||||
margin: 0 0 12px;
|
margin: 0 0 12px;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-item {
|
.form-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-label {
|
.form-label {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #555555;
|
color: #555555;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-input {
|
.form-input {
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #d0d7de;
|
border: 1px solid #d0d7de;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-input:focus {
|
.form-input:focus {
|
||||||
border-color: #0969da;
|
border-color: #0969da;
|
||||||
box-shadow: 0 0 0 1px rgba(9, 105, 218, 0.2);
|
box-shadow: 0 0 0 1px rgba(9, 105, 218, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-actions {
|
.card-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-button {
|
.card-button {
|
||||||
padding: 8px 14px;
|
padding: 8px 14px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: none;
|
border: none;
|
||||||
background-color: #0969da;
|
background-color: #0969da;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-button:disabled {
|
.card-button:disabled {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-message {
|
.form-message {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-message-success {
|
.form-message-success {
|
||||||
color: #1a7f37;
|
color: #1a7f37;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-message-error {
|
.form-message-error {
|
||||||
color: #d1242f;
|
color: #d1242f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 3px solid #f0f0f0;
|
||||||
|
border-top-color: #0969da;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #57606a;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.page-hint {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #57606a;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { Context } from 'hono';
|
import { Context } from 'hono';
|
||||||
import { Bindings } from '../../bindings';
|
import { Bindings } from '../../bindings';
|
||||||
|
import { isValidEmail } from '../../utils/email';
|
||||||
function isValidEmail(email: string) {
|
|
||||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const setAdminEmail = async (c: Context<{ Bindings: Bindings }>) => {
|
export const setAdminEmail = async (c: Context<{ Bindings: Bindings }>) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Context } from 'hono';
|
import { Context } from 'hono';
|
||||||
import { UAParser } from 'ua-parser-js';
|
import { UAParser } from 'ua-parser-js';
|
||||||
import { Bindings } from '../../bindings';
|
import { Bindings } from '../../bindings';
|
||||||
import { sendCommentNotification, sendCommentReplyNotification } from '../../utils/email';
|
import { sendCommentNotification, sendCommentReplyNotification, isValidEmail } from '../../utils/email';
|
||||||
|
|
||||||
// 检查内容,将<script>标签之间的内容删除
|
// 检查内容,将<script>标签之间的内容删除
|
||||||
export function checkContent(content: string): string {
|
export function checkContent(content: string): string {
|
||||||
@@ -26,6 +26,9 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
if (!email || typeof email !== 'string') {
|
if (!email || typeof email !== 'string') {
|
||||||
return c.json({ message: '邮箱不能为空' }, 400);
|
return c.json({ message: '邮箱不能为空' }, 400);
|
||||||
}
|
}
|
||||||
|
if (!isValidEmail(email)) {
|
||||||
|
return c.json({ message: '邮箱格式不正确' }, 400);
|
||||||
|
}
|
||||||
const userAgent = c.req.header('user-agent') || "";
|
const userAgent = c.req.header('user-agent') || "";
|
||||||
|
|
||||||
// 1. 获取 IP (Worker 获取 IP 的标准方式)
|
// 1. 获取 IP (Worker 获取 IP 的标准方式)
|
||||||
@@ -122,7 +125,8 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
"SELECT created_at FROM EmailLog WHERE recipient = ? AND type = 'user-reply' ORDER BY created_at DESC LIMIT 1"
|
"SELECT created_at FROM EmailLog WHERE recipient = ? AND type = 'user-reply' ORDER BY created_at DESC LIMIT 1"
|
||||||
).bind(parentComment.email).first<{ created_at: string }>();
|
).bind(parentComment.email).first<{ created_at: string }>();
|
||||||
const canSendUserMail = !recentUserMail || (Date.now() - new Date(recentUserMail.created_at).getTime() > 60 * 1000);
|
const canSendUserMail = !recentUserMail || (Date.now() - new Date(recentUserMail.created_at).getTime() > 60 * 1000);
|
||||||
if (canSendUserMail) {
|
|
||||||
|
if (canSendUserMail && isValidEmail(parentComment.email)) {
|
||||||
console.log('PostComment:mailDispatch:userReply:send', {
|
console.log('PostComment:mailDispatch:userReply:send', {
|
||||||
toEmail: parentComment.email,
|
toEmail: parentComment.email,
|
||||||
toName: parentComment.author
|
toName: parentComment.author
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { cors } from 'hono/cors'
|
import { cors } from 'hono/cors'
|
||||||
|
|
||||||
export const customCors = (_allowOriginStr: string | undefined) => {
|
export const customCors = () => {
|
||||||
return cors({
|
return cors({
|
||||||
origin: '*',
|
origin: '*',
|
||||||
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { Bindings } from '../bindings';
|
import { Bindings } from '../bindings';
|
||||||
|
|
||||||
|
export function isValidEmail(email: string) {
|
||||||
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 回复通知邮件
|
* 回复通知邮件
|
||||||
*/
|
*/
|
||||||
@@ -54,6 +58,11 @@ export async function sendCommentReplyNotification(
|
|||||||
throw new Error('未配置邮件发送绑定或发件人地址');
|
throw new Error('未配置邮件发送绑定或发件人地址');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isValidEmail(toEmail)) {
|
||||||
|
console.warn('EmailReplyNotification:invalidRecipient', { toEmail });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await env.SEND_EMAIL.send({
|
await env.SEND_EMAIL.send({
|
||||||
to: [{ email: toEmail }],
|
to: [{ email: toEmail }],
|
||||||
from: { email: env.CF_FROM_EMAIL },
|
from: { email: env.CF_FROM_EMAIL },
|
||||||
@@ -106,6 +115,11 @@ export async function sendCommentNotification(
|
|||||||
throw new Error('未配置邮件发送绑定或发件人地址');
|
throw new Error('未配置邮件发送绑定或发件人地址');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isValidEmail(toEmail)) {
|
||||||
|
console.warn('EmailAdminNotification:invalidRecipient', { toEmail });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await env.SEND_EMAIL.send({
|
await env.SEND_EMAIL.send({
|
||||||
to: [{ email: toEmail }],
|
to: [{ email: toEmail }],
|
||||||
from: { email: env.CF_FROM_EMAIL },
|
from: { email: env.CF_FROM_EMAIL },
|
||||||
@@ -125,18 +139,21 @@ async function getAdminNotifyEmail(env: Bindings): Promise<string> {
|
|||||||
const row = await env.CWD_DB.prepare('SELECT value FROM Settings WHERE key = ?')
|
const row = await env.CWD_DB.prepare('SELECT value FROM Settings WHERE key = ?')
|
||||||
.bind('admin_notify_email')
|
.bind('admin_notify_email')
|
||||||
.first<{ value: string }>();
|
.first<{ value: string }>();
|
||||||
if (row?.value) {
|
if (row?.value && isValidEmail(row.value)) {
|
||||||
console.log('EmailAdminNotification:useDbEmail', {
|
console.log('EmailAdminNotification:useDbEmail', {
|
||||||
email: row.value
|
email: row.value
|
||||||
});
|
});
|
||||||
return row.value;
|
return row.value;
|
||||||
}
|
}
|
||||||
if (env.EMAIL_ADDRESS) {
|
if (env.EMAIL_ADDRESS && isValidEmail(env.EMAIL_ADDRESS)) {
|
||||||
console.log('EmailAdminNotification:useEnvEmail', {
|
console.log('EmailAdminNotification:useEnvEmail', {
|
||||||
email: env.EMAIL_ADDRESS
|
email: env.EMAIL_ADDRESS
|
||||||
});
|
});
|
||||||
return env.EMAIL_ADDRESS;
|
return env.EMAIL_ADDRESS;
|
||||||
}
|
}
|
||||||
console.error('EmailAdminNotification:noAdminEmail');
|
console.error('EmailAdminNotification:noAdminEmail', {
|
||||||
throw new Error('未配置管理员通知邮箱');
|
dbValue: row?.value,
|
||||||
|
envValue: env.EMAIL_ADDRESS
|
||||||
|
});
|
||||||
|
throw new Error('未配置管理员通知邮箱或格式不正确');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ npm install
|
|||||||
|
|
||||||
| 变量名 | 描述 |
|
| 变量名 | 描述 |
|
||||||
| -------------------- | ------------------------------------------------------------------------------------ |
|
| -------------------- | ------------------------------------------------------------------------------------ |
|
||||||
| `ALLOW_ORIGIN` | 允许跨域请求的域名,用逗号分隔 |
|
|
||||||
| `CF_FROM_EMAIL` | 作为发件人显示的邮箱地址(需在 Cloudflare Email 路由中预先配置) |
|
| `CF_FROM_EMAIL` | 作为发件人显示的邮箱地址(需在 Cloudflare Email 路由中预先配置) |
|
||||||
| `EMAIL_ADDRESS` | 管理员接收通知邮件的默认邮箱(可在后台设置中覆盖) |
|
| `EMAIL_ADDRESS` | 管理员接收通知邮件的默认邮箱(可在后台设置中覆盖) |
|
||||||
| `ADMIN_NAME` | 管理员登录名称,默认为 admin |
|
| `ADMIN_NAME` | 管理员登录名称,默认为 admin |
|
||||||
|
|||||||
Reference in New Issue
Block a user